diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index fde4eef32..92d4fc1b7 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -1239,21 +1239,36 @@ and a number that passes every round is left open, since passing is not a proof
| `"(10^30 + 1) in PP".ToEntity().Evaled` | `1000000000000000000000000000001 in PP` | `False` |
| `"(10^30 + 57) in PP".ToEntity().Evaled` | `1000000000000000000000000000057 in PP` | `1000000000000000000000000000057 in PP` |
-### An exact power too large to build is the decimal
-
-**Not silent: what did not return now answers.** An exact number to a whole power was built exactly however large it
-was. `46655^(10^20)` has 4.7e20 digits, and its evaluation neither returned nor stopped when cancelled. Nor did a
-`Simplify` of anything holding it. Past sixteen million bits, about five million digits, a power is now evaluated as the
-decimal, as the same power with a half added already was: `+oo` past the context's range, `-oo` for a negative base to
-an odd power, and `0` below the range. An exact simplification leaves it as written, since the infinity is the
-decimal's. Each of -1, i and -i keeps its exact power. No issue was filed; a check of compiled expressions against
-evaluation found it.
+### An exact power too large to build is left written
+
+**Not silent: what did not return now answers, or stays written.** An exact number to a whole power was built exactly
+however large it was. `46655^(10^20)` has 4.7e20 digits, and its evaluation neither returned nor stopped when cancelled.
+Nor did a `Simplify` of anything holding it. Past sixteen million bits, about five million digits, such a power is now
+left as written, by evaluation and by `Simplify` alike, and `EvalNumerical` refuses it with `CannotEvalException`. It
+is finite and not zero, so it is never `+oo`, `-oo` or `0`. An expression holding it is settled where structure settles
+it, as with `a - a` or `0 * a`, or where the exponents settle it, as with `2^m / 2^n`. A comparison is settled by the
+signs, by the exponents for one positive base, or by the logarithms of the magnitudes where they differ by more than
+their rounding. Anything else stays written. Each of -1, i and -i keeps its exact power. Measured on 2.6.0 with a
+twenty-second limit. https://github.com/asc-community/AngouriMath/issues/1448
| Input | Was (2.6.0) | Now |
|---|---|---|
-| `"46655^(10^20)".EvalNumerical()` | does not return | `+oo` |
-| `"(5/2)^(10^7)".EvalNumerical()` | does not return | `+oo` |
+| `"2^(10^9)".ToEntity().Evaled` | does not return | `2 ^ 1000000000` |
+| `"2^(-(10^9))".ToEntity().Evaled` | does not return | `2 ^ (-1000000000)` |
+| `"46655^(10^20)".EvalNumerical()` | does not return | throws `CannotEvalException` |
| `"2^(10^10)".Simplify()` | does not return | `2 ^ 10000000000` |
+| `"2^(10^9) - 2^(10^9)".Simplify()` | does not return | `0` |
+| `"x - x + 2^(10^9) - 2^(10^9)".Simplify()` | does not return | `0` |
+| `"2^(10^9) / 2^(10^9 - 1)".Simplify()` | does not return | `2` |
+| `"2^(10^9) * 2^(-(10^9))".Simplify()` | does not return | `1` |
+| `"0 * 2^(10^9)".ToEntity().Evaled` | does not return | `0` |
+| `"2^(10^9) > 2^(10^9 - 1)".ToEntity().Evaled` | does not return | `True` |
+| `"2^(10^9) = 2^(10^9 - 1)".ToEntity().Evaled` | does not return | `False` |
+| `"3^(10^9) > 2^(10^9)".ToEntity().Evaled` | does not return | `True` |
+| `"2^(10^9) > 5".ToEntity().Evaled` | does not return | `True` |
+| `"2^(-(10^9)) > 0".Simplify()` | does not return | `True` |
+| `"2^(10^9) = 4^(5 * 10^8)".ToEntity().Evaled` | does not return | `2 ^ 1000000000 = 4 ^ 500000000` |
+| `"forall n in ZZ* : 3^(n + 1) divides 2^(3^n) + 1".ToEntity().Evaled` | does not return | as written |
### The inverse of a symbolic matrix carries no condition of a pivot
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
index e2931bcff..64fd9024f 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
@@ -627,8 +627,10 @@ static Complex BinaryIntPow(Complex num, EInteger val)
}
// An exact power past MaxExactPowerBits is not built: 46655^(10^20) would have 4.7e20
// digits, and the multiplications for it never returned or stopped when cancelled, so
- // a simplification of the expression did not either. It is the decimal, as the same
- // power with a half added is: +oo past the context's range, 0 below it.
+ // a simplification of the expression did not either. Evaluating a power node leaves it
+ // written before this is reached (https://github.com/asc-community/AngouriMath/issues/1448);
+ // a caller of Pow itself gets the decimal, as the same power with a half added does:
+ // +oo past the context's range, 0 below it.
if (@base.IsFinite && pow is { } && IsTooLargeToBuild(@base, pow))
{
var largeContext = MathS.Settings.DecimalPrecisionContext;
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 d051593ba..4974424df 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
@@ -409,6 +409,17 @@ private bool IsEvenRoot
=> Exponent is Rational and not Integer and Rational ratio
&& ratio.ERational.Denominator.Remainder(2).IsZero;
+ ///
+ /// Whether an exact to the rational is past the size an
+ /// exact power is built to, a fraction p / q read as the whole power just above |p / q|: a
+ /// q-th root makes the power no longer than that, and one that is not exact is a decimal of the
+ /// same magnitude. A whole exponent allocates nothing here, being asked of every exact power.
+ ///
+ internal static bool IsTooLargeToBuild(Complex @base, Rational exponent)
+ => exponent is Integer { EInteger: var whole }
+ ? Number.IsTooLargeToBuild(@base, whole)
+ : Number.IsTooLargeToBuild(@base, exponent.ERational.Numerator.Abs().Divide(exponent.ERational.Denominator).Add(PeterO.Numbers.EInteger.One));
+
private static bool TryPower(Matrix m, int exp, out Entity res)
{
// Only a square matrix has powers; Pow throws for any other. And only one with
@@ -461,6 +472,12 @@ protected override Entity InnerSimplify(bool isExact) =>
? 0
: MathS.NaN
: new Providedf(0, Re(x) > 0),
+ // An exact power too large to build stays a power. It is finite and not zero, and as
+ // the decimal it was +oo or 0, which then fed exact arithmetic: 2^(10^9) - 2^(10^9)
+ // was NaN, 0 * 2^(10^9) was NaN and 2^(10^9) > 2^(10^9 - 1) was False. Left written,
+ // what can be said of it is said by structure or by its exponent, and the rest is
+ // left undecided. https://github.com/asc-community/AngouriMath/issues/1448
+ (Complex n1, Rational n2) when IsTooLargeToBuild(n1, n2) => null,
(Complex n1, Complex n2) when !isExact => Number.Pow(n1, n2), // returns 1 for 0^0 and 0 for 0^(negative real part), we need to handle these cases above
(Integer(1), { DomainCondition: var condition }) => Integer.One.Provided(condition),
(var n1, Integer(-1)) => (1 / n1).InnerSimplified(isExact),
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs
index 05ffd31a5..b93e5e296 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs
@@ -324,6 +324,10 @@ protected override Entity InnerSimplify(bool isExact)
// A size that is not a number is compared as a size, never by the difference
// of two expressions: card(ZZ) - card(QQ) is not zero, and the two are equal.
: AngouriMath.Core.Sets.Cardinality.IsSize(left) || AngouriMath.Core.Sets.Cardinality.IsSize(right) ? AngouriMath.Core.Sets.Cardinality.Compare(left, right, static sign => sign == 0, isExact)
+ // An exact power too large to build is not a constant, its value being left written, and is
+ // told apart from a number or another such power where its sign, exponent or magnitude decides it.
+ // https://github.com/asc-community/AngouriMath/issues/1448
+ : OrderOfUnbuiltPowers(left, right) is { } order ? order == 0
: left.IsConstant && right.IsConstant ? (HasAnUnreadSize(left) || HasAnUnreadSize(right) ? null : ConstantsAreEqual(left, right))
: left is Set setLeft && right is Set setRight ? SetsAreEqual(setLeft, setRight, isExact)
: null,
@@ -347,6 +351,80 @@ protected override Entity InnerSimplify(bool isExact)
_ => null,
};
+ ///
+ /// An exact power too large to build, which evaluation leaves written: an exact base that is not zero to a
+ /// rational exponent. for anything else.
+ /// https://github.com/asc-community/AngouriMath/issues/1448
+ ///
+ private static (Number.Rational Base, Number.Rational Exponent)? AsAnUnbuiltPower(Entity expr)
+ => expr is Powf(Number.Rational { IsZero: false } @base, Number.Rational exponent) && Powf.IsTooLargeToBuild(@base, exponent)
+ ? (@base, exponent) : null;
+
+ ///
+ /// The sign of a real power of an exact real base that is not zero, where its exponent decides it: positive
+ /// for a positive base, and for a negative one by the parity of a whole exponent.
+ ///
+ private static int? SignOfAnUnbuiltPower((Number.Rational Base, Number.Rational Exponent) power)
+ => power.Base.ERational.Sign > 0 ? 1
+ : power.Exponent is Number.Integer { EInteger: var whole } ? (whole.IsEven ? 1 : -1)
+ : null;
+
+ ///
+ /// The sign of left - right where either side is an exact power too large to build and the other is a
+ /// real number or another such power, decided without building either: two powers of one positive base by
+ /// their exponents, exactly, and otherwise by the signs and then by the logarithms of the magnitudes, where
+ /// those differ by more than the error of computing them. where nothing decides it,
+ /// which leaves the comparison written; in particular, two magnitudes that may be equal are never told
+ /// apart. As the decimals, these powers were +oo and 0, and 2^(10^9) > 2^(10^9 - 1)
+ /// was False. https://github.com/asc-community/AngouriMath/issues/1448
+ ///
+ private static int? OrderOfUnbuiltPowers(Entity left, Entity right)
+ {
+ var (powerLeft, powerRight) = (AsAnUnbuiltPower(left), AsAnUnbuiltPower(right));
+ if (powerLeft is null && powerRight is null)
+ return null;
+ if (powerLeft is { } l && powerRight is { } r && l.Base.ERational.Equals(r.Base.ERational) && l.Base.ERational.Sign > 0)
+ {
+ var byExponent = l.Exponent.ERational.CompareTo(r.Exponent.ERational);
+ return l.Base.ERational.CompareTo(ERational.One) > 0 ? byExponent : -byExponent;
+ }
+ if (SignAndLogarithm(left, powerLeft) is not var (signLeft, logLeft) || SignAndLogarithm(right, powerRight) is not var (signRight, logRight))
+ return null;
+ // A power is not zero, so a side of the other sign, or zero, is decided by the signs alone.
+ if (signLeft != signRight || logLeft is null || logRight is null)
+ return signLeft.CompareTo(signRight);
+ // Each logarithm is within a relative 1e-99 of its value: a decimal side is rounded to a hundred digits,
+ // and the rest is computed to OrderContext's. So a difference past 1e-90 of their size is not rounding.
+ var difference = logLeft.Subtract(logRight, OrderContext);
+ var bound = logLeft.Abs().Add(logRight.Abs()).Add(EDecimal.One).Multiply(OrderTolerance);
+ if (difference.Abs().CompareTo(bound) <= 0)
+ return null;
+ return signLeft * difference.Sign;
+ }
+
+ ///
+ /// The sign of an exact power too large to build, or of a finite real number, and the natural logarithm of
+ /// its magnitude, for zero. for anything else, and for a power
+ /// whose sign its exponent does not settle.
+ ///
+ private static (int Sign, EDecimal? Logarithm)? SignAndLogarithm(Entity side, (Number.Rational Base, Number.Rational Exponent)? power)
+ {
+ if (power is { } p)
+ return SignOfAnUnbuiltPower(p) is { } sign
+ ? (sign, p.Exponent.ERational.ToEDecimal(OrderContext)
+ .Multiply(p.Base.ERational.Abs().ToEDecimal(OrderContext).Log(OrderContext), OrderContext))
+ : null;
+ if (side is Number number && AsAReal(number) is { EDecimal: { IsFinite: true } real })
+ return real.IsZero ? (0, null) : (real.Sign, real.Abs().Log(OrderContext));
+ return null;
+ }
+
+ /// The working precision of : twenty digits past the decimals'.
+ [Core.ConstantField] private static readonly EContext OrderContext = EContext.ForPrecision(120);
+
+ /// The relative difference of two logarithms below which leaves them undecided.
+ [Core.ConstantField] private static readonly EDecimal OrderTolerance = EDecimal.FromString("1e-90");
+
partial record Greaterf
{
// Inequality comparisons are only defined for real numbers.
@@ -361,6 +439,7 @@ protected override Entity InnerSimplify(bool isExact)
(Number numLeft, Number numRight) when AsAReal(numLeft) is { } reLeft && AsAReal(numRight) is { } reRight => reLeft > reRight,
(Number numLeft, Number numRight) => MathS.NaN,
_ when AngouriMath.Core.Sets.Cardinality.IsSize(a) || AngouriMath.Core.Sets.Cardinality.IsSize(b) => AngouriMath.Core.Sets.Cardinality.Compare(a, b, static sign => sign > 0, isExact),
+ _ when OrderOfUnbuiltPowers(a, b) is { } sign => sign > 0,
_ => null
},
(@this, a, b) => ((Greaterf)@this).New(a, b), isExact);
@@ -381,6 +460,7 @@ protected override Entity InnerSimplify(bool isExact)
(Number numLeft, Number numRight) when AsAReal(numLeft) is { } reLeft && AsAReal(numRight) is { } reRight => reLeft >= reRight,
(Number numLeft, Number numRight) => MathS.NaN,
_ when AngouriMath.Core.Sets.Cardinality.IsSize(a) || AngouriMath.Core.Sets.Cardinality.IsSize(b) => AngouriMath.Core.Sets.Cardinality.Compare(a, b, static sign => sign >= 0, isExact),
+ _ when OrderOfUnbuiltPowers(a, b) is { } sign => sign >= 0,
_ => null
},
(@this, a, b) => ((GreaterOrEqualf)@this).New(a, b), isExact);
@@ -401,6 +481,7 @@ protected override Entity InnerSimplify(bool isExact)
(Number numLeft, Number numRight) when AsAReal(numLeft) is { } reLeft && AsAReal(numRight) is { } reRight => reLeft < reRight,
(Number numLeft, Number numRight) => MathS.NaN,
_ when AngouriMath.Core.Sets.Cardinality.IsSize(a) || AngouriMath.Core.Sets.Cardinality.IsSize(b) => AngouriMath.Core.Sets.Cardinality.Compare(a, b, static sign => sign < 0, isExact),
+ _ when OrderOfUnbuiltPowers(a, b) is { } sign => sign < 0,
_ => null
},
(@this, a, b) => ((Lessf)@this).New(a, b), isExact);
@@ -420,6 +501,7 @@ protected override Entity InnerSimplify(bool isExact)
(Number numLeft, Number numRight) when AsAReal(numLeft) is { } reLeft && AsAReal(numRight) is { } reRight => reLeft <= reRight,
(Number numLeft, Number numRight) => MathS.NaN,
_ when AngouriMath.Core.Sets.Cardinality.IsSize(a) || AngouriMath.Core.Sets.Cardinality.IsSize(b) => AngouriMath.Core.Sets.Cardinality.Compare(a, b, static sign => sign <= 0, isExact),
+ _ when OrderOfUnbuiltPowers(a, b) is { } sign => sign <= 0,
_ => null
},
(@this, a, b) => ((LessOrEqualf)@this).New(a, b), isExact);
diff --git a/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs b/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs
index df1204b96..fd69ff22b 100644
--- a/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs
+++ b/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs
@@ -17,27 +17,89 @@ namespace AngouriMath.Tests.Core
///
/// An exact number to a whole power was built exactly however large, so 46655^(10^20), which has
/// 4.7e20 digits, neither returned nor stopped when cancelled, and nor did a simplification of anything
- /// holding it. Past about sixteen million bits the power is now evaluated as the decimal, as the same
- /// power with a half added already was: +oo past the context's range. An exact simplification
- /// leaves it as written, since the infinity is the decimal's and not the power's. Found by compilesweep,
- /// on (x^x - 1)^(e^(e^x)) at 6; no issue was filed.
+ /// holding it. Found by compilesweep, on (x^x - 1)^(e^(e^x)) at 6. Past about sixteen million bits
+ /// the power is left written, evaluated or simplified: it is finite and not zero, and as the decimal it was
+ /// +oo or 0, which made 2^(10^9) - 2^(10^9) NaN and 2^(10^9) > 2^(10^9 - 1)
+ /// False. What is decided of it is decided by structure, by its sign or by its exponent.
+ /// https://github.com/asc-community/AngouriMath/issues/1448
///
[Trait("Area", "Core")]
public sealed class AnExactPowerTooLargeToBuildTest
{
[Theory]
- [InlineData("46655^(10^20)", "+oo")]
- [InlineData("46655^(e^(e^6))", "+oo")]
- [InlineData("(-3)^(10^20 + 1)", "-oo")]
- [InlineData("(-3)^(10^20)", "+oo")]
- [InlineData("(5/2)^(10^7)", "+oo")]
+ [InlineData("46655^(10^20)")]
+ [InlineData("(-3)^(10^20 + 1)")]
+ [InlineData("(-3)^(10^20)")]
+ [InlineData("(5/2)^(10^7)")]
+ [InlineData("2^(10^9)")]
+ [InlineData("2^(-(10^9))")]
+ [InlineData("8^(10^9/3)")]
+ [InlineData("46655^(e^(e^6))")]
+ [InlineData("(1 + i)^(10^9)")]
+ public void IsLeftWritten(string input)
+ {
+ var evaluated = Within(() => input.ToEntity().Evaled, input);
+ Assert.True(evaluated is Powf, $"{input} was evaluated to {evaluated}");
+ }
+
+ [Theory]
// Each of -1, i and -i keeps its exact power, however large.
[InlineData("i^(10^20)", "1")]
[InlineData("(-1)^(10^20 + 1)", "-1")]
[InlineData("(-i)^(10^20 + 3)", "i")]
+ // What holds the power is settled where structure, sign or exponent settle it.
+ [InlineData("0 * 2^(10^9)", "0")]
+ [InlineData("2^(10^9) > 2^(10^9 - 1)", "true")]
+ [InlineData("2^(10^9) < 2^(10^9 - 1)", "false")]
+ [InlineData("(1/2)^(10^9) > (1/2)^(10^9 - 1)", "false")]
+ [InlineData("2^(-(10^9)) > 0", "true")]
+ [InlineData("2^(10^9) <= 0", "false")]
+ [InlineData("(-3)^(10^20 + 1) < 0", "true")]
+ [InlineData("(-3)^(10^20 + 1) >= 5", "false")]
+ [InlineData("2^(10^9) = 2^(10^9)", "true")]
+ [InlineData("2^(10^9) = 2^(10^9 - 1)", "false")]
+ [InlineData("2^(10^9) = 0", "false")]
+ // Otherwise by the logarithms of the magnitudes, where they differ by more than their error.
+ [InlineData("2^(10^9) > 5", "true")]
+ [InlineData("3^(10^9) > 2^(10^9)", "true")]
+ [InlineData("2^(10^9) > 10^(10^8)", "true")]
+ [InlineData("2^(-(10^9)) < 10^(-100)", "true")]
+ [InlineData("(-3)^(10^20 + 1) < -5", "true")]
+ [InlineData("(1 + 2^(-40))^(10^6) > 1", "true")]
public void IsEvaluated(string input, string expected)
=> Assert.Equal(expected.ToEntity().Evaled, Within(() => input.ToEntity().Evaled, input));
+ [Theory]
+ [InlineData("2^(10^9) - 2^(10^9)", "0")]
+ [InlineData("x - x + 2^(10^9) - 2^(10^9)", "0")]
+ [InlineData("2^(10^9) / 2^(10^9 - 1)", "2")]
+ [InlineData("2^(10^9) * 2^(-(10^9))", "1")]
+ [InlineData("2^(-(10^9)) > 0", "true")]
+ [InlineData("2^(10^9)", "2^(10^9)")]
+ public void IsSimplified(string input, string expected)
+ => Assert.Equal(expected.ToEntity().InnerSimplified, Within(() => input.ToEntity().Simplify(), input));
+
+ ///
+ /// Two magnitudes that may be equal are not told apart, and two that differ by less than any rounding are
+ /// not compared through it: as the decimals, the first was True only because both sides were +oo.
+ ///
+ [Theory]
+ [InlineData("2^(10^9) = 4^(5 * 10^8)")]
+ [InlineData("2^(10^9) > 2^(10^9) - 1")]
+ public void IsLeftUndecided(string input)
+ {
+ var evaluated = Within(() => input.ToEntity().Evaled, input);
+ Assert.True(evaluated is not Entity.Boolean, $"{input} was decided as {evaluated}");
+ }
+
+ /// A witness search that meets the power moves on, rather than hanging on it.
+ [Fact]
+ public void AQuantifierOverATowerReturns()
+ {
+ const string input = "forall n in ZZ* : 3^(n + 1) divides 2^(3^n) + 1";
+ Assert.Equal(input.ToEntity(), Within(() => input.ToEntity().Evaled, input));
+ }
+
[Theory]
[InlineData("2^(10^10)")]
[InlineData("46655^(10^20) + 1")]