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

transcendental equation solver for solveset: transolve #14736

Merged
merged 34 commits into from Jul 17, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
059b5ae
initial commit for transolved and exponential solver
Yathartha22 May 23, 2018
f7c38cb
added tests for exponential solver
Yathartha22 May 23, 2018
885923b
added docstring for exponential solver
Yathartha22 May 23, 2018
fd0a4f6
Merge branch 'master' of https://github.com/sympy/sympy into transolv…
Yathartha22 May 23, 2018
f8bc1ad
added documentation for transolve
Yathartha22 May 24, 2018
9dfaf58
minor changes in code and tests.
Yathartha22 May 24, 2018
a2305da
minor changes
Yathartha22 May 25, 2018
d2f067c
improved documentation and imported tests from solve.
Yathartha22 May 31, 2018
217b11f
minor documentation fixes
Yathartha22 Jun 2, 2018
c38ea87
added proof of correctness and minor changes in documentation
Yathartha22 Jun 5, 2018
197355d
imported tests for log and lambert form solve, minor documentation fi…
Yathartha22 Jun 8, 2018
5be292f
improvements for XFAILS to pass
Yathartha22 Jun 8, 2018
ffef477
updated and improved documentation for sphinx
Yathartha22 Jun 10, 2018
a7574b0
removed repeated test
Yathartha22 Jun 11, 2018
648da0e
minor documentation fixes
Yathartha22 Jun 12, 2018
f8597b5
documentation fxes according to pep8; made `_check_expo` to work for …
Yathartha22 Jun 13, 2018
a45df5c
documentation fixes for: `transolve`, `check_expo` and `expo_solver`;…
Yathartha22 Jun 18, 2018
3e0972c
improved case for handling more than 2 terms in `expo_solver`
Yathartha22 Jun 21, 2018
2309ce7
minor documentation changes
Yathartha22 Jun 21, 2018
5e89449
removed unnecessay lines from documentation
Yathartha22 Jun 21, 2018
f2e6ab0
do test before working with args
smichr Jun 21, 2018
89ce4c9
added test for `expo_solver` to return `None`; used top import for Ad…
Yathartha22 Jun 21, 2018
e2f12ac
removed `pow_type()` as this case is handled in `solveset`; removed l…
Yathartha22 Jun 26, 2018
154ee3b
minor document changes
Yathartha22 Jun 28, 2018
8345107
documentation imporovements
Yathartha22 Jul 3, 2018
c0bc441
improved `Philosophy behind the module`; changed the naming conventio…
Yathartha22 Jul 4, 2018
6ddc927
minor documnetation changes
Yathartha22 Jul 4, 2018
de66d1e
Improved documentation for "How to add new class of equations"
Yathartha22 Jul 5, 2018
cca2d14
added `make_expr_args()` to get all the terms of the expressions
Yathartha22 Jul 7, 2018
e49647e
renamed `make_expr_args` to `term_factors()` and improved its documen…
Yathartha22 Jul 8, 2018
e329518
made `term_factors()` an iterator; added some examples in its docstring
Yathartha22 Jul 9, 2018
90efc7a
removed unused list in `term_factors()`; improved doctests for the same
Yathartha22 Jul 9, 2018
e51734a
made `term_factors()` private; minor documentation changes; renamed l…
Yathartha22 Jul 10, 2018
254f45f
removed repeated test and minor change in documentation
Yathartha22 Jul 15, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
189 changes: 185 additions & 4 deletions sympy/solvers/solveset.py
Expand Up @@ -14,10 +14,12 @@
from sympy.core.containers import Tuple
from sympy.core.facts import InconsistentAssumptions
from sympy.core.numbers import I, Number, Rational, oo
from sympy.core.function import (Lambda, expand_complex, AppliedUndef, Function)
from sympy.core.function import (Lambda, expand_complex, AppliedUndef, Function,
Copy link
Member

Choose a reason for hiding this comment

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

Please remove unused import Function

expand_log)
from sympy.core.relational import Eq
from sympy.core.symbol import Symbol
from sympy.simplify.simplify import simplify, fraction, trigsimp
from sympy.simplify import powdenest
from sympy.functions import (log, Abs, tan, cot, sin, cos, sec, csc, exp,
acos, asin, acsc, asec, arg,
piecewise_fold, Piecewise)
Expand All @@ -30,9 +32,9 @@
from sympy.sets.sets import Set
from sympy.matrices import Matrix
from sympy.polys import (roots, Poly, degree, together, PolynomialError,
RootOf)
RootOf, factor)
from sympy.solvers.solvers import (checksol, denoms, unrad,
_simple_dens, recast_to_symbols)
_simple_dens, recast_to_symbols, _ispow)
from sympy.solvers.polysys import solve_poly_system
from sympy.solvers.inequalities import solve_univariate_inequality
from sympy.utilities import filldedent
Expand Down Expand Up @@ -943,7 +945,12 @@ def _solveset(f, symbol, domain, _check=False):
elif equation.has(Abs):
result += _solve_abs(f, symbol, domain)
else:
result += _solve_as_rational(equation, symbol, domain)
new_result = _solve_as_rational(equation, symbol, domain)
if isinstance(new_result, ConditionSet):
# may be a transcendental type equation
result += transolve(equation, symbol, domain)
else:
result += new_result
else:
result += solver(equation, symbol)

