From 0738f2a03e66a4a3da2c9abdc991279feff6dab4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 4 Aug 2026 12:25:54 +0000 Subject: [PATCH] Add up two arctangents, and name the angles arctan knows 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. The addition formula is the tangent one read backwards, arctan(a) + arctan(b) = arctan((a + b)/(1 - ab)). 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, so arctan(2) + arctan(3) is 3pi/4 rather than arctan(-1). Both arguments are required to be numbers, which makes ab < 1 a question with an answer, and the ones past the boundary are left alone. arctan(1) and arctan(-1) are handled where arctan is simplified rather than offered to the simplifier as candidates. A candidate has to win on node count, and pi/4 does not beat arctan(1), so a sum that came to it stopped one step short. They sit beside the values at plus and minus infinity, which were already there. The two irrational ones, arctan(sqrt(3)) and arctan(1/sqrt(3)), do win their contest and stay where the other trigonometric patterns are. Checked over 289 pairs either side of the boundary: whatever a sum of two arctangents simplifies to is the same number it was. 17 tests, 8 of which fail without the change. 4026 in all, 127 F#, corpus 102/117 with simp:inverse-trig at 100% from 50%, and 1320 property checks. --- ...luation.Continuous.Trigonometry.Classes.cs | 6 ++ .../Patterns/Patterns.Trigonometry.cs | 15 ++++ .../UnitTests/Common/ArctanIdentitiesTest.cs | 80 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 Sources/Tests/UnitTests/Common/ArctanIdentitiesTest.cs 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)); + } +}