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
18 changes: 18 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,24 @@ the rules for a root of an even power.
to 309 of 422 with five fewer timeouts, and family 6 from 377 to 378; no row is lost in families
1, 4, 5, 6 or 7, and the 1774-problem suite stays at 1707 with no wrong answer.

### A power of `a cos + b sin` with symbolic coefficients is integrated under the tangent

`sec(x)^2/(a cos(x) + b sin(x))^4` was left as written after ten minutes, where
`sec(x)^2/(2 cos(x) + 3 sin(x))^4` took a tenth of a second. Both are quotients of
homogeneous polynomials in the sine and cosine, which `t = tan(x)` turns into a rational
function, and the rule that does it read the denominator by expanding it first: with numbers
the expanded quartic in `t` is `(2 + 3t)^4` again as soon as the rational integrator factors
it, and with symbols it is `a^4 + 4a^3 b t + 6a^2 b^2 t^2 + 4a b^3 t^3 + b^4 t^4`, which nothing
factors back. A whole power of a homogeneous factor is now read as that power of it, so the
substitution hands on `(1 + t^2)^2/(a + b t)^4`.

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"sec(x)^2/(a*cos(x)+b*sin(x))^4".Integrate("x")` | left unevaluated | a rational function of `tan(x)` |
| `"cos(x)/(a*cos(x)+b*sin(x))^3".Integrate("x")` | left unevaluated | a rational function of `tan(x)` |

Rubi's 4.7.2, `trig^m (a trig + b trig)^n`: 199 to 215 of 290, no row lost, 27 timeouts to 20.

### `binomial(n, k)` is a function

**Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4871,8 +4871,8 @@ node is Logf(var @base, var argument) && !@base.ContainsNode(x) && @base != Math
{
if (!TryReadAsQuotient(expr, out var numerator, out var denominator))
return null;
if (!TryReadAHomogeneousTrigonometricPolynomial(numerator, x, out var above, out var aboveDegree, out var argument)
|| !TryReadAHomogeneousTrigonometricPolynomial(denominator, x, out var below, out var belowDegree, out var otherArgument))
if (!TryReadAHomogeneousTrigonometricProduct(numerator, x, out var above, out var aboveDegree, out var argument)
|| !TryReadAHomogeneousTrigonometricProduct(denominator, x, out var below, out var belowDegree, out var otherArgument))
return null;
// A side free of the variable is degree zero and names no argument -- `1/(cos + sin)^2`
// is the common case and was declined for it. Only one side may be silent; if both
Expand Down Expand Up @@ -4903,6 +4903,60 @@ node is Logf(var @base, var argument) && !@base.ContainsNode(x) && @base != Math
: null;
}

/// <summary>
/// <see cref="TryReadAHomogeneousTrigonometricPolynomial"/> over a product, a factor at a
/// time, with a whole power of a factor read as that power of it: the degrees add, and
/// the rewritten factors are multiplied and raised as they were written.
/// </summary>
/// <remarks>
/// Reading the whole product at once expands it first, and an expanded power is a
/// polynomial the rational integrator has to factor again: <c>(a cos(x) + b sin(x))^4</c>
/// became <c>a^4 + 4 a^3 b t + 6 a^2 b^2 t^2 + 4 a b^3 t^3 + b^4 t^4</c> under
/// <c>t = tan(x)</c>, which with numbers is <c>(2 + 3t)^4</c> again in a moment and with
/// symbols is a quartic nothing factors -- <c>sec(x)^2/(a cos(x) + b sin(x))^4</c> ran past
/// ten minutes where <c>(1 + t^2)^2/(a + b t)^4</c> takes half a second. The whole product is
/// still read at once where a factor on its own is not homogeneous.
/// </remarks>
private static bool TryReadAHomogeneousTrigonometricProduct(
Entity expr, Entity.Variable x, out Entity rewritten, out int degree, out Entity? argument)
{
rewritten = Number.Integer.One;
degree = 0;
argument = null;
var factors = Mulf.LinearChildren(expr);
if (factors.Count > 1 || expr is Powf(_, Number.Integer))
{
var product = (Entity)Number.Integer.One;
var total = 0;
Entity? common = null;
var read = true;
foreach (var factor in factors)
{
var (@base, power) = factor is Powf(var b, Number.Integer whole) && whole.EInteger.CanFitInInt32() && !whole.EInteger.IsZero
? (b, whole.EInteger.ToInt32Checked())
: (factor, 1);
if (!TryReadAHomogeneousTrigonometricPolynomial(@base, x, out var one, out var oneDegree, out var oneArgument)
|| oneArgument is not null && common is not null && oneArgument != common)
{
read = false;
break;
}
common ??= oneArgument;
total += power * oneDegree;
var raised = power == 1 ? one : MathS.Pow(one, Number.Integer.Create(power));
product = product == Number.Integer.One ? raised : product * raised;
}
if (read)
{
rewritten = product;
degree = total;
argument = common;
return true;
}
}
return TryReadAHomogeneousTrigonometricPolynomial(expr, x, out rewritten, out degree, out argument);
}

/// <summary>The two placeholders a homogeneous polynomial is rewritten over.</summary>
[ConstantField] private static readonly Entity.Variable HomogeneousTangent =
Entity.Variable.CreateVariableOrConstant("__homogeneous_tangent");
Expand Down
36 changes: 35 additions & 1 deletion Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
Expand Down Expand Up @@ -65,6 +65,40 @@ private static void DifferentiatesBack(string integrand, double[] points)
+ "so this asserts almost nothing");
}

/// <summary>
/// The same with symbolic coefficients, where the power has to be read as a power. Read as
/// one expanded polynomial, <c>(a cos(x) + b sin(x))^4</c> became a quartic in the tangent
/// with a symbol in every coefficient, which the rational integrator cannot factor back
/// into <c>(a + b t)^4</c> -- with numbers it can, which is why the cases above never
/// showed it: <c>sec(x)^2/(a cos(x) + b sin(x))^4</c> ran past ten minutes and takes a
/// tenth of a second. Rubi's 4.7.2, sixteen rows. At <c>a = 1.7</c>, <c>b = 0.6</c> the
/// sum vanishes at <c>x = -1.23</c>, and the points stay clear of it.
/// </summary>
[Theory]
[InlineData("sec(x)^2/(a*cos(x) + b*sin(x))^4")]
[InlineData("1/(a*cos(x) + b*sin(x))^4")]
[InlineData("cos(x)/(a*cos(x) + b*sin(x))^3")]
[InlineData("sin(x)^3/(a*cos(x) + b*sin(x))^3")]
[InlineData("csc(x)/(a*cos(x) + b*sin(x))^3")]
public void APowerOfASumWithSymbolicCoefficients(string integrand)
{
var integral = integrand.ToEntity().Integrate("x");
Assert.DoesNotContain("NaN", integral.Stringize());
Assert.DoesNotContain("integral(", integral.Stringize());
var derivative = integral.Substitute("C", 0).Substitute("a", 1.7).Substitute("b", 0.6).Differentiate("x");
var original = integrand.ToEntity().Substitute("a", 1.7).Substitute("b", 0.6);
foreach (var at in new[] { 0.3, 0.8, 1.2, -0.5 })
{
var got = derivative.Substitute("x", at).EvalNumerical();
var want = original.Substitute("x", at).EvalNumerical();
Assert.False(got.IsNaN, $"the antiderivative of {integrand} differentiates to NaN at x = {at}");
var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart);
var scale = Math.Max(1.0, Math.Abs((double)want.RealPart));
Assert.True(difference / scale < 1e-9,
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
}

/// <summary>
/// A power of a sum of a sine and a cosine, which is the case that took 239 seconds to be
/// declined.
Expand Down
Loading