Expand Down Expand Up @@ -978,6 +985,180 @@ def _solveset(f, symbol, domain, _check=False):
return result


def _expo_solver(f, symbol):
"""
Helper function for solving exponential equations.
Copy link
Member Author

Choose a reason for hiding this comment

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

I will make it: Helper function for solving exponential equations (supported type).


This function solves exponential equation by using logarithms.
Exponents are converted to a more general form of logs which can further be
Copy link
Member

Choose a reason for hiding this comment

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

...which can be solved by solveset.

solved by passing to _solveset.
It deals with equations of type `a*f(x) + b*g(x)`, where f(x) and g(x)
are power terms.

eg: 3**(2*x) - 2**(x + 3) can be transformed to a better log form,
2*x*log(3) - (x+3)*log(2), which is easily solvable.
"""

a, b = ordered(f.args)

lhs = a
rhs = -b

lhs = expand_log(log(lhs), force=True)
rhs = expand_log(log(rhs), force=True)
Copy link
Member

Choose a reason for hiding this comment

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

force=True does operations that are not mathematically correct for all values. Is it possible for this function to return wrong results because of this?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, log(x*y) will expand into log(x)+log(y) but that is not always true. e.g. if x == y == -1, the former is 0 while the latter is 2*pi*I in SymPy.


return (lhs - rhs)
Copy link
Member

Choose a reason for hiding this comment

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

redundant parenthesis around



def _check_expo(f, symbol):
Copy link
Member

Choose a reason for hiding this comment

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

Add tests for this function

"""
Helper to check whether an equation is exponential or not.
Returns True if it is of exponential type otherwise False.
"""
try:
a, b = ordered(f.args)
Copy link
Member

Choose a reason for hiding this comment

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

please use meaningful variable names.

except ValueError:
return False

ad = a.as_independent(symbol)[1]
bd = b.as_independent(symbol)[1]
Copy link
Member

Choose a reason for hiding this comment

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

same here


return any(_ispow(i) for i in (ad, bd))


def transolve(f, symbol, domain, **flags):
Copy link
Member

Choose a reason for hiding this comment

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

please make it private

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean making it _transolve ??

Copy link
Member

Choose a reason for hiding this comment

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

yes

