diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index eb7b7aa15..aeb4936b0 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -74,6 +74,7 @@ read first.
| **Silent** | `RewriteRules.ExpandFactorialDivisions.Rules[0].Growth` | `Collects` — guessed from string length | `Unknown`; whether it collects depends on the offsets |
| **Silent** | `RewriteStep.Soundness` on a rewrite whose rule declares a tier — `RewriteRules.SetOperator` on `A /\ A`, and every rewrite of the nineteen sets described from their data form | `SoundUnderAssumptions` — its rule set's tier, which is the minimum over every rule in the set | `Sound` — the rule's own |
| **Silent** | `DerivationStep.Soundness` | its rule set's tier | the weakest tier any rewrite that actually fired inside the step holds at |
+| **Silent** | `"arccotan(-1)".ToEntity().Simplify()`, and every negative argument the inverse-trigonometric table knows | `3/4 * pi` — the textbook range, and **not equal to `arccotan(-1)`**, whose value is `-pi/4` | `-1/4 * pi` |
### Nineteen rule sets describe the rules they run
@@ -147,6 +148,32 @@ rewrite that **actually fired** inside the step holds at, rather than its set's
the step may never have reached. A pass of nine unconditional rewrites and one conditional one is
still a conditional pass; a pass of ten unconditional ones now says so.
+### A negative `arccotan` is negative
+
+`arccotan` here is `arctan(1/x)`, with range `(-pi/2, pi/2]` — **not** the textbook `(0, pi)`. The
+inverse-trigonometric table read it as the textbook `pi/2 - arctan(x)`. The two agree on every
+positive argument and on nothing negative, so a closed form came back that was not the value:
+
+```csharp
+"arccotan(-1)".ToEntity().EvalNumerical() // -0.7853981633974483…, which is -pi/4
+"arccotan(-1)".ToEntity().Simplify() // was 3/4 * pi, is -1/4 * pi
+```
+
+`Simplify` and `EvalNumerical` disagreeing about a constant is the sharpest form this kind of defect
+takes, and it reached every table value with a negative argument: `arccotan(-sqrt(3))` was `5/6 * pi`
+and is `-1/6 * pi`, `arccotan(-(2 + sqrt(3)))` was `11/12 * pi` and is `-1/12 * pi`.
+
+**The rule for `arctan(x) + arccotan(x)` had the convention right all along** — it answers `pi/2` for
+a non-negative argument and `-pi/2` for a negative one, which is
+[#887](https://github.com/asc-community/AngouriMath/issues/887). The table's docstring claimed to
+take "the same reading of it" and took the opposite one; the comment asserting agreement is what let
+the disagreement stand. The two now agree, and `ArccotanTableSignTest` measures the range at a
+positive argument, a negative one and zero rather than recalling it.
+
+`arccos` uses the same complement helper and is **unaffected**: its range is `[0, pi]` and `arcsin`'s
+is `[-pi/2, pi/2]`, so `pi/2 - arcsin(x)` holds for every argument. That is asserted too, so that a
+later tidy-up cannot merge the two paths back together.
+
### An equation nothing settled is no longer answered with the empty set
`Solve` and `SolveEquation` returned an empty `FiniteSet` for two different things: an equation
diff --git a/Sources/AngouriMath/Functions/Simplification/InverseTrigonometricTableValues.cs b/Sources/AngouriMath/Functions/Simplification/InverseTrigonometricTableValues.cs
index 726319e53..68c2f1019 100644
--- a/Sources/AngouriMath/Functions/Simplification/InverseTrigonometricTableValues.cs
+++ b/Sources/AngouriMath/Functions/Simplification/InverseTrigonometricTableValues.cs
@@ -82,12 +82,47 @@ internal static bool PullArccos(Complex arg, [NotNullWhen(true)] out Entity? res
=> Complement(PullArcsin(arg, out var arcsin), arcsin, out res);
///
- /// arccotan(x) = pi/2 - arctan(x), the same reading of it the simplification rule for
- /// arctan(x) + arccotan(x) already takes.
+ /// arccotan(x) here is arctan(1/x), with range (-pi/2, pi/2] — not
+ /// the textbook (0, pi). So the complement is pi/2 - arctan(x) for a
+ /// non-negative argument and -pi/2 - arctan(x) for a negative one.
///
+ ///
+ ///
+ /// This read pi/2 - arctan(x) for every argument, which is the textbook identity and
+ /// is wrong on every negative one: arccotan(-1) came back as 3/4 * pi
+ /// where the function's own value is -pi/4, so
+ /// and EvalNumerical disagreed about a closed-form constant. Measured at the three
+ /// arguments that settle a range: arccotan(1) is 0.785…, arccotan(-1)
+ /// is -0.785… and arccotan(0) is 1.570…, so the range is
+ /// (-pi/2, pi/2] and the function is odd away from zero.
+ ///
+ ///
+ /// The docstring it replaces claimed to take "the same reading of it the simplification
+ /// rule for arctan(x) + arccotan(x) already takes". That rule answers pi/2
+ /// for a non-negative argument and -pi/2 for a negative one
+ /// (#887) — so the
+ /// two readings were opposite, and the comment asserting they agreed is what let it stand.
+ ///
+ ///
internal static bool PullArccotan(Complex arg, [NotNullWhen(true)] out Entity? res)
- => Complement(PullArctan(arg, out var arctan), arctan, out res);
+ {
+ if (!PullArctan(arg, out var arctan) || arctan is null)
+ {
+ res = null;
+ return false;
+ }
+ // The sign is taken from the argument rather than from the angle, because arctan(0)
+ // is 0 and carries none: arccotan(0) is pi/2 and not -pi/2.
+ var negative = arg is Real { EDecimal: var given } && given.IsNegative;
+ res = ((negative ? -pi / 2 : pi / 2) - arctan).InnerSimplified;
+ return true;
+ }
+ ///
+ /// arccos(x) = pi/2 - arcsin(x), which is also the range arccos is stated over — and unlike
+ /// this holds for every argument, arccos running over
+ /// [0, pi] while arcsin runs over [-pi/2, pi/2].
+ ///
private static bool Complement(bool found, Entity? angle, [NotNullWhen(true)] out Entity? res)
{
if (!found || angle is null)
diff --git a/Sources/Tests/UnitTests/Common/ArccotanTableSignTest.cs b/Sources/Tests/UnitTests/Common/ArccotanTableSignTest.cs
new file mode 100644
index 000000000..2bb634cc8
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/ArccotanTableSignTest.cs
@@ -0,0 +1,112 @@
+//
+// 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.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// and EvalNumerical have to agree about a
+ /// closed-form constant, and for a negative arccotan they did not.
+ ///
+ ///
+ ///
+ /// arccotan here is arctan(1/x), with range (-pi/2, pi/2]. The
+ /// inverse-trigonometric table read it as the textbook pi/2 - arctan(x), whose range is
+ /// (0, pi): the two agree on every positive argument and on nothing negative, so
+ /// arccotan(-1) simplified to 3/4 * pi while the function's own value there is
+ /// -pi/4.
+ ///
+ ///
+ /// The convention is measured here rather than recalled, at the three arguments that settle a
+ /// range — a positive one, a negative one and zero. That is the discipline this defect exists
+ /// for: the identity is right in a textbook and wrong in this library, and nothing but running
+ /// the function says which convention it keeps.
+ ///
+ ///
+ [Trait("Area", "Common")]
+ public sealed class ArccotanTableSignTest
+ {
+ ///
+ /// The range, at the three arguments that pin it. arccotan is odd away from zero and
+ /// lands on pi/2 there, which is (-pi/2, pi/2] and not (0, pi).
+ ///
+ [Theory]
+ [InlineData("arccotan(1)", 0.7853981633974483)]
+ [InlineData("arccotan(-1)", -0.7853981633974483)]
+ [InlineData("arccotan(0)", 1.5707963267948966)]
+ public void TheRangeIsTheOneThisLibraryKeeps(string expression, double expected)
+ => Assert.Equal(expected,
+ (double)expression.ToEntity().EvalNumerical().RealPart, 12);
+
+ ///
+ /// Every table value, positive and negative, and the closed form has to be the value.
+ ///
+ [Theory]
+ [InlineData("arccotan(1)")]
+ [InlineData("arccotan(-1)")]
+ [InlineData("arccotan(0)")]
+ [InlineData("arccotan(sqrt(3))")]
+ [InlineData("arccotan(-sqrt(3))")]
+ [InlineData("arccotan(1 / sqrt(3))")]
+ [InlineData("arccotan(-1 / sqrt(3))")]
+ [InlineData("arccotan(2 + sqrt(3))")]
+ [InlineData("arccotan(-(2 + sqrt(3)))")]
+ [InlineData("arccotan(sqrt(2) - 1)")]
+ [InlineData("arccotan(1 - sqrt(2))")]
+ public void TheClosedFormIsTheValue(string expression)
+ {
+ var entity = expression.ToEntity();
+ Assert.Equal(
+ (double)entity.EvalNumerical().RealPart,
+ (double)entity.Simplify().EvalNumerical().RealPart,
+ 12);
+ }
+
+ ///
+ /// The named case, exactly as it was wrong.
+ ///
+ [Fact]
+ public void ANegativeArccotanIsNegative()
+ {
+ Assert.Equal("-1/4 * pi".ToEntity(), "arccotan(-1)".ToEntity().Simplify());
+ Assert.Equal("pi / 4".ToEntity(), "arccotan(1)".ToEntity().Simplify());
+ }
+
+ ///
+ /// And the sum rule, which had the convention right all along, still does — so the two
+ /// readings of arccotan in the library now agree instead of contradicting.
+ ///
+ [Theory]
+ [InlineData("arctan(1) + arccotan(1)", "pi / 2")]
+ [InlineData("arctan(-1) + arccotan(-1)", "-1/2 * pi")]
+ [InlineData("arctan(0) + arccotan(0)", "pi / 2")]
+ public void TheSumRuleAndTheTableAgree(string expression, string expected)
+ => Assert.Equal(expected.ToEntity().Simplify(), expression.ToEntity().Simplify());
+
+ ///
+ /// arccos uses the same complement helper and is not affected: its range is
+ /// [0, pi] and arcsin's is [-pi/2, pi/2], so pi/2 - arcsin(x)
+ /// holds for every argument. Asserted so that a later tidy-up cannot merge the two paths
+ /// back together.
+ ///
+ [Theory]
+ [InlineData("arccos(1/2)")]
+ [InlineData("arccos(-1/2)")]
+ [InlineData("arccos(sqrt(3) / 2)")]
+ [InlineData("arccos(-sqrt(2) / 2)")]
+ public void ArccosIsUnaffected(string expression)
+ {
+ var entity = expression.ToEntity();
+ Assert.Equal(
+ (double)entity.EvalNumerical().RealPart,
+ (double)entity.Simplify().EvalNumerical().RealPart,
+ 12);
+ }
+ }
+}