2025, Nov 22 00:02

Универсальные решения в SymPy: фильтрация результатов solve

Разбираем, как в SymPy получать решения, верные для всех остальных символов: постобработка результатов solve, проверка тождеств equals(0), примеры и советы.

Универсальные решения в символьных системах кажутся обманчиво простыми: «решить относительно выбранного поднабора переменных и оставить только то, что работает при любых значениях остальных». На практике прямой вызов решателя компьютерной алгебры может вернуть варианты, верные лишь при скрытых ограничениях на оставшиеся символы. Если вашему конвейеру нужны решения, которые выполняются безусловно относительно прочих свободных символов, их нужно отделять от зависящих от условий.

Постановка задачи

Предположим, вам нужны значения выбранного поднабора переменных, которые удовлетворяют всей системе уравнений независимо от значений остальных свободных символов. Прямой вызов вида solve(system, subset) этого не гарантирует. Два минимальных примера показывают расхождение между тем, что возвращает решатель, и тем, что считается решением, действительным для всех значений прочих символов.

from sympy import solve, symbols

a, b = symbols('a b')
system_eqs = [a, b]
targets = [a]
solve(system_eqs, targets)

Это дает {a: 0}. Однако {a: 0} удовлетворяет системе только тогда, когда b тоже равно 0. Не существует значения a, которое удовлетворяет обоим уравнениям при любом b, так что по смыслу результат должен быть «нет решения».

from sympy import solve, symbols

a, b = symbols('a b')
system_eqs = [a, a + b]
targets = [a]
solve(system_eqs, targets)

Здесь возвращается [], и это уже соответствует задуманной семантике: не существует значения a, решающего всю систему для всех b.

Откуда появляется несоответствие

Символьные решатели обычно выдают кандидатные подстановки для целевых переменных, которые делают систему выполнимой, порой неявно полагаясь на связи между оставшимися символами. Для многих алгебраических задач это нормально, но это расходится с более жестким требованием: решение должно обнулять каждое уравнение при любых допустимых значениях остальных свободных символов. В таком режиме кандидат принимается только тогда, когда после подстановки каждое уравнение тождественно обращается в 0 без дополнительных условий.

Практический способ обеспечить «верно для всех остальных символов»

Добиться этого можно постфильтрацией результатов solve. Запрашивайте решения в виде словарей, подставляйте каждый вариант обратно во все уравнения и оставляйте только те, при которых они тождественно обращаются в ноль. Проверка ниже следует именно этой схеме.

from sympy import solve, symbols

# Система: a == 0 и b == 0
# Нам нужны решения для a, верные независимо от b
p, q = symbols('p q')
exprs = [p, q]
unknowns = [p]

# Получаем кандидатные подстановки для p
candidates = solve(exprs, unknowns, dict=True)

# Оставляем только те варианты, которые обнуляют все уравнения без дополнительных условий
universal = []
for cand in candidates:
    if all(term.xreplace(cand).equals(0) for term in exprs):
        universal.append(cand)

print(universal)  # []

Для этой системы отфильтрованный результат пуст, что соответствует ожидаемой интерпретации. Если заменить второе уравнение на p*q, фильтр примет [{p: 0}] как универсальное решение, поскольку подстановка p = 0 делает и p, и p*q равными 0 при любом q.

Замечания о вычислимости и корректности

Проверка тождественности equals(0) для некоторых выражений может быть дорогой. Универсального метода, одинаково хорошо подходящего для всех систем, нет, и нет гарантии, что каждый случай можно эффективно решить. Более быстрая, «вероятно корректная» стратегия повторяет внутреннюю логику решателя: сохранять кандидатов, пока проверка согласованности не даёт однозначный False. В такой парадигме варианты с непрояснённым исходом остаются. Это обмен жёсткости на скорость и лучшая масштабируемость при большом числе кандидатов, но иногда он пропускает решения, опирающиеся на скрытые ограничения.

Почему это важно

Дальнейший код часто зависит от чёткой семантики: либо решение безоговорочно удовлетворяет каждому уравнению относительно остальных символов, либо нет. Смешивание безусловных и условных решений усложняет автоматизированные процессы и ведёт к скрытым ошибкам, особенно когда последующие этапы подразумевают отсутствие скрытых ограничений. Явное разведение этих случаев на границе решателя упрощает логику дальше по конвейеру.

Выводы

Если вам нужны решения, справедливые при любых значениях остальных свободных символов, не полагайтесь на «сырые» результаты обычного вызова решателя. Постобрабатывайте кандидатные подстановки: подставляйте их обратно и проверяйте, что каждое уравнение тождественно обращается в ноль. equals(0) дороже, но строже; если на первом месте быстродействие и допустима некоторая неопределённость, используйте более быстрый тест согласованности, который отклоняет решения лишь тогда, когда их несостоятельность доказуема. При наличии дополнительных структурных предположений о системе оптимум может меняться, но в их отсутствие фильтрация после solve — надёжная общая тактика. Все примеры выше проверены в SymPy 1.13.3.