Тригонометрические функции разложения Тейлора в Z3-Python

Мне нужно спроектировать функцию косинуса (и синуса) в Z3, но в целом это сложно и неразрешимо (например, см. Как я могу использовать встроенные тригонометрические функции в Z3 Python?).

Однако меня вполне устраивают аппроксимированные прецизионные методы. Таким образом, я разработал следующую функцию (для cos(a), где a в радианах), которая использует расширение Тейлора:

from z3 import *

def calculate_cosine(angle):
    solver = Solver()
    result = Real('result')

    #Note the Taylor expansion for cos (e.g., next term is "+ angle**8/40320")
    #Also, note 2!=2, 4!=24 etc.
    solver.add(result == 1 - angle**2/2 + angle**4/24 - angle**6/720)

    if solver.check() == sat:
        model = solver.model()
        cosine = model[result]
        return cosine

    return None

angle = math.pi / 8  # Angle in radians
cosine = calculate_cosine(angle)
print(cosine)

Однако это всего лишь код Python, тогда как я хотел бы выполнять запросы типа Exists y. 0<=approx_cos(y)<=1, для которых, я думаю, мне придется создать Z3-функцию approx_cos (с типичным синтаксисом If...), которая в данном случае будет вызывать некоторый решатель NRA.

Но я не знаю, как создавать такие функции, можете ли вы мне помочь?

PS: Есть ли в Z3 встроенная операция для факториала?

Вы не можете написать свою собственную функцию факториала?

Scott Hunter 22.03.2024 13:31

Думаю, да, но я не уверен. Действительно, в (stackoverflow.com/questions/41810719/… ) в ответе говорится: «Это выходит далеко за рамки того, для чего предназначен Z3. В нем нет встроенного понимания факториалов [...]. Он выполняет очень ограниченные рассуждения об экспонентах», а в более старой статье ( microsoft.com/en-us/research/wp-content/uploads/2016/12/…) говорится: «Факториал не может быть проверен Z3, поскольку умножения на неконстанты в текущей версии Z3 по существу игнорируются».

Theo Deep 22.03.2024 13:39
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
74
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете запрограммировать разложение ряда Тейлора с помощью простого итерационного цикла, складывая члены по ходу дела. Ниже приведена реализация, которая принимает количество терминов, которые вы хотите включить в качестве параметра:

from z3 import *

# Approximate cosine using taylor expansion
# n is the number of terms we want to include
# Essentially, we implement the formula
#
#     sum_{k=0}^{n-1} (-1)^k * x^2k / (2k)!
#
# in an iterative way, to reduce calculations.
def taylor_cosine(x, n):
    if n < 2:
        raise Exception("taylor_cosine: n must be at least 2, got: %d" % n)

    s    = 1
    sign = 1
    term = 1

    # In each iteration:
    #   sign flips
    #   term gets multiplied by x^2 / (2k * (2k-1))
    for k in range(1, n):
        sign *= -1
        term *= x*x
        term /= 2*k*(2*k - 1)
        s    += sign * term

    return s

Обратите внимание, что эта функция не имеет ничего общего с z3, вы можете использовать ее как есть:

>> taylor_cosine(math.pi, 5)
-0.9760222126236076

Чем больше терминов вы включите, тем точнее будет результат.

Но в этой функции хорошо то, что мы постарались написать ее так, чтобы ее можно было использовать и символически. То есть параметр x может быть символьной переменной z3. (Обратите внимание, что n должно быть константой: не существует простого/разрешимого способа поддержать символический n. Но это уже совсем другая дискуссия.)

В качестве примера приведем небольшую тестовую программу, которая пытается аппроксимировать значение pi, запрашивая z3, последовательные приближения, где результат вызова косинуса равен -1:

def test(numberOfTerms):
  s  = Solver()
  x  = Real('x')
  s.add(And(0 <= x, x <= 2*math.pi))
  cx = taylor_cosine(x, numberOfTerms)
  s.add(And(cx == -1))

  print("Testing with %d terms:" % numberOfTerms)

  r = s.check()
  if r == sat:
      mx = s.model()[x]
      if (is_algebraic_value(mx)):
          mx = mx.approx()
      vx = float(mx.as_fraction())
      print("  x                    = %s"  % vx)
      print("  taylor_cosine(x, %2d) = %s" % (numberOfTerms, taylor_cosine(vx, numberOfTerms)))
      print("  cosine(x)            = %s"  % math.cos(vx))
  else:
      print("  Solver said: %s" % r)

Мы просто создаем Real и просим решающую программу убедиться, что ее косинус равен -1. Мы стараемся обрабатывать как вещественные, так и алгебраически-действительные числа, поскольку z3 может выдавать оба типа результатов для запросов, включающих вещественные значения. (Первый вариант, когда решение рационально, второй — когда они алгебраические, т. е. как корни многочлена.)

Давайте посмотрим на это в действии:

for i in range(2, 10):
    test(i)
    print("=== = ")

Это печатает:

Testing with 2 terms:
  x                    = 2.0
  taylor_cosine(x,  2) = -1.0
  cosine(x)            = -0.4161468365471424
====
Testing with 3 terms:
  Solver said: unsat
====
Testing with 4 terms:
  x                    = 2.751711543190595
  taylor_cosine(x,  4) = -1.000000000000076
  cosine(x)            = -0.9249542537694367
====
Testing with 5 terms:
  Solver said: unsat
====
Testing with 6 terms:
  x                    = 3.087083093614183
  taylor_cosine(x,  6) = -1.0000000000000144
  cosine(x)            = -0.9985147217565723
====
Testing with 7 terms:
  Solver said: unsat
====
Testing with 8 terms:
  x                    = 3.138726428355767
  taylor_cosine(x,  8) = -0.9999999999999999
  cosine(x)            = -0.999995892379266
====
Testing with 9 terms:
  Solver said: unsat
====

Наблюдения:

  1. Решатель может сказать unsat (случаи 3, 5, 7 и 9): Это потому, что для количества терминов, которые мы включаем для данного прогона, он может прийти к выводу, что выражение, полученное в результате вызова taylor_cosine, никогда не будет именно -1. Если вы этого не хотите, вам следует быть осторожным и отстаивать свои ограничения, чтобы оставить некоторую свободу действий. (т. е. в пределах выбранного пользователем эпсилона.)

  2. Результат, как правило, будет становиться все более и более точным по мере увеличения количества терминов. Мы можем наблюдать это поразительным образом: с двумя членами значение, которое мы получаем, очень далеко: 2. При 8 слагаемых получаем 3.139, что не так уж и страшно. И мы видим, что наше приближение pi становится все лучше и лучше с увеличением количества терминов: 2, 2.75, 3.08, 3.139 и т. д.

  3. Излишне говорить, что чем больше терминов вы включите, тем сложнее станет проблема для z3. Если вы придерживаетесь только вещественнозначных ограничений, вы всегда получите ответ от z3, поскольку у него есть процедура принятия решения для нелинейной вещественной арифметики. (Конечно, это не означает, что он будет «быстрым». Просто он сможет принимать решения, имея достаточно времени/памяти и т. д.) Если вы смешаете туда целочисленные ограничения, тогда все ставки отключены, поскольку вы окажетесь в полуразрешимом фрагменте. Я предполагаю, что вы будете получать unknown чаще, чем хотелось бы, но все зависит от других ограничений и вашего фактического приложения.

В любом случае... Надеюсь, это поможет вам сдвинуться с мертвой точки. Важно помнить, что вам нужно больше условий для точности за счет вычислительной сложности. А поскольку вы имеете дело с приближениями, убедитесь, что ваши ограничения также позволяют получить приблизительные результаты; то есть вместо того, чтобы запрашивать «именно это значение», попросите «около этого значения с эпсилоном по вашему выбору». В противном случае вы получите unsat, как показано выше.

Привет. Это потрясающий ответ. Я прочитал и внимательно протестировал и кажется, что это именно то, что я хотел. У меня только два небольших вопроса: (1) В наблюдении 1 и в конце вы говорите, что мы могли бы использовать некоторый эпсилон, чтобы избежать ответов unsat, вы имеете в виду, что, например, вместо s.add(And(cx == -1)) мы могли бы использовать что-то вроде s.add(And([(cx <= -1-epsilon), (cx >= -1+epsilon)]))?

Theo Deep 22.03.2024 17:14

(2) Я также протестировал пример с квантификаторами в части добавления решателя; например, y = Real('y'), затем to_add = (And([(0 <= cx), (cx <= 0.2+y)])), а затем s.add(Exists([y], ForAll([x],to_add))) и получил unsat, когда n=6. Мой вопрос: что касается лежащей в основе теории первого порядка, должен ли я всегда получать sat/unsat и никогда unknown, если я использую только действительные числа? Я имею в виду при использовании кванторов.

Theo Deep 22.03.2024 17:15

На ваш первый вопрос: Да, именно. В общем, точная форма «релаксации» действительно зависит от того, какое у вас приложение и где вы хотите дать решателю пространство для маневра. (Я не думаю, что здесь можно дать универсальный совет.)

alias 22.03.2024 17:24

Что касается вашего второго вопроса: если вы придерживаетесь реальности, тогда да; z3 имеет процедуру принятия решения, которая может решать произвольные формулы с кванторами во фрагменте NRA. (Это следует из процедуры исключения кванторов Тарского для действительных чисел, если вы хотите узнать о ней больше.) Но имейте в виду, что разрешимое не означает «дешевое». Но я думаю, вам это уже хорошо знакомо.

alias 22.03.2024 17:26

Думаю, мне следует добавить, что z3 является полным для решений, которые являются либо рациональными, либо алгебраическими. т. е. корни многочленов с рациональными коэффициентами. Итак, z3 имеет процедуру принятия решения для NRA для алгебраических вещественных чисел, но не для трансцендентных чисел.

alias 22.03.2024 17:29

Большое спасибо. Мне до сих пор не удается понять разницу между дельта-выполнимостью и классической выполнимостью с помощью аппроксимированных функций (например, косинуса выше). Есть ли у вас какие-либо идеи? Я имею в виду, если я правильно понимаю, при дельта-выполнимости мы проверяем выполнимость с определенной точностью; тогда как в классической выполнимости с помощью аппроксимаций мы также делаем это неявно, верна ли эта интуиция? Хотя надо читать дальше :)

Theo Deep 22.03.2024 22:48

Вот статья для чтения: scungao.github.io/papers/ea.pdf Существует очень точное определение того, что означает дельта-спутник; но и это поле для комментариев недостаточно велико для этого, и я недостаточно осведомлен, чтобы авторитетно говорить об этом.

alias 23.03.2024 00:51

Другие вопросы по теме