"""
Function to solve transcendental equations. It is a
helper to solveset and should be used internally as of now.
Handles the following class of transcendental equations:

- Exponential equations
- Logarithmic equations
- LambertW type equations.
- Trigonometric equations


Parameters
==========

f: Expr
The target equation.
symbol: Symbol
The variable for which the eqquation is solved.
doamin: Set
The domain over which the equation is solved.
flags: Dictionary
Takes care of the recursive calls.


Copy link
Member

Choose a reason for hiding this comment

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

remove empty line, (as it is not separating a big enough block of code)

How is transolve better than _tsolve.
====================================

1) Improved Output

The output of many types of equations is much better and easy to
understand than the ones computed by _tsolve.

eg: for 3**(2*x) - 2**(x + 3) transolve would return
FiniteSet(-3*log(2)/(-2*log(3) + log(2))), whereas _tsolve
would return [-log(2**(3/log(2/9)))]. Both are same but the former
increases readability and simplicity.


2) Less Complex API

Copy link
Member

Choose a reason for hiding this comment

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

Please make flags as explicit and add documentation for it regarding why and how it works.

transolve's API and its flow is simple to understand. Unlike _tsolve which
computes by solving with lots of recursive calls.
transolve eases the task by reducing it to a two step procedure as
dicussed below.
Copy link
Member

Choose a reason for hiding this comment

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

typos

inver_real - invert_real
eqaution - equation
generalised - generalized (I know, variant) :-)
dicussed - discussed
doamin - domain
eqquation - equation



3) Extensible

transolve is easily extensible, unlike _tsolve which requires in depth knowledge
of the method and adding new class of equation is difficult due to its complex
structure.
To add a new class of equation in transolve one needs to figure out a way to
identify the equation and a generalised way to solve that particular
class of eqaution.


How transolve works
===================

The way transolve solves any transcendental equation is very much
different from the old solve way of solving as pointed out above.

transolve solves equations in two step procedure.

i) Identification of the type of equation.

Helpers are used with heuristics implemented to determine
if the equation is of a certain type.

ii) Invoking the respective helper to solve the equation.

Once identified what family the equation belongs, respective
Copy link
Member

Choose a reason for hiding this comment

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

Once the family type of the equation has been identified, the corresponding helper is called...

helpers are called and the equation is solved by either returning
the solution or by simplifying the original one to the one that can be
handled by _solveset.


Examples
========

>>> from sympy.solvers.solveset import transolve

>>> x = symbols('x')
>>> transolve(5**(x-3) - 3**(2*x + 1), x, S.Reals)
FiniteSet(-log(375)/(-log(5) + 2*log(3)))
"""

if 'tsolve_saw' not in flags:
Copy link
Member

Choose a reason for hiding this comment

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

flags.setdefault('tsolve_saw', [])

Copy link
Member

Choose a reason for hiding this comment

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

Or all in one:

if f in flags.setdefault('tsolve_saw', []):
    return...
flags['tsolve_saw'].append(f)

flags['tsolve_saw'] = []
if f in flags['tsolve_saw']:
return ConditionSet(symbol, Eq(f, 0), domain)
else:
flags['tsolve_saw'].append(f)

inverter = invert_real if domain.is_subset(S.Reals) else invert_complex
Copy link
Member

Choose a reason for hiding this comment

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

A call to invert_complex(..., domain) will handle which inverter to call (like calling to solveset will just work when you pass a domain).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, we can do that. Will add a comment so as to avoid confusion.

lhs, rhs_s = inverter(f, 0, symbol, domain)

result = ConditionSet(symbol, Eq(f, 0), domain)
Copy link
Member

Choose a reason for hiding this comment

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

You can also write this as:

unsolved_result = ConditionSet(symbol, Eq(f, 0), domain)
result = unsolved_result

and use the unsolved_result in Line 1330, rather than duplicating the same thing.


if rhs_s is S.EmptySet:
result = S.EmptySet
else:
rhs = rhs_s.args[0]
Copy link
Member

Choose a reason for hiding this comment

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

This comment is not addressed yet

Copy link
Member Author

Choose a reason for hiding this comment

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

I added loop for this case before but now it seems to me that loop isn't necessary because _solveset already uses loop here and _transolve is called within this loop, therfore rhs_s will always have one argument

Copy link
Member

Choose a reason for hiding this comment

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

That's fine, but add an assert here to make your intentions clear, so that if someone changes that in solveset tomorrow, transolve should tell the world that it broke.

# do we need a loop for rhs_s?
Copy link
Member

Choose a reason for hiding this comment

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

assert (rhs_s.args) == 1  # can't think of a case where there would be more than one

Copy link
Member

Choose a reason for hiding this comment

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

+1

@Yathartha22 This comment doesn't seem to be addressed as of yet. Never assume anything, always handle all the cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

I will add a loop here rather an assert statement.

# Can't determine a case as of now.
if lhs == symbol:
Copy link
Member Author

Choose a reason for hiding this comment

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

This case is added so that even if one tries to solve the equation by calling transolve, result can be obtained.
cases like: 2**x -32 will be handled here.

Copy link
Member

Choose a reason for hiding this comment

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

yes

result = rhs_s

elif lhs.is_Add:
equation = factor(powdenest(lhs-rhs))
Copy link
Member

Choose a reason for hiding this comment

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

you have to capture denominators containing possible singularities before doing lhs-rhs or else terms with denominators may cancel, e.g. x*y - 1/x = x*z - 1/x for which x = 0 is not a solution.

Copy link
Member

Choose a reason for hiding this comment

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

Would factor_terms be sufficient for what you are expecting? See its options, too, for handling radicals.

Copy link
Member Author

Choose a reason for hiding this comment

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

I will have a look over it. But here I am trying to create a P.O.S of the equation.

Copy link
Member

Choose a reason for hiding this comment

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

space around operator -


if equation.is_Mul:
result = _solveset(equation, symbol, domain)

# check if it is exponential type equation
elif _check_expo(equation, symbol):
new_f = _expo_solver(equation, symbol)
result = _solveset(new_f, symbol, domain)
Copy link
Member

Choose a reason for hiding this comment

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

what if new_f is same as given f?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think this will happen, because even if an equation gets into the condition, new_f will contain expression in log. which will be different than f.


if isinstance(result, ConditionSet):
result = ConditionSet(symbol, Eq(f, 0), domain)

else:
result = transolve(f, symbol, domain, **flags)

elif lhs.is_Pow:
new_f = _expo_solver(lhs, symbol)
result = _solveset(new_f, symbol, domain)
Copy link
Member

Choose a reason for hiding this comment

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

same as this


return result


