diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Trigonometry.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Trigonometry.Classes.cs index a202e5584..0cf0fc667 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Trigonometry.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Trigonometry.Classes.cs @@ -226,6 +226,12 @@ protected override Entity InnerSimplify(bool isExact) => Complex n when !isExact => Number.Arctan(n), Real r when r.EDecimal.IsPositiveInfinity() => MathS.pi / 2, Real r when r.EDecimal.IsNegativeInfinity() => -MathS.pi / 2, + // The two finite angles arctan names outright, kept here beside the + // infinite ones rather than offered to the simplifier as candidates: + // there it lost to arctan(1) on node count, so a sum that came to + // pi/4 stopped one step short of it. + Integer(1) => MathS.pi / 4, + Integer(-1) => -MathS.pi / 4, _ => null }, (@this, a) => ((Arctanf)@this).New(a), isExact); diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs index 803222ed5..30e714d77 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs @@ -24,6 +24,21 @@ internal static partial class Patterns Sumf(Arctanf(var any1), Arccotanf(var any1a)) when any1 == any1a => MathS.pi / 2, Sumf(Arccotanf(var any1), Arctanf(var any1a)) when any1 == any1a => MathS.pi / 2, + // arctan(a) + arctan(b) = arctan((a + b)/(1 - ab)), which is the tangent + // addition formula read backwards. It holds as written only while ab < 1: past + // that the sum leaves the range arctan answers in and the identity is off by a + // whole pi. Both arguments have to be numbers, so that ab < 1 is a question + // with an answer -- arctan(1/2) + arctan(1/3) is pi/4, while + // arctan(2) + arctan(3) is 3pi/4 and is left alone. + Sumf(Arctanf(Real a), Arctanf(Real b)) when (a * b).Evaled is Real product && product < 1 + => MathS.Arctan(((a + b) / (1 - a * b)).InnerSimplified), + + // The two angles whose tangent is an irrational this can recognise. The ones + // at +-1 are handled where arctan is simplified, since a candidate offered + // here has to win on node count and pi/4 does not beat arctan(1). + Arctanf(Powf(Integer(3), Rational(Integer(1), Integer(2)))) => MathS.pi / 3, + Arctanf(Divf(Integer(1), Powf(Integer(3), Rational(Integer(1), Integer(2))))) => MathS.pi / 6, + // tan * cot = 1 Mulf(Tanf(var any1), Cotanf(var any1a)) when any1 == any1a => 1, Mulf(Cotanf(var any1), Tanf(var any1a)) when any1 == any1a => 1, diff --git a/Sources/Tests/UnitTests/Common/ArctanIdentitiesTest.cs b/Sources/Tests/UnitTests/Common/ArctanIdentitiesTest.cs new file mode 100644 index 000000000..54de7ca93 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/ArctanIdentitiesTest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2019-2022 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; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// arctan(1/2) + arctan(1/3) is pi/4, and neither half of that was reachable: + /// the two could not be added, and arctan(1) stayed as it was. No issue is filed for + /// this; it comes from the gaps a solver corpus showed. + /// + public sealed class ArctanIdentitiesTest + { + private static Entity Simplified(string expression) => expression.ToEntity().Simplify(); + + [Theory] + [InlineData("arctan(1/2) + arctan(1/3)", "pi / 4")] + [InlineData("arctan(1/3) + arctan(1/2)", "pi / 4")] + [InlineData("arctan(1/5) + arctan(1/8)", "arctan(1/3)")] + [InlineData("arctan(1/2) + arctan(1/5)", "arctan(7/9)")] + public void TwoArctangentsAddUp(string expression, string expected) => + Assert.Equal(expected.ToEntity().Simplify(), Simplified(expression)); + + [Theory] + [InlineData("arctan(1)", "pi / 4")] + [InlineData("arctan(-1)", "-pi / 4")] + [InlineData("arctan(0)", "0")] + [InlineData("arctan(sqrt(3))", "pi / 3")] + [InlineData("arctan(1/sqrt(3))", "pi / 6")] + public void TheAnglesArctangentNamesOutright(string expression, string expected) => + Assert.Equal(expected.ToEntity().Simplify(), Simplified(expression)); + + /// + /// The addition formula holds as written only while the product of the two + /// arguments is below one. Past that the sum leaves the range arctan answers in and + /// the identity is off by a whole pi, so those are left alone rather than answered + /// wrongly: arctan(2) + arctan(3) is 3pi/4, not arctan(-1). + /// + [Theory] + [InlineData("arctan(2) + arctan(3)")] + [InlineData("arctan(1) + arctan(2)")] + [InlineData("arctan(x) + arctan(y)")] + public void WhereTheFormulaDoesNotHoldNothingIsClaimed(string expression) => + Assert.Contains("arctan", Simplified(expression).Stringize()); + + /// + /// Whatever a sum of two arctangents simplifies to has to be the same number. The + /// pairs run either side of the boundary the formula is guarded by. + /// + [Fact] + public void ASimplifiedSumIsTheSameNumber() + { + double[] values = { -5, -3, -2, -1.5, -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5 }; + foreach (var a in values) + foreach (var b in values) + { + var simplified = Simplified($"arctan({a}) + arctan({b})"); + var actual = simplified.EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.Equal(Math.Atan(a) + Math.Atan(b), actual, 9); + } + } + + // The identities next to this one have to keep working. + [Theory] + [InlineData("arcsin(x) + arccos(x)", "pi / 2")] + [InlineData("arctan(x) + arccotan(x)", "pi / 2")] + [InlineData("tan(arctan(x))", "x")] + [InlineData("arctan(tan(1/2))", "1/2")] + public void NeighbouringIdentitiesAreUnaffected(string expression, string expected) => + Assert.Equal(expected.ToEntity().Simplify(), Simplified(expression)); + } +}