diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 39dbaceb9..8a458fe47 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -90,6 +90,7 @@ read first. | | `"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)` | | | `"sum(x^k, k, 0, +oo)".ToEntity().Simplify()`, and every summation of a power with the index in the exponent times something free of the index, to a bound or to `+oo` | left as written | `1 / (1 - x) provided abs(x) < 1`; `sum(2^(-k), k, 0, +oo)` is `2`, `sum(x^k, k, 0, n)` is a piecewise with the ratio 1 and the empty range as cases of their own, and `integral(2^(-floor(x)), x, 0, +oo)` is `2` | +| | `"sum(2^k, k, 0, +oo)".ToEntity().Simplify()`, and every summation to `+oo` whose terms do not tend to zero and whose limit has a sign | left as written | `+oo`, or `-oo` where the limit is negative; `sum(k, k, 1, +oo)` is `+oo` and `integral(floor(x)^floor(x), x, 1, +oo)` is `+oo`. A vanishing limit, a limit that does not exist, and a pole in the index are each still left as written | | **Loud** | `"divides".ToEntity()`, and every expression with a name spelt `divides` in it | the variable `divides`; `2 divides` was `2 * divides` and `3 divides 12` was `3 * divides ^ 12` | a parse error — `divides` is the keyword of the new statement `a divides b`, which reads `3 divides 12` as the statement that 12 is a multiple of 3 | | **Silent** | `"card(x)".ToEntity()` and `"#x"`, and every expression with a name spelt `card` followed by a parenthesis, or a `#` | `card * x` — a variable times the parenthesis; `#` was a parse error; `card({ 1, 2, 3 })` evaluated to the set `{ card, card * 2, card * 3 }` | `#x`, the number of elements of the set `x` (`card( )` accepted, `#` canonical); `#{ 1, 2, 3 }` is `3` | | **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` | @@ -1132,6 +1133,39 @@ 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 | +### A series whose terms do not vanish is answered `+oo` rather than left as written + +`sum(2^k, k, 0, +oo)` and `sum(k, k, 1, +oo)` were left as written. They have no finite value, and +saying nothing was the only thing missing: the **nth-term test** settles them. Terms that do not +tend to zero mean the series diverges, and the *sign* of the limit says which infinity — where the +terms tend to a positive `L` they are eventually all above `L / 2`, so the partial sums pass every +bound. A negative limit gives `-oo` the same way, and the finitely many terms before that point are +finite and cannot change it. + +**A limit of zero is declined, and that is the point of the test rather than a gap in it.** It is +exactly the case the nth-term test says nothing about: `sum(1/k, k, 1, +oo)` diverges and +`sum(1/k^2, k, 1, +oo)` converges, and the terms of both tend to 0. A limit that does not exist is +declined too — `sum((-1)^k, k, 0, +oo)` has no value rather than an infinite one, and telling "the +limit does not exist" apart from "the limit was not computed" is not something to infer from a +failed computation. **And a summand that can fail to exist at some index is declined**, because one +undefined term makes the sum undefined rather than infinite: `sum(k / (k - 5), k, 0, +oo)` has terms +tending to 1, and the term at `k = 5` does not exist. Rather than hunt for poles, the index is +allowed only where none can arise, so a division by anything containing it is refused outright. + +This runs last, after every closed form, so a series that converges is summed rather than tested. +Asked for on the review of [#1218](https://github.com/asc-community/AngouriMath/pull/1218), where +`integral(floor(x)^floor(x), x, 1, +oo)` splits into `sum(n^n, n, 1, +oo)` and was left as written +for want of it. Part of [#1212](https://github.com/asc-community/AngouriMath/issues/1212). Both +columns measured on a build, `102911be` against this change. + +| | Was | Is | +|---|---|---| +| `"sum(2^k, k, 0, +oo)".ToEntity().Simplify()`, `sum(k, k, 1, +oo)`, `sum(k^2 + 1, k, 0, +oo)`, `sum(n^n, n, 1, +oo)` | left as written | `+oo` | +| `"sum(-k, k, 1, +oo)".ToEntity().Simplify()`, `sum(-2^k, k, 0, +oo)` | left as written | `-oo` | +| `"integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify()` | left as written | `+oo` | +| `"sum(1/k, k, 1, +oo)"`, `sum(1/k^2, k, 1, +oo)`, `sum((-1)^k, k, 0, +oo)`, `sum(k / (k - 5), k, 0, +oo)` | left as written | the same — a vanishing limit, a limit that does not exist, and a pole in the index are each declined | +| `"sum(2^(-k), k, 0, +oo)"`, `sum(x^k / k!, k, 0, +oo)`, `sum(2^k, k, 0, 5)` | `2`, `e^x`, `63` | the same — what converges is summed, and a finite range is a finite sum | + ### A geometric series is summed in closed form `sum(x^k, k, 0, +oo)` was left as written, and so was every other sum of a power with the index in diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 92095833f..4eff38e84 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -225,6 +225,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/ExtremumTest.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/DivergentSeries.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/DivergentSeriesTest.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/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 diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md index 8e15ffcb3..2233a05d4 100644 --- a/Sources/AngouriMath/Docs/Usage/Syntax.md +++ b/Sources/AngouriMath/Docs/Usage/Syntax.md @@ -259,6 +259,14 @@ concrete that condition is decidable and the answer is a number. A bound that is a whole one still stays as written, since the index runs over the integers and `sum(k, k, 1, 5/2)` is `1 + 2` rather than the polynomial at `5/2`. +A sum **to `+oo`** is answered where it converges — the geometric, exponential and binomial series +have closed forms — and, failing that, by the **nth-term test**: terms that do not tend to zero +mean no finite value, and a limit with a sign says which infinity, so `sum(2^k, k, 0, +oo)` and +`sum(k, k, 1, +oo)` are `+oo`. Terms that *do* tend to zero are left as written, because that is +the case the test says nothing about — `sum(1/k, k, 1, +oo)` diverges and `sum(1/k^2, k, 1, +oo)` +converges and neither is decided by the size of a term. A summand that can fail to exist at some +index is left alone too, since one undefined term makes the sum undefined rather than infinite. + A `product` gets the same treatment over the narrower class its shape allows: a **monomial** in the index, since a product has no linearity to take a sum of terms apart with. So `product(k, k, 1, n)` is `factorial(n)`, `product(k ^ 2, k, 1, n)` is `factorial(n) ^ 2`, and diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs new file mode 100644 index 000000000..7c9e3d1e8 --- /dev/null +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs @@ -0,0 +1,197 @@ +// +// 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 static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Functions +{ + /// + /// A summation to +oo whose terms do not tend to zero, which therefore has no finite + /// value: sum(2^k, k, 0, +oo) and sum(n^n, n, 1, +oo) are +oo. + /// + /// + /// + /// The nth-term test: if the terms do not tend to zero the series diverges. That alone + /// says a series has no sum; it does not say what to answer instead. What settles the answer is + /// the sign of the limit — where the terms tend to a positive L, they are + /// eventually all above L / 2, so the partial sums pass every bound and the value is + /// +oo; a negative limit gives -oo the same way. The finitely many terms before + /// that point are finite and cannot change it. + /// + /// + /// A limit of zero is declined, and that is the whole difficulty of the test. It is + /// exactly the case the nth-term test says nothing about: sum(1/k) diverges and + /// sum(1/k^2) converges, and their terms both tend to 0. Answering either from this + /// reader would be a guess. So would a limit that does not exist — sum((-1)^k) has no + /// value rather than an infinite one — and that is left as written too, since telling "the + /// limit does not exist" apart from "the limit was not computed" is not something to infer + /// from a failed computation. + /// + /// + /// The summand must have no pole in the index, and this is checked before the limit is + /// asked for. A single undefined term makes the whole sum undefined, not infinite: + /// sum(k / (k - 5), k, 0, +oo) has terms tending to 1, and answering +oo would + /// be wrong because the term at k = 5 does not exist. Rather than hunt for poles, the + /// index is allowed to occur only where none can arise — under +, -, *, + /// and powers of the three shapes lists. Division by + /// anything containing the index, a factorial of it, a logarithm of it and the rest are + /// declined outright. That check is also what keeps this cheap: it runs first, so a summand + /// this cannot speak about never reaches the limit engine. + /// + /// + /// Last in the chain, after every closed form, so that a series which does converge is + /// summed rather than tested. Asked for on the review of + /// #1218, where + /// integral(floor(x)^floor(x), x, 1, +oo) splits into sum(n^n, n, 1, +oo) and was + /// left as written for want of this; part of + /// #1212. + /// + /// + internal static class DivergentSeries + { + /// + /// +oo or -oo where the terms tend to a non-zero limit of that sign, and + /// wherever the test does not settle the question. + /// + internal static Entity? ClosedForm(Entity expression, Entity var, Entity from, Entity to) + { + if (var is not Variable index) + return null; + // To +oo only: a finite range is a finite sum, and the expansion has had it already. + if (to.Evaled is not Real { IsFinite: false, IsNaN: false, IsNegative: false }) + return null; + // A concrete whole lower bound, so that the range is a genuine tail of the integers. + // A symbolic one could be -oo, where there is no first term to be finite. + if (from.Evaled is not Integer start || !start.EInteger.CanFitInInt32()) + return null; + // A condition that holds for every index in range says nothing about the sum and is + // dropped; one that does not is a term that may fail to exist, and the pole check + // below then declines it as the Providedf it still is. + var summand = WithoutConditionsThatHold(expression.InnerSimplified, index, start.EInteger.ToInt32Checked()); + if (!HasNoPoleInTheIndex(summand, index, start)) + return null; + + var limit = summand.Limit(index, Real.PositiveInfinity).Evaled; + return limit switch + { + // Tends to +oo or -oo: the terms pass every bound, so the sums do. + Real { IsFinite: false, IsNaN: false } infinite => infinite, + // Tends to a non-zero L: eventually every term has L's sign and half its size. + Real { IsFinite: true } value when !IsZero(value) => + value.IsNegative ? Real.NegativeInfinity : Real.PositiveInfinity, + // A zero limit says nothing, and anything else was not settled. + _ => null + }; + } + + /// + /// Whether the index can only occur where no term can fail to exist: under +, + /// - and *, and in a power that is one of three safe shapes — a whole + /// non-negative exponent (k ^ 3), a base free of the index and not zero + /// (2 ^ k), or the index itself raised to something over a range that starts at 1 + /// or above (n ^ n). Anything else containing the index is declined. + /// + private static bool HasNoPoleInTheIndex(Entity expression, Variable index, Integer start) + { + // Free of the index is free of poles *in the index*, whatever else it is. + if (!expression.ContainsNode(index)) + return true; + return expression switch + { + Variable => true, + Sumf(var left, var right) => Both(left, right), + Minusf(var left, var right) => Both(left, right), + Mulf(var left, var right) => Both(left, right), + // k ^ 3: a whole non-negative exponent is a repeated product. + Powf(var @base, Integer { IsNegative: false }) => Go(@base), + // 2 ^ k: a base that does not vary and is not zero. + Powf(var @base, var exponent) when !@base.ContainsNode(index) + && @base.Evaled is Complex value && !IsZero(value) => Go(exponent), + // n ^ n: the base is the index, which over a range starting at 1 or above is + // positive, so the power exists at every term of it. + Powf(var @base, var exponent) when @base == index && start.EInteger.Sign > 0 + => Go(exponent), + _ => false + }; + + bool Go(Entity part) => HasNoPoleInTheIndex(part, index, start); + bool Both(Entity left, Entity right) => Go(left) && Go(right); + } + + /// + /// The summand with any attached condition removed that is true for every index in the + /// range, and left alone otherwise. + /// + /// + /// integral(n ^ n, t, 0, 1) is n ^ n provided not n = 0 or n > 0, because + /// 0 ^ 0 is the one power that has to say what it assumes — so the summand the step + /// split hands over carries a condition that is simply true from 1 upwards. + /// has a sibling of this specialised to the clauses a + /// division by a factorial carries; the two vocabularies do not overlap, and a third + /// reader needing one would be the point at which to merge them rather than now. + /// + private static Entity WithoutConditionsThatHold(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 is true for every whole index >= start. Only comparisons + /// of the index with zero are read, since those are what a power attaches; anything else + /// is answered , which costs a declined summation and never a wrong + /// sum. + /// + private static bool HoldsOverTheRange(Entity condition, Variable index, int start) + => condition switch + { + Greaterf(var argument, var zero) + when IsExactlyZero(zero) && TryReadShift(argument, index, out var a) && start + a > 0 => true, + GreaterOrEqualf(var argument, var zero) + when IsExactlyZero(zero) && TryReadShift(argument, index, out var a) && start + a >= 0 => true, + // Never zero because it is always above it; the other direction is not read. + Notf(Equalsf(var argument, var zero)) + when IsExactlyZero(zero) && TryReadShift(argument, index, out var a) && start + a > 0 => 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 IsExactlyZero(Entity what) => what.Evaled is Integer { IsZero: true }; + + /// + /// as index + shift for a whole shift: the index + /// itself, or the index plus or minus a whole number, in either order. Read off the tree, + /// because k - k simplifies to a conditional zero rather than to nothing + /// (#1174). + /// + private static bool TryReadShift(Entity argument, Variable index, out int shift) + { + shift = 0; + if (argument == index) + return true; + switch (argument) + { + case Sumf(var left, Integer right) when left == index && right.EInteger.CanFitInInt32(): + shift = right.EInteger.ToInt32Checked(); + return true; + case Sumf(Integer left, var right) when right == index && left.EInteger.CanFitInInt32(): + shift = left.EInteger.ToInt32Checked(); + return true; + case Minusf(var left, Integer right) when left == index && right.EInteger.CanFitInInt32(): + shift = -right.EInteger.ToInt32Checked(); + return true; + default: + return false; + } + } + } +} 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 c42961068..926b0276e 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 @@ -239,6 +239,9 @@ protected override Entity InnerSimplify(bool isExact) => ?? Functions.ExponentialSeries.ClosedForm(Expression, Var, From, To) ?? Functions.BinomialSum.ClosedForm(Expression, Var, From, To) ?? Functions.GeometricSeries.ClosedForm(Expression, Var, From, To) + // Last: a series that converges is summed above, and only what none of them + // answered is asked whether it diverges. + ?? Functions.DivergentSeries.ClosedForm(Expression, Var, From, To) ?? this; /// diff --git a/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs index 881660680..3cc233518 100644 --- a/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs +++ b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs @@ -102,16 +102,16 @@ public void ASymbolicBoundIsLeftAsWritten() Assert.Equal("2".ToEntity(), value.Substitute("n", 6).Simplify()); } - // The split leaves a geometric series, which is summed in closed form. A sum the closed - // forms do not answer yet leaves the integral as written rather than an unevaluated sum: - // the second one is sum(n^n, n, 1, +oo), whose terms do not tend to zero, so it is +oo -- - // the answer a divergence test would give, and there is none yet. Not a verdict that it - // cannot be answered; the day it is, this line moves to the value. + // The split leaves a summation, and whatever answers that answers the integral. The first + // is a geometric series; the second is sum(n^n, n, 1, +oo), whose terms do not tend to + // zero. This line used to assert the integral stayed as written, with a comment saying it + // would move to the value the day a divergence test existed. See DivergentSeriesTest. [Fact] - public void TheSumTheSplitLeavesIsAnsweredOrTheIntegralStays() + public void TheSumTheSplitLeavesIsWhatAnswersTheIntegral() { Assert.Equal("2".ToEntity(), "integral(2^(-floor(x)), x, 0, +oo)".ToEntity().Simplify()); - Assert.IsType("integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify()); + Assert.Equal(Number.Real.PositiveInfinity, + "integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify()); } // Not split and not integrated either: a floor of something other than the variable diff --git a/Sources/Tests/UnitTests/Calculus/DivergentSeriesTest.cs b/Sources/Tests/UnitTests/Calculus/DivergentSeriesTest.cs new file mode 100644 index 000000000..322eacf6a --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/DivergentSeriesTest.cs @@ -0,0 +1,106 @@ +// +// 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; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A summation to +oo whose terms do not tend to zero has no finite value, and where + /// the limit has a sign the answer is +oo or -oo rather than nothing. The + /// nth-term test, asked for on the review of + /// https://github.com/asc-community/AngouriMath/pull/1218. + /// + public sealed class DivergentSeriesTest + { + [Theory] + [InlineData("sum(2^k, k, 0, +oo)")] + [InlineData("sum(n^n, n, 1, +oo)")] + [InlineData("sum(1, k, 0, +oo)")] + [InlineData("sum(k, k, 1, +oo)")] + [InlineData("sum(k^2 + 1, k, 0, +oo)")] + [InlineData("sum(3 * 2^k, k, 0, +oo)")] + [InlineData("sum(k^3 - k, k, 2, +oo)")] + public void TermsThatDoNotVanishSumToPositiveInfinity(string sum) + => Assert.Equal(Number.Real.PositiveInfinity, sum.ToEntity().Simplify()); + + [Theory] + [InlineData("sum(-1, k, 0, +oo)")] + [InlineData("sum(-k, k, 1, +oo)")] + [InlineData("sum(-2^k, k, 0, +oo)")] + public void ANegativeLimitSumsToNegativeInfinity(string sum) + => Assert.Equal(Number.Real.NegativeInfinity, sum.ToEntity().Simplify()); + + // The case the nth-term test says nothing about, and the reason it must not guess: + // both of these have terms tending to 0, one diverges and one converges. + [Theory] + [InlineData("sum(1/k, k, 1, +oo)")] + [InlineData("sum(1/k^2, k, 1, +oo)")] + [InlineData("sum(1/k^3, k, 1, +oo)")] + public void AVanishingLimitIsLeftAsWritten(string sum) + => Assert.IsType(sum.ToEntity().Simplify()); + + // A limit that does not exist means no value rather than an infinite one, and telling + // "does not exist" from "was not computed" is not something to infer from a failure. + [Theory] + [InlineData("sum((-1)^k, k, 0, +oo)")] + [InlineData("sum(sin(k), k, 1, +oo)")] + public void ALimitThatDoesNotExistIsLeftAsWritten(string sum) + => Assert.IsType(sum.ToEntity().Simplify()); + + // A single undefined term makes the sum undefined, not infinite. The terms of the first + // tend to 1, so a reader that only looked at the limit would answer +oo and be wrong + // about the term at k = 5. + [Theory] + [InlineData("sum(k / (k - 5), k, 0, +oo)")] + [InlineData("sum(k / (k - 5), k, 0, n)")] + [InlineData("sum(1 / (k - 5) + k, k, 0, +oo)")] + public void APoleInTheIndexIsLeftAsWritten(string sum) + => Assert.IsType(sum.ToEntity().Simplify()); + + // What converges is summed by a closed form, not tested for divergence. + [Theory] + [InlineData("sum(2^(-k), k, 0, +oo)", "2")] + [InlineData("sum(1 / 2^k, k, 1, +oo)", "1")] + [InlineData("sum(x^k / k!, k, 0, +oo)", "e^x")] + public void WhatConvergesIsStillSummed(string sum, string expected) + => Assert.Equal(expected.ToEntity().Simplify(), sum.ToEntity().Simplify()); + + // A finite range is a finite sum whatever the terms do. + [Theory] + [InlineData("sum(2^k, k, 0, 5)", "63")] + [InlineData("sum(k, k, 1, 100)", "5050")] + public void AFiniteRangeIsUnaffected(string sum, string expected) + => Assert.Equal(expected.ToEntity(), sum.ToEntity().Simplify()); + + // A symbolic lower bound could be -oo, where there is no first term to be finite. + [Fact] + public void ASymbolicLowerBoundIsLeftAsWritten() + => Assert.IsType("sum(2^k, k, a, +oo)".ToEntity().Simplify()); + + // The condition a power attaches is dropped where it holds over the range: the split + // hands over `n ^ n provided not n = 0 or n > 0`, which from 1 upwards is just n ^ n. + [Fact] + public void AConditionThatHoldsOverTheRangeIsDropped() + => Assert.Equal(Number.Real.PositiveInfinity, + "sum(n^n provided not n = 0 or n > 0, n, 1, +oo)".ToEntity().Simplify()); + + // A condition that does *not* hold over the range is a term that may fail to exist. + [Fact] + public void AConditionThatDoesNotHoldIsLeftAsWritten() + => Assert.IsType("sum(k^2 provided k > 100, k, 1, +oo)".ToEntity().Simplify()); + + // The integral that asked for this: #1218's review. The split leaves sum(n^n, n, 1, +oo). + [Fact] + public void TheStepIntegralThatAskedForThisIsInfinite() + => Assert.Equal(Number.Real.PositiveInfinity, + "integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify()); + } +} diff --git a/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs b/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs index 3e5c007f8..0936a1955 100644 --- a/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs +++ b/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs @@ -88,8 +88,9 @@ public void ASymbolicLowerBoundIsAnsweredToo(string a, string n, string expected // Not this family: a ratio at or beyond 1 to +oo, a polynomial beside the power, an // exponent that is not linear in the index, a base with the index in it. + // `sum(2^k, k, 0, +oo)` was here: this reader still declines it, a ratio at or beyond 1 + // having no sum, but the nth-term test now answers it `+oo`. See DivergentSeriesTest. [Theory] - [InlineData("sum(2^k, k, 0, +oo)")] [InlineData("sum((-1)^k, k, 0, +oo)")] [InlineData("sum(k * (1/2)^k, k, 0, +oo)")] [InlineData("sum(2^(k^2), k, 0, 200)")] diff --git a/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs b/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs index a43bd9e2e..4ac5f20e9 100644 --- a/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs +++ b/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs @@ -356,8 +356,8 @@ public void AnInclusiveRangeAndItsIdentity(string written, string expected) => /// to 5/2 would be 35/8, which answers a different question. /// [Theory] + // `sum(k, k, 1, +oo)` used to be here and is now `+oo`, the terms not tending to zero. [InlineData("sum(k, k, 1, 5/2)")] - [InlineData("sum(k, k, 1, +oo)")] [InlineData("sum(k ^ k, k, 1, n)")] public void ARangeThatIsNotAnIntegerOneStaysAsWritten(string written) => Assert.Equal(written.ToEntity(), written.ToEntity().Simplify()); diff --git a/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs b/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs index a09677e62..cc03c973e 100644 --- a/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs +++ b/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs @@ -131,9 +131,10 @@ public void ARangeTooLongToWriteOutIsStillAnswered(string expression, string exp /// 5/2 is 35/8. Neither is a rounding of the other. /// [Theory] + // `sum(k, k, 1, +oo)` used to be here and is now `+oo`: the nth-term test answers an + // infinite range this closed form still declines. See DivergentSeriesTest. [InlineData("sum(k, k, 1, 5/2)")] [InlineData("sum(k, k, 1, 1.5)")] - [InlineData("sum(k, k, 1, +oo)")] [InlineData("sum(k, k, -oo, n)")] public void ABoundThatIsNotAWholeNumberIsLeftAlone(string expression) => Assert.IsType(expression.ToEntity().Simplify());