Skip to content

Conversation

0sidharth
Copy link
Member

@0sidharth 0sidharth commented Apr 6, 2021

References to other Issues or PRs

Closes #11465
Closes #14563
Fixes #20697
Fixes #21176
Fixes #21177
Fixes #21245
Fixes #21550

Brief description of what is fixed or changed

Add._eval_as_leading_term - If the initially obtained leading term contained irrational or imaginary terms, together was insufficient to check whether the term was 0 or not, as it only cancels rational coefficients. _mexpand along with factor is being used instead now to fix this.

Pow._eval_as_leading_term - In a few cases where g is 0, but cannot be caught by g.is_zero (returns None), NotImplementedError was being raised. In these cases however, it is indeed possible to compute the leading term, so this case has been added. _mexpand at the last return is to make get a simplified answer (in cases like #20697), if cancelling of terms is possible.

tan._eval_as_leading_term and cot._eval_as_leading_term - Both methods were incomplete, and did not return the correct answer for angles outside their principle domains -
(Before)

>>> f = tan(pi*(x + S(3)/2))/(3*x)
>>> f.as_leading_term(x)
zoo/x
>>> f = cot(pi*(x + 4))/(3*x)
>>> f.as_leading_term(x)
zoo/x

Release Notes

  • core
    • Leading term methods of Add and Pow now simplify irrational and imaginary expressions
  • functions
    • Leading term methods of tan and cot now work for angles outside their principle domain

@sympy-bot
Copy link

sympy-bot commented Apr 6, 2021

Hi, I am the SymPy bot (v161). I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

Your release notes are in good order.

Here is what the release notes will look like:

  • core

    • Leading term methods of Add and Pow now simplify irrational and imaginary expressions (#21253 by @0sidharth)
  • functions

    • Leading term methods of tan and cot now work for angles outside their principle domain (#21253 by @0sidharth)

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.9.

Click here to see the pull request description that was parsed.
<!-- 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. -->
Closes #11465
Closes #14563
Fixes #20697
Fixes #21176
Fixes #21177
Fixes #21245
Fixes #21550 

#### Brief description of what is fixed or changed
`Add._eval_as_leading_term` - If the initially obtained leading term contained irrational or imaginary terms, `together` was insufficient to check whether the term was 0 or not, as it only cancels rational coefficients. `_mexpand` along with `factor` is being used instead now to fix this.

`Pow._eval_as_leading_term` - In a few cases where g is 0, but cannot be caught by `g.is_zero` (returns None), `NotImplementedError` was being raised. In these cases however, it is indeed possible to compute the leading term, so this case has been added. `_mexpand` at the last return is to make get a simplified answer (in cases like #20697), if cancelling of terms is possible.

`tan._eval_as_leading_term` and `cot._eval_as_leading_term` - Both methods were incomplete, and did not return the correct answer for angles outside their principle domains - 
(Before)
```py
>>> f = tan(pi*(x + S(3)/2))/(3*x)
>>> f.as_leading_term(x)
zoo/x
>>> f = cot(pi*(x + 4))/(3*x)
>>> f.as_leading_term(x)
zoo/x
```

#### 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.

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 -->
* core
  * Leading term methods of `Add` and `Pow` now simplify irrational and imaginary expressions
* functions
  * Leading term methods of `tan` and `cot` now work for angles outside their principle domain
<!-- END RELEASE NOTES -->

Update

The release notes on the wiki have been updated.

@0sidharth 0sidharth force-pushed the buggy_leading_terms branch from 6d39d72 to 1da36d0 Compare April 6, 2021 09:55
@0sidharth 0sidharth force-pushed the buggy_leading_terms branch from 1da36d0 to 7fca4a6 Compare April 6, 2021 10:00
@0sidharth 0sidharth force-pushed the buggy_leading_terms branch from d29d587 to 748294e Compare April 15, 2021 15:39
@0sidharth
Copy link
Member Author

The two failing tests are for a similar reason to #21334, i.e. expressions which can be simplified to 0 on a call to simplify, but not other methods, get stuck in the while loop in Add._eval_as_leading_term. The following diff fixes that, but I am not sure how much slower simplify is compared to cancel, powsimp and trigsimp. Is it significantly slower, and is not to be used or is it fine to go ahead with this?

diff --git a/sympy/core/add.py b/sympy/core/add.py
index 1da38afed6..7a52597102 100644
--- a/sympy/core/add.py
+++ b/sympy/core/add.py
@@ -1011,15 +1011,7 @@ def _eval_as_leading_term(self, x, cdir=0):
             new_expr = new_expr.simplify()
             iszero = new_expr.is_zero
         if iszero is True:
-            # simple leading term analysis gave us cancelled terms but we have to send
-            # back a term, so compute the leading term (via series)
-            n0 = min.getn()
-            res = Order(1)
-            incr = S.One
-            while res.is_Order:
-                res = old._eval_nseries(x, n=n0+incr, logx=None, cdir=cdir).cancel().powsimp().trigsimp()
-                incr *= 2
-            return res.as_leading_term(x, cdir=cdir)
+            return old.simplify().compute_leading_term(x)
 
         elif new_expr is S.NaN:
             return old.func._from_args(infinite)

@jksuom
Copy link
Member

jksuom commented Apr 16, 2021

I would rather avoid using compute_leading_term. It is based on calculate_series that is not in core and also fragile. I think that it should be possible to improve as_leading_term making it available in all cases.

@0sidharth 0sidharth force-pushed the buggy_leading_terms branch from c39ec35 to a87f972 Compare April 22, 2021 10:12
@0sidharth
Copy link
Member Author

0sidharth commented Apr 22, 2021

It is difficult to get the suggestion in #21253 (comment) to work. The commit I pushed partially fixes #21334, in that there is no longer a RecursionError being thrown, but the final value being output is just the original expression apart from the Order term, which can be simplified to 0. A more accurate answer should be O(w), which can be obtained by doing something like -

diff --git a/sympy/core/add.py b/sympy/core/add.py
index 404e5aa9f0..bd0f528970 100644
--- a/sympy/core/add.py
+++ b/sympy/core/add.py
@@ -1006,14 +1006,21 @@ def _eval_as_leading_term(self, x, cdir=0):
             return expr
 
         new_expr = new_expr.trigsimp().factor()
-        if not new_expr:
+        iszero = new_expr.is_zero
+        if iszero is None:
+            check = new_expr.simplify()
+            iszero = check.is_zero
+        if iszero is True:
             # simple leading term analysis gave us cancelled terms but we have to send
             # back a term, so compute the leading term (via series)
+            re = (self - new_expr).expand()
+            if re.is_Order:
+                return re
             n0 = min.getn()
             res = Order(1)
             incr = S.One
             while res.is_Order:
-                res = old._eval_nseries(x, n=n0+incr, logx=None, cdir=cdir).cancel().trigsimp()
+                res = old._eval_nseries(x, n=n0+incr, logx=None, cdir=cdir).cancel().powsimp().trigsimp()
                 incr *= 2
             return res.as_leading_term(x, cdir=cdir)
 

However, with this, limit((exp(x*exp(-x)/(exp(-x) + exp(-2*x**2/(x + 1)))) - exp(x))/x, x, oo) (one of the two limits failing in 748294e), remains unevaluated.

This limit has #21334 as an intermediate step, and having the example in the issue output O(w) instead of the 0 part of the expression leads to this limit remaining unevaluated. I wasn't able to figure out a fix for this after trying a lot, so I have removed the simplify block for now. (All tests in the series module pass with this diff)

new_expr = new_expr.simplify()
iszero = new_expr.is_zero
if iszero is True:
new_expr = new_expr.trigsimp().factor()
Copy link
Member

Choose a reason for hiding this comment

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

I would recommend new_expr = factor_terms(new_expr.trigsimp().cancel())

Copy link
Member Author

@0sidharth 0sidharth Apr 22, 2021

Choose a reason for hiding this comment

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

An issue with this -

In [1]: a, b, c, x = symbols('a b c x', positive=True)

In [2]: limit((a + 1)*x - sqrt((a + 1)**2*x**2 + b*x + c), x, oo)
Out[2]: 
      ⎛       ______________    ⎞
      ⎜      ╱  2               ⎟
∞⋅signa - ╲╱  a  + 2a + 1  + 1

(Currently outputs -b/(2*a + 2))

Copy link
Member

Choose a reason for hiding this comment

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

This highlights the importance of getting the expression into form before it enters the limit code. If cse were used on the expression before beginning and the assumptions copied to the substituted variable, the correct result is obtained in master:

>>> cse(eq)
([(x0, a + 1)], [x*x0 - sqrt(b*x + c + x**2*x0**2)])
>>> r, e = _
>>> x0=r[0][0]
>>> _x0 = Symbol('x0',**r[0][1]._assumptions)
>>> limit(e[0].subs(x0, _x0), x, oo).subs(_x0, r[0][1])
-b/(2*(a + 1))

The core workings should not have to deal with all the different ways to write expressions more than necessary. Even a wide-acting call like simplify should be used with great reservation (and preferably not at all).

Copy link
Member Author

Choose a reason for hiding this comment

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

My exams are over and I will resume contributing now.

I think this should be the place where the expression changes to a suitable form -

try:
r = limitinf(e0, z)
except ValueError:
r = limitinf(e0, z, leadsimp=True)
Should I apply cse on e0? I only see cse being used in one other place in the codebase (apart from printing and codegen), which is at
commons, rexpr = cse(expr)
So should cse be used in library code or is there any other preferred alternative?

@@ -422,7 +422,7 @@ def test_issue_5740():
def test_issue_6366():
n = Symbol('n', integer=True, positive=True)
r = (n + 1)*x**(n + 1)/(x**(n + 1) - 1) - x/(x - 1)
assert limit(r, x, 1) == n/2
assert limit(r, x, 1).cancel() == n/2
Copy link
Member

Choose a reason for hiding this comment

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

with the suggestion I made, cancel is not necessary here

Added non-integer power of positive Add expression simplification
@0sidharth 0sidharth force-pushed the buggy_leading_terms branch from 3c23245 to cfb8b76 Compare May 31, 2021 06:01
@0sidharth 0sidharth force-pushed the buggy_leading_terms branch from 506dabe to 721a509 Compare May 31, 2021 19:32
@jksuom
Copy link
Member

jksuom commented Jun 1, 2021

This looks good.Thanks!

@jksuom jksuom merged commit d720d82 into sympy:master Jun 1, 2021
@0sidharth 0sidharth deleted the buggy_leading_terms branch June 1, 2021 05:56
@0sidharth 0sidharth mentioned this pull request Jun 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
5 participants