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

log solver for transolve #14792

Merged
merged 28 commits into from Aug 10, 2018

Conversation

Projects
None yet
6 participants
@Yathartha22
Contributor

Yathartha22 commented Jun 11, 2018

This PR is an extension to #14736 and is built over its branch.

The PR aims to implement log solver as part of transolve.

TODO's:

  • Documentation for _solve_ and _is_ routines for log

  • Tests for the helpers

  • Improvement in _solve_ routine to handle corner cases.

Release Notes

  • solvers
    • added a new solver for logarithmic equations as part of _transolve in solvers.solveset
@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Jun 11, 2018

I am not sure if this is the correct way to get some features from a different PR that this branch is dependent on.

ping @aktech @smichr @asmeurer

@smichr

This comment has been minimized.

Member

smichr commented Jun 12, 2018

I am not sure if this is the correct way to get some features from a different PR that this branch is dependent on.

If the other branch is very stable, this is ok...but I have often run into rebase issues when doing this.

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Jun 12, 2018

If the other branch is very stable, this is ok...but I have often run into rebase issues when doing this.

I guess it won't be stable, it will have few commits more. I will give it a try though otherwise I will wait for main branch to get merge and add a new PR.

new_f = new_lhs - rhs
result = S.EmptySet
solutions = _solveset(new_f, symbol, domain)

This comment has been minimized.

@smichr

smichr Jun 26, 2018

Member

I believe you do not want to do this. You want to do the solution and domain checking all in one place, not duplicate it in every helper for every type of equation.

return new_f
def _is_logarithmic(f, symbol):

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 7, 2018

Contributor

Will be adding documentation in the next commit.

Logic has been changed to check each term of the expression of log form rather than previously used logcombine method:

  • using logcombine can be risky as it may manipulate the equation like log(x) - log(2*x) would reduce to log(1/2) and the routine would return False as there will be no symbol.
for expr_arg in expr_args:
if check_log(expr_arg, symbol):
return True
if isinstance(expr_arg, Pow):

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 7, 2018

Contributor

This is for expressions like log(x)**2.

@@ -975,6 +982,438 @@ def _solveset(f, symbol, domain, _check=False):
if isinstance(s, RootOf)
or domain_check(fx, symbol, s)])
if domain.is_subset(S.Reals) and _is_logarithmic(f, symbol):

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 7, 2018

Contributor

This check is done so as to remove unwanted solutions form the original result.
This method is implemented rather than using checksol because there are some unwanted solution that would still creep into the actual result.

Take for example log(3*x) - log(-x + 1) - log(4*x + 1), the result in general is -1/2 and 1/2 but the negative solution is only valid if we consier complex valued logairthm but since we are concerned with the Real domain as of now only natural logairthm will work which is undefined for negative as well as for 0 therefore -1/2 can't be the solution in real doamin.
If the result is verified using checksol the subsitution would cause the cancelling of I*pi and hence it would return True, making -1/2 as the solution

In this method each solution is tried in each term of the expression, if its result is not in a real form then it is not the solution (This is specifically beneficial for checking for logarithmic expressions).

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Jul 7, 2018

I will add the remaining tests as well as the documentation once we agree on the current changes.

ping @aktech @smichr

return result
def log_singularities(f, symbol, result, domain):

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 11, 2018

Contributor

Will be adding docstring in the next commit.

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Jul 17, 2018

@smichr @aktech I have started a discussion to arrive at a conclusion for logarithmic conclusion. Have a look.

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Jul 18, 2018

  • I have removed checking log singularities as of now unless we get to a conclusion here. This needs more discussion.

  • I have included the use of flag variable (and this time it is explicitly declared). Some equations that do not get solved by exponential or logarithmic solver but are still one of these (eg: equations solvable as lambert) can get into a recursion.

ping @aktech @smichr

@Yathartha22 Yathartha22 changed the title from [WIP]: log solver for transolve to log solver for transolve Jul 18, 2018

Returns
=======
An improved equation containg a single instance of log.

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

->containing (sp)

return True
expr_args = _term_factors(f)
for expr_arg in expr_args:

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

suggest combining into for expr_arg in _term_factors(f):

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

Just used to improve readability. One could at once get what is returned from _term_factors. Should I change it?

expr_args = _term_factors(f)
for expr_arg in expr_args:
if check_log(expr_arg, symbol):

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

is the helper necessary? if isinstance(expr_arg, log) and symbol in expr_arg.free_symbols: will be less than 72 characters.

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

oh...you use it again below. Then how about prefacing with

base = expr_arg.base if isinstance(expr_arg, Pow) else expr_arg
if isinstance(base, log) and symbol in base.free_symbols:
    return True

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

This can be done. Thanks!

def _is_logarithmic(f, symbol):
r"""
Helper to check if the given equation is logarithmic or not.

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

Like exponential equations, you should describe the definition of being logarithmic.

rhs = rhs_s.args[0]
new_f = None
new_lhs = logcombine(lhs, force=True)

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

I am nervous about using force here. I think this will allow solutions which may not be valid and which will not pass solution checking with True or False. solveset should never give a solution that is unknown to be a solution.

The docstring of logcombine indicates that log(x) + log(y) == log(x*y) if both are not negative.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

Yeah you are right but it seems to me using force=True is a necessity, otherwise equations won't get simplified like take for eg: log(x-3) + log(x+3), even if x is assumed to be positive using just logcombine would not simplify it to log((x-3)*(x+3)) because arguments x - 3 and x + 3 canot be assumed to be positive.
One thing that I think can be done is to use force=True and later we should check if this has imparted any kind of bad solutions (may be we can use checksol). Another thing which can be done is to modify the assumptions of the free_symbols internally (I guess it won't be the optimal way)
I used checksol before but found a problem in it (as mentioned in the google groups discussion), I am not sure as of now that is why I have not included any type of check that would remove bad solutions. What are your thoughts on this?

This comment has been minimized.

@smichr

smichr Jul 19, 2018

Member

if x is positive, x+3 must also be positive

>>> var('x', positive=True)
x
>>> (x+3).is_positive
True
>>> (x-3).is_positive
>>>

I don't recall why logcombine would require that both arguments must be positive. It seems that if one of them is positive and the other negative that the same result will be obtained: log(neg)+log(pos) = log(neg*pos). The set output of solveset will allow you to add this as a constraint on the solution, however.

If checksol is used to provide the solution, then there will be cases where you won't be able to determine if a solution satisfies the original equation...and then the objective of solveset will have been lost, viz. to provide solutions for which the original expression is zero.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

It seems that if one of them is positive and the other negative that the same result will be obtained: log(neg)+log(pos) = log(neg*pos)

This result is valid if arguments are numbers, this is because log(neg) = log(|neg|) + I*pi

>>> f = log(2) + log(-3)       # log(2) + log(3) + I*pi
>>>  logcombine(f)
log(6) + I*pi

>>> x = symbols('x', positive=True); y = symbols('y', negative=True)
>>> f = log(x) + log(y)
>>> logcombine(f)
log(x) + log(y)
def test_logarithmic():
assert solveset_real(log(x - 3) + log(x + 3), x) == FiniteSet(
sqrt(10))
-sqrt(10), sqrt(10))

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

Is this like the removable singularity case? IF this is rewritten as log((x - 3)*(x + 3)) then see -sqrt(10) is a solution. But if it's not rewritten like this then substituting in the -sqrt(10) gives an I*pi from each term and these don't cancel.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

Is this like the removable singularity case?

Yes, it is.
In my opinion -sqrt(10) should not be a solution as substituting will cause log(x - 3) and log(x + 3) to have negative arguments.
As mentioned here I am not sure as of now what definition should be considered to remove spurious solutions.

  • Should we substitute every solution in each of the log argument and check if it makes it non-real (as it is mentioned in the third example here), or

  • Substitute directly each solution to the original function and see if we get 0 (this can be done using checksol). But using checksol would sometime cancel out I*pi terms which may give wrong solution, like in log(3*x) - log(1 - x) - log(4*x + 1)

This comment has been minimized.

@smichr

smichr Jul 19, 2018

Member

You can return ConditionSet(x, Or(x+3 >= 0, x - 3 >=0), {sqrt(10), -sqrt(10)}) -> sqrt(10), i.e. keep track of conditions for solution. Again, I think that all but one arg must be positive in order for the combined logarithm to be valid. That would make a more complicated condition but at least it is expressible. For 3 args, Or(And(arg1>=0, arg2>=0), And(arg3>=0, arg2>=0), And(arg1>=0, arg3>=0)). This could be constructed with subsets.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

Yeah I guess using ConditionSet seems to be handy. I will have a look at it.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

Can we simply use something like: ConditionSet(x, And(arg1>=0, arg2>=0, arg3>=0), {solutions}) ? where arg1, arg2, arg3 ... will be arguments of every log term in the equation
This works for the following:

# log(x - 3) + log(x + 3)
>>> ConditionSet(x, And(x + 3 >= 0, x - 3 >=0), {sqrt(10), -sqrt(10)})
{sqrt(10)}

# log(3*x) - log(1 - x) - log(4*x + 1)
>>> ConditionSet(x, And(3*x>=0, 1 - x>=0, 4*x + 1>=0), {S(1)/2, -S(1)/2})
{1/2}

This comment has been minimized.

@smichr

smichr Jul 19, 2018

Member

What about log(1-x) - log(3*x) - log(4*x + 1) = 0 for which x = -1/2 is a solution which makes only two args 0?

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 20, 2018

Contributor

What about log(1-x) - log(3x) - log(4x + 1) = 0 for which x = -1/2 is a solution which makes only two args 0?

Wolfram alpha gives 1/6 as the only solution to this equation. It rules out -1/2 maybe because -1/2 makes log(3*x) and log(4*x + 1) negative.

To see we are on the same page what should be followed while removing bad solutions.

  • Any of the solution makes any of the log arg negative should not be included, or

  • Substituting each solution to the original equation and see it evaluates to 0.

I think wolfram alpha follows the first criteria, as evident from these two equations:
log(3x) - log(1 - x) - log(4x + 1) and log(1-x) - log(3x) - log(4x + 1)

The ConditionSet case I gave above is valid considering the first case. What do you suggest?

>>> ConditionSet(x, And(1 - x >0, 3*x > 0, 4*x + 1), {-1/2, 1/6})
1/6

This comment has been minimized.

@smichr

smichr Jul 20, 2018

Member

Although log(a) + log(b) = log(a*b) when either a or b is positive, log(a) - log(b) is only log(a/b)when both are positive. Andlog(a) + log(b) + log(c)will only belog(abc)when all three arguments are positive or else there would be an extra multiple ofI*pi` in the former case.

A solution to log(a) + log(b) = 0 will only be found when both are positive. If one or both are negative there will be a non-cancelling imaginary part. The same applies to the case of 3 terms: if ever any of them is negative there will be a non-cancelling I*pi in the numerator or denominator that will make the solution not be zero.

So, lacking a counterexample, it would seem that your simpler criteria that all args be positive is valid.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 22, 2018

Contributor

Talking about removing unwanted solutions @jksuom suggested that we can allow solutions which we get by rewriting an equation in its equivalent form. Taking an example: sin(x)/x = 1, x can have 0 as the solution because we can write the original equation to sin(x) = x
I assume we can follow this interpretation while solving logarithmic equation

This comment has been minimized.

@smichr

smichr Jul 22, 2018

Member

I thought this was a case of a removable singularity. If division by 0 is not defined then sin(x)/x is undefined even though the limit as x -> 0 is defined.

assert _is_logarithmic(log(x), x) is True
assert _is_logarithmic(log(x)*log(y), x) is True
assert _is_logarithmic(log(x)**2, x) is True
assert _is_logarithmic(log(x**y) - y*log(x), x) is True

This comment has been minimized.

@smichr

smichr Jul 18, 2018

Member

here, logcombine for a*log(x) == log(x**a) is only valid "if x is positive and a is real"...so if these conditions are not met, the solution will not be valid.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

Yeah the assumptions need to be taken care of, but it would still be a logarithmic equation irrespective of the assumption of the free_symbols

This comment has been minimized.

@smichr

smichr Jul 19, 2018

Member

It depends on your definition of logarithmic, right? If being logarithmic means being able to rewrite as a single log then assumptions matter. (That's why I said that a good definition is needed in the docstring.) I don't see a point in saying something is logarithmic if it can't be rearranged into a form suitable for solving...so that's my bias in terms of what the routine should say. But if you are going to always use force and return conditions for the coefficients and arguments of log then I guess the current beahvior is ok.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 19, 2018

Contributor

I will be preferring this definition:

Logarithmic equation is an equation that involves the logarithm of an expression containing a variable.

Is it fine?

I intend to make the routine generic. If the equation has any log term containing the variable to be solved it is logarithmic equation.

This comment has been minimized.

@smichr

smichr Jul 19, 2018

Member

It's too vague...sin(log(x)) = 0 is not logarithmic. You should be answering True if the equation is likely to benefit from rewriting via logcombine. That an equation might be logarithmic at some point of inversion doesn't mean that it is always logarithmic. Another example is log(x)**2 - 2*log(x) - 1 which is polynomial with log(x) the generator.

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 20, 2018

Contributor

You should be answering True if the equation is likely to benefit from rewriting via logcombine

I see your point, but using logcombine can give sometime wrong answer. See for eg: log(x) + log(2/x), reduces to log(2), if I simply check that the equation gets reduced then the routine will return True for cases involving constants, like log(2) + log(3) will also give True, so to avoid this if I add a check to see symbol in .args.free_symbols then the first equation would return False, because it reduces to a constant.

Another example is log(x)**2 - 2*log(x) - 1 which is polynomial with log(x) the generator

For this example, correct me if I interpret logarithmic equation wrong, even though it is polynomial in nature it still can be said as logarithmic since log has x in its arguments. Also once we get the solutions of the polynomial t**2 - 2*t - 1 we need to solve log(x) - t and this is again logarithmic equation.

This comment has been minimized.

@smichr

smichr Jul 20, 2018

Member

Yes. I am thinking of analyzing the equation in such a way as to know what the next step is in solving the equation. x*log(x) - 3 will be logarithmic with your definition but is actually a Lambert form. Will your code allow this to pass into lambert testing or will it, after getting a ConditionSet solution from _solve_log, return as unsolved?

This comment has been minimized.

@Yathartha22

Yathartha22 Jul 20, 2018

Contributor

Will your code allow this to pass into lambert testing or will it, after getting a ConditionSet solution from _solve_log, return as unsolved?

In this branch, it would return a ConditionSet, but as soon as I add lambert solving (in another PR) the equations that both _solve_log and _solve_expo can't solve will be passed into lambert solving. I don't think we will be having identification for lambert because they don't take any specific form, so we will directly solve those equations considering only possible way left.

x*log(x) - 3 will be logarithmic with your definition but is actually a Lambert form

Yeah you are right but similar thing would happen with _is_exponential() too. For exp(x) + x the routine would return True but it is actually a lambert form.

This comment has been minimized.

@smichr

smichr Jul 21, 2018

Member

I think the definitions should be more precisely tuned so you don't have this ambiguity.

Regarding Lambert forms...they don't have a single specific form but I recall going through some pains to elaborate and identify such equations in whatever form they came in the work that I did with solve.

made log identifier to identify only equations reducable to single lo…
…g form; improved naming convention of the helpers; added tests; minor chnages int the documentation
assert solveset_real(a**x - b**x, x) == ConditionSet(
x, Eq(a**x - b**x, 0), S.Reals)
# a, b, x, y = symbols('a b x y')
# assert solveset_real(a**x - b**x, x) == ConditionSet(

This comment has been minimized.

@Yathartha22

Yathartha22 Aug 5, 2018

Contributor

Can't figure out why this test gives RecursionError. Everything else seems to work fine

>>> x, y, a, b = symbols('x y a b')
>>> f = a**x - b**(x + 1)
>>> solveset(f, x, S.Reals)
ConditionSet(x, Eq(a**x - b**(x + 1), 0), Reals)

@jksuom can you help to point out the cause for recursion error. It seems something is going wrong with Piecewise here for this very equation (a**x - b**x)

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 5, 2018

Have a look now @smichr

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 6, 2018

Seems that calling _solveset(...) within Piecewise is not good enough, because irrespective of the condition the function is invoked. (I have asked about this scenario in gitter)
I guess using if won't make a difference.

ping @aktech @smichr

@smichr

This comment has been minimized.

Member

smichr commented Aug 6, 2018

My main concern is that if there are conditions that are neither false nor true -- just undecided -- I would like to see the solution and the conditions come back, not a generic Eq(foo, 0). Then a person doesn't have to go through the solving process when parameters are ready to be substituted. Give the maximum information for their query. (I'll see what I can make of the Piecewise problem.)

@smichr

This comment has been minimized.

Member

smichr commented Aug 7, 2018

This will handle the recursion error:

old = [log(a), log(-b)]
new = list(expand_log, old))
if new == old:
    return unsolved_result
return _solveset(new[0] - new[1], symbol, domain)

Nothing in solveset is ready to handle Piecwise solutions. It expects sets to be returned. That being the case, I don't know how the solution with conditions can be returned. You might as well just return functions (instead of solutions) from the helpers. In this case, new[0] - new[1].

One possible use of conditions, however, would be to return EmptySet if the conditions are False.

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 7, 2018

That being the case, I don't know how the solution with conditions can be returned.

IMO we can do this in two ways:

  • Raise a ValueError, with a message like: "Proper assumptions needs to be givien to solve the given type of exponential equation "
  • Force the equation to get solved but return a ConditionSet (a different than ususal) with the required assumption, like:
    ConditionSet(x, And(a_base>0, b_base>0, Eq(im(a_exp), 0), Eq((im(b_exp), 0))), {solutions})

for equation a**x - b**x (vanilla), we can have
ConditionSet(x, And(a>0, b>0, Eq(im(x), 0), Eq((im(x), 0))), {0})

One possible use of conditions, however, would be to return EmptySet if the conditions are False.

Returning EmptySet could be misleading in a way as it would be pointing out to two things: either x doesn't have a solution in the respective domain or the assumptions are invalid.

@smichr

This comment has been minimized.

Member

smichr commented Aug 7, 2018

doesn't have a solution in the respective domain or the assumptions are invalid.

In either case it seems the appropriate answer is EmptySet. e.g. that there is no answer in a domain doesn't mean there isn't an answer. If the user gives a domain and we know the answer is not in that domain then the empty set is appropriate:

>>> solveset(x**3+8,x,Interval(0,oo))
EmptySet()

Regarding the condition set return value, I think you are right. What about:

return `Intersection(ConditionSet(...), domain)`
@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 7, 2018

return Intersection(ConditionSet(...), domain)

Will Intersection make any difference? because I think solutions will be intersected with the domain by _solveset itself

return unsolved_result
return ConditionSet(symbol, conditions, solutions)

This comment has been minimized.

@Yathartha22

Yathartha22 Aug 7, 2018

Contributor

Just one query about this scenario. Forcing the equation to be solved prompts solveset to return equation as an intersection with the domain.
For eg: if I force the following equation to get solved

>>> a, b, x, y = symbols('a b x y')
>>> solveset(a**x - b**(x + 1), x, S.Reals)
Intersection(Reals, {log(b)/(log(a) - log(b))})

This is something similar to what we get in logarithmic eqautions. So is returning this kind of ConditionSet still appropriate or should we try force solving the equation (to get something as above)?

@smichr

This comment has been minimized.

Member

smichr commented Aug 8, 2018

I think solutions will be intersected with the domain by _solveset itself

If this is the case then passing domain to the helpers is not necessary. Once can helper-solve in the complex domain and _solveset will apply the limiting domain in the intersection.(?)

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 8, 2018

Once can helper-solve in the complex domain and _solveset will apply the limiting domain in the intersection.(?)

But still, we will be needing to pass the "domain" parameter in _solveset (S.Complexes is not the default argument for domain).

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 8, 2018

@smichr @aktech I am hoping to get the PR merge before this week ends (last week for GSoC). It would great that we make it ready by then. I can open another PR for any other related issues (that is not breaking this PR)

@aktech

This comment has been minimized.

Member

aktech commented Aug 8, 2018

@Yathartha22 I am fine with merging this as long as @smichr doesn't have any concerns.

@smichr

This comment has been minimized.

Member

smichr commented Aug 9, 2018

diff --git a/sympy/solvers/solveset.py b/sympy/solvers/solveset.py
index 83b6ffb..1626d8d 100644
--- a/sympy/solvers/solveset.py
+++ b/sympy/solvers/solveset.py
@@ -1113,10 +1113,9 @@ def _solve_exponential(lhs, rhs, symbol, domain):
         Eq(im(b_exp), 0)
         )
 
-    if conditions is S.true:
-        return _solveset(expand_log(log(a)) - expand_log(log(-b)), symbol, domain)
-
-    return unsolved_result
+    neweq = expand_log(log(a), force=True) - expand_log(log(-b), force=True)
+    soln = _solveset(neweq, symbol, domain)
+    return ConditionSet(symbol, conditions, soln)
 
 
 def _is_exponential(f, symbol):
@@ -1528,9 +1527,6 @@ def add_type(lhs, rhs, symbol, domain):
     else:
         result = rhs_s
 
-    if isinstance(result, ConditionSet):
-        result = unsolved_result
-
     return result
 
 
diff --git a/sympy/solvers/tests/test_solveset.py b/sympy/solvers/tests/test_solveset.py
index 37c9166..a6879cf 100644
--- a/sympy/solvers/tests/test_solveset.py
+++ b/sympy/solvers/tests/test_solveset.py
@@ -1867,8 +1867,10 @@ def test_exponential_symbols():
         (x - z)/log(2)) - FiniteSet(0)
 
     a, b, x, y = symbols('a b x y')
+    assert solveset(a**x - b**x, x) == ConditionSet(
+        x, (a > 0) & (b > 0) & Eq(im(x), 0), {0})
     assert solveset_real(a**x - b**x, x) == ConditionSet(
-        x, Eq(a**x - b**x, 0), S.Reals)
+        x, (a > 0) & (b > 0), {0})
 
 
 @XFAIL

The docstrings needs a touch up if you use this diff to reflect that ConditionSet may be returned if there are conditions that must be met for the solution to be valid. It seems a little ill-defined right now as to who decides there is no solution, and whether the difference between 'can't solve' and 'no solution' is clearly being made.

If used, a few tests need editing, too, to put the constant on the rhs of the Eq:

___________ sympy\solvers\tests\test_solveset.py:test_conditionset ____________
Traceback (most recent call last):
  File "c:\users\leslie\sympy\sympy\solvers\tests\test_solveset.py", line 954, i
n test_conditionset
    ) == ConditionSet(x, Eq(x**2 + x*sin(x) - 1, 0), S.Reals)
AssertionError
_______________________________________________________________________________
_________ sympy\solvers\tests\test_solveset.py:test_expo_conditionset _________
Traceback (most recent call last):
  File "c:\users\leslie\sympy\sympy\solvers\tests\test_solveset.py", line 1843,
in test_expo_conditionset
    x, Eq(2**x - exp(x) - 3, 0), S.Reals)
AssertionError
@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 9, 2018

Maybe we define the "conditions " for real domain only:

conditions=True
if domain.is_subset(Reals):
        conditions = And(
            a_base > 0,
            b_base > 0,
            Eq(im(a_exp), 0),
            Eq(im(b_exp), 0))

and solveset(a**x - b**x, x, S.Complexes) should return simply {0}.

Note: Even though doing this we will be getting the solutions in Complex domain as well (but they won't be general ones). I would rather make a separate PR for solving exponential equations in complex domain (genral solutions)

who decides there is no solution, and whether the difference between 'can't solve' and 'no solution' is clearly be made.

I guess _solveset takes care of this

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 9, 2018

if used, a few tests need editing, too, to put the constant on the rhs of the Eq:

@smichr I have already made the changes in the latest commit below this comment. I guess the branch you are using is not updated.

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 9, 2018

This will be the behaviour

>>> f = a**x - b**x
>>> solveset(f, x, S.Reals)
ConditionSet(x, (a > 0) & (b > 0), {0})
>>> solveset(f, x, S.Complexes)
{0}
>>> a, b = symbols('a b', positive=True)
>>> solveset(f, x, S.Reals)
{0}
@smichr

This comment has been minimized.

Member

smichr commented Aug 9, 2018

This will be the behaviour

There are still assumptions when the domain is complex:

>>> q=a**x+b**(x+1)
>>> solveset(q,x)
{(log(b) + I*pi)/(log(a) - log(b))}

We should require that a and b be nonzero and that a not equal b: Ne(a, b), Ne(a, 0), Ne(b, 0)

@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 9, 2018

Oh yes I missed this for complex domain. Thanks!
I guess Ne(a, b) can be used for real domain as well?
I see a problem with Ne(a, b) for equations having exp as their base: -9*exp(-2*x + 5) + 4*exp(3*x + 1). Should we leave it?

@smichr

This comment has been minimized.

Member

smichr commented Aug 9, 2018

Please sharpen a pencil and check out the assertions in the following code comments:

diff --git a/sympy/solvers/solveset.py b/sympy/solvers/solveset.py
index 5ac6781..15f4849 100644
--- a/sympy/solvers/solveset.py
+++ b/sympy/solvers/solveset.py
@@ -13,7 +13,7 @@
 from __future__ import print_function, division
 
 from sympy.core.sympify import sympify
-from sympy.core import (S, Pow, Dummy, pi, Expr, Wild, Mul, Equality,
+from sympy.core import (S, Pow, Dummy, pi, Expr, Wild, Mul,
                         Add)
 from sympy.core.containers import Tuple
 from sympy.core.facts import InconsistentAssumptions
@@ -27,10 +27,11 @@
 from sympy.functions import (log, Abs, tan, cot, sin, cos, sec, csc, exp,
                              acos, asin, acsc, asec, arg,
                              piecewise_fold, Piecewise)
+from sympy.functions.elementary.complexes import sign, im
 from sympy.functions.elementary.trigonometric import (TrigonometricFunction,
                                                       HyperbolicFunction)
 from sympy.functions.elementary.miscellaneous import real_root
-from sympy.logic.boolalg import And
+from sympy.logic.boolalg import And, ITE
 from sympy.sets import (FiniteSet, EmptySet, imageset, Interval, Intersection,
                         Union, ConditionSet, ImageSet, Complement, Contains)
 from sympy.sets.sets import Set
@@ -246,10 +247,14 @@ def _invert_real(f, g_ys, symbol):
                 s, b = integer_log(rhs, base)
                 if b:
                     return _invert_real(expo, FiniteSet(s), symbol)
-            elif rhs is S.One:
-                # special case: 0**x - 1
-                return (expo, FiniteSet(0))
-            return (expo, S.EmptySet)
+            elif base.is_zero:
+                one = Eq(rhs, 1)
+                if one == True:
+                    # special case: 0**x - 1
+                    return (expo, FiniteSet(0))
+                elif one == False:
+                    return (expo, S.EmptySet)
+            return (f, g_ys)
 
 
     if isinstance(f, TrigonometricFunction):
@@ -877,7 +882,7 @@ def _solveset(f, symbol, domain, _check=False):
     inverter = lambda f, rhs, symbol: _invert(f, rhs, symbol, domain)
 
     result = EmptySet()
-
+    print(f)
     if f.expand().is_zero:
         return domain
     elif not f.has(symbol):
@@ -1104,27 +1109,64 @@ def _solve_exponential(lhs, rhs, symbol, domain):
         return unsolved_result
 
     a, b = list(ordered(lhs.args))
-    a_term = a.as_independent(symbol)[1]
-    b_term = b.as_independent(symbol)[1]
+    a_co, a_term = a.as_independent(symbol)
+    b_co, b_term = b.as_independent(symbol)
+
+    # if we don't know if the equation really is two args then
+    # we can't return a solution as though it were and there
+    # is no way to represent a Piecewise solution in solveset
+    # so we have to return an unsolved result
+    if And(Ne(a_co, 0), Ne(b_co, 0)) != True:
+        # indeterminate:
+        # if a==0 or c==0, the solution of a*b**f(x) + c*d**g(x)
+        # is empty set if d or b (respectively) is not zero else
+        # if a==0 and d==0 then g(x)=1 or if c==0 and b==0 then
+        # f(x) == 1; if a and c are 0 the solution is the domain
+        return unsolved_result
 
     a_base, a_exp = a_term.base, a_term.exp
     b_base, b_exp = b_term.base, b_term.exp
 
-    from sympy.functions.elementary.complexes import im
+    if ITE(
+            Eq(a_base, b_base),
+            Ne(a_exp, b_exp),
+            ITE(
+                Eq(a_exp, b_exp),
+                Ne(a_base, b_base),
+                Ne(a, b))) != True:
+        # indeterminate:
+        # if bases are equal there are two possible solutions:
+        #  if(b=d=B) then a*b**f(x) + c*d**g(x)
+        #  is a*B**f(x) + c*B**g(x); if the exponents are the same, f=g=h, then
+        #  the solution is given by B**h(x)*(a + c); if B is zero then the solution
+        #  is h(x) != 0 else the solution is the empty set.
+        # If the exponents are the same, there are 3 possible solutions
+        #  if f=g=h, then we have a*b**h(x)+c*d**h(x). The case of b==d
+        #  was handled above and there are two cases: h(x) != 0 or the empty set.
+        # If b!=d then log(a) + h(x)*log(b) - log(-c) - h(x)*log(d)
+        # which gives log(a/-c) + h(x)*log(b/d) and h(x) = -log(a/-c)/log(b/d)
+        return unsolved_result
 
+    L, R = map(lambda i: expand_log(log(i), force=True), (a, -b))
     if domain.is_subset(S.Reals):
         conditions = And(
+            Ne(sign(a_co), sign(b_co)),
             a_base > 0,
             b_base > 0,
             Eq(im(a_exp), 0),
-            Eq(im(b_exp), 0))
+            Eq(im(b_exp), 0),
+            )
+        solutions = _solveset(L - R, symbol,
+            # when domain is Reals, conditions will take care
+            # of keeping the solution Real, otherwise we
+            # need the clipped solution from solveset
+            S.Complexes if domain is S.Reals else domain)
     else:
         conditions = And(
             Ne(a_base, 0),
-            Ne(b_base, 0))
-
-    log_type_equation = expand_log(log(a), force=True) - expand_log(log(-b), force=True)
-    solutions = _solveset(log_type_equation, symbol, domain)
+            Ne(b_base, 0),
+            )
+        solutions = _solveset(L - R, symbol, domain)
 
     return ConditionSet(symbol, conditions, solutions)
 
@@ -1884,7 +1926,7 @@ def linear_eq_to_matrix(equations, *symbols):
 
     for equation in equations:
         f = sympify(equation)
-        if isinstance(f, Equality):
+        if isinstance(f, Eq):
             f = f.lhs - f.rhs
 
         # Extract coeff of symbols
@smichr

This comment has been minimized.

Member

smichr commented Aug 9, 2018

The comments are not "production ready" but I have to leave this to you while I end my own week's break and finish things delayed.

@smichr

This comment has been minimized.

Member

smichr commented Aug 10, 2018

I opened an issue for the mishandling of a single exponential term, so that doesn't have to be fixed. I think this routine is giving an answer in cases where there is not enough information to return a Set solution, but that can be fixed later. The main thing is that the structure is in place. I think the GSOC has a category for "future improvements". So there's no reason to delay this PR.

Congrats. I will be checking during breaks today to see if there is anything else you need reviewed.

@smichr smichr merged commit bcdc254 into sympy:master Aug 10, 2018

2 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
sympy-bot/release-notes The release notes look OK
Details
@Yathartha22

This comment has been minimized.

Contributor

Yathartha22 commented Aug 10, 2018

Well, thanks @smichr for the merge and all the reviews :).

I was actually working on diff that you suggested.
Few points on it:

  1. The generalization of the equations was great but what I deduced (and correct me if I am wrong) that these types are not necessaary to be handled here, because they can be handled in solveset (specially coefficient being 0 and the base being 0 cases).
  2. condition like Ne(sign(a_co), sign(b_co) can be included in conditions.
  3. The use of map (a better pythonic way)

For points 2, 3 and the issue you created, I can create a seperate PR addressing all these points.
Anything else that needs to be fixed from this branch?

I will be checking during breaks today to see if there is anything else you need reviewed.

Next aim would be adding a lambert solver #14972. I will rebase it with the current master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment