built-in max understanding sympy.Expr

30 views
Skip to first unread message

Gábor Horváth

unread,
Sep 8, 2025, 6:17:15 PMSep 8
to sy...@googlegroups.com

Hi Team!

Opening a new topic for this.

I played around with sympy's Min/Max, and then once I accidentally
mistyped to use min/max, ie the built-in function. To my big surprise,
it didn't throw an error, but actually gave the correct answer:

>>> import sympy
>>> t = sympy.Symbol('t', nonnegative=True)
>>> max(t/(t+1), 1)
1

I started playing around with it, and apparently, it can even simplify in
some cases, but not in others:

>>> max(t/(t+1), t**2/(t*(t+1)))
t/(t + 1)
>>> max(t/(t+1), t**2/(t**2+t))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File
"/home/ghorvath/work/python_venv/lib/python3.9/site-packages/sympy/core/relational.py",
line 519, in __bool__
raise TypeError(
TypeError: cannot determine truth value of Relational: t**2/(t**2 + t) >
t/(t + 1)

Now, in the latter, sympy.Max actually works, even though it chooses the
'less simple' representation in my view:

>>> sympy.Max(t/(t+1), t**2/(t**2+t))
t**2/(t**2 + t)

And then there is the case, where both of them fails:

>>> max(t/(t+1), (t+1)/(t+2))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File
"/home/ghorvath/work/python_venv/lib/python3.9/site-packages/sympy/core/relational.py",
line 519, in __bool__
raise TypeError(
TypeError: cannot determine truth value of Relational: (t + 1)/(t + 2) >
t/(t + 1)
>>> sympy.Max(t/(t+1), (t+1)/(t+2))
Max(t/(t + 1), (t + 1)/(t + 2))

Well, technically sympy.Max doesn't fail, it just can't figure out without
help which is maximal out of the two. That said, with a bit of help, both
of them are able to find the correct answer:

>>> max(sympy.simplify(t/(t+1) - (t+1)/(t+2)), 0)
0
>>> sympy.Max(sympy.simplify(t/(t+1) - (t+1)/(t+2)), 0)
0

So I have a few questions here:

1a) How come the built-in max knows about sympy's expressions and symbol
assumptions? Is there some hidden place in sympy's code which overrides
the built-in behaviour of max? Or the built-in max is actually prepared
for sympy in some way? Or something else?
Hm, thinking about it from the error messages above, is the case that > is
overridden, or actually defined for sympy expressions, and the built-in
max is simply using that?

1b) If max is really using sympy's >, how come Max is superior over it?
(max fails on max(t/(t+1), t**2/(t**2+t)), but Max does not, so it must
be doing some extra over simply comparing by >)

2) Would it not make sense for sympy.Max (and correspondingly, Min),
that whenever it runs to a relational error on a>b, it would try to do
simplify(a-b)>0? Better yet, shouldn't it be somewhere in the relational
code? That is, if a>b throws a TypeError, then it tries to do
simplify(a-b)>0 before caving? If you think it makes sense, I'm happy to
open an issue on this. Especially if Max is already doing something extra
over just simply comparing via >, it might make sense to fall back on
simplify before throwing the TypeError?

Thanks,
Gábor

Gábor Horváth

unread,
Sep 8, 2025, 6:41:57 PMSep 8
to sy...@googlegroups.com

One more interesting thing I noticed:

>>> s = sympy.Symbol('s', positive=True, integer=True)
>>> max(s/(s+1), Fraction(1, 2))
s/(s + 1)
>>> sympy.Max(s/(s+1), Fraction(1, 2))
s/(s + 1)
>>>

So both max and Max knows without help that s/(s+1) has its minimum on the
integers at s==1, but it doesn't know that it's monotone, because it can't
decide between s/(s+1) and (s+1)/(s+2) (see examples in previous email).
So I'm really wondering here about the exact magic behind the relational
decision.

Thanks,
Gábor

Aaron Meurer

unread,
Sep 9, 2025, 12:16:58 PMSep 9
to sy...@googlegroups.com
The built-in max() is just comparing the objects using <. It works
because in some cases SymPy is able to compute the value of <. Python
lets objects override < but not max().

>>> t = sympy.Symbol('t', nonnegative=True)
>>> t/(t+1) < 1
True

The reason Max() is better is because it can remain symbolic. max()
cannot do that. It has to return one argument or the other. max(a, b)
is basically doing something like

if bool(a >= b):
return a
else:
return b

The bool() part in particular means that a >= b cannot be symbolic. It
has to evaluate to either True or False. This is why SymPy raises a
TypeError in the cases where it cannot be determined to be True or
False.

For what it's worth, I don't think it's good that the inequalities do
automatic simplification based on assumptions like this. I would
prefer to remove it, because it's an expensive operation to be done in
an automatic evaluation. It should be done only when calling
ask(t/(t+1) < 1) or something like that.

Similarly, if Max can be improved to compute things better that would
be good, but it would be better to put that code in doit() or
_eval_simplify() rather than happening automatically. The main issue I
see with your suggestion is how it would work when there are more than
just two arguments to Max. This also starts to get into questions
about inequalities and the assumptions in SymPy, which can be a deeper
rabbit hole than you might initially expect.

Aaron Meurer
> --
> You received this message because you are subscribed to the Google Groups "sympy" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to sympy+un...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/sympy/90a86786-fda8-15ad-dacb-3b5c3545a038%40gmail.com.

Gábor Horváth

unread,
Sep 9, 2025, 3:22:35 PMSep 9
to sy...@googlegroups.com

Hi Aaron,

Thank you for the reply, and for the explanation on max behaviour. It
makes full sense. I guess the interesting part is how much sympy can (or
does) evaluate >, how much simplification it is willing to do on its own,
before one needs to specifically ask for it with simplify or doit. I also
understand to not wanting to do these automatic evaluations as they can be
expensive. That is fine, but then where was the line drawn on when to
simplify and when not to?

I'm not sure I understand why multiple arguments poses a problem. Even in
the current implementation, the documentation says that Max does
evaluation on > as much as possible, and return Max(x_1, x_2, ...) where
x_1, x_2, ... are top elements in the poset defined by the partial
evaluation of >. The very same thing can be done regardless of how you
compute the poset of >, the difference is that in case instead of trying
to evaluate a > b, you do simplify(a-b) > 0, or something like that, then
your poset of > will possibly contain more comparisons.

So with this approach one can do a better evaluating Max with some
additional parameter. Like Max(expr_1, expr_2, .... , simplify=True) would
mean that instead of evaluating expr_i > expr_j, it would resort to
simplify(expr_i - expr_j) > 0. And default of simplify can be False, that
would not alter current behaviour.

Thanks,
Gábor
> To view this discussion visit https://groups.google.com/d/msgid/sympy/CAKgW%3D6%2BbbaUJH7U8Dj5kge4ELeTpSmQnV5ys-rHLvXoCqeMBrQ%40mail.gmail.com.
>

Aaron Meurer

unread,
Sep 10, 2025, 1:07:04 PMSep 10
to sy...@googlegroups.com
On Tue, Sep 9, 2025 at 1:22 PM Gábor Horváth <hungabo...@gmail.com> wrote:
>
>
> Hi Aaron,
>
> Thank you for the reply, and for the explanation on max behaviour. It
> makes full sense. I guess the interesting part is how much sympy can (or
> does) evaluate >, how much simplification it is willing to do on its own,
> before one needs to specifically ask for it with simplify or doit. I also
> understand to not wanting to do these automatic evaluations as they can be
> expensive. That is fine, but then where was the line drawn on when to
> simplify and when not to?

Right now SymPy doesn't have a clear line which is why it can be slow
sometimes. Ideally the line should be drawn that automatic
simplifications are not done if the require the assumptions system.

>
> I'm not sure I understand why multiple arguments poses a problem. Even in
> the current implementation, the documentation says that Max does
> evaluation on > as much as possible, and return Max(x_1, x_2, ...) where
> x_1, x_2, ... are top elements in the poset defined by the partial
> evaluation of >. The very same thing can be done regardless of how you
> compute the poset of >, the difference is that in case instead of trying
> to evaluate a > b, you do simplify(a-b) > 0, or something like that, then
> your poset of > will possibly contain more comparisons.

OK, so maybe Max is already handling this.

The real issue here is that the logic for this should be handled
separately from Max. The idea place that should be doing this logic is
ask(). But presently ask doesn't do very much with inequalities, which
is a major issue that we want to improve. Really Max() should just be
calling ask(a > b) instead of just checking if a > b auto-evaluates to
True or False. Then inside of ask(), there should be a handler (or a
theory solver, if it's sophisticated enough), that can handle these
kinds of nonlinear inequalities. Presently we only have a theory
solver for handling systems of linear inequalities. This may require
something like the CAD algorithm to handle in full generality,
although there might be simpler algorithms for handling expressions
with only one variable.

Aaron Meurer
> To view this discussion visit https://groups.google.com/d/msgid/sympy/1948c8be-e4ff-f6f3-36d1-48d5123015eb%40SamsungLaptop.WORKGROUP.
Reply all
Reply to author
Forward
0 new messages