Skip to content

Commit fb520e2

Browse files
The integral over the root of a square quadratic is its modulus's, not the table's (#1672)
* The integral over the root of a square quadratic is its modulus's, not the table's The table's entry for k/sqrt(Q) and sqrt(Q) takes a quadratic apart by the sign of its leading coefficient. The arcsine arm divides by the root of the discriminant, and the logarithm arm takes ln|2ax + b + 2 sqrt(a) sqrt(Q)|, which is ln(0) on one side of the root when Q is a square: 1/sqrt(x^2 + 2x + 1) was ln(0) for every x below -1, and 1/sqrt(-a^2 - 2abx - b^2 x^2) NaN at every x. A linear beside the root writes Q in t = 1/(x - p), still a square, and asked the same entry. - The entry does not read a quadratic whose discriminant vanishes. Its root is the modulus of a linear, which the rule for a root of a perfect square writes. - That rule takes a leading coefficient negative for a real parameter too, since sqrt(a (x + h)^2) is sqrt(a) |x + h| for either sign: -a^2 - 2abx - b^2 x^2 is answered with sqrt(-b^2) and b^2 > 0. - The discriminant is tested with its symbols pinned first, and simplified only where it is zero there. Closes #1670. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura * A breaking-changes entry for the root of a square quadratic Measured on the 2.5.0 tag and on this branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent be009d5 commit fb520e2

4 files changed

Lines changed: 105 additions & 14 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,24 @@ here that writes one does: left inside, a rule below differentiated it, and
21282128
| `"sqrt(a^2+2*a*b*x+b^2*x^2)*sqrt(c+pe*x+d*x^2)".Integrate("x")` | left unevaluated | the antiderivative |
21292129
| `"(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 |
21302130

2131+
### The root of a quadratic that is a square is the modulus of a linear, and was read as any quadratic
2132+
2133+
**Wrong answers, silent.** The table's entry for `k/sqrt(Q)` and `sqrt(Q)` reads a quadratic by the
2134+
sign of its leading coefficient: an arcsine over the root of the discriminant, or the logarithm of
2135+
`2 a x + b + 2 sqrt(a) sqrt(Q)`. Where `Q` is a square the discriminant is zero, and that logarithm
2136+
is of zero beyond the root. So `1/sqrt(x^2 + 2x + 1)` had no value for any `x < -1`, where it is
2137+
`1/|x + 1|`, and with a negative leading coefficient there was no value anywhere. The root of a
2138+
square is now the modulus of its linear, `sqrt(a (x + h)^2) = sqrt(a) |x + h|`, for a leading
2139+
coefficient of either sign ([#1670](https://github.com/asc-community/AngouriMath/issues/1670)).
2140+
2141+
| Input | Was (2.5.0) | Now |
2142+
|---|---|---|
2143+
| `"1/sqrt(x^2+2*x+1)".Integrate("x")` | `ln(2 x + 2 + 2 sqrt(x^2 + 2 x + 1))`, `ln(0)` for every `x < -1` | `sgn(x + 1) ln(x + 1)` |
2144+
| `"sqrt(x^2+2*x+1)".Integrate("x")` | `(2 x + 2) sqrt(x^2 + 2 x + 1)/4 provided not 2 x + 2 + 2 sqrt(x^2 + 2 x + 1) = 0`, which says nothing for `x <= -1` | `sgn(x + 1) (x^2/2 + x)` |
2145+
| `"1/sqrt(-4-4*x-x^2)".Integrate("x")`, and `sqrt(-4 - 4 x - x^2)` | `NaN` | `sgn(x + 2) ln(i x + 2 i)/i`, and `i sgn(x + 2) (x^2/2 + 2 x)` |
2146+
| `"1/sqrt(-a^2-2*a*b*x-b^2*x^2)".Integrate("x")` | a piecewise whose arcsine divides by `sqrt(0)`, `NaN` for every real `b` but zero | a logarithm over `sqrt(-b^2)`, `provided b^2 > 0` |
2147+
| `"1/(x*sqrt(a^2+2*a*b*x+b^2*x^2))".Integrate("x")`, the same with `-a^2 - 2 a b x - b^2 x^2`, or with `d + e x` for `x` | left unevaluated | the antiderivative, `provided b^2 > 0` |
2148+
21312149
### A power of the variable times a sine or cosine of a logarithm, and a power of a monomial
21322150

21332151
`x^2 sin(a + b ln(c x^n))` and `(c x^n)^b` were both left as written. Two rules, each exact:

‎Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13697,14 +13697,11 @@ private static bool IsAWholeOrHalfOddMultiple(Entity timesN)
1369713697
/// Symbolically as well as numerically: for <c>a^2 + 2 a b x + b^2 x^2</c> it is
1369813698
/// <c>4 a^2 b^2 - 4 a^2 b^2</c>, which <see cref="Entity.InnerSimplified"/> does not
1369913699
/// collect, so a guard that asks only what it evaluates to reads the square as an
13700-
/// ordinary quadratic.
13700+
/// ordinary quadratic. Asked of every quadratic under a root the table meets, so the
13701+
/// simplification is only for a discriminant that is zero with its symbols pinned.
1370113702
/// </summary>
13702-
private static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c)
13703-
{
13704-
var discriminant = (b * b - Number.Integer.Create(4) * a * c).InnerSimplified;
13705-
return discriminant.Evaled is Number.Complex { IsZero: true }
13706-
|| discriminant.Vars.Any() && Functions.PartialFractions.Bare(discriminant.Simplify()).Evaled is Number.Complex { IsZero: true };
13707-
}
13703+
internal static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c)
13704+
=> VanishesIdentically(b * b - Number.Integer.Create(4) * a * c);
1370813705

1370913706
/// <summary>
1371013707
/// A square root of a perfect square in <paramref name="x"/> is the modulus:
@@ -13798,15 +13795,17 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x)
1379813795
var b = monomials.TryGetValue(half, out var b1) ? b1 : Number.Integer.Zero;
1379913796
var c = monomials.TryGetValue(EInteger.Zero, out var c0) ? c0 : Number.Integer.Zero;
1380013797
Entity w = half.Equals(EInteger.One) ? x : MathS.Pow(x, Number.Integer.Create(half));
13801-
// A positive leading coefficient, since `sqrt(a (x + h)^2)` is `sqrt(a) |x + h|`:
13802-
// a positive number, or one positive for a real parameter -- `b^2` is, and
13803-
// `a^2 + 2 a b x + b^2 x^2` is the square Rubi writes -- whose condition travels
13798+
// A leading coefficient of known sign, since `sqrt(a (x + h)^2)` is
13799+
// `sqrt(a) |x + h|` for either: a number, or one positive or negative for a real
13800+
// parameter -- `b^2` is, and `a^2 + 2 a b x + b^2 x^2` is the square Rubi writes,
13801+
// and `-b^2` is, whose square has an imaginary root -- whose condition travels
1380413802
// with the answer. The other two coefficients are real or symbolic, not a
13805-
// number off the line, for the same reason.
13803+
// number off the line, so that `(x + h)^2` is not negative.
1380613804
// A whole power needs no sign of anything: (a (w + h)^2)^n is a^n (w + h)^(2n).
13807-
if (!whole && a.Evaled is not Number.Real { IsPositive: true })
13805+
if (!whole && a.Evaled is not Number.Real { IsZero: false })
1380813806
{
13809-
if (!IsPositiveForARealParameter(a, x, out var leadingAssumed, assumed))
13807+
if (!IsPositiveForARealParameter(a, x, out var leadingAssumed, assumed)
13808+
&& !IsPositiveForARealParameter((-a).InnerSimplified, x, out leadingAssumed, assumed))
1381013809
return node;
1381113810
assumed = leadingAssumed;
1381213811
}

‎Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,11 @@ _ when GaussianMoment(expr, x) is { } moment => moment,
480480
/ (rate * rate + frequency * frequency),
481481

482482
// ∫ sqrt(ax^2 + bx + c) dx, which is one integration by parts away from the
483-
// reciprocal form below and is written in terms of it.
483+
// reciprocal form below and is written in terms of it. Not of a square, as below.
484484
Entity.Powf(var radicand, Entity.Number.Rational(Entity.Number.Integer(1), Entity.Number.Integer(2))) when
485485
TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var qa, out var qb, out var qc)
486486
&& qa.Evaled is Entity.Number.Complex { IsZero: false }
487+
&& !IndefiniteIntegralSolver.IsAPerfectSquareDiscriminant(qa, qb, qc)
487488
=> IntegrateRootOfQuadratic(qa, qb, qc, radicand, x),
488489

489490
// ∫ k / (x^2 * sqrt(ax^2 + c)) dx, the shape a trigonometric substitution is
@@ -496,10 +497,17 @@ _ when TryReadOverSquareTimesRoot(expr, x, out var overFactor, out var overRadic
496497

497498
// ∫ k / sqrt(ax^2 + bx + c) dx -- the arcsine and logarithm forms. Without
498499
// these, 1/sqrt(1 - x^2) had no antiderivative at all.
500+
// Not of a square: its root is the modulus of a linear, `sqrt(a (x + h)^2)` being
501+
// `sqrt(a) |x + h|`, which the rule for a root of a perfect square writes. Here the
502+
// arcsine divides by the root of the discriminant, zero, and the logarithm is of
503+
// `2a (x + h) + 2a |x + h|`, zero beyond the root: `1/sqrt(x^2 + 2x + 1)` was
504+
// `ln(0)` for every x below -1, and `1/sqrt(-a^2 - 2abx - b^2 x^2)` NaN everywhere.
505+
// https://github.com/asc-community/AngouriMath/issues/1670
499506
Entity.Divf(var numerator,
500507
Entity.Powf(var radicand, Entity.Number.Rational(Entity.Number.Integer(1), Entity.Number.Integer(2)))) when
501508
!numerator.ContainsNode(x)
502509
&& TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var ra, out var rb, out var rc)
510+
&& !IndefiniteIntegralSolver.IsAPerfectSquareDiscriminant(ra, rb, rc)
503511
=> IntegrateOverRootOfQuadratic(numerator, ra, rb, rc, radicand, x),
504512

505513
// ∫ (px + q)/(ax^2 + bx + c) dx. Only the constant numerator was covered, so

‎Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,72 @@ public void AWholePowerOfASquareInAPowerOfTheVariable(string integrand)
162162
Assert.True(compared >= 5, $"only {compared} points were comparable for {integrand}");
163163
}
164164

165+
/// <summary>
166+
/// The table's arcsine and logarithm for a root of a quadratic are not for a square: the
167+
/// arcsine divides by the root of the discriminant, and the logarithm is of zero beyond
168+
/// the root, so <c>1/sqrt(x^2 + 2x + 1)</c> was <c>ln(0)</c> for every x below -1 and
169+
/// <c>1/sqrt(-a^2 - 2 a b x - b^2 x^2)</c> NaN everywhere. Compared at every point where
170+
/// the integrand has a value, on both sides of each root -- an answer with none there is
171+
/// wrong, not incomparable -- with the leading coefficient of either sign: <c>-b^2</c> is
172+
/// negative for a real <c>b</c>, and the root of its square is imaginary. Rubi's
173+
/// 1.2.1.2 #2737 among them.
174+
/// <a href="https://github.com/asc-community/AngouriMath/issues/1670">#1670</a>
175+
/// </summary>
176+
[Theory]
177+
[InlineData("1/sqrt(x^2 + 2*x + 1)")]
178+
[InlineData("sqrt(x^2 + 2*x + 1)")]
179+
[InlineData("3/sqrt(9*x^2 - 6*x + 1)")]
180+
[InlineData("1/sqrt(-4 - 4*x - x^2)")]
181+
[InlineData("sqrt(-4 - 4*x - x^2)")]
182+
[InlineData("1/sqrt(-a^2 - 2*a*b*x - b^2*x^2)")]
183+
[InlineData("x/sqrt(-a^2 - 2*a*b*x - b^2*x^2)")]
184+
[InlineData("1/(x*sqrt(-a^2 - 2*a*b*x - b^2*x^2))")]
185+
[InlineData("1/(x*sqrt(a^2 + 2*a*b*x + b^2*x^2))")]
186+
[InlineData("1/((d + h*x)*sqrt(a^2 + 2*a*b*x + b^2*x^2))")]
187+
[InlineData("1/((d + h*x)*sqrt(-a^2 - 2*a*b*x - b^2*x^2))")]
188+
public void TheTableDoesNotReadASquareAsAQuadratic(string integrand)
189+
{
190+
var integral = integrand.ToEntity().Integrate("x").Substitute("C", 0);
191+
Assert.DoesNotContain("integral(", integral.Stringize());
192+
HoldsWhereverTheIntegrandHasAValue(integrand, integral);
193+
}
194+
195+
/// <summary>
196+
/// A square with a symbol of no known sign in front, <c>c (a + b x)^2</c>, is not read as
197+
/// one, and the table's forms gave no value between its roots: declined, or answered
198+
/// everywhere the integrand has a value.
199+
/// </summary>
200+
[Fact]
201+
public void ASquareOfUnknownSignIsNotAnsweredWrongly()
202+
{
203+
var integrand = "1/(x*sqrt(c*(a + b*x)^2))";
204+
var integral = integrand.ToEntity().Integrate("x").Substitute("C", 0);
205+
if (!integral.Stringize().Contains("integral("))
206+
HoldsWhereverTheIntegrandHasAValue(integrand, integral);
207+
}
208+
209+
private static void HoldsWhereverTheIntegrandHasAValue(string integrand, Entity integral)
210+
{
211+
Entity Pin(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.6)
212+
.Substitute("d", 1.9).Substitute("h", 1.1);
213+
var derivative = Pin(integral).Differentiate("x");
214+
var original = Pin(integrand.ToEntity());
215+
var compared = 0;
216+
foreach (var at in new[] { -3.7, -2.6, -1.8, -1.2, -0.6, 0.4, 1.9 })
217+
{
218+
var want = original.Substitute("x", at).EvalNumerical();
219+
if (want.IsNaN)
220+
continue;
221+
var got = derivative.Substitute("x", at).EvalNumerical();
222+
compared++;
223+
var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart);
224+
var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart));
225+
Assert.True(difference / scale < 1e-9,
226+
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}: {integral}");
227+
}
228+
Assert.True(compared >= 6, $"only {compared} points were comparable for {integrand}");
229+
}
230+
165231
[Fact]
166232
public void TheSignIsTheSignOfTheLinearFactor()
167233
{

0 commit comments

Comments
 (0)