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 @@ -557,6 +557,24 @@ Every spelling but `lcm` was a parse error in 2.5.0, the congruence node being n
| `forall n in ZZ : 6 divides n^3 + 5 n` | `UnhandledParseException` | `True` |
| `exists x, y in ZZ : 3 x^2 - 5 y^2 = 1` | `UnhandledParseException` | `False` |

### An antiderivative built through an even root says where the root is real

The linear-radical substitution, `u = (a x + b)^(1/q)`, and the general substitution for an even
root both take a power of `u` out of a radical on the strength of `u` being a non-negative real —
`sqrt(u^2 (1 + u))` as `u sqrt(1 + u)` — which it is only where the radical is. Beyond the
radicand's zero `u` is imaginary, and the integrand can still be real there: Rubi's
`x sqrt(c - a c x) / e^(3 atanh(a x))` above `a x = 1` is the product of two imaginary factors,
and the answer built for a real `u` is not its antiderivative — 14% off at `x = 0.59` with
`a = 3.1`, `c = 0.7`, and it shipped as an answer. An answer that used the step now carries the
condition, `provided a x + b >= 0`: `NaN` beyond it, which is no claim, and unchanged where it
holds. An answer the step did not touch — `x/sqrt(1 + x)`, `1/(sqrt(x) - x^(-1/3))` — is as it
was ([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"x * sqrt(c - a*c*x) / e^(3 * atanh(a*x))".Integrate("x")` | `integral(…)` — left unevaluated, with `e` evaluated to a hundred digits inside it | an antiderivative in `sqrt(c - a c x)`, `provided c - a * c * x >= 0` |
| `"1/sqrt(x + x^(3/2))".Integrate("x")` | `integral(1 / sqrt(x + x ^ (3/2)), x)` — left unevaluated | `2 * sqrt(1 + sqrt(x)) / (1/2) + C provided x >= 0` |

### `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 @@ -5169,10 +5169,16 @@ static bool IsTwice(Entity argument, Entity.Variable x)
/// two substitutions at once and is declined rather than half-rewritten.
/// </para>
/// <para>
/// <b>No condition is owed by the rewrite.</b> <c>a</c> is non-zero, since a zero
/// <b>No condition is owed by the rewrite itself.</b> <c>a</c> is non-zero, since a zero
/// coefficient is not a linear expression in <c>x</c> and the reader below rejects it,
/// and <c>u^q = a*x + b</c> is invertible wherever the radical it came from is defined.
/// The answer inherits the radical's own domain and adds nothing.
/// The answer inherits the radical's own domain and adds nothing -- <b>except where the
/// even-root step below has read <c>u</c> as a non-negative real</b>, which it is only
/// where the radical is real. Beyond that, <c>u</c> is imaginary and the integrand can
/// still be real: <c>x sqrt(c - a c x) / e^(3 atanh(a x))</c> above <c>a x = 1</c> is the
/// product of two imaginary factors, and the answer built for a real <c>u</c> is not its
/// antiderivative there (Rubi 7.3.6). So the answer says what it used: it is given
/// <c>provided a x + b &gt;= 0</c> exactly when that step changed the integrand.
/// </para>
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
Expand Down Expand Up @@ -5304,13 +5310,24 @@ node is Powf(var radical, Number.Rational e) && e is not Number.Integer
integrand = inner;
// For an even q the principal root is not negative wherever it is real, and a root
// holding a power of u gives that power up: `1/sqrt(t + t^(3/2))` under `u = sqrt(t)`
// is `2u/sqrt(u^2 + u^3)`, a root of a cubic, and is `2/sqrt(1 + u)`.
// is `2u/sqrt(u^2 + u^3)`, a root of a cubic, and is `2/sqrt(1 + u)`. Where the
// radical is not real, u is not a non-negative real and the identities do not hold,
// so an answer that used them is conditioned on the radicand -- unless the caller
// knows its variable to be non-negative, as an exponential is.
// https://github.com/asc-community/AngouriMath/issues/718
Entity? condition = null;
if (q % 2 == 0)
integrand = FactorANonnegativeVariableOutOfRadicals(integrand, u);
{
var factored = FactorANonnegativeVariableOutOfRadicals(integrand, u);
if (factored != integrand && !variableIsNonnegative)
condition = radicalBase >= Number.Integer.Zero;
integrand = factored;
}

return Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is { } result
? result.Substitute(u, MathS.Pow(radicalBase, Number.Rational.Create(1, q)))
: null;
if (Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is not { } result)
return null;
var back = result.Substitute(u, MathS.Pow(radicalBase, Number.Rational.Create(1, q)));
return condition is null ? back : back.Provided(condition);
}

/// <summary>
Expand Down Expand Up @@ -13787,11 +13804,16 @@ private static bool IsARationalFunctionOfExponentials(Entity expr, Entity.Variab
&& Integration.ComputeIndefiniteIntegral(factored, uSub, integrateByParts) is { } forPositiveU
&& !forPositiveU.Nodes.Any(node => node == MathS.NaN))
{
// An even root is not negative, and what holds for u > 0 is the answer as
// it stands; `sqrt(x^(1/3))` under `1/(sqrt(x) - x^(-1/3))` was extended by
// parity it did not need, to an answer with a sign function in it.
if (u is Powf(_, Number.Rational root) && root.ERational.Denominator.IsEven)
return Functions.PartialFractions.Bare(forPositiveU).Substitute(uSub, u);
// An even root is not negative where it is real, and what holds for u > 0
// is the answer as it stands there; `sqrt(x^(1/3))` under
// `1/(sqrt(x) - x^(-1/3))` was extended by parity it did not need, to an
// answer with a sign function in it. Where the radicand is negative the root
// is imaginary and the identities the factoring used do not hold, so the
// answer is conditioned on the radicand, as the linear-radical substitution
// conditions its own. https://github.com/asc-community/AngouriMath/issues/718
if (u is Powf(var radicand, Number.Rational root) && root.ERational.Denominator.IsEven)
return Functions.PartialFractions.Bare(forPositiveU).Substitute(uSub, u)
.Provided(radicand >= Number.Integer.Zero);
if (ExtendedByParity(integrandInU, forPositiveU, uSub) is { } onBothSides)
return onBothSides.Substitute(uSub, u);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,46 @@ private static void DifferentiatesBack(string integrand)
[InlineData("x/sqrt(1 + x^2 + (1 + x^2)^(3/2))")]
public void APowerOfTheRootLeavesTheRadical(string integrand) => DifferentiatesBack(integrand);

/// <summary>
/// That step reads <c>u</c> as a non-negative real, which it is only where the radical
/// is real, and the answer says so: <c>1/sqrt(x + x^(3/2))</c> comes back
/// <c>provided x &gt;= 0</c>. Beyond the radicand's zero <c>u</c> is imaginary, and the
/// integrand can still be real there -- Rubi's <c>x sqrt(c - a c x) / e^(3 atanh(a x))</c>
/// above <c>a x = 1</c> is the product of two imaginary factors -- while the answer built
/// for a real <c>u</c> is not its antiderivative: with <c>a = 3.1</c>, <c>c = 0.7</c> at
/// <c>x = 0.59</c> its derivative was 14% off, and it shipped as an answer. Now it is
/// <c>NaN</c> there, which is no claim, and right where the condition holds.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </summary>
[Fact]
public void TheEvenRootStepSaysWhereItHolds()
{
var conditioned = "1/sqrt(x + x^(3/2))".ToEntity().Integrate("x");
Assert.Contains("provided x >= 0", conditioned.Stringize());

var integrand = "x * sqrt(c - a*c*x) / e^(3 * atanh(a*x))".ToEntity();
var integral = integrand.Integrate("x");
Assert.DoesNotContain("integral(", integral.Stringize());
Assert.Contains("provided c - a * c * x >= 0", integral.Stringize());
var pinned = integral.Substitute("C", 0).Substitute("a", 3.1).Substitute("c", 0.7);
var derivative = pinned.Differentiate("x");
var original = integrand.Substitute("a", 3.1).Substitute("c", 0.7);
// Inside the condition, a x < 1: the derivative is the integrand.
foreach (var at in new[] { 0.05, 0.17, 0.31 })
{
var got = derivative.Substitute("x", at).EvalNumerical();
var want = original.Substitute("x", at).EvalNumerical();
Assert.False(got.IsNaN, $"no answer at x = {at}, inside the condition");
Assert.True(Math.Abs((double)(got - want).RealPart) < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)),
$"d/dx of the antiderivative is {got} at x = {at}, where the integrand is {want}");
}
// Beyond it the integrand is real, 3.1 * 0.59 being above 1, and the answer claims
// nothing rather than something wrong.
var outside = original.Substitute("x", 0.59).EvalNumerical();
Assert.True(Math.Abs((double)outside.ImaginaryPart) < 1e-12, $"the integrand at x = 0.59 is {outside}, not real");
Assert.True(derivative.Substitute("x", 0.59).Evaled.IsNaN, "the answer claims a derivative outside its condition");
}

/// <summary>
/// A polynomial over a square root of something linear. None of these had an
/// antiderivative, and each is a first-year exercise.
Expand Down
Loading