Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating symbolic nsolve expression. #968

Closed
wants to merge 2 commits into from

Conversation

Krastanov
Copy link
Member

When the equation in nsolve contains too many free symbols the nsolve is
not evaluated but it's placed in a lambda within an implemented_funtion.
That implemented_function is called on the free symbols and the resulting
expression is returned.

Examples

>>> r = nsolve(tanh(y*x)-x, x, 1, imp_function='root_of_equ')
>>> r
root_of_equ(y)
>>> r.evalf(subs={y : 0.9})
0
>>> r.evalf(subs={y : 1.1})
0.502940574944642

Problems

evalf() does not play well with sympy.mpmath.matrices.matrices.matrix.
The tests for this are commented out.
An issue was created for the problem: issue 2994

In [14]: r2.evalf(subs={y : 1.2})
Out[14]: root2(y) # not as expected
In [15]: r2.imp(1.2)
Out[15]: # as expected
[ 1.2]
[-1.2]
In [16]: type(_)
Out[16]: sympy.mpmath.matrices.matrices.matrix

@Krastanov
Copy link
Member Author

SymPy Bot Summary: There were test failures.

@Krastanov: Please fix the test failures.

Test results html report: http://reviews.sympy.org/report/agZzeW1weTNyDAsSBFRhc2sYqIEKDA

Interpreter: /usr/bin/python (2.7.1-final-0)
Architecture: Linux (64-bit)
Cache: yes
Test command: setup.py test
master hash: b085b35
branch hash: d758bc9d839ab02befb5844a005de946ee6780e6

Automatic review by SymPy Bot.

