From 54176cae166fca3929f289f206bc6be3ede94dcb Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 1 Nov 2026 14:40:03 +0000 Subject: [PATCH] Compiled max and min have no value off the real line #2304 made max and min defined only where both arguments are real, so max(i, i) and max(x, x) at a complex x have no value. The compiled forms, FastExpression and LINQ over Complex, share RealOnly.Ordered, which returned an argument equal to the other before it asked whether either was real, as the interpreter once did. Compiled max(x, x) at 1/2 + 6/5 i was 1/2 + 6/5 i, and max(x, x) * 2 was 1 + 12/5 i, where evaluation says NaN. The check for a non-real argument now comes first. Found by work/compilesweep: 27 of its findings are max or min at a complex point. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 6 +++-- .../Functions/Compilation/RealOnly.cs | 12 ++++++---- .../Convenience/MaxMinOffTheRealLineTest.cs | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2977ffe9a..e11efd135 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1041,13 +1041,15 @@ list its poles beside the other points it has no value at, which says nothing ne **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)). +defined everywhere, and `max(x, x)` simplified to `x`, which has a value at `i`. Compiled by either compiler over +`Complex`, `max(x, x)` was `x` there too, and is `NaN`. 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` | +| `"max(x, x)".ToEntity().Compile("x").Call(new Complex(0.5, 1.2))` | `<0.5; 1.2>` | `` | ### Over the complex numbers, an equation in and out of a modulus is solved in the parts of `x` diff --git a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs index b9841f1dc..2e8542681 100644 --- a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs +++ b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs @@ -47,9 +47,11 @@ public static NumericsComplex Mod(NumericsComplex a, NumericsComplex b) } /// - /// The larger of two numbers, or NaN where they differ and are not both real: the complex - /// numbers are not ordered, and the interpreter leaves max(2, 1 + i) unevaluated. - /// Two equal arguments are their own maximum, which the interpreter says as well. + /// The larger of two numbers, or NaN where they are not both real: the complex numbers are + /// not ordered, and the interpreter leaves max(2, 1 + i) unevaluated and gives + /// max(i, i) no value. Two equal complex arguments had themselves for a maximum + /// here after the interpreter stopped saying so, and compiled max(x, x) was + /// x off the real line. https://github.com/asc-community/AngouriMath/issues/2256 /// public static NumericsComplex Max(NumericsComplex a, NumericsComplex b) => Ordered(a, b, larger: true); @@ -59,9 +61,9 @@ public static NumericsComplex Min(NumericsComplex a, NumericsComplex b) => Ordered(a, b, larger: false); private static NumericsComplex Ordered(NumericsComplex a, NumericsComplex b, bool larger) - => a == b ? a - : a.Imaginary is not 0 || b.Imaginary is not 0 || double.IsNaN(a.Real) || double.IsNaN(b.Real) + => a.Imaginary is not 0 || b.Imaginary is not 0 || double.IsNaN(a.Real) || double.IsNaN(b.Real) ? new NumericsComplex(double.NaN, double.NaN) + : a == b ? a : new NumericsComplex(larger == a.Real >= b.Real ? a.Real : b.Real, 0); } } diff --git a/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs b/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs index 7e69ac762..0b4793036 100644 --- a/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs +++ b/Sources/Tests/UnitTests/Convenience/MaxMinOffTheRealLineTest.cs @@ -52,5 +52,28 @@ public void ASimplificationHasNoValueWhereTheOriginalHasNone(string expression) Assert.True(simplified.Substitute("x", "i".ToEntity()).Evaled.IsNaN, $"{simplified} at i"); Assert.Equal(Entity.Number.Integer.Create(3), simplified.Substitute("x", 3).Evaled); } + + /// + /// Compiled, they have no value where evaluation has none. FastExpression and the LINQ + /// compiler over Complex answered max(x, x) at 1/2 + 6/5 i with + /// 1/2 + 6/5 i, and so max(x, x) * 2 with 1 + 12/5 i. + /// + [Theory] + [InlineData("max(x, x)")] + [InlineData("min(x, x)")] + [InlineData("max(x, x) * 2")] + public void CompiledItHasNoValueWhereEvaluationHasNone(string expression) + { + var entity = expression.ToEntity(); + var fastExpression = entity.Compile("x"); + var linq = entity.Compile("x"); + var offTheLine = new System.Numerics.Complex(0.5, 1.2); + Assert.True(double.IsNaN(fastExpression.Call(offTheLine).Real), $"FastExpression gives {fastExpression.Call(offTheLine)}"); + Assert.True(double.IsNaN(linq(offTheLine).Real), $"LINQ gives {linq(offTheLine)}"); + var onTheLine = new System.Numerics.Complex(0.5, 0); + var value = entity.Substitute("x", "1/2".ToEntity()).EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.Equal(value, fastExpression.Call(onTheLine).Real); + Assert.Equal(value, linq(onTheLine).Real); + } } }