From 140829bf8959fd7769fe128d0823be4169493cc6 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 15:22:41 +0000 Subject: [PATCH] Take an interval's size without squaring, so a size past the doubles does not agree IntervalEvaluation.Agree took the sizes of two values as sqrt(re^2 + im^2) in doubles. Past about 1.3e154 the square is infinite, so was the tolerance relative to it, and any two values that large agreed. A coefficient of c^294, read in lowest terms at the pinned symbols (c = 3.71), was taken for zero, and (b x^(-4) + b x^(-6) + b x^(-8))/sqrt(1/c^2 + x^2) was answered with two of its three terms missing, wrong at every point. Every check behind DerivativeHoldsAtSampledPoints and IsZeroAsAValue went through the same comparison, and any answer that large at a sampled point passed. 2.5.0 declined the integrand, so the wrong answer never shipped. The size is now the hypotenuse scaled by its larger part, widened outward; a scale that is not finite is left to the decimals. Part of #1019. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 +++++++ .../Numerics/IntervalEvaluation.cs | 31 +++++++++++-- ...TheDoublesIsNotTakenForZeroIntegralTest.cs | 45 +++++++++++++++++++ .../UnitTests/Core/IntervalEvaluationTest.cs | 16 +++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ACoefficientPastTheDoublesIsNotTakenForZeroIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 7d8b0e0ac..07216a887 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -4292,6 +4292,21 @@ Both columns measured on a build, `v2.5.0` against this change. | `"x^2/x^2"` | `1 provided not x ^ 2 = 0` | `1 provided not x = 0` | | `"(x^2 - 1)/(x^2 + 2x + 1) + 1/(x + 1)"` | `x / (x + 1) provided not x ^ 2 + 2 * x + 1 = 0` | `x / (x + 1)` | +### A coefficient past the doubles is not taken for zero + +**Answers where there were none.** `(b x^(-4) + b x^(-6) + b x^(-8))/sqrt(1/c^2 + x^2)` was declined. Since the interval +evaluation of [#1019](https://github.com/asc-community/AngouriMath/issues/1019) it was answered on the unreleased +master with two of its three terms missing, wrong at every point. Its coefficients run to `c^294`, past `1.3e154` at +the symbols a coefficient is pinned to when it is read in lowest terms. The check that two values agree took their +sizes squared, which is infinite there, and so was the tolerance relative to them. Every two values that large agreed, +and each of those coefficients was taken for zero. The sizes are taken without squaring now, and a size past the +doubles is left to the decimals. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(b*x^(-4) + b*x^(-6) + b*x^(-8))*(1/c^2 + x^2)^(-1/2)".ToEntity().Integrate("x")` | `integral(...)` | 1,445 characters | +| `"(b*x^12 + b*x^10 + b*x^8)*(1/c^2 + x^2)^(-1/2)/x^16".ToEntity().Integrate("x")` | `integral(...)` | 1,254 characters | + ### A partial-fraction coefficient with symbols in it is in lowest terms, its rational content included **Improvement, not silent.** The decomposition over written factors with symbols among their diff --git a/Sources/AngouriMath/Numerics/IntervalEvaluation.cs b/Sources/AngouriMath/Numerics/IntervalEvaluation.cs index 50e74cf58..a406deb6a 100644 --- a/Sources/AngouriMath/Numerics/IntervalEvaluation.cs +++ b/Sources/AngouriMath/Numerics/IntervalEvaluation.cs @@ -168,7 +168,21 @@ internal ComplexInterval(Interval re, Interval im) internal bool IsFinite => Re.IsFinite && Im.IsFinite; internal bool IsReal => Im.Low == 0 && Im.High == 0; - internal double Magnitude => Math.Sqrt(Re.Magnitude * Re.Magnitude + Im.Magnitude * Im.Magnitude); + internal double Magnitude => Hypotenuse(Re.Magnitude, Im.Magnitude); + + /// + /// sqrt(a^2 + b^2) for and at least zero, + /// scaled by the larger so that it does not overflow: squared, anything past about + /// 1.3e154 is infinite. + /// + internal static double Hypotenuse(double a, double b) + { + var (large, small) = a >= b ? (a, b) : (b, a); + if (large == 0 || !Interval.IsFiniteDouble(large)) + return large; + var ratio = small / large; + return large * Math.Sqrt(1 + ratio * ratio); + } public static ComplexInterval operator +(ComplexInterval a, ComplexInterval b) => new(a.Re + b.Re, a.Im + b.Im); public static ComplexInterval operator -(ComplexInterval a, ComplexInterval b) => new(a.Re - b.Re, a.Im - b.Im); @@ -308,15 +322,26 @@ internal static class IntervalEvaluation /// of one, where both are smaller): true where the whole of their difference is within /// it, false where the whole of it is beyond, null where the intervals cannot tell. /// + /// + /// The sizes are taken without squaring. Squared, a size past about 1.3e154 was + /// infinite, and so was the tolerance relative to it, and any two values that large agreed: + /// a coefficient of c^294 read at c = 3.71 was taken for zero, which dropped + /// two of the three terms of (b x^(-4) + b x^(-6) + b x^(-8))/sqrt(1/c^2 + x^2)'s + /// antiderivative, and an answer that large at a sampled point was passed whatever it was. + /// A size past the doubles is left to the decimals. + /// https://github.com/asc-community/AngouriMath/issues/1019 + /// internal static bool? Agree(ComplexInterval left, ComplexInterval right, double relativeTolerance) { var difference = left - right; var scale = Math.Max(1.0, Math.Max(left.Magnitude, right.Magnitude)); + if (!Interval.IsFiniteDouble(scale)) + return null; var allowed = relativeTolerance * scale; - var largest = Math.Sqrt(difference.Re.Magnitude * difference.Re.Magnitude + difference.Im.Magnitude * difference.Im.Magnitude); + var largest = Interval.Wider(ComplexInterval.Hypotenuse(difference.Re.Magnitude, difference.Im.Magnitude), 1); if (largest <= allowed) return true; - var smallest = Math.Sqrt(difference.Re.Mignitude * difference.Re.Mignitude + difference.Im.Mignitude * difference.Im.Mignitude); + var smallest = Interval.Wider(ComplexInterval.Hypotenuse(difference.Re.Mignitude, difference.Im.Mignitude), -1); if (smallest > allowed) return false; return null; diff --git a/Sources/Tests/UnitTests/Calculus/ACoefficientPastTheDoublesIsNotTakenForZeroIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ACoefficientPastTheDoublesIsNotTakenForZeroIntegralTest.cs new file mode 100644 index 000000000..db61ffb5a --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ACoefficientPastTheDoublesIsNotTakenForZeroIntegralTest.cs @@ -0,0 +1,45 @@ +// +// 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 AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// The coefficients of these antiderivatives run to c^294, past 1.3e154 at the + /// symbols the lowest-terms reading pins, where the interval check took a size squared to an + /// infinity and agreed that each was zero: two of the three terms were dropped, and the answer + /// differed from the integral at every point. + /// #1019 + /// + [Trait("Area", "Calculus")] + public sealed class ACoefficientPastTheDoublesIsNotTakenForZeroIntegralTest + { + [Theory] + [InlineData("(b*x^(-4) + b*x^(-6) + b*x^(-8))*(1/c^2 + x^2)^(-1/2)")] + [InlineData("(b*x^12 + b*x^10 + b*x^8)*(1/c^2 + x^2)^(-1/2)/x^16")] + public void KeepsEveryTerm(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + if (integral.Stringize().Contains("integral(")) + return; + Entity Pinned(Entity e) => e.Substitute("b", 0.7).Substitute("c", 1.3); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -2.1, -1.3, -0.6, 0.5, 1.2, 2.3 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +} diff --git a/Sources/Tests/UnitTests/Core/IntervalEvaluationTest.cs b/Sources/Tests/UnitTests/Core/IntervalEvaluationTest.cs index 53c814bfe..e7c27a0be 100644 --- a/Sources/Tests/UnitTests/Core/IntervalEvaluationTest.cs +++ b/Sources/Tests/UnitTests/Core/IntervalEvaluationTest.cs @@ -160,6 +160,22 @@ public void MoreDigitsSettleWhatDoublesCannot() Assert.False(PreciseEvaluation.Agree(PreciseEvaluation.Of(nearby, 40)!.Value, PreciseEvaluation.Of(sine, 40)!.Value, 1e-9)); } + /// + /// A size past about 1.3e154 squared to an infinity, and so did the tolerance relative + /// to it: two values that large agreed whatever they were, and a coefficient that large was + /// taken for zero. Neither is decided now, and an exact difference between them is seen. + /// + [Theory] + [InlineData("2 * 10^167", "0")] + [InlineData("2 * 10^167", "3 * 10^167")] + [InlineData("2 * 10^167 * i", "0")] + public void ASizePastTheDoublesDoesNotAgree(string left, string right) + => Assert.NotEqual(true, IntervalEvaluation.Agree(IntervalEvaluation.Of(left.ToEntity())!.Value, IntervalEvaluation.Of(right.ToEntity())!.Value, 1e-30)); + + [Fact] + public void TheSizeOfALargeComplexValueIsFinite() + => Assert.InRange(IntervalEvaluation.Of("3 * 10^160 + 4 * 10^160 * i".ToEntity())!.Value.Magnitude, 4.99e160, 5.01e160); + [Theory] [InlineData("sqrt(2)")] [InlineData("e^2 - pi")]