diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 59064adbc..23461c32b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -616,6 +616,19 @@ Both are real, `im(3 + 4i)` being `4`, real-linear, and turned by the conjugate | `"im(3 + 4i)".ToEntity().Simplify()` | `(3 + 4i) * im` | `4` | | `"re * x".ToEntity()` | `re * x` | `re * x` | +### The factorial and the gamma function are defined off the real line + +**Silent: the condition a simplified factorial carries is `x in ZZ implies x >= 0`, where it was `x in RR and (x >= 0 +or not x in ZZ)`.** A factorial, and the gamma function with it, has poles at the negative integers and nowhere else. +Its condition also asked for a real argument, though `i!` is `0.498 - 0.155i`. So `x!/x!` simplified to an answer with +no value at `i`, where it is 1 ([#2256](https://github.com/asc-community/AngouriMath/issues/2256)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x!/x!".ToEntity().Simplify()` | `1 provided x in RR and (x >= 0 or not x in ZZ)` | `1 provided x in ZZ implies x >= 0` | +| `"x! = 0".ToEntity().Simplify()` | `False provided x in RR and (x >= 0 or not x in ZZ)` | `False provided x in ZZ implies x >= 0` | +| `"x!".ToEntity().DomainCondition` | `x in RR and (x >= 0 or not x in ZZ)` | `x in ZZ implies x >= 0` | + ### A number's conjugate is made under the setting in force **Silent: `Complex.Conjugate` is made under the downcasting setting of the caller, where the first caller's setting was diff --git a/Sources/.editorconfig b/Sources/.editorconfig index b677d246c..8b9e25e3d 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -240,6 +240,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Convenience/ConjugateAndArgumentTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Convenience/FactorialOffTheRealLineTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Algebra/SolveTest/FractionalPowerRangeTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs index 287da1421..bb36745d5 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs @@ -202,6 +202,10 @@ private static bool HoldsOverTheRange(Entity condition, Variable index, int star => HoldsOverTheRange(left, index, start) && HoldsOverTheRange(right, index, start), Orf(var left, var right) => HoldsOverTheRange(left, index, start) || HoldsOverTheRange(right, index, start), + // A factorial's own condition, index + a in ZZ implies index + a >= 0: true wherever + // its conclusion is. https://github.com/asc-community/AngouriMath/issues/2256 + Impliesf(_, var conclusion) + => HoldsOverTheRange(conclusion, index, start), _ => false, }; 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 f8a8bc6fd..2ee76bce1 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 @@ -524,9 +524,15 @@ public partial record Factorialf { // Factorial is defined for non-negative integers in the traditional sense, // but extends to complex numbers via the gamma function: n! = Γ(n+1) - // The gamma function has poles at negative integers, so factorial is undefined there - private protected override Entity IntrinsicCondition => - Argument.In(MathS.Sets.R) & (Argument >= 0 | !Argument.In(MathS.Sets.Z)); + // The gamma function has poles at negative integers, so factorial is undefined there, + // and only there. The condition asked for a real argument as well, though i! is + // 0.498 - 0.155i, so x!/x! simplified to 1 provided x in RR, which has no value at i. + // Off the integers x >= 0 may have no truth value and x in ZZ is false, so the implication + // is true. It is written as Simplify writes it, so that a quotient's x! = 0, which Simplify + // reads as False under this same condition, folds into it. + // https://github.com/asc-community/AngouriMath/issues/2256 + private protected override Entity IntrinsicCondition => + new Impliesf(Argument.In(MathS.Sets.Z), Argument >= 0); /// protected override Entity InnerSimplify(bool isExact) => diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs index 98dcae852..79fd0c2ee 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs @@ -280,7 +280,7 @@ internal static Entity PushNotInside(IEnumerable operands, bool disjoin) // a! = 0 // A factorial is never zero *where it is defined*, and at a negative integer it is // not: `(-1)!` is a pole, so `(-1)! = 0` is NaN rather than False. The condition owed - // is therefore the factorial's own -- `x in RR and (x >= 0 or not x in ZZ)`. + // is therefore the factorial's own -- `x in ZZ implies x >= 0`. // // What was written was `Factorialf({ DomainCondition: var condition })`, a property // pattern on the factorial's *argument*. For a bare variable that condition is True, diff --git a/Sources/Tests/UnitTests/Convenience/FactorialOffTheRealLineTest.cs b/Sources/Tests/UnitTests/Convenience/FactorialOffTheRealLineTest.cs new file mode 100644 index 000000000..fa7533431 --- /dev/null +++ b/Sources/Tests/UnitTests/Convenience/FactorialOffTheRealLineTest.cs @@ -0,0 +1,48 @@ +// +// 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; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Convenience +{ + /// + /// The factorial, and the gamma function with it, is defined everywhere but at the negative integers, its poles, where + /// its condition asked for a real argument as well: i! is 0.498 - 0.155i, and x!/x! simplified to + /// 1 provided x in RR, which has no value at i. + /// #2256 + /// + [Trait("Area", "Convenience")] + public sealed class FactorialOffTheRealLineTest + { + [Theory] + [InlineData("x!", "i", true)] + [InlineData("x!", "1/2 + 6/5 * i", true)] + [InlineData("x!", "-2 * i", true)] + [InlineData("x!", "-1/2", true)] + [InlineData("x!", "0", true)] + [InlineData("x!", "3", true)] + [InlineData("x!", "-1", false)] + [InlineData("x!", "-2", false)] + [InlineData("gamma(x)", "i", true)] + [InlineData("gamma(x)", "0", false)] + [InlineData("gamma(x)", "-3", false)] + public void ItIsDefinedOffThePoles(string expression, string at, bool defined) + { + var condition = expression.ToEntity().DomainCondition.Substitute("x", at.ToEntity()).Evaled; + Assert.Equal(defined ? Entity.Boolean.True : Entity.Boolean.False, condition); + Assert.Equal(!defined, expression.ToEntity().Substitute("x", at.ToEntity()).Evaled.IsNaN); + } + + [Theory] + [InlineData("x! / x!", "i")] + [InlineData("gamma(x) / gamma(x)", "1/2 + 6/5 * i")] + public void ASimplificationKeepsItsValueThere(string expression, string at) + => Assert.Equal(Entity.Number.Integer.One, expression.ToEntity().Simplify().Substitute("x", at.ToEntity()).Evaled); + } +} diff --git a/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs b/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs index 355c54979..ae0ce22c2 100644 --- a/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs @@ -89,13 +89,16 @@ [Fact] public void Patt8Negative() => // with the unsimplified quotient at every one -- NaN at the three poles and 1 elsewhere. // The old form was adequate here by accident, since `(1 + x)!` is NaN at a pole and a // comparison against NaN is not True; the new one says so rather than relying on it. - [Fact] public void FactorialXP1OverFactorialXP1() => AssertSimplify(MathS.Factorial(x + 1) / MathS.Factorial(x + 1), "1 provided 1 + x in RR and (1 + x >= 0 or not 1 + x in ZZ)"); + // A factorial is defined off its poles, the negative integers, and off the real line too: the + // condition asked for 1 + x in RR, so this had no value at x = i, where it is 1. + // https://github.com/asc-community/AngouriMath/issues/2256 + [Fact] public void FactorialXP1OverFactorialXP1() => AssertSimplify(MathS.Factorial(x + 1) / MathS.Factorial(x + 1), "1 provided 1 + x in ZZ implies 1 + x >= 0"); [Fact] public void FactorialXP1OverFactorialX() => AssertSimplify(MathS.Factorial(1 + x) / MathS.Factorial(x), 1 + x); [Fact] public void FactorialXP1OverFactorialXM2() => AssertSimplify(MathS.Factorial(1 + x) / MathS.Factorial(-2 + x), "x3 - x"); [Fact] public void FactorialXP1OverFactorialXM1() => AssertSimplify(MathS.Factorial(1 + x) / MathS.Factorial(-1 + x), x * (1 + x)); [Fact] public void FactorialXP1OverFactorialXM3() => AssertSimplifyIdentical(MathS.Factorial(x + 1) / MathS.Factorial(x - 3)); [Fact] public void FactorialXOverFactorialXP1() => AssertSimplify(MathS.Factorial(x) / MathS.Factorial(x + 1), 1 / (1 + x)); - [Fact] public void FactorialXOverFactorialX() => AssertSimplify(MathS.Factorial(x) / MathS.Factorial(x), "1 provided x in RR and (x >= 0 or not x in ZZ)"); + [Fact] public void FactorialXOverFactorialX() => AssertSimplify(MathS.Factorial(x) / MathS.Factorial(x), "1 provided x in ZZ implies x >= 0"); [Fact] public void FactorialXOverFactorialXM1() => AssertSimplify(MathS.Factorial(0 + x) / MathS.Factorial(x - 1), x); [Fact] public void FactorialXOverFactorialXM2() => AssertSimplify(MathS.Factorial(x + 0) / MathS.Factorial(x - 2), x.Pow(2) - x); [Fact] public void FactorialXOverFactorialXM3() => AssertSimplifyIdentical(MathS.Factorial(x) / MathS.Factorial(x - 3)); @@ -105,7 +108,7 @@ [Fact] public void Patt8Negative() => // commit, because a bare `x!` reached this through the candidate polynomial long division // produces rather than through a cancellation rule. That division carries its divisor's // domain now as well, so all three of these agree again. - [Fact] public void FactorialXM1OverFactorialXM1() => AssertSimplify(MathS.Factorial(x - 1) / MathS.Factorial(x - 1), "1 provided x - 1 in RR and (x - 1 >= 0 or not x - 1 in ZZ)"); + [Fact] public void FactorialXM1OverFactorialXM1() => AssertSimplify(MathS.Factorial(x - 1) / MathS.Factorial(x - 1), "1 provided x - 1 in ZZ implies x - 1 >= 0"); [Fact] public void FactorialXM1OverFactorialXM2() => AssertSimplify(MathS.Factorial(-1 + x) / MathS.Factorial(-2 + x), x - 1); // Same two factors, now in ascending order: the polynomial factorer offers // (x - 1) * (x - 2) and the complexity metric cannot tell the two orders apart.