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..767abe0d1 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 @@ -51,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; @@ -185,5 +206,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.