def solveset(f, symbol=None, domain=S.Complexes):
r"""Solves a given inequality or equation with set as output

Expand Down
67 changes: 64 additions & 3 deletions sympy/solvers/tests/test_solveset.py
Expand Up @@ -38,7 +38,7 @@
solveset_real, domain_check, solveset_complex, linear_eq_to_matrix,
linsolve, _is_function_class_equation, invert_real, invert_complex,
solveset, solve_decomposition, substitution, nonlinsolve, solvify,
_is_finite_with_finite_vars)
_is_finite_with_finite_vars, transolve)


a = Symbol('a', real=True)
Expand Down Expand Up @@ -1613,9 +1613,9 @@ def test_issue_10555():
f = Function('f')
g = Function('g')
assert solveset(f(x) - pi/2, x, S.Reals) == \
ConditionSet(x, Eq(2*f(x) - pi, 0), S.Reals)
ConditionSet(x, Eq(f(x) - pi/2, 0), S.Reals)
assert solveset(f(g(x)) - pi/2, g(x), S.Reals) == \
ConditionSet(g(x), Eq(2*f(g(x)) - pi, 0), S.Reals)
ConditionSet(g(x), Eq(f(g(x)) - pi/2, 0), S.Reals)


def test_issue_8715():
Expand Down Expand Up @@ -1749,3 +1749,64 @@ def test_issue_14454():
number = CRootOf(x**4 + x - 1, 2)
raises(ValueError, lambda: invert_real(number, 0, x, S.Reals))
assert invert_real(x**2, number, x, S.Reals) # no error


def test_transolve():
from sympy.abc import x
from sympy.simplify import simplify

assert transolve(2**x - 32, x, S.Reals) == FiniteSet(log(32)/log(2))
assert transolve(3**x, x, S.Reals) == S.EmptySet
assert simplify(transolve(3**x - 9**(x+5), x, S.Reals)) == FiniteSet(-10)


def test_expo_solver():
from sympy.abc import x, y, z
from sympy.simplify import simplify

e1 = 3**(2*x) - 2**(x + 3)
e2 = 4**(5 - 9*x) - 8**(2 - x)
e3 = 2**x + 4**x
e4 = exp(log(5)*x) - 2**x
e5 = x**(y*z) - x # issue 10864
e6 = 5**(x/2) - 2**(x/3)
e7 = 2**(x) + 4**(x) + 8**(x) - 84
Copy link
Member

Choose a reason for hiding this comment

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

parenthesis around x is redundant

e8 = 4**(x+1) + 4**(x+2) + 4**(x-1) - 3**(x+2) -3**(x+3)

f1 = exp(x/y)*exp(-z/y) - 2
f2 = x**x

assert solveset(e1, x, S.Reals) == FiniteSet(-3*log(2)/(-2*log(3) + log(2)))
assert simplify(solveset(e2, x, S.Reals)) == FiniteSet(4/S(15))
Copy link
Member Author

Choose a reason for hiding this comment

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

Need to return a simplified result for such equations.
eg: 2**x - 32 currently gives {log(32)/log(2)}, should rather be {5}.
Is it a good idea to use simplify() here?

Copy link
Member

Choose a reason for hiding this comment

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

Raw simplify can be a very expensive function call and is generally avoided in library code. In the old solve there is a flag to enable or disable simplify.

Is it possible instead to invert powers using a two argument log, like log(32, 2)?

Copy link
Member Author

Choose a reason for hiding this comment

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

@asmeurer it seems that two argument log does not work well with imageset. I tried

>>> g = FiniteSet(32)
>>> imageset(Lambda(n, log(n, 2)), g)
{log(32)/log(2)}

assert solveset(e3, x, S.Reals) == S.EmptySet
assert solveset(e4, x, S.Reals) == FiniteSet(0)
assert solveset(e5, x, S.Reals) == FiniteSet(1)
assert solveset(e6, x, S.Reals) == FiniteSet(0)
assert simplify(solveset(e7, x, S.Reals)) == FiniteSet(2)
assert solveset(e8, x, S.Reals) == FiniteSet(2)
assert solveset(f1, x, S.Reals) == Intersection(S.Reals, FiniteSet(y*log(2*exp(z/y))))
assert solveset(f2, x, S.Reals) == S.EmptySet


def test_expo_conditionset():
from sympy.abc import x, y

f1 = (exp(x) + 1)**x - 2
f2 = (x + 2)**y*x - 3

assert solveset(f1, x, S.Reals) == ConditionSet(x, Eq(log(x) - log(-exp(x) - 1), 0), S.Reals)
assert solveset(f2, x ,S.Reals) == ConditionSet(x, Eq(x*(x + 2)**y - 3, 0), S.Reals)

@XFAIL
def test_expo_fail():
from sympy.abc import x, y, z, a, b

assert solveset(z**x - y, x, S.Reals) == Intersection(S.Reals, FiniteSet(log(y)/log(z)))
assert solveset(y - a*x**b, x) == FiniteSet((y/a)**(1/b))

w = symbols('w', integer=True)
f1 = 2*x**w - 4*y**w
f2 = (x/y)**w - 2
ans1 = solveset(f1, w, S.Reals)
ans2 = solveset(f2, w, S.Reals)
assert ans1 == ans2