diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 259465ab3..4ed1a19c9 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -243,6 +243,26 @@ carry their coefficient as a divisor, and no rule found a squared sine and cosin
|---|---|---|
| `"cos(x)^2 - (1 + cos(2*x))/2".Simplify()` | `cos(x) ^ 2 - (1 + cos(2 * x)) / 2` | `0` |
+### A trigonometric identity is proved where it needs several of the rules in one order
+
+**Silent: an identity reaches its value.** The tangent addition formula, the half-angle formulas, the Weierstrass
+forms and sum-to-product were not proved. Each needs an angle opened, doubled or halved and the Pythagorean pair taken,
+in one order, and the rules take them a step at a time. A rational function of the six trigonometric functions, whose
+angles are rational combinations of its variables with no constant term, is now decided. It is written over a sine and
+a cosine of each variable's base angle, with every `sin(u)^2` written as `1 - cos(u)^2`, and it is a constant exactly
+when its numerator is that constant times its denominator. The constant is its value wherever it has one, and carries
+the expression's own domain as its condition ([#2142](https://github.com/asc-community/AngouriMath/issues/2142)).
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"tan(s + t) - (tan(s) + tan(t))/(1 - tan(s)*tan(t))".Simplify()` | `tan(s + t) - (tan(s) + tan(t)) / (1 - tan(s) * tan(t))` | `0 provided not cos(s + t) = 0 and not 1 - tan(s) * tan(t) = 0 and not cos(s) = 0 and not cos(t) = 0` |
+| `"sin(a/2)^2 - (1 - cos(a))/2".Simplify()` | `sin(a / 2) ^ 2 - (1 - cos(a)) / 2` | `0` |
+| `"tan(a/2) - sin(a)/(1 + cos(a))".Simplify()` | `tan(a / 2) - sin(a) / (1 + cos(a))` | `0 provided not cos(a / 2) * (cos(a) + 1) = 0` |
+| `"sin(a) - 2*tan(a/2)/(1 + tan(a/2)^2)".Simplify()` | `sin(a) - 2 * tan(a / 2) / (1 + tan(a / 2) ^ 2)` | `0 provided not 1 + tan(a / 2) ^ 2 = 0 and not cos(a / 2) = 0` |
+| `"sin(a) + sin(b) - 2*sin((a + b)/2)*cos((a - b)/2)".Simplify()` | `sin(a) + sin(b) - 2 * sin((a + b) / 2) * cos((a - b) / 2)` | `0` |
+| `"(1 - cos(a))/(1 + cos(a))/tan(a/2)^2".Simplify()` | `(1 - cos(a)) / ((cos(a) + 1) * tan(a / 2) ^ 2)` | `1 provided not cos(a) + 1 = 0 and not tan(a / 2) = 0 and not cos(a / 2) = 0` |
+| `"(cos(2*x) - 1)^2 + sin(2*x)^2 - 4*sin(x)^2".Simplify()` ([#2133](https://github.com/asc-community/AngouriMath/issues/2133)) | `2 + (-2) * cos(2 * x) + (-4) * sin(x) ^ 2` | `0` |
+
### `conj`, `arg` and `det` are refused by name
**Not silent: these now throw `UnrecognizedFunctionParseException` where they parsed.** A name the parser does not
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index a834a9318..002009400 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -170,7 +170,7 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Core/KnownLimitsAreNotBugsTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
-[AngouriMath/Functions/Algebra/Polynomials/{IntegerPolynomial,PrimeFieldPolynomial,PrimeFieldFactorization,SquareFreeDecomposition,PolynomialFactorization,PolynomialResultant,RationalPolynomial,PartialFractions,RationalFunction}.cs]
+[AngouriMath/Functions/Algebra/Polynomials/{IntegerPolynomial,PrimeFieldPolynomial,PrimeFieldFactorization,SquareFreeDecomposition,PolynomialFactorization,PolynomialResultant,RationalPolynomial,PartialFractions,RationalFunction,TrigonometricIdentity}.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
[Tests/UnitTests/PatternsTest/ParityTest.cs]
@@ -282,6 +282,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/CosineOfASumTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+[Tests/UnitTests/Common/TrigonometricIdentityTest.cs]
+file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+
[Tests/UnitTests/Common/LambdaArrowSyntaxTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/TrigonometricIdentity.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/TrigonometricIdentity.cs
new file mode 100644
index 000000000..202e7e494
--- /dev/null
+++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/TrigonometricIdentity.cs
@@ -0,0 +1,361 @@
+//
+// 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 System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using PeterO.Numbers;
+using static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Functions
+{
+ ///
+ /// Decides whether a rational function of sines, cosines, tangents, cotangents, secants and
+ /// cosecants is a constant where it has a value, and which constant. Proving a trigonometric
+ /// identity written as a difference is asking whether that constant is zero.
+ ///
+ ///
+ ///
+ /// Every angle has to be a combination of the variables with rational coefficients and no
+ /// constant term. Each variable v then has a base angle u = v / L, where L
+ /// is the least common denominator of its coefficients, and every angle is a whole combination
+ /// of base angles. A cosine of n u is the Chebyshev polynomial T_n(cos u) and a sine
+ /// is sin(u) U_(n-1)(cos u), and a sum of base angles is opened by the addition theorem.
+ /// So the expression is a quotient of two polynomials over Q in its variables and in a
+ /// sine and a cosine of each base angle.
+ ///
+ ///
+ /// The only relation among those is sin(u)^2 + cos(u)^2 = 1, since x and
+ /// e^(ix) are algebraically independent, and so are different variables. Writing each
+ /// sin(u)^2 as 1 - cos(u)^2 leaves every sine to the first power at most, and that
+ /// form is unique: the relations' leading monomials are coprime, so they are a Gröbner basis. A
+ /// polynomial therefore vanishes identically exactly when its form is zero, and the quotient is
+ /// the constant k exactly when the numerator's form is k times the denominator's.
+ ///
+ ///
+ /// Both theorems hold for every complex angle, and the denominator is the product of the ones
+ /// the expression divides by, so it vanishes only where the expression has no value. The
+ /// constant is the expression's value everywhere else, and a caller states where that is.
+ /// #2142
+ ///
+ ///
+ internal static class TrigonometricIdentity
+ {
+ ///
+ /// The largest multiple of a base angle written out, and the largest base denominator. A
+ /// cosine of n u is a polynomial of degree n, so this bounds the degrees before
+ /// bounds the products.
+ ///
+ private const int MaxMultiple = 16;
+
+ /// The largest power of a subexpression multiplied out.
+ private const int MaxExponent = 16;
+
+ ///
+ /// The constant equals wherever it has a value, or
+ /// where it is not a constant, is not a rational function of
+ /// trigonometric functions of the kind described, or where a bound is reached.
+ ///
+ internal static bool TryConstant(Entity expr, [NotNullWhen(true)] out Entity? constant)
+ {
+ constant = null;
+ var polynomialVariables = new HashSet();
+ var angles = new List();
+ if (!Collect(expr, polynomialVariables, angles) || angles.Count == 0)
+ return false;
+
+ // Each angle as whole multiples of base angles, and each variable's base denominator.
+ var coefficients = new List>(angles.Count);
+ var denominators = new Dictionary();
+ foreach (var angle in angles)
+ {
+ if (LinearCombination(angle) is not { } combination)
+ return false;
+ foreach (var (variable, coefficient) in combination)
+ {
+ var denominator = coefficient.Denominator;
+ denominators[variable] = denominators.TryGetValue(variable, out var known)
+ ? known.Divide(known.GreatestCommonDivisor(denominator)).Multiply(denominator)
+ : denominator;
+ if (denominators[variable].CompareTo(EInteger.FromInt32(MaxMultiple)) > 0)
+ return false;
+ }
+ coefficients.Add(combination);
+ }
+
+ var baseAngles = denominators.Keys.OrderBy(variable => variable.Name, StringComparer.Ordinal).ToArray();
+ var polynomial = polynomialVariables.OrderBy(variable => variable.Name, StringComparer.Ordinal).ToArray();
+ var variableCount = polynomial.Length + 2 * baseAngles.Length;
+ if (variableCount > MultivariatePolynomial.MaxVariables)
+ return false;
+ var indices = new Dictionary(polynomial.Length);
+ for (var i = 0; i < polynomial.Length; i++)
+ indices[polynomial[i]] = i;
+ var pairs = new Dictionary(baseAngles.Length);
+ for (var i = 0; i < baseAngles.Length; i++)
+ pairs[baseAngles[i]] = (polynomial.Length + 2 * i, polynomial.Length + 2 * i + 1);
+
+ var opened = new Dictionary(angles.Count);
+ for (var i = 0; i < angles.Count; i++)
+ {
+ var cosine = MultivariatePolynomial.One(variableCount);
+ var sine = MultivariatePolynomial.Zero(variableCount);
+ var total = 0;
+ foreach (var (variable, coefficient) in coefficients[i])
+ {
+ var multiple = coefficient.Multiply(ERational.FromEInteger(denominators[variable])).ToEInteger();
+ if (!multiple.CanFitInInt32())
+ return false;
+ var n = multiple.ToInt32Checked();
+ total += Math.Abs(n);
+ if (total > MaxMultiple)
+ return false;
+ var (cosineOfIt, sineOfIt) = OfAMultiple(Math.Abs(n), pairs[variable], variableCount);
+ if (cosineOfIt is null || sineOfIt is null)
+ return false;
+ if (n < 0)
+ sineOfIt = sineOfIt.ScaleBy(ERational.FromInt32(-1));
+ // The addition theorem: cos(A + B) = cos A cos B - sin A sin B, and
+ // sin(A + B) = sin A cos B + cos A sin B.
+ if (cosine.Multiply(cosineOfIt) is not { } cc || sine.Multiply(sineOfIt) is not { } ss
+ || sine.Multiply(cosineOfIt) is not { } sc || cosine.Multiply(sineOfIt) is not { } cs)
+ return false;
+ cosine = cc.Subtract(ss);
+ sine = sc.Add(cs);
+ }
+ opened[angles[i]] = (cosine, sine);
+ }
+
+ if (Gathered(expr, indices, opened, variableCount) is not ({ } top, { } bottom))
+ return false;
+ var cosines = pairs.Values.ToArray();
+ if (Reduced(top, cosines) is not { } reducedTop || Reduced(bottom, cosines) is not { } reducedBottom
+ || reducedBottom.IsZero)
+ return false;
+ if (reducedTop.IsZero)
+ {
+ constant = Integer.Create(0);
+ return true;
+ }
+ var leading = reducedBottom.LeadingMonomial(MonomialOrder.Lexicographic);
+ var ratio = reducedTop.CoefficientOf(leading).Divide(reducedBottom.CoefficientOf(leading)).InLowestTerms();
+ if (ratio.IsZero || !reducedTop.Subtract(reducedBottom.ScaleBy(ratio)).IsZero)
+ return false;
+ constant = Rational.Create(ratio);
+ return true;
+ }
+
+ ///
+ /// Whether is built from rational numbers, variables, the four
+ /// operations, whole powers and the six trigonometric functions, collecting the variables
+ /// outside the functions and the functions' arguments.
+ ///
+ private static bool Collect(Entity node, HashSet polynomialVariables, List angles)
+ {
+ switch (node)
+ {
+ case Rational:
+ return true;
+ case Variable variable:
+ if (variable.IsConstant)
+ return false;
+ polynomialVariables.Add(variable);
+ return true;
+ case Sumf(var left, var right):
+ return Collect(left, polynomialVariables, angles) && Collect(right, polynomialVariables, angles);
+ case Minusf(var left, var right):
+ return Collect(left, polynomialVariables, angles) && Collect(right, polynomialVariables, angles);
+ case Mulf(var left, var right):
+ return Collect(left, polynomialVariables, angles) && Collect(right, polynomialVariables, angles);
+ case Divf(var left, var right):
+ return Collect(left, polynomialVariables, angles) && Collect(right, polynomialVariables, angles);
+ case Powf(var @base, Integer exponent):
+ return exponent.EInteger.Abs().CompareTo(EInteger.FromInt32(MaxExponent)) <= 0
+ && Collect(@base, polynomialVariables, angles);
+ default:
+ if (AngleOf(node) is not { } angle)
+ return false;
+ if (!angles.Contains(angle))
+ angles.Add(angle);
+ return true;
+ }
+ }
+
+ private static Entity? AngleOf(Entity node) => node switch
+ {
+ Sinf(var angle) => angle,
+ Cosf(var angle) => angle,
+ Tanf(var angle) => angle,
+ Cotanf(var angle) => angle,
+ Secantf(var angle) => angle,
+ Cosecantf(var angle) => angle,
+ _ => null
+ };
+
+ ///
+ /// as a sum of its variables times rational coefficients, or
+ /// where it is not one, or has a constant term.
+ ///
+ private static List<(Variable Variable, ERational Coefficient)>? LinearCombination(Entity angle)
+ {
+ var variables = angle.VarsAndConsts.ToArray();
+ if (variables.Length == 0 || variables.Length > MultivariatePolynomial.MaxVariables || variables.Any(variable => variable.IsConstant))
+ return null;
+ var indices = new Dictionary(variables.Length);
+ for (var i = 0; i < variables.Length; i++)
+ indices[variables[i]] = i;
+ if (MultivariatePolynomial.TryParse(angle, indices) is not { IsZero: false } parsed)
+ return null;
+ var combination = new List<(Variable, ERational)>(variables.Length);
+ foreach (var monomial in parsed.Monomials)
+ {
+ if (MultivariatePolynomial.TotalDegree(monomial) != 1)
+ return null;
+ for (var i = 0; i < variables.Length; i++)
+ if (MultivariatePolynomial.PowerOfMonomial(monomial, i) == 1)
+ combination.Add((variables[i], parsed.CoefficientOf(monomial)));
+ }
+ return combination;
+ }
+
+ ///
+ /// cos(n u) and sin(n u) for a whole n >= 0, as polynomials in the sine
+ /// and the cosine of u: T_n(cos u) and sin(u) U_(n-1)(cos u), by
+ /// T_(k+1) = 2 c T_k - T_(k-1) and the same recurrence for U.
+ ///
+ private static (MultivariatePolynomial? Cosine, MultivariatePolynomial? Sine) OfAMultiple(
+ int n, (int Sine, int Cosine) pair, int variableCount)
+ {
+ var one = MultivariatePolynomial.One(variableCount);
+ if (n == 0)
+ return (one, MultivariatePolynomial.Zero(variableCount));
+ var c = MultivariatePolynomial.Monomial(variableCount, pair.Cosine);
+ var twoC = c.ScaleBy(ERational.FromInt32(2));
+ var (previousT, t) = (one, c);
+ var (previousU, u) = (one, twoC);
+ for (var k = 1; k < n; k++)
+ {
+ if (twoC.Multiply(t) is not { } nextT || twoC.Multiply(u) is not { } nextU)
+ return (null, null);
+ (previousT, t) = (t, nextT.Subtract(previousT));
+ (previousU, u) = (u, nextU.Subtract(previousU));
+ }
+ // u is now U_n and previousU is U_(n-1).
+ return (t, MultivariatePolynomial.Monomial(variableCount, pair.Sine).Multiply(previousU));
+ }
+
+ ///
+ /// as a numerator and a denominator, the denominator the product of
+ /// the denominators of the quotients it is built from.
+ ///
+ private static (MultivariatePolynomial? Top, MultivariatePolynomial? Bottom) Gathered(
+ Entity expr, IReadOnlyDictionary indices,
+ IReadOnlyDictionary opened,
+ int variableCount)
+ {
+ var one = MultivariatePolynomial.One(variableCount);
+ switch (expr)
+ {
+ case Rational rational:
+ return (MultivariatePolynomial.Constant(variableCount, rational.ERational), one);
+ case Variable variable:
+ return indices.TryGetValue(variable, out var index)
+ ? (MultivariatePolynomial.Monomial(variableCount, index), one)
+ : (null, null);
+ case Sumf(var left, var right):
+ return Combined(left, right, subtract: false);
+ case Minusf(var left, var right):
+ return Combined(left, right, subtract: true);
+ case Mulf(var left, var right):
+ {
+ if (Gathered(left, indices, opened, variableCount) is not ({ } leftTop, { } leftBottom)
+ || Gathered(right, indices, opened, variableCount) is not ({ } rightTop, { } rightBottom))
+ return (null, null);
+ return (leftTop.Multiply(rightTop), leftBottom.Multiply(rightBottom));
+ }
+ case Divf(var left, var right):
+ {
+ if (Gathered(left, indices, opened, variableCount) is not ({ } leftTop, { } leftBottom)
+ || Gathered(right, indices, opened, variableCount) is not ({ } rightTop, { } rightBottom))
+ return (null, null);
+ return (leftTop.Multiply(rightBottom), leftBottom.Multiply(rightTop));
+ }
+ case Powf(var @base, Integer exponent):
+ {
+ if (Gathered(@base, indices, opened, variableCount) is not ({ } baseTop, { } baseBottom))
+ return (null, null);
+ var power = exponent.EInteger.ToInt32Checked();
+ return power >= 0
+ ? (baseTop.Power(power), baseBottom.Power(power))
+ : (baseBottom.Power(-power), baseTop.Power(-power));
+ }
+ default:
+ if (AngleOf(expr) is not { } angle || !opened.TryGetValue(angle, out var trigonometric))
+ return (null, null);
+ var (cosine, sine) = trigonometric;
+ return expr switch
+ {
+ Sinf => (sine, one),
+ Cosf => (cosine, one),
+ Tanf => (sine, cosine),
+ Cotanf => (cosine, sine),
+ Secantf => (one, cosine),
+ _ => (one, sine),
+ };
+ }
+
+ (MultivariatePolynomial?, MultivariatePolynomial?) Combined(Entity left, Entity right, bool subtract)
+ {
+ if (Gathered(left, indices, opened, variableCount) is not ({ } leftTop, { } leftBottom)
+ || Gathered(right, indices, opened, variableCount) is not ({ } rightTop, { } rightBottom))
+ return (null, null);
+ // Over one denominator the sum keeps it, so that a sum of n terms over d is not
+ // carried over d^n.
+ if (leftBottom.SameAs(rightBottom))
+ return (subtract ? leftTop.Subtract(rightTop) : leftTop.Add(rightTop), leftBottom);
+ if (leftTop.Multiply(rightBottom) is not { } first || rightTop.Multiply(leftBottom) is not { } second)
+ return (null, null);
+ return (subtract ? first.Subtract(second) : first.Add(second), leftBottom.Multiply(rightBottom));
+ }
+ }
+
+ ///
+ /// with every sin(u)^2 written as 1 - cos(u)^2,
+ /// which is its unique form modulo sin(u)^2 + cos(u)^2 - 1.
+ ///
+ private static MultivariatePolynomial? Reduced(MultivariatePolynomial polynomial, IReadOnlyList<(int Sine, int Cosine)> pairs)
+ {
+ var variableCount = polynomial.VariableCount;
+ var one = MultivariatePolynomial.One(variableCount);
+ var result = MultivariatePolynomial.Zero(variableCount);
+ foreach (var monomial in polynomial.Monomials)
+ {
+ var kept = monomial;
+ var factor = one;
+ foreach (var (sine, cosine) in pairs)
+ {
+ var half = MultivariatePolynomial.PowerOfMonomial(monomial, sine) / 2;
+ if (half == 0)
+ continue;
+ kept -= MultivariatePolynomial.PackMonomial(sine, 2 * half);
+ var cosineSquared = MultivariatePolynomial.Term(variableCount, MultivariatePolynomial.PackMonomial(cosine, 2), ERational.One);
+ if (one.Subtract(cosineSquared).Power(half) is not { } replaced || factor.Multiply(replaced) is not { } next)
+ return null;
+ factor = next;
+ }
+ if (MultivariatePolynomial.Term(variableCount, kept, polynomial.CoefficientOf(monomial)).Multiply(factor) is not { } term)
+ return null;
+ result = result.Add(term);
+ if (result.TermCount > MultivariatePolynomial.MaxTerms)
+ return null;
+ }
+ return result;
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs
index f9e9a3613..cbb7b3a06 100644
--- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs
@@ -643,6 +643,22 @@ not solve this issue completely and yet too slow to be accepted.
&& RationalFunction.TryCanonicalize(stage1, out var canonical, wholeCoefficients: true))
AddHistory(Noted(recording, stage1, canonical, "RationalCanonicalization", canonicalMark));
+ // The constant a rational function of sines and cosines is, where it is one. The rules
+ // above open an angle sum, double an angle and take the Pythagorean pair a step at a
+ // time, and an identity that needs all three in one order was not reached:
+ // sin(a/2)^2 - (1 - cos(a))/2, the Weierstrass forms and sum-to-product, and the tangent
+ // addition formula. TrigonometricIdentity decides every such identity. It is the value
+ // wherever the expression has one, so the condition is the expression's own domain, as
+ // for a - a. Under the same complexity bound as the quotient above, and asked by a walk
+ // that allocates nothing. Not where the rules already reached the same answer, so that
+ // what a derivation records is their steps and not one step that names no identity.
+ // https://github.com/asc-community/AngouriMath/issues/2142
+ var identityMark = recording?.Mark() ?? 0;
+ if (stage1.Complexity <= 64 && IsARationalFunctionOfTrigonometricFunctions(stage1)
+ && TrigonometricIdentity.TryConstant(stage1, out var identity)
+ && identity.Provided(stage1.DomainCondition) is var proved && !registered.Contains(proved))
+ AddHistory(Noted(recording, stage1, proved, "TrigonometricIdentity", identityMark));
+
return history.Values.SelectMany(x => x);
}
@@ -681,6 +697,46 @@ private static bool IsARationalFunction(Entity node, ref bool quotient)
}
}
+ ///
+ /// Whether is made of numbers, variables, the four operations, whole
+ /// powers and sines, cosines, tangents, cotangents, secants and cosecants, with at least one
+ /// of those: the expressions may decide.
+ /// What the functions are applied to is its own question.
+ ///
+ private static bool IsARationalFunctionOfTrigonometricFunctions(Entity expr)
+ {
+ var trigonometric = false;
+ return IsARationalFunctionOfTrigonometricFunctions(expr, ref trigonometric) && trigonometric;
+ }
+
+ private static bool IsARationalFunctionOfTrigonometricFunctions(Entity node, ref bool trigonometric)
+ {
+ switch (node)
+ {
+ case Number or Variable:
+ return true;
+ case Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf:
+ trigonometric = true;
+ return true;
+ case Divf(var dividend, var divisor):
+ return IsARationalFunctionOfTrigonometricFunctions(dividend, ref trigonometric)
+ && IsARationalFunctionOfTrigonometricFunctions(divisor, ref trigonometric);
+ case Powf(var @base, Integer):
+ return IsARationalFunctionOfTrigonometricFunctions(@base, ref trigonometric);
+ case Sumf(var left, var right):
+ return IsARationalFunctionOfTrigonometricFunctions(left, ref trigonometric)
+ && IsARationalFunctionOfTrigonometricFunctions(right, ref trigonometric);
+ case Minusf(var left, var right):
+ return IsARationalFunctionOfTrigonometricFunctions(left, ref trigonometric)
+ && IsARationalFunctionOfTrigonometricFunctions(right, ref trigonometric);
+ case Mulf(var left, var right):
+ return IsARationalFunctionOfTrigonometricFunctions(left, ref trigonometric)
+ && IsARationalFunctionOfTrigonometricFunctions(right, ref trigonometric);
+ default:
+ return false;
+ }
+ }
+
///
/// Sorts an expression into a polynomial.
/// See more at
diff --git a/Sources/Tests/UnitTests/Common/TrigonometricIdentityTest.cs b/Sources/Tests/UnitTests/Common/TrigonometricIdentityTest.cs
new file mode 100644
index 000000000..e6f11e556
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/TrigonometricIdentityTest.cs
@@ -0,0 +1,123 @@
+//
+// 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 AngouriMath;
+using AngouriMath.Extensions;
+using AngouriMath.Functions;
+using Xunit;
+using static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// The identities of the reference chapter of Modenov's Problems in Geometry that
+ /// Simplify did not prove: each needs an angle opened, doubled or halved and the
+ /// Pythagorean pair taken, in that order, and the rules take them a step at a time.
+ /// #2142
+ ///
+ [Trait("Area", "Common")]
+ public sealed class TrigonometricIdentityTest
+ {
+ private static Entity Bare(Entity expr)
+ {
+ while (expr is Providedf(var inner, _))
+ expr = inner;
+ return expr;
+ }
+
+ ///
+ /// The tangent addition formula, the half-angle formulas, the Weierstrass forms and
+ /// sum-to-product, each written as a difference.
+ ///
+ [Theory]
+ [InlineData("tan(s + t) - (tan(s) + tan(t)) / (1 - tan(s) * tan(t))")]
+ [InlineData("tan(s - t) - (tan(s) - tan(t)) / (1 + tan(s) * tan(t))")]
+ [InlineData("sin(a/2)^2 - (1 - cos(a)) / 2")]
+ [InlineData("cos(a/2)^2 - (1 + cos(a)) / 2")]
+ [InlineData("tan(a/2) - (1 - cos(a)) / sin(a)")]
+ [InlineData("tan(a/2) - sin(a) / (1 + cos(a))")]
+ [InlineData("sin(a) - 2 * tan(a/2) / (1 + tan(a/2)^2)")]
+ [InlineData("cos(a) - (1 - tan(a/2)^2) / (1 + tan(a/2)^2)")]
+ [InlineData("tan(a) - 2 * tan(a/2) / (1 - tan(a/2)^2)")]
+ [InlineData("sin(a) + sin(b) - 2 * sin((a + b)/2) * cos((a - b)/2)")]
+ [InlineData("sin(a) - sin(b) - 2 * cos((a + b)/2) * sin((a - b)/2)")]
+ [InlineData("cos(a) + cos(b) - 2 * cos((a + b)/2) * cos((a - b)/2)")]
+ [InlineData("cos(a) - cos(b) + 2 * sin((a + b)/2) * sin((a - b)/2)")]
+ [InlineData("cot(a/2) - sin(a) / (1 - cos(a))")]
+ [InlineData("sec(x)^2 - 1 / (1 - sin(x)^2)")]
+ public void TheIdentityIsZero(string identity)
+ => Assert.Equal(Integer.Zero, Bare(identity.Simplify()));
+
+ ///
+ /// The same sum written two ways, one of which the rules finished and the other not:
+ /// (cos(2x) - 1)^2 + sin(2x)^2 - 4 sin(x)^2 stopped at
+ /// 2 + (-2) cos(2x) + (-4) sin(x)^2.
+ /// #2133
+ ///
+ [Fact]
+ public void TheValueDoesNotDependOnHowTheSumIsWritten()
+ => Assert.Equal(Integer.Zero, "(cos(2*x) - 1)^2 + sin(2*x)^2 - 4*sin(x)^2".Simplify());
+
+ ///
+ /// A difference with no quotient has a value everywhere, so its zero has no condition.
+ ///
+ [Theory]
+ [InlineData("sin(a/2)^2 - (1 - cos(a)) / 2")]
+ [InlineData("sin(a) + sin(b) - 2 * sin((a + b)/2) * cos((a - b)/2)")]
+ public void WithoutAQuotientTheZeroHasNoCondition(string identity)
+ => Assert.Equal(Integer.Zero, identity.Simplify());
+
+ ///
+ /// A difference with a quotient has none where the quotient has none, and its zero keeps
+ /// that: tan(a/2) - sin(a)/(1 + cos(a)) is undefined at a = pi.
+ ///
+ [Fact]
+ public void WithAQuotientTheZeroKeepsWhereItHasNoValue()
+ {
+ var zero = "tan(a/2) - sin(a) / (1 + cos(a))".Simplify();
+ Assert.IsType(zero);
+ Assert.Equal(Integer.Zero, Bare(zero));
+ }
+
+ ///
+ /// A constant other than zero: tan(a/2)^2 = (1 - cos a)/(1 + cos a) as a quotient.
+ ///
+ [Fact]
+ public void AQuotientOfTwoSidesIsOne()
+ => Assert.Equal(Integer.One, Bare("(1 - cos(a)) / (1 + cos(a)) / tan(a/2)^2".Simplify()));
+
+ ///
+ /// One sign changed in each identity, so none of them is a constant.
+ ///
+ [Theory]
+ [InlineData("tan(s + t) - (tan(s) + tan(t)) / (1 + tan(s) * tan(t))")]
+ [InlineData("sin(a/2)^2 - (1 + cos(a)) / 2")]
+ [InlineData("tan(a/2) - (1 + cos(a)) / sin(a)")]
+ [InlineData("sin(a) + sin(b) - 2 * sin((a + b)/2) * sin((a - b)/2)")]
+ public void ANearMissIsNotAConstant(string expression)
+ {
+ Assert.False(TrigonometricIdentity.TryConstant(expression.ToEntity(), out _));
+ Assert.IsNotType(Bare(expression.Simplify()));
+ }
+
+ ///
+ /// The decision needs the only relation between the sines and cosines of the base angles
+ /// to be the Pythagorean one, so an angle with a constant term or a named constant is
+ /// declined rather than decided wrongly: sin(pi) is 0, which the relation does not
+ /// know. The rules settle these.
+ ///
+ [Theory]
+ [InlineData("sin(x + 1)^2 + cos(x + 1)^2")]
+ [InlineData("sin(pi - x) - sin(x)")]
+ [InlineData("sin(2) - 2 * sin(1) * cos(1)")]
+ [InlineData("sin(x^2)^2 + cos(x^2)^2")]
+ [InlineData("sin(sqrt(2) * x)^2 + cos(sqrt(2) * x)^2")]
+ public void AnAngleThatIsNotARationalCombinationOfVariablesIsDeclined(string expression)
+ => Assert.False(TrigonometricIdentity.TryConstant(expression.ToEntity(), out _));
+ }
+}