From e1e900ae4a0babaa13d9ed18cf5f138e33625eb3 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 8 Sep 2026 08:48:47 +0000 Subject: [PATCH 1/2] An integral across a step is split at the jumps, never taken through an antiderivative integral(x - floor(x), x, 0, 3) answered 0: the rules find an antiderivative of an integrand with floor(x) in it by taking the floor for a constant, right between two of its jumps and wrong across one, and the definite integral evaluated that at the bounds. (x - floor(x))^2 from 0 to 4 was 0 where it is 4/3, and from 0 to a symbolic n was (n - floor(n))^3 / 3, wrong for every whole n but 0. An integrand with floor(x) or ceil(x) of the variable no longer goes through an antiderivative between two bounds. Between whole bounds it is split into unit intervals -- on each the floor is n, the ceiling n + 1 and x is n + t -- and the integral is a sum over n of an integral over t with no step in it, which the summation's closed forms then answer; a numeric bound that is not whole contributes the piece up to the nearest whole number, on which the step is one known constant; a symbolic bound is left as written. Offered only where every piece resolves. The fractional part is substituted as a unit, since written out it arrives as n + t - n, which is a conditional zero and not nothing (#1174); and the series reader accepts the condition a division by a factorial of the index carries, which holds for every whole index in range. Question I.2 of #1212: integral((x - floor(x)) / floor(x)!, x, 1, +oo) is (e - 1) / 2. BREAKING-CHANGES.md has the rows, measured on both builds. Part of #1212. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 31 +++++ Sources/.editorconfig | 6 + .../Algebra/Polynomials/ExponentialSeries.cs | 45 ++++++- .../Integration/Integration.Definition.cs | 14 +- .../Integration/UnitIntervalIntegration.cs | 121 ++++++++++++++++++ .../Calculus/UnitIntervalIntegrationTest.cs | 81 ++++++++++++ 6 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs create mode 100644 Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9059b39d2..93d4efaa4 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -88,6 +88,9 @@ read first. | | `"sum(x^k / k!, k, 0, +oo)".ToEntity().Simplify()`, and every summation to `+oo` of a polynomial in the index times a power with the index in the exponent over a factorial of the index | `sum(x ^ k / k!, k, 0, +oo)` — left as written | `e ^ x`; `sum(3^(k+2) * (k^2 + k + 1) / (k + 3)!, k, 0, +oo)` is `4/3 * e ^ 3 - 41/6` | | | `"sum(N! / (k! * (N - k)!) * x^k, k, 0, N)".ToEntity().Simplify()`, and every binomial sum with a power or a cosine or sine of the index as its weight | `sum(N! / (k! * (N - k)!) * x ^ k, k, 0, N)` — left as written | `piecewise((1 + x) ^ N provided N >= 0, 0)`; with `cos(k * pi / 3)`, `piecewise(2 ^ N * cos(pi / 6) ^ N * cos(N * pi / 6) provided N >= 0, 0)` | | | `"ln(4/3) + ln(16/9) / 2".ToEntity().Simplify()`, and every sum of logarithms of rational literals one of which is a perfect power of another | `ln(4/3) + ln(16/9) / 2` — left as written | `2 * ln(4/3)`; the integral it came from, `integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)`, answers `ln(16/9)` | +| **Silent** | `"integral(x - floor(x), x, 0, 3)".ToEntity().Simplify()`, and every definite integral over numeric bounds whose integrand has `floor(x)` or `ceil(x)` in it | `0` — an antiderivative that took the floor for a constant across its jumps | `3/2` — split at the jumps; `(x - floor(x))^2` from 0 to 4 was `0` and is `4/3` | +| **Silent** | `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()`, and every such integral with a symbolic bound | `(n - floor(n)) ^ 3 / 3` — wrong for every whole `n` but 0 | left as written | +| | `"integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify()`, and `integral(floor(x), x, 0, 5)` | left as written | `(e - 1) / 2`, `10` | ### A cancelled quotient says its operand is defined, not only non-zero @@ -1092,6 +1095,34 @@ columns measured on a build, `60545afa` against this change. | `"ln(16/9)"`, `ln(64)`, `ln(8) - 3 * ln(2)`, `log(3, 81) / 4` | `ln(16/9)`, `ln(64)`, `0`, `1` | the same | | `RewriteRules.Power.Rules.Count`, and `RewriteRules.All` by growth | `31`; 124 / 49 / 31 / 123 | `32`; 124 / 49 / 32 / 123 | +### An integral across a step is split at the jumps, never taken through an antiderivative + +`integral(x - floor(x), x, 0, 3)` answered `0`. The rules find an antiderivative of an integrand with +`floor(x)` in it by taking the floor for a constant — which is right between two of its jumps and +wrong across one — and the definite integral then evaluated that antiderivative at the bounds. +`(x - floor(x))^2` from 0 to 4 was `0` where it is `4/3`, and from 0 to a symbolic `n` was +`(n - floor(n))^3 / 3`, which is wrong for every whole `n` but 0. + +An integrand with `floor(x)` or `ceil(x)` of the variable no longer goes through an antiderivative +between two bounds. Between whole bounds it is split into unit intervals, on each of which the +floor is `n`, the ceiling `n + 1` and `x` is `n + t`, and the integral is a sum over `n` of an +integral over `t` with no step in it; a numeric bound that is not whole contributes the piece up to +the nearest whole number, on which the step is one known constant; a symbolic bound is left as +written, since an integral's bound is not whole by convention as a summation's index is. Offered +only where every piece resolves. Question I.2 of +[#1212](https://github.com/asc-community/AngouriMath/issues/1212). Both columns measured on a +build, `4fd6dda5` against this change. + +| | Was | Is | +|---|---|---| +| `"integral(x - floor(x), x, 0, 3)".ToEntity().Simplify()` | `0` | `3/2` | +| `"integral((x - floor(x))^2, x, 0, 4)".ToEntity().Simplify()` | `0` | `4/3` | +| `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()` | `(n - floor(n)) ^ 3 / 3` | left as written; with `n = 6` substituted, `2` | +| `"integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify()` | left as written | `(e - 1) / 2` | +| `"integral(floor(x), x, 0, 5)"`, `integral(floor(x) * x, x, 0, 3)`, `integral(floor(x) / (floor(x) + 1)!, x, 0, +oo)` | left as written | `10`, `13/2`, `1` | +| `"integral(floor(x), x, 1/2, 2)"`, `integral(x - floor(x), x, 0, 5/2)`, `integral(ceil(x) - x, x, 1/2, 2)` | left as written | `1`, `9/8`, `5/8` | +| `"integral(2^(-floor(x)), x, 0, +oo)"`, `integral(floor(x^2), x, 0, 2)` | left as written | the same — a geometric series has no closed form here yet; a floor of something other than the variable is not a step this reads | + ## 2.4.0 — since 2.3.0 Released 2026-08-28. These entries sat under “Unreleased” while 2.4.0 was tagged and diff --git a/Sources/.editorconfig b/Sources/.editorconfig index b5fbc8010..d17ed480c 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -204,6 +204,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/BinomialSumTest.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 +[AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.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/Calculus/UnitIntervalIntegrationTest.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 + [AngouriMath/Functions/Algebra/Polynomials/PolynomialSummation.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/Core/ClosedFormSummationTest.cs] diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs index e2b89d59f..287da1421 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs @@ -8,6 +8,7 @@ using PeterO.Numbers; using static AngouriMath.Entity; using static AngouriMath.Entity.Number; +using static AngouriMath.Entity.Set; namespace AngouriMath.Functions { @@ -65,7 +66,8 @@ internal static class ExponentialSeries Entity? exponent = null; Entity polynomial = Integer.One; Entity constant = Integer.One; - foreach (var factor in Mulf.LinearChildren(expression.InnerSimplified)) + var summand = WithoutFactorialConditions(expression.InnerSimplified, index, start); + foreach (var factor in Mulf.LinearChildren(summand)) { if (!factor.ContainsNode(index)) { @@ -165,6 +167,47 @@ internal static class ExponentialSeries return result.InnerSimplified; } + /// + /// The summand without a condition that says a factorial of the index is not zero: a + /// division by (k + a)! carries provided not (k + a)! = 0 from the moment + /// it is simplified, and over a range that starts at or above -a the factorial is + /// that of a whole non-negative number, which is never zero. Any other condition is left + /// where it is and the summand then declines below. + /// + private static Entity WithoutFactorialConditions(Entity summand, Variable index, int start) + { + while (summand is Providedf(var body, var condition) && HoldsOverTheRange(condition, index, start)) + summand = body; + return summand; + } + + /// + /// Whether a condition holds for every whole index >= start: a factorial of + /// index + a not being zero, index + a being real, or being at least zero, + /// each where start + a >= 0 -- the clauses a division by a factorial carries. + /// + private static bool HoldsOverTheRange(Entity condition, Variable index, int start) + => condition switch + { + Notf(Equalsf(Factorialf(var argument), var zero)) + when zero.Evaled is Integer { IsZero: true } && AtLeastZero(argument, index, start) + => true, + Inf(var argument, var set) + when set == MathS.Sets.R && AtLeastZero(argument, index, start) + => true, + GreaterOrEqualf(var argument, var zero) + when zero.Evaled is Integer { IsZero: true } && AtLeastZero(argument, index, start) + => true, + Andf(var left, var right) + => HoldsOverTheRange(left, index, start) && HoldsOverTheRange(right, index, start), + Orf(var left, var right) + => HoldsOverTheRange(left, index, start) || HoldsOverTheRange(right, index, start), + _ => false, + }; + + private static bool AtLeastZero(Entity argument, Variable index, int start) + => TryReadShift(argument, index, out var a) && start + a >= 0; + /// /// as index + shift for a whole shift: the /// index itself, or the index plus or minus a whole number, in either order. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 52bfd6564..571c5bb4b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -47,9 +47,17 @@ public Entity Integrate(Variable x) => /// An integrated expression. It might remain the same or be transformed into nodes with no integrals. /// public Entity Integrate(Variable x, Entity from, Entity to) => - Transformation.Integration(x).Apply(this).Output is { } antiderivative - ? antiderivative.Substitute(x, to) - antiderivative.Substitute(x, from) - : new Integralf(this, x, (from, to)); + // An integrand with floor(x) or ceil(x) in it never goes through an antiderivative + // between two bounds: the rules integrate it as if the step were a constant, which + // is right between two of its jumps and wrong across one -- (x - floor(x))^2 from + // 0 to 4 came back as 0 that way, where it is 4/3, and from 0 to n as + // (n - floor(n))^3 / 3. It is split at the jumps instead, and where that cannot be + // done it is left as written. See UnitIntervalIntegration. + Functions.Algebra.UnitIntervalIntegration.HasAStep(this, x) + ? Functions.Algebra.UnitIntervalIntegration.Split(this, x, from, to) ?? new Integralf(this, x, (from, to)) + : Transformation.Integration(x).Apply(this).Output is { } antiderivative + ? antiderivative.Substitute(x, to) - antiderivative.Substitute(x, from) + : new Integralf(this, x, (from, to)); /// /// Integrates numerically over between two bounds, without diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs b/Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs new file mode 100644 index 000000000..d81c891a5 --- /dev/null +++ b/Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs @@ -0,0 +1,121 @@ +// +// 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.Linq; +using PeterO.Numbers; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Functions.Algebra +{ + /// + /// A definite integral whose integrand mentions floor(x) or ceil(x), split at + /// the jumps: on [n, n + 1) the floor is n and the ceiling n + 1 + /// exactly, x is n + t with t over [0, 1), and the integral + /// over whole bounds is a sum over n of an integral over t that mentions no + /// step at all. A numeric bound that is not whole contributes the piece from it to the + /// nearest whole number, on which the step is one known constant. + /// + /// + /// + /// Exact and unconditional: the step is constant on each piece, a single point has no + /// measure, and the pieces are exactly the ones the bounds cover. The fractional part + /// x - floor(x) becomes t by the same substitution, taken first as a unit + /// because written out it arrives as n + t - n, and a term subtracted from itself + /// simplifies to a conditional zero rather than to nothing + /// (#1174). The + /// upper bound may be +oo, in which case the sum runs to +oo and is answered + /// where and its neighbours answer it. + /// + /// + /// Offered only where every piece then resolves -- the unit-interval integral to something + /// that is not an integral, the sum to something that is not a sum -- since an integral + /// rewritten as an unevaluated sum of unevaluated integrals is not an answer. A symbolic + /// bound is declined: an integral's bound is not a whole number by convention as a + /// summation's index is, and the pieces depend on where the jumps fall. Question I.2 of + /// #1212: + /// integral((x - floor(x)) / floor(x)!, x, 1, +oo) is (e - 1) / 2. + /// + /// + internal static class UnitIntervalIntegration + { + /// Whether the integrand mentions floor(x) or ceil(x) of the variable itself. + internal static bool HasAStep(Entity expr, Variable x) + => expr.Nodes.Any(node => node is Floorf(var f) && f == x || node is Ceilf(var c) && c == x); + + internal static Entity? Split(Entity expr, Variable x, Entity from, Entity to) + { + if (!HasAStep(expr, x)) + return null; + if (from.Evaled is not Real lower || !lower.IsFinite) + return null; + var unbounded = to.Evaled is Real { IsFinite: false, IsNaN: false, IsNegative: false }; + if (!unbounded && (to.Evaled is not Real || !((Real)to.Evaled).IsFinite)) + return null; + var upper = unbounded ? null : (Real)to.Evaled; + if (upper is not null && upper < lower) + return null; + + // The step is one constant from the lower bound up to the next whole number, and + // one constant from the last whole number up to the upper bound. + var firstWhole = Ceiling(lower); + Entity total = Integer.Zero; + if (upper is not null && Floor(upper) == Floor(lower) && !IsWhole(lower)) + // Both bounds inside one unit interval: a single piece with the step known. + return OnAPiece(expr, x, from, to, Floor(lower)) is { } single ? single.InnerSimplified : null; + if (!IsWhole(lower)) + { + if (OnAPiece(expr, x, from, Integer.Create(firstWhole), Floor(lower)) is not { } head) + return null; + total += head; + } + if (upper is not null && !IsWhole(upper)) + { + var lastWhole = Floor(upper); + if (OnAPiece(expr, x, Integer.Create(lastWhole), to, lastWhole) is not { } tail) + return null; + total += tail; + } + + var taken = expr.Vars.Append(x).ToList(); + var n = Variable.CreateTemp(taken); + taken.Add(n); + var t = Variable.CreateTemp(taken); + var piece = expr + .Substitute(x - new Floorf(x), t) + .Substitute(new Floorf(x), n) + .Substitute(new Ceilf(x), n + Integer.One) + .Substitute(x, n + t); + var overUnitInterval = new Integralf(piece, t, (Integer.Zero, Integer.One)).InnerSimplified; + if (overUnitInterval is Integralf) + return null; + Entity lastInterval = unbounded ? to : Integer.Create(Floor(upper!).Subtract(EInteger.One)); + var sum = new Summationf(overUnitInterval, n, Integer.Create(firstWhole), lastInterval).InnerSimplified; + if (sum is Summationf) + return null; + return (total + sum).InnerSimplified; + } + + /// + /// The integral from to on a piece where + /// the floor is throughout (and the ceiling one more, + /// the ends aside), or where that integral is not found. + /// + private static Entity? OnAPiece(Entity expr, Variable x, Entity from, Entity to, EInteger floorValue) + { + var known = expr + .Substitute(new Floorf(x), Integer.Create(floorValue)) + .Substitute(new Ceilf(x), Integer.Create(floorValue.Add(EInteger.One))); + var result = known.Integrate(x, from, to); + return result is Integralf ? null : result; + } + + private static bool IsWhole(Real value) => value.EDecimal.IsInteger(); + private static EInteger Floor(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Floor)).ToEInteger(); + private static EInteger Ceiling(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Ceiling)).ToEInteger(); + } +} diff --git a/Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs new file mode 100644 index 000000000..6fe62c2ff --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs @@ -0,0 +1,81 @@ +// +// 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; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// https://github.com/asc-community/AngouriMath/issues/1212, question I.2: an integral over + /// whole bounds of an integrand with floor(x) in it is a sum of integrals over unit + /// intervals, on each of which the floor is a number. + /// + public sealed class UnitIntervalIntegrationTest + { + [Fact] + public void TheOrientationWeekIntegralIsHalfOfEMinusOne() + { + var value = "integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify(); + Assert.IsNotType(value); + Assert.Equal("(e - 1) / 2".ToEntity().Simplify(), value); + } + + [Theory] + [InlineData("integral(floor(x), x, 0, 5)", "10")] + [InlineData("integral(x - floor(x), x, 0, 3)", "3/2")] + [InlineData("integral((x - floor(x))^2, x, 0, 4)", "4/3")] + [InlineData("integral(floor(x) * x, x, 0, 3)", "13/2")] + [InlineData("integral(floor(x) / (floor(x) + 1)!, x, 0, +oo)", "1")] + public void AnIntegralOverWholeBoundsIsSummedOverUnitIntervals(string integral, string expected) + => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); + + // The split is exact but the sum it leaves has no closed form here yet -- a geometric + // series -- so the integral stays as written rather than becoming an unevaluated sum. + [Fact] + public void ASumWithoutAClosedFormLeavesTheIntegralAsWritten() + => Assert.IsType("integral(2^(-floor(x)), x, 0, +oo)".ToEntity().Simplify()); + + // A bound that is not whole contributes the piece up to the next whole number, on + // which the step is one known constant. + [Theory] + [InlineData("integral(floor(x), x, 1/2, 2)", "1")] + [InlineData("integral(x - floor(x), x, 0, 5/2)", "9/8")] + [InlineData("integral(floor(x), x, 1/4, 3/4)", "0")] + [InlineData("integral(x - floor(x), x, 3/2, 7/4)", "5/32")] + [InlineData("integral(ceil(x), x, 0, 3)", "6")] + [InlineData("integral(ceil(x) - x, x, 1/2, 2)", "5/8")] + public void ABoundThatIsNotWholeContributesItsOwnPiece(string integral, string expected) + => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); + + // A symbolic bound is declined rather than integrated as if the step were constant, + // which used to answer (n - floor(n))^3 / 3 for this one; with the bound a number the + // same integral splits. + [Fact] + public void ASymbolicBoundIsLeftAsWritten() + { + var value = "integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify(); + Assert.IsType(value); + Assert.Equal("2".ToEntity(), value.Substitute("n", 6).Simplify()); + } + + // Not split and not integrated either: a floor of something other than the variable + // is not a step this reads, and an integrand the unit interval cannot integrate stays. + [Theory] + [InlineData("integral(floor(x^2), x, 0, 2)")] + [InlineData("integral(floor(x) * e^(e^x) , x, 0, 2)")] + public void WhatCannotBeSplitIsLeftAsWritten(string integral) + => Assert.IsType(integral.ToEntity().Simplify()); + + [Fact] + public void AnIntegralWithoutAFloorIsUnaffected() + => Assert.Equal("ln(4/3) + ln(16/9) / 2".ToEntity(), "integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)".ToEntity().Simplify()); + } +} From 8d312341fd575701fe5afb50d18629bcfa6944bb Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 8 Sep 2026 12:07:46 +0000 Subject: [PATCH 2/2] An integral across a jump is split at the jumps, never taken through an antiderivative F(b) - F(a) is the integral only where F is continuous on [a, b], and the definite integral evaluated an antiderivative at its bounds whatever the integrand did in between. Three shapes jump. A piecewise whose conditions mention the variable: the generic argument expansion built the antiderivative case by case and handed it back with the integration variable still inside it -- the tent map over [0, 1] answered 1, and piecewise(x provided x < 1, x^2) over [0, 2] answered piecewise(2 provided x < 1, 8/3). A provided on the variable, distributed the same way. And floor(x) or ceil(x), taken for constants across their jumps: x - floor(x) from 0 to 3 answered 0, (x - floor(x))^2 from 0 to 4 answered 0 where it is 4/3, and from 0 to a symbolic n answered (n - floor(n))^3 / 3. An integrand that can jump never goes through an antiderivative between two bounds now (BreakpointIntegration, and a guard at the integral node so that a declined split stays an integral rather than being handed to the expansion). Finitely many breakpoints -- a piecewise or a provided whose conditions compare the variable with numbers -- cut the range, and on each piece the case that holds at the midpoint is integrated through the antiderivative and added; a provided that fails on a piece leaves the integral as written. Infinitely many, evenly spaced -- a floor or a ceiling -- are the same split as a sum over unit intervals, which the summation's closed forms answer, with the pieces up to the nearest whole number for a numeric bound that is not whole. A condition against a symbol or a symbolic bound is left as written. Offered only where every piece resolves. The fractional part is substituted as a unit, since written out it arrives as n + t - n, which is a conditional zero and not nothing (#1174); and the series reader accepts the condition a division by a factorial of the index carries, which holds for every whole index in range. Question I.2 of #1212 -- integral((x - floor(x)) / floor(x)!, x, 1, +oo) is (e - 1) / 2 -- generalised on the review of #1215. BREAKING-CHANGES.md has the rows, measured on both builds. Part of #1212. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 52 +-- Sources/.editorconfig | 4 +- .../Integration/BreakpointIntegration.cs | 295 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 17 +- .../Integration/UnitIntervalIntegration.cs | 121 ------- .../Evaluation.Continuous.Calculus.Classes.cs | 18 +- .../Calculus/BreakpointIntegrationTest.cs | 128 ++++++++ .../Calculus/UnitIntervalIntegrationTest.cs | 81 ----- 8 files changed, 485 insertions(+), 231 deletions(-) create mode 100644 Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs delete mode 100644 Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs create mode 100644 Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs delete mode 100644 Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 93d4efaa4..da6453c4b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -88,8 +88,9 @@ read first. | | `"sum(x^k / k!, k, 0, +oo)".ToEntity().Simplify()`, and every summation to `+oo` of a polynomial in the index times a power with the index in the exponent over a factorial of the index | `sum(x ^ k / k!, k, 0, +oo)` — left as written | `e ^ x`; `sum(3^(k+2) * (k^2 + k + 1) / (k + 3)!, k, 0, +oo)` is `4/3 * e ^ 3 - 41/6` | | | `"sum(N! / (k! * (N - k)!) * x^k, k, 0, N)".ToEntity().Simplify()`, and every binomial sum with a power or a cosine or sine of the index as its weight | `sum(N! / (k! * (N - k)!) * x ^ k, k, 0, N)` — left as written | `piecewise((1 + x) ^ N provided N >= 0, 0)`; with `cos(k * pi / 3)`, `piecewise(2 ^ N * cos(pi / 6) ^ N * cos(N * pi / 6) provided N >= 0, 0)` | | | `"ln(4/3) + ln(16/9) / 2".ToEntity().Simplify()`, and every sum of logarithms of rational literals one of which is a perfect power of another | `ln(4/3) + ln(16/9) / 2` — left as written | `2 * ln(4/3)`; the integral it came from, `integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)`, answers `ln(16/9)` | +| **Silent** | `"integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)".ToEntity().Simplify()`, and every definite integral of a piecewise whose conditions mention the variable | `1` — the first case's antiderivative at both ends | `1/2` — split at the case boundaries; `piecewise(x provided x < 1, x^2)` from 0 to 2 was `piecewise(2 provided x < 1, 8/3)`, with the integration variable still in it, and is `17/6` | | **Silent** | `"integral(x - floor(x), x, 0, 3)".ToEntity().Simplify()`, and every definite integral over numeric bounds whose integrand has `floor(x)` or `ceil(x)` in it | `0` — an antiderivative that took the floor for a constant across its jumps | `3/2` — split at the jumps; `(x - floor(x))^2` from 0 to 4 was `0` and is `4/3` | -| **Silent** | `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()`, and every such integral with a symbolic bound | `(n - floor(n)) ^ 3 / 3` — wrong for every whole `n` but 0 | left as written | +| **Silent** | `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()`, and every such integral with a symbolic bound or a condition against a symbol | `(n - floor(n)) ^ 3 / 3` — wrong for every whole `n` but 0; `piecewise(x provided x < a, 0)` from 0 to 2 was `piecewise(2 provided x < a, 0)` | left as written | | | `"integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify()`, and `integral(floor(x), x, 0, 5)` | left as written | `(e - 1) / 2`, `10` | ### A cancelled quotient says its operand is defined, not only non-zero @@ -1095,26 +1096,41 @@ columns measured on a build, `60545afa` against this change. | `"ln(16/9)"`, `ln(64)`, `ln(8) - 3 * ln(2)`, `log(3, 81) / 4` | `ln(16/9)`, `ln(64)`, `0`, `1` | the same | | `RewriteRules.Power.Rules.Count`, and `RewriteRules.All` by growth | `31`; 124 / 49 / 31 / 123 | `32`; 124 / 49 / 32 / 123 | -### An integral across a step is split at the jumps, never taken through an antiderivative - -`integral(x - floor(x), x, 0, 3)` answered `0`. The rules find an antiderivative of an integrand with -`floor(x)` in it by taking the floor for a constant — which is right between two of its jumps and -wrong across one — and the definite integral then evaluated that antiderivative at the bounds. -`(x - floor(x))^2` from 0 to 4 was `0` where it is `4/3`, and from 0 to a symbolic `n` was -`(n - floor(n))^3 / 3`, which is wrong for every whole `n` but 0. - -An integrand with `floor(x)` or `ceil(x)` of the variable no longer goes through an antiderivative -between two bounds. Between whole bounds it is split into unit intervals, on each of which the -floor is `n`, the ceiling `n + 1` and `x` is `n + t`, and the integral is a sum over `n` of an -integral over `t` with no step in it; a numeric bound that is not whole contributes the piece up to -the nearest whole number, on which the step is one known constant; a symbolic bound is left as -written, since an integral's bound is not whole by convention as a summation's index is. Offered -only where every piece resolves. Question I.2 of -[#1212](https://github.com/asc-community/AngouriMath/issues/1212). Both columns measured on a -build, `4fd6dda5` against this change. +### An integral across a jump is split at the jumps, never taken through an antiderivative + +`F(b) - F(a)` is the integral only where `F` is continuous on `[a, b]`, and the definite integral +evaluated an antiderivative at its bounds whatever the integrand did in between. Three shapes jump: +a piecewise whose conditions mention the variable, whose antiderivative the generic argument +expansion built case by case and then handed back with the integration variable still inside it — +the tent map `piecewise(2x provided x <= 1/2, 2 - 2x)` over `[0, 1]` answered `1` (the first case's +antiderivative at both ends), and `piecewise(x provided x < 1, x^2)` over `[0, 2]` answered +`piecewise(2 provided x < 1, 8/3)`; a `provided` on the variable, distributed the same way; and +`floor(x)` or `ceil(x)`, taken for constants across their jumps — `x - floor(x)` from 0 to 3 answered +`0`, `(x - floor(x))^2` from 0 to 4 answered `0` where it is `4/3`, and from 0 to a symbolic `n` +answered `(n - floor(n))^3 / 3`, wrong for every whole `n` but 0. + +An integrand that can jump never goes through an antiderivative between two bounds now. A +piecewise or a `provided` with finitely many breakpoints — conditions comparing the variable with +numbers — has the range cut at the ones inside it, and on each piece is the case that holds at the +piece's midpoint, integrated through the antiderivative and added; a `provided` whose condition +fails on a piece makes the integral undefined and it is left as written. A floor or ceiling has +infinitely many, evenly spaced: between whole bounds the integral is a sum over unit intervals, on +each of which the floor is `n`, the ceiling `n + 1` and `x` is `n + t`, and the summation's closed +forms answer the sum; a numeric bound that is not whole contributes the piece up to the nearest +whole number. A condition against a symbol or a symbolic bound is left as written, since the pieces +depend on where the jumps fall. Offered only where every piece resolves. Question I.2 of +[#1212](https://github.com/asc-community/AngouriMath/issues/1212), generalised on the review of +[#1215](https://github.com/asc-community/AngouriMath/pull/1215). Both columns measured on a build, +`4fd6dda5` and `60545afa` against this change. | | Was | Is | |---|---|---| +| `"integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)".ToEntity().Simplify()` | `1` | `1/2` | +| `"integral(piecewise(x provided x < 1, x^2), x, 0, 2)".ToEntity().Simplify()` | `piecewise(2 provided (x < 1), (8/3) provided True)` | `17/6` | +| `"integral(piecewise(1 provided x < 0, 2 provided x < 1, 3), x, -1, 2)".ToEntity().Simplify()` | `piecewise(3 provided (x < 0), 6 provided (x < 1), 9 provided True)` | `6` | +| `"integral(piecewise(x provided x < a, 0), x, 0, 2)"`, and `integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, t)` | `piecewise(2 provided (x < a), 0 provided True)`, `piecewise((t ^ 2) provided (x <= 1/2), (2 * t - t ^ 2) provided True)` | left as written | +| `"integral(x provided x > 0, x, 0, 1)"`, and the same from -1 to 1 | `1/2 provided x > 0`, `0 provided x > 0` | `1/2`; left as written | +| `"integral(sgn(x), x, -1, 2)"`, `integral(abs(x - 1), x, 0, 3)` | `1`, `5/2` | the same — the antiderivatives of a sign and a modulus are continuous | | `"integral(x - floor(x), x, 0, 3)".ToEntity().Simplify()` | `0` | `3/2` | | `"integral((x - floor(x))^2, x, 0, 4)".ToEntity().Simplify()` | `0` | `4/3` | | `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()` | `(n - floor(n)) ^ 3 / 3` | left as written; with `n = 6` substituted, `2` | diff --git a/Sources/.editorconfig b/Sources/.editorconfig index d17ed480c..ecd5ddbe7 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -204,10 +204,10 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/BinomialSumTest.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 -[AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs] +[AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.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/Calculus/UnitIntervalIntegrationTest.cs] +[Tests/UnitTests/Calculus/BreakpointIntegrationTest.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 [AngouriMath/Functions/Algebra/Polynomials/PolynomialSummation.cs] diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs b/Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs new file mode 100644 index 000000000..144a349f7 --- /dev/null +++ b/Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs @@ -0,0 +1,295 @@ +// +// 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.Collections.Generic; +using System.Linq; +using PeterO.Numbers; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Functions.Algebra +{ + /// + /// A definite integral whose integrand can jump inside the range, split at the jumps. A + /// piecewise breaks where a case's condition changes truth; floor(x) and + /// ceil(x) break at every whole number. Between two breakpoints the piecewise is one + /// of its cases and the step is one constant, so the pieces integrate through an + /// antiderivative and add. + /// + /// + /// + /// F(b) - F(a) is the integral only where F is continuous on [a, b], and + /// an antiderivative found with the piecewise's cases or the floor taken as constants is + /// continuous between two jumps and not across one. Taken across one it answered + /// integral(x - floor(x), x, 0, 3) with 0 and the tent map's integral over + /// [0, 1] with 1, where they are 3/2 and 1/2; with a symbolic + /// bound it answered (n - floor(n))^3 / 3, and with a piecewise it handed back a + /// piecewise that still mentioned the integration variable. None of that happens here: an + /// integrand with a break never goes through an antiderivative between two bounds. + /// + /// + /// Finitely many breakpoints -- a piecewise whose conditions compare the variable with + /// numbers -- are collected, the range is cut at the ones inside it, and on each piece the + /// piecewise is replaced by the case that holds at the piece's midpoint, which is exact + /// because the condition has no other place to change. A condition that compares the + /// variable with something symbolic, or a bound that is symbolic, is declined: the pieces + /// depend on where the jumps fall, and answering across them is what this exists to stop. + /// A piecewise whose conditions do not mention the variable is a constant here and goes + /// through the antiderivative as one. + /// + /// + /// Infinitely many, evenly spaced -- a floor or a ceiling of the variable -- are the + /// same split written as a sum: on [n, n + 1) the floor is n, the ceiling + /// n + 1, x is n + t with t over [0, 1), and the integral + /// over whole bounds is a sum over n of an integral over t with no step in it, + /// which the summation's closed forms answer; +oo is allowed as the upper bound. A + /// numeric bound that is not whole contributes the piece up to the nearest whole number, + /// on which the step is one constant. The fractional part x - floor(x) is substituted + /// as a unit, since written out it arrives as n + t - n, and a term subtracted from + /// itself simplifies to a conditional zero rather than to nothing + /// (#1174). + /// + /// + /// Offered only where every piece then resolves; an integral rewritten as an unevaluated sum + /// of unevaluated integrals is not an answer. Question I.2 of + /// #1212, and the + /// review of #1215, + /// which asked for the general mechanism rather than the floor's special case. + /// + /// + internal static class BreakpointIntegration + { + /// + /// Whether the integrand can jump in : a piecewise whose conditions + /// mention it, or a floor or ceiling of it. + /// + internal static bool HasABreak(Entity expr, Variable x) + => expr.Nodes.Any(node => IsABreak(node, x)); + + private static bool IsABreak(Entity node, Variable x) + => node switch + { + Floorf(var argument) => argument == x, + Ceilf(var argument) => argument == x, + Piecewise piecewise => piecewise.Cases.Any(c => c.Predicate.ContainsNode(x)), + // A condition on the variable is a one-case piecewise whose other case is undefined. + Providedf provided => provided.Predicate.ContainsNode(x), + _ => false, + }; + + internal static Entity? Split(Entity expr, Variable x, Entity from, Entity to) + { + if (expr.Nodes.Any(node => node is Piecewise or Providedf && IsABreak(node, x))) + return OverCaseBoundaries(expr, x, from, to); + if (HasAStep(expr, x)) + return OverUnitIntervals(expr, x, from, to); + return null; + } + + // ---- finitely many breakpoints: a piecewise -------------------------------------------- + + private static Entity? OverCaseBoundaries(Entity expr, Variable x, Entity from, Entity to) + { + if (from.Evaled is not Real lower || !lower.IsFinite || to.Evaled is not Real upper || !upper.IsFinite) + return null; + if (upper < lower) + return null; + + var thresholds = new List(); + foreach (var node in expr.Nodes) + switch (node) + { + case Piecewise piecewise: + foreach (var @case in piecewise.Cases) + if (!CollectThresholds(@case.Predicate, x, thresholds)) + return null; + break; + case Providedf provided: + if (!CollectThresholds(provided.Predicate, x, thresholds)) + return null; + break; + } + + var points = new List { lower }; + foreach (var threshold in thresholds.Where(t => lower < t && t < upper).OrderBy(t => t.EDecimal)) + if (!points.Contains(threshold)) + points.Add(threshold); + if (upper > lower) + points.Add(upper); + + Entity total = Integer.Zero; + for (var i = 0; i + 1 < points.Count; i++) + { + var (start, end) = (points[i], points[i + 1]); + if (((Entity)start + end).Evaled is not Real twice) + return null; + var midpoint = (Real)(twice / Integer.Create(2)).Evaled; + var resolved = ResolvedAt(expr, x, midpoint); + if (resolved is null) + return null; + var piece = resolved.Integrate(x, start, end); + if (piece is Integralf) + return null; + total += piece; + } + return total.InnerSimplified; + } + + /// + /// The values of at which can change + /// truth, added to ; where it + /// compares with something that is not a number, or is a shape + /// this does not read. + /// + private static bool CollectThresholds(Entity predicate, Variable x, List thresholds) + { + if (!predicate.ContainsNode(x)) + return true; + bool Both(Entity left, Entity right) + => CollectThresholds(left, x, thresholds) && CollectThresholds(right, x, thresholds); + switch (predicate) + { + case Andf(var a, var b): + return Both(a, b); + case Orf(var a, var b): + return Both(a, b); + case Impliesf(var a, var b): + return Both(a, b); + case Xorf(var a, var b): + return Both(a, b); + case Notf(var operand): + return CollectThresholds(operand, x, thresholds); + case ComparisonSign comparison when comparison.DirectChildren.Count == 2: + { + var (left, right) = (comparison.DirectChildren[0], comparison.DirectChildren[1]); + var other = left == x ? right : right == x ? left : null; + if (other is null || other.ContainsNode(x)) + return false; + if (other.Evaled is not Real value || !value.IsFinite) + return false; + thresholds.Add(value); + return true; + } + default: + return false; + } + } + + /// + /// with every piecewise replaced by the case that holds at + /// , or where a case's truth there cannot be + /// decided or no case holds. + /// + private static Entity? ResolvedAt(Entity expr, Variable x, Real at) + { + var failed = false; + var resolved = expr.Replace(node => + { + if (failed) + return node; + if (node is Providedf provided && provided.Predicate.ContainsNode(x)) + { + // Where the condition fails the integrand has no value, and neither has + // the integral. + if (provided.Predicate.Substitute(x, at).Evaled is Entity.Boolean { Value: true }) + return provided.Expression; + failed = true; + return node; + } + if (node is not Piecewise piecewise) + return node; + foreach (var @case in piecewise.Cases) + { + if (@case.Predicate.Substitute(x, at).Evaled is not Entity.Boolean holds) + { + failed = true; + return node; + } + if (holds.Value) + return @case.Expression; + } + failed = true; + return node; + }); + return failed ? null : resolved; + } + + // ---- infinitely many, evenly spaced: a floor or a ceiling ------------------------------ + + private static bool HasAStep(Entity expr, Variable x) + => expr.Nodes.Any(node => node is Floorf(var f) && f == x || node is Ceilf(var c) && c == x); + + private static Entity? OverUnitIntervals(Entity expr, Variable x, Entity from, Entity to) + { + if (from.Evaled is not Real lower || !lower.IsFinite) + return null; + var unbounded = to.Evaled is Real { IsFinite: false, IsNaN: false, IsNegative: false }; + if (!unbounded && (to.Evaled is not Real || !((Real)to.Evaled).IsFinite)) + return null; + var upper = unbounded ? null : (Real)to.Evaled; + if (upper is not null && upper < lower) + return null; + + // The step is one constant from the lower bound up to the next whole number, and + // one constant from the last whole number up to the upper bound. + var firstWhole = Ceiling(lower); + Entity total = Integer.Zero; + if (upper is not null && Floor(upper) == Floor(lower) && !IsWhole(lower)) + // Both bounds inside one unit interval: a single piece with the step known. + return OnAPiece(expr, x, from, to, Floor(lower)) is { } single ? single.InnerSimplified : null; + if (!IsWhole(lower)) + { + if (OnAPiece(expr, x, from, Integer.Create(firstWhole), Floor(lower)) is not { } head) + return null; + total += head; + } + if (upper is not null && !IsWhole(upper)) + { + var lastWhole = Floor(upper); + if (OnAPiece(expr, x, Integer.Create(lastWhole), to, lastWhole) is not { } tail) + return null; + total += tail; + } + + var taken = expr.Vars.Append(x).ToList(); + var n = Variable.CreateTemp(taken); + taken.Add(n); + var t = Variable.CreateTemp(taken); + var piece = expr + .Substitute(x - new Floorf(x), t) + .Substitute(new Floorf(x), n) + .Substitute(new Ceilf(x), n + Integer.One) + .Substitute(x, n + t); + var overUnitInterval = new Integralf(piece, t, (Integer.Zero, Integer.One)).InnerSimplified; + if (overUnitInterval is Integralf) + return null; + Entity lastInterval = unbounded ? to : Integer.Create(Floor(upper!).Subtract(EInteger.One)); + var sum = new Summationf(overUnitInterval, n, Integer.Create(firstWhole), lastInterval).InnerSimplified; + if (sum is Summationf) + return null; + return (total + sum).InnerSimplified; + } + + /// + /// The integral from to on a piece where + /// the floor is throughout (and the ceiling one more, + /// the ends aside), or where that integral is not found. + /// + private static Entity? OnAPiece(Entity expr, Variable x, Entity from, Entity to, EInteger floorValue) + { + var known = expr + .Substitute(new Floorf(x), Integer.Create(floorValue)) + .Substitute(new Ceilf(x), Integer.Create(floorValue.Add(EInteger.One))); + var result = known.Integrate(x, from, to); + return result is Integralf ? null : result; + } + + private static bool IsWhole(Real value) => value.EDecimal.IsInteger(); + private static EInteger Floor(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Floor)).ToEInteger(); + private static EInteger Ceiling(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Ceiling)).ToEInteger(); + } +} diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 571c5bb4b..c3d222c32 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -47,14 +47,15 @@ public Entity Integrate(Variable x) => /// An integrated expression. It might remain the same or be transformed into nodes with no integrals. /// public Entity Integrate(Variable x, Entity from, Entity to) => - // An integrand with floor(x) or ceil(x) in it never goes through an antiderivative - // between two bounds: the rules integrate it as if the step were a constant, which - // is right between two of its jumps and wrong across one -- (x - floor(x))^2 from - // 0 to 4 came back as 0 that way, where it is 4/3, and from 0 to n as - // (n - floor(n))^3 / 3. It is split at the jumps instead, and where that cannot be - // done it is left as written. See UnitIntervalIntegration. - Functions.Algebra.UnitIntervalIntegration.HasAStep(this, x) - ? Functions.Algebra.UnitIntervalIntegration.Split(this, x, from, to) ?? new Integralf(this, x, (from, to)) + // An integrand that can jump -- a piecewise whose conditions mention x, a floor or a + // ceiling of x -- never goes through an antiderivative between two bounds: the rules + // integrate it with the case or the step taken for a constant, which is right + // between two jumps and wrong across one. (x - floor(x))^2 from 0 to 4 came back as + // 0 that way, where it is 4/3, and the tent map over [0, 1] as 1, where it is 1/2. + // It is split at the jumps instead, and where that cannot be done it is left as + // written. See BreakpointIntegration. + Functions.Algebra.BreakpointIntegration.HasABreak(this, x) + ? Functions.Algebra.BreakpointIntegration.Split(this, x, from, to) ?? new Integralf(this, x, (from, to)) : Transformation.Integration(x).Apply(this).Output is { } antiderivative ? antiderivative.Substitute(x, to) - antiderivative.Substitute(x, from) : new Integralf(this, x, (from, to)); diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs b/Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs deleted file mode 100644 index d81c891a5..000000000 --- a/Sources/AngouriMath/Functions/Continuous/Integration/UnitIntervalIntegration.cs +++ /dev/null @@ -1,121 +0,0 @@ -// -// 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.Linq; -using PeterO.Numbers; -using static AngouriMath.Entity; -using static AngouriMath.Entity.Number; - -namespace AngouriMath.Functions.Algebra -{ - /// - /// A definite integral whose integrand mentions floor(x) or ceil(x), split at - /// the jumps: on [n, n + 1) the floor is n and the ceiling n + 1 - /// exactly, x is n + t with t over [0, 1), and the integral - /// over whole bounds is a sum over n of an integral over t that mentions no - /// step at all. A numeric bound that is not whole contributes the piece from it to the - /// nearest whole number, on which the step is one known constant. - /// - /// - /// - /// Exact and unconditional: the step is constant on each piece, a single point has no - /// measure, and the pieces are exactly the ones the bounds cover. The fractional part - /// x - floor(x) becomes t by the same substitution, taken first as a unit - /// because written out it arrives as n + t - n, and a term subtracted from itself - /// simplifies to a conditional zero rather than to nothing - /// (#1174). The - /// upper bound may be +oo, in which case the sum runs to +oo and is answered - /// where and its neighbours answer it. - /// - /// - /// Offered only where every piece then resolves -- the unit-interval integral to something - /// that is not an integral, the sum to something that is not a sum -- since an integral - /// rewritten as an unevaluated sum of unevaluated integrals is not an answer. A symbolic - /// bound is declined: an integral's bound is not a whole number by convention as a - /// summation's index is, and the pieces depend on where the jumps fall. Question I.2 of - /// #1212: - /// integral((x - floor(x)) / floor(x)!, x, 1, +oo) is (e - 1) / 2. - /// - /// - internal static class UnitIntervalIntegration - { - /// Whether the integrand mentions floor(x) or ceil(x) of the variable itself. - internal static bool HasAStep(Entity expr, Variable x) - => expr.Nodes.Any(node => node is Floorf(var f) && f == x || node is Ceilf(var c) && c == x); - - internal static Entity? Split(Entity expr, Variable x, Entity from, Entity to) - { - if (!HasAStep(expr, x)) - return null; - if (from.Evaled is not Real lower || !lower.IsFinite) - return null; - var unbounded = to.Evaled is Real { IsFinite: false, IsNaN: false, IsNegative: false }; - if (!unbounded && (to.Evaled is not Real || !((Real)to.Evaled).IsFinite)) - return null; - var upper = unbounded ? null : (Real)to.Evaled; - if (upper is not null && upper < lower) - return null; - - // The step is one constant from the lower bound up to the next whole number, and - // one constant from the last whole number up to the upper bound. - var firstWhole = Ceiling(lower); - Entity total = Integer.Zero; - if (upper is not null && Floor(upper) == Floor(lower) && !IsWhole(lower)) - // Both bounds inside one unit interval: a single piece with the step known. - return OnAPiece(expr, x, from, to, Floor(lower)) is { } single ? single.InnerSimplified : null; - if (!IsWhole(lower)) - { - if (OnAPiece(expr, x, from, Integer.Create(firstWhole), Floor(lower)) is not { } head) - return null; - total += head; - } - if (upper is not null && !IsWhole(upper)) - { - var lastWhole = Floor(upper); - if (OnAPiece(expr, x, Integer.Create(lastWhole), to, lastWhole) is not { } tail) - return null; - total += tail; - } - - var taken = expr.Vars.Append(x).ToList(); - var n = Variable.CreateTemp(taken); - taken.Add(n); - var t = Variable.CreateTemp(taken); - var piece = expr - .Substitute(x - new Floorf(x), t) - .Substitute(new Floorf(x), n) - .Substitute(new Ceilf(x), n + Integer.One) - .Substitute(x, n + t); - var overUnitInterval = new Integralf(piece, t, (Integer.Zero, Integer.One)).InnerSimplified; - if (overUnitInterval is Integralf) - return null; - Entity lastInterval = unbounded ? to : Integer.Create(Floor(upper!).Subtract(EInteger.One)); - var sum = new Summationf(overUnitInterval, n, Integer.Create(firstWhole), lastInterval).InnerSimplified; - if (sum is Summationf) - return null; - return (total + sum).InnerSimplified; - } - - /// - /// The integral from to on a piece where - /// the floor is throughout (and the ceiling one more, - /// the ends aside), or where that integral is not found. - /// - private static Entity? OnAPiece(Entity expr, Variable x, Entity from, Entity to, EInteger floorValue) - { - var known = expr - .Substitute(new Floorf(x), Integer.Create(floorValue)) - .Substitute(new Ceilf(x), Integer.Create(floorValue.Add(EInteger.One))); - var result = known.Integrate(x, from, to); - return result is Integralf ? null : result; - } - - private static bool IsWhole(Real value) => value.EDecimal.IsInteger(); - private static EInteger Floor(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Floor)).ToEInteger(); - private static EInteger Ceiling(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Ceiling)).ToEInteger(); - } -} diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs index 30021b4a0..972b5dcfd 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs @@ -106,6 +106,22 @@ public partial record Integralf private static Entity? ConditionallySimplified(Entity e, bool isExact) => e is Integralf ? null : e.InnerSimplified(isExact); + /// + /// The definite integral, or the node as written where the integrand can jump and + /// the split at its jumps declined. Not there: null hands the + /// expression to the expansion below, which distributes the integral into the + /// cases of a piecewise and under a provided whether or not their + /// conditions mention the integration variable -- and where they do, that is an + /// answer with the variable still in it. See BreakpointIntegration. + /// + private static Entity? DefiniteOrDeclined(Entity expr, Variable var, Entity from, Entity to, bool isExact) + { + var result = expr.Integrate(var, from, to); + if (result is not Integralf) + return result.InnerSimplified(isExact); + return Functions.Algebra.BreakpointIntegration.HasABreak(expr, var) ? result : null; + } + /// /// The antiderivative of with respect to /// , by changing variables rather than by renaming. @@ -129,7 +145,7 @@ protected override Entity InnerSimplify(bool isExact) => ExpandOnTwoAndTArguments(Expression, Var, Range, (a, b, c) => (a, b, c) switch { - (var expr, Variable var, var (from, to)) => Core.Binding.Written(var, ConditionallySimplified(expr.Integrate(var, from, to), isExact)), + (var expr, Variable var, var (from, to)) => Core.Binding.Written(var, DefiniteOrDeclined(expr, var, from, to, isExact)), // The rename, under the same guard as in Derivativef above: exact only // where nothing of the subexpression's own variables is left over. // https://github.com/asc-community/AngouriMath/issues/964 diff --git a/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs new file mode 100644 index 000000000..36e354b41 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs @@ -0,0 +1,128 @@ +// +// 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; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A definite integral whose integrand can jump is split at the jumps rather than taken + /// through an antiderivative that holds the case or the step constant across them. Question + /// I.2 of https://github.com/asc-community/AngouriMath/issues/1212, and the review of #1215. + /// + public sealed class BreakpointIntegrationTest + { + // ---- a piecewise: finitely many breakpoints --------------------------------------------- + + // The tent map over [0, 1] used to answer 1: the antiderivative of the first case, + // x^2, evaluated at both ends. + [Theory] + [InlineData("integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)", "1/2")] + [InlineData("integral(piecewise(x provided x < 1, x^2), x, 0, 2)", "17/6")] + [InlineData("integral(piecewise(1 provided x < 0, 2 provided x < 1, 3), x, -1, 2)", "6")] + [InlineData("integral(piecewise(x provided x <= 1/2, 1 - x), x, 1/4, 3/4)", "3/16")] + [InlineData("integral(piecewise(x provided x < 1, x^2), x, 3, 4)", "37/3")] + [InlineData("integral(piecewise(1 provided x > 0 and x < 1, 0), x, -2, 2)", "1")] + [InlineData("integral(piecewise(sin(x) provided x < pi, 0), x, 0, 2pi)", "2")] + public void APiecewiseIsIntegratedCaseByCase(string integral, string expected) + => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); + + // A condition against a symbol, or a symbolic bound, is declined rather than answered + // with a piecewise that still mentions the integration variable. + [Theory] + [InlineData("integral(piecewise(x provided x < a, 0), x, 0, 2)")] + [InlineData("integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, t)")] + public void ASymbolicBreakpointOrBoundIsLeftAsWritten(string integral) + => Assert.IsType(integral.ToEntity().Simplify()); + + // A condition on the variable is a one-case piecewise whose other case is undefined: + // integrated where it holds, and declined where the range reaches outside it. + [Fact] + public void AConditionOnTheVariableIsIntegratedWhereItHolds() + { + Assert.Equal("1/2".ToEntity(), "integral(x provided x > 0, x, 0, 1)".ToEntity().Simplify()); + Assert.IsType("integral(x provided x > 0, x, -1, 1)".ToEntity().Simplify()); + } + + // A piecewise whose conditions do not mention the variable is a constant here. + [Fact] + public void APiecewiseInAnotherSymbolIsAConstant() + { + var value = "integral(piecewise(1 provided a > 0, 2), x, 0, 3)".ToEntity().Simplify(); + Assert.IsNotType(value); + Assert.Equal("3".ToEntity(), value.Substitute("a", 1).Simplify()); + Assert.Equal("6".ToEntity(), value.Substitute("a", -1).Simplify()); + } + + // ---- a floor or a ceiling: infinitely many, evenly spaced ------------------------------ + + [Fact] + public void TheOrientationWeekIntegralIsHalfOfEMinusOne() + { + var value = "integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify(); + Assert.IsNotType(value); + Assert.Equal("(e - 1) / 2".ToEntity().Simplify(), value); + } + + [Theory] + [InlineData("integral(floor(x), x, 0, 5)", "10")] + [InlineData("integral(x - floor(x), x, 0, 3)", "3/2")] + [InlineData("integral((x - floor(x))^2, x, 0, 4)", "4/3")] + [InlineData("integral(floor(x) * x, x, 0, 3)", "13/2")] + [InlineData("integral(floor(x) / (floor(x) + 1)!, x, 0, +oo)", "1")] + public void AnIntegralOverWholeBoundsIsSummedOverUnitIntervals(string integral, string expected) + => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); + + [Theory] + [InlineData("integral(floor(x), x, 1/2, 2)", "1")] + [InlineData("integral(x - floor(x), x, 0, 5/2)", "9/8")] + [InlineData("integral(floor(x), x, 1/4, 3/4)", "0")] + [InlineData("integral(x - floor(x), x, 3/2, 7/4)", "5/32")] + [InlineData("integral(ceil(x), x, 0, 3)", "6")] + [InlineData("integral(ceil(x) - x, x, 1/2, 2)", "5/8")] + public void ABoundThatIsNotWholeContributesItsOwnPiece(string integral, string expected) + => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); + + // A symbolic bound is declined rather than integrated as if the step were constant, + // which used to answer (n - floor(n))^3 / 3 for this one; with the bound a number the + // same integral splits. + [Fact] + public void ASymbolicBoundIsLeftAsWritten() + { + var value = "integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify(); + Assert.IsType(value); + Assert.Equal("2".ToEntity(), value.Substitute("n", 6).Simplify()); + } + + // The split is exact but the sum it leaves has no closed form here yet -- a geometric + // series -- so the integral stays as written rather than becoming an unevaluated sum. + [Fact] + public void ASumWithoutAClosedFormLeavesTheIntegralAsWritten() + => Assert.IsType("integral(2^(-floor(x)), x, 0, +oo)".ToEntity().Simplify()); + + // Not split and not integrated either: a floor of something other than the variable + // is not a break this reads, and an integrand the unit interval cannot integrate stays. + [Theory] + [InlineData("integral(floor(x^2), x, 0, 2)")] + [InlineData("integral(floor(x) * e^(e^x) , x, 0, 2)")] + public void WhatCannotBeSplitIsLeftAsWritten(string integral) + => Assert.IsType(integral.ToEntity().Simplify()); + + // ---- what does not break is unaffected -------------------------------------------------- + + [Theory] + [InlineData("integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)", "ln(16/9)")] + [InlineData("integral(sgn(x), x, -1, 2)", "1")] + [InlineData("integral(abs(x - 1), x, 0, 3)", "5/2")] + public void AnIntegrandWithoutABreakIsUnaffected(string integral, string expected) + => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); + } +} diff --git a/Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs deleted file mode 100644 index 6fe62c2ff..000000000 --- a/Sources/Tests/UnitTests/Calculus/UnitIntervalIntegrationTest.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -// 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; -using static AngouriMath.Entity; -using static AngouriMath.Entity.Number; - -namespace AngouriMath.Tests.Calculus -{ - /// - /// https://github.com/asc-community/AngouriMath/issues/1212, question I.2: an integral over - /// whole bounds of an integrand with floor(x) in it is a sum of integrals over unit - /// intervals, on each of which the floor is a number. - /// - public sealed class UnitIntervalIntegrationTest - { - [Fact] - public void TheOrientationWeekIntegralIsHalfOfEMinusOne() - { - var value = "integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify(); - Assert.IsNotType(value); - Assert.Equal("(e - 1) / 2".ToEntity().Simplify(), value); - } - - [Theory] - [InlineData("integral(floor(x), x, 0, 5)", "10")] - [InlineData("integral(x - floor(x), x, 0, 3)", "3/2")] - [InlineData("integral((x - floor(x))^2, x, 0, 4)", "4/3")] - [InlineData("integral(floor(x) * x, x, 0, 3)", "13/2")] - [InlineData("integral(floor(x) / (floor(x) + 1)!, x, 0, +oo)", "1")] - public void AnIntegralOverWholeBoundsIsSummedOverUnitIntervals(string integral, string expected) - => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); - - // The split is exact but the sum it leaves has no closed form here yet -- a geometric - // series -- so the integral stays as written rather than becoming an unevaluated sum. - [Fact] - public void ASumWithoutAClosedFormLeavesTheIntegralAsWritten() - => Assert.IsType("integral(2^(-floor(x)), x, 0, +oo)".ToEntity().Simplify()); - - // A bound that is not whole contributes the piece up to the next whole number, on - // which the step is one known constant. - [Theory] - [InlineData("integral(floor(x), x, 1/2, 2)", "1")] - [InlineData("integral(x - floor(x), x, 0, 5/2)", "9/8")] - [InlineData("integral(floor(x), x, 1/4, 3/4)", "0")] - [InlineData("integral(x - floor(x), x, 3/2, 7/4)", "5/32")] - [InlineData("integral(ceil(x), x, 0, 3)", "6")] - [InlineData("integral(ceil(x) - x, x, 1/2, 2)", "5/8")] - public void ABoundThatIsNotWholeContributesItsOwnPiece(string integral, string expected) - => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify()); - - // A symbolic bound is declined rather than integrated as if the step were constant, - // which used to answer (n - floor(n))^3 / 3 for this one; with the bound a number the - // same integral splits. - [Fact] - public void ASymbolicBoundIsLeftAsWritten() - { - var value = "integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify(); - Assert.IsType(value); - Assert.Equal("2".ToEntity(), value.Substitute("n", 6).Simplify()); - } - - // Not split and not integrated either: a floor of something other than the variable - // is not a step this reads, and an integrand the unit interval cannot integrate stays. - [Theory] - [InlineData("integral(floor(x^2), x, 0, 2)")] - [InlineData("integral(floor(x) * e^(e^x) , x, 0, 2)")] - public void WhatCannotBeSplitIsLeftAsWritten(string integral) - => Assert.IsType(integral.ToEntity().Simplify()); - - [Fact] - public void AnIntegralWithoutAFloorIsUnaffected() - => Assert.Equal("ln(4/3) + ln(16/9) / 2".ToEntity(), "integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)".ToEntity().Simplify()); - } -}