Skip to content

Commit b3c34db

Browse files
Integrate a trigonometric function of a + b/x beside a rational function in 1/x (#1878)
* Integrate a trigonometric function of a + b/x beside a rational function in 1/x Under x = 1/u, sin(c + d/x)/(e + f x) is -sin(c + d u)/(u (e u + f)) du, which the partial fractions and the sine and cosine integrals answer. Rubi 4.1.12:267, 268, 293, 294, 321, 330, 390-392, 395-397. Part of #718 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura * Record the reciprocal's new answers in BREAKING-CHANGES.md 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 f3dd6f6 commit b3c34db

4 files changed

Lines changed: 173 additions & 0 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,26 @@ in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take
396396
| `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters |
397397
| `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters |
398398

399+
### A trigonometric function of `a + b/x` beside a rational function is integrated in `1/x`
400+
401+
**Answers where there were none.** `sin(c + d/x)/(e + f x)` was declined: the substitution search reads a candidate's
402+
derivative but never solves the candidate for `x`. Under `x = 1/u` it is `-sin(c + d u)/(u (e u + f)) du`, which the
403+
partial fractions and the sine and cosine integrals answer, and `x = 1/u` is one-to-one on either side of 0. Below a
404+
root of a linear, the linear radical substitution gets there first
405+
([#718](https://github.com/asc-community/AngouriMath/issues/718)).
406+
407+
The answers below a root are large because the sine over the linears that come out of the substitution carries
408+
expanded coefficients ([#1877](https://github.com/asc-community/AngouriMath/issues/1877)).
409+
410+
| Input | Was (2.5.0) | Now |
411+
|---|---|---|
412+
| `"sin(c + d/x)/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 196 characters |
413+
| `"(a + b*sin(c + d/x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 470 characters |
414+
| `"(a + b*sin(c + d/x))^2/(e + f*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 2,516 characters |
415+
| `"(c*e + d*e*x)^(1/3)*sin(a + b/(c + d*x)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 1,453 characters |
416+
| `"sin(a + b/sqrt(c + d*x))/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 12,010 characters |
417+
| `"sin(a + b/sqrt(c + d*x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 32,780 characters |
418+
399419
### A power of the cosine beside a secant binomial is integrated in the cosine
400420

401421
**Answers where there were none.** `cos(c + d x)^2/(a + b sec(c + d x))` was declined, and so were the cosine's powers

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19120,6 +19120,77 @@ Entity TakeOut(Entity node)
1912019120
/// <summary>Set while <see cref="SolveByTakingTheWholePowersOutOfAPowerOfAProduct"/> asks its question.</summary>
1912119121
[System.ThreadStatic] private static bool takingTheWholePowersOut;
1912219122

19123+
/// <summary>
19124+
/// A rational function of <c>x</c> and of trigonometric functions of <c>a + b/x</c>, under
19125+
/// <c>x = 1/u</c>: <c>sin(c + d/x)/(e + f x)</c> is
19126+
/// <c>-sin(c + d u)/(u (e u + f)) du</c>, which the partial fractions and the sine and
19127+
/// cosine integrals answer.
19128+
/// </summary>
19129+
/// <remarks>
19130+
/// <c>x = 1/u</c> is one-to-one on either side of 0, so the answer in <c>u</c> written back
19131+
/// in <c>1/x</c> holds on both. Every argument of a function holding <c>x</c> has to be
19132+
/// linear in <c>1/x</c> and not in <c>x</c>, so the integrand in <c>u</c> is one this rule
19133+
/// does not read again. Rubi's 4.1.12 <c>(a + b sin(c + d/x))^k/(e + f x)^m</c> were
19134+
/// declined: the substitution search reads a candidate's derivative but never solves
19135+
/// the candidate for <c>x</c>. https://github.com/asc-community/AngouriMath/issues/718
19136+
/// </remarks>
19137+
internal static Entity? SolveByTheReciprocalOfAFunctionOfTheReciprocal(Entity expr, Entity.Variable x, bool integrateByParts)
19138+
{
19139+
var u = Variable.CreateUnique(expr, "u_rec");
19140+
// Each function of x, named, and written in u: its argument the linear in u it is.
19141+
var inU = new Dictionary<Entity, Entity>();
19142+
var names = new Dictionary<Entity, Variable>();
19143+
foreach (var node in expr.Nodes)
19144+
{
19145+
Entity argument;
19146+
switch (node)
19147+
{
19148+
case Sinf(var a): argument = a; break;
19149+
case Cosf(var a): argument = a; break;
19150+
case Tanf(var a): argument = a; break;
19151+
case Cotanf(var a): argument = a; break;
19152+
case Secantf(var a): argument = a; break;
19153+
case Cosecantf(var a): argument = a; break;
19154+
case Powf(_, Number.Integer):
19155+
case Sumf or Minusf or Mulf or Divf or Variable or Number:
19156+
continue;
19157+
default:
19158+
if (node.ContainsNode(x))
19159+
return null;
19160+
continue;
19161+
}
19162+
if (!argument.ContainsNode(x) || inU.ContainsKey(node))
19163+
continue;
19164+
if (!TreeAnalyzer.TryGetPolynomial(argument.Substitute(x, Number.Integer.One / u).InnerSimplified, u, out var read)
19165+
|| read.Keys.Any(k => k.Sign < 0 || k.CompareTo(EInteger.One) > 0) || !read.TryGetValue(EInteger.One, out var slope))
19166+
return null;
19167+
Entity linear = read.TryGetValue(EInteger.Zero, out var constant)
19168+
? constant.InnerSimplified + slope.InnerSimplified * u
19169+
: slope.InnerSimplified * u;
19170+
inU[node] = node switch
19171+
{
19172+
Sinf => MathS.Sin(linear),
19173+
Cosf => MathS.Cos(linear),
19174+
Tanf => MathS.Tan(linear),
19175+
Cotanf => MathS.Cotan(linear),
19176+
Secantf => MathS.Sec(linear),
19177+
Cosecantf => MathS.Cosec(linear),
19178+
_ => node,
19179+
};
19180+
names[node] = Variable.CreateUnique(expr, "f_rec" + names.Count);
19181+
}
19182+
if (inU.Count == 0)
19183+
return null;
19184+
// The rest one quotient, the way the rules read it: not `e + f/u` but `(e u + f)/u`.
19185+
var named = expr.Replace(node => names.TryGetValue(node, out var name) ? name : node);
19186+
var integrand = Functions.SingleQuotient.Combine(-named.Substitute(x, Number.Integer.One / u) / MathS.Sqr(u)).InnerSimplified;
19187+
foreach (var pair in names)
19188+
integrand = integrand.Substitute(pair.Value, inU[pair.Key]);
19189+
if (Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is not { } inTermsOfU)
19190+
return null;
19191+
return inTermsOfU.Substitute(u, Number.Integer.One / x);
19192+
}
19193+
1912319194
/// <summary>
1912419195
/// A root of a product beside a root of one of its factors, the product split:
1912519196
/// <c>(P Q)^r</c> is <c>K P^r Q^r</c>, where <c>K = (P Q)^r/(P^r Q^r)</c> is constant wherever

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
666666
if ((answer = IndefiniteIntegralSolver.SolveAGaussianInAReciprocal(expr, x)) is { }) return answer;
667667
// And a trigonometric function of the reciprocal of a linear beside a power of it, under u = 1/L.
668668
if ((answer = IndefiniteIntegralSolver.SolveAFunctionOfTheReciprocalOfALinear(expr, x, integrateByParts)) is { }) return answer;
669+
// And one of `a + b/x` beside any rational function, under `x = 1/u`:
670+
// `sin(c + d/x)/(e + f x)` is `-sin(c + d u)/(u (e u + f)) du`.
671+
if ((answer = IndefiniteIntegralSolver.SolveByTheReciprocalOfAFunctionOfTheReciprocal(expr, x, integrateByParts)) is { }) return answer;
669672
// An exponential of a multiple of a logarithm is a power of the argument, which is
670673
// how every inverse hyperbolic function under an exponential arrives.
671674
if ((answer = IndefiniteIntegralSolver.SolveByFoldingAnExponentialOfALogarithm(expr, x, integrateByParts)) is { }) return answer;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//
2+
// Copyright (c) 2019-2026 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using System;
9+
using AngouriMath.Extensions;
10+
using Xunit;
11+
12+
namespace AngouriMath.Tests.Calculus
13+
{
14+
/// <summary>
15+
/// A trigonometric function of <c>a + b/x</c> beside a rational function, under <c>x = 1/u</c>:
16+
/// <c>sin(c + d/x)/(e + f x)</c> is <c>-sin(c + d u)/(u (e u + f)) du</c>.
17+
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
18+
/// </summary>
19+
/// <remarks>
20+
/// Rubi's 4.1.12 <c>(a + b sin(c + d/x))^k/(e + f x)^m</c> were declined. Each answer is
21+
/// differentiated back on both sides of 0.
22+
/// </remarks>
23+
[Trait("Area", "Calculus")]
24+
public sealed class AFunctionOfTheReciprocalIntegralTest
25+
{
26+
private static void DifferentiatesBack(string integrand, double[] points, params (string, double)[] pins)
27+
{
28+
var integral = integrand.ToEntity().Integrate("x");
29+
Assert.DoesNotContain("integral(", integral.Stringize());
30+
Assert.DoesNotContain("NaN", integral.Stringize());
31+
32+
var derivative = integral.Substitute("C", 0).Differentiate("x");
33+
Entity original = integrand.ToEntity();
34+
foreach (var (name, value) in pins)
35+
{
36+
derivative = derivative.Substitute(name, value);
37+
original = original.Substitute(name, value);
38+
}
39+
40+
var compared = 0;
41+
foreach (var at in points)
42+
{
43+
var got = derivative.Substitute("x", at).EvalNumerical();
44+
var want = original.Substitute("x", at).EvalNumerical();
45+
if (got.IsNaN || want.IsNaN)
46+
continue;
47+
compared++;
48+
var difference = Math.Abs((double)(got - want).RealPart)
49+
+ Math.Abs((double)(got - want).ImaginaryPart);
50+
var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart));
51+
Assert.True(difference / scale < 1e-8,
52+
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, "
53+
+ $"where the integrand is {want}");
54+
}
55+
Assert.True(compared >= 3,
56+
$"only {compared} of {points.Length} points were comparable for {integrand}");
57+
}
58+
59+
[Theory]
60+
[InlineData("sin(c+d/x)/(e+f*x)")]
61+
[InlineData("cos(c+d/x)/(e+f*x)^2")]
62+
[InlineData("(a+b*sin(c+d/x))/(e+f*x)^3")]
63+
[InlineData("(a+b*sin(c+d/x))^2/(e+f*x)")]
64+
public void AFunctionOfAPlusBOverX(string integrand)
65+
{
66+
var points = new[] { -1.4, -0.3, 0.8, 2.25 };
67+
DifferentiatesBack(integrand, points, ("a", 1.0 / 3), ("b", 2), ("c", 1.5), ("d", 1.25), ("e", 2), ("f", 3));
68+
}
69+
70+
[Theory]
71+
[InlineData("(c*e+d*e*x)^(1/3)*sin(a+b/(c+d*x)^(1/3))")]
72+
[InlineData("sin(a+b/sqrt(c+d*x))/(e+f*x)")]
73+
public void AFunctionOfAPlusBOverARootOfALinear(string integrand)
74+
{
75+
var points = new[] { -0.3, 0.8, 2.25 };
76+
DifferentiatesBack(integrand, points, ("a", 1.0 / 3), ("b", 2), ("c", 1.5), ("d", 1.25), ("e", 2), ("f", 3));
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)