Repository: sympy/sympy
Pull request: watch for iterable sols
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->
fixes #25820

#### Brief description of what is fixed or changed


#### Other comments


#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers. Formerly, `log(-x)` incorrectly gave `-log(x)`.

* physics.units
  * Corrected a semantical error in the conversion between volt and statvolt which
    reported the volt as being larger than the statvolt.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
NO ENTRY
<!-- END RELEASE NOTES -->


Problem statement:
AttributeError: when solve differential equation: `y(x).diff(x).diff(x)*y(x)**3+49`
```python
from sympy import Derivative, Function, Symbol, dsolve
x = Symbol('x')
y = Function('y')
dsolve(y(x).diff(x).diff(x)*y(x)**3+49, y(x))
```

the error:
```python
Traceback (most recent call last):
  File "sympy/solvers/ode/ode.py", line 640, in dsolve
    return _helper_simplify(eq, hint, hints, simplify, ics=ics)
  File "sympy/solvers/ode/ode.py", line 690, in _helper_simplify
    rv = _remove_redundant_solutions(eq, rv, order, func.args[0])
  File "sympy/solvers/ode/ode.py", line 2627, in _remove_redundant_solutions
    if is_special_case_of(soln1, soln2):
  File "sympy/solvers/ode/ode.py", line 2622, in is_special_case_of
    return _is_special_case_of(soln1, soln2, eq, order, var)
  File "sympy/solvers/ode/ode.py", line 2655, in _is_special_case_of
    soln1 = soln1.rhs - soln1.lhs
AttributeError: 'list' object has no attribute 'rhs'
```

Hints from issue discussion:
Some how the solutions are being returned from somewhere as a list of lists of solutions rather than a list of solutions.
```diff
diff --git a/sympy/solvers/ode/ode.py b/sympy/solvers/ode/ode.py
index 75bccdf8d5..4fc6d75d01 100644
--- a/sympy/solvers/ode/ode.py
+++ b/sympy/solvers/ode/ode.py
@@ -639,6 +639,7 @@ def recur_len(l):
             hint = hints['hint']
             return _helper_simplify(eq, hint, hints, simplify, ics=ics)
 
+
 def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
     r"""
     Helper function of dsolve that calls the respective
@@ -670,7 +671,13 @@ def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
         else:
             sols = solvefunc(eq, func, order, match)
         if iterable(sols):
-            rv = [odesimp(eq, s, func, hint) for s in sols]
+            rv = []
+            for s in sols:
+                simp = odesimp(eq, s, func, hint)
+                if len(s) > 1:
+                    rv.extend(simp)
+                else:
+                    rv.append(simp)
         else:
             rv =  odesimp(eq, sols, func, hint)
     else:
@@ -686,6 +693,7 @@ def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
             rv = _handle_Integral(exprs, func, hint)
 
     if isinstance(rv, list):
+        assert all(isinstance(i, Eq) for i in rv), rv
         if simplify:
             rv = _remove_redundant_solutions(eq, rv, order, func.args[0])
         if len(rv) == 1:
@@ -707,6 +715,7 @@ def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
             rv = rv1
     return rv
 
+
 def solve_ics(sols, funcs, constants, ics):
     """
     Solve for the constants given initial conditions
```

Commit message: watch for iterable sols

Patch to review:
diff --git a/sympy/solvers/ode/ode.py b/sympy/solvers/ode/ode.py
index 75bccdf8d56b..d9d40e37f4fe 100644
--- a/sympy/solvers/ode/ode.py
+++ b/sympy/solvers/ode/ode.py
@@ -639,6 +639,7 @@ def recur_len(l):
             hint = hints['hint']
             return _helper_simplify(eq, hint, hints, simplify, ics=ics)
 
+
 def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
     r"""
     Helper function of dsolve that calls the respective
@@ -670,9 +671,15 @@ def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
         else:
             sols = solvefunc(eq, func, order, match)
         if iterable(sols):
-            rv = [odesimp(eq, s, func, hint) for s in sols]
+            rv = []
+            for s in sols:
+                simp = odesimp(eq, s, func, hint)
+                if iterable(simp):
+                    rv.extend(simp)
+                else:
+                    rv.append(simp)
         else:
-            rv =  odesimp(eq, sols, func, hint)
+            rv = odesimp(eq, sols, func, hint)
     else:
         # We still want to integrate (you can disable it separately with the hint)
         if isinstance(solvefunc, SingleODESolver):
@@ -686,6 +693,7 @@ def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
             rv = _handle_Integral(exprs, func, hint)
 
     if isinstance(rv, list):
+        assert all(isinstance(i, Eq) for i in rv), rv  # if not => internal error
         if simplify:
             rv = _remove_redundant_solutions(eq, rv, order, func.args[0])
         if len(rv) == 1:
@@ -707,6 +715,7 @@ def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs):
             rv = rv1
     return rv
 
+
 def solve_ics(sols, funcs, constants, ics):
     """
     Solve for the constants given initial conditions
diff --git a/sympy/solvers/ode/tests/test_ode.py b/sympy/solvers/ode/tests/test_ode.py
index 547b8e425c34..76f6508c0f2a 100644
--- a/sympy/solvers/ode/tests/test_ode.py
+++ b/sympy/solvers/ode/tests/test_ode.py
@@ -1094,3 +1094,12 @@ def test_issue_23425():
     assert classify_ode(eq) == \
         ('Liouville', 'nth_order_reducible', \
         '2nd_power_series_ordinary', 'Liouville_Integral')
+
+
+def test_issue_25820():
+    x = Symbol('x')
+    y = Function('y')
+    eq = y(x)**3*Derivative(y(x), (x, 1)) + 49
+    r = (C1 - 196*x)**(S(1)/4)
+    assert dsolve(eq, y(x)) == [
+        Eq(y(x), -I*r), Eq(y(x), I*r), Eq(y(x), -r), Eq(y(x), r)]


Code review comments:
diff_hunk: @@ -1094,3 +1094,12 @@ def test_issue_23425():
     assert classify_ode(eq) == \
         ('Liouville', 'nth_order_reducible', \
         '2nd_power_series_ordinary', 'Liouville_Integral')
+
+
+def test_issue_25820():
+    x = Symbol('x')
+    y = Function('y')
+    eq = y(x)**3*Derivative(y(x), (x, 1)) + 49
path: sympy/solvers/ode/tests/test_ode.py
text: @user1:
It should be a second derivative:
```
In [1]: dsolve(f(x).diff(x, 2)*f(x)**3 + 49)
---------------------------------------------------------------------------
AttributeError 
```

@author:
That took excessively long so I picked something that traversed the same added code path. I'll revisit tonight.