diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2af804431..3294b7e6b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2245,6 +2245,34 @@ 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` | +### The floors, the rounding and the extremes compile, and the solver's numerical search declines what it cannot represent + +`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` | `{ 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 `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..5fb13510d 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); @@ -288,8 +350,70 @@ 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; + private protected override void CompileNode(Compiler compiler) { Argument.InnerCompile(compiler); @@ -299,6 +423,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 +434,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/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/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..72c218789 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, forTheSolver: false)!; + + /// + /// 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 + /// -- 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, forTheSolver: true); + + 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) @@ -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 (forTheSolver && !(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); @@ -91,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 new file mode 100644 index 000000000..d2631fac7 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs @@ -0,0 +1,155 @@ +// +// 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 System.Linq; +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 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 + { + /// + /// 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", 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")); + + /// + /// 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 solver's fallback declines. + /// + [Fact] + public void NewtonsMethodAskedByNameStillSaysWhy() + => 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!")]