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
27 changes: 27 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,33 @@ quotients that nothing below reads. Rubi's 6.3.2 and 6.5.3
| `"sech(a+2*ln(c/x^(1/2)))^3".Integrate("x")` | left unevaluated | the antiderivative |
| `"x*tanh(a+2*ln(x))^2".Integrate("x")` | left unevaluated | the antiderivative |

### An exponential times a trigonometric at the same frequency no longer divides by zero, and a phase is expanded

`x e^(-i x) cos(x)` was answered `NaN` at every point of its domain. The closed form
`int e^(a x)(c cos(b x) + d sin(b x)) = e^(a x)(...)/(a^2 + b^2)` is guarded against the resonance
`a = ±i b`, where the product is a sum of two exponentials and the formula divides by zero -- but
the guard tested whether `a^2 + b^2` *evaluates* to a number, and with a symbolic frequency
`(-i f)^2 + f^2` is `-f^2 + f^2`, which `InnerSimplified` does not collect. So the guard passed,
the formula divided by a zero it could not see, and the answer was `NaN`: a wrong answer, not a
decline. Both places that test it -- the table pattern and the polynomial route -- see the
resonance symbolically now.

With that fixed, two capabilities: a **phase** in the trigonometric's argument is expanded by the
angle-sum identity where an exponential stands beside it (`cos(f x + p)` is
`cos(p) cos(f x) - sin(p) sin(f x)`; the rule reads one frequency and no phase, an exponential's
own offset being a constant factor where a trigonometric's is not), and **`A + i A tan(z)` below
the bar** is written as `A e^(i z)/cos(z)`, with `A + i A cot(z)` as `i A e^(-i z)/sin(z)` --
exactly, since `cos z + i sin z` is `e^(i z)`. Beside a polynomial that is the shape the closed
rule answers, where the imaginary unit in the coefficient is read by nothing else. Rubi's 4.3.10,
4.4.1 and 4.4.10 ([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"x*e^(-i*x)*cos(x)".Integrate("x")` | left unevaluated | `x^2/4 + e^(-2 i x)(1 + 2 i x)/8` up to the form -- where `master` between the releases answered `NaN` |
| `"x*e^(2*x)*cos(3*x+1)".Integrate("x")` | left unevaluated | the antiderivative |
| `"x/(2+2*i*tan(x))".Integrate("x")` | left unevaluated | the antiderivative |
| `"(c+d*x)/(a+i*a*tan(pe+f*x))".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 @@ -3359,7 +3359,10 @@ void Add(int k, bool isSine, Entity coefficient)
var exponential = MathS.Pow(MathS.e, rate * x);
var cosine = MathS.Cos(frequency * x);
var sine = MathS.Sin(frequency * x);
if (scale == 0 || scale.Evaled is Number.Complex { IsZero: true })
// Symbolically as well as numerically: with a symbolic frequency `f` the rate `-i f`
// leaves `-f^2 + f^2`, which `InnerSimplified` does not collect.
if (scale == 0 || scale.Evaled is Number.Complex { IsZero: true }
|| scale.Vars.Any() && Functions.PartialFractions.Bare(scale.Simplify()).Evaled is Number.Complex { IsZero: true })
{
// Resonant: by Euler, `c cos(bx) + d sin(bx)` is
// `(c - i d) e^(i b x)/2 + (c + i d) e^(-i b x)/2`, and each term beside
Expand Down Expand Up @@ -14479,6 +14482,134 @@ static bool IsAWholeNumberOfSteps(ERational difference)
return (inFront * antiderivative - frontSlope * integralOfAntiderivative).InnerSimplified;
}

/// <summary>
/// A sine or cosine of a linear form with a phase, beside an exponential, expanded by the
/// angle-sum identity so that both are of the same bare multiple of the variable:
/// <c>cos(f x + p)</c> is <c>cos(p) cos(f x) - sin(p) sin(f x)</c>. The closed rule for a
/// polynomial times an exponential times a trigonometric reads one frequency and no
/// phase -- an exponential's own offset is a constant factor, a trigonometric's is not --
/// and a phase is what every row of Rubi's 4.3.10 and 4.4.10 carries once
/// <c>A + i A tan(pe + f x)</c> is written as an exponential over a cosine.
/// </summary>
/// <remarks>
/// Only beside an exponential of the variable, which is the shape that rule answers: the
/// expansion doubles the terms of every sine and cosine it touches, and the sum split
/// would then answer `sin(a + b x)` as two integrals where one closed rule answers it as
/// one. Once: the expanded form has no phase left.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
internal static Entity? SolveByExpandingATrigonometricPhaseBesideAnExponential(Entity expr, Entity.Variable x, bool integrateByParts)
{
if (!Integration.AnsweringTheQuestionAsked)
return null;
// An exponential of the variable among the factors, and a phase to expand.
if (!expr.Nodes.Any(node => node is Powf(var @base, var exponent) && !@base.ContainsNode(x) && exponent.ContainsNode(x)))
return null;
var expanded = expr.Replace(node =>
{
Entity argument;
bool isSine;
switch (node)
{
case Sinf(var inner): (argument, isSine) = (inner, true); break;
case Cosf(var inner): (argument, isSine) = (inner, false); break;
default: return node;
}
if (!argument.ContainsNode(x) || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out var offset)
|| TreeAnalyzer.IsZero(slope) || offset.ContainsNode(x) || TreeAnalyzer.IsZero(offset)
|| offset.Evaled is Number.Integer(0))
return node;
var bare = (slope * x).InnerSimplified;
return isSine
? MathS.Sin(offset) * MathS.Cos(bare) + MathS.Cos(offset) * MathS.Sin(bare)
: MathS.Cos(offset) * MathS.Cos(bare) - MathS.Sin(offset) * MathS.Sin(bare);
});
if (expanded == expr)
return null;
return Integration.ComputeAsAQuestionOfItsOwn(expanded.Expand().InnerSimplified, x, integrateByParts);
}

/// <summary>
/// <c>A + i A tan(z)</c> is <c>A e^(i z)/cos(z)</c>, and <c>A + i A cot(z)</c> is
/// <c>i A e^(-i z)/sin(z)</c> -- exactly, wherever the tangent is defined, since
/// <c>cos(z) + i sin(z)</c> is <c>e^(i z)</c>. A power of one of them below the bar is
/// then a power of the cosine times an exponential, and beside a polynomial that is a
/// shape the closed rules answer: <c>(c + d x)^2/(a + i a tan(pe + f x))^2</c> is a
/// search past the budget as written and a second once rewritten.
/// </summary>
/// <remarks>
/// The four signs are the four identities: <c>A - i A tan(z)</c> is
/// <c>A e^(-i z)/cos(z)</c> and <c>A - i A cot(z)</c> is <c>-i A e^(i z)/sin(z)</c>.
/// Rubi's 4.3.10 and 4.4.10, where every such row carries the imaginary unit in the
/// coefficient and nothing else reads it. The same question, not one of its own: what is
/// handed on is the integrand in another spelling.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
internal static Entity? SolveByWritingAnImaginaryTangentAsAnExponential(Entity expr, Entity.Variable x, bool integrateByParts)
{
// Below the bar only: `(A + i A tan(z))^3` above it is a polynomial in the tangent,
// which the rules for those answer in the tangent and more shortly, where the
// reciprocal is what nothing reads.
var (above, below) = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(expr));
if (!below.ContainsNode(x))
return null;
var rewritten = below.Replace(node =>
{
if (node is not Sumf and not Minusf || !node.ContainsNode(x))
return node;
Entity constant = Number.Integer.Zero;
Entity? coefficient = null;
Entity? argument = null;
var isTangent = false;
foreach (var term in Sumf.LinearChildren(node))
{
if (!term.ContainsNode(x))
{
constant += term;
continue;
}
if (coefficient is not null)
return node;
Entity factors = Number.Integer.One;
foreach (var factor in Mulf.LinearChildren(term))
switch (factor)
{
case Tanf(var inner) when argument is null:
(argument, isTangent) = (inner, true);
break;
case Cotanf(var inner) when argument is null:
(argument, isTangent) = (inner, false);
break;
default:
if (factor.ContainsNode(x))
return node;
factors *= factor;
break;
}
if (argument is null)
return node;
coefficient = factors;
}
if (coefficient is null || argument is null || constant == Number.Integer.Zero)
return node;
// The ratio is the imaginary unit one way or the other, and nothing else.
// Bare: `i a/a` simplifies to `i provided not a = 0`, and a condition is not a
// number to compare against.
var ratio = Functions.PartialFractions.Bare((coefficient / constant).InnerSimplified);
if (ratio.Evaled is not Number.Complex)
ratio = Functions.PartialFractions.Bare(ratio.Simplify());
var plus = ratio.Evaled == MathS.i.Evaled;
if (!plus && ratio.Evaled != (-MathS.i).Evaled)
return node;
var exponential = MathS.Pow(MathS.e, ((plus == isTangent ? MathS.i : -MathS.i) * argument).InnerSimplified);
// A + i A tan(z) is A e^(iz)/cos(z); A + i A cot(z) is i A e^(-iz)/sin(z).
return isTangent
? constant * exponential / MathS.Cos(argument)
: (plus ? MathS.i : -MathS.i) * constant * exponential / MathS.Sin(argument);
});
return rewritten == below ? null : Integration.ComputeAsTheSameQuestion((above / rewritten).InnerSimplified, x, integrateByParts);
}

/// <summary>
/// Bioche's first two rules for the hyperbolic functions: a rational function of
/// <c>sinh(y)</c> and <c>cosh(y)</c> that is odd in the hyperbolic sine is a rational
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,19 @@ private static bool IsZeroOnceSimplified(Entity expr)
/// exponential times a sine or cosine divides by: zero for a rate of <c>i</c> times
/// the frequency, where the product is a sum of two exponentials instead.
/// </summary>
/// <remarks>
/// Symbolically as well as numerically. With a symbolic frequency <c>f</c>, the rate
/// <c>-i f</c> -- which is what <c>A + i A tan(f x)</c> leaves once written as
/// <c>A e^(i f x)/cos(f x)</c> -- gives <c>-f^2 + f^2</c>, which
/// <see cref="Entity.InnerSimplified"/> does not collect, so the evaluation is not a
/// number and the guard passed: the answer then divided by a zero it could not see and
/// was <c>NaN</c> at every point.
/// </remarks>
private static bool NotResonant(Entity rate, Entity frequency)
=> (rate * rate + frequency * frequency).InnerSimplified.Evaled is not Entity.Number.Complex { IsZero: true };
{
var scale = (rate * rate + frequency * frequency).InnerSimplified;
return scale.Evaled is not Entity.Number.Complex { IsZero: true } && !IsZeroOnceSimplified(scale);
}

/// <summary>
/// Whether <paramref name="expr"/> is an exponential in <paramref name="x"/>, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// An exponential of a multiple of a logarithm is a power of the argument, which is
// how every inverse hyperbolic function under an exponential arrives.
if ((answer = IndefiniteIntegralSolver.SolveByFoldingAnExponentialOfALogarithm(expr, x, integrateByParts)) is { }) return answer;
// `A + i A tan(z)` is `A e^(i z)/cos(z)`, which beside a polynomial is a shape the
// closed rules answer, where the imaginary unit in the coefficient is read by none.
if ((answer = IndefiniteIntegralSolver.SolveByWritingAnImaginaryTangentAsAnExponential(expr, x, integrateByParts)) is { }) return answer;
// A fractional power of a perfect square is the power of the modulus, sgn(P) P^(2r).
if ((answer = IndefiniteIntegralSolver.SolveByTakingARootOfAPerfectSquare(expr, x, integrateByParts)) is { }) return answer;
// x^(n - 1) g(x^n) with a symbolic n is g(u)/n under u = x^n.
Expand Down Expand Up @@ -637,6 +640,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// rules, because none of them reads the exponential and all of them would have to
// decline it.
if ((answer = IndefiniteIntegralSolver.SolveAPolynomialTimesAnExponentialAndATrigonometric(expr, x)) is { }) return answer;
// And the same shape with a phase in the trigonometric's argument, expanded by the
// angle-sum identity so that the rule above reads one frequency and no phase.
if ((answer = IndefiniteIntegralSolver.SolveByExpandingATrigonometricPhaseBesideAnExponential(expr, x, integrateByParts)) is { }) return answer;
// A half-integer power of `1 + sin` or `1 - cos` and their kin, closed by one
// cancellation that `a^2 = b^2` allows. Before the reductions, which do not read a
// fractional power at all.
Expand Down
57 changes: 57 additions & 0 deletions Sources/Tests/UnitTests/Calculus/PolynomialExponentialTrigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,5 +205,62 @@ public void TheSearchThisAvoidsOpening(string integrand)
return;
DifferentiatesBack(integrand);
}

