sympy solve против solveset против nsolve

#sympy #solver

#sympy #решатель

Вопрос:

Я пытаюсь решить следующее уравнение для r:

 from sympy import pi, S, solve, solveset, nsolve, symbols

(n_go, P_l, T, gamma_w, P_g, r, R_mol) = symbols(
    'n_go, P_l, T, gamma_w, P_g, r, R_mol', real=True)

expr = -P_g   P_l - 3*R_mol*T*n_go/(4*r**3*pi)   2*gamma_w/r
soln = solveset(expr, r, domain=S.Reals)
soln1 = solve(expr, r)
  

soln имеет форму Complement(Intersection(FiniteSet(...))) , с которой я действительно не знаю, что делать.
soln1 представляет собой список из 3 выражений, два из которых являются сложными. На самом деле, если я подставлю значения для символов и вычислю решения для soln1, все они будут сложными:

 vdict = {n_go: 1e-09, P_l: 101325, T: 300, gamma_w: 0.07168596252716256, P_g: 3534.48011713030, R_mol: 8.31451457896800}
for result in soln1:
    print(result.subs(vdict).n())
  

ВОЗВРАТ:

 -9.17942953565355e-5   0.000158143657514283*I
-9.17942953565355e-5 - 0.000158143657514283*I
0.000182122477993494   1.23259516440783e-32*I
  

Интересно, что сначала подстановка значений, а затем использование solveset() или solve() дает реальный результат:

 solveset(expr.subs(vdict), r, domain=S.Reals).n()
  

{0.000182122477993494}

И наоборот, nsolve завершается ошибкой с этим уравнением, если только начальная точка не содержит первые 7 значащих цифр решения (!):

 nsolve(expr.subs(vdict), r,0.000182122 )
  

Ошибка значения: не удалось найти корень в пределах заданного допуска. (9562985778.9619347103> 2.16840434497100886801e-19)

Это не должно быть так сложно, вот сюжет:

введите описание изображения здесь

Мои вопросы:

  1. Почему здесь nsolve так бесполезно?
  2. Как я могу использовать решение, возвращенное из solveset , для вычисления любых численных решений?
  3. Почему я не могу получить реальное решение, solve если я сначала решаю, а затем подставляю значения?

Комментарии:

1. Если я заменю vdict на {n_go: 1e-10, P_l: 3615, T: 300, gamma_w: 0.07168596252716256, P_g: 3531.98618872273, R_mol: 8.31451457896800} в приведенном выше примере, solve и solveset не найдут никаких решений, но действительная часть solve(expr, r)[2].subs(vdict) соответствует корню, когда я его строю.

Ответ №1:

Ответ от Maelstrom хороший, но я просто хочу добавить несколько моментов.

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

Кроме того, перед заменой символов solve не может узнать, какой из трех корней является реальным. Вот почему вы получаете три решения solve(expr, r) и только одно решение solve(expr.subs(vdict), r) . Третье решение, которое является реальным после подстановки, такое же (игнорируя крошечную мнимую часть), возвращаемое solve после подстановки:

 In [7]: soln1[2].subs(vdict).n()                                                                                                                              
Out[7]: 0.000182122477993494   1.23259516440783e-32⋅ⅈ

In [8]: solve(expr.subs(vdict), r)                                                                                                                            
Out[8]: [0.000182122477993494]
  

Поскольку многочлен плохо обусловлен и имеет большой градиент в корне nsolve , ему трудно найти этот корень. Однако nsolve можно найти корень, если задан достаточно узкий интервал:

 In [9]: nsolve(expr.subs(vdict), r, [0.0001821, 0.0001823])                                                                                                   
Out[9]: 0.000182122477993494
  

Поскольку это, по сути, многочлен, вам лучше всего преобразовать его в многочлен и использовать nroots . Самый быстрый способ сделать это — использовать as_numer_denom , хотя в этом случае это вводит ложный корень в ноль:

 In [26]: Poly(expr.subs(vdict).as_numer_denom()[0], r).nroots()                                                                                               
Out[26]: [0, 0.000182122477993494, -9.17942953565356e-5 - 0.000158143657514284⋅ⅈ, -9.17942953565356e-5   0.000158143657514284⋅ⅈ]
  

Комментарии:

1. Спасибо, значит, разница между заменой до или после решения в основном связана с ошибками округления? Если бы мне удалось заменить into soln и оценить, не дало бы ли это никакого решения, поскольку все три являются сложными (см. soln1 )? Я попробую этот Poly подход.

2. Да, soln в идеале должно оцениваться при замене. Я думаю, что, возможно .evalf , не совсем правильно работает с наборами. Также вы получите пустое множество, потому что реальный корень будет иметь небольшую мнимую часть.

3. @OscarBenjamin Я провел некоторую отладку и обнаружил, что .evalf с наборами все в порядке. Проблема в том, что у нас есть ...Intersection(<3 complex floats>, Reals)... , который возвращает EmptySet . Даже chop=True не помогает, поскольку этот аргумент теряется по пути.

4. Еще раз спасибо за вашу помощь, @OscarBenjamin и @Maelstrom. Поскольку с набором решений, заданным в solveset настоящее время, нелегко работать, этот ответ был наиболее полезным для поиска решения моей конкретной проблемы, которую я описал в отдельном ответе. Поэтому я отмечу ответ @OscarBenjamin как правильный ответ.

Ответ №2:

