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
24 changes: 24 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,30 @@ and reads nothing here ([#718](https://github.com/asc-community/AngouriMath/issu
| `"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 |

### A perfect square written with symbols is read as one

`(A + B x)(d + e x)/(a^2 + 2 a b x + b^2 x^2)^(5/2)` was left as written: the radicand is
`(a + b x)^2`, and the power is `|a + b x|^5`, but three conditions between the rule and the
rewrite each said no. The discriminant `4 a^2 b^2 - 4 a^2 b^2` is a zero
`InnerSimplified` does not collect, so the square read as an ordinary quadratic. The leading
coefficient `b^2` is not a *number*, though it is positive for a real parameter, which the answer
now says as `provided b^2 > 0`. And the rule's own bound on how large a radicand it will read --
there so that it costs nothing at every level of the descent -- is lifted at the top, where any
half-odd power of a square is the power of the modulus; below it the square root only, since a
sign written for a substitution's variable is a factor the rest of the search has to carry.

The sign goes in front of the integral rather than into the integrand, which is what every rule
here that writes one does: left inside, a rule below differentiated it, and
`sqrt(a^2 + 2 a b x + b^2 x^2) sqrt(c + e x + d x^2)` threw
`CannotEvalException: derivative(sgn(...))` rather than answering
([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"(A+B*x)*(d+pe*x)/(a^2+2*a*b*x+b^2*x^2)^(5/2)".Integrate("x")` | left unevaluated | the antiderivative, `provided b^2 > 0` |
| `"sqrt(a^2+2*a*b*x+b^2*x^2)*sqrt(c+pe*x+d*x^2)".Integrate("x")` | left unevaluated | the antiderivative |
| `"(a^2+2*a*b*x+b^2*x^2)^(5/2)".Integrate("x")` | the antiderivative, through the substitution search | `sgn(a + b x) (a + b x)^6 |b|^5/(6 b)` up to the form |

### `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 @@ -10197,6 +10197,20 @@ Entity Without(Entity side)
return answer;
}

/// <summary>
/// Whether <c>b^2 - 4ac</c> is zero, which makes <c>a x^2 + b x + c</c> a perfect square.
/// Symbolically as well as numerically: for <c>a^2 + 2 a b x + b^2 x^2</c> it is
/// <c>4 a^2 b^2 - 4 a^2 b^2</c>, which <see cref="Entity.InnerSimplified"/> does not
/// collect, so a guard that asks only what it evaluates to reads the square as an
/// ordinary quadratic.
/// </summary>
private static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c)
{
var discriminant = (b * b - Number.Integer.Create(4) * a * c).InnerSimplified;
return discriminant.Evaled is Number.Complex { IsZero: true }
|| discriminant.Vars.Any() && Functions.PartialFractions.Bare(discriminant.Simplify()).Evaled is Number.Complex { IsZero: true };
}

/// <summary>
/// A square root of a perfect square in <paramref name="x"/> is the modulus:
/// <c>sqrt(x^2)</c> is <c>|x|</c>, which for a real <c>x</c> is <c>sgn(x) x</c>, and
Expand Down Expand Up @@ -10225,14 +10239,23 @@ Entity Without(Entity side)
{
// Asked of every integrand at every depth, so the radicand is read as a polynomial
// only where it is small enough to be a written square: x^2, or a x^2 + b x + c.
static bool MayBeARootOfASquare(Entity node, Entity.Variable x)
// At the top, any half-odd power of a perfect square is the power of the modulus;
// below it the square root only, since a sign written for a substitution's variable
// is a factor the rest of the search carries -- `(1 + 2u + u^2)^(5/2)` under
// `u = e^(2x)` cost twenty seconds that way. A bare `x^2` is the exception at any
// depth, nothing else reading `(x^2)^(3/2)`.
var atTheTop = Integration.AnsweringTheQuestionAsked;
bool MayBeARootOfASquare(Entity node, Entity.Variable x)
=> node is Powf(var radicand, Number.Rational r) && r.ERational.Denominator.Equals(EInteger.FromInt32(2))
&& (r.ERational.Equals(ERational.Create(1, 2)) || radicand is Powf(var @base, var degree) && @base == x && degree == Number.Integer.Create(2))
&& radicand.ContainsNode(x) && radicand.Complexity <= 12
&& (atTheTop || r.ERational.Equals(ERational.Create(1, 2))
|| radicand is Powf(var @base, var degree) && @base == x && degree == Number.Integer.Create(2))
&& radicand.ContainsNode(x) && radicand.Complexity <= (atTheTop ? 40 : 12)
&& radicand.Nodes.Count(inner => inner == x) <= 2;
if (!expr.Nodes.Any(node => MayBeARootOfASquare(node, x)))
return null;
var changed = false;
Entity assumed = Entity.Boolean.True;
Entity signs = Number.Integer.One;
var written = expr.Replace(node =>
{
if (!MayBeARootOfASquare(node, x))
Expand All @@ -10247,20 +10270,43 @@ static bool MayBeARootOfASquare(Entity node, Entity.Variable x)
var a = monomials.TryGetValue(EInteger.FromInt32(2), out var a2) ? a2 : Number.Integer.Zero;
var b = monomials.TryGetValue(EInteger.One, out var b1) ? b1 : Number.Integer.Zero;
var c = monomials.TryGetValue(EInteger.Zero, out var c0) ? c0 : Number.Integer.Zero;
if (a.Evaled is not Number.Real { IsPositive: true } leading || b.Evaled is not Number.Real || c.Evaled is not Number.Real
|| (b * b - Number.Integer.Create(4) * a * c).InnerSimplified.Evaled is not Number.Complex { IsZero: true })
// A positive leading coefficient, since `sqrt(a (x + h)^2)` is `sqrt(a) |x + h|`:
// a positive number, or one positive for a real parameter -- `b^2` is, and
// `a^2 + 2 a b x + b^2 x^2` is the square Rubi writes -- whose condition travels
// with the answer. The other two coefficients are real or symbolic, not a
// number off the line, for the same reason.
Entity leading;
if (a.Evaled is Number.Real { IsPositive: true })
leading = a;
else if (IsPositiveForARealParameter(a, x, out var leadingAssumed, assumed))
{
leading = a;
assumed = leadingAssumed;
}
else
return node;
if (b.Evaled is Number.Complex and not Number.Real || c.Evaled is Number.Complex and not Number.Real
|| !IsAPerfectSquareDiscriminant(a, b, c))
return node;
// a (x + h)^2 with h = b/(2a): (a (x + h)^2)^r is a^r |x + h|^(2r), and with 2r odd
// (a rational is held in lowest terms) that is a^r sgn(x + h) (x + h)^(2r).
var h = (b / (Number.Integer.Create(2) * a)).InnerSimplified;
var linear = h == Number.Integer.Zero ? x : (x + h).InnerSimplified;
var twoR = Number.Integer.Create(r.ERational.Numerator);
Entity power = twoR == Number.Integer.One ? linear : MathS.Pow(linear, twoR);
Entity signed = MathS.Signum(linear) * power;
// The sign in front of the integral rather than inside it: it is constant
// between the linear factor's zeros, and a rule below that differentiates the
// integrand cannot evaluate `derivative(sgn(...))` -- which is the exception
// `sqrt(a^2 + 2abx + b^2x^2) sqrt(c + ex + dx^2)` threw with it left in place.
signs = signs * MathS.Signum(linear);
changed = true;
return leading == Number.Integer.One ? signed : MathS.Pow(leading, r) * signed;
return leading == Number.Integer.One ? power : MathS.Pow(leading, r) * power;
});
return changed ? Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) : null;
if (!changed || Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) is not { } answer)
return null;
if (signs != Number.Integer.One)
answer = signs * answer;
return assumed == Entity.Boolean.True ? answer : answer.Provided(assumed);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,44 @@ private static void DifferentiatesBack(string integrand)
[InlineData("sqrt(sinh(x)^2)")]
public void ARootOfAPerfectSquareIsTheModulus(string integrand) => DifferentiatesBack(integrand);

/// <summary>
/// A square written with symbols: <c>a^2 + 2 a b x + b^2 x^2</c> is <c>(a + b x)^2</c>,
/// whose discriminant <c>4a^2b^2 - 4a^2b^2</c> <see cref="Entity.InnerSimplified"/> does
/// not collect, and whose leading coefficient <c>b^2</c> is not a number -- it is
/// positive for a real parameter, which is what the answer's <c>provided b^2 &gt; 0</c>
/// says. At the top the power may be any half-odd one, since the sign written there is
/// not a factor some substitution below has to carry. Rubi's 1.2.1.9.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Theory]
[InlineData("(a^2 + 2*a*b*x + b^2*x^2)^(5/2)")]
[InlineData("(A + B*x)*(d + pe*x)/(a^2 + 2*a*b*x + b^2*x^2)^(5/2)")]
[InlineData("1/(a^2 + 2*a*b*x + b^2*x^2)^(3/2)")]
public void ASquareWrittenWithSymbols(string integrand)
{
var integral = integrand.ToEntity().Integrate("x").Substitute("C", 0);
Assert.DoesNotContain("integral(", integral.Stringize());
Assert.DoesNotContain("NaN", integral.Stringize());
Entity Pin(Entity e) => e.Substitute("a", 0.9).Substitute("b", 1.7).Substitute("A", 0.7)
.Substitute("B", 1.3).Substitute("d", 0.4).Substitute("pe", 1.1);
var derivative = Pin(integral).Differentiate("x");
var original = Pin(integrand.ToEntity());
var compared = 0;
foreach (var at in Points)
{
var got = derivative.Substitute("x", at).EvalNumerical();
var want = original.Substitute("x", at).EvalNumerical();
if (got.IsNaN || want.IsNaN)
continue;
compared++;
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}");
}
Assert.True(compared >= 5, $"only {compared} points were comparable for {integrand}");
}

[Fact]
public void TheSignIsTheSignOfTheLinearFactor()
{
Expand Down
Loading