try:
name = kwargs.pop('imp_function')
except KeyError:
raise TypeError('To create a symbolic nsolve function you must'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make a default name? Anyway, I don't like the except KeyError. It's better to wrap this around an if block (i.e., if 'imp_function' in kwargs). But if you use a default name, you can just send it as the second argument to pop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was changed to a default name. It's nicer, thanks.

@Krastanov
Copy link
Member Author

The test still fails.

F = lambdify((x1, x2), f.T, modules='mpmath')
for x0 in [(-1, 1), (1, -2), (4, 4), (-4, -4)]:
# The next line crashes because I try to sympify a matrix...
x = nsolve(f, (x1, x2), x0, tol=1.e-8)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that it crashes here because now I try to sympify the args. Should I just make an exception for matrices? Is there a smarter solution?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think for now you can just make an exception. Matrix should be the only SymPy class that will give you problems with this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll do that. And I'll check if there is a problem with Expr*Matrix (in the tests).

@asmeurer
Copy link
Member

Implemented functions are apparently not callable, which is a good thing, because the order of the arguments would be arbitrary (at least in terms of this method). But the way that they print does not make this clear.

@Krastanov
Copy link
Member Author

I did not understand you last post.

In a = implemented_function(......) the a will be a class Function and a(x) will be Expr and finally a(1).evalf() will just call a._imp_(1).

And yes, currently the arguments for the nsolve expression have an arbitrary order, but this is not a problem because they are already applied. I mean that nsolve(x*y*z, x, 0) will return root_nsolve(z,y) or root_nsolve(y,z) and not root_nsolve without the y and z applied.

@Krastanov
Copy link
Member Author

The workaround for Matrix is done.

@Krastanov
Copy link
Member Author

SymPy Bot Summary: All tests have passed.

Test results html report: http://reviews.sympy.org/report/agZzeW1weTNyDAsSBFRhc2sY-5AKDA

Interpreter: /usr/bin/python (2.7.1-final-0)
Architecture: Linux (64-bit)
Cache: yes
Test command: setup.py test
master hash: 329f634
branch hash: a4bf8f8c82cd34dbecaf43c91bc1654fee5dd732

Automatic review by SymPy Bot.

@Krastanov
Copy link
Member Author

Now it is ready for a review.

# TODO: Ugly workaround for sympification of a matrix
# and the lack of free_symbols property.
# TODO: Same problem with lists.
f = Tuple(*tuple(f)) if isinstance(f, (Matrix, list)) else sympify(f)
fargs = args[1]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reviewing please check those lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the Matrix bit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the workaround: lines 1933..1936

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks OK to me, though Matrix.free_symbols could be added if that would help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sympification will still be a problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine. It will be fixed by issue 1015.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a link to the issue? I found only 388. 1015 seems to be about limits.
Is the code here good for inclusion or should the workaround be changed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you mean pull 1015. I did not understand at first. I suppose that this may wait for 1015 then.

@Krastanov
Copy link
Member Author

@asmeurer, can this go in as I would like to use it in plotting examples.

@asmeurer
Copy link
Member

SymPy Bot Summary: There were merge conflicts; could not test the branch.

@Krastanov: Please rebase or merge your branch with master. See the report for a list of the merge conflicts.

Test results html report: http://reviews.sympy.org/report/agZzeW1weTNyDAsSBFRhc2sY1tcPDA

Interpreter: /usr/local/bin/python2.5 (2.5.0-final-0)
Architecture: Darwin (32-bit)
Cache: yes
Test command: setup.py test
master hash: f2e2705
branch hash: a4bf8f8c82cd34dbecaf43c91bc1654fee5dd732

Automatic review by SymPy Bot.

@@ -1961,6 +1996,15 @@ def nsolve(*args, **kwargs):
x = findroot(f, x0, J=J, **kwargs)
return x

def _nsolve_implemented_function(f, fargs, x0, **kwargs):
#TODO the order of the substitutions is random
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be fixed. The order of the args will be completely random and might not even be the same for two runs of the same expression.

Krastanov added 2 commits May 5, 2012 23:32
When the equation in nsolve contains too many free symbols the nsolve is
not evaluated but it's placed in a lambda within an implemented_funtion.
That implemented_function is called on the free symbols and the resulting
expression is returned.

Examples
========
>>> r = nsolve(tanh(y*x)-x, x, 1, imp_function='root_of_equ')
>>> r
root_of_equ(y)
>>> r.evalf(subs={y : 0.9})
0
>>> r.evalf(subs={y : 1.1})
0.502940574944642

Problems
========
evalf() does not play well with sympy.mpmath.matrices.matrices.matrix.
The tests for this are commented out.
An issue was created for the problem: issue 2994

In [14]: r2.evalf(subs={y : 1.2})
Out[14]: root2(y) # not as expected
In [15]: r2._imp_(1.2)
Out[15]: # as expected
[ 1.2]
[-1.2]
In [16]: type(_)
Out[16]: sympy.mpmath.matrices.matrices.matrix

* Default choice of name for the nsolve expression.

When you call nsolve with too many free symbols it will create an
implemented_function with a default name if you do not supply one.

In [1]: nsolve(x-y,x,0)
Out[1]: nsolve_root(y)

In [2]: _.subs({y:1})
Out[2]: nsolve_root(1)

In [3]: _.evalf()
Out[3]: 1.0000000000000

* Workaround for the creation of nsolve symbolic expression from Matrix.

Two problems were present:

- sympify does not support Matrix
- Matrix and list do not have free_symbols

Both of those are casted to Tuples as a workaround:

f = Tuple(*tuple(f)) if isinstance(f, (Matrix, list)) else sympify(f)

* Exchange wrongly used atoms(Symbol) in nsolve with free_symbols.

nsolve permits the user to not specify the variable to solve for.
To find the right variable it's poping one from atoms(Symbol).
This will cause an error if solving for an Integral for examples.

Still nsolving for expressions of that type is imposible because
lambdify does not support them.
When creating a symbolic nsolve one gets a random ordering for the arguments.
This is generally not a problem as the function object is already called with
the arguments in the correct order and the user is expected to use subs on the
resulting expression.

However if the user wants to use the .func argument there is no easy way to get
the argument order. This commit fixes this by permitting the user to specify
the desired order.
@Krastanov
Copy link
Member Author

@asmeurer, I am sorry for taking so long to address your remarks on this PR.

You had already reviewed the workaround that I use in order to work with Matrix and list. The need for the workaround has not disappeared.

You asked for an option to enforce the order of the arguments for the generated function. This was added.

Test are running now. If they pass, can this go in?

The most widely used cases (1D problems) work ok. The multidimensional cases work thanks to the workaround.

@Krastanov
Copy link
Member Author

SymPy Bot Summary: ✳️ All tests have passed.

Test results html report: http://reviews.sympy.org/report/agZzeW1weTNyDAsSBFRhc2sY5Y0YDA

Interpreter: /usr/bin/python (2.7.2-final-0)
Architecture: Linux (64-bit)
Cache: yes
Test command: setup.py test
master hash: 985c21a
branch hash: 6762c90

Automatic review by SymPy Bot.

@rlamy
Copy link
Member

rlamy commented May 7, 2012

Hmm... I'm not sure I understand what's going on here. The concept of "symbolic nsolve" is rather obviously self-contradicting and I get the feeling that you're gluing some unrelated functionality onto nsolve().

@Krastanov
Copy link
Member Author

@ronan, It is possible that my proposition is not idiomatic in sympy,
so I will ask a question before trying to defend the proposal:

if you want for example the function:
a ---> solution of tan(a*x)-x
what should you do in sympy?

Obviously if this PR is accepted, the docstring should be made clearer
(if you did not get it, the new user wont either).

@rlamy
Copy link
Member

rlamy commented May 7, 2012

Actually, we can't solve x = tan(a_x), so, a fortiori, we can't define the function. Also, I'm not even sure what function you mean: is "solution of tan(a_x)-x" supposed to be the set of solutions, a particular solution specified somehow, something else?

@asmeurer
Copy link
Member

asmeurer commented May 7, 2012

@Krastanov be careful about user names. @ ronan pinged some other guy. You wanted @rlamy.

@rlamy I think he means strictly in numerical terms (because this is nsolve). So it probably means just one solution, whichever one nsolve finds with the given starting value. @Krastanov correct me if I am wrong.

@Krastanov
Copy link
Member Author

@rlamy, my example was not the best one. I give a more detailed examples and justification here.

TL;DR @asmeurer is correct, and this will return only the first solution found by nsolve.

Examples why one would want this functionality:

(in some form, not necessarily part of nsolve)

Here is a more canonical example: "solution to x=tanh(a*x) as a function of a" This one is very important for solving some toy models in the physics of phase transitions. For example a user will want to plot the phase diagram for a simple Ising model. To do this he will need to evaluate these solutions many times. Presumably he will be aware of the fact that there are 3 (or 1) solutions to this equation and will set his starting seed accordingly.

Back to the example of "x=tan(a*x)". This again is a classical example from physics. It gives you the characteristics of a wave on a massive spring attached to a mass (or in acoustics it can be a toy model for a flute). If I want to know the frequency as a function of the mass you need to evaluate it.

Why it is natural to have this in nsolve:

Well... Give me a more intuitive way to write this:
plot(nsolve(tanh(a*x)-x, x, 2), (a,0,10), title="Ising model magnetization vs temperature")

Obviously the title is unimportant, but I wanted to give a complete example.

The only way to do this in sympy at the moment is:

f = Function('f')
imp = lambda a : nsolve(tanh(a*x)-x, x, 2)
from sympy.utilities.lambdify import implemented_function
f_imp = implemented_function(f, imp)
plot(f_imp(a), (a,0,10), title="blahblah")

@Krastanov
Copy link
Member Author

@rlamy, maybe it will be clearer if I say that I "fake" lazy evaluation in python.

@Krastanov
Copy link
Member Author

@rlamy, You raised the question about "defining what a solution means". By "solution" I mean the output of nsolve for sensible numerical seed. Obviously if the initial guess is not sensible the results will be useless. However in this case nsolve should not have been used to start with.

@rlamy
Copy link
Member

rlamy commented May 7, 2012

It seems that what you want is plot(lambda a: nsolve(tanh(a*x) - x, x, 2), (0, 10), title="Title"). I think that it's an issue with plot(), not with nsolve(). Allowing a syntax like plot(callable, (x_min, x_max)) would be more generally useful than what you're trying to do here, and probably less complicated as well.

@Krastanov
Copy link
Member Author

@rlamy

To me it seems uglier to mix sympy expressions and python expressions. What is the use of a symbolic library if you have to use lambda on each step to fake lazy evaluation. But this discussion is tangential to the issue here. Instead I give you yet another example that is currently impossible/cumbersome:

How do you integrate the curve that I just plotted in the previous example?

In [40]: expr = nsolve(tanh(y*x)-x, x, 2)

In [41]: integrate(expr, (y,0.5,0.6)).evalf()
Out[41]: 0

In [42]: integrate(expr, (y,1.1,1.2)).evalf()
Out[42]: 0.0588406804168056

Disclaimer: the limits for the integral are close because of numerical instability. I suppose that this can be fixed with a better choice of kwargs.

For me it is still very unclear what the issue is. Could you try to clearly state why this should not be part of nsolve? One reason that I could guess is that solve functions should return sets of solutions, so nsolve should return {x} instead of x. But this is just not how nsolve works at the moment. If this is the issue, I agree, but if it is please say it explicitly.

@Krastanov
Copy link
Member Author

@rlamy btw if you think that plot() should work with lambdas, do you think the same thing about integrate, Derivative, etc.

If this is the case you are pointing to a deficiency in sympify and not in plot.

@smichr
Copy link
Member

smichr commented May 8, 2012

I think what we need, perhaps, is a Table(expr, (x, xmin, xmax, step))
class which can accept an explicit or (usually) an implicit expr and
then uses all the tricks of the trade to give values for the dependent
variable over the range of independent variable. I think a symbolic
nsolve expression will be of limited use since a given initial guess
for x may be insufficient for obaining a solution (or staying on a
given solution branch) as y changes. The Table could then be plotted,
integrated and differentiated (the latter two being approximations
dependent on the step size).

@Krastanov
Copy link
Member Author

@smichr, I like your proposition very much (there are minor details on which I disagree, but we can discuss them when we start working on this). Just for reference - see also InterpolatedFunction from Mathematica.

However I believe that here is a deeper issue that sparked my disagreement with @rlamy. nsolve behaves like a mathematical function (returning a scalar) instead of a solver (returning a set). I think this should be fixed.

# TODO: evalf does not play well with mpmath matrix.
# For the moment I'm only testing _imp_.
#assert r2.evalf(subs={y : 1.2})[0] == 1.2
assert r2._imp_(1.2)[0] == 1.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that implemented_function doesn't have a __call__ method instead of having to use _imp_?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I misunderstood the question but the __call__ method is necessary for the creation of "applied function expressions".

f = implemented_function(blah blah)
f is an instance of Function
f(x) uses the f.__call__ method to create the expression f(x)
f(x).evalf() uses the f._imp_ method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just wondering why you were writing assert r2._imp_(1.2)[0]
instead of r2(1.2)[0].

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r2 is an expression, so it is not callable. I tried r2.evalf() but evalf does not work when the answer is an mpmath matrix. r2._imp_ returns the _imp_ of r2.func so there may be a deeper problem here:

f(x).func == f # this is ok
f(x)._imp_ == f._imp_ # this does not make sense as you already remarked

Anyway, this seems to be a bug in something around the Function classes, not the behavior of nsolve. I should update the test so it uses the stricter semantics.

@smichr
Copy link
Member

smichr commented May 8, 2012

behaves like a mathematical function (returning a scalar) instead of a solver (returning a set)

What type of set would you return and in what case(s)?

@rlamy
Copy link
Member

rlamy commented May 8, 2012

@Krastanov: it's basically the nature of nsolve() that it returns a single solution. In the case of x = tan(a*x), I don't see how it's even possible for it to return the full infinite set of solutions.

The real issue is that you're making nsolve() a lot more complicated by allowing it to return completely different types depending on circumstances. That kind of behaviour is an accident waiting to happen in users' code and is hard to memorise.

@Krastanov
Copy link
Member Author

@smichr, @rlamy, check Mathematica's documentation for NSolve returning set of solutions.

example:
in simple cases like nsolve(x-1) return {1} instead of 1.
In case there are multiple solutions a smarter nsolve (like in mathematica) just returns them all in a set.
In case there are an infinite number of solutions raise some warning.

@rlamy, nsolve is returning "maximally evaluated expression" (the basic idea behind Mathematica, see reference.wolfram.com/legacy/v1/contents/4.2.7.pdf the second paragraph). I don't see how this is unexpected from a symbolic algebra library.

@Krastanov
Copy link
Member Author

@rlamy, I filed an issue (more of a feature request). If you are not persuaded by my arguments and if the others agree with you we can just close the request and move the discussion to the issue tracker.

http://code.google.com/p/sympy/issues/detail?id=3259

I still believe that the approach in this PR is the best proposed so far for solving the problem, however it will be counterproductive if we just argue over the semantics.

@rlamy, You have not answered my last question about your view on the possibility to have integrate(lambda ...) and so on. I find the question important.

@asmeurer
Copy link
Member

I agree that this functionality would be useful to have. Whether it should be part of nsolve or some other functionality I don't care. To put an argument forward for "doing things symbolically" as opposed to "just using lambdas", would it be possible to let an implemented function take in symbolic arguments, and remaining unevaluated until a numerical argument (or evalf) is called? In other words, it would just be sugar for creating a custom Function with eval/_eval_evalf that calls nsolve.

@Krastanov
Copy link
Member Author

@asmeurer. The discussion with @rlamy made me think about something like that. Maybe a lazify decorator that permits a python function called with symbolic arguments to return an instance of Application (or Function if they do get merged together).

It would work something like that:

@lazify(some_info_about_the_args)
def nsolve(...):
    .... the same old nsolve

There are many issues to be ironed out with such approach, especially when we have so many sympy functions that guess, for example, the free symbols.

@Krastanov
Copy link
Member Author

Also, it should be considered which approach is more idiomatic for sympy. It is a beautiful feature, but lazy evaluation is not part of python.

@Krastanov
Copy link
Member Author

I am closing this. There is an issue for the subject: http://code.google.com/p/sympy/issues/detail?id=3276

@Krastanov Krastanov closed this May 31, 2012
@Krastanov Krastanov mentioned this pull request Mar 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants