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 _)); + } +}