From fea21de26b880421fb6438ad8cc9649b10c03bb9 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 1 Nov 2026 13:12:19 +0000 Subject: [PATCH 1/2] Over the complex numbers, an equation in and out of a modulus is solved in the parts of x #2307 left an equation with x inside abs, sgn, conj, Re, Im or arg and outside it too unsolved over the complex numbers, since Newton's method assumes a holomorphic function and its near-copies of a root came back as roots. Leaving it unsolved is not solving it. With x = a + b i and each |u| a real t >= 0 with t^2 = (Re u)^2 + (Im u)^2, the real and imaginary parts of the equation and one equation per modulus are a polynomial system in as many unknowns, which the Groebner solver answers exactly and completely or not at all. The roots are its solutions with a, b and every t real and no t negative, each put back into the equation as written, since clearing the denominators can add a pole. conj, Re and Im split directly. sgn(u) is u/|u| where u is not zero, and the case where it is zero is solved on its own. x^2/|1 - x^2| = x is { 0, (1 + sqrt(5))/2, (sqrt(5) - 1)/2 }, x |x| = 4 is { 2 }, x + sgn(x) = 3 is { 2 } again, and x^2 + 1 = |x| is +-i (sqrt(5) - 1)/2, which neither the search nor a split by the sign found. Left unsolved: infinitely many solutions (|x| = Re x), roots with no exact form (x^5 = |x| + 1), arg, and a system above 32 solutions by Bezout's bound, past which x^7 = |x| + 1 spent over 20 s declining. The route runs once, on the equation as asked and ahead of the spelling loop, whose spellings would each be the same system: asked of every one, x^5 = |x^3 + 1| + 1 ran it dozens of times. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 28 +- .../Polynomials/MultivariatePolynomial.cs | 12 + .../AnalyticalEquationSolver.cs | 14 +- .../Solvers/EquationSolver/ModulusSolver.cs | 320 +++++++++++++++++- .../Algebra/SolveTest/ModulusEquationTest.cs | 52 ++- 5 files changed, 400 insertions(+), 26 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 0af64e36f..2977ffe9a 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1049,20 +1049,26 @@ unchanged ([#2256](https://github.com/asc-community/AngouriMath/issues/2256)). | `"max(x, x)".ToEntity().Simplify()` | `x` | `x provided x in RR` | | `"max(x, 2)".ToEntity().DomainCondition` | `True` | `x in RR` | -### Over the complex numbers, an equation in and out of a modulus is left unsolved - -**Silent: an equation with `x` inside `abs`, `sgn`, `conj`, `Re`, `Im` or `arg` and outside it too is left unsolved -in the complex codomain, the default, where a numerical search answered it.** Newton's method assumes a holomorphic -function, and over the complex numbers `|u| = ±u` only where `u` is real, which a root need not make it, so neither -the search nor a split by the sign finds every root. The search's near-copies of a root came back as roots. Where it -happened to be right, as for `x * abs(x) = 4`, the answer is given up with the rest; over the reals each is still -solved by cases, exactly ([#2139](https://github.com/asc-community/AngouriMath/issues/2139)). +### Over the complex numbers, an equation in and out of a modulus is solved in the parts of `x` + +**Silent: an equation with `x` inside `abs`, `sgn`, `conj`, `Re` or `Im` and outside it too is solved exactly in the +complex codomain, the default, where a numerical search answered it in decimals with near-copies of a root that solve +nothing.** Newton's method assumes a holomorphic function, and over the complex numbers `|u| = ±u` only where `u` is +real, which a root need not make it, so a split by the sign misses roots too: `x^2 + 1 = abs(x)` is solved by +`±0.618… i`. With `x = a + b i` and each modulus a real `t ≥ 0` with `t^2 = (Re u)^2 + (Im u)^2`, the equation is a +system of polynomial equations in `a`, `b` and the `t`s, solved by a Gröbner basis, and its real solutions are the +roots; `sgn(u)` is `u / |u|` where `u` is not zero and `0` where it is, and both cases are solved. Where that system +has more than finitely many solutions, roots that are not found exactly, or more than 32 by Bézout's bound, the +equation is left unsolved, as it is with `arg` in it +([#2139](https://github.com/asc-community/AngouriMath/issues/2139)). | Input | Was (2.6.0) | Now | |---|---|---| -| `"x^2/abs(1-x^2) = x".ToEntity().Solve("x")` | `{ 0, 0.618…, 1.618…, 1.618… + 1.7E-9i, 1.618… + 9.9E-8i }` | `{ x : x ^ 2 / abs(1 - x ^ 2) = x }` | -| `"abs(x^2 - 4) = 3x".ToEntity().Solve("x")` | `{ 1, 4, 4.0000000000000062… + 1.6E-7i }` | `{ x : abs(x ^ 2 - 4) = 3 * x }` | -| `"x * abs(x) = 4".ToEntity().Solve("x")` | `{ 2 }` | `{ x : x * abs(x) = 4 }` | +| `"x^2/abs(1-x^2) = x".ToEntity().Solve("x")` | `{ 0, 0.618…, 1.618…, 1.618… + 1.7E-9i, 1.618… + 9.9E-8i }` | `{ 0, (1 + sqrt(5)) / 2, (sqrt(5) - 1) / 2 }` | +| `"abs(x^2 - 4) = 3x".ToEntity().Solve("x")` | `{ 1, 4, 4.0000000000000062… + 1.6E-7i }` | `{ 1, 4 }` | +| `"1/abs(x) = x^2".ToEntity().Solve("x")` | `{ -1, -0.999999… - 2.1E-7i, -0.999999… + 2.1E-7i, …, 1 }`, eight entries | `{ 1, -1 }` | +| `"x^2 + 1 = abs(x)".ToEntity().Solve("x")` | `{ x : x ^ 2 + 1 = abs(x) }` | `{ 1/2 * i * (sqrt(5) - 1), 1/2 * i * (1 - sqrt(5)) }` | +| `"x^5 = abs(x) + 1".ToEntity().Solve("x")` | `{ -0.944368… - 0.686124…i, …, 1.167303… }`, eight entries for five roots | `{ x : x ^ 5 = abs(x) + 1 }` | ### `conjugate`, `conj` and `arg` are functions diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/MultivariatePolynomial.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/MultivariatePolynomial.cs index 20bd57858..e4ba14342 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/MultivariatePolynomial.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/MultivariatePolynomial.cs @@ -147,6 +147,18 @@ internal int DegreeIn(int variable) return degree; } + /// The highest total degree of a term; zero for a constant. + internal int Degree + { + get + { + var degree = 0; + foreach (var monomial in terms.Keys) + degree = System.Math.Max(degree, TotalDegree(monomial)); + return degree; + } + } + /// The greatest monomial in lexicographic order; zero if there is none. private ulong LeadingMonomial() { diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs index d2225b5e7..3bc79a3cf 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs @@ -130,6 +130,16 @@ internal static Set Solve(Entity expr, Variable x, bool compensateSolving = fals if (MathS.Settings.Codomain.Value == Core.Domain.Real && ModulusSolver.SolveOverTheReals(expr, x) is { } overTheReals) return overTheReals; + // Over the complex numbers, one with x inside a modulus, a conjugate, a real or an + // imaginary part and outside it as well is solved in the real and imaginary parts of + // x, exactly: x |x| = 4 is { 2 }. Asked here, of the equation as it was asked, rather + // than of each spelling below, every one of which would be the same system to solve. + // https://github.com/asc-community/AngouriMath/issues/2139 + if (!compensateSolving && MathS.Settings.Codomain.Value != Core.Domain.Real + && ModulusSolver.IsInsideAndOutsideAFunctionThatIsNotHolomorphic(expr, x) + && ModulusSolver.SolveOverTheComplexNumbers(expr, x) is { } overTheComplexNumbers) + return overTheComplexNumbers; + // Whether a candidate root really is one. The loose tolerance that guesses the // rational must not also be what decides this: at 1e-7, x^41 + 6x + 1 accepted // -1/6, whose residual is -1.25e-32 -- small, but the difference between an @@ -395,8 +405,8 @@ bool ProductIsDefinedAt(Entity root) // Over the complex numbers, x inside a modulus or another function that is not // holomorphic and outside it as well is not the search's either: Newton's method // assumes a holomorphic function, and its near-copies of a root came back as roots. - // Left unsolved; over the reals it is solved by cases, above. - // https://github.com/asc-community/AngouriMath/issues/2139 + // Left unsolved where it is not solved in the parts of x, above; over the reals it is + // solved by cases. https://github.com/asc-community/AngouriMath/issues/2139 if (MathS.Settings.Codomain.Value != Core.Domain.Real && ModulusSolver.IsInsideAndOutsideAFunctionThatIsNotHolomorphic(expr, x)) return Unsolved(expr, x); diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs index 39c13636f..5ead14c39 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs @@ -14,8 +14,9 @@ namespace AngouriMath.Functions.Algebra.AnalyticalSolving { /// - /// Equations in moduli: where one is real for every complex x, and how one in moduli of - /// linear functions is solved over the reals. + /// Equations in moduli: where one is real for every complex x, how one in moduli of + /// polynomials is solved over the reals, and how one in and out of moduli, conjugates, real + /// and imaginary parts is solved over the complex numbers. /// /// /// Over the complex numbers |x - 2| = |x - 3| holds on the whole line @@ -185,5 +186,320 @@ private static bool RealWhereverItIs(Entity expr, Variable x) private static bool Within(Real value, Entity left, Entity right) => left.Evaled is Real low && right.Evaled is Real high && low.EDecimal.CompareTo(value.EDecimal) <= 0 && value.EDecimal.CompareTo(high.EDecimal) <= 0; + + /// + /// The complex at which is zero, where it is + /// made by arithmetic and whole powers from , rational numbers, and + /// moduli, signums, conjugates, real and imaginary parts of such expressions; + /// otherwise, or where the roots are not finitely many or not + /// found exactly. + /// + /// + /// + /// With x = a + b i for real a and b, and each modulus |u| a + /// real t, not negative, with t^2 = (Re u)^2 + (Im u)^2, the real and the + /// imaginary part of the equation and one equation for each modulus are polynomial + /// equations in as many unknowns, solved exactly by a Groebner basis. The roots are the + /// solutions with a, b and every t real and no t negative. + /// x |x| = 4 is a t = 4, b t = 0 and t^2 = a^2 + b^2, so + /// t = 2 and x = 2; and x^2 + 1 = |x| has the roots +-0.618 i, + /// which no split by the sign of x finds. + /// https://github.com/asc-community/AngouriMath/issues/2139 + /// + /// + /// A signum sgn(u) is u / |u| where u is not zero and 0 where + /// it is, so each is solved in both cases: x + sgn(x) = 3 is { 2 } where + /// x is not zero, and has no root where it is. An argument is not taken, since it + /// is not a polynomial in the parts. + /// + /// + internal static Set? SolveOverTheComplexNumbers(Entity expr, Variable x) + { + if (expr.Vars.Any(variable => variable != x)) + return null; + var signums = expr.Nodes.OfType().Select(signum => signum.Argument).Distinct().ToList(); + if (signums.Count > MostSignums) + return null; + var roots = new List(); + for (var zero = 0; zero < 1 << signums.Count; zero++) + { + var zeroed = signums.Where((_, i) => (zero >> i & 1) == 1).ToList(); + if (RootsWhereZero(expr, x, zeroed) is not { } found) + return null; + roots.AddRange(found); + } + return (Set)new FiniteSet(roots).InnerSimplified; + } + + /// The most signums an equation solved in the parts of x has, each doubling the systems to solve. + private const int MostSignums = 2; + + /// + /// The roots of at which every argument of a signum in + /// is zero and every other is not; where + /// they are not found. + /// + private static List? RootsWhereZero(Entity expr, Variable x, List zeroed) + { + var taken = new List { x }; + var a = Variable.CreateTemp(taken); + taken.Add(a); + var b = Variable.CreateTemp(taken); + taken.Add(b); + var splitter = new Splitter(x, a, b, taken); + var where = expr.Replace(node => node is Signumf(var argument) && zeroed.Contains(argument) ? Integer.Zero : node); + if (splitter.Split(where) is not { } parts) + return null; + var equations = new List(splitter.Equations) { parts.Real, parts.Imaginary }; + foreach (var argument in zeroed) + { + if (splitter.Split(argument) is not { } zero) + return null; + equations.Add(zero.Real); + equations.Add(zero.Imaginary); + } + var unknowns = new List(splitter.Moduli.Values) { b, a }; + if (!IsSmallEnoughToSolve(equations, unknowns)) + return null; + if (!Groebner.GroebnerSystemSolver.TrySolve(equations, unknowns, out var solutions)) + return null; + var roots = new List(); + if (solutions is null) + return roots; + var ofSignums = new HashSet(splitter.Signums.Select(argument => splitter.Moduli[argument])); + + for (var row = 0; row < solutions.RowCount; row++) + { + var kept = true; + for (var column = 0; column < unknowns.Count; column++) + { + if (!TryReadReal(solutions[row, column], out var value)) + return null; + // Each unknown is real, and a modulus is not negative, nor zero where it is the + // modulus a signum divides by. + if (value is null) + kept = false; + else if (column < unknowns.Count - 2 && (value.IsNegative || value.IsZero && ofSignums.Contains(unknowns[column]))) + { + if (!value.IsZero && !IsClearOfZero(value.EDecimal, value.EDecimal)) + return null; + kept = false; + } + } + if (!kept) + continue; + var real = solutions[row, unknowns.Count - 1]; + var imaginary = solutions[row, unknowns.Count - 2]; + var root = (imaginary.Evaled is Integer { IsZero: true } ? real : real + imaginary * Complex.ImaginaryOne).Simplify(); + // The system has the equation with its denominators cleared, so a root of it can + // be a pole of the equation as written, where it has no value. + switch (expr.Substitute(x, root).Evaled) + { + case Rational { IsZero: true }: + case Complex { IsFinite: true } residual when residual is not Rational && Number.IsZero(residual): + roots.Add(root); + break; + case Real { IsNaN: true }: + case Complex { IsFinite: false }: + break; + default: + return null; + } + } + return roots; + } + + /// The most solutions, counted by Bezout's bound, of a system solved for the parts of x. + /// + /// The system for x^2/|1 - x^2| = x has degrees 2, 2 and 4, a bound of 16, and is + /// solved in a tenth of a second; the one for x^5 = |x| + 1 has 5, 5 and 2, a + /// bound of 50, and spent 1.3 seconds before it was declined, and x^7 = |x| + 1 + /// more than twenty. + /// + private const int MostSolutions = 32; + + /// + /// Whether are polynomials over Q in + /// with a product of degrees no more than + /// . + /// + private static bool IsSmallEnoughToSolve(List equations, List unknowns) + { + var index = new Dictionary(); + for (var i = 0; i < unknowns.Count; i++) + index[unknowns[i]] = i; + var bound = 1; + foreach (var equation in equations) + { + if (Functions.MultivariatePolynomial.TryParse(equation, index) is not { } polynomial) + return false; + if (polynomial.IsZero) + continue; + bound *= polynomial.Degree; + if (bound > MostSolutions) + return false; + } + return true; + } + + /// + /// Reads as a real number, where it is not + /// one; where that is not decided, as where its imaginary part is + /// too small to be told from rounding. + /// + private static bool TryReadReal(Entity value, out Real? real) + { + real = null; + switch (value.Evaled) + { + case Real { IsFinite: true } number: + real = number; + return true; + case Complex { IsFinite: true } complex when complex.ImaginaryPart.IsZero: + real = complex.RealPart; + return true; + case Complex { IsFinite: true } complex: + return IsClearOfZero(complex.ImaginaryPart.EDecimal, complex.RealPart.EDecimal); + default: + return false; + } + } + + /// + /// Whether is too large, beside , to be + /// what rounding leaves of a zero. + /// + private static bool IsClearOfZero(PeterO.Numbers.EDecimal part, PeterO.Numbers.EDecimal scale) + => part.Abs().CompareTo(PeterO.Numbers.EDecimal.FromString("1E-30").Multiply(PeterO.Numbers.EDecimal.Max(PeterO.Numbers.EDecimal.One, scale.Abs()))) > 0; + + /// + /// An expression as (Real + Imaginary i) / Denominator, each of the three real + /// wherever the real unknowns are. + /// + private sealed record Parts(Entity Real, Entity Imaginary, Entity Denominator); + + /// + /// Writes an expression in x = a + b i as its , polynomials in + /// a, b and one real unknown for each modulus, with the equation that + /// defines each in . + /// + private sealed class Splitter + { + /// The highest whole power taken apart, past which the parts are too large to solve. + private const int HighestPower = 16; + + private readonly Variable x; + private readonly Parts ofX; + private readonly List taken; + + internal Splitter(Variable x, Variable a, Variable b, List taken) + { + this.x = x; + ofX = new(a, b, Integer.One); + this.taken = taken; + } + + /// The unknown standing for each modulus, by its argument. + internal Dictionary Moduli { get; } = new(); + + internal List Equations { get; } = new(); + + /// The arguments of the signums, each split as itself over its modulus. + internal HashSet Signums { get; } = new(); + + internal Parts? Split(Entity expr) + => expr switch + { + _ when expr == x => ofX, + Variable => null, + Rational => new(expr, Integer.Zero, Integer.One), + Complex { RealPart: Rational real, ImaginaryPart: Rational imaginary } => new(real, imaginary, Integer.One), + Sumf(var augend, var addend) => Split(augend) is { } left && Split(addend) is { } right ? Add(left, right, Plus) : null, + Minusf(var minuend, var subtrahend) => Split(minuend) is { } left && Split(subtrahend) is { } right ? Add(left, right, Less) : null, + Mulf(var multiplier, var multiplicand) => Split(multiplier) is { } left && Split(multiplicand) is { } right ? Multiply(left, right) : null, + Divf(var dividend, var divisor) => Split(dividend) is { } left && Split(divisor) is { } right ? Divide(left, right) : null, + Powf(var @base, Integer exponent) when exponent.EInteger.Abs().CompareTo(HighestPower) <= 0 + => Split(@base) is { } parts ? Power(parts, exponent.EInteger.ToInt32Checked()) : null, + Absf(var argument) => Split(argument) is { } parts ? Modulus(argument, parts) : null, + // Where its argument is not zero, which is the case it is split in. + Signumf(var argument) => Split(argument) is { } parts ? Divide(parts, Signum(argument, parts)) : null, + Conjugatef(var argument) => Split(argument) is { } parts ? new(parts.Real, Less(Integer.Zero, parts.Imaginary), parts.Denominator) : null, + Ref(var argument) => Split(argument) is { } parts ? new(parts.Real, Integer.Zero, parts.Denominator) : null, + Imf(var argument) => Split(argument) is { } parts ? new(parts.Imaginary, Integer.Zero, parts.Denominator) : null, + _ when !expr.ContainsNode(x) && expr.Evaled is Rational value => new(value, Integer.Zero, Integer.One), + _ => null, + }; + + private Parts Signum(Entity argument, Parts parts) + { + Signums.Add(argument); + return Modulus(argument, parts); + } + + private Parts Modulus(Entity argument, Parts parts) + { + if (!Moduli.TryGetValue(argument, out var modulus)) + { + modulus = Variable.CreateTemp(taken); + taken.Add(modulus); + Moduli[argument] = modulus; + Equations.Add(Less( + Times(Times(modulus, modulus), Times(parts.Denominator, parts.Denominator)), + Plus(Times(parts.Real, parts.Real), Times(parts.Imaginary, parts.Imaginary)))); + } + return new(modulus, Integer.Zero, Integer.One); + } + + private static Parts Add(Parts left, Parts right, System.Func add) + => left.Denominator == right.Denominator + ? new(add(left.Real, right.Real), add(left.Imaginary, right.Imaginary), left.Denominator) + : new( + add(Times(left.Real, right.Denominator), Times(right.Real, left.Denominator)), + add(Times(left.Imaginary, right.Denominator), Times(right.Imaginary, left.Denominator)), + Times(left.Denominator, right.Denominator)); + + private static Parts Multiply(Parts left, Parts right) + => new( + Less(Times(left.Real, right.Real), Times(left.Imaginary, right.Imaginary)), + Plus(Times(left.Real, right.Imaginary), Times(left.Imaginary, right.Real)), + Times(left.Denominator, right.Denominator)); + + // (p + q i) / d divided by (r + s i) / e is (p + q i)(r - s i) e / (d (r^2 + s^2)). + private static Parts Divide(Parts left, Parts right) + => IsZero(right.Imaginary) + ? new(Times(left.Real, right.Denominator), Times(left.Imaginary, right.Denominator), Times(left.Denominator, right.Real)) + : new( + Times(Plus(Times(left.Real, right.Real), Times(left.Imaginary, right.Imaginary)), right.Denominator), + Times(Less(Times(left.Imaginary, right.Real), Times(left.Real, right.Imaginary)), right.Denominator), + Times(left.Denominator, Plus(Times(right.Real, right.Real), Times(right.Imaginary, right.Imaginary)))); + + private static Parts Power(Parts parts, int exponent) + { + Parts result = new(Integer.One, Integer.Zero, Integer.One); + var square = parts; + for (var remaining = System.Math.Abs(exponent); remaining > 0; remaining >>= 1) + { + if ((remaining & 1) == 1) + result = Multiply(result, square); + if (remaining > 1) + square = Multiply(square, square); + } + return exponent < 0 ? Divide(new(Integer.One, Integer.Zero, Integer.One), result) : result; + } + + private static bool IsZero(Entity part) => part is Integer { IsZero: true }; + + private static Entity Times(Entity left, Entity right) + => IsZero(left) || IsZero(right) ? Integer.Zero + : left == Integer.One ? right + : right == Integer.One ? left + : left * right; + + private static Entity Plus(Entity left, Entity right) + => IsZero(left) ? right : IsZero(right) ? left : left + right; + + private static Entity Less(Entity left, Entity right) + => IsZero(right) ? left : left - right; + } } } diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs index abe2c7596..e1553f5e9 100644 --- a/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs @@ -34,21 +34,51 @@ public void OverTheComplexNumbersACurveIsNotAListOfPoints(string equation) => Assert.IsType(equation.ToEntity().Solve("x")); /// - /// x inside a function that is not holomorphic and outside it too: Newton's method - /// assumes a holomorphic function, and over the complex numbers |u| = +-u only where - /// u is real, which a root need not make it, so neither the search nor a split by the - /// sign finds every root. Left unsolved. The search's near-copies of a root came back as - /// roots: x^2/|1 - x^2| = x had 1.618 + 1.7e-9 i beside 1.618. + /// x inside a modulus, a conjugate, a real or an imaginary part and outside it too, + /// solved in the real and imaginary parts of x, exactly. Newton's method assumes a + /// holomorphic function, and its near-copies of a root came back as roots: + /// x^2/|1 - x^2| = x had 1.618 + 1.7e-9 i beside 1.618. Nor does a + /// split by the sign find every root, since over the complex numbers |u| = +-u only + /// where u is real: x^2 + 1 = |x| is solved by +-0.618 i. /// #2139 /// [Theory] - [InlineData("x^2/abs(1 - x^2) = x")] - [InlineData("abs(x^2 - 4) = 3 x")] - [InlineData("x^2 + 1 = abs(x)")] - [InlineData("x * abs(x) = 4")] - [InlineData("x + sgn(x) = 3")] + [InlineData("x^2/abs(1 - x^2) = x", "0", "(sqrt(5) - 1) / 2", "(sqrt(5) + 1) / 2")] + [InlineData("abs(x^2 - 4) = 3 x", "1", "4")] + [InlineData("x * abs(x) = 4", "2")] + [InlineData("x^2 + 1 = abs(x)", "i * (sqrt(5) - 1) / 2", "-i * (sqrt(5) - 1) / 2")] + [InlineData("x * (abs(x) - x^2) = 0", "0", "1", "-1")] + [InlineData("1 / abs(x) = x^2", "1", "-1")] + [InlineData("abs(1 / (x - 1)) = x", "(sqrt(5) + 1) / 2")] + [InlineData("conj(x) = x^2", "0", "1", "-1/2 + i * sqrt(3) / 2", "-1/2 - i * sqrt(3) / 2")] + [InlineData("x + Re(x) = 3 + i", "3/2 + i")] + [InlineData("Im(x)^2 + x = 1", "1")] [InlineData("x + conj(x) = 2 + i")] - public void OverTheComplexNumbersAnEquationInAndOutOfAModulusIsLeftUnsolved(string equation) + [InlineData("abs(x)^2 = x + i")] + [InlineData("x + sgn(x) = 3", "2")] + [InlineData("x * sgn(x) = 2", "2", "-2")] + [InlineData("x^2 = sgn(x)", "0", "1")] + [InlineData("sgn(x^2 - 1) = x")] + public void OverTheComplexNumbersAnEquationInAndOutOfAModulusIsSolvedInThePartsOfX(string equation, params string[] roots) + { + var answer = Assert.IsType(equation.ToEntity().Solve("x")); + Assert.Equal(roots.Length, answer.Count); + foreach (var root in roots) + Assert.Contains(answer, found => Entity.Number.IsZero((found - root.ToEntity()).EvalNumerical())); + } + + /// + /// Left unsolved where the roots are not finitely many, as the ray |x| = Re x; where + /// one is not found exactly, as the real root of x^5 - x - 1; where the system is + /// too large to solve in the time an answer is worth; and with an argument, which is not a + /// polynomial in the parts of x. + /// + [Theory] + [InlineData("abs(x) = Re(x)")] + [InlineData("x^5 = abs(x) + 1")] + [InlineData("x^7 = abs(x) + 1")] + [InlineData("x + arg(x) = 1")] + public void OverTheComplexNumbersTheRestIsLeftUnsolved(string equation) => Assert.IsType(equation.ToEntity().Solve("x")); /// The variable only inside the modulus, or not in one at all, is answered as before. From b192ae0b45fad29bf97f919d2ae3204d8ad294f4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 1 Nov 2026 13:31:05 +0000 Subject: [PATCH 2/2] Walk the tree by hand for the test that picks out these equations It is asked of every equation the solver is given, ahead of the spelling loop. Enumerating Nodes allocates an iterator at each level, and that was 4.5 MB of the kernel benchmark's SolveHard, 45% of it, which has no modulus in it. The walk allocates nothing: SolveHard allocates 187,558,968 bytes per solve on master and on the branch. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Solvers/EquationSolver/ModulusSolver.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs index 5ead14c39..767abe0d1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs @@ -52,7 +52,27 @@ internal static bool IsRealForEveryComplex(Entity expr, Variable x) /// 1.618. https://github.com/asc-community/AngouriMath/issues/2139 /// internal static bool IsInsideAndOutsideAFunctionThatIsNotHolomorphic(Entity expr, Variable x) - => expr.Nodes.Any(node => IsNotHolomorphic(node) && node.ContainsNode(x)) && IsOutsideThem(expr, x); + => IsInsideOne(expr, x) && IsOutsideThem(expr, x); + + /// + /// Whether is inside a function that is not holomorphic somewhere in + /// . + /// + /// + /// Walked by hand, since the solver asks it of every equation it is given: enumerating + /// allocates an iterator at each level, and that was 4.5 MB of + /// the solve the kernel benchmark calls SolveHard, which has no such function in it. + /// + private static bool IsInsideOne(Entity expr, Variable x) + { + if (IsNotHolomorphic(expr)) + return expr.ContainsNode(x); + var children = expr.DirectChildren; + for (var i = 0; i < children.Count; i++) + if (IsInsideOne(children[i], x)) + return true; + return false; + } private static bool IsNotHolomorphic(Entity node) => node is Absf or Signumf or Conjugatef or Ref or Imf or Argf;