Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,47 @@ internal static bool PullArccos(Complex arg, [NotNullWhen(true)] out Entity? res
=> Complement(PullArcsin(arg, out var arcsin), arcsin, out res);

/// <summary>
/// arccotan(x) = pi/2 - arctan(x), the same reading of it the simplification rule for
/// arctan(x) + arccotan(x) already takes.
/// <b><c>arccotan(x)</c> here is <c>arctan(1/x)</c>, with range <c>(-pi/2, pi/2]</c> — not
/// the textbook <c>(0, pi)</c>.</b> So the complement is <c>pi/2 - arctan(x)</c> for a
/// non-negative argument and <c>-pi/2 - arctan(x)</c> for a negative one.
/// </summary>
/// <remarks>
/// <para>
/// This read <c>pi/2 - arctan(x)</c> for every argument, which is the textbook identity and
/// is <b>wrong on every negative one</b>: <c>arccotan(-1)</c> came back as <c>3/4 * pi</c>
/// where the function's own value is <c>-pi/4</c>, so <see cref="Entity.Simplify(int)"/>
/// and <c>EvalNumerical</c> disagreed about a closed-form constant. Measured at the three
/// arguments that settle a range: <c>arccotan(1)</c> is <c>0.785…</c>, <c>arccotan(-1)</c>
/// is <c>-0.785…</c> and <c>arccotan(0)</c> is <c>1.570…</c>, so the range is
/// <c>(-pi/2, pi/2]</c> and the function is odd away from zero.
/// </para>
/// <para>
/// The docstring it replaces claimed to take "the same reading of it the simplification
/// rule for <c>arctan(x) + arccotan(x)</c> already takes". That rule answers <c>pi/2</c>
/// for a non-negative argument and <c>-pi/2</c> for a negative one
/// (<a href="https://github.com/asc-community/AngouriMath/issues/887">#887</a>) — so the
/// two readings were opposite, and the comment asserting they agreed is what let it stand.
/// </para>
/// </remarks>
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;
}

/// <summary>
/// arccos(x) = pi/2 - arcsin(x), which is also the range arccos is stated over — and unlike
/// <see cref="PullArccotan"/> this holds for every argument, arccos running over
/// <c>[0, pi]</c> while arcsin runs over <c>[-pi/2, pi/2]</c>.
/// </summary>
private static bool Complement(bool found, Entity? angle, [NotNullWhen(true)] out Entity? res)
{
if (!found || angle is null)
Expand Down
112 changes: 112 additions & 0 deletions Sources/Tests/UnitTests/Common/ArccotanTableSignTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// <see cref="Entity.Simplify(int)"/> and <c>EvalNumerical</c> have to agree about a
/// closed-form constant, and for a negative <c>arccotan</c> they did not.
/// </summary>
/// <remarks>
/// <para>
/// <b><c>arccotan</c> here is <c>arctan(1/x)</c>, with range <c>(-pi/2, pi/2]</c>.</b> The
/// inverse-trigonometric table read it as the textbook <c>pi/2 - arctan(x)</c>, whose range is
/// <c>(0, pi)</c>: the two agree on every positive argument and on nothing negative, so
/// <c>arccotan(-1)</c> simplified to <c>3/4 * pi</c> while the function's own value there is
/// <c>-pi/4</c>.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
[Trait("Area", "Common")]
public sealed class ArccotanTableSignTest
{
/// <summary>
/// The range, at the three arguments that pin it. <c>arccotan</c> is odd away from zero and
/// lands on <c>pi/2</c> there, which is <c>(-pi/2, pi/2]</c> and not <c>(0, pi)</c>.
/// </summary>
[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);

/// <summary>
/// Every table value, positive and negative, and the closed form has to be the value.
/// </summary>
[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);
}

/// <summary>
/// The named case, exactly as it was wrong.
/// </summary>
[Fact]
public void ANegativeArccotanIsNegative()
{
Assert.Equal("-1/4 * pi".ToEntity(), "arccotan(-1)".ToEntity().Simplify());
Assert.Equal("pi / 4".ToEntity(), "arccotan(1)".ToEntity().Simplify());
}

/// <summary>
/// And the sum rule, which had the convention right all along, still does — so the two
/// readings of <c>arccotan</c> in the library now agree instead of contradicting.
/// </summary>
[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());

/// <summary>
/// <c>arccos</c> uses the same complement helper and is <b>not</b> affected: its range is
/// <c>[0, pi]</c> and <c>arcsin</c>'s is <c>[-pi/2, pi/2]</c>, so <c>pi/2 - arcsin(x)</c>
/// holds for every argument. Asserted so that a later tidy-up cannot merge the two paths
/// back together.
/// </summary>
[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);
}
}
}
Loading