Ваше выражение по сути является кубическим уравнением.

Применение subs до или после решения не должно существенно ничего менять.

soln

soln имеет вид Complement(Intersection(FiniteSet(<3 cubic solutions>), Reals), FiniteSet(0)) , т.е. кубическое решение в вещественной области, исключая 0.

Следующее должно дать вам простой FiniteSet ответ, но evalf , похоже, не очень хорошо реализовано для наборов.

 print(soln.subs(vdict).evalf())
  

Надеюсь, что-то будет сделано с этим в ближайшее время.

1

Причина nsolve , по которой это бесполезно, заключается в том, что график почти асимптотически вертикальный. Согласно вашему графику, градиент составляет примерно 1.0e8. Я не думаю nsolve , что это полезно для таких крутых графиков.

Выводя ваше замещенное выражение, мы получаем: введите описание изображения здесьуменьшая масштаб, мы получаем: введите описание изображения здесь

Это довольно дикая функция, и я подозреваю nsolve , что использует эпсилон, который слишком велик, чтобы быть полезным в этой ситуации. Чтобы исправить это, вы могли бы предоставить более разумные числа, которые при замене ближе к 1. (Рассмотрите возможность предоставления разных единиц измерения. например. вместо метров / год рассмотрим км / час)

2

Трудно сказать вам, как обращаться с выводом solveset в целом, потому что с каждым типом набора нужно обращаться по-разному. Это также не имеет математического смысла, поскольку soln.args[0].args[0].args[0] должно давать первое кубическое решение, но оно забывает, что оно должно быть реальным и отличным от нуля.

Вы можете использовать args или preorder_traversal или вещи для навигации по дереву. Также должно помочь чтение документации по различным наборам. solve и solveset должны использоваться «в интерактивном режиме», потому что существует множество возможных результатов с множеством способов их понимания.

3

Я считаю soln1 , что имеет 3 решения вместо 4, как вы заявляете. В противном случае ваш цикл будет печатать 4 строки вместо 3. Все они технически сложны (как и природа с плавающими числами в Python). Однако третье решение, которое вы предоставляете, имеет очень маленький воображаемый компонент. Чтобы удалить подобные привередливые вещи, есть аргумент, chop который должен помочь:

 for result in soln1:
    print(result.subs(vdict).n(chop=True))
  

Один из результатов 0.000182122477993494 — это то, что выглядит как ваш корень.

Комментарии:

1. Спасибо!! Так soln.subs(vdict).evalf() что должен быть в состоянии вернуть набор реальных результатов, не так ли? Должен ли я создать проблему для этого? Или вы могли бы? Я не совсем понимаю, как Complement(Intersection(FiniteSet())) должна работать вложенная конструкция.

2. @StanSchymanski да, он должен иметь возможность возвращать набор реальных результатов. Но оно возвращается EmptySet . Я думаю, это связано с тем, что при оценке у нас есть 3 complex решения, которые при пересечении с Reals , возвращаются EmptySet . Я не уверен, как это исправить, не меняя кардинально архитектуру SymPy. Вложенная конструкция такая же, как и ее математическая конструкция. Вы можете print(latex(expr)) поместить его в средство просмотра latex, чтобы красиво просматривать любое выражение. Я предлагаю также использовать отладчик во время работы, чтобы вы могли видеть, какие атрибуты доступны вам во время выполнения, чтобы узнать больше.

3. Я не понимаю, почему вы получаете пустой набор. Для меня print(soln.subs(vdict).evalf()) возвращает: Complement(Intersection(FiniteSet(-4.88704239859008e-7 - ...

4. Реальная проблема была бы простой, я мог бы просто удалить domain=S.Reals из solveset, но в этом примере я все еще не получаю никаких чисел evalf() .

5. @StanSchymanski Возврат пустого набора — это ошибка в SymPy, и я просто сообщаю вам, что этот конкретный случай нелегко исправить. Я думаю, что ваш план удаления domain=Reals на данный момент является хорошим обходным путем. Затем вы можете удалить комплексные числа вручную.

Ответ №3:

Вот ответ на основной вопрос: как эффективно вычислить корни приведенного выше уравнения?

Основываясь на предложении @OscarBenjamin, мы можем работать еще лучше и быстрее, используя Poly и roots вместо nroots . Ниже sympy мгновенно вычисляет корни уравнения для 100 различных значений P_g, сохраняя при этом все остальное постоянным:

 from sympy import pi, Poly, roots, solve, solveset, nsolve, nroots, symbols
(n_go, P_l, T, gamma_w, P_g, r, R_mol) = symbols(
    'n_go, P_l, T, gamma_w, P_g, r, R_mol', real=True)
vdict = {pi:pi.n(), n_go:1e-09, P_l:101325, T:300, gamma_w:0.0717, R_mol: 8.31451457896800}
expr = -P_g   P_l - 3*R_mol*T*n_go/(4*r**3*pi)   2*gamma_w/r
expr_poly = Poly(expr.as_numer_denom()[0], n_go, P_l, T, gamma_w, P_g, r, R_mol, domain='RR[pi]')
result = [roots(expr_poly.subs(vdict).subs(P_g, val)).keys() for val in range(4000,4100)]
  

Все, что остается, это проверить, соответствуют ли решения нашим условиям (положительным, реальным). Спасибо, @OscarBenjamin!

PS: Должен ли я расширить тему выше, чтобы включить nroots и корни?