diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 04c61b66e..2d2cbd956 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -560,24 +560,38 @@ same answer as on the unreleased master, in 1.4 seconds rather than 30 **Answers where there were none.** `sqrt(a + b x + c x^2)/((d + k x)(f + g x)^2)` was declined: with no quadratic below the bar, powers of linears were left to Euler's substitution, which writes the root of a symbolic leading coefficient into every coefficient and does not finish. They are taken apart over the linears now where the -integrand has a symbol in it, each piece closed beside the root. The answers are large because each piece's cases, -by the sign of the radicand at its pole and of `c`, multiply in the sum -([#1874](https://github.com/asc-community/AngouriMath/issues/1874)): the fourth row is 27 cases of about 3,000 -characters each ([#718](https://github.com/asc-community/AngouriMath/issues/718)). - -| Input | Was (2.5.0) | Now | -|---|---|---| -| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 12,694 characters | -| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 20,029 characters | -| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^4)".ToEntity().Integrate("x")` | `integral(...)` | 29,203 characters | -| `"(a + b*x + c*x^2)^(3/2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 79,744 characters | -| `"1/((d + k*x)*(f + g*x)^2*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 11,335 characters | -| `"1/((d + k*x)*(f + g*x)^3*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 18,103 characters | -| `"sqrt(a + b*x^2 + c*x^4)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 5,902 characters | -| `"(a + b*x^2 + c*x^4)^(3/2)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 27,184 characters | -| `"1/(x^3*(d + k*x^2)*sqrt(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 5,515 characters | -| `"asec(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 11,873 characters | -| `"acsc(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 11,864 characters | +integrand has a symbol in it, each piece closed beside the root, and the pieces' answers are a sum of their piecewises +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 4,421 characters | +| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 7,052 characters | +| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^4)".ToEntity().Integrate("x")` | `integral(...)` | 10,281 characters | +| `"(a + b*x + c*x^2)^(3/2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 9,304 characters | +| `"1/((d + k*x)*(f + g*x)^2*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,989 characters | +| `"1/((d + k*x)*(f + g*x)^3*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 6,416 characters | +| `"sqrt(a + b*x^2 + c*x^4)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 2,063 characters | +| `"(a + b*x^2 + c*x^4)^(3/2)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,100 characters | +| `"1/(x^3*(d + k*x^2)*sqrt(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 1,933 characters | +| `"asec(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 4,073 characters | +| `"acsc(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 4,072 characters | + +### A sum of two piecewise answers whose cases do not line up stays a sum + +**Shorter answers, and answers where there were none.** An antiderivative made of terms that each split +on the signs of their constants was combined into one piecewise listing every case of one term against +every case of the other. It stays a sum of the terms' piecewises now wherever combining them would +give more cases than they have between them. `1/(b x^2 + m) - 1/(sqrt(a + b x^2) (b x^2 + m))` was nine +cases; `(A + B x)(d + k x)^(7/2)/(b x + c x^2)^3` came to seven million characters on the unreleased +master and is answered in two seconds now +([#1874](https://github.com/asc-community/AngouriMath/issues/1874)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(b*x^2 + m) - 1/(sqrt(a + b*x^2)*(b*x^2 + m))".ToEntity().Integrate("x")` | `integral(...)` | 975 characters | +| `"(A + B*x)*(d + k*x)^(7/2)/(b*x + c*x^2)^3".ToEntity().Integrate("x")` | `integral(...)` | 15,079 characters | +| `"(A + B*x)/((d + k*x)^(5/2)*(b*x + c*x^2)^3)".ToEntity().Integrate("x")` | `integral(...)` | 17,637 characters | ### A trinomial the numerator holds is divided out of a power of it diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index bf2c21f94..bea48da67 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -189,7 +189,7 @@ internal static partial class Integration /// /// private static Entity Normalized(Entity expr, Entity.Variable x) => - expr.Replace(Patterns.GatherPowersOfOneBase).Replace(node => IndefiniteIntegralSolver.GatherSignsAndModuli(node, x)) + WithItsPiecewiseSumsCombined(expr.Replace(Patterns.GatherPowersOfOneBase).Replace(node => IndefiniteIntegralSolver.GatherSignsAndModuli(node, x)) // A whole power of a fractional power is the power of the product of the // exponents, exact for every base: `((c^2 x^2 + 1)^(-1/2))^2` is // `(c^2 x^2 + 1)^(-1)`, and left as a square of a root it was handed to Euler's @@ -209,7 +209,36 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // `1/(a + b x^2 + c x^4)` is answered. .Replace(node => node is Entity.Powf(var zeroth, Entity.Number.Integer { IsZero: true }) && zeroth.ContainsNode(x) ? Entity.Number.Integer.One - : node); + : node)); + + /// + /// with every sum of piecewises in it combined case by case, as + /// the evaluator did before an answer's sum of piecewises was left a sum: an integrand + /// is searched as the one piecewise, and taken apart term by term it was searched once a + /// term. The sums are written anew, so that no simplification of them is remembered from + /// outside the scope. + /// https://github.com/asc-community/AngouriMath/issues/1874 + /// + private static Entity WithItsPiecewiseSumsCombined(Entity expr) + { + if (!expr.Nodes.Any(node => node is Entity.Sumf or Entity.Minusf && node.DirectChildren.Any(child => child is Entity.Piecewise))) + return expr; + var combining = Entity.CombinesEveryPiecewiseSum; + Entity.CombinesEveryPiecewiseSum = true; + try + { + return expr.Replace(node => node switch + { + Entity.Sumf(var augend, var addend) => new Entity.Sumf(augend, addend), + Entity.Minusf(var minuend, var subtrahend) => new Entity.Minusf(minuend, subtrahend), + _ => node + }).InnerSimplified; + } + finally + { + Entity.CombinesEveryPiecewiseSum = combining; + } + } /// /// The integrals already answered under the settings in force, so that the same question diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs index 51671f9c2..d8c6ba35d 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs @@ -162,6 +162,10 @@ Entity ops(Entity a, Entity b) (Providedf a, var b) => ExpandOnTwoArguments(a.Expression, b, operation, defaultCtor, isExact).Provided(a.Predicate), (var a, Providedf b) => ExpandOnTwoArguments(a, b.Expression, operation, defaultCtor, isExact).Provided(b.Predicate), (Piecewise a, Piecewise b) => CombinedCaseByCase(a, b, operation, defaultCtor, isExact), + // A sum already holding a piecewise is not copied into each case of another: it + // stays beside it, as CombinedCaseByCase leaves two piecewises that do not combine. + (Piecewise a, var b) when this is Sumf or Minusf && !CombinesEveryPiecewiseSum && b.Nodes.Any(node => node is Piecewise) => defaultCtor(this, left, right), + (var a, Piecewise b) when this is Sumf or Minusf && !CombinesEveryPiecewiseSum && a.Nodes.Any(node => node is Piecewise) => defaultCtor(this, left, right), (Piecewise a, var b) => a.ApplyToValues(a => ops(a, b)), (var a, Piecewise b) => b.ApplyToValues(b => ops(a, b)), (Matrix a, Matrix b) => a.InnerMatrix.Shape == b.InnerMatrix.Shape ? a.Elementwise(b, ops) : defaultCtor(this, left, right), @@ -177,6 +181,17 @@ Entity ops(Entity a, Entity b) }; } + /// + /// Set while the integrator normalises an integrand: a sum of piecewises in one is combined + /// case by case whatever the count, as before + /// #1874. An + /// integrand holding the cases of an answer -- what parts leaves, `u' v` with `v` an + /// antiderivative -- is searched as one piecewise, where kept a sum it was taken apart + /// term by term and every term searched: `(a + b atanh(c x^2))^2/x^5` went from 19 + /// seconds to 134. Answers stay sums. + /// + [ThreadStatic] internal static bool CombinesEveryPiecewiseSum; + /// /// A piecewise combined with a piecewise, case by case: every case of one against every /// case of the other, under the conjunction of their predicates. @@ -227,7 +242,19 @@ private Entity CombinedCaseByCase(Piecewise a, Piecewise b, for (var i = 0; i < pairs.Count; i++) if (keep[i]) cases.Add((ExpandOnTwoArguments(pairs[i].First.Expression, pairs[i].Second.Expression, operation, defaultCtor, isExact), pairs[i].Predicate).ToProvided()); - return MathS.Piecewise(cases); + var combined = MathS.Piecewise(cases); + // A sum or a difference is combined only where the cases line up: where they do not, + // every case of one against every case of the other is the product of the two counts, + // and the two piecewises as written say the same in the sum of them. Counted once the + // cases no value reaches are gone, since a sum over `k < a` for each k is one case per + // k after that and the product before it. The antiderivative of + // `1/(b x^2 + m) - 1/(sqrt(a + b x^2) (b x^2 + m))` was nine cases in 3,685 + // characters, where its two terms are 287 and 685. + // https://github.com/asc-community/AngouriMath/issues/1874 + if (this is Sumf or Minusf && !CombinesEveryPiecewiseSum && combined.InnerSimplified(isExact) is var simplified + && (simplified is Piecewise piecewise ? piecewise.Cases.Count() : 1) > a.Cases.Count() + b.Cases.Count()) + return defaultCtor(this, a, b); + return combined; } /// diff --git a/Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs b/Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs new file mode 100644 index 000000000..af23b8116 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs @@ -0,0 +1,60 @@ +// +// 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.Extensions; +using Xunit; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Core +{ + /// + /// A sum of two piecewises whose cases do not line up stays a sum of the two: combined case + /// by case it lists every case of one against every case of the other. + /// #1874 + /// + [Trait("Area", "Core")] + public sealed class APiecewiseSumStaysASumTest + { + private static Entity ThreeSigns(string of, string positive, string zero, string negative) + => $"piecewise({positive} provided {of} > 0, {zero} provided {of} = 0, {negative} provided {of} < 0)".ToEntity(); + + [Fact] + public void CasesOnDifferentQuantitiesStayASum() + { + var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("b", "x", "y", "z")).InnerSimplified; + var asASum = Assert.IsType(sum); + Assert.IsType(asASum.Augend); + Assert.IsType(asASum.Addend); + } + + [Fact] + public void CasesOnOneQuantityStillCombine() + { + // The three that agree and the two opposite strict signs: five, not nine. + var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("a", "x", "y", "z")).InnerSimplified; + Assert.IsType(sum); + } + + [Fact] + public void ASumHoldingAPiecewiseIsNotCopiedIntoTheCasesOfAnother() + { + var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("b", "x", "y", "z") + ThreeSigns("c", "u", "v", "w")).InnerSimplified; + Assert.Equal(3, sum.Nodes.Count(node => node is Piecewise)); + } + + [Fact] + public void TheIssuesIntegralIsTheSumOfItsTerms() + { + Entity one = "1/(b*x^2 + m)"; + Entity two = "1/(sqrt(a + b*x^2)*(b*x^2 + m))"; + var whole = (one - two).Integrate("x").Stringize().Length; + var apart = one.Integrate("x").Stringize().Length + two.Integrate("x").Stringize().Length; + Assert.True(whole <= apart + 16, $"{whole} characters for the difference, {apart} for its terms"); + } + } +} diff --git a/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs b/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs index 5184ccd71..360bcec05 100644 --- a/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs +++ b/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs @@ -148,7 +148,8 @@ public void AConstantFactorIsTheSameQuantity() [Fact] public void OppositeSignsStayWhereALaterCaseDoesNotTestTheQuantity() { - var sum = "piecewise(1 provided q > 0, 2 provided q < 0, 3 provided p > 0) + piecewise(10 provided q < 0, 20 provided q > 0, 30 provided p > 0)".ToEntity().InnerSimplified; + // A product: a sum whose cases do not line up stays a sum of its two piecewises (#1874). + var sum = "piecewise(1 provided q > 0, 2 provided q < 0, 3 provided p > 0) * piecewise(10 provided q < 0, 20 provided q > 0, 30 provided p > 0)".ToEntity().InnerSimplified; Assert.Contains(Assert.IsType(sum).Cases, @case => @case.Predicate.ToString().Contains("q > 0") && @case.Predicate.ToString().Contains("q < 0")); }