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
20 changes: 20 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,26 @@ keeps its decline ([#718](https://github.com/asc-community/AngouriMath/issues/71
| `"tan(x)/sqrt(2+tan(x)+tan(x)^2)".Integrate("x")` | left unevaluated | the antiderivative |
| `"tan(x)^2/(2+tan(x)+tan(x)^2)^(3/2)".Integrate("x")` | left unevaluated | the antiderivative |

### A power of the variable below the bar beside a root of a quadratic

`1/(x^2 sqrt(4 + 3x + 2x^2))` was left as written, while `1/(x sqrt(...))` came out: the single
factor was read and the repeated one was not. Under `x = 1/t` the integrand is
`-sgn(t) t^(n - 1)/sqrt(q0 t^2 + q1 t + q2)` -- a polynomial over the root of the quadratic read
the other way round, which the rules for those answer. That is what one round of parts against
`arccos(a + b x)/x^4` leaves, so the inverse trigonometric rows over a power of `x` come with it.

The radicand is assembled rather than substituted into, as `Q(1/t)` is `R(t)/t^2` and writing the
quotient inside the root leaves a nesting nothing reduces; the modulus that leaves is a `sgn(t)`
in front, which at `t = 1/x` is `sgn(x)`. `SolveByReciprocalSubstitution` is the same substitution
for a different shape -- a palindromic quartic, where the reciprocal maps the quartic to itself --
and reads nothing here ([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"1/(x^3*sqrt(4+3*x+2*x^2))".Integrate("x")` | left unevaluated | the antiderivative |
| `"1/(x^2*sqrt(1-(a+b*x)^2))".Integrate("x")` | left unevaluated | the antiderivative |
| `"acos(a+b*x)/x^4".Integrate("x")` | left unevaluated | the antiderivative |

### `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 @@ -14431,6 +14431,112 @@ private static (Entity Factor, Entity Inside)? TryTakeACommonLinearFactorOfASum(
return shifted.Nodes.Any(node => node == MathS.NaN) ? null : shifted;
}

/// <summary>
/// A power of the variable below the bar beside a root of a quadratic, by
/// <c>x = 1/t</c>: <c>1/(x^n sqrt(q0 + q1 x + q2 x^2))</c> is
/// <c>-sgn(t) t^(n - 1)/sqrt(q0 t^2 + q1 t + q2)</c>, a polynomial over the root of the
/// quadratic with its coefficients reversed, which the rules for those answer.
/// </summary>
/// <remarks>
/// <para>
/// <c>1/(x sqrt(1 - (a + b x)^2))</c> was answered and <c>1/(x^2 sqrt(...))</c> was not:
/// the repeated factor is what nothing reads, and it is what a round of parts against
/// <c>acos(a + b x)/x^4</c> leaves. <see cref="SolveByReciprocalSubstitution"/> is the
/// same substitution for a different shape -- a palindromic quartic under the root, where
/// the reciprocal maps the quartic to itself -- and reads nothing here.
/// </para>
/// <para>
/// The radicand is assembled rather than substituted into: <c>Q(1/t)</c> is
/// <c>R(t)/t^2</c> with <c>R</c> the reversed quadratic, so the root is
/// <c>R^(p/2) |t|^(-p)</c>, and writing the quotient inside the root instead leaves a
/// nesting nothing downstream reduces. The modulus is a <c>sgn(t)</c> in front for an odd
/// <c>p</c>, constant between its zeros; with <c>t = 1/x</c> it is <c>sgn(x)</c>, which is
/// what the answers of this family carry anyway.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </para>
/// </remarks>
internal static Entity? SolveByTheReciprocalBesideARootOfAQuadratic(Entity expr, Entity.Variable x, bool integrateByParts)
{
if (!Integration.AnsweringTheQuestionAskedOrOneBelow)
return null;
var (numerator, denominator) = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(expr));
// A power of x, two or more, below the bar; one root of a quadratic; polynomials else.
var powerOfX = 0;
Entity? radicand = null;
Number.Rational? rootPower = null;
Entity rest = Number.Integer.One;
foreach (var (side, isBelow) in new[] { (numerator, false), (denominator, true) })
foreach (var factor in Mulf.LinearChildren(side))
{
if (!factor.ContainsNode(x))
{
rest = isBelow ? rest / factor : rest * factor;
continue;
}
switch (factor)
{
case Variable bare when bare == x && isBelow:
powerOfX += 1;
continue;
case Powf(Variable bare, Number.Integer whole) when bare == x && isBelow && whole.EInteger.Sign > 0 && whole.EInteger.CanFitInInt32():
powerOfX += whole.EInteger.ToInt32Unchecked();
continue;
case Powf(var inner, Number.Rational power) when power is not Number.Integer && radicand is null:
if (!power.ERational.Denominator.Equals(EInteger.FromInt32(2)))
return null;
radicand = inner;
rootPower = isBelow ? Number.Rational.Create(power.ERational.Negate()) : power;
continue;
default:
if (!TreeAnalyzer.TryGetPolynomial(factor, x, out _))
return null;
rest = isBelow ? rest / factor : rest * factor;
continue;
}
}
if (powerOfX < 2 || radicand is null || rootPower is null)
return null;
if (!TreeAnalyzer.TryGetPolynomial(radicand, x, out var monomials) || monomials.Count == 0
|| monomials.Keys.Any(k => k.Sign < 0 || k.CompareTo(EInteger.FromInt32(2)) > 0)
|| !monomials.ContainsKey(EInteger.FromInt32(2)))
return null;
if (!TreeAnalyzer.TryGetPolynomial(rest, x, out var restMonomials) || restMonomials.Keys.Any(k => k.Sign < 0))
return null;

var t = Variable.CreateUnique(expr, "u_recip");
// Q(1/t) is R(t)/t^2 with R the quadratic read the other way round.
var q0 = monomials.TryGetValue(EInteger.Zero, out var m0) ? m0 : Number.Integer.Zero;
var q1 = monomials.TryGetValue(EInteger.One, out var m1) ? m1 : Number.Integer.Zero;
var q2 = monomials[EInteger.FromInt32(2)];
var reversed = (q0 * MathS.Sqr(t) + q1 * t + q2).InnerSimplified;
if (q0.Evaled is Number.Complex { IsZero: true })
return null; // the reversed quadratic is linear, which is the substitution's own shape
// The rest at 1/t, term by term, so that no quotient is left nested.
Entity restInT = Number.Integer.Zero;
foreach (var pair in restMonomials)
{
if (!pair.Key.CanFitInInt32())
return null;
var degree = pair.Key.ToInt32Unchecked();
restInT += degree == 0 ? pair.Value : pair.Value / MathS.Pow(t, Number.Integer.Create(degree));
}
// dx = -dt/t^2, x^(-n) = t^n, and the root is R^(p/2) |t|^(-p).
var numeratorPower = Number.Integer.Create(rootPower.ERational.Numerator);
Entity integrand = -restInT * MathS.Pow(t, Number.Integer.Create(powerOfX - 2))
* MathS.Pow(reversed, rootPower) * MathS.Pow(t, (-numeratorPower).InnerSimplified);
Entity signs = numeratorPower.EInteger.IsEven ? Number.Integer.One : MathS.Signum(t);
// Bare: the reciprocal's own `t != 0` rides along as a condition -- it is `1/x`, so
// it says what the integrand already says -- and a condition is not an integrand to
// the rules below, which decline it.
integrand = Functions.PartialFractions.Bare(integrand.InnerSimplified);
if (integrand.ContainsNode(x) || integrand.Nodes.Any(node => node == MathS.NaN))
return null;
if (Integration.ComputeAsAQuestionOfItsOwn(integrand, t, integrateByParts) is not { } answer)
return null;
var back = (signs == Number.Integer.One ? answer : signs * answer).Substitute(t, (Number.Integer.One / x).InnerSimplified);
return back.Nodes.Any(node => node == MathS.NaN) ? null : back;
}

/// <summary>
/// A pair of hyperbolic powers two apart whose coefficients kill the reduction's
/// residual: <c>cosh(y)^p - (p - 1)/p cosh(y)^(p - 2)</c> is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,10 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// And a root of a palindromic quartic, which is a root of a quadratic in x -+ 1/x:
// Charlwood's `(1 + x^2)/((1 - x^2) sqrt(1 + x^4))` is `-du/(u sqrt(u^2 + 2))`.
if ((answer = IndefiniteIntegralSolver.SolveByReciprocalSubstitution(expr, x)) is { }) return answer;
// And a power of the variable below the bar beside a root of a quadratic, by the
// same reciprocal: `1/(x^2 sqrt(Q))` is a polynomial over the root of the reversed
// quadratic, which the rules for those answer.
if ((answer = IndefiniteIntegralSolver.SolveByTheReciprocalBesideARootOfAQuadratic(expr, x, integrateByParts)) is { }) return answer;
// A rational function of x and one cube root of a polynomial: no substitution
// rationalises it, and the elementary ones are logarithms of `L - y` for linear L
// whose cube agrees with the polynomial at the poles, found by an ansatz.
Expand Down
28 changes: 28 additions & 0 deletions Sources/Tests/UnitTests/Calculus/ReciprocalSubstitutionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,33 @@ public void WhatIsNotAFunctionOfTheReciprocalVariable(string integrand)
return; // unanswered is a legitimate verdict; a wrong answer is not
DifferentiatesBack(integrand);
}

/// <summary>
/// A power of the variable below the bar beside a root of a quadratic, by the same
/// reciprocal: <c>1/(x^n sqrt(q0 + q1 x + q2 x^2))</c> is
/// <c>-sgn(t) t^(n - 1)/sqrt(q0 t^2 + q1 t + q2)</c>, a polynomial over the root of the
/// quadratic read the other way round. <c>1/(x sqrt(1 - (a + b x)^2))</c> was answered
/// and the repeated factor was not, which is what a round of parts against
/// <c>acos(a + b x)/x^4</c> leaves.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Theory]
[InlineData("1/(x^2*sqrt(1 + x^2))")]
[InlineData("1/(x^3*sqrt(4 + 3*x + 2*x^2))")]
[InlineData("1/(x^2*sqrt(1 + x + x^2))")]
[InlineData("1/(x^4*sqrt(2 - x + x^2))")]
public void APowerOfTheVariableBesideARootOfAQuadratic(string integrand) => DifferentiatesBack(integrand);

/// <summary>
/// And with symbols among the coefficients, which is the shape Rubi's 5.2 rows leave:
/// <c>acos(a + b x)/x^4</c> by parts is <c>1/(x^3 sqrt(1 - (a + b x)^2))</c> up to the
/// constants.
/// </summary>
[Theory]
[InlineData("1/(x^2*sqrt(1 - (a + b*x)^2))")]
[InlineData("1/(x^3*sqrt(1 - (a + b*x)^2))")]
[InlineData("acos(a + b*x)/x^4")]
public void TheSameWithSymbolicCoefficients(string integrand)
=> DifferentiatesBack(integrand, ("a", 0.3), ("b", 0.4));
}
}
Loading