From e1833182db981fd7b2d684ffce2a03bae5357f3f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 30 Aug 2026 17:17:03 +0000 Subject: [PATCH] A negative arccotan is negative `arccotan` here is `arctan(1/x)`, with range `(-pi/2, pi/2]` -- not the textbook `(0, pi)`. `InverseTrigonometricTableValues.PullArccotan` 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: "arccotan(-1)".ToEntity().EvalNumerical() // -0.7853981633974483..., which is -pi/4 "arccotan(-1)".ToEntity().Simplify() // 3/4 * pi `Simplify` and `EvalNumerical` disagreeing about a constant is the sharpest form this kind of defect takes. 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**, answering `pi/2` for a non-negative argument and `-pi/2` for a negative one (#887). The table's docstring claimed to take "the same reading of it the simplification rule for arctan(x) + arccotan(x) already takes" and took the opposite one -- and the comment asserting they agreed is what let the disagreement stand for as long as it did. A docstring is not a check. The fix takes the sign 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`. `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. The helper keeps that reading and now says why on itself, and `ArccotanTableSignTest` asserts arccos is unmoved so that a later tidy-up cannot merge the two paths back together. **How it was found, which is the part worth keeping.** Writing an identity for the `arctan + arccotan` rule during the rule-description work, and measuring the function at a positive argument, a negative one and zero before writing the interval down rather than recalling it. The measurement confirmed the identity and showed `Simplify` contradicting the numeric value two lines further up the same output. The test measures the range the same way rather than asserting it. Recorded in BREAKING-CHANGES.md, both values measured on a build. Full suite: 9010 passed, 14 skipped, 0 failed. --- BREAKING-CHANGES.md | 27 +++++ .../InverseTrigonometricTableValues.cs | 41 ++++++- .../UnitTests/Common/ArccotanTableSignTest.cs | 112 ++++++++++++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 Sources/Tests/UnitTests/Common/ArccotanTableSignTest.cs 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); + } + } +}