From a9e0028597fabe457bbe12449ef464c7f0af3630 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 30 Sep 2026 05:24:51 +0000 Subject: [PATCH 1/2] An equation the numerical search cannot compile is left unsolved, not thrown out of Solve floor(x) = x/2 + 1/3 and max(x, 1) = 2x threw UncompilableNodeException out of Solve, in 2.5.0 as on master. Nothing analytical reads them, and the last resort, Newton's method, compiles the expression and its derivative; the compiler has no form for floor, nor for the unevaluated derivative(max(x, 1), x) that max differentiates to, nor for ceil, round, min, the error functions, the exponential and trigonometric integrals, sums and products. The compiler now says what it compiles: every node type whose CompileNode compiles rather than throws declares CompilesItself beside it, and HasCompiledForm asks that of every node. Newton's method gains TrySolveNt, which returns null where SolveNt throws, checked on exactly the two expressions it would compile; Solve's fallback asks it and leaves the equation unsolved. SolveNt, asked for by name, still throws, since the caller is owed the reason. No exception is caught: the check comes first. Closes #1603. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 14 ++++ .../Functions/Compilation/IntoFE/Compiler.cs | 68 +++++++++++++++++++ .../AnalyticalEquationSolver.cs | 8 ++- .../Solvers/NumericalSolving/NewtonSolver.cs | 23 ++++++- .../Algebra/SolveTest/NewtonFallbackTest.cs | 49 +++++++++++++ 5 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2af804431..2b1aaacf9 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2245,6 +2245,20 @@ before: `(x^2 - 1)/(x - 1) - x - 1` is zero for every `x` but 1, where `CC` woul | `"sqrt(x)^2 = x".ToEntity().Solve("x")` | `{ -10, -49/5, -48/5, …, -8 - 8i, … }`, 191 numbers | `CC` | | `"sin(x)^2 + cos(x)^2 = 1".ToEntity().Solve("x")` | the same 191 numbers | `CC` | +### An equation the numerical search cannot compile is left unsolved, not thrown out of `Solve` + +`floor(x) = x/2 + 1/3` and `max(x, 0) = x` threw `UncompilableNodeException` out of `Solve`. Nothing +analytical reads them, and the last resort, Newton's method, compiles the expression and its +derivative, where the compiler has no form for `floor`, nor for the unevaluated +`derivative(max(x, 0), x)`. The search now asks first whether the compiler has a form for every node, +and declines where it has not. `SolveNt`, which asks for Newton's method by name, still throws +([#1603](https://github.com/asc-community/AngouriMath/issues/1603)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"floor(x) = x/2 + 1/3".ToEntity().Solve("x")` | `UncompilableNodeException` | `{ x : floor(x) = x / 2 + 1/3 }` | +| `"max(x, 0) = x".ToEntity().Solve("x")` | `UncompilableNodeException` | `{ x : max(x, 0) = x }` | + ### An equation the solver cannot invert is left unsolved, not answered with no roots `x! = 6` was answered `{ }`, a claim that it has no roots, and it has 3. The solver isolates `x` diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index ab99829a1..d65fb83e5 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -13,6 +13,22 @@ namespace AngouriMath { partial record Entity { + /// + /// Whether this node, its children aside, has a compiled form: false by default, as + /// 's default is to throw, and true beside every override that + /// compiles rather than throws. + /// + private protected virtual bool CompilesItself => false; + + /// + /// Whether the compiler has a form for every node of this expression, so that a caller + /// that would fall back rather than fail can ask first instead of catching + /// . The variables are not checked against a + /// list: which ones an expression is compiled over is the caller's to say. + /// https://github.com/asc-community/AngouriMath/issues/1603 + /// + internal bool HasCompiledForm => CompilesItself && DirectChildren.All(child => child.HasCompiledForm); + private protected virtual void CompileNode(Compiler compiler) => throw new UncompilableNodeException( $"`{Stringize()}`, a node of type {GetType()}, does not support compilation. " @@ -38,12 +54,16 @@ internal void InnerCompile(Compiler compiler) public partial record Number { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) => compiler.Instructions.Add(new(InstructionType.PUSH_CONST, Value: ((Complex)this).ToNumerics())); } public partial record Variable { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) => compiler.Instructions.Add(new(InstructionType.PUSH_VAR, compiler.VarNamespace.TryGetValue(this, out var slot) @@ -65,6 +85,8 @@ public override int GetHashCode() public partial record Constant { + private protected override bool CompilesItself => true; + // A constant is its value. The named ones are substituted by value before // compiling; the base of ln and exp is Euler's number standing in the operator's // definition, which substitution leaves alone (#994), so it arrives here. @@ -82,6 +104,8 @@ private protected override void CompileNode(Compiler compiler) => // Note: We pop values when executing instructions, so we add instructions in reverse child order public partial record Sumf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Addend.InnerCompile(compiler); @@ -92,6 +116,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Minusf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Subtrahend.InnerCompile(compiler); @@ -102,6 +128,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Mulf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Multiplicand.InnerCompile(compiler); @@ -112,6 +140,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Divf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Divisor.InnerCompile(compiler); @@ -122,6 +152,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Modf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Divisor.InnerCompile(compiler); @@ -132,6 +164,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Powf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Exponent.InnerCompile(compiler); @@ -142,6 +176,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Sinf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -151,6 +187,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Cosf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -160,6 +198,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Secantf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -169,6 +209,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Cosecantf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -178,6 +220,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Tanf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -187,6 +231,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Cotanf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -196,6 +242,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Logf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { // Unlike AngouriMath which accepts Base as the first parameter, @@ -209,6 +257,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Arcsinf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -218,6 +268,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Arccosf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -227,6 +279,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Arctanf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -236,6 +290,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Arccotanf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -245,6 +301,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Arcsecantf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -254,6 +312,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Arccosecantf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -263,6 +323,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Factorialf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -290,6 +352,8 @@ private protected override void CompileNode(Compiler compiler) => public partial record Signumf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -299,6 +363,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Absf { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -308,6 +374,8 @@ private protected override void CompileNode(Compiler compiler) public partial record Phif { + private protected override bool CompilesItself => true; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs index a4282ea4b..acc8eeb5d 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs @@ -385,9 +385,13 @@ bool ProductIsDefinedAt(Entity root) // nothing to find: Newton's method is started from finitely many points inside // a bounded region, so an empty result is a fact about the search rather than // about the equation. Where it does find roots the answer is theirs, unchanged. + // And one the compiler has no form for -- `floor(x)`, or the derivative `max(x, 1)` + // has -- is not the search's to run, and is left unsolved rather than thrown out of + // `Solve`. https://github.com/asc-community/AngouriMath/issues/1603 if (MathS.Settings.AllowNewton && expr.Vars.Count == 1) - return expr.SolveNt(x).Select(ent => TryDowncast(expr, x, ent)).ToSet() - is FiniteSet { IsSetEmpty: false } found ? found : Unsolved(expr, x); + return NumericalSolving.NewtonSolver.TrySolveNt(expr, x, MathS.Settings.NewtonSolver) is { } roots + && roots.Select(ent => TryDowncast(expr, x, ent)).ToSet() is FiniteSet { IsSetEmpty: false } found + ? found : Unsolved(expr, x); return Unsolved(expr, x); } diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs index c2f2a7d43..45ec33a38 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs @@ -26,6 +26,21 @@ internal static class NewtonSolver /// How many approximations we need to do before we reach the most precise result. /// internal static HashSet SolveNt(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings) + => Search(expr, v, settings, declineWhatDoesNotCompile: false)!; + + /// + /// for a + /// caller that falls back rather than fails: where the expression + /// or its derivative holds a node the compiler has no form for, where that method throws + /// . `floor(x)` has none, and + /// neither has the unevaluated `derivative(max(x, 1), x)` that `max` differentiates to, + /// and `Solve` threw for both from its last resort. + /// https://github.com/asc-community/AngouriMath/issues/1603 + /// + internal static HashSet? TrySolveNt(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings) + => Search(expr, v, settings, declineWhatDoesNotCompile: true); + + private static HashSet? Search(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings, bool declineWhatDoesNotCompile) { // Perform one iteration of searching for a root with Newton-Raphson method static Complex NewtonIter(FastExpression f, FastExpression df, NumericsComplex value, int precision) @@ -56,8 +71,12 @@ NumericsComplex ChooseGood() => using var _ = MathS.Settings.FloatToRationalIterCount.Set(0); var res = new HashSet(); - var df = WithoutConditions(expr.Differentiate(v).Simplify()).Compile(v); - var f = WithoutConditions(expr.Simplify()).Compile(v); + var derivative = WithoutConditions(expr.Differentiate(v).Simplify()); + var function = WithoutConditions(expr.Simplify()); + if (declineWhatDoesNotCompile && !(derivative.HasCompiledForm && function.HasCompiledForm)) + return null; + var df = derivative.Compile(v); + var f = function.Compile(v); void IterateFrom(NumericsComplex start) { var root = NewtonIter(f, df, start, settings.Precision); diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs new file mode 100644 index 000000000..8cbc9ec64 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs @@ -0,0 +1,49 @@ +// +// 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.Core.Exceptions; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Algebra +{ + /// + /// Newton's method is the last resort of , and it compiles the + /// expression and its derivative. Where the compiler has no form for a node -- floor(x), + /// or the unevaluated derivative(max(x, 1), x) that max differentiates to -- + /// the solver threw out of the public method, in 2.5.0 + /// as on master. + /// #1603 + /// + [Trait("Area", "Algebra")] + public sealed class NewtonFallbackTest + { + /// No analytical route answers these, and the search cannot run on them. + [Theory] + [InlineData("floor(x) = x/2 + 1/3")] + [InlineData("ceil(x) = x/3 + 1/7")] + [InlineData("round(x) = x/2 + 1/5")] + [InlineData("max(x, 1) = 2*x")] + [InlineData("max(x, 0) = x")] + [InlineData("min(x, 2) = x^2 + 1/5")] + public void AnEquationTheCompilerHasNoFormForIsLeftUnsolved(string equation) + => Assert.IsType(equation.ToEntity().Solve("x")); + + /// One the compiler has a form for is still searched, and its roots kept. + [Fact] + public void AnEquationTheCompilerHasAFormForIsStillSearched() + => Assert.IsType("sin(x) = x/3".ToEntity().Solve("x")); + + /// + /// Asked for Newton's method by name, the caller is told why it cannot run, as before: + /// only the fallback declines. + /// + [Fact] + public void NewtonsMethodAskedByNameStillSaysWhy() + => Assert.Throws(() => "floor(x) - x/2 - 1/3".ToEntity().SolveNt("x")); + } +} From 6936253ef550e972084c8ed977f37a1c1f2c8b10 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 30 Sep 2026 06:15:50 +0000 Subject: [PATCH 2/2] The floors, the rounding and the extremes compile, and the search declines a continuum As suggested on #1605: floor, ceil, round, max and min get compiled forms, through Compile and Compile alike. They agree with the interpreter: componentwise on a complex number, round to even at a half, and no value for max or min of two numbers that differ and are not both real, which are not ordered. max and min also differentiate where their arguments are real, as |f| does: (u' + v' +- sgn(u - v)(u' - v'))/2, provided u - v is not 0. So Newton's method runs where it threw: floor(x) = x/2 + 1/3 is { 4/3 } and max(x, 1) = 2x is { 1/2 }. Compiling more lets the search reach equations that hold on a whole interval, and max(x, 0) = x came back as fifty-one points of [0, 10], the same wrong shape abs(x) - x = 0 has had. The solver's search keeps a set of roots only where they are isolated: where the equation also holds midway between two of them, the points are samples of a continuum, and the equation is left unsolved. SolveNt, asked for by name, still returns what it finds. What has no compiled form at all -- erf, Si -- is still declined. Closes #1420. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 30 ++-- .../Functions/Compilation/IntoFE/Compiler.cs | 60 ++++++++ .../Compilation/IntoFE/FastExpression.cs | 32 +++++ .../IntoLinq/CompilationProtocol.cs | 5 + .../Compilation/IntoLinq/MathAllMethods.cs | 70 +++++++++ .../Compilation/IntoLinq/MathAllMethods.tt | 44 +++++- .../Functions/Compilation/RealOnly.cs | 18 +++ .../Functions/Continuous/Differentiation.cs | 32 +++++ .../Solvers/NumericalSolving/NewtonSolver.cs | 47 ++++-- .../Algebra/SolveTest/NewtonFallbackTest.cs | 134 ++++++++++++++++-- Sources/Tests/UnitTests/Common/PublicApi.txt | 2 + .../UnitTests/Core/UserInvalidExceptions.cs | 6 +- 12 files changed, 442 insertions(+), 38 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2b1aaacf9..3294b7e6b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2245,19 +2245,33 @@ before: `(x^2 - 1)/(x - 1) - x - 1` is zero for every `x` but 1, where `CC` woul | `"sqrt(x)^2 = x".ToEntity().Solve("x")` | `{ -10, -49/5, -48/5, …, -8 - 8i, … }`, 191 numbers | `CC` | | `"sin(x)^2 + cos(x)^2 = 1".ToEntity().Solve("x")` | the same 191 numbers | `CC` | -### An equation the numerical search cannot compile is left unsolved, not thrown out of `Solve` +### The floors, the rounding and the extremes compile, and the solver's numerical search declines what it cannot represent -`floor(x) = x/2 + 1/3` and `max(x, 0) = x` threw `UncompilableNodeException` out of `Solve`. Nothing -analytical reads them, and the last resort, Newton's method, compiles the expression and its -derivative, where the compiler has no form for `floor`, nor for the unevaluated -`derivative(max(x, 0), x)`. The search now asks first whether the compiler has a form for every node, -and declines where it has not. `SolveNt`, which asks for Newton's method by name, still throws -([#1603](https://github.com/asc-community/AngouriMath/issues/1603)). +`floor`, `ceil`, `round`, `max` and `min` had no compiled form, and `max` and `min` had no +derivative. So `Compile` threw `UncompilableNodeException` on them, and so did `Solve`, whose last +resort, Newton's method, compiles the expression and its derivative. They compile now, through +`Compile` and `Compile` alike, and they agree with the interpreter: + +- `floor`, `ceil` and `round` are taken componentwise on a complex number, and `round` rounds a half + to even; +- `max` and `min` of two numbers that differ and are not both real have no value, since those are + not ordered; +- `max` and `min` differentiate where their arguments are real, as `|f|` does. + +The search still declines what has no compiled form, so `erf(x) = x/2 + 1/7` is left unsolved. It +now also declines a set of roots that are not isolated: where the equation holds midway between two +of them, the points are samples of a continuum and not the set +([#1603](https://github.com/asc-community/AngouriMath/issues/1603), +[#1420](https://github.com/asc-community/AngouriMath/issues/1420)). | Input | Was (2.5.0) | Now | |---|---|---| -| `"floor(x) = x/2 + 1/3".ToEntity().Solve("x")` | `UncompilableNodeException` | `{ x : floor(x) = x / 2 + 1/3 }` | +| `"floor(x) = x/2 + 1/3".ToEntity().Solve("x")` | `UncompilableNodeException` | `{ 1.333… }`: 4/3, by Newton's method | +| `"max(x, 1) = 2*x".ToEntity().Solve("x")` | `UncompilableNodeException` | `{ 1/2 }` | | `"max(x, 0) = x".ToEntity().Solve("x")` | `UncompilableNodeException` | `{ x : max(x, 0) = x }` | +| `"abs(x) - x = 0".ToEntity().Solve("x")` | `{ 0, 1/5, 2/5, …, 10 }`, 51 numbers | `{ x : abs(x) - x = 0 }` | +| `"floor(x)".Compile("x")`, and `ceil`, `round`, `max`, `min` | `UncompilableNodeException` | compiled | +| `"max(x, 1)".ToEntity().Differentiate("x")` | `derivative(max(x, 1), x)` | `(1 + sgn(x - 1)) / 2 provided not x - 1 = 0` | ### An equation the solver cannot invert is left unsolved, not answered with no roots diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index d65fb83e5..5fb13510d 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -350,6 +350,66 @@ private protected override void CompileNode(Compiler compiler) => throw new UncompilableNodeException($"A limit cannot be compiled: `{Stringize()}`"); } + // The floors, the rounding and the extremes, which the solver's numerical search needs + // compiled to run on `floor(x) = x/2 + 1/3` or `max(x, 1) = 2 x` at all. + // https://github.com/asc-community/AngouriMath/issues/1603 + public partial record Floorf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_FLOOR)); + } + } + + public partial record Ceilf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_CEIL)); + } + } + + public partial record Roundf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_ROUND)); + } + } + + public partial record Maxf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_MAX)); + } + } + + public partial record Minf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_MIN)); + } + } + public partial record Signumf { private protected override bool CompilesItself => true; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs index 7c2844721..32b4a0cde 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs @@ -37,6 +37,9 @@ internal enum InstructionType CALL_SIGNUM, CALL_ABS, CALL_PHI, + CALL_FLOOR, + CALL_CEIL, + CALL_ROUND, // 2-arg functions CALL_SUM = 100, @@ -46,6 +49,8 @@ internal enum InstructionType CALL_MOD, CALL_POW, CALL_LOG, + CALL_MAX, + CALL_MIN, } internal sealed partial record Instruction(InstructionType Type, int Reference = -1, System.Numerics.Complex Value = default) { @@ -145,6 +150,33 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value case InstructionType.CALL_MOD: stack.Push(Core.Compilation.RealOnly.Mod(stack.Pop(), stack.Pop())); break; + case InstructionType.CALL_MAX: + stack.Push(Core.Compilation.RealOnly.Max(stack.Pop(), stack.Pop())); + break; + case InstructionType.CALL_MIN: + stack.Push(Core.Compilation.RealOnly.Min(stack.Pop(), stack.Pop())); + break; + // Componentwise, as the interpreter takes them, and to even at a half for + // the rounding. + case InstructionType.CALL_FLOOR: + { + var z = stack.Pop(); + stack.Push(new System.Numerics.Complex(System.Math.Floor(z.Real), System.Math.Floor(z.Imaginary))); + break; + } + case InstructionType.CALL_CEIL: + { + var z = stack.Pop(); + stack.Push(new System.Numerics.Complex(System.Math.Ceiling(z.Real), System.Math.Ceiling(z.Imaginary))); + break; + } + case InstructionType.CALL_ROUND: + { + var z = stack.Pop(); + stack.Push(new System.Numerics.Complex( + System.Math.Round(z.Real, System.MidpointRounding.ToEven), System.Math.Round(z.Imaginary, System.MidpointRounding.ToEven))); + break; + } case InstructionType.CALL_POW: stack.Push(System.Numerics.Complex.Pow(stack.Pop(), stack.Pop())); break; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 1b8a27d18..2568988b4 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -134,6 +134,9 @@ Arccosecantf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef Absf => Expression.Call(GetDef("Abs", 1, e.Type), e), Signumf => Expression.Call(GetDef("Sgn", 1, e.Type), e), + Floorf => Expression.Call(GetDef("Floor", 1, e.Type), e), + Ceilf => Expression.Call(GetDef("Ceil", 1, e.Type), e), + Roundf => Expression.Call(GetDef("Round", 1, e.Type), e), Notf => Expression.Not(e), @@ -193,6 +196,8 @@ Logf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight => Expression.Call(GetDef("Log", 2, newRight.Type), newLeft, newRight), + Maxf => Expression.Call(GetDef("Max", 2, left.Type), left, right), + Minf => Expression.Call(GetDef("Min", 2, left.Type), left, right), Andf => Expression.And(left, right), Orf => Expression.Or(left, right), diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs index d95112125..0b687f76d 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs @@ -248,6 +248,46 @@ public static BigInteger Sgn(BigInteger a) return 0; } + // The floors and the rounding, componentwise on a complex number and to even at a half, + // as the interpreter takes them; an integer is its own. And the extremes, which have no + // value for two complex numbers that differ and are not both real, since those are not + // ordered. https://github.com/asc-community/AngouriMath/issues/1603 + public static System.Numerics.Complex Floor(System.Numerics.Complex a) => new(Math.Floor(a.Real), Math.Floor(a.Imaginary)); + public static double Floor(double a) => Math.Floor(a); + public static float Floor(float a) => (float)Math.Floor(a); + public static long Floor(long a) => a; + public static int Floor(int a) => a; + public static BigInteger Floor(BigInteger a) => a; + + public static System.Numerics.Complex Ceil(System.Numerics.Complex a) => new(Math.Ceiling(a.Real), Math.Ceiling(a.Imaginary)); + public static double Ceil(double a) => Math.Ceiling(a); + public static float Ceil(float a) => (float)Math.Ceiling(a); + public static long Ceil(long a) => a; + public static int Ceil(int a) => a; + public static BigInteger Ceil(BigInteger a) => a; + + public static System.Numerics.Complex Round(System.Numerics.Complex a) + => new(Math.Round(a.Real, MidpointRounding.ToEven), Math.Round(a.Imaginary, MidpointRounding.ToEven)); + public static double Round(double a) => Math.Round(a, MidpointRounding.ToEven); + public static float Round(float a) => (float)Math.Round(a, MidpointRounding.ToEven); + public static long Round(long a) => a; + public static int Round(int a) => a; + public static BigInteger Round(BigInteger a) => a; + + public static System.Numerics.Complex Max(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Core.Compilation.RealOnly.Max(a, b); + public static double Max(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Max(a, b); + public static float Max(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? float.NaN : Math.Max(a, b); + public static long Max(long a, long b) => Math.Max(a, b); + public static int Max(int a, int b) => Math.Max(a, b); + public static BigInteger Max(BigInteger a, BigInteger b) => BigInteger.Max(a, b); + + public static System.Numerics.Complex Min(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Core.Compilation.RealOnly.Min(a, b); + public static double Min(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Min(a, b); + public static float Min(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? float.NaN : Math.Min(a, b); + public static long Min(long a, long b) => Math.Min(a, b); + public static int Min(int a, int b) => Math.Min(a, b); + public static BigInteger Min(BigInteger a, BigInteger b) => BigInteger.Min(a, b); + /// /// True where System.Numerics.Complex.IsNaN is, spelled out because that overload is /// .NET 7 and later and this assembly also targets netstandard2.0. A complex number is NaN @@ -284,6 +324,18 @@ public static bool IsNaN(System.Numerics.Complex a) { ("Pow", 2, typeof(long)), Def((a, b) => Pow(a, b)) }, { ("Pow", 2, typeof(int)), Def((a, b) => Pow(a, b)) }, { ("Pow", 2, typeof(BigInteger)), Def((a, b) => Pow(a, b)) }, + { ("Max", 2, typeof(System.Numerics.Complex)), Def((a, b) => Max(a, b)) }, + { ("Max", 2, typeof(double)), Def((a, b) => Max(a, b)) }, + { ("Max", 2, typeof(float)), Def((a, b) => Max(a, b)) }, + { ("Max", 2, typeof(long)), Def((a, b) => Max(a, b)) }, + { ("Max", 2, typeof(int)), Def((a, b) => Max(a, b)) }, + { ("Max", 2, typeof(BigInteger)), Def((a, b) => Max(a, b)) }, + { ("Min", 2, typeof(System.Numerics.Complex)), Def((a, b) => Min(a, b)) }, + { ("Min", 2, typeof(double)), Def((a, b) => Min(a, b)) }, + { ("Min", 2, typeof(float)), Def((a, b) => Min(a, b)) }, + { ("Min", 2, typeof(long)), Def((a, b) => Min(a, b)) }, + { ("Min", 2, typeof(int)), Def((a, b) => Min(a, b)) }, + { ("Min", 2, typeof(BigInteger)), Def((a, b) => Min(a, b)) }, { ("Sin", 1, typeof(System.Numerics.Complex)), Def(a => Sin(a)) }, { ("Sin", 1, typeof(double)), Def(a => Sin(a)) }, { ("Sin", 1, typeof(float)), Def(a => Sin(a)) }, @@ -368,6 +420,24 @@ public static bool IsNaN(System.Numerics.Complex a) { ("Sgn", 1, typeof(long)), Def(a => Sgn(a)) }, { ("Sgn", 1, typeof(int)), Def(a => Sgn(a)) }, { ("Sgn", 1, typeof(BigInteger)), Def(a => Sgn(a)) }, + { ("Floor", 1, typeof(System.Numerics.Complex)), Def(a => Floor(a)) }, + { ("Floor", 1, typeof(double)), Def(a => Floor(a)) }, + { ("Floor", 1, typeof(float)), Def(a => Floor(a)) }, + { ("Floor", 1, typeof(long)), Def(a => Floor(a)) }, + { ("Floor", 1, typeof(int)), Def(a => Floor(a)) }, + { ("Floor", 1, typeof(BigInteger)), Def(a => Floor(a)) }, + { ("Ceil", 1, typeof(System.Numerics.Complex)), Def(a => Ceil(a)) }, + { ("Ceil", 1, typeof(double)), Def(a => Ceil(a)) }, + { ("Ceil", 1, typeof(float)), Def(a => Ceil(a)) }, + { ("Ceil", 1, typeof(long)), Def(a => Ceil(a)) }, + { ("Ceil", 1, typeof(int)), Def(a => Ceil(a)) }, + { ("Ceil", 1, typeof(BigInteger)), Def(a => Ceil(a)) }, + { ("Round", 1, typeof(System.Numerics.Complex)), Def(a => Round(a)) }, + { ("Round", 1, typeof(double)), Def(a => Round(a)) }, + { ("Round", 1, typeof(float)), Def(a => Round(a)) }, + { ("Round", 1, typeof(long)), Def(a => Round(a)) }, + { ("Round", 1, typeof(int)), Def(a => Round(a)) }, + { ("Round", 1, typeof(BigInteger)), Def(a => Round(a)) }, }; // An expression tree is how C# spells "the MethodInfo of this method" without a string: diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt index 581b632f2..f89bb3b67 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt @@ -7,8 +7,8 @@ // to one and forgotten in the other. var unary = new[]{ "Sin", "Cos", "Tan", "Asin", "Acos", "Atan", "Cot", "Sec", "Csc", "Acot", "Asec", "Acsc", - "Abs", "Sgn" }; - var binary = new[]{ "Log", "Pow" }; + "Abs", "Sgn", "Floor", "Ceil", "Round" }; + var binary = new[]{ "Log", "Pow", "Max", "Min" }; var types = new[]{ "System.Numerics.Complex", "double", "float", "long", "int", "BigInteger" }; #> // @@ -148,6 +148,46 @@ namespace AngouriMath.Core.Compilation.IntoLinq return 0; } + // The floors and the rounding, componentwise on a complex number and to even at a half, + // as the interpreter takes them; an integer is its own. And the extremes, which have no + // value for two complex numbers that differ and are not both real, since those are not + // ordered. https://github.com/asc-community/AngouriMath/issues/1603 + public static System.Numerics.Complex Floor(System.Numerics.Complex a) => new(Math.Floor(a.Real), Math.Floor(a.Imaginary)); + public static double Floor(double a) => Math.Floor(a); + public static float Floor(float a) => (float)Math.Floor(a); + public static long Floor(long a) => a; + public static int Floor(int a) => a; + public static BigInteger Floor(BigInteger a) => a; + + public static System.Numerics.Complex Ceil(System.Numerics.Complex a) => new(Math.Ceiling(a.Real), Math.Ceiling(a.Imaginary)); + public static double Ceil(double a) => Math.Ceiling(a); + public static float Ceil(float a) => (float)Math.Ceiling(a); + public static long Ceil(long a) => a; + public static int Ceil(int a) => a; + public static BigInteger Ceil(BigInteger a) => a; + + public static System.Numerics.Complex Round(System.Numerics.Complex a) + => new(Math.Round(a.Real, MidpointRounding.ToEven), Math.Round(a.Imaginary, MidpointRounding.ToEven)); + public static double Round(double a) => Math.Round(a, MidpointRounding.ToEven); + public static float Round(float a) => (float)Math.Round(a, MidpointRounding.ToEven); + public static long Round(long a) => a; + public static int Round(int a) => a; + public static BigInteger Round(BigInteger a) => a; + + public static System.Numerics.Complex Max(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Core.Compilation.RealOnly.Max(a, b); + public static double Max(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Max(a, b); + public static float Max(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? float.NaN : Math.Max(a, b); + public static long Max(long a, long b) => Math.Max(a, b); + public static int Max(int a, int b) => Math.Max(a, b); + public static BigInteger Max(BigInteger a, BigInteger b) => BigInteger.Max(a, b); + + public static System.Numerics.Complex Min(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Core.Compilation.RealOnly.Min(a, b); + public static double Min(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Min(a, b); + public static float Min(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? float.NaN : Math.Min(a, b); + public static long Min(long a, long b) => Math.Min(a, b); + public static int Min(int a, int b) => Math.Min(a, b); + public static BigInteger Min(BigInteger a, BigInteger b) => BigInteger.Min(a, b); + /// /// True where System.Numerics.Complex.IsNaN is, spelled out because that overload is /// .NET 7 and later and this assembly also targets netstandard2.0. A complex number is NaN diff --git a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs index 951cd6bd4..b9841f1dc 100644 --- a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs +++ b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs @@ -45,5 +45,23 @@ public static NumericsComplex Mod(NumericsComplex a, NumericsComplex b) : truncated + b.Real) + 0.0, 0); } + + /// + /// 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. + /// + public static NumericsComplex Max(NumericsComplex a, NumericsComplex b) + => Ordered(a, b, larger: true); + + /// The smaller of two numbers, on the terms of . + 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) + ? new NumericsComplex(double.NaN, double.NaN) + : new NumericsComplex(larger == a.Real >= b.Real ? a.Real : b.Real, 0); } } diff --git a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs index 8ab6e4315..5a6a72fd7 100644 --- a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs +++ b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs @@ -528,6 +528,38 @@ protected override Entity InnerDifferentiate(Variable variable) : MathS.Derivative(this, variable); } + partial record Maxf + { + // Where both are real, max(u, v) is (u + v + |u - v|)/2, so its derivative is + // (u' + v' + sgn(u - v)(u' - v'))/2 wherever the two differ, and there is none where + // they cross: the corner |t| has at 0. A comparison says nothing off the real line, so + // arguments not shown to be real are left unevaluated, as |f|' is. Without this the + // derivative was always left as written, and Newton's method, which compiles it, could + // not run on `max(x, 1) = 2 x`. https://github.com/asc-community/AngouriMath/issues/1603 + /// + protected override Entity InnerDifferentiate(Variable variable) + => IsConstantIn(variable) ? Integer.Zero + : TreeAnalyzer.IsRealValued(Left - Right, variable) + ? ((Left.InnerDifferentiate(variable) + Right.InnerDifferentiate(variable) + + MathS.Signum(Left - Right) * (Left.InnerDifferentiate(variable) - Right.InnerDifferentiate(variable))) / 2) + .Provided(!(Left - Right).EqualTo(Integer.Zero)) + : MathS.Derivative(this, variable); + } + + partial record Minf + { + // As max, with the sign of the difference the other way: min(u, v) is + // (u + v - |u - v|)/2 where both are real. + /// + protected override Entity InnerDifferentiate(Variable variable) + => IsConstantIn(variable) ? Integer.Zero + : TreeAnalyzer.IsRealValued(Left - Right, variable) + ? ((Left.InnerDifferentiate(variable) + Right.InnerDifferentiate(variable) + - MathS.Signum(Left - Right) * (Left.InnerDifferentiate(variable) - Right.InnerDifferentiate(variable))) / 2) + .Provided(!(Left - Right).EqualTo(Integer.Zero)) + : MathS.Derivative(this, variable); + } + partial record Floorf { // Flat between consecutive integers and discontinuous at each of them, so the diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs index 45ec33a38..72c218789 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs @@ -26,21 +26,21 @@ internal static class NewtonSolver /// How many approximations we need to do before we reach the most precise result. /// internal static HashSet SolveNt(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings) - => Search(expr, v, settings, declineWhatDoesNotCompile: false)!; + => Search(expr, v, settings, forTheSolver: false)!; /// - /// for a - /// caller that falls back rather than fails: where the expression + /// for the + /// solver's last resort, which answers a set: where the expression /// or its derivative holds a node the compiler has no form for, where that method throws - /// . `floor(x)` has none, and - /// neither has the unevaluated `derivative(max(x, 1), x)` that `max` differentiates to, - /// and `Solve` threw for both from its last resort. - /// https://github.com/asc-community/AngouriMath/issues/1603 + /// -- an unevaluated derivative, + /// integral or limit binds the variable and has no value at a point -- and where the + /// roots it finds are not isolated, so that they are samples of a continuum rather than + /// the set. https://github.com/asc-community/AngouriMath/issues/1603 /// internal static HashSet? TrySolveNt(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings) - => Search(expr, v, settings, declineWhatDoesNotCompile: true); + => Search(expr, v, settings, forTheSolver: true); - private static HashSet? Search(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings, bool declineWhatDoesNotCompile) + private static HashSet? Search(Entity expr, Entity.Variable v, MathS.Settings.NewtonSetting settings, bool forTheSolver) { // Perform one iteration of searching for a root with Newton-Raphson method static Complex NewtonIter(FastExpression f, FastExpression df, NumericsComplex value, int precision) @@ -73,7 +73,7 @@ NumericsComplex ChooseGood() => var res = new HashSet(); var derivative = WithoutConditions(expr.Differentiate(v).Simplify()); var function = WithoutConditions(expr.Simplify()); - if (declineWhatDoesNotCompile && !(derivative.HasCompiledForm && function.HasCompiledForm)) + if (forTheSolver && !(derivative.HasCompiledForm && function.HasCompiledForm)) return null; var df = derivative.Compile(v); var f = function.Compile(v); @@ -110,9 +110,36 @@ void IterateFrom(NumericsComplex start) // the values actually handed back. var distinct = OnePerRoot(WithoutIterationNoise(res), f); distinct.RemoveWhere(root => !Satisfies(expr, v, root)); + // A root the search finds is one of a finite set only where it is isolated. Where the + // equation holds midway between two of them as well, it holds along the segment, and + // the points the grid converged to are samples of the solutions and not the set: + // `abs(x) - x = 0` came back as fifty-one points of [0, 10], and `max(x, 0) = x`, once + // `max` compiled, did too. The solver leaves such an equation unsolved; asked for by + // name, the search still returns the points it found. + // https://github.com/asc-community/AngouriMath/issues/1420 + if (forTheSolver && !AreIsolated(distinct, f)) + return null; return distinct; } + /// + /// Whether no two of the real that are more than a thousandth + /// apart have the equation holding midway between them as well. + /// + private static bool AreIsolated(HashSet roots, FastExpression f) + { + var reals = roots + .Where(root => root.ImaginaryPart.IsZero) + .Select(root => root.RealPart.EDecimal.ToDouble()) + .OrderBy(root => root) + .ToList(); + for (var i = 1; i < reals.Count; i++) + if (reals[i] - reals[i - 1] > 1e-3 + && f.Call(new NumericsComplex((reals[i - 1] + reals[i]) / 2, 0)).ToNumber().Abs() < MathS.Settings.PrecisionErrorCommon.Value) + return false; + return true; + } + /// /// How much of the value at a real point may be imaginary before the expression /// is taken not to be real-valued there, relative to the real part's own size. diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs index 8cbc9ec64..d2631fac7 100644 --- a/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs @@ -5,6 +5,7 @@ // Website: https://am.angouri.org. // +using System.Linq; using AngouriMath.Core.Exceptions; using AngouriMath.Extensions; using Xunit; @@ -13,37 +14,142 @@ namespace AngouriMath.Tests.Algebra { /// /// Newton's method is the last resort of , and it compiles the - /// expression and its derivative. Where the compiler has no form for a node -- floor(x), - /// or the unevaluated derivative(max(x, 1), x) that max differentiates to -- - /// the solver threw out of the public method, in 2.5.0 - /// as on master. + /// expression and its derivative. Where the compiler had no form for a node -- floor(x), + /// or the unevaluated derivative(max(x, 1), x) that max differentiated to -- the + /// solver threw out of the public method, in 2.5.0 as on + /// master. The floors, the rounding and the extremes compile now, and max and min + /// differentiate; what still has no compiled form is declined, and so is a set of roots that + /// are not isolated. /// #1603 /// [Trait("Area", "Algebra")] public sealed class NewtonFallbackTest { - /// No analytical route answers these, and the search cannot run on them. + /// + /// Each root checked by hand: floor(x) = x/2 + 1/3 holds only at 4/3, where the + /// floor is 1; ceil(x) = x/3 + 1/7 only at -3/7; round(x) = x/2 + 1/5 only at + /// -2/5; min(x, 2) = x^2 + 1/5 at the two roots of x^2 - x + 1/5, both below 2. + /// [Theory] - [InlineData("floor(x) = x/2 + 1/3")] - [InlineData("ceil(x) = x/3 + 1/7")] - [InlineData("round(x) = x/2 + 1/5")] - [InlineData("max(x, 1) = 2*x")] - [InlineData("max(x, 0) = x")] - [InlineData("min(x, 2) = x^2 + 1/5")] + [InlineData("floor(x) = x/2 + 1/3", new[] { 4.0 / 3 })] + [InlineData("ceil(x) = x/3 + 1/7", new[] { -3.0 / 7 })] + [InlineData("round(x) = x/2 + 1/5", new[] { -2.0 / 5 })] + [InlineData("max(x, 1) = 2*x", new[] { 0.5 })] + [InlineData("min(x, 2) = x^2 + 1/5", new[] { 0.27639320225002103, 0.72360679774997897 })] + public void TheFloorsAndTheExtremesAreSearched(string equation, double[] expected) + { + var roots = Assert.IsType(equation.ToEntity().Solve("x")); + Assert.Equal(expected.Length, roots.Count); + foreach (var root in roots) + Assert.Contains(expected, e => System.Math.Abs(e - root.EvalNumerical().RealPart.EDecimal.ToDouble()) < 1e-9); + } + + /// No analytical route answers these, and the compiler still has no form for them. + [Theory] + [InlineData("erf(x) = x/2 + 1/7")] + [InlineData("Si(x) = x/2 + 1/7")] public void AnEquationTheCompilerHasNoFormForIsLeftUnsolved(string equation) => Assert.IsType(equation.ToEntity().Solve("x")); - /// One the compiler has a form for is still searched, and its roots kept. + /// + /// Each holds on the whole of [0, +oo), and the search's grid converged to fifty-one points + /// of [0, 10] as though they were the set. + /// #1420 + /// + [Theory] + [InlineData("abs(x) - x = 0")] + [InlineData("abs(x) = x")] + [InlineData("max(x, 0) = x")] + public void AContinuumIsLeftUnsolved(string equation) + => Assert.IsType(equation.ToEntity().Solve("x")); + + /// One the compiler has a form for is still searched, and its isolated roots kept. [Fact] public void AnEquationTheCompilerHasAFormForIsStillSearched() => Assert.IsType("sin(x) = x/3".ToEntity().Solve("x")); /// /// Asked for Newton's method by name, the caller is told why it cannot run, as before: - /// only the fallback declines. + /// only the solver's fallback declines. /// [Fact] public void NewtonsMethodAskedByNameStillSaysWhy() - => Assert.Throws(() => "floor(x) - x/2 - 1/3".ToEntity().SolveNt("x")); + => Assert.Throws(() => "erf(x) - x/2 - 1/7".ToEntity().SolveNt("x")); + + /// + /// The compiled floors, rounding and extremes against the interpreter, off the real line + /// too: the floors and the rounding are taken componentwise, and an extreme of two + /// numbers that are not both real has no value. + /// + [Theory] + [InlineData("floor(x)", 2.7, 0.5)] + [InlineData("floor(x)", -2.5, -1.25)] + [InlineData("ceil(x)", 2.2, -0.5)] + [InlineData("round(x)", 2.5, 3.5)] + [InlineData("round(x)", -1.5, 0.5)] + [InlineData("max(x, 1)", 0.5, 0)] + [InlineData("max(x, 1)", 3, 0)] + [InlineData("min(x, 1)", 0.5, 0)] + [InlineData("min(x^2, 2)", 3, 0)] + public void TheCompiledFormAgreesWithTheInterpreter(string expression, double re, double im) + { + var compiled = expression.ToEntity().Compile("x").Call(new System.Numerics.Complex(re, im)); + var interpreted = expression.ToEntity().Substitute("x", Entity.Number.Complex.Create(re, im)).EvalNumerical(); + Assert.Equal((double)interpreted.RealPart, compiled.Real, 12); + Assert.Equal((double)interpreted.ImaginaryPart, compiled.Imaginary, 12); + } + + /// + /// And through Compile<TIn, TOut>, the compiler to .NET expression trees, over + /// doubles and over complex numbers. + /// + [Theory] + [InlineData("floor(x)", 2.7)] + [InlineData("floor(x)", -2.5)] + [InlineData("ceil(x)", 2.2)] + [InlineData("round(x)", 2.5)] + [InlineData("round(x)", -1.5)] + [InlineData("max(x, 1)", 0.5)] + [InlineData("min(x^2, 2)", 3)] + public void TheExpressionTreeAgreesWithTheInterpreterOverDoubles(string expression, double at) + { + var compiled = expression.ToEntity().Compile("x")(at); + var interpreted = expression.ToEntity().Substitute("x", at).EvalNumerical(); + Assert.Equal((double)interpreted.RealPart, compiled, 12); + } + + [Theory] + [InlineData("floor(x)", 2.7, 0.5)] + [InlineData("ceil(x)", 2.2, -0.5)] + [InlineData("round(x)", 2.5, 3.5)] + [InlineData("max(x, 1)", 3, 0)] + public void TheExpressionTreeAgreesWithTheInterpreterOverComplexNumbers(string expression, double re, double im) + { + var compiled = expression.ToEntity().Compile("x")(new System.Numerics.Complex(re, im)); + var interpreted = expression.ToEntity().Substitute("x", Entity.Number.Complex.Create(re, im)).EvalNumerical(); + Assert.Equal((double)interpreted.RealPart, compiled.Real, 12); + Assert.Equal((double)interpreted.ImaginaryPart, compiled.Imaginary, 12); + } + + [Fact] + public void AnExtremeOfNumbersNotBothRealHasNoCompiledValue() + => Assert.True(double.IsNaN("max(x, 1)".ToEntity().Compile("x").Call(new System.Numerics.Complex(2, 1)).Real)); + + /// + /// max and min differentiate where their arguments are real, as |f| does; + /// off the real line the derivative is left as written. + /// + [Theory] + [InlineData("max(x, 1)", 3, 1)] + [InlineData("max(x, 1)", 0, 0)] + [InlineData("min(x, 1)", 3, 0)] + [InlineData("min(x, 1)", 0, 1)] + [InlineData("min(x^2, 2)", 1, 2)] + public void TheExtremesDifferentiate(string expression, double at, double slope) + => Assert.Equal(slope, (double)expression.ToEntity().Differentiate("x").Substitute("x", at).EvalNumerical().RealPart, 12); + + [Fact] + public void AnExtremeOffTheRealLineIsNotDifferentiated() + => Assert.Contains("derivative", "max(x, i)".ToEntity().Differentiate("x").Stringize()); } } diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt index 0284d33c0..d7302cae4 100644 --- a/Sources/Tests/UnitTests/Common/PublicApi.txt +++ b/Sources/Tests/UnitTests/Common/PublicApi.txt @@ -1470,6 +1470,7 @@ AngouriMath.Entity+Maxf.Equals(AngouriMath.Entity+Maxf) : System.Boolean AngouriMath.Entity+Maxf.Equals(System.Object) : System.Boolean AngouriMath.Entity+Maxf.GetHashCode() : System.Int32 AngouriMath.Entity+Maxf.InitDirectChildren() : AngouriMath.Entity[] +AngouriMath.Entity+Maxf.InnerDifferentiate(AngouriMath.Entity+Variable) : AngouriMath.Entity AngouriMath.Entity+Maxf.InnerSimplify(System.Boolean) : AngouriMath.Entity AngouriMath.Entity+Maxf.Left { } : AngouriMath.Entity AngouriMath.Entity+Maxf.NodeFirstChild { } : AngouriMath.Entity @@ -1509,6 +1510,7 @@ AngouriMath.Entity+Minf.Equals(AngouriMath.Entity+Minf) : System.Boolean AngouriMath.Entity+Minf.Equals(System.Object) : System.Boolean AngouriMath.Entity+Minf.GetHashCode() : System.Int32 AngouriMath.Entity+Minf.InitDirectChildren() : AngouriMath.Entity[] +AngouriMath.Entity+Minf.InnerDifferentiate(AngouriMath.Entity+Variable) : AngouriMath.Entity AngouriMath.Entity+Minf.InnerSimplify(System.Boolean) : AngouriMath.Entity AngouriMath.Entity+Minf.Left { } : AngouriMath.Entity AngouriMath.Entity+Minf.NodeFirstChild { } : AngouriMath.Entity diff --git a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs index c1ce1d562..33f140962 100644 --- a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs +++ b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs @@ -54,11 +54,9 @@ [Fact] public void UncompilableNode4() => // https://github.com/asc-community/AngouriMath/issues/894 // A node the Linq converter has no case for threw AngouriBugException, which asks the // caller to report a bug -- for a gap in coverage the library already knows about. Four - // of these are nodes 2.0 added and never taught the compiler. + // of these were nodes 2.0 added and never taught the compiler; floor, ceil and round have + // been taught since (https://github.com/asc-community/AngouriMath/issues/1603). [Theory] - [InlineData("floor(x)")] - [InlineData("ceil(x)")] - [InlineData("round(x)")] [InlineData("phi(x)")] [InlineData("gamma(x)")] [InlineData("x!")]