Repository: sympy/sympy
Pull request: manualintegrate modifications
fixes #9687
closes #10611 as a replacement and modification

Problem statement:
integrate(1/((x**2+4)*sqrt(4*x**2+1)), x) raises `TypeError: 'NoneType' object has no attribute '__getitem__'`
```
In [1]: integrate(1/((x**2+4)*sqrt(4*x**2+1)), x)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-b25b3537657d> in <module>()
----> 1 integrate(1/((x**2+4)*sqrt(4*x**2+1)), x)

/home/ondrej/repos/sympy/sympy/utilities/decorator.py in threaded_func(expr, *args, **kwargs)
     35                                       func(expr.rhs, *args, **kwargs))
     36             else:
---> 37                 return func(expr, *args, **kwargs)
     38 
     39     return threaded_func

/home/ondrej/repos/sympy/sympy/integrals/integrals.pyc in integrate(*args, **kwargs)
   1253     if isinstance(integral, Integral):
   1254         return integral.doit(deep=False, meijerg=meijerg, conds=conds,
-> 1255                              risch=risch, manual=manual)
   1256     else:
   1257         return integral

/home/ondrej/repos/sympy/sympy/integrals/integrals.pyc in doit(self, **hints)
    480                     function, xab[0],
    481                     meijerg=meijerg1, risch=risch, manual=manual,
--> 482                     conds=conds)
    483                 if antideriv is None and meijerg1 is True:
    484                     ret = try_meijerg(function, xab)

/home/ondrej/repos/sympy/sympy/integrals/integrals.pyc in _eval_integral(self, f, x, meijerg, risch, manual, conds)
    884             if h is None and manual is not False:
    885                 try:
--> 886                     result = manualintegrate(g, x)
    887                     if result is not None and not isinstance(result, Integral):
    888                         if result.has(Integral):

/home/ondrej/repos/sympy/sympy/integrals/manualintegrate.py in manualintegrate(f, var)
   1143     sympy.integrals.integrals.Integral
   1144     """
-> 1145     return _manualintegrate(integral_steps(f, var))

/home/ondrej/repos/sympy/sympy/integrals/manualintegrate.py in integral_steps(integrand, symbol, **options)
    944             null_safe(trig_substitution_rule)
    945         ),
--> 946         fallback_rule)(integral)
    947     del _integral_cache[cachekey]
    948     return result

/home/ondrej/repos/sympy/sympy/strategies/core.pyc in do_one_rl(expr)
     83     def do_one_rl(expr):
     84         for rl in rules:
---> 85             result = rl(expr)
     86             if result != expr:
     87                 return result

/home/ondrej/repos/sympy/sympy/strategies/core.pyc in do_one_rl(expr)
     83     def do_one_rl(expr):
     84         for rl in rules:
---> 85             result = rl(expr)
     86             if result != expr:
     87                 return result

/home/ondrej/repos/sympy/sympy/strategies/core.pyc in null_safe_rl(expr)
     63     """ Return original expr if rule returns None """
     64     def null_safe_rl(expr):
---> 65         result = rule(expr)
     66         if result is None:
     67             return expr

/home/ondrej/repos/sympy/sympy/integrals/manualintegrate.py in trig_substitution_rule(integral)
    688         for expr in matches:
    689             match = expr.match(a + b*symbol**2)
--> 690             a = match[a]
    691             b = match[b]
    692 

TypeError: 'NoneType' object has no attribute '__getitem__'
```

Hints from issue discussion:
@lidavidm, do you have any idea what is going on here? The error seems to come from `manualintegrate`.

It's assuming that `subexpr.match(query)` is not `None` for each `subexpr in expr.find(query)`. I guess it should check if the `match` is `None`, and if so, move on to the next subexpression.

Commit message: fix ode tests

Patch to review:
diff --git a/sympy/integrals/manualintegrate.py b/sympy/integrals/manualintegrate.py
index 8ef504501bbd..8040a16949b6 100644
--- a/sympy/integrals/manualintegrate.py
+++ b/sympy/integrals/manualintegrate.py
@@ -28,6 +28,8 @@
 from sympy.strategies.core import switch, do_one, null_safe, condition
 
 
+ZERO = sympy.S.Zero
+
 def Rule(name, props=""):
     # GOTCHA: namedtuple class name not considered!
     def __eq__(self, other):
@@ -300,8 +302,7 @@ def make_inverse_trig(RuleClass, base_exp, a, sign_a, b, sign_b):
             substep = ConstantTimesRule(constant, factored, substep, integrand, symbol)
         return substep
 
