Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 32 additions & 18 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ internal static partial class Integration
/// </para>
/// </remarks>
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
Expand All @@ -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));

/// <summary>
/// <paramref name="expr"/> 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
/// </summary>
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;
}
}

/// <summary>
/// The integrals already answered under the settings in force, so that the same question
Expand Down
29 changes: 28 additions & 1 deletion Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -177,6 +181,17 @@ Entity ops(Entity a, Entity b)
};
}

/// <summary>
/// Set while the integrator normalises an integrand: a sum of piecewises in one is combined
/// case by case whatever the count, as before
/// <a href="https://github.com/asc-community/AngouriMath/issues/1874">#1874</a>. 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.
/// </summary>
[ThreadStatic] internal static bool CombinesEveryPiecewiseSum;

/// <summary>
/// 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.
Expand Down Expand Up @@ -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;
}

/// <summary>
Expand Down
60 changes: 60 additions & 0 deletions Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1874">#1874</a>
/// </summary>
[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<Sumf>(sum);
Assert.IsType<Piecewise>(asASum.Augend);
Assert.IsType<Piecewise>(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<Piecewise>(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");
}
}
}
3 changes: 2 additions & 1 deletion Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Piecewise>(sum).Cases, @case => @case.Predicate.ToString().Contains("q > 0") && @case.Predicate.ToString().Contains("q < 0"));
}

Expand Down
Loading