symforce-org / symforce

Fast symbolic computation, code generation, and nonlinear optimization for robotics
https://symforce.org
Apache License 2.0
1.44k stars 147 forks source link

Symforce solve different than sympy solve #376

Closed hflemmen closed 11 months ago

hflemmen commented 12 months ago

The symbolic solve function appears to be different than sympys equivalent. In addition, it appears to be sparsely documented, so I am not entirely sure it should be the same.

I tried to run the example from sympys documentation:

import symforce.symbolic as sf
x, y, z = sf.symbols("x y z")
solutions = sf.solve([x**2 + y - 2*z, y + 4*z], x, y, dict=True)
print(solutions)

, but got the error:

TypeError: solve() got an unexpected keyword argument 'dict'

Removing the dict parameter gave me the error:

TypeError: Cannot convert list to symengine.lib.symengine_wrapper.Basic

From the code it seems like symforce is just calling the sympy.solve, which leads me to expect the behaviour in the sympy documentation.

Environment (please complete the following information):

aaron-skydio commented 11 months ago

Yeah, the behavior should roughly match the sympy equivalent (we should document this better). However, when SymForce is using the SymEngine symbolic API (the default), it calls symengine.solve instead of sympy.solve, and symengine.solve does not accept a dict argument.

If you want something in sympy.solve that's not in symengine.solve, you can either symforce.set_symbolic_api('sympy'), or do something like:

import sympy as sm
import symforce.symbolic as sf
x, y, z = sf.symbols("x y z")
solutions = sm.solve([x**2 + y - 2*z, y + 4*z], x, y, dict=True)
print(solutions)
sf_solutions = [{sf.S(k): sf.S(v) for k, v in solution.items()} for solution in solutions]

to call sympy.solve while SymForce is using the SymEngine symbolic API

hflemmen commented 11 months ago

Thank you, the explanation makes sense. The workaround works when I use simple symbols that already exists in sympy, but this workaround means that I can't use more complex symforce types?

E.g.

import symforce.symbolic as sf
import sympy as sm
R = sf.Rot2.symbolic("R")
solutions = sm.solve(R.to_tangent(epsilon=sf.epsilon())[0] - 0, R, dict=True)
print(solutions)

fails with the (shortened) error message:

SymPyDeprecationWarning: 

The string fallback in sympify() is deprecated.

To explicitly convert the string form of an object, use
sympify(str(obj)). To add define sympify behavior on custom
objects, use sympy.core.sympify.converter or define obj._sympy_
(see the sympify() docstring).

sympify() performed the string fallback resulting in the following string:

'<Rot2 <C real=R_re, imag=R_im>>'

See https://docs.sympy.org/latest/explanation/active-deprecations.html#deprecated-sympify-string-fallback
for details.

This has been deprecated since SymPy version 1.6. It
will be removed in a future version of SymPy.

  return list(map(sympify, w if iterable(w) else [w]))
ValueError: Error from parse_expr with transformed code: "<Symbol ('Rot2' )<Symbol ('C' )Symbol ('real' )=Symbol ('R_re' ),imag =Symbol ('R_im' )>>"

Is there a way to use sympys solve with an expression including symforce types, or do I have to choose one of them? I.e. if I understand it correctly, I have two options: 1: I either can use symengine backend that allows complex types, but without the option to solve a set of coupled equations, or 2: I can use the sympy backend that does not support complex types but can solve a set of coupled equations. Is there a way to get both symforce types and a solve function that accepts the dict argument?

aaron-skydio commented 11 months ago

You can at least do something like this, although it's a little cumbersome:

In [3]: solutions = sm.solve(R.to_tangent(epsilon=sf.epsilon())[0] - 0, R.to_storage(), dict=True)

In [4]: solutions
Out[4]: [{R_im: 0}]

Noting that I don't think the symengine API supports solve with complex types either, e.g. I get this:

In [6]: solutions = sf.solve(R.to_tangent(epsilon=sf.epsilon())[0] - 0, R)
---------------------------------------------------------------------------
SympifyError                              Traceback (most recent call last)
Cell In[6], line 1
----> 1 solutions = sf.solve(R.to_tangent(epsilon=sf.epsilon())[0] - 0, R)

File ~/symforce/symforce/internal/symbolic.py:529, in solve(*args, **kwargs)
    528 def solve(*args: T.Any, **kwargs: T.Any) -> T.List[Scalar]:
--> 529     solution = sympy.solve(*args, **kwargs)
    530     from symengine.lib.symengine_wrapper import EmptySet as _EmptySet
    532     if isinstance(solution, FiniteSet):

File symengine_wrapper.pyx:5402, in symengine.lib.symengine_wrapper.solve()

File symengine_wrapper.pyx:547, in symengine.lib.symengine_wrapper.sympify()

File symengine_wrapper.pyx:590, in symengine.lib.symengine_wrapper._sympify()

File symengine_wrapper.pyx:513, in symengine.lib.symengine_wrapper.sympy2symengine()

SympifyError: sympy2symengine: Cannot convert '<Rot2 <C real=R_re, imag=R_im>>' (of type <class 'symforce.geo.rot2.Rot2'>) to a symengine type.
hflemmen commented 11 months ago

That works, but you are right that it is a little cumbersome. Thanks for the explanation.