From 6cd752382fddb9c254d589d14c5cd7fbf8c0eeb8 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 19:57:49 +0000 Subject: [PATCH] A function of a quotient of two linears is integrated over the quotient's denominator `sin((a + b x)/(c + d x))` and its powers, the cosine's, the hyperbolic sine's and cosine's and the exponential of the same quotient were declined, where `sin(p + k/(c + d x))` is answered in the sine and cosine integrals and `e^(p + k/(c + d x))` in the exponential integral. The quotient is `b/d + (a d - b c)/(d (c + d x))`; each such argument is written so, with the constant and the multiple named while the question is asked again. Rubi's 4.7.7, 6.1.5 and 6.2.5. Part of #718 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 17 ++++ .../Integration/IndefiniteIntegralSolver.cs | 91 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + ...otientOfLinearsAsAnArgumentIntegralTest.cs | 55 +++++++++++ 4 files changed, 166 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index dc39a1fe6..834bd9e0a 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -328,6 +328,23 @@ polynomial exponent in `u` now, and the question asked in `u`. Rubi's 2.3 | `"F^(a + b/(c + d*x))/(c + d*x)".ToEntity().Integrate("x")` | `integral(...)` | an exponential integral | | `"F^(a + b/(c + d*x)^3)*(c + d*x)^2".ToEntity().Integrate("x")` | `integral(...)` | an exponential integral of the cube, and the exponential | +### A function of a quotient of two linears is integrated over the quotient's denominator + +**Answers where there were none.** `sin((a + b x)/(c + d x))` and its powers were declined, and so +were the cosine's, the hyperbolic sine's and cosine's and the exponential of the same quotient, +where `sin(p + k/(c + d x))` is answered in the sine and cosine integrals and `e^(p + k/(c + d x))` +in the exponential integral. The quotient is one of those: `b/d + (a d - b c)/(d (c + d x))`, by +polynomial division, for `d` and `a d - b c` not zero. Each such argument is written so and the +question asked again. Rubi's 4.7.7, 6.1.5 and 6.2.5 +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin((a + b*x)/(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | in the sine and cosine integrals of `(a d - b c)/(d (c + d x))` | +| `"cos((a + b*x)/(c + d*x))^2".ToEntity().Integrate("x")` | `integral(...)` | in the sine and cosine integrals of twice that | +| `"sinh((a + b*x)/(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | in the exponential integrals of `±(a d - b c)/(d (c + d x))` | +| `"e^((a + b*x)/(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | in the exponential integral of `(a d - b c)/(d (c + d x))` | + ### A polynomial over a power of a binomial past the cube is integrated **Answers where there were none.** `P(x)/(a + b x^n)^k` with symbols in the binomial, `n >= 3`, was diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 875a31f1c..84b28927f 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -10519,6 +10519,97 @@ static bool IsEvenIn(Entity polynomial, Entity.Variable even, Entity.Variable ot return !inPlaceholder.ContainsNode(x) && TreeAnalyzer.TryGetPolynomial(inPlaceholder, placeholder, out _); } + /// + /// A function whose argument is a quotient of two linears, (a + b x)/(c + d x), with + /// that argument written over the denominator: b/d + (a d - b c)/(d (c + d x)), a + /// constant plus a multiple of the reciprocal of a linear. + /// + /// + /// + /// Rubi's sin((a + b x)/(c + d x)) and its powers were declined, and the hyperbolic + /// sine and cosine of the same, written in exponentials, likewise, where + /// sin(a + k/(c + d x)) is answered in sines and cosine integrals and + /// e^(a + k/(c + d x)) in exponential integrals: those rules read a reciprocal of a + /// linear, and a quotient of two linears is one plus a constant. Exact: polynomial division, + /// for d and a d - b c not zero, as everywhere in the integrator. + /// + /// + /// The arguments of the sine, cosine, tangent, cotangent, secant and cosecant and the + /// exponents of the exponential, each written so; the same question in another spelling. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveByWritingAQuotientOfLinearsOverItsDenominator(Entity expr, Entity.Variable x, bool integrateByParts) + { + var names = new Dictionary(); + Entity? Divided(Entity argument) + { + // Written as a quotient, or a constant times one: the sum this writes is neither, + // so the same question asked again does not divide it a second time. + Entity factor = Number.Integer.One; + var quotient = argument; + // The product's two children as written: read through, `-(a + b x)/(c + d x)` as a + // constant times a quotient is a quotient taken apart into its factors. + if (argument is Mulf(var left, var right)) + (factor, quotient) = left.ContainsNode(x) ? (right, left) : (left, right); + if (factor.ContainsNode(x)) + return null; + if (quotient is not Divf(var above, var below)) + return null; + if (!below.ContainsNode(x) || !above.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(above, x, out var b, out var a) || b.ContainsNode(x) || a.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(below, x, out var d, out var c) || d.ContainsNode(x) || c.ContainsNode(x)) + return null; + var remainder = (a * d - b * c).InnerSimplified; + if (remainder.Evaled is Number.Complex { IsZero: true } || d.Evaled is Number.Complex { IsZero: true }) + return null; + // The constant and the multiple named, each a symbol of its own while the question + // is asked: `sin(b/d + ((a d - b c)/d)/(c + d x))` was declined where + // `sin(p + k/(c + d x))` is answered, the rules reading a symbol where they meet a + // quotient of symbols. + Entity Named(Entity value) + { + value = value.InnerSimplified; + if (value is Variable || value is Number) + return value; + foreach (var pair in names) + if (pair.Value == value) + return pair.Key; + var name = Variable.CreateUnique(expr + names.Keys.Aggregate((Entity)Number.Integer.Zero, (sum, v) => sum + v), "k_quotient"); + names[name] = value; + return name; + } + // A constant in front stays in front, so that `e^Q` and `e^(-Q)` keep one argument + // and their product is 1. + var divided = Named(b / d) + Named(remainder / d) / below; + return factor == Number.Integer.One ? divided : factor * divided; + } + var changed = false; + var rewritten = expr.Replace(node => + { + Entity? divided; + switch (node) + { + case Sinf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Sin(divided); + case Cosf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Cos(divided); + case Tanf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Tan(divided); + case Cotanf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Cotan(divided); + case Secantf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Sec(divided); + case Cosecantf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Cosec(divided); + case Powf(var @base, var exponent) when @base == MathS.e && (divided = Divided(exponent)) is not null: + changed = true; + return MathS.Pow(MathS.e, divided); + default: + return node; + } + }); + if (!changed || Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts) is not { } answer) + return null; + foreach (var pair in names) + answer = answer.Substitute(pair.Key, pair.Value); + return answer; + } + /// /// Trigonometric functions of several arguments that are whole or rational multiples of one /// linear, p + q x, written in u = p + q x: csc(a + b x) csc(2a + 2b x)^2 diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 15f973f6b..82d0b2a4b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -853,6 +853,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveByTheHalfAngleTangentBesideAHalfOddPowerOfOnePlusASecant(expr, x, integrateByParts)) is { }) return answer; // And `a ± a cosh(y)` under a fractional power: `2a cosh(y/2)^2`, `-2a sinh(y/2)^2`. if ((answer = IndefiniteIntegralSolver.SolveByTheHalfAngleWhereOnePlusAHyperbolicCosineIsASquare(expr, x, integrateByParts)) is { }) return answer; + // A quotient of linears as a function's argument, written over its denominator: a + // constant plus a multiple of the reciprocal of a linear, which the rules read. + if ((answer = IndefiniteIntegralSolver.SolveByWritingAQuotientOfLinearsOverItsDenominator(expr, x, integrateByParts)) is { }) return answer; // Several trigonometric arguments that are multiples of one linear with an offset or a // symbolic slope, written in that linear: before the substitution search, which reads // each function on its own. diff --git a/Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.cs new file mode 100644 index 000000000..24f73c174 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A sine, cosine or exponential of a quotient of two linears, (a + b x)/(c + d x), + /// written as the constant b/d plus (a d - b c)/d over c + d x, and + /// answered in the sine, cosine and exponential integrals. Rubi's 4.7.7, 6.1.5 and 6.2.5. + /// #718 + /// + /// + /// Checked by differentiating back with a = 0.3, b = 0.9, c = 1.1, + /// d = 0.7, on both sides of the pole at c + d x = 0. + /// + [Trait("Area", "Calculus")] + public sealed class QuotientOfLinearsAsAnArgumentIntegralTest + { + [Theory] + [InlineData("sin((a + b*x)/(c + d*x))")] + [InlineData("sin((a + b*x)/(c + d*x))^2")] + [InlineData("sin((a + b*x)/(c + d*x))^3")] + [InlineData("cos((a + b*x)/(c + d*x))")] + [InlineData("cos((a + b*x)/(c + d*x))^2")] + [InlineData("sinh((a + b*x)/(c + d*x))")] + [InlineData("sinh((a + b*x)/(c + d*x))^3")] + [InlineData("cosh((a + b*x)/(c + d*x))^2")] + [InlineData("e^((a + b*x)/(c + d*x))")] + [InlineData("sin(2*(a + b*x)/(c + d*x))")] + public void IsWrittenOverTheDenominator(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 0.3).Substitute("b", 0.9).Substitute("c", 1.1).Substitute("d", 0.7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -2.5, -2.0, 0.3, 0.8, 1.4 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}