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
15 changes: 15 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,21 @@ of a linear over a linear, which is answered in a second
| `"1/(sqrt(a + i*a*tan(x))*(c + d*tan(x))^(3/2))".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 2,217 characters |
| `"1/((a + i*a*tan(x))^(3/2)*(c + d*tan(x))^(5/2))".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 3,333 characters |

### A symbolic quadratic with a square discriminant is split into its linears before the division

**Answers where there were none.** `(d + e x)^8/(a d e + (c d^2 + a e^2) x + c d e x^2)^2` ran past a minute, with
eleven more of Rubi's 1.2.1.2: a power of a linear over a power of a quadratic that is that linear times another.
The quadratic's discriminant is the square of a polynomial in the symbols, so it is written as its two linears,
also where it has been made monic and its coefficients stand over the leading one; the power the numerator shares
then cancels before the division of the improper fraction
([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"(d + e*x)^8/(a*d*e + (c*d^2 + a*e^2)*x + c*d*e*x^2)^2".ToEntity().Integrate("x")` | past a minute | 5,126 characters |
| `"(a + b*x)^6/(a*c + (b*c + a*d)*x + b*d*x^2)^2".ToEntity().Integrate("x")` | past a minute | 3,592 characters |
| `"csc(e + f*x)^3/(a + b*tan(e + f*x)^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 7,992 characters |

### One linear twice is not two linear powers

**Answers that had no value.** `(a c + b c x)^(-3 - 2 p) (f + g x) (a^2 + 2 a b x + b^2 x^2)^p` was answered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,23 @@ Entity Back(Entity mapped)
if (SolveByReducingThePolynomialOverALinearFactor(expr, x, integrateByParts, alone: true) is { } overThePowersOfTheLinear)
return overThePowersOfTheLinear;

// A symbolic quadratic whose discriminant is a square in the symbols, written as its two
// linears before the division: the numerator may share one of them, and over the
// quadratic as written the division of `(d + e x)^8` ran past a minute.
// The numerator's sums are written as the split writes its linears, so that a power the
// two sides share is gathered and cancelled before anything else reads the quotient.
// Only where the numerator shares one of the linears, which is what the split is for:
// splitting `1 - c^2 x^2` that parts left beside an inverse hyperbolic cotangent, with
// nothing to cancel, sent 7.4.1's `(a + b arccoth(c x)) (d + e ln(1 - c^2 x^2))` from
// thirteen seconds past two minutes.
if (WithSymbolicQuadraticsInAPowerSplit(denominator, x, 1) is { } overLinears
&& WithSumsAsPolynomialsInTheSymbols(numerator, x) is var writtenAlike
&& SharesAFactor(writtenAlike, overLinears, x)
&& Patterns.GatherPowersOfOneBase(writtenAlike / overLinears) is var overTheSplit
&& (SolveByPartialFractions(overTheSplit, x, integrateByParts)
?? Integration.ComputeIndefiniteIntegral(overTheSplit, x, integrateByParts)) is { } overTheLinearsOfAQuadratic)
return overTheLinearsOfAQuadratic;

// The helper answers null for a fraction that is already proper, so this cannot
// fire on one and recurse into the problem it started from. The check on the
// quotient is the second half of that guarantee: a division that came back with
Expand All @@ -750,6 +767,12 @@ is var (multiple, leftover)
&& (leftover is Divf(var leftoverTop, _) ? leftoverTop : leftover).InnerSimplified.Evaled is Number.Complex { IsZero: true })
return (multiple * MathS.Ln(denominator)).InnerSimplified;

// A symbolic quartic in x^2 that is two quadratics in it, written as them first.
if (WithSymbolicBiquadraticsSplit(denominator, x) is { } split
&& (SolveByPartialFractions(numerator / split, x, integrateByParts)
?? Integration.ComputeIndefiniteIntegral(numerator / split, x, integrateByParts)) is { } overTheQuadratics)
return overTheQuadratics;

// A written sum with a symbol among its coefficients that they all share is that
// symbol times a polynomial over the rationals, and is written so first: `(a u + a)`
// beside `1 - u^2` shares a root with it, which the symbolic split declines, and a
Expand All @@ -761,12 +784,6 @@ is var (multiple, leftover)
// And the powers of x it takes out joined to the ones beside them: `x (a x + b x^3 + c x^5)^2`
// is `x^3 (a + b x^2 + c x^4)^2`, and written `x x^2 (a + b x^2 + c x^4)^2` the splits
// below read `x` and `x^2` as two factors and the search ran past a minute.
// A symbolic quartic in x^2 that is two quadratics in it, written as them first.
if (WithSymbolicBiquadraticsSplit(denominator, x) is { } split
&& (SolveByPartialFractions(numerator / split, x, integrateByParts)
?? Integration.ComputeIndefiniteIntegral(numerator / split, x, integrateByParts)) is { } overTheQuadratics)
return overTheQuadratics;

if (WithTheContentOutOfEachSumFactor(denominator, x) is { } primitive
&& Patterns.GatherPowersOfOneBase(numerator / primitive) is var overThePrimitives
&& (SolveByPartialFractions(overThePrimitives, x, integrateByParts)
Expand Down Expand Up @@ -15041,6 +15058,54 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
return true;
}

/// <summary>
/// Whether <paramref name="above"/> vanishes at the root of a linear in <paramref name="x"/>
/// among the factors of <paramref name="below"/>, or the bases of their powers: whether the
/// two share that linear, up to a constant.
/// </summary>
private static bool SharesAFactor(Entity above, Entity below, Entity.Variable x)
{
// A polynomial numerator only: a logarithm in it is no value at a root, not zero there.
if (!TreeAnalyzer.TryGetPolynomial(above, x, out _))
return false;
foreach (var factor in Mulf.LinearChildren(below))
{
var @base = factor is Powf(var b, Number.Integer) ? b : factor;
if (@base is not (Sumf or Minusf) || !TreeAnalyzer.TryGetPolyLinear(@base, x, out var slope, out var intercept)
|| slope.ContainsNode(x) || intercept.ContainsNode(x) || TreeAnalyzer.IsZero(slope))
continue;
if (Functions.PartialFractions.IsZeroAsAValue(above.Substitute(x, -intercept / slope)))
return true;
}
return false;
}

/// <summary>
/// <paramref name="expr"/> with each sum among its factors, or the base of a power of one,
/// written as <see cref="MultivariatePolynomial.ToEntity"/> writes it: the spelling
/// <see cref="WithSymbolicQuadraticsInAPowerSplit"/> gives its factors, so that equal
/// factors on the two sides of a bar are written alike.
/// </summary>
private static Entity WithSumsAsPolynomialsInTheSymbols(Entity expr, Entity.Variable x)
{
Entity product = Number.Integer.One;
foreach (var factor in Mulf.LinearChildren(expr))
{
var (@base, power) = factor is Powf(var b, Number.Integer p) ? (b, (Entity)p) : (factor, (Entity)Number.Integer.One);
if (@base is (Sumf or Minusf) && @base.ContainsNode(x))
{
var variables = @base.Vars.OrderBy(v => v.Name, System.StringComparer.Ordinal).ToList();
var indices = new Dictionary<Variable, int>();
for (var i = 0; i < variables.Count; i++)
indices[variables[i]] = i;
if (variables.Count <= MultivariatePolynomial.MaxVariables && MultivariatePolynomial.TryParse(@base, indices) is { } polynomial)
@base = polynomial.ToEntity(variables);
}
product *= power == Number.Integer.One ? @base : MathS.Pow(@base, power);
}
return product;
}

/// <summary>
/// <paramref name="denominator"/> with each written factor <c>A x^4 + B x^2 + C</c> with symbols
/// in it whose discriminant <c>B^2 - 4AC</c> is the square of a polynomial <c>S</c> in them
Expand All @@ -15055,16 +15120,40 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
private static Entity? WithSymbolicBiquadraticsSplit(Entity denominator, Entity.Variable x)
=> WithSymbolicQuadraticsInAPowerSplit(denominator, x, 2);

/// <summary>
/// <paramref name="denominator"/> with each written factor <c>A x^(2k) + B x^k + C</c> with
/// symbols in it whose discriminant is the square of a polynomial <c>S</c> in them written as
/// <c>(2A x^k + B - S)(2A x^k + B + S)/(4A)</c>; <see langword="null"/> where there is none.
/// For <c>k = 1</c>, a quadratic as its two linears.
/// </summary>
/// <remarks>
/// Rubi's 1.2.1.2 has `(d + e x)^8/(a d e + (c d^2 + a e^2) x + c d e x^2)^2` and its kind, a
/// power of a linear over a power of a quadratic that is that linear times another. As written
/// the division of the improper fraction ran past a minute; over `(d + e x)(a e + c d x)` the
/// shared factor cancels and the rest is answered in a tenth of a second.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
private static Entity? WithSymbolicQuadraticsInAPowerSplit(Entity denominator, Entity.Variable x, int step)
{
var (low, middleDegree, high) = (EInteger.Zero, EInteger.FromInt32(step), EInteger.FromInt32(2 * step));
var changed = false;
Entity product = Number.Integer.One;
foreach (var factor in Mulf.LinearChildren(denominator))
{
var (@base, power) = factor is Powf(var b, Number.Integer { EInteger.Sign: > 0 } p) ? (b, p) : (factor, Number.Integer.One);
// A factor made monic has its coefficients over the leading one's symbols: split what is
// over the bar, and keep the bar.
Entity below = Number.Integer.One;
if (@base is (Sumf or Minusf) && @base.Nodes.Any(node => node is Divf(_, var under) && under.Vars.Any())
&& Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(@base)) is var (overTheBar, underTheBar)
&& !underTheBar.ContainsNode(x) && underTheBar != Number.Integer.One)
(@base, below) = (overTheBar, underTheBar);
if (@base is not (Sumf or Minusf) || !@base.ContainsNode(x) || !@base.Vars.Any(v => v != x)
|| !TreeAnalyzer.TryGetPolynomial(@base, x, out var read)
|| !read.Keys.All(degree => degree.Equals(EInteger.Zero) || degree.Equals(EInteger.FromInt32(2)) || degree.Equals(EInteger.FromInt32(4)))
|| !read.ContainsKey(EInteger.FromInt32(4)) || !read.ContainsKey(EInteger.Zero))
|| !read.Keys.All(degree => degree.Equals(low) || degree.Equals(middleDegree) || degree.Equals(high))
|| !read.ContainsKey(high) || !read.ContainsKey(low))
{
product *= factor;
continue;
Expand All @@ -15080,16 +15169,16 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
indices[variables[i]] = i;
var at = indices[x];
if (MultivariatePolynomial.TryParse(@base, indices) is not { } polynomial
|| !polynomial.CoefficientsIn(at).TryGetValue(4, out var a)
|| !polynomial.CoefficientsIn(at).TryGetValue(2 * step, out var a)
|| !polynomial.CoefficientsIn(at).TryGetValue(0, out var c))
{
product *= factor;
continue;
}
var b2 = polynomial.CoefficientsIn(at).TryGetValue(2, out var middle) ? middle : MultivariatePolynomial.Zero(variables.Count);
var b2 = polynomial.CoefficientsIn(at).TryGetValue(step, out var middle) ? middle : MultivariatePolynomial.Zero(variables.Count);
if (b2.Multiply(b2) is not { } bSquared || a.Multiply(c) is not { } ac
|| bSquared.Subtract(ac.ScaleBy(ERational.FromInt32(4))).TrySquareRoot() is not { IsZero: false } root
|| a.ScaleBy(ERational.FromInt32(2)).ShiftedBy(at, 2) is not { } twiceA)
|| a.ScaleBy(ERational.FromInt32(2)).ShiftedBy(at, step) is not { } twiceA)
{
product *= factor;
continue;
Expand All @@ -15098,8 +15187,8 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
var second = twiceA.Add(b2).Add(root).ToEntity(variables);
var scale = a.ScaleBy(ERational.FromInt32(4)).ToEntity(variables);
product *= power == Number.Integer.One
? first * second / scale
: MathS.Pow(first, power) * MathS.Pow(second, power) / MathS.Pow(scale, power);
? first * second / (scale * below)
: MathS.Pow(first, power) * MathS.Pow(second, power) / MathS.Pow(scale * below, power);
changed = true;
}
return changed ? product : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// 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;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Calculus
{
/// <summary>
/// A symbolic quadratic whose discriminant is a square in the symbols, written as its two
/// linears before the partial fractions, so that a power of one the numerator shares cancels.
/// Rubi's 1.2.1.2.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class ASymbolicQuadraticSplitIntegralTest
{
[Theory]
[InlineData("(d + g*x)^8/(a*d*g + (c*d^2 + a*g^2)*x + c*d*g*x^2)^2")]
[InlineData("(a + b*x)^6/(a*c + (b*c + a*d)*x + b*d*x^2)^3")]
public void OverItsLinears(string integrand)
{
var integral = integrand.ToEntity().Integrate("x");
var text = integral.Stringize();
Assert.DoesNotContain("integral(", text);
Assert.True(text.Length < 40000, $"{text.Length} characters of answer for {integrand}");
Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.4).Substitute("c", 1.9).Substitute("d", 1.6).Substitute("f", 0.7).Substitute("g", 0.45);
var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
var original = Pinned(integrand.ToEntity());
var compared = 0;
foreach (var at in new[] { -2.1, -0.4, 0.3, 0.6, 1.6, 2.5 })
{
var want = original.Substitute("x", at).EvalNumerical();
var got = derivative.Substitute("x", at).EvalNumerical();
if (want.IsNaN)
continue;
compared++;
Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
< 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)),
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}");
}
}
}
Loading