Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions Sources/AngouriMath/Docs/Usage/Syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
197 changes: 197 additions & 0 deletions Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A summation to <c>+oo</c> whose terms do not tend to zero, which therefore has no finite
/// value: <c>sum(2^k, k, 0, +oo)</c> and <c>sum(n^n, n, 1, +oo)</c> are <c>+oo</c>.
/// </summary>
/// <remarks>
/// <para>
/// The <b>nth-term test</b>: 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 <em>sign</em> of the limit — where the terms tend to a positive <c>L</c>, they are
/// eventually all above <c>L / 2</c>, so the partial sums pass every bound and the value is
/// <c>+oo</c>; a negative limit gives <c>-oo</c> the same way. The finitely many terms before
/// that point are finite and cannot change it.
/// </para>
/// <para>
/// <b>A limit of zero is declined, and that is the whole difficulty of the test.</b> It is
/// exactly the case the nth-term test says nothing about: <c>sum(1/k)</c> diverges and
/// <c>sum(1/k^2)</c> 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 — <c>sum((-1)^k)</c> 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.
/// </para>
/// <para>
/// <b>The summand must have no pole in the index, and this is checked before the limit is
/// asked for.</b> A single undefined term makes the whole sum undefined, not infinite:
/// <c>sum(k / (k - 5), k, 0, +oo)</c> has terms tending to 1, and answering <c>+oo</c> would
/// be wrong because the term at <c>k = 5</c> does not exist. Rather than hunt for poles, the
/// index is allowed to occur only where none can arise — under <c>+</c>, <c>-</c>, <c>*</c>,
/// and powers of the three shapes <see cref="HasNoPoleInTheIndex"/> 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.
/// </para>
/// <para>
/// Last in the chain, after every closed form, so that a series which <em>does</em> converge is
/// summed rather than tested. Asked for on the review of
/// <a href="https://github.com/asc-community/AngouriMath/pull/1218">#1218</a>, where
/// <c>integral(floor(x)^floor(x), x, 1, +oo)</c> splits into <c>sum(n^n, n, 1, +oo)</c> and was
/// left as written for want of this; part of
/// <a href="https://github.com/asc-community/AngouriMath/issues/1212">#1212</a>.
/// </para>
/// </remarks>
internal static class DivergentSeries
{
/// <summary>
/// <c>+oo</c> or <c>-oo</c> where the terms tend to a non-zero limit of that sign, and
/// <see langword="null"/> wherever the test does not settle the question.
/// </summary>
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
};
}

/// <summary>
/// Whether the index can only occur where no term can fail to exist: under <c>+</c>,
/// <c>-</c> and <c>*</c>, and in a power that is one of three safe shapes — a whole
/// non-negative exponent (<c>k ^ 3</c>), a base free of the index and not zero
/// (<c>2 ^ k</c>), or the index itself raised to something over a range that starts at 1
/// or above (<c>n ^ n</c>). Anything else containing the index is declined.
/// </summary>
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);
}

/// <summary>
/// The summand with any attached condition removed that is true for every index in the
/// range, and left alone otherwise.
/// </summary>
/// <remarks>
/// <c>integral(n ^ n, t, 0, 1)</c> is <c>n ^ n provided not n = 0 or n &gt; 0</c>, because
/// <c>0 ^ 0</c> 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.
/// <see cref="ExponentialSeries"/> 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.
/// </remarks>
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;
}

/// <summary>
/// Whether a condition is true for every whole <c>index &gt;= start</c>. Only comparisons
/// of the index with zero are read, since those are what a power attaches; anything else
/// is answered <see langword="false"/>, which costs a declined summation and never a wrong
/// sum.
/// </summary>
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 };

/// <summary>
/// <paramref name="argument"/> as <c>index + shift</c> for a whole <c>shift</c>: the index
/// itself, or the index plus or minus a whole number, in either order. Read off the tree,
/// because <c>k - k</c> simplifies to a conditional zero rather than to nothing
/// (<a href="https://github.com/asc-community/AngouriMath/issues/1174">#1174</a>).
/// </summary>
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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand Down
Loading
Loading