From 0bb920d223ba4b9d4482209b9de938b1532ea47b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 14 Oct 2026 06:37:07 +0000 Subject: [PATCH 1/2] Keep a sum of piecewises a sum where their cases do not line up An answer's sum of two piecewises was combined case by case into one piecewise with every case of one against every case of the other. It stays a sum where that combination has more cases than the two together, counted after the unreachable ones are gone; and a sum holding a piecewise is not copied into another's cases. Integrands are normalised with the old combination, so the search sees what it saw before. Fixes #1874 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/Integration.Definition.cs | 33 +++++++++- .../Evaluation/Evaluation.Classes.cs | 29 ++++++++- .../Core/APiecewiseSumStaysASumTest.cs | 60 +++++++++++++++++++ .../Core/ContradictoryConjunctionTest.cs | 3 +- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs 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")); } From f1ec77f3eb9b5965bf400a4c26f5e28e1ac12ba6 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 14 Oct 2026 09:22:46 +0000 Subject: [PATCH 2/2] Record the answers a sum of piecewises shortens Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 50 +++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 18 deletions(-) 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