diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 302c211bd..13ca3f066 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -380,6 +380,22 @@ 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. + +| 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^10)".Simplify()` | does not return | `2 ^ 10000000000` | + ### A remainder of two ratios is exact **Silent: a remainder that came back as the divisor, or as NaN, is the remainder.** `%` on `Real`, `mod` and evaluation diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs index bb7382737..1070164b5 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs @@ -586,6 +586,21 @@ static Complex BinaryIntPow(Complex num, EInteger val) return Integer.Zero; return infinite.IsNegative && !wholeOfInfinity.IsEven ? Real.NegativeInfinity : Real.PositiveInfinity; } + // 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. + if (@base.IsFinite && pow is { } && IsTooLargeToBuild(@base, pow)) + { + var largeContext = MathS.Settings.DecimalPrecisionContext; + if (@base is Real { EDecimal: var realOfLarge }) + { + var magnitude = PowerOfReals(realOfLarge.Abs(), EDecimal.FromEInteger(pow), largeContext); + return Real.CreateAtScale(realOfLarge.IsNegative && !pow.IsEven ? magnitude.Negate() : magnitude, EDecimal.Zero); + } + return ByThePolarForm(@base.RealPart.EDecimal, @base.ImaginaryPart.EDecimal, @base.Abs().EDecimal, + EDecimal.FromEInteger(pow), EDecimal.Zero, largeContext); + } if (@base.IsFinite && pow is { }) { // A real base that is not exact -- a decimal, which is what a numerical @@ -680,7 +695,13 @@ static Complex BinaryIntPow(Complex num, EInteger val) if (baseReal.IsZero && baseImaginary.IsZero) return Integer.Zero; - var rho = @base.Abs().EDecimal; + return ByThePolarForm(baseReal, baseImaginary, @base.Abs().EDecimal, powerReal, powerImaginary, context); + } + + /// base^power by the polar form, for a base that is not 0 with modulus . + private static Complex ByThePolarForm(EDecimal baseReal, EDecimal baseImaginary, EDecimal rho, + EDecimal powerReal, EDecimal powerImaginary, EContext context) + { var theta = baseImaginary.Arctan2(baseReal, context); var newRho = powerReal.MultiplyAndAdd(theta, powerImaginary.Multiply(rho.NaturalLogarithm(context), context), context); var t = PowerOfReals(rho, powerReal, context).Multiply(powerImaginary.Multiply(-theta, context).Exponential(context), context); @@ -690,6 +711,35 @@ static Complex BinaryIntPow(Complex num, EInteger val) return Complex.CreateAtScales(t.Multiply(newRho.Cos(context), context), t.Multiply(newRho.Sin(context), context), modulusScale, modulusScale); } + /// + /// The most bits an exact power is built to: 3^(10^7), at 15.8 million, takes three seconds, + /// and each doubling of the exponent doubles the size and more than doubles the time. + /// + [ConstantField] private static readonly EInteger MaxExactPowerBits = EInteger.FromInt32(1 << 24); + + /// + /// About how many bits an exact to the whole has: for + /// each part of the base, the bits of its numerator and of its denominator past the first, the larger + /// part's, and one more where both parts are not 0, times the power. Each of -1, i and -i has none, + /// and a power of one is as small as it is. A base that is not exact is 0, being a decimal. + /// + internal static bool IsTooLargeToBuild(Complex @base, EInteger power) + => ExactPowerBits(@base, power).CompareTo(MaxExactPowerBits) > 0; + + private static EInteger ExactPowerBits(Complex @base, EInteger power) + { + if (@base.RealPart is not Rational { ERational: var re } || @base.ImaginaryPart is not Rational { ERational: var im }) + return EInteger.Zero; + static EInteger Bits(ERational part) + => part.IsZero ? EInteger.Zero + : part.Numerator.Abs().GetUnsignedBitLengthAsEInteger().Subtract(1) + .Add(part.Denominator.GetUnsignedBitLengthAsEInteger().Subtract(1)); + var bits = Bits(re).CompareTo(Bits(im)) >= 0 ? Bits(re) : Bits(im); + if (!re.IsZero && !im.IsZero) + bits = bits.Add(1); + return bits.Multiply(power.Abs()); + } + /// e.g. Log(2, 32) = 5 /// Log's base, log(base, x) is a number y such that base^y = x /// The number of which we want to get its base power diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs index b5d0ccfa7..487682612 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs @@ -55,6 +55,19 @@ private static bool IsAZeroOnlyRoundingMade(Entity node, Entity value) => value is Integer { IsZero: true } && (node is Mulf or Divf or Powf && CannotBeZero(node) || node is Sumf or Minusf && SignOfANonzero(node) is not null); + /// + /// Whether is an infinity that only the decimals made: an exact power too + /// large to build is evaluated as a decimal, which is +oo past the context's range, and an + /// infinity counts as exact, so that 2^(10^10), and every expression holding it, was + /// simplified to +oo. Asked only of a value that is not finite. + /// + private static bool IsAnInfinityOnlyRoundingMade(Entity node, Entity value) + => value is Complex { IsFinite: false } && !value.IsNaN + && node.Nodes.Any(static part => part is Powf(var @base, var exponent) + && @base.Evaled is Complex { IsExact: true, IsFinite: true } exactBase + && exponent.Evaled is Integer { EInteger: var power } + && Number.IsTooLargeToBuild(exactBase, power)); + /// /// Whether is a number or a constant that is not zero, or a product, /// a quotient or a power of those with a finite divisor or exponent. @@ -128,7 +141,8 @@ private Entity ExpandOnTwoArguments( bool propagateSet = true, bool settlesNaN = false) { - if (isExact && this.Evaled is (Number { IsExact: true } or Boolean) and var n && !IsAZeroOnlyRoundingMade(this, n)) + if (isExact && this.Evaled is (Number { IsExact: true } or Boolean) and var n && !IsAZeroOnlyRoundingMade(this, n) + && !IsAnInfinityOnlyRoundingMade(this, n)) return n; left = left.InnerSimplified(isExact); right = right.InnerSimplified(isExact); @@ -151,7 +165,8 @@ Entity ops(Entity a, Entity b) { if (operation(a, b) is { } res) return res; - if (isExact && defaultCtor(this, a, b) is var built && built.Evaled is Number { IsExact: true } n && !IsAZeroOnlyRoundingMade(built, n)) + if (isExact && defaultCtor(this, a, b) is var built && built.Evaled is Number { IsExact: true } n && !IsAZeroOnlyRoundingMade(built, n) + && !IsAnInfinityOnlyRoundingMade(built, n)) return n; return defaultCtor(this, a, b); } diff --git a/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs b/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs new file mode 100644 index 000000000..df1204b96 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/AnExactPowerTooLargeToBuildTest.cs @@ -0,0 +1,63 @@ +// +// 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 System; +using System.Threading.Tasks; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +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. + /// + [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")] + // 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")] + public void IsEvaluated(string input, string expected) + => Assert.Equal(expected.ToEntity().Evaled, Within(() => input.ToEntity().Evaled, input)); + + [Theory] + [InlineData("2^(10^10)")] + [InlineData("46655^(10^20) + 1")] + [InlineData("(1/3)^(10^20)")] + public void AnExactSimplificationLeavesItAsWritten(string input) + { + var simplified = Within(() => input.ToEntity().InnerSimplified, input); + Assert.True(simplified is not Number, $"{input} was simplified to {simplified}"); + Assert.True(Within(() => input.ToEntity().Simplify(), input) is not Number, $"{input} was simplified to a number"); + } + + [Fact] + public void APowerBelowTheBoundIsStillExact() + => Assert.Equal(Integer.Create(PeterO.Numbers.EInteger.One.ShiftLeft(1000000)), Within(() => "2^(10^6)".ToEntity().Evaled, "2^(10^6)")); + + private static Entity Within(Func evaluation, string input) + { + var task = Task.Factory.StartNew(evaluation, TaskCreationOptions.LongRunning); + Assert.True(task.Wait(TimeSpan.FromSeconds(30)), $"{input} did not finish in thirty seconds"); + return task.Result; + } + } +}