2025, Oct 18 10:16
Почему SymPy не упрощает Abs(cos(x))/sqrt(cos(x)**2) и как это исправить
Почему SymPy не упрощает Abs(cos(x))/sqrt(cos(x)**2) до 1: роль допущений, подводные камни parse_expr и subs, решение через local_dict и верные assumptions.
Когда библиотеки символьной математики ведут себя «слишком буквально», дело часто в отсутствующих допущениях или несогласованных символах. Типичная ситуация в SymPy: мы ожидаем, что Abs(cos(x))/sqrt(cos(x)**2) упростится до 1, а получаем исходное выражение. И причина, и решение просты, если знать, где искать.
Reproducing the issue
В следующем примере создаётся символ с допущениями, после чего строковое выражение разбирается парсером. Мы рассчитываем увидеть 1, но упрощения не происходит, а прямая подстановка строк ничего не меняет.
from sympy import Symbol, parse_expr, simplify
ang = Symbol('ang', positive=True, real=True)
src = '1*Abs(cos(ang))/sqrt(cos(ang)**2)'
expr = parse_expr(src)
failed = expr.subs("Abs(cos(ang))/sqrt(cos(ang)**2)", "1")
# expr всё ещё выглядит как Abs(cos(ang))/sqrt(cos(ang)**2)
# failed не изменился, замена не произошла
Why the simplification does not happen
Суть в том, что parse_expr не знает о заранее объявленном символе, пока вы явно его не передадите. Увидев в строке имя ang, он тихо создаёт новый Symbol("ang") без каких‑либо допущений. Такой заново созданный символ по умолчанию не считается вещественным, и без допущений выражение Abs(cos(ang))/sqrt(cos(ang)**2) не может быть сведено к 1. Для сравнения, x/x упрощается даже для комплексного x, поэтому тот случай ведёт себя иначе.
Есть и вторая ловушка при замене. Когда subs получает строку первым аргументом, она преобразуется в одиночный символ, а не в дерево выражения. В вашем выражении такого отдельного символа нет, поэтому ничего не заменяется. Надёжнее передавать в subs объекты SymPy или числа.
Два связанных замечания поясняют наблюдаемое. Во‑первых, тождество sqrt(x**2) == Abs(x) верно только для вещественных x; для произвольных комплексных символов такая эквивалентность не работает из‑за разреза ветви у sqrt. Во‑вторых, по умолчанию символы в SymPy считаются комплексными, если вы явно не укажете допущения вроде real=True или positive=True, поэтому упрощения, зависящие от вещественности, сами собой не срабатывают.
The fix: bind your existing symbol to the parser
Передайте local_dict в parse_expr, чтобы он использовал уже объявленный символ с его допущениями. После этого выражение, как и ожидается, станет равным 1. Кроме того, выполняйте подстановки с помощью выражений, а не строк.
from sympy import Symbol, parse_expr
ang = Symbol('ang', positive=True, real=True)
src = '1*Abs(cos(ang))/sqrt(cos(ang)**2)'
expr_ok = parse_expr(src, local_dict={'ang': ang})
# expr_ok равен 1
# Если вам нужен шаблон для подстановки, передавайте выражения, а не строки
pattern = parse_expr('Abs(cos(ang))/sqrt(cos(ang)**2)', local_dict={'ang': ang})
replaced = expr_ok.subs(pattern, 1)
# replaced равен 1
Если по какой‑то причине нужно отключить автоматические вычисления при разборе, можно вызвать parse_expr с evaluate=False, но для описанного выше решения это не требуется.
Why this detail matters
Проверки алгебраической эквивалентности, преобразования по правилам и автоматические упрощения зависят от согласованных символов и корректных допущений о области определения. Если парсер создаёт новый символ без допущений, реальные аналитические тождества не применяются, и проверки эквивалентности могут тихо проваливаться. Аналогично, использование строк в subs приводит к «пустым» шаблонам: они выглядят похоже синтаксически, но не совпадают структурно и потому не срабатывают.
Takeaways
При разборе явно привязывайте имена, чтобы итоговое выражение использовало ваши символы и их допущения. Помечайте символы как real или positive, если опираетесь на тождества, справедливые лишь на вещественной оси. Для гарантии структурного совпадения используйте subs с объектами SymPy, а не со строками. С такими мерами предосторожности выражения вроде Abs(cos(x))/sqrt(cos(x)**2) упрощаются до 1 именно тогда, когда это уместно, а ваши символьные расчёты остаются предсказуемыми.