-    a, b = match[a], match[b]
-
+    a, b = [match.get(i, ZERO) for i in (a, b)]
     # list of (rule, base_exp, a, sign_a, b, sign_b, condition)
     possibilities = []
 
@@ -678,14 +679,16 @@ def trig_sincos_rule(integral):
     if any(integrand.has(f) for f in (sympy.sin, sympy.cos)):
         pattern, a, b, m, n = sincos_pattern(symbol)
         match = integrand.match(pattern)
+        if not match:
+            return
 
-        if match:
-            a, b, m, n = match.get(a, 0), match.get(b, 0), match.get(m, 0), match.get(n, 0)
-            return multiplexer({
-                sincos_botheven_condition: sincos_botheven,
-                sincos_sinodd_condition: sincos_sinodd,
-                sincos_cosodd_condition: sincos_cosodd
-            })((a, b, m, n, integrand, symbol))
+        return multiplexer({
+            sincos_botheven_condition: sincos_botheven,
+            sincos_sinodd_condition: sincos_sinodd,
+            sincos_cosodd_condition: sincos_cosodd
+        })(tuple(
+            [match.get(i, ZERO) for i in (a, b, m, n)] +
+            [integrand, symbol]))
 
 def trig_tansec_rule(integral):
     integrand, symbol = integral
@@ -697,14 +700,16 @@ def trig_tansec_rule(integral):
     if any(integrand.has(f) for f in (sympy.tan, sympy.sec)):
         pattern, a, b, m, n = tansec_pattern(symbol)
         match = integrand.match(pattern)
+        if not match:
+            return
 
-        if match:
-            a, b, m, n = match.get(a, 0),match.get(b, 0), match.get(m, 0), match.get(n, 0)
-            return multiplexer({
-                tansec_tanodd_condition: tansec_tanodd,
-                tansec_seceven_condition: tansec_seceven,
-                tan_tansquared_condition: tan_tansquared
-            })((a, b, m, n, integrand, symbol))
+        return multiplexer({
+            tansec_tanodd_condition: tansec_tanodd,
+            tansec_seceven_condition: tansec_seceven,
+            tan_tansquared_condition: tan_tansquared
+        })(tuple(
+            [match.get(i, ZERO) for i in (a, b, m, n)] +
+            [integrand, symbol]))
 
 def trig_cotcsc_rule(integral):
     integrand, symbol = integral
@@ -717,13 +722,15 @@ def trig_cotcsc_rule(integral):
     if any(integrand.has(f) for f in (sympy.cot, sympy.csc)):
         pattern, a, b, m, n = cotcsc_pattern(symbol)
         match = integrand.match(pattern)
+        if not match:
+            return
 
