diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2e78fbd59..bea3c1595 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -15,6 +15,74 @@ read first. --- +## Unreleased — since 2.1.0 + +### At a glance + +| Silent? | What | Was | Is | +|---|---|---|---| +| **silent** | `log(x, x)` simplified | `1 provided x > 0` — `NaN` at every negative `x`, and `1` at `x = 1` where the logarithm is `NaN` | `1 provided not x = 0 and not x = 1` | +| **silent** | `DomainCondition` of a logarithm, under the default reading | the real condition, so `log(-3, -3)` was declared undefined while evaluating to `1` | `not b = 0 and not b = 1 and not a = 0` | +| **silent** | `ln(x).DomainCondition` | `x > 0` | `not x = 0` | +| **silent** | `"x ^ n".Differentiate("x").Simplify()` | `x ^ n * n / x provided x > 0` — `NaN` at every negative `x` | `x ^ n * n / x provided not x = 0` | + +### The logarithm's domain follows the reading, as every other node's already did + +`Arcsinf`, `Arccosf`, `Arcsecantf` and `Arccosecantf` each state one condition over the reals and +another over the complex plane, and pick between them by the reading. `Logf` stated the real one as +though it were the only one, which is what +[#890](https://github.com/asc-community/AngouriMath/issues/890) reported as a contradiction: the +library gave an expression a value and declared it undefined at the same time. + +``` +log(-3, -3) 1 — unchanged +log(-3, -3).DomainCondition False -> True +ln(x).DomainCondition x > 0 -> not x = 0 +``` + +Over the complex plane `log_b(a)` is `ln(a) / ln(b)`, which asks only that both logarithms exist +and that the denominator is not zero — no complex number has `0` as its exponential, so `a` and `b` +must be nonzero, and `ln(b) = 0` is `b = 1`. Negative and non-real arguments are admitted: +`log(2, -8)` is `3 + 4.53i` and was declared undefined. + +**The real condition is still there and still reachable.** `expr.WithCodomain(Domain.Real)` gives +`x > 0 and not x = 1` for `log(x, x)` exactly as before; what changed is which of the two is the +default, and the default is now the same complex reading the evaluator has always used. + +### `log_b(b) -> 1` carries the condition it has rather than one written into the rule + +The rewrite asserted `provided x > 0`, and that was wrong in both directions at once — it withdrew +an answer that exists and kept one that does not: + +``` +"log(x, x)".Simplify() 1 provided x > 0 -> 1 provided not x = 0 and not x = 1 + at x = -3 NaN -> 1 (the logarithm is 1) + at x = 1 1 -> NaN (the logarithm is NaN) +``` + +`boundcheck` reports one disagreement where it reported two. The survivor, +`ln(x) + ln(x+1) -> ln(x*(1+x))`, needs an assumption travelling with the variable and is +untouched here. + +### The derivative of a symbolic power loses a condition it never needed + +`d/dx x^n` goes through `ln(x) * 0`, and `anything * 0 = 0` carries the operand's domain condition. +That condition was the logarithm's real one, so the derivative was undefined at every negative `x`: + +``` +"x ^ n".Differentiate("x").Simplify() x ^ n * n / x provided x > 0 + -> x ^ n * n / x provided not x = 0 + at n = 3, x = -2.5 NaN -> 75/4 (and 3x^2 at -2.5 is 75/4) +``` + +The condition does not vanish: `ln(0) * 0` is `-oo * 0`, which is `NaN`, so `not x = 0` is kept. + +From [#721](https://github.com/asc-community/AngouriMath/issues/721) and +[#890](https://github.com/asc-community/AngouriMath/issues/890), in PR +[#916](https://github.com/asc-community/AngouriMath/pull/916). + +--- + ## 2.1.0 — since 2.0.0 Everything here landed **after** the 2.0.0 tag, so it is not in the package a reader of that 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 3f7942eae..a2272ea61 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 @@ -220,12 +220,29 @@ x.Evaled is Complex c public partial record Logf { - // Logarithm is undefined when: - // - base <= 0 or base = 1 - // - antilogarithm <= 0 - // For complex logarithms, we use the principal branch - private protected override Entity IntrinsicCondition => - Base > 0 & !Base.EqualTo(1) & Antilogarithm > 0; + // Which of the two conditions below applies is the reading, exactly as it is + // for Arcsinf and its siblings. This node alone stated the real one as though + // it were the only one, so the library declared an expression undefined and + // gave it a value at the same time: log(-3, -3) evaluates to 1 while its + // DomainCondition was False. + // + // Over the reals the base must be positive and not 1, and the antilogarithm + // positive. Over the complex plane log_b(a) is ln(a)/ln(b), which asks only + // that both logarithms exist and that the denominator is not zero: no complex + // number has 0 as its exponential, so a and b must be nonzero, and ln(b) = 0 + // is b = 1. Negative and non-real arguments are fine -- log(-3, -3) is 1 and + // log(2, -8) is 3 + 4.53i -- which is the whole of what the real condition was + // wrongly forbidding. + // + // The zero cases are stated rather than read off evaluation, which returns the + // extended-real -oo for ln(0) and so would call it defined. -oo is not a + // complex number, and taking it for one loses conditions that are needed + // downstream: `ln(x) * 0` is 0 only away from x = 0, since -oo * 0 is NaN. + // https://github.com/asc-community/AngouriMath/issues/721 + private protected override Entity IntrinsicCondition => + Codomain < Domain.Complex + ? Base > 0 & !Base.EqualTo(1) & Antilogarithm > 0 + : !Base.EqualTo(0) & !Base.EqualTo(1) & !Antilogarithm.EqualTo(0); /// protected override Entity InnerSimplify(bool isExact) => diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs index b156debec..2a818bf8d 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs @@ -182,7 +182,15 @@ expr is Powf(var @base, Integer { IsNegative: true } pow) // https://github.com/asc-community/AngouriMath/issues/902 Logf(var any1, Powf(var any2, var any3)) when IsPositiveReal(any2) && MayBeTakenAsReal(any3) => any3 * MathS.Log(any1, any2), - Logf(var any1, var any1a) when any1 == any1a => new Providedf(1, any1 > 0), + // log_b(b) is 1 wherever it is defined at all, so the condition to carry is the + // node's own and not one written out here. Asserting `any1 > 0` stated the real + // reading inside the rule and was wrong in both directions at once: undefined at + // x = -3, where log(-3, -3) evaluates to 1, and defined at x = 1, where + // log(1, 1) is NaN. Reading the condition instead gets both, and follows the + // reading rather than fixing it. + // https://github.com/asc-community/AngouriMath/issues/721 + Logf(var any1, var any1a) logarithm when any1 == any1a + => new Providedf(1, logarithm.DomainCondition), Logf(Divf(Integer(1), var any1), Divf(Integer(1), var any2)) => MathS.Log(any1, any2), Logf(var any1, Divf(Integer(1), var any2)) => -MathS.Log(any1, any2), Logf(Divf(Integer(1), var any1), var any2) => -MathS.Log(any1, any2), diff --git a/Sources/Tests/UnitTests/Calculus/DerivativeGapsTest.cs b/Sources/Tests/UnitTests/Calculus/DerivativeGapsTest.cs index f62b51ca1..4cb34b9bf 100644 --- a/Sources/Tests/UnitTests/Calculus/DerivativeGapsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/DerivativeGapsTest.cs @@ -66,9 +66,14 @@ public void AntiderivativeOfAPowerDifferentiatesBackEverywhere() } // A genuinely symbolic exponent must still take the logarithmic rule, condition - // and all. + // and all. The condition is `not x = 0` and was `x > 0` until the logarithm's + // domain stopped being stated over the reals regardless of the reading: it comes + // from `ln(x) * 0`, which is 0 everywhere ln(x) has a value and NaN at x = 0, + // where -oo * 0 is indeterminate. The old condition also cost the answer at every + // negative x -- at n = 3, x = -2.5 the derivative is 75/4 and used to be NaN. + // https://github.com/asc-community/AngouriMath/issues/721 [Theory] - [InlineData("x ^ n", "x ^ n * n / x provided x > 0")] + [InlineData("x ^ n", "x ^ n * n / x provided not x = 0")] [InlineData("2 ^ x", "ln(2) * 2 ^ x")] // Compared as printed text: what is under test is that the logarithmic rule and // its condition are still used, and the tree differs from the parsed expectation diff --git a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs index c2d2e32fd..bfecb6a2b 100644 --- a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs @@ -1005,5 +1005,66 @@ public void SimplifyAndEvaluationAgreeOffTheRealLine(string input) Assert.Equal(original.Substitute("x", "i").Evaled, original.Simplify().Substitute("x", "i").Evaled); } + + // https://github.com/asc-community/AngouriMath/issues/721 + // log_b(b) is 1 wherever it is defined, and the rewrite carried `provided x > 0` -- + // the real-domain condition, written out inside the rule rather than read from the + // node. It was wrong in both directions at once: at x = -3 the logarithm is 1 and + // the simplification was NaN, and at x = 1 the logarithm is NaN and the + // simplification was 1. + [Theory] + [InlineData("-3")] + [InlineData("-1/2")] + [InlineData("2")] + [InlineData("1")] + [InlineData("0")] + public void ALogarithmOfItsOwnBaseKeepsItsValueOffThePositiveReals(string at) + { + var original = "log(x, x)".ToEntity(); + var simplified = original.Simplify(); + Assert.Equal(original.Substitute("x", at.ToEntity()).Evaled, + simplified.Substitute("x", at.ToEntity()).Evaled); + } + + // The contradiction #890 reported, which is the same defect seen from the domain + // side: the library gave the expression a value and declared it undefined + // everywhere at the same time. + [Theory] + [InlineData("log(-3, -3)")] + [InlineData("log(2, -8)")] + [InlineData("log(-2, 8)")] + public void ALogarithmWithNegativeArgumentsIsNotDeclaredUndefined(string input) + { + var expression = input.ToEntity(); + Assert.NotEqual(MathS.NaN, expression.Evaled); + Assert.Equal(Entity.Boolean.True, expression.DomainCondition.Evaled); + } + + // Which of the two conditions applies is the reading, as it already was for arcsin + // and its siblings. A negative base is admitted over the complex plane and refused + // over the reals; 0 and 1 are refused under either. + [Theory] + [InlineData("-3", true)] + [InlineData("2", true)] + [InlineData("1", false)] + [InlineData("0", false)] + public void TheLogarithmsDomainOverTheComplexPlaneAdmitsANegativeBase(string at, bool defined) + { + var condition = "log(x, x)".ToEntity().DomainCondition; + Assert.Equal(defined ? Entity.Boolean.True : Entity.Boolean.False, + condition.Substitute("x", at.ToEntity()).Evaled); + } + + [Theory] + [InlineData("-3", false)] + [InlineData("2", true)] + [InlineData("1", false)] + [InlineData("0", false)] + public void TheLogarithmsDomainOverTheRealsStillRefusesANegativeBase(string at, bool defined) + { + var condition = "log(x, x)".ToEntity().WithCodomain(Domain.Real).DomainCondition; + Assert.Equal(defined ? Entity.Boolean.True : Entity.Boolean.False, + condition.Substitute("x", at.ToEntity()).Evaled); + } } }