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