Skip to content

Commit 033953d

Browse files
Keep a sum of piecewise answers a sum where their cases do not line up (#1882)
* 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura * Record the answers a sum of piecewises shortens Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 790feec commit 033953d

5 files changed

Lines changed: 153 additions & 22 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -560,24 +560,38 @@ same answer as on the unreleased master, in 1.4 seconds rather than 30
560560
**Answers where there were none.** `sqrt(a + b x + c x^2)/((d + k x)(f + g x)^2)` was declined: with no quadratic
561561
below the bar, powers of linears were left to Euler's substitution, which writes the root of a symbolic leading
562562
coefficient into every coefficient and does not finish. They are taken apart over the linears now where the
563-
integrand has a symbol in it, each piece closed beside the root. The answers are large because each piece's cases,
564-
by the sign of the radicand at its pole and of `c`, multiply in the sum
565-
([#1874](https://github.com/asc-community/AngouriMath/issues/1874)): the fourth row is 27 cases of about 3,000
566-
characters each ([#718](https://github.com/asc-community/AngouriMath/issues/718)).
567-
568-
| Input | Was (2.5.0) | Now |
569-
|---|---|---|
570-
| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 12,694 characters |
571-
| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 20,029 characters |
572-
| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^4)".ToEntity().Integrate("x")` | `integral(...)` | 29,203 characters |
573-
| `"(a + b*x + c*x^2)^(3/2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 79,744 characters |
574-
| `"1/((d + k*x)*(f + g*x)^2*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 11,335 characters |
575-
| `"1/((d + k*x)*(f + g*x)^3*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 18,103 characters |
576-
| `"sqrt(a + b*x^2 + c*x^4)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 5,902 characters |
577-
| `"(a + b*x^2 + c*x^4)^(3/2)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 27,184 characters |
578-
| `"1/(x^3*(d + k*x^2)*sqrt(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 5,515 characters |
579-
| `"asec(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 11,873 characters |
580-
| `"acsc(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 11,864 characters |
563+
integrand has a symbol in it, each piece closed beside the root, and the pieces' answers are a sum of their piecewises
564+
([#718](https://github.com/asc-community/AngouriMath/issues/718)).
565+
566+
| Input | Was (2.5.0) | Now |
567+
|---|---|---|
568+
| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 4,421 characters |
569+
| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 7,052 characters |
570+
| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^4)".ToEntity().Integrate("x")` | `integral(...)` | 10,281 characters |
571+
| `"(a + b*x + c*x^2)^(3/2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 9,304 characters |
572+
| `"1/((d + k*x)*(f + g*x)^2*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,989 characters |
573+
| `"1/((d + k*x)*(f + g*x)^3*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 6,416 characters |
574+
| `"sqrt(a + b*x^2 + c*x^4)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 2,063 characters |
575+
| `"(a + b*x^2 + c*x^4)^(3/2)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,100 characters |
576+
| `"1/(x^3*(d + k*x^2)*sqrt(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 1,933 characters |
577+
| `"asec(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 4,073 characters |
578+
| `"acsc(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 4,072 characters |
579+
580+
### A sum of two piecewise answers whose cases do not line up stays a sum
581+
582+
**Shorter answers, and answers where there were none.** An antiderivative made of terms that each split
583+
on the signs of their constants was combined into one piecewise listing every case of one term against
584+
every case of the other. It stays a sum of the terms' piecewises now wherever combining them would
585+
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
586+
cases; `(A + B x)(d + k x)^(7/2)/(b x + c x^2)^3` came to seven million characters on the unreleased
587+
master and is answered in two seconds now
588+
([#1874](https://github.com/asc-community/AngouriMath/issues/1874)).
589+
590+
| Input | Was (2.5.0) | Now |
591+
|---|---|---|
592+
| `"1/(b*x^2 + m) - 1/(sqrt(a + b*x^2)*(b*x^2 + m))".ToEntity().Integrate("x")` | `integral(...)` | 975 characters |
593+
| `"(A + B*x)*(d + k*x)^(7/2)/(b*x + c*x^2)^3".ToEntity().Integrate("x")` | `integral(...)` | 15,079 characters |
594+
| `"(A + B*x)/((d + k*x)^(5/2)*(b*x + c*x^2)^3)".ToEntity().Integrate("x")` | `integral(...)` | 17,637 characters |
581595

582596
### A trinomial the numerator holds is divided out of a power of it
583597

‎Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ internal static partial class Integration
189189
/// </para>
190190
/// </remarks>
191191
private static Entity Normalized(Entity expr, Entity.Variable x) =>
192-
expr.Replace(Patterns.GatherPowersOfOneBase).Replace(node => IndefiniteIntegralSolver.GatherSignsAndModuli(node, x))
192+
WithItsPiecewiseSumsCombined(expr.Replace(Patterns.GatherPowersOfOneBase).Replace(node => IndefiniteIntegralSolver.GatherSignsAndModuli(node, x))
193193
// A whole power of a fractional power is the power of the product of the
194194
// exponents, exact for every base: `((c^2 x^2 + 1)^(-1/2))^2` is
195195
// `(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) =>
209209
// `1/(a + b x^2 + c x^4)` is answered.
210210
.Replace(node => node is Entity.Powf(var zeroth, Entity.Number.Integer { IsZero: true }) && zeroth.ContainsNode(x)
211211
? Entity.Number.Integer.One
212-
: node);
212+
: node));
213+
214+
/// <summary>
215+
/// <paramref name="expr"/> with every sum of piecewises in it combined case by case, as
216+
/// the evaluator did before an answer's sum of piecewises was left a sum: an integrand
217+
/// is searched as the one piecewise, and taken apart term by term it was searched once a
218+
/// term. The sums are written anew, so that no simplification of them is remembered from
219+
/// outside the scope.
220+
/// https://github.com/asc-community/AngouriMath/issues/1874
221+
/// </summary>
222+
private static Entity WithItsPiecewiseSumsCombined(Entity expr)
223+
{
224+
if (!expr.Nodes.Any(node => node is Entity.Sumf or Entity.Minusf && node.DirectChildren.Any(child => child is Entity.Piecewise)))
225+
return expr;
226+
var combining = Entity.CombinesEveryPiecewiseSum;
227+
Entity.CombinesEveryPiecewiseSum = true;
228+
try
229+
{
230+
return expr.Replace(node => node switch
231+
{
232+
Entity.Sumf(var augend, var addend) => new Entity.Sumf(augend, addend),
233+
Entity.Minusf(var minuend, var subtrahend) => new Entity.Minusf(minuend, subtrahend),
234+
_ => node
235+
}).InnerSimplified;
236+
}
237+
finally
238+
{
239+
Entity.CombinesEveryPiecewiseSum = combining;
240+
}
241+
}
213242

214243
/// <summary>
215244
/// The integrals already answered under the settings in force, so that the same question

‎Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ Entity ops(Entity a, Entity b)
162162
(Providedf a, var b) => ExpandOnTwoArguments(a.Expression, b, operation, defaultCtor, isExact).Provided(a.Predicate),
163163
(var a, Providedf b) => ExpandOnTwoArguments(a, b.Expression, operation, defaultCtor, isExact).Provided(b.Predicate),
164164
(Piecewise a, Piecewise b) => CombinedCaseByCase(a, b, operation, defaultCtor, isExact),
165+
// A sum already holding a piecewise is not copied into each case of another: it
166+
// stays beside it, as CombinedCaseByCase leaves two piecewises that do not combine.
167+
(Piecewise a, var b) when this is Sumf or Minusf && !CombinesEveryPiecewiseSum && b.Nodes.Any(node => node is Piecewise) => defaultCtor(this, left, right),
168+
(var a, Piecewise b) when this is Sumf or Minusf && !CombinesEveryPiecewiseSum && a.Nodes.Any(node => node is Piecewise) => defaultCtor(this, left, right),
165169
(Piecewise a, var b) => a.ApplyToValues(a => ops(a, b)),
166170
(var a, Piecewise b) => b.ApplyToValues(b => ops(a, b)),
167171
(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)
177181
};
178182
}
179183

184+
/// <summary>
185+
/// Set while the integrator normalises an integrand: a sum of piecewises in one is combined
186+
/// case by case whatever the count, as before
187+
/// <a href="https://github.com/asc-community/AngouriMath/issues/1874">#1874</a>. An
188+
/// integrand holding the cases of an answer -- what parts leaves, `u' v` with `v` an
189+
/// antiderivative -- is searched as one piecewise, where kept a sum it was taken apart
190+
/// term by term and every term searched: `(a + b atanh(c x^2))^2/x^5` went from 19
191+
/// seconds to 134. Answers stay sums.
192+
/// </summary>
193+
[ThreadStatic] internal static bool CombinesEveryPiecewiseSum;
194+
180195
/// <summary>
181196
/// A piecewise combined with a piecewise, case by case: every case of one against every
182197
/// case of the other, under the conjunction of their predicates.
@@ -227,7 +242,19 @@ private Entity CombinedCaseByCase(Piecewise a, Piecewise b,
227242
for (var i = 0; i < pairs.Count; i++)
228243
if (keep[i])
229244
cases.Add((ExpandOnTwoArguments(pairs[i].First.Expression, pairs[i].Second.Expression, operation, defaultCtor, isExact), pairs[i].Predicate).ToProvided());
230-
return MathS.Piecewise(cases);
245+
var combined = MathS.Piecewise(cases);
246+
// A sum or a difference is combined only where the cases line up: where they do not,
247+
// every case of one against every case of the other is the product of the two counts,
248+
// and the two piecewises as written say the same in the sum of them. Counted once the
249+
// cases no value reaches are gone, since a sum over `k < a` for each k is one case per
250+
// k after that and the product before it. The antiderivative of
251+
// `1/(b x^2 + m) - 1/(sqrt(a + b x^2) (b x^2 + m))` was nine cases in 3,685
252+
// characters, where its two terms are 287 and 685.
253+
// https://github.com/asc-community/AngouriMath/issues/1874
254+
if (this is Sumf or Minusf && !CombinesEveryPiecewiseSum && combined.InnerSimplified(isExact) is var simplified
255+
&& (simplified is Piecewise piecewise ? piecewise.Cases.Count() : 1) > a.Cases.Count() + b.Cases.Count())
256+
return defaultCtor(this, a, b);
257+
return combined;
231258
}
232259

233260
/// <summary>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Copyright (c) 2019-2026 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using System.Linq;
9+
using AngouriMath.Extensions;
10+
using Xunit;
11+
using static AngouriMath.Entity;
12+
13+
namespace AngouriMath.Tests.Core
14+
{
15+
/// <summary>
16+
/// A sum of two piecewises whose cases do not line up stays a sum of the two: combined case
17+
/// by case it lists every case of one against every case of the other.
18+
/// <a href="https://github.com/asc-community/AngouriMath/issues/1874">#1874</a>
19+
/// </summary>
20+
[Trait("Area", "Core")]
21+
public sealed class APiecewiseSumStaysASumTest
22+
{
23+
private static Entity ThreeSigns(string of, string positive, string zero, string negative)
24+
=> $"piecewise({positive} provided {of} > 0, {zero} provided {of} = 0, {negative} provided {of} < 0)".ToEntity();
25+
26+
[Fact]
27+
public void CasesOnDifferentQuantitiesStayASum()
28+
{
29+
var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("b", "x", "y", "z")).InnerSimplified;
30+
var asASum = Assert.IsType<Sumf>(sum);
31+
Assert.IsType<Piecewise>(asASum.Augend);
32+
Assert.IsType<Piecewise>(asASum.Addend);
33+
}
34+
35+
[Fact]
36+
public void CasesOnOneQuantityStillCombine()
37+
{
38+
// The three that agree and the two opposite strict signs: five, not nine.
39+
var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("a", "x", "y", "z")).InnerSimplified;
40+
Assert.IsType<Piecewise>(sum);
41+
}
42+
43+
[Fact]
44+
public void ASumHoldingAPiecewiseIsNotCopiedIntoTheCasesOfAnother()
45+
{
46+
var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("b", "x", "y", "z") + ThreeSigns("c", "u", "v", "w")).InnerSimplified;
47+
Assert.Equal(3, sum.Nodes.Count(node => node is Piecewise));
48+
}
49+
50+
[Fact]
51+
public void TheIssuesIntegralIsTheSumOfItsTerms()
52+
{
53+
Entity one = "1/(b*x^2 + m)";
54+
Entity two = "1/(sqrt(a + b*x^2)*(b*x^2 + m))";
55+
var whole = (one - two).Integrate("x").Stringize().Length;
56+
var apart = one.Integrate("x").Stringize().Length + two.Integrate("x").Stringize().Length;
57+
Assert.True(whole <= apart + 16, $"{whole} characters for the difference, {apart} for its terms");
58+
}
59+
}
60+
}

‎Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ public void AConstantFactorIsTheSameQuantity()
148148
[Fact]
149149
public void OppositeSignsStayWhereALaterCaseDoesNotTestTheQuantity()
150150
{
151-
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;
151+
// A product: a sum whose cases do not line up stays a sum of its two piecewises (#1874).
152+
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;
152153
Assert.Contains(Assert.IsType<Piecewise>(sum).Cases, @case => @case.Predicate.ToString().Contains("q > 0") && @case.Predicate.ToString().Contains("q < 0"));
153154
}
154155

0 commit comments

Comments
 (0)