diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 87aef94bf..5215ce8da 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -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` | --- @@ -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 diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs index c4f8e70fe..353474c3b 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs @@ -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); diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index 868d7d70f..c5972d1f9 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -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); diff --git a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs index 0743c8855..10be70359 100644 --- a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs @@ -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 @@ -654,6 +699,5 @@ public void ArctanPlusArccotanOfASymbolIsLeftAlone() Assert.NotEqual(MathS.pi / 2, simplified); Assert.NotEqual(-MathS.pi / 2, simplified); } - } }