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
34 changes: 34 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ read first.
| loud | a known gap, e.g. a cubic inequality | `AngouriBugException`, asking to be reported | `NotSufficientlySupportedException` |
| **silent** | `arcsin(sin(x))` and three siblings | `x`, wrong wherever `x` leaves the principal interval | left as written unless `x` is a real in that interval |
| **silent** | `arctan(x) + arccotan(x)` | `pi/2`, wrong for every negative `x` | `pi/2` or `-pi/2` where the sign is known, else left as written |
| **silent** | `log(1, 1)` | `0` | `NaN`, since it is `0/0` |
| **silent** | `log(b, 1)` | `0` for any base | `0 provided not b = 1` |
| **silent** | `log(1/2, 0)` and any base below 1 | `-oo` | `+oo` |

---

Expand Down Expand Up @@ -241,6 +244,37 @@ when in fact it merely has another value — trading a wrong value for a wrong d
asserts that the expression is left alone. Issue
[#884](https://github.com/asc-community/AngouriMath/issues/884).

### A logarithm behaves like the division it is defined as

`log_b(z)` is `ln(z) / ln(b)`, and three answers did not follow from that. Every division by zero in
this library is `NaN` — `0/0`, `2/0` and `-2/0` all are — so a logarithm whose base is `1` divides by
`ln(1) = 0` and must be `NaN` too.

| | was | is |
|---|---|---|
| `log(1, 1)` | `0` | `NaN` — it is `0/0` |
| `log(1, 2)` | `+oo` | `NaN` — dividing by `ln 1 = 0` has no signed answer |
| `log(b, 1)` | `0`, for any base including 1 | `0 provided not b = 1` |
| `log(1/2, 0)`, and any base below 1 | `-oo` | `+oo` |
| `log(2, 0)`, and any base above 1 | `-oo` | `-oo`, unchanged |
| `log(x, 0)` for symbolic `x` | `-oo` | left as written |

The first two came from a shortcut: `Number.Log` uses `EDecimal.LogN` for a positive real base, and
`LogN` answers `0` for `log_1(1)` and `+oo` for `log_1(2)` where falling through to `Ln(x)/Ln(base)`
gives `NaN` for both. A base of `1` is now excluded from the shortcut so the two paths agree.

`log(b, 1)` is `0/ln(b)`, which is `0` for every base but `1`. **A condition is right here**, unlike
the interval cases above: at `b = 1` the expression genuinely is undefined rather than merely
something else, so narrowing the domain is what the mathematics says.

`log(b, 0)` is `-oo/ln(b)`, so the sign of the answer follows the sign of `ln(b)`. It answered `-oo`
for every base, which is wrong below `1`. For a base that cannot be placed on one side of `1` there is
no signed answer to give, so the node is left as written.

Issue [#890](https://github.com/asc-community/AngouriMath/issues/890), which also records a second
half not fixed here: `DomainCondition` of `log(-3, -3)` is `False` while the expression evaluates to
`1`, so the declared domain and the evaluation disagree. That is #721's question and wants a decision.

### `arctan(x) + arccotan(x)` is not always `pi/2`, and `arccotan(cotan(x))` was guarded wrongly

Both follow from one fact about this library's `arccotan`: it is `arctan(1/x)` extended by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,14 @@ public static Complex Log(Complex @base, Complex x)
{
if (LostToExponentRange(x) || LostToExponentRange(@base))
return Ln(x) / Ln(@base);
if (x is Real real && real.EDecimal.CompareTo(EDecimal.Zero) > 0 && @base is Real realBase && realBase.EDecimal.CompareTo(EDecimal.Zero) > 0)
// A base of 1 is excluded from the real shortcut so that it falls through to the
// ratio below. log_1(x) is ln(x)/ln(1) = ln(x)/0, and every division by zero in
// this library is NaN -- but LogN answers 0 for log_1(1) and +oo for log_1(2),
// so the shortcut and the definition disagreed.
// https://github.com/asc-community/AngouriMath/issues/890
if (x is Real real && real.EDecimal.CompareTo(EDecimal.Zero) > 0
&& @base is Real realBase && realBase.EDecimal.CompareTo(EDecimal.Zero) > 0
&& realBase.EDecimal.CompareTo(EDecimal.One) != 0)
return real.EDecimal.LogN(realBase.EDecimal, MathS.Settings.DecimalPrecisionContext);
// From https://source.dot.net/#System.Runtime.Numerics/System/Numerics/Complex.cs,cf15f2e5cc49cef1
return Ln(x) / Ln(@base);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,27 @@ protected override Entity InnerSimplify(bool isExact) =>
(a, b) => (a, b) switch
{
(Complex n1, Complex n2) when !isExact => Number.Log(n1, n2),
({ DomainCondition: var condition }, Integer(0)) => Real.NegativeInfinity.Provided(condition),
({ DomainCondition: var condition }, Integer(1)) => Integer.Zero.Provided(condition),

// log_b(0) is ln(0)/ln(b), that is -oo/ln(b), so the sign of the answer
// is the sign of ln(b): -oo for a base above 1 and +oo for one between
// 0 and 1. This answered -oo for every base, which is wrong below 1 --
// and for a base this cannot place on one side of 1 there is no signed
// answer to give, so the node is left as written.
// https://github.com/asc-community/AngouriMath/issues/890
({ DomainCondition: var condition }, Integer(0))
when a.Evaled is Real above && above > 1
=> Real.NegativeInfinity.Provided(condition),
({ DomainCondition: var condition }, Integer(0))
when a.Evaled is Real below && below > 0 && below < 1
=> Real.PositiveInfinity.Provided(condition),

// log_b(1) is 0/ln(b), which is 0 for every base but 1, where it is
// 0/0 -- and every division by zero here is NaN. The condition belongs
// on the answer rather than gating the rule, because at b = 1 the
// expression is genuinely undefined and not merely something else.
({ DomainCondition: var condition }, Integer(1))
=> Integer.Zero.Provided(condition).Provided(!a.EqualTo(1)),

_ => null
},
(@this, a, b) => ((Logf)@this).New(a, b), isExact);
Expand Down
46 changes: 45 additions & 1 deletion Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,51 @@ public void ComposingAFunctionOverItsOwnInverseIsStillTheIdentity(string input)
static double Magnitude(Entity difference) =>
((System.Numerics.Complex)difference.EvalNumerical()).Magnitude;

// https://github.com/asc-community/AngouriMath/issues/890
// log_b(1) is ln(1)/ln(b), which is 0/ln(b) -- so 0 for every base except 1, where it
// is 0/0. The rewrite answered 0 for any base at all, so log(1, 1) was 0 where every
// division by zero in this library is NaN.
[Fact]
public void LogarithmOfOneIsZeroOnlyWhereTheBaseIsNotOne()
{
Assert.Equal(MathS.NaN, "log(1, 1)".ToEntity().Simplify());
Assert.Equal(MathS.NaN, "log(1, 1)".ToEntity().EvalNumerical());
}

[Theory]
[InlineData("log(2, 1)")]
[InlineData("log(1/2, 1)")]
[InlineData("log(e, 1)")]
public void LogarithmOfOneStillCollapsesForAnOrdinaryBase(string expression) =>
Assert.Equal(Entity.Number.Integer.Create(0), expression.ToEntity().Simplify());

// A symbolic base cannot be placed away from 1, so the answer carries the condition --
// and here a condition is right, because at b = 1 the expression really is undefined
// rather than merely different.
[Fact]
public void LogarithmOfOneOverASymbolCarriesItsCondition()
{
var simplified = "log(x, 1)".ToEntity().Simplify();
Assert.NotEqual(Entity.Number.Integer.Create(0), simplified);
Assert.Equal(MathS.NaN, simplified.Substitute("x", 1).EvalNumerical());
}

// log_b(0) is ln(0)/ln(b) = -oo/ln(b), whose sign follows the sign of ln(b): -oo above
// 1 and +oo between 0 and 1. It answered -oo for every base.
[Theory]
[InlineData("log(2, 0)", "-oo")]
[InlineData("log(3, 0)", "-oo")]
[InlineData("log(1/2, 0)", "+oo")]
[InlineData("log(1/3, 0)", "+oo")]
public void LogarithmOfZeroFollowsTheSignOfItsBase(string expression, string expected) =>
Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());

// For a base whose side of 1 cannot be read there is no signed answer to give, so the
// node is left as written rather than answered with one of the two.
[Fact]
public void LogarithmOfZeroOverASymbolIsLeftAlone() =>
Assert.Equal("log(x, 0)".ToEntity(), "log(x, 0)".ToEntity().Simplify());

// This library's arccotan is arctan(1/x) extended with arccotan(0) = pi/2, so its
// range is (-pi/2, pi/2] and not the (0, pi) some texts use. #884 guarded
// arccotan(cotan(x)) with [0, pi] on the assumption it was the latter, which left the
Expand Down Expand Up @@ -654,6 +699,5 @@ public void ArctanPlusArccotanOfASymbolIsLeftAlone()
Assert.NotEqual(MathS.pi / 2, simplified);
Assert.NotEqual(-MathS.pi / 2, simplified);
}

}
}
Loading