diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 1a54a20b2..c83cb36be 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -417,6 +417,21 @@ taken through its decimal, which had already rounded it onto the integer beside | `"round(10^120 + 3/2)".ToEntity().Evaled` | `10^120` | `10^120 + 2` | | `"round(+oo)".ToEntity().Evaled` | throws `OverflowException` | `+oo` | +### A factor a numerator's terms hold to a power cancels + +**Silent: a quotient whose numerator's terms each hold a factor of the denominator, as it is or to a whole power, +is cancelled by it.** A term holding the factor only to a power was not read as holding it, so +`(x (y + 1) + (y + 1)^2) / (y + 1)^4` was left as written while `(x (y + 1) + (y + 1)) / (y + 1)^2` cancelled. The +quotient rule squares the denominator at each derivative, so a Taylor expansion of `e^x / (1 + x^2)` took its seventh +derivative in 5,117 nodes, and takes it in 149: 9 terms took 8 s and take 1 s +([#2301](https://github.com/asc-community/AngouriMath/issues/2301)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"(x * (y + 1) + (y + 1)^2) / (y + 1)^4".ToEntity().Simplify()` | `(x * (y + 1) + (y + 1) ^ 2) / (y + 1) ^ 4` | `(x + y + 1) / (y + 1) ^ 3` | +| `"(x * (x^2 + 1) + (x^2 + 1)^2) / (x^2 + 1)^4".ToEntity().Simplify()` | `(x * (x ^ 2 + 1) + (x ^ 2 + 1) ^ 2) / (x ^ 2 + 1) ^ 4` | `(x ^ 2 + 1 + x) / (x ^ 2 + 1) ^ 3` | +| `"(a * (x^2 + 1) + b * (x^2 + 1)^2) / (x^2 + 1)^3".ToEntity().Simplify()` | `(b * (x ^ 2 + 1) + a) / (x ^ 4 + 2 * x ^ 2 + 1) provided not x ^ 2 + 1 = 0` | `(a + b * (x ^ 2 + 1)) / (x ^ 2 + 1) ^ 2` | + ### A squared sine and cosine with one coefficient are found anywhere in a sum **Silent: an identity reaches its value however the sum is written.** `k sin(x)^2 + k cos(x)^2` was reduced to `k` diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 70e9a7037..ddd6649cc 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -1019,6 +1019,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Common/AFactorHeldToAPowerCancelsTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Common/HyperbolicTangentPastTheRangeTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Rational.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Rational.cs index c30a2dd68..05347ebcf 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Rational.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Rational.cs @@ -77,9 +77,75 @@ internal static Entity FractionCommonDenominatorRules(Entity expr, TreeAnalyzer. /// internal static Entity PairwiseGroupedQuotient( Entity whole, Entity num, Entity den, TreeAnalyzer.SortLevel level) - => PairwiseGrouping(num, den, level).Select(PowerRules).MultiplyAll() + => PairwiseGrouping(WithAFactorOfTheOtherOut(num, den), WithAFactorOfTheOtherOut(den, num), level) + .Select(PowerRules).MultiplyAll() .InnerSimplified.Replace(CollapseMultipleFractions); + /// + /// with a factor of taken out of it, where + /// every term of the sum holds that factor, as it is or to a whole power, so that the + /// pairing meets it: x (y + 1) + (y + 1)^2 beside (y + 1)^4 is + /// (y + 1) (x + y + 1). The sum as it is where no polynomial factor is in every term. + /// + /// + /// A term that held the factor only to a power was not read as holding it, so + /// (x (y + 1) + (y + 1)^2) / (y + 1)^4 was left as written while + /// (x (y + 1) + (y + 1)) / (y + 1)^2 cancelled. The quotient rule squares the + /// denominator at each derivative, so the Taylor expansion of e^x / (1 + x^2) took + /// its derivatives over (x^2 + 1)^2, ^4, ^8. + /// https://github.com/asc-community/AngouriMath/issues/2301 + /// + private static Entity WithAFactorOfTheOtherOut(Entity sum, Entity other) + { + if (sum is not (Sumf or Minusf)) + return sum; + var terms = Sumf.LinearChildren(sum); + foreach (var candidate in Mulf.LinearChildren(other)) + { + var factor = candidate is Powf(var @base, Integer { IsPositive: true }) ? @base : candidate; + if (factor is Number || !factor.Vars.Any() || !IsAPolynomial(factor)) + continue; + var lowest = terms.Min(term => PowerOf(factor, term)); + if (lowest < 1) + continue; + return MathS.Pow(factor, lowest) * Sumf.Sum(terms.Select(term => Without(factor, lowest, term)).ToList()); + } + return sum; + + // A polynomial factor only. A power of a sine has a cancellation of its own, through the + // opened angles and a common denominator, and taking it out first left the reporter's + // second expression of https://github.com/asc-community/AngouriMath/issues/557 short of 0. + static bool IsAPolynomial(Entity factor) + => factor.Nodes.All(node => node is Number or Variable or Sumf or Minusf or Mulf + || node is Powf(_, Integer { IsPositive: true })); + + // How many times the factor is in the product, counting a whole power of it as that many. + static int PowerOf(Entity factor, Entity term) + => Mulf.LinearChildren(term).Sum(child => + child == factor ? 1 + : child is Powf(var b, Integer { IsPositive: true } k) && b == factor && k.EInteger.CanFitInInt32() ? k.EInteger.ToInt32Checked() + : 0); + + // The product with that many of the factor taken off it. + static Entity Without(Entity factor, int count, Entity term) + { + var kept = new List(); + foreach (var child in Mulf.LinearChildren(term)) + { + var here = child == factor ? 1 + : child is Powf(var b, Integer { IsPositive: true } k) && b == factor && k.EInteger.CanFitInInt32() ? k.EInteger.ToInt32Checked() + : 0; + var taken = System.Math.Min(here, count); + count -= taken; + if (here - taken > 0) + kept.Add(here - taken == 1 ? factor : MathS.Pow(factor, here - taken)); + else if (here == 0) + kept.Add(child); + } + return kept.Count == 0 ? Integer.One : kept.Aggregate((left, right) => left * right); + } + } + /// n^(p/q) for a q of 2 or more -- a root that is not a whole power. private static bool IsSurd(Entity node) => node is Powf(_, Rational and not Integer); diff --git a/Sources/Tests/UnitTests/Common/AFactorHeldToAPowerCancelsTest.cs b/Sources/Tests/UnitTests/Common/AFactorHeldToAPowerCancelsTest.cs new file mode 100644 index 000000000..fe67b4858 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/AFactorHeldToAPowerCancelsTest.cs @@ -0,0 +1,55 @@ +// +// 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; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A factor that every term of a quotient's numerator holds, as it is or to a whole power, is taken out of the + /// numerator and paired off against the denominator. A term holding it only to a power was not read as holding it, + /// so (x (y + 1) + (y + 1)^2) / (y + 1)^4 was left as written while (x (y + 1) + (y + 1)) / (y + 1)^2 + /// cancelled. #2301 + /// + [Trait("Area", "Common")] + public sealed class AFactorHeldToAPowerCancelsTest + { + [Theory] + [InlineData("(x * (y + 1) + (y + 1)^2) / (y + 1)^4", "(x + y + 1) / (y + 1) ^ 3")] + [InlineData("(x * (x^2 + 1) + (x^2 + 1)^2) / (x^2 + 1)^4", "(x ^ 2 + 1 + x) / (x ^ 2 + 1) ^ 3")] + [InlineData("(x * (y + 1) + (y + 1)) / (y + 1)^2", "(x + 1) / (y + 1)")] + public void TheFactorCancels(string quotient, string expected) + { + var simplified = quotient.ToEntity().Simplify(); + // As printed: the answer's sums are grouped as Simplify builds them, not as the parser does. + Assert.Equal(expected, simplified.Stringize()); + foreach (var (x, y) in new[] { (2.0, 0.5), (-1.5, 3.0), (0.25, -0.75) }) + { + var before = quotient.ToEntity().Substitute("x", x).Substitute("y", y).EvalNumerical(); + var after = simplified.Substitute("x", x).Substitute("y", y).EvalNumerical(); + Assert.True((before - after).Abs() < 1e-9, $"{after} where the quotient is {before} at x = {x}, y = {y}"); + } + } + + /// + /// The quotient rule squares the denominator at each derivative, and what the new numerator shares with it was + /// not taken back: the seventh derivative of e^x / (1 + x^2), simplified at each step, had 5,117 nodes, + /// and has 149 once the factor cancels. + /// + [Fact] + public void TheDerivativesOfAQuotientStaySmall() + { + Entity derivative = "e^x / (1 + x^2)"; + for (var i = 0; i < 7; i++) + derivative = derivative.Differentiate("x").Simplify(); + Assert.True(derivative.Nodes.Count() < 500, $"{derivative.Nodes.Count()} nodes: {derivative}"); + } + } +}