Эффективный способ продолжать крутить рукоятку в вычислении с отслеживанием состояния

У меня есть процесс с отслеживанием состояния, который моделируется как i -> RWS r w s a. Я хочу передать ему вход cmds :: [i]; в настоящее время я делаю это оптом:

    let play = runGame theGame . go
          where
            go [] = finished
            go ((v, n):cmds) = do
                end1 <- stepWorld
                end2 <- ite (SBV.isJust end1) (return end1) $ stepPlayer (v, n)
                ite (SBV.isJust end2) (return end2) $ go cmds

Я могу попробовать найти ввод заданного размера следующим образом:

    result <- satWith z3{ verbose = True } $ do
        cmds <- mapM sCmd [1..inputLength]
        return $ SBV.fromMaybe sFalse $ fst $ play cmds

Однако это дает мне ужасную производительность в самом SBV, то есть до вызова Z3 (я вижу, что это так, потому что вывод verbose показывает мне, что все время тратится до вызова (check-sat)). Это даже если inputLength установлено на что-то маленькое, например 4.

Однако если для inputLength установлено значение 1 или 2, весь процесс выполняется очень быстро. Поэтому я надеюсь, что есть способ запустить SBV, чтобы получить модель поведения одного шага i -> s -> (s, a), а затем указать SMT-решателю продолжать повторять эту модель для n разных is.

Итак, вот мой вопрос: в вычислении с отслеживанием состояния, подобном этому, когда я хочу подавать переменные SMT в качестве входных данных для вычисления с отслеживанием состояния, есть ли способ позволить решателю SMT повернуть свою рукоятку на избежать плохой работы SBV?

Я предполагаю, что упрощенный типовой вопрос будет таким: если у меня есть функция f :: St -> St и предикат p :: St -> SBool, и я хочу решить для n :: SInt такое, что p (iterateN n f x0), каков рекомендуемый способ сделать это с SBV, предположим Mergeable St?

EDIT: я загрузил весь код на Github но имейте в виду, что это не минималистичный пример; на самом деле это даже не очень хороший код на Haskell.


person Cactus    schedule 03.07.2020    source источник


Ответы (1)


Полное символическое исполнение

Трудно судить, не видя полного кода, который мы можем выполнить. (Переполнение стека работает лучше всего, когда вы публикуете сегменты кода, которые люди действительно могут запустить.) Но в вашей программе появляются некоторые контрольные признаки экспоненциальной сложности. Рассмотрим следующий сегмент, который вы разместили:

        go [] = finished
        go ((v, n):cmds) = do
                end1 <- stepWorld
                end2 <- ite (SBV.isJust end1) (return end1) $ stepPlayer (v, n)
                ite (SBV.isJust end2) (return end2) $ go cmds

Это выглядит как линейная прогулка, если вы программируете с конкретными значениями. Но имейте в виду, что конструкция ite должна оценивать обе ветви на каждом шаге. И у вас есть вложенное если: Вот почему вы получаете экспоненциальное замедление с коэффициентом 4 на каждой итерации. Как вы заметили, это довольно быстро выходит из-под контроля. (Одним из способов думать об этом является то, что SBV должен выполнять все возможные результаты этих вложенных условий на каждом этапе. Вы можете нарисовать дерево вызовов, чтобы увидеть, как оно растет в геометрической прогрессии.)

Не зная деталей ваших stepWorld или stepPlayer, трудно предложить какие-либо альтернативные схемы. Но суть в том, что вы хотите максимально исключить эти вызовы ite и поместить их как можно ниже в рекурсивной цепочке. Возможно, может помочь стиль передачи продолжения, но все зависит от семантики этих операций и от того, сможете ли вы успешно откладывать решения.

Режим запроса

Тем не менее, я считаю, что вам лучше попробовать использовать режим query SBV. В этом режиме вы не выполняете символьную симуляцию всего перед вызовом решателя. Вместо этого вы постепенно добавляете ограничения к решателю, запрашиваете выполнимость и на основе полученных значений выбираете разные пути. Я считаю, что этот подход будет работать лучше всего в вашей ситуации, когда вы динамически исследуете пространство состояний, но также принимаете решения по пути. Пример этого есть в документации: HexPuzzle. В частности, search< Функция /a> показывает, как вы можете перемещаться по одному ходу за раз, используя решатель в инкрементном режиме (используя push/pop).

Я не совсем уверен, соответствует ли эта модель исполнения логике вашей игры. Надеюсь, это может по крайней мере дать вам представление. Но в прошлом мне везло с инкрементным подходом, когда вы могли постепенно исследовать такие большие области поиска, избегая необходимости делать все выборы перед отправкой данных на z3.

person alias    schedule 03.07.2020
comment
Я загрузил полный код на Github; см. ссылку, отредактированную в вопросе. - person Cactus; 04.07.2020
comment
Итак, вся моя цель состояла в том, чтобы посмотреть, смогу ли я использовать SBV с интерпретатором, который преднамеренно написан в прямолинейном стиле — в основном, написать его с использованием конкретных значений, затем изменить координаты состояния, чтобы они были символическими, и затем идти. Если я понимаю ваш первый пункт здесь, этот подход совершенно неосуществим. - person Cactus; 04.07.2020
comment
Ожидать, что простое кодирование интерпретатора будет хорошо символически работать, нереально, как вы выяснили. Это классическая проблема взрыва в пространстве состояний, которая является основным препятствием в системах символьного моделирования. - person alias; 06.07.2020
comment
Я не слишком хорошо знаком с Rossette, но я был бы удивлен, если бы они могли запускать символические программы так же быстро, как и конкретные. Даже если вы можете символически выполнить быстро, решатель SAT/SMT займет некоторое время: вы, по сути, проверяете, является ли это удовлетворительным экземпляром (алгоритм с полиномиальным временем), чтобы найти удовлетворяющий экземпляр (который является NP-полным). Возможно. Rossette использует более умную эвристику во внешнем интерфейсе, но фундаментальная сложность остается. Я хотел бы увидеть ссылки, которые показывают обратное. - person alias; 08.07.2020
comment
но разве это не фактическое решение SAT, которое должно быть NP-полным? Под этим я подразумеваю, что меня, конечно, не удивило бы долгое ожидание возврата решателя SMT, но здесь то, что я вижу, так это то, что начинается запустить решатель - это то, что медленно. - person Cactus; 08.07.2020
comment
Это правда. Но символическая симуляция имеет аналогичные затраты, вытекающие из тех же причин. Всякий раз, когда есть if-then-else, вы должны выполнить обе ветви и объединить результаты. Когда вы моделируете доступ к массиву, вы не можете выполнить простой поиск: вам нужно просканировать весь массив на предмет совпадения символического адреса. Обычно они требуют значительной работы еще до того, как вы начнете решать. Грубое сравнение состоит в том, что вам нужно развернуть все возможные пути выполнения и поместить мультиплексоры в конце каждого из них, чтобы объединить результаты. Делать это эффективно — это не только техника, но и искусство. - person alias; 08.07.2020