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
14 changes: 14 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,20 @@ angle, each of which the integrator answers
| `"x^3*(1-c^2*x^2)^(5/2)/(a+b*asin(c*x))".ToEntity().Integrate("x")` | `integral(...)` | 508 characters |
| `"x^3*(1-c^2*x^2)^(5/2)/(a+b*asin(c*x))^2".ToEntity().Integrate("x")` | `integral(...)` | 1,470 characters |

### A constant common to a half-angle denominator is divided out

**Answers where there were none.** `x/(k + k sin(x))^2` was declined, and so were `x/(d c + c d sin(x))^2` and Rubi's
5.1.4 `(a + b asin(c x))/((d + c d x)^(5/2) sqrt(f - c f x))`. After the half-angle substitution, such a denominator
is a constant times a polynomial with number coefficients, `k^2 (t + 1)^4`. The constant is now divided out before the
partial fractions
([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"x/(k+k*sin(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 285 characters |
| `"x/(d*c+c*d*sin(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 329 characters |
| `"(a+b*asin(c*x))/((d+c*d*x)^(5/2)*sqrt(f-c*f*x))".ToEntity().Integrate("x")` | `integral(...)` | 927 characters |

### A piecewise is defined where the case it takes is

**Domain conditions that were false.** The domain condition of a piecewise asked every case's predicate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32678,6 +32678,33 @@ bool IsAPolynomialInBoth(Entity polynomial)
?.Pipe(answer => underneath ? answer / constant : answer * constant);
}

/// <summary>
/// A rational function of <paramref name="t"/> as a polynomial over a polynomial, both divided by
/// the denominator's leading coefficient with each coefficient in lowest terms over the symbols,
/// so that a constant common to the denominator's coefficients cancels; null where either side is
/// not a polynomial in <paramref name="t"/> with coefficients free of it.
/// </summary>
private static Entity? WithTheLeadingCoefficientDividedOut(Entity rational, Entity.Variable t)
{
var (above, below) = Functions.SingleQuotient.Of(rational);
if (!TreeAnalyzer.TryGetPolynomial(above.Expand(), t, out var top) || !TreeAnalyzer.TryGetPolynomial(below.Expand(), t, out var bottom)
|| bottom.Count == 0 || top.Keys.Concat(bottom.Keys).Any(key => key.Sign < 0 || !key.CanFitInInt32())
|| top.Values.Concat(bottom.Values).Any(coefficient => coefficient.ContainsNode(t)))
return null;
var lead = bottom[bottom.Keys.Max()!];
// Only where the symbols in the denominator are one factor common to all of it: there the
// rules below formed `k^2/k^2`, and anywhere else this respelling sent fifteen of Rubi's
// 4.x problems the half-angle answered in a second to a timeout.
if (!lead.Vars.Any() || bottom.Values.Any(coefficient => Functions.PartialFractions.InLowestTermsOverTheSymbols(coefficient / lead) is not Number))
return null;
Entity Rebuilt(Dictionary<EInteger, Entity> monomials)
=> monomials.Aggregate((Entity)Number.Integer.Zero, (sum, pair)
=> sum + Functions.PartialFractions.InLowestTermsOverTheSymbols(pair.Value / lead)
// t^0 written as 1: as a power it brings `provided not t = 0` with it.
* (pair.Key.IsZero ? Number.Integer.One : MathS.Pow(t, Number.Integer.Create(pair.Key))));
return (Rebuilt(top) / Rebuilt(bottom)).InnerSimplified;
}

/// <summary>
/// A rational function of <c>sin(x)</c> and <c>cos(x)</c>, turned into a rational function
/// of one variable by the half-angle substitution <c>t = tan(x/2)</c> and handed to the
Expand Down Expand Up @@ -32869,6 +32896,14 @@ static bool NoRemainder(Entity rest)
if (divided)
integrand = (numerator / denominator).InnerSimplified;

// Over the denominator's leading coefficient, every coefficient in lowest terms over the
// symbols: `1/(k + k sin(x))^2` is `2 (1 + t^2)/(k^2 (t + 1)^4)`, and with `k^2` left in the
// coefficients the rules below formed `k^2/k^2`, which the simplification does not cancel,
// so the quadratic's discriminant was not decidably zero and `x/(k + k sin(x))^2` was
// answered `NaN` through a branch of the piecewise that can never hold.
if (integrand.Vars.Any(symbol => symbol != t) && WithTheLeadingCoefficientDividedOut(integrand, t) is { } normalized)
integrand = normalized;

return Integration.ComputeIndefiniteIntegral(integrand, t, integrateByParts) is { } result
? result.Substitute(t, MathS.Tan(argument / 2))
: null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// 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 constant common to the whole denominator after the half-angle substitution, divided out:
/// <c>x/(k + k sin(x))^2</c> was <c>NaN</c>, and so was Rubi's 5.1.4
/// <c>(a + b asin(c x))/((d + c d x)^(5/2) sqrt(f - c f x))</c>, which comes to it.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class AConstantCommonToAHalfAngleDenominatorIntegralTest
{
private static void DifferentiatesBack(string integrand, params (string, double)[] pins)
{
var integral = integrand.ToEntity().Integrate("x");
Assert.DoesNotContain("integral(", integral.Stringize());
Assert.DoesNotContain("NaN", integral.Stringize());

var pinned = integral.Substitute("C", 0);
Entity original = integrand.ToEntity();
foreach (var (name, value) in pins)
{
pinned = pinned.Substitute(name, value);
original = original.Substitute(name, value);
}
var derivative = pinned.Differentiate("x");
foreach (var at in new[] { -2.3, -0.7, 0.4, 1.9 })
{
var got = derivative.Substitute("x", at).EvalNumerical();
var want = original.Substitute("x", at).EvalNumerical();
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) + Math.Abs((double)want.ImaginaryPart));
Assert.True(difference / scale < 1e-8,
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
}

[Theory]
[InlineData("x/(k+k*sin(x))^2")]
[InlineData("1/(d*q+q*d*sin(x))^2")]
[InlineData("(a+b*asin(q*x))/((d+q*d*x)^(5/2)*sqrt(f-q*f*x))")]
public void IsAnsweredAndDifferentiatesBack(string integrand)
{
// |q x| below 1 at every sampled x, where the inverse sine and the roots are real.
DifferentiatesBack(integrand, ("k", 1.3), ("a", 0.7), ("b", -0.4), ("d", 1.1), ("f", 1.5), ("q", 0.3));
DifferentiatesBack(integrand, ("k", -0.7), ("a", -2), ("b", 1.5), ("d", 0.6), ("f", 2), ("q", -0.2));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// 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>
/// The half-angle substitution with a constant common to the denominator's coefficients: it is
/// divided out in lowest terms over the symbols, which the simplification does not do for
/// <c>k^2/k^2</c>. Kept, it left a quadratic whose discriminant was not decidably zero, and
/// <c>x/(k + k sin(x))^2</c> -- and Rubi's 5.1.4 <c>(a + b asin(c x))/((d + c d x)^(5/2) sqrt(f - c f x))</c>,
/// which comes to it -- were answered <c>NaN</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class AHalfAngleSubstitutionWithACommonConstantIntegralTest
{
private static void DifferentiatesBack(string integrand, params (string, double)[] pins)
{
var integral = integrand.ToEntity().Integrate("x");
Assert.DoesNotContain("integral(", integral.Stringize());

var pinned = integral.Substitute("C", 0);
Entity original = integrand.ToEntity();
foreach (var (name, value) in pins)
{
pinned = pinned.Substitute(name, value);
original = original.Substitute(name, value);
}
var derivative = pinned.Differentiate("x");
foreach (var at in new[] { -2.3, -0.7, 0.4, 1.9 })
{
var got = derivative.Substitute("x", at).EvalNumerical();
var want = original.Substitute("x", at).EvalNumerical();
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) + Math.Abs((double)want.ImaginaryPart));
Assert.True(difference / scale < 1e-8,
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
}

// d with the sign that keeps the argument real at every sampled x, and |d| below 2/2.3^2
// so that the inverse sine and cosine stay inside [-1, 1]; off it the exact checks hold too.
[Theory]
[InlineData("x/(k+k*sin(x))^2")]
[InlineData("1/(d*q+q*d*sin(x))^2")]
[InlineData("x/(k-k*cos(x))^2")]
[InlineData("(a+b*asin(q*x))/((d+q*d*x)^(5/2)*sqrt(f-q*f*x))")]
public void IsAnsweredAndDifferentiatesBack(string integrand)
{
Assert.DoesNotContain("NaN", integrand.ToEntity().Integrate("x").Stringize());
DifferentiatesBack(integrand, ("a", 0.6), ("b", 1.25), ("q", 0.3), ("d", 0.75), ("f", 1.4), ("k", 1.5));
DifferentiatesBack(integrand, ("a", -0.75), ("b", 0.4), ("q", -0.4), ("d", -2), ("f", 0.5), ("k", -0.3));
}
}
}
Loading