diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2e1480250..e2a58264a 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -135,6 +135,25 @@ except where shown. | `([[1, 2], [2, 4]] ^ (-1)).Evaled` | throws `InvalidDeterminantException` | `[[1, 2], [2, 4]] ^ (-1)` | | `([[1, 2], [2, 4]] ^ (-1)).Simplify()` | throws `InvalidDeterminantException` | `1 / [[1, 2], [2, 4]]` | +### A sum of fractions that is a constant over one denominator simplifies to it + +**Silent: an identity in fractions reaches its value, and conditions are written shorter.** `Simplify` gathered +fractions a pair or a sort level at a time and left some sums apart that are a constant over one denominator. It now +also offers the rational canonical form (`Transformation.RationalCanonicalization`) as a candidate, where the +expression is a rational function with a quotient in it and of complexity at most 64, and not while the integrator is +running. The candidate wins only where it rates shorter, and a tie keeps the old answer. Where it wins, the condition +is the canonical one: square-free, and without what the answer's own denominator already excludes +([#2137](https://github.com/asc-community/AngouriMath/issues/2137)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x/(x+y) + y/(x+y)".Simplify()` | `x / (x + y) + y / (x + y)` | `1 provided not x + y = 0` | +| `"1/(1 - (b/a)^2) - a^2/(a^2 - b^2)".Simplify()` | unchanged | `0 provided not a ^ 3 - a * b ^ 2 = 0` | +| `"x/c + (y*c - x*d)/(c^2*(z + d/c)) - (x*z + y)/(c*z + d)".Simplify()` | unchanged | `0 provided not c * d + c ^ 2 * z = 0` | +| `"R^2 - R^2*(a+b)*(1/a+1/b)/4 - R^2*(a-b)*(1/a-1/b)/4".Simplify()` | `R ^ 2 + -1/2 * (R ^ 2 + R ^ 2) provided not a = 0 and not b = 0` | `0 provided not a * b = 0` | +| `"(a - 1)/(1 - b) - (1 - a)/(b - 1)".Simplify()` | `0 provided not (b - 1) * (1 - b) = 0` | `0 provided not b - 1 = 0` | +| `"(x*y*a*b*c)/(c*b*a*x*x)".Simplify()` | `y / x provided not a * b * c * x = 0` | `y / x provided not a * b * c = 0` | + ### A polynomial system is not answered with some of its solutions missing **Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/RationalFunction.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/RationalFunction.cs index ceb494fd1..41afc14c1 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/RationalFunction.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/RationalFunction.cs @@ -71,7 +71,14 @@ internal static class RationalFunction /// where it is not a rational function over Q in its /// free variables, or where a bound is reached. /// - internal static bool TryCanonicalize(Entity expr, [NotNullWhen(true)] out Entity? canonical) + /// The expression. + /// Its canonical form. + /// Whether the quotient is scaled at the end so that its + /// denominator has whole coprime coefficients and a positive leading one, rather than a + /// leading one of 1: 2/(4x^2 + 1), not (1/2)/(x^2 + 1/4). The same quotient + /// either way, and still one tree per function; the monic form is the specification's, + /// and this is the one written for a reader, as Simplify offers it. + internal static bool TryCanonicalize(Entity expr, [NotNullWhen(true)] out Entity? canonical, bool wholeCoefficients = false) { canonical = null; if (expr.Complexity > MaxComplexity) @@ -140,6 +147,11 @@ internal static bool TryCanonicalize(Entity expr, [NotNullWhen(true)] out Entity denominator = denominator.ScaleBy(inverse); } + if (wholeCoefficients && !denominator.IsConstant) + { + denominator = denominator.Normalized(out var whole); + numerator = numerator.ScaleBy(whole); + } // The denominator is now monic, so a constant one is exactly 1 and is dropped. var quotient = denominator.IsConstant ? numerator.ToEntity(variables) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 9687c84b1..88fd5b114 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -448,6 +448,13 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) /// internal static bool AnsweringTheQuestionAsked => descentDepth == 1; + /// + /// Whether the integrator is running on this thread: for a Simplify candidate that + /// is wanted by its callers and not by the integrator's rules, which read the shape of what + /// Simplify returns. + /// + internal static bool IsIntegrating => descentDepth > 0; + /// /// The question asked or one step below it: the remainder by parts leaves, the /// integrand a substitution makes. For a rewriting rule that lands on the chain and is diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs index 757254203..f9e9a3613 100644 --- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs +++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs @@ -619,9 +619,68 @@ not solve this issue completely and yet too slow to be accepted. } } + // A rational function's canonical form, offered as one more candidate. The passes + // above gather fractions a pair or a sort level at a time, which reaches most sums of + // fractions and not all: x/(x + y) + y/(x + y) stayed as it was, and so did + // 1/(1 - (b/a)^2) - a^2/(a^2 - b^2), whose canonical forms are 1 and 0 with their + // conditions. RationalCanonicalization decides every such identity, and the complexity + // metric decides whether its quotient is the better answer, which as an expanded + // quotient it often is not. Last, so that a tie goes to the form the rules reached; + // with whole coefficients, so that it reads 2/(4x^2 + 1) and not (1/2)/(x^2 + 1/4); + // and only where there is a quotient to gather, since a polynomial's canonical form is + // its expansion, which is offered above. That is asked by a walk that allocates + // nothing: SimplifyEasy, which has a logarithm in it and is never a rational function, + // allocated 3.4% more when it was asked through Nodes. Only up to a complexity of 64, + // well above the 11 to 39 of the identities this is for, so that a large quotient is + // not gathered for a form that will mostly lose. And not while the integrator is + // running: its rules read the shape of what Simplify hands back, and a gathered + // quotient that rated shorter sent 1/(b cosh x + c sinh x + sqrt(b^2 - c^2))^4 down a + // route that took 4.8 s to an answer of 737 characters, where master's took 1.1 s to + // one of 427. + // https://github.com/asc-community/AngouriMath/issues/2137 + var canonicalMark = recording?.Mark() ?? 0; + if (!Algebra.Integration.IsIntegrating && stage1.Complexity <= 64 && IsARationalFunctionWithAQuotient(stage1) + && RationalFunction.TryCanonicalize(stage1, out var canonical, wholeCoefficients: true)) + AddHistory(Noted(recording, stage1, canonical, "RationalCanonicalization", canonicalMark)); + return history.Values.SelectMany(x => x); } + /// + /// Whether is made of numbers, variables, sums, differences, + /// products, quotients and whole powers alone, with a quotient or a negative power among + /// them: the expressions may gather into + /// one quotient. + /// + private static bool IsARationalFunctionWithAQuotient(Entity expr) + { + var quotient = false; + return IsARationalFunction(expr, ref quotient) && quotient; + } + + private static bool IsARationalFunction(Entity node, ref bool quotient) + { + switch (node) + { + case Number or Variable: + return true; + case Divf(var dividend, var divisor): + quotient = true; + return IsARationalFunction(dividend, ref quotient) && IsARationalFunction(divisor, ref quotient); + case Powf(var @base, Integer exponent): + quotient |= exponent.EInteger.Sign < 0; + return IsARationalFunction(@base, ref quotient); + case Sumf(var left, var right): + return IsARationalFunction(left, ref quotient) && IsARationalFunction(right, ref quotient); + case Minusf(var left, var right): + return IsARationalFunction(left, ref quotient) && IsARationalFunction(right, ref quotient); + case Mulf(var left, var right): + return IsARationalFunction(left, ref quotient) && IsARationalFunction(right, ref quotient); + default: + return false; + } + } + /// /// Sorts an expression into a polynomial. /// See more at diff --git a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs index 9c17e4c51..4286b5347 100644 --- a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs @@ -121,8 +121,10 @@ public void Issue403_FractionsOverACommonDenominatorCollapse(string input, strin // The general case still brings unlike denominators together, which needs the // numerator fully simplified to reach 0. [Fact] + // The condition is the square-free part of (b - 1)(1 - b), which vanishes at the same + // points, since #2137 offers the rational canonical form as a candidate. public void Issue403_UnlikeDenominatorsStillCombine() => - Assert.Equal("0 provided not (b - 1) * (1 - b) = 0".ToEntity(), + Assert.Equal("0 provided not b - 1 = 0".ToEntity(), "(a - 1) / (1 - b) - (1 - a) / (b - 1)".ToEntity().Simplify()); // Three separate defects sat on top of each other in the integrator, diff --git a/Sources/Tests/UnitTests/Common/SumOfFractionsOverOneDenominatorTest.cs b/Sources/Tests/UnitTests/Common/SumOfFractionsOverOneDenominatorTest.cs new file mode 100644 index 000000000..2756ecd41 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/SumOfFractionsOverOneDenominatorTest.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Common +{ + /// + /// Simplify gathered fractions a pair or a sort level at a time, and left some sums of + /// fractions apart that are a constant over one denominator. The rational canonical form + /// decides them, and is offered as a candidate since + /// #2137; the first + /// row of #2133 is the + /// same shape. + /// + [Trait("Area", "Common")] + public sealed class SumOfFractionsOverOneDenominatorTest + { + [Theory] + [InlineData("x/(x + y) + y/(x + y)", 1)] + [InlineData("1/(1 - (b/a)^2) - a^2/(a^2 - b^2)", 0)] + [InlineData("x/c + (y*c - x*d)/(c^2*(z + d/c)) - (x*z + y)/(c*z + d)", 0)] + [InlineData("R^2 - R^2*(a + b)*(1/a + 1/b)/4 - R^2*(a - b)*(1/a - 1/b)/4", 0)] + public void ItIsTheConstantItIs(string input, int constant) + { + var simplified = input.Simplify(); + while (simplified is Providedf(var inner, _)) + simplified = inner; + Assert.Equal(Integer.Create(constant), simplified); + } + + /// + /// The condition the answer carries is the input's: defined where the input is. + /// + [Theory] + [InlineData("x/(x + y) + y/(x + y)", "x", "1", "y", "2")] + [InlineData("1/(1 - (b/a)^2) - a^2/(a^2 - b^2)", "a", "3", "b", "1")] + [InlineData("R^2 - R^2*(a + b)*(1/a + 1/b)/4 - R^2*(a - b)*(1/a - 1/b)/4", "a", "2", "b", "5")] + public void ItIsDefinedWhereTheInputIs(string input, string first, string firstValue, string second, string secondValue) + { + var at = input.Simplify().Substitute(first, firstValue.ToEntity()).Substitute(second, secondValue.ToEntity()).Substitute("R", 7); + Assert.IsAssignableFrom(at.Evaled); + Assert.NotEqual(MathS.NaN, at.Evaled); + } + } +} diff --git a/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs b/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs index 7589137fd..c63349bbc 100644 --- a/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs +++ b/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs @@ -22,7 +22,7 @@ public sealed class FractionSimplify [InlineData("x/x-x/x", "0 provided not x = 0")] [InlineData("(xy)/(xy)", "1 provided not xy = 0")] [InlineData("0*(x+3)^-4", "0 provided not 3 + x = 0")] - [InlineData("(a - 1) / (1 - b) - (1 - a) / (b - 1)", "0 provided not (b - 1) * (1 - b) = 0")] // #254 + [InlineData("(a - 1) / (1 - b) - (1 - a) / (b - 1)", "0 provided not b - 1 = 0")] // #254, and #2137 for the square-free condition [InlineData("(4a - 2) / (2x) + (1 - 2a) / x", "0 provided not x = 0")] [InlineData("sin(a) * a * b / a", "b * sin(a) provided not a = 0")] // #311 [InlineData("sin(a) * cos(b) * tan(c) / (tan(c)3 * sin(a)2 * cos(b)^(-2))", "csc(a) * cos(b) ^ 3 * cotan(c) ^ 2")] diff --git a/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs b/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs index 90afb0faf..355c54979 100644 --- a/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs @@ -61,8 +61,10 @@ [Fact] public void Patt8Negative() => // The condition used to read `not c = 0 and not a * b * c * x ^ 2 = 0`, which says // the same thing twice: a product is nonzero exactly when each of its factors is, // so a b c x^2 != 0 already carries c != 0. Cancelling the common divisor in one - // step states the domain once, over the same set of points. - [Fact] public void Patt12() => AssertSimplify((x * y * a * b * c) / (c * b * a * x * x), "y / x provided not a * b * c * x = 0", 4); + // step states the domain once, over the same set of points. And x is not repeated in + // the condition, since y/x already has no value at x = 0: the rational canonical form + // writes the condition without what its own denominator excludes (#2137). + [Fact] public void Patt12() => AssertSimplify((x * y * a * b * c) / (c * b * a * x * x), "y / x provided not a * b * c = 0", 4); [Fact] public void Frac1() => AssertSimplify("x / (y / z)", "x * z / y"); [Fact] public void Frac2() => AssertSimplify("x / y / z", "x / (y * z)"); [Fact] public void Factorial2() => AssertSimplify(MathS.Factorial(2), 2);