From 36d95e6041baa7d724efab420145325af5972a92 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 17:05:48 +0000 Subject: [PATCH] max and min are defined where their arguments are real The complex numbers are not ordered, and max and min are read over the reals, so max(i, i) is NaN and max(i, 2) is left as written. Their condition was True, and max(x, x) simplified to x, which has a value at x = i. The condition is now that both arguments are real. The node's own fold of max(a, a) and min(a, a) to a did not consult the condition, and is now a provided a in RR, simplified in place: for a number that decides it, so max(i, i) stays NaN and max(3, 3) is 3, and for a symbol it is the condition. MaxMinOffTheRealLineTest takes the condition and the value at four points, max(i, i) and min(1 + i, 1 + i), and max(x, x) and min(x, x) simplified, at i and at 3. Four of the cases fail on master. A value where the condition fails is NaN, or the node left as written. RoundMinMaxGcdTest pinned min(x, x) as x, and now has x provided x in RR, with min(3, 3) beside it. Found by work/domaincheck. Part of #2256. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 12 ++++ Sources/.editorconfig | 3 + ...aluation.Continuous.Arithmetics.Classes.cs | 13 +++-- .../Convenience/MaxMinOffTheRealLineTest.cs | 56 +++++++++++++++++++ .../Convenience/RoundMinMaxGcdTest.cs | 7 ++- 5 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 1481f5f6a..634038644 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -628,6 +628,18 @@ conjugate as `0 - 1i`, in decimals, on the one instance that every `i` is, in ev |---|---|---| | `Complex.ImaginaryOne.Conjugate`, after `(1 / Complex.ImaginaryOne).Evaled` with `DowncastingEnabled` off | `0 - 1i` | `-i` | +### `max` and `min` are defined where their arguments are real + +**Silent: `max(x, x)` and `min(x, x)` simplify to `x provided x in RR`, where they were `x`.** The complex numbers are +not ordered, and both nodes are read over the reals, so `max(i, i)` has no value. Their condition said they were +defined everywhere, and `max(x, x)` simplified to `x`, which has a value at `i`. A comparison of two real arguments is +unchanged ([#2256](https://github.com/asc-community/AngouriMath/issues/2256)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"max(x, x)".ToEntity().Simplify()` | `x` | `x provided x in RR` | +| `"max(x, 2)".ToEntity().DomainCondition` | `True` | `x in RR` | + ### `conjugate`, `conj` and `arg` are functions **Not silent: these names followed by a bracket are now the complex conjugate and the principal argument, where they diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 059d1e5ff..5cbc5b29f 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -237,6 +237,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Convenience/BinomialTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Convenience/ConjugateAndArgumentTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index f8a8bc6fd..10cbe23cf 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -1327,7 +1327,10 @@ protected override Entity InnerSimplify(bool isExact) public partial record Minf { - private protected override Entity IntrinsicCondition => Boolean.True; + // Defined where both arguments are real: the complex numbers are not ordered, and the node is + // read over the reals, so min(i, i) is NaN. The condition said True, and min(x, x) simplified + // to x, which has a value at x = i. https://github.com/asc-community/AngouriMath/issues/2256 + private protected override Entity IntrinsicCondition => Left.In(MathS.Sets.R) & Right.In(MathS.Sets.R); /// protected override Entity InnerSimplify(bool isExact) @@ -1337,7 +1340,8 @@ protected override Entity InnerSimplify(bool isExact) // Only ordered arguments compare. A complex one is left alone rather // than guessed at, which is what SymPy's Min does too. (Real l, Real r) => l.EDecimal.CompareTo(r.EDecimal) <= 0 ? l : r, - var (l, r) when l == r => l, + // min(a, a) is a where a is real, and has no value where it is not. + var (l, r) when l == r => l.Provided(l.In(MathS.Sets.R)).InnerSimplified(isExact), _ => null }, (@this, a, b) => ((Minf)@this).New(a, b), isExact); @@ -1345,7 +1349,8 @@ protected override Entity InnerSimplify(bool isExact) public partial record Maxf { - private protected override Entity IntrinsicCondition => Boolean.True; + // As Minf's: defined where both arguments are real. https://github.com/asc-community/AngouriMath/issues/2256 + private protected override Entity IntrinsicCondition => Left.In(MathS.Sets.R) & Right.In(MathS.Sets.R); /// protected override Entity InnerSimplify(bool isExact) @@ -1353,7 +1358,7 @@ protected override Entity InnerSimplify(bool isExact) (a, b) => (a, b) switch { (Real l, Real r) => l.EDecimal.CompareTo(r.EDecimal) >= 0 ? l : r, - var (l, r) when l == r => l, + var (l, r) when l == r => l.Provided(l.In(MathS.Sets.R)).InnerSimplified(isExact), _ => null }, (@this, a, b) => ((Maxf)@this).New(a, b), isExact); diff --git a/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs b/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs new file mode 100644 index 000000000..7e69ac762 --- /dev/null +++ b/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Convenience +{ + /// + /// max and min are defined where both arguments are real, as their values are: the complex numbers are not + /// ordered, and max(i, i) is NaN. Their condition said True, so max(x, x) simplified to + /// x, which has a value at x = i. + /// #2256 + /// + [Trait("Area", "Convenience")] + public sealed class MaxMinOffTheRealLineTest + { + [Theory] + [InlineData("max(x, 2)", "i", false)] + [InlineData("min(x, 2)", "1/2 + 6/5 * i", false)] + [InlineData("max(x, 2)", "-3", true)] + [InlineData("min(x, x)", "pi", true)] + public void TheConditionIsWhereTheValueIs(string expression, string at, bool defined) + { + var condition = expression.ToEntity().DomainCondition.Substitute("x", at.ToEntity()).Evaled; + Assert.Equal(defined ? Entity.Boolean.True : Entity.Boolean.False, condition); + // Where it is not defined, evaluation gives NaN or leaves the node, as max(i, 2) is left: no number. + var value = expression.ToEntity().Substitute("x", at.ToEntity()).Evaled; + Assert.Equal(defined, value is Entity.Number && !value.IsNaN); + } + + [Theory] + [InlineData("max(i, i)")] + [InlineData("min(1 + i, 1 + i)")] + public void OfAComplexNumberWithItselfItHasNoValue(string expression) + { + Assert.True(expression.ToEntity().Evaled.IsNaN, $"{expression} evaluates to {expression.ToEntity().Evaled}"); + Assert.True(expression.ToEntity().Simplify().IsNaN, $"{expression} simplifies to {expression.ToEntity().Simplify()}"); + } + + [Theory] + [InlineData("max(x, x)")] + [InlineData("min(x, x)")] + public void ASimplificationHasNoValueWhereTheOriginalHasNone(string expression) + { + var simplified = expression.ToEntity().Simplify(); + Assert.True(simplified.Substitute("x", "i".ToEntity()).Evaled.IsNaN, $"{simplified} at i"); + Assert.Equal(Entity.Number.Integer.Create(3), simplified.Substitute("x", 3).Evaled); + } + } +} diff --git a/Sources/Tests/UnitTests/Convenience/RoundMinMaxGcdTest.cs b/Sources/Tests/UnitTests/Convenience/RoundMinMaxGcdTest.cs index f52e6f0d7..10d7eb41e 100644 --- a/Sources/Tests/UnitTests/Convenience/RoundMinMaxGcdTest.cs +++ b/Sources/Tests/UnitTests/Convenience/RoundMinMaxGcdTest.cs @@ -68,9 +68,12 @@ public void RoundIsNotFloorOfTheArgumentPlusAHalf(string at) public void MinAndMaxCompareWhereTheArgumentsAreOrdered(string input, int expected) => Assert.Equal(Entity.Number.Integer.Create(expected), input.ToEntity().Simplify()); + // That thing where it is real: the complex numbers are not ordered, and min(i, i) has no value. + // https://github.com/asc-community/AngouriMath/issues/2256 [Theory] - [InlineData("min(x, x)", "x")] - [InlineData("max(x, x)", "x")] + [InlineData("min(x, x)", "x provided x in RR")] + [InlineData("max(x, x)", "x provided x in RR")] + [InlineData("min(3, 3)", "3")] public void MinAndMaxOfOneThingWithItselfIsThatThing(string input, string expected) => Assert.Equal(expected.ToEntity(), input.ToEntity().Simplify());