diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index dd5a2d106..609d2395b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -351,6 +351,22 @@ rule that does the same for a polynomial under a square root, whose answers are | `"1/sqrt(a*cot(x)^2)".Integrate("x")` | `-sgn(tan(x)) ln(1/sqrt(1 + abs(tan(x))^2))/sqrt(a)` | `sgn(cot(x)) ln(1 + tan(x)^2)/(2 sqrt(a))` | | `"(b*tan(x)^2)^(5/2)".Integrate("x")` | `sgn(tan(x)) b^(5/2) ((abs(tan(x))^2)^2/2 - abs(tan(x))^2 + ln(abs(tan(x))^2 + 1))/2` | the same with `tan(x)` for `abs(tan(x))` | +### A fractional power of a product of trigonometric functions keeps its sign + +**Wrong answers, silent.** The `sin^p cos^q` reader took a power of a product with an even +denominator as the product of the powers: `(sin(x) tan(x))^(3/2)` as `sin(x)^3 cos(x)^(-3/2)`, +where it is `|sin(x)|^3 cos(x)^(-3/2)`. After 2.5.0 the integrator answered it so, with the wrong +sign wherever the sine is negative and the cosine positive, where the integrand is real. What was +read is the integrand only up to a constant on each interval where the factors keep their signs, +so the answer is now multiplied by that constant, the integrand over what was read, and is the +integrand's antiderivative wherever both are defined. An odd denominator is read as before, as the +real root. Rubi's 4.7.7. + +| | Was (2.5.0) | Now | +|---|---|---| +| `"(sin(x)*tan(x))^(3/2)".Integrate("x")` | `integral((sin(x) * tan(x)) ^ (3/2), x)` | `(sin(x) tan(x))^(3/2)/(sin(x)^3 cos(x)^(-3/2))` times `2/sqrt(cos(x)) + 2/3 cos(x)^(3/2)` | +| `"(sin(x)*tan(x))^(1/2)".Integrate("x")` | `integral(sqrt(sin(x) * tan(x)), x)` | `sqrt(sin(x) tan(x))/(sin(x)/sqrt(cos(x)))` times `-2 sqrt(cos(x))` | + ### A shared symbol among a denominator sum's coefficients comes out in front of it **Improvement, not silent.** `tan(x)/(a + a csc(x))` came back unevaluated while diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 80bce58cc..b487816be 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -2702,11 +2702,23 @@ private static (Entity Constant, bool Plus, bool IsSine)? ReadAsOnePlusMinusAFun { if (!asked && !Integration.AnsweringTheQuestionAsked) return null; - if (!TryReadSineCosinePowers(expr, out var argument, out var sinePower, out var cosinePower, out var factor)) + if (!TryReadSineCosinePowers(expr, out var argument, out var sinePower, out var cosinePower, out var factor, out var throughAProduct)) return null; if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var rate, out _) || rate.Evaled == 0) return null; - return IntegrateAPowerOfSineTimesAPowerOfCosine(argument, sinePower, cosinePower, factor, rate); + if (IntegrateAPowerOfSineTimesAPowerOfCosine(argument, sinePower, cosinePower, factor, rate) is not { } integral) + return null; + if (!throughAProduct) + return integral; + // A fractional power of a product was read as the product of the powers, which is the + // integrand only up to a constant on each interval where the factors keep their signs: + // `(sin(x) tan(x))^(3/2)` is `|sin(x)|^3 cos(x)^(-3/2)` and was read as `sin(x)^3 cos(x)^(-3/2)`, + // wrong by its sign on every other half-turn. The integrand over what was read is that + // constant, so it multiplies the answer, and the answer is the integrand's wherever + // both are defined. + var read = factor * MathS.Pow(MathS.Sin(argument), Number.Rational.Create(sinePower)) + * MathS.Pow(MathS.Cos(argument), Number.Rational.Create(cosinePower)); + return expr / read * integral; } /// @@ -4834,16 +4846,27 @@ when @base.ContainsNode(x) /// private static bool TryReadSineCosinePowers( Entity expr, out Entity argument, out ERational sinePower, out ERational cosinePower, out Entity factor) + => TryReadSineCosinePowers(expr, out argument, out sinePower, out cosinePower, out factor, out _); + + /// + /// , + /// and whether a fractional power of a product was read as the product of the powers: + /// true only up to a constant on each interval where the factors keep their signs. + /// + private static bool TryReadSineCosinePowers( + Entity expr, out Entity argument, out ERational sinePower, out ERational cosinePower, out Entity factor, out bool throughAProduct) { Entity? common = null; var sine = ERational.Zero; var cosine = ERational.Zero; Entity constant = 1; + var distributed = false; var read = Read(expr, ERational.One); argument = common ?? 0; sinePower = sine; cosinePower = cosine; factor = constant; + throughAProduct = distributed; return read && common is not null && !(sine.IsZero && cosine.IsZero); bool Agrees(Entity candidate) @@ -4883,6 +4906,16 @@ bool Read(Entity node, ERational multiplicity) case Powf(var @base, Number.Rational power) when power is not Number.Integer && HasAnEvenPowerOfATrigonometricFunction(@base): return false; case Powf(var @base, Number.Rational power): + // Through anything but one function, or a positive multiple of one, a + // power with an even denominator splits across factors whose signs it + // does not see: `(sin tan)^(3/2)` is real where the sine is negative and + // `sin^3 cos^(-3/2)` is its negative there. An odd denominator is read as + // it always was, the real root: `(c sin^3)^(1/3)` as `c^(1/3) sin`, which + // is the integrand wherever that is real. + if ((multiplicity * power.ERational).ToLowestTerms().Denominator.IsEven + && @base is not TrigonometricFunction + && !(@base is Mulf(Number.Rational { IsPositive: true }, TrigonometricFunction))) + distributed = true; return Read(@base, multiplicity * power.ERational); // A rational factor rides along; anything else is declined rather than // carried, since carrying it would claim the rest of the product is diff --git a/Sources/Tests/UnitTests/Calculus/TrigonometricPowerProductTest.cs b/Sources/Tests/UnitTests/Calculus/TrigonometricPowerProductTest.cs index 0265bbf4b..efd752e2c 100644 --- a/Sources/Tests/UnitTests/Calculus/TrigonometricPowerProductTest.cs +++ b/Sources/Tests/UnitTests/Calculus/TrigonometricPowerProductTest.cs @@ -169,6 +169,41 @@ public void ARootOfAnEvenPowerIsNotThePower(string integrand) } } + /// + /// A fractional power of a product is the product of the powers only up to a constant + /// on each interval where the factors keep their signs: (sin(x) tan(x))^(3/2) is + /// |sin(x)|^3 cos(x)^(-3/2), and read as sin(x)^3 cos(x)^(-3/2) it was + /// integrated with the wrong sign wherever the sine is negative and the cosine positive, + /// where the integrand is real. The answer carries the constant, and is right at points + /// with either sign of either function. Rubi's 4.7.7. + /// + [Theory] + [InlineData("(sin(x)*tan(x))^(3/2)")] + [InlineData("(sin(x)*tan(x))^(1/2)")] + [InlineData("(sin(x)*tan(x))^(5/2)")] + [InlineData("cos(x)*(sin(x)*tan(x))^(3/2)")] + public void AFractionalPowerOfAProductKeepsItsSign(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("NaN", integral.Stringize()); + Assert.DoesNotContain("integral(", integral.Stringize()); + var derivative = integral.Substitute("C", 0).Differentiate("x"); + var original = integrand.ToEntity(); + var compared = 0; + foreach (var at in new[] { 0.7, -0.5, -1.2, 5.6, 2.1, 3.9 }) + { + 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)); + 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 >= 4, $"only {compared} points compared"); + } + /// /// Two proportional linears under roots are one radical with a constant in it: the /// quotient-of-two-radicals rule took them, its determinant was zero, and