/// <summary>
/// A phase in the trigonometric's argument, expanded by the angle-sum identity where an
/// exponential stands beside it: the closed rule reads one frequency and no phase, an
/// exponential's own offset being a constant factor where a trigonometric's is not.
/// </summary>
[Theory]
[InlineData("x*e^(2*x)*cos(3*x + 1)")]
[InlineData("x^2*e^x*sin(x + 2)")]
[InlineData("e^(3*x)*cos(2*x + 1/2)*sin(2*x + 1/2)")]
public void APhaseIsExpandedBesideAnExponential(string integrand) => DifferentiatesBack(integrand);

/// <summary>
/// The resonant case, where the rate is the frequency times <c>i</c> and the closed
/// form's <c>a^2 + b^2</c> is zero: the product is a sum of two exponentials instead.
/// With a symbolic frequency <c>-f^2 + f^2</c> is not collected by
/// <see cref="Entity.InnerSimplified"/>, so the guard read it as a nonzero number and the
/// answer divided by it -- <c>NaN</c> at every point, for every row of Rubi's 4.3.10 that
/// carries <c>a + i a tan(pe + f x)</c> below the bar.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Theory]
[InlineData("x*e^(-i*f*x)*cos(f*x)", "f")]
[InlineData("e^(i*f*x)*sin(f*x)", "f")]
[InlineData("x^2*e^(-i*x)*cos(x)", null)]
public void TheResonanceIsSeenSymbolically(string integrand, string? symbol)
{
var integral = integrand.ToEntity().Integrate("x").Substitute("C", 0);
Assert.DoesNotContain("NaN", integral.Stringize());
Assert.DoesNotContain("integral(", integral.Stringize());
Entity Pin(Entity e) => symbol is null ? e : e.Substitute(symbol, 1.3);
var derivative = Pin(integral).Differentiate("x");
var original = Pin(integrand.ToEntity());
foreach (var at in Points)
{
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) + Math.Abs((double)want.ImaginaryPart));
Assert.True(difference / scale < 1e-9,
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
}

/// <summary>
/// <c>A + i A tan(z)</c> below the bar is <c>A e^(i z)/cos(z)</c>, and
/// <c>A + i A cot(z)</c> is <c>i A e^(-i z)/sin(z)</c>: beside a polynomial that is the
/// shape above, where the imaginary unit in the coefficient is read by nothing else.
/// Rubi's 4.3.10 and 4.4.10. Above the bar the tangent's own rules answer it, and the
/// rewrite leaves it alone.
/// </summary>
[Theory]
[InlineData("x/(2 + 2*i*tan(x))")]
[InlineData("x^2/(3 - 3*i*cot(x))")]
[InlineData("(1 + 2*x)/(3 + 3*i*tan(1/2 + x))^2")]
public void AnImaginaryTangentBelowTheBarIsAnExponential(string integrand) => DifferentiatesBack(integrand);
}
}
Loading