-        if match:
-            a, b, m, n = match.get(a, 0),match.get(b, 0), match.get(m, 0), match.get(n, 0)
-            return multiplexer({
-                cotcsc_cotodd_condition: cotcsc_cotodd,
-                cotcsc_csceven_condition: cotcsc_csceven
-            })((a, b, m, n, integrand, symbol))
+        return multiplexer({
+            cotcsc_cotodd_condition: cotcsc_cotodd,
+            cotcsc_csceven_condition: cotcsc_csceven
+        })(tuple(
+            [match.get(i, ZERO) for i in (a, b, m, n)] +
+            [integrand, symbol]))
 
 def trig_powers_products_rule(integral):
     return do_one(null_safe(trig_sincos_rule),
@@ -732,19 +739,18 @@ def trig_powers_products_rule(integral):
 
 def trig_substitution_rule(integral):
     integrand, symbol = integral
-    a = sympy.Wild('a', exclude=[0, symbol])
-    b = sympy.Wild('b', exclude=[0, symbol])
+    A = sympy.Wild('a', exclude=[0, symbol])
+    B = sympy.Wild('b', exclude=[0, symbol])
     theta = sympy.Dummy("theta")
 
-    matches = integrand.find(a + b*symbol**2)
+    matches = integrand.find(A + B*symbol**2)
     if matches:
         for expr in matches:
-            match = expr.match(a + b*symbol**2)
-            a = match[a]
-            b = match[b]
+            match = expr.match(A + B*symbol**2) or {}
+            a, b = [match.get(i, ZERO) for i in (A, B)]
+            a_positive = a.is_positive
+            b_positive = b.is_positive
 
-            a_positive = ((a.is_number and a > 0) or a.is_positive)
-            b_positive = ((b.is_number and b > 0) or b.is_positive)
             x_func = None
             if a_positive and b_positive:
                 # a**2 + b*x**2. Assume sec(theta) > 0, -pi/2 < theta < pi/2
@@ -753,12 +759,12 @@ def trig_substitution_rule(integral):
                 # value on the interval -pi/2 < theta < pi/2 so x takes on
                 # any value
                 restriction = True
-            elif a_positive and not b_positive:
+            elif a_positive:
                 # a**2 - b*x**2. Assume cos(theta) > 0, -pi/2 < theta < pi/2
                 constant = sympy.sqrt(a)/sympy.sqrt(-b)
                 x_func = constant * sympy.sin(theta)
                 restriction = sympy.And(symbol > -constant, symbol < constant)
-            elif not a_positive and b_positive:
+            elif b_positive:
                 # b*x**2 - a**2. Assume sin(theta) > 0, 0 < theta < pi
                 constant = sympy.sqrt(-a)/sympy.sqrt(b)
                 x_func = constant * sympy.sec(theta)
diff --git a/sympy/integrals/tests/test_manual.py b/sympy/integrals/tests/test_manual.py
index b26858d3e534..75d7b09a3788 100644
--- a/sympy/integrals/tests/test_manual.py
+++ b/sympy/integrals/tests/test_manual.py
@@ -261,11 +261,21 @@ def test_issue_6746():
     assert manualintegrate((y + 1)**(n*x), x) == \
         (y + 1)**(n*x)/(n*log(y + 1))
     a = Symbol('a', negative=True)
-    assert manualintegrate(1 / (a + b*x**2), x) == \
+    b = Symbol('b')
+    assert manualintegrate(1/(a + b*x**2), x) == \
         Piecewise((atan(x/sqrt(a/b))/(b*sqrt(a/b)), a/b > 0), \
         (-acoth(x/sqrt(-a/b))/(b*sqrt(-a/b)), And(a/b < 0, x**2 > -a/b)), \
         (-atanh(x/sqrt(-a/b))/(b*sqrt(-a/b)), And(a/b < 0, x**2 < -a/b)))
-
+    b = Symbol('b', negative=True)
+    assert manualintegrate(1/(a + b*x**2), x) == \
+        atan(x/(sqrt(-a)*sqrt(-1/b)))/(b*sqrt(-a)*sqrt(-1/b))
+    assert manualintegrate(1/((x**a + y**b + 4)*sqrt(a*x**2 + 1)), x) == \
+        y**(-b)*Integral(x**(-a)/(y**(-b)*sqrt(a*x**2 + 1) +
+        x**(-a)*sqrt(a*x**2 + 1) + 4*x**(-a)*y**(-b)*sqrt(a*x**2 + 1)), x)
+    assert manualintegrate(1/((x**2 + 4)*sqrt(4*x**2 + 1)), x) == \
+        Integral(1/((x**2 + 4)*sqrt(4*x**2 + 1)), x)
+    assert manualintegrate(1/(x - a**x + x*b**2), x) == \
+        Integral(1/(-a**x + b**2*x + x), x)
 
 
 def test_issue_2850():
diff --git a/sympy/solvers/tests/test_ode.py b/sympy/solvers/tests/test_ode.py
index 97d3aa156908..cfb33d428276 100644
--- a/sympy/solvers/tests/test_ode.py
+++ b/sympy/solvers/tests/test_ode.py
@@ -542,21 +542,17 @@ def test_checksysodesol():
 
 @slow
 def test_nonlinear_3eq_order1():
-    x, y, z = symbols('x, y, z', function=True)
+    x, y, z, C3 = symbols('x, y, z, C3', function=True)
     t = Symbol('t')
-    eq1 = (4*diff(x(t),t) + 2*y(t)*z(t), 3*diff(y(t),t) - z(t)*x(t), 5*diff(z(t),t) - x(t)*y(t))
-    sol1 = "[Eq(Integral(4/(sqrt(-4*_y**2 - 3*C1 + C2)*sqrt(-4*_y**2 + 5*C1 - C2)), (_y, x(t))), "\
-    "C3 + Integral(-sqrt(15)/15, t)), Eq(Integral(3/(sqrt(-6*_y**2 - C1 + 5*C2)*sqrt(3*_y**2 + C1 - 4*C2)), "\
-    "(_y, y(t))), C3 + Integral(sqrt(5)/10, t)), Eq(Integral(5/(sqrt(-10*_y**2 - 3*C1 + C2)*"\
-    "sqrt(5*_y**2 + 4*C1 - C2)), (_y, z(t))), C3 + Integral(sqrt(3)/6, t))]"
-    assert str(dsolve(eq1)) == sol1
-
-    eq2 = (4*diff(x(t),t) + 2*y(t)*z(t)*sin(t), 3*diff(y(t),t) - z(t)*x(t)*sin(t), 5*diff(z(t),t) - x(t)*y(t)*sin(t))
-    sol2 = "[Eq(Integral(3/(sqrt(-6*_y**2 - C1 + 5*C2)*sqrt(3*_y**2 + C1 - 4*C2)), (_y, x(t))), C3 + "\
-    "Integral(-sqrt(5)*sin(t)/10, t)), Eq(Integral(4/(sqrt(-4*_y**2 - 3*C1 + C2)*sqrt(-4*_y**2 + 5*C1 - C2)), "\
-    "(_y, y(t))), C3 + Integral(sqrt(15)*sin(t)/15, t)), Eq(Integral(5/(sqrt(-10*_y**2 - 3*C1 + C2)*"\
-    "sqrt(5*_y**2 + 4*C1 - C2)), (_y, z(t))), C3 + Integral(-sqrt(3)*sin(t)/6, t))]"
-    assert str(dsolve(eq2)) == sol2
+    eq1 = (4*diff(x(t),t) + 2*y(t)*z(t), 3*diff(y(t),t)
+        - z(t)*x(t), 5*diff(z(t),t) - x(t)*y(t))
+    assert dsolve(eq1) == [C3 - sqrt(15)*t/15,
+        C3 + sqrt(5)*t/10, C3 + sqrt(3)*t/6]
+
+    eq2 = (4*diff(x(t),t) + 2*y(t)*z(t)*sin(t), 3*diff(y(t),t)
+        - z(t)*x(t)*sin(t), 5*diff(z(t),t) - x(t)*y(t)*sin(t))
+    assert str(dsolve(eq2)) == [C3 + sqrt(5)*cos(t)/10,
+        C3 - sqrt(15)*cos(t)/15, C3 + sqrt(3)*cos(t)/6]
 
 
 def test_checkodesol():


Code review comments:
diff_hunk: @@ -542,21 +542,17 @@ def test_checksysodesol():
 
 @slow
 def test_nonlinear_3eq_order1():
-    x, y, z = symbols('x, y, z', function=True)
+    x, y, z, C3 = symbols('x, y, z, C3', function=True)
     t = Symbol('t')
-    eq1 = (4*diff(x(t),t) + 2*y(t)*z(t), 3*diff(y(t),t) - z(t)*x(t), 5*diff(z(t),t) - x(t)*y(t))
-    sol1 = "[Eq(Integral(4/(sqrt(-4*_y**2 - 3*C1 + C2)*sqrt(-4*_y**2 + 5*C1 - C2)), (_y, x(t))), "\
-    "C3 + Integral(-sqrt(15)/15, t)), Eq(Integral(3/(sqrt(-6*_y**2 - C1 + 5*C2)*sqrt(3*_y**2 + C1 - 4*C2)), "\
-    "(_y, y(t))), C3 + Integral(sqrt(5)/10, t)), Eq(Integral(5/(sqrt(-10*_y**2 - 3*C1 + C2)*"\
-    "sqrt(5*_y**2 + 4*C1 - C2)), (_y, z(t))), C3 + Integral(sqrt(3)/6, t))]"
-    assert str(dsolve(eq1)) == sol1
-
-    eq2 = (4*diff(x(t),t) + 2*y(t)*z(t)*sin(t), 3*diff(y(t),t) - z(t)*x(t)*sin(t), 5*diff(z(t),t) - x(t)*y(t)*sin(t))
-    sol2 = "[Eq(Integral(3/(sqrt(-6*_y**2 - C1 + 5*C2)*sqrt(3*_y**2 + C1 - 4*C2)), (_y, x(t))), C3 + "\
-    "Integral(-sqrt(5)*sin(t)/10, t)), Eq(Integral(4/(sqrt(-4*_y**2 - 3*C1 + C2)*sqrt(-4*_y**2 + 5*C1 - C2)), "\
-    "(_y, y(t))), C3 + Integral(sqrt(15)*sin(t)/15, t)), Eq(Integral(5/(sqrt(-10*_y**2 - 3*C1 + C2)*"\
-    "sqrt(5*_y**2 + 4*C1 - C2)), (_y, z(t))), C3 + Integral(-sqrt(3)*sin(t)/6, t))]"
-    assert str(dsolve(eq2)) == sol2
+    eq1 = (4*diff(x(t),t) + 2*y(t)*z(t), 3*diff(y(t),t)
path: sympy/solvers/tests/test_ode.py
text: @author:
This doesn't satisfy the original ode, eq1, so it must be wrong.