diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index f01c6a30b..c224df473 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -2821,7 +2821,10 @@ private static bool TryReadAHomogeneousTrigonometricPolynomial(
var found = (Entity?)null;
var theDegree = (int?)null;
- foreach (var term in Entity.Sumf.LinearChildren(expr.Expand()))
+ // The terms with their degrees, for the parity case below.
+ var terms = new List<(Entity Term, int Degree)>();
+ var expanded = expr.Expand();
+ foreach (var term in Entity.Sumf.LinearChildren(expanded))
{
var thisDegree = 0;
foreach (var factor in Entity.Mulf.LinearChildren(term))
@@ -2848,10 +2851,11 @@ private static bool TryReadAHomogeneousTrigonometricPolynomial(
break;
}
}
+ terms.Add((term, thisDegree));
if (theDegree is null)
theDegree = thisDegree;
else if (theDegree != thisDegree)
- return false;
+ theDegree = -1; // not homogeneous as written; the parity case below
}
if (theDegree is null)
return false;
@@ -2863,9 +2867,31 @@ private static bool TryReadAHomogeneousTrigonometricPolynomial(
return !expr.ContainsNode(x);
}
- degree = theDegree.Value;
+ // **Homogeneous up to parity is homogeneous**: a term two degrees short of the
+ // highest is the same term times `sin^2 + cos^2`, which is one. `sin + sin^2 cos`
+ // has degrees one and three and is `sin (sin^2 + cos^2) + sin^2 cos`, of degree
+ // three throughout; it is what `cos(x)/(sin(x)(2 + sin(2x)))` has below the bar once
+ // its arguments are unified, and it was declined here for the spelling. A term an
+ // odd number short is a genuine boundary -- no power of one bridges it.
+ var highest = terms.Max(pair => pair.Degree);
+ if (theDegree == -1)
+ {
+ if (terms.Any(pair => (highest - pair.Degree) % 2 != 0))
+ return false;
+ var pythagorean = MathS.Sqr(MathS.Sin(found)) + MathS.Sqr(MathS.Cos(found));
+ Entity raised = Number.Integer.Zero;
+ foreach (var (term, thisDegree) in terms)
+ {
+ var levels = (highest - thisDegree) / 2;
+ var lifted = levels == 0 ? term : term * MathS.Pow(pythagorean, levels);
+ raised = raised == Number.Integer.Zero ? lifted : raised + lifted;
+ }
+ expanded = raised.Expand();
+ }
+
+ degree = highest;
argument = found;
- rewritten = expr.Expand().Replace(node => node switch
+ rewritten = expanded.Replace(node => node switch
{
Sinf(var a) when a == found => HomogeneousTangent * HomogeneousCosine,
Cosf(var a) when a == found => HomogeneousCosine,
diff --git a/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs b/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs
index 804c6e57f..e0dd61aed 100644
--- a/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs
@@ -102,9 +102,26 @@ public void ALinearArgument(string integrand, double[] points)
=> DifferentiatesBack(integrand, points);
///
- /// What the read must refuse: a polynomial that is not homogeneous, and a degree
- /// difference that is odd — there the substitution leaves a square root of 1 + t^2
- /// behind, which is a different problem.
+ /// Homogeneous up to parity is homogeneous: a term two degrees short of the highest is
+ /// the same term times sin^2 + cos^2, which is one. sin + sin^2 cos has
+ /// degrees one and three and is sin (sin^2 + cos^2) + sin^2 cos, of degree three
+ /// throughout; it is what Timofeev's cos(x)/(sin(x)(2 + sin(2x))) has below the
+ /// bar once its arguments are unified, and it was declined for the spelling.
+ ///
+ [Theory]
+ [InlineData("cos(x)/(sin(x) + sin(x)^2*cos(x))", new[] { 0.3, 0.8, 1.2, 2.0 })]
+ [InlineData("cos(x)/(sin(x)*(2 + sin(2*x)))", new[] { 0.3, 0.8, 1.2, 2.0 })]
+ [InlineData("1/(1 + sin(x)^2)", new[] { 0.3, 0.8, 1.2, -0.5 })]
+ [InlineData("(1 + sin(x)^2)/(1 + cos(x)^2)", new[] { 0.3, 0.8, 1.2, -0.5 })]
+ [InlineData("1/(sin(x)^2 + 2*sin(x)*cos(x))", new[] { 0.3, 0.8, 1.2, 2.0 })]
+ public void HomogeneousUpToParity(string integrand, double[] points)
+ => DifferentiatesBack(integrand, points);
+
+ ///
+ /// What the read must refuse: a polynomial whose degrees differ by an odd number, which
+ /// no power of sin^2 + cos^2 bridges, and a degree difference that is odd — there
+ /// the substitution leaves a square root of 1 + t^2 behind, which is a different
+ /// problem.
///
[Theory]
[InlineData("1/(1 + cos(x))", new[] { 0.3, 0.8, 1.2, -0.5 })]