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
40 changes: 39 additions & 1 deletion BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ read first.
| | `"a => a + 3".ToEntity()`, and every lambda written with an arrow | `UnhandledParseException` | `lambda(a, a + 3)` |
| | `"sum(k, k, 1, n)".ToEntity().Simplify()`, and every summation whose body is a polynomial in the index | `sum(k, k, 1, n)` — carried | `piecewise((n + n ^ 2) / 2 provided n >= 0, 0)` |
| | `"sum(k, k, 1, 100000)".ToEntity().Simplify()`, and every concrete range past a hundred terms | `sum(k, k, 1, 100000)` — carried | `5000050000` |
| | `"product(k, k, 1, n)".ToEntity().Simplify()`, and every product whose body is a monomial in the index | `product(k, k, 1, n)` — carried | `piecewise(n! provided n >= 1, 1)` |

### A polynomial summand is summed in closed form

Expand Down Expand Up @@ -147,14 +148,51 @@ answers a different question. `+oo` is refused the same way, so an infinite seri
**A product is unchanged, whatever its body.** `product(k, k, 1, n)` is still carried, and
`factorial(n)` would be a wrong answer for it rather than a missing one: the empty product is `1`
at every `n < 1`, and `factorial` is undefined at the negative integers. Answering it needs the
same kind of condition the sum now carries, and is not done here.
same kind of condition the sum now carries, and is not done here. *It is done in the entry below,
which gives it that condition.*

No Bernoulli numbers are involved. The sum of a degree-`d` polynomial is a polynomial of degree
`d + 1`, so `d + 2` of its values determine it, and those values are short sums computed directly;
interpolating them recovers the coefficients exactly in rational arithmetic.

[#717](https://github.com/asc-community/AngouriMath/issues/717).

### A monomial body is multiplied in closed form

The same for `product`, with the narrower reach a product has: a sum of two terms is the sum of
their sums, and a product of two terms is not the product of their products in any way that
helps. What separates is the body that **is** one term.

```
"product(k, k, 1, n)".Simplify() was product(k, k, 1, n)
is piecewise(n! provided n >= 1, 1)

"product(2, k, 1, 500)".Simplify() was product(2, k, 1, 500)
is 3273390607896141870013189696827599152216642046043064789483291368096133796404674554883270092325904157150886684127560071009217256545885393053328527589376
```

`product(k^2, k, 1, n)` is `(n!)^2`, `product(2 * k, k, 1, n)` is `n! * 2^n`, and a constant body
needs no factorial at all — `product(c, k, m, n)` is `c^(n - m + 1)`, symbolic lower bound and
all.

**The condition is `to >= from`, where the sum's is `to >= from - 1`,** and the one point between
them is the whole reason. At the empty range the closed form is `c^0`, which is `1` for every `c`
except zero and undefined there, while the empty product is `1` for every `c` including zero.
Giving that point to the identity branch keeps a value from becoming an undefinedness, and costs
nothing, since both branches say `1` there.

**A lower bound that is not a concrete integer of at least one is declined** where the index is in
the body, rather than conditioned. `product(k, k, a, b)` is `b!/(a-1)!` only for `a >= 1`; below
that the range runs through zero so the product is `0`, while `(a-1)!` is undefined. That cannot
share a branch with the empty-range case, because `a < 1` does not make the range empty — a
piecewise reading "identity otherwise" would be wrong there. So `product(k, k, 0, n)` and
`product(k, k, m, n)` stay as written.

**What is not one term is carried**, `product(k + 1, k, 1, n)` included. As for the sum, a bound
that is a number and not a whole one is carried too.

[#717](https://github.com/asc-community/AngouriMath/issues/717).

### A lambda is written with an arrow as well as a call

`a => a + 3` was a parse error and is now the same entity as `lambda(a, a + 3)`. Several
Expand Down
5 changes: 5 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,8 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
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]
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/MonomialProduct.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/ClosedFormProductTest.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
19 changes: 16 additions & 3 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6999,14 +6999,27 @@ public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
/// <summary>
/// A product of <paramref name="expr"/> as <paramref name="var"/> runs from
/// <paramref name="from"/> to <paramref name="to"/> inclusive. Mirrors
/// <see cref="Sum(Entity, Entity, Entity, Entity)"/> exactly, with an empty range
/// multiplying to <c>1</c>.
/// <see cref="Sum(Entity, Entity, Entity, Entity)"/>, with an empty range multiplying to
/// <c>1</c>.
/// </summary>
/// <param name="expr">The factor. It may mention <paramref name="var"/>.</param>
/// <param name="var">The index, which this binds.</param>
/// <param name="from">The first value of the index.</param>
/// <param name="to">The last value of the index, inclusive.</param>
/// <returns>The product written out where it can be, and an unevaluated node otherwise.</returns>
/// <returns>
/// The product written out where the bounds are concrete integers and there are not too
/// many terms; a closed form where the factor is a <b>monomial</b> in the index — a
/// narrower class than the sum's, a product having no linearity to take a sum of terms
/// apart with; and an unevaluated node otherwise.
/// </returns>
/// <remarks>
/// The closed form's condition is <c>to &gt;= from</c> rather than the sum's
/// <c>to &gt;= from - 1</c>: at the empty range itself it would be <c>c ^ 0</c>, which is
/// undefined at <c>c = 0</c> where the empty product is <c>1</c>. Where the index is in
/// the factor the lower bound must be a concrete integer of at least one, since the
/// answer is a ratio of factorials and <c>factorial</c> has no value at the negative
/// integers.
/// </remarks>
/// <example>
/// <code>
/// using System;
Expand Down
27 changes: 13 additions & 14 deletions Sources/AngouriMath/Docs/Usage/Comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,20 +248,19 @@ Long answers are truncated in the table below at 64 characters, with `…`.
| holonomic | holonomic functions | `expr_to_holonomic exists: True` | `<no member at all matching "Holonomic">` | absent |
| codegen | compile to a callable | `9` | `9` | answers |

### One row has moved since the run

**concrete / symbolic summation** was measured as `declines` at `6b93b401` and is answered now:
`sum(k, k, 1, n)` is `(n + n^2)/2`, carrying the condition `n >= 0` that this library's
empty-range convention requires and SymPy's reversed-range convention does not. The row is left
as it was measured rather than edited, because the table is a record of one run at one commit and
a hand-corrected cell would be a measurement nobody took; it will read `answers` when
`sympyparity` is next run.

The two rows either side of it have **not** moved. *Infinite series* is still declined —
`sum(1 / k ^ 2, k, 1, +oo)` needs the zeta function, which the row four below records as absent.
*Symbolic product* is still declined, and deliberately: `factorial(n)` is undefined at the
negative integers where the empty product is `1`, so answering it needs a condition of the same
kind the sum now carries.
### Two rows have moved since the run

**concrete / symbolic summation** and **concrete / symbolic product** were both measured as
`declines` at `6b93b401` and are answered now: `sum(k, k, 1, n)` is `(n + n^2)/2` and
`product(k, k, 1, n)` is `factorial(n)`, each carrying a condition on the range being non-empty
that this library's empty-range convention requires and SymPy's reversed-range convention does
not. The rows are left as they were measured rather than edited, because the table is a record of
one run at one commit and a hand-corrected cell would be a measurement nobody took; they will
read `answers` when `sympyparity` is next run.

The row between them has **not** moved. *Infinite series* is still declined —
`sum(1 / k ^ 2, k, 1, +oo)` needs the zeta function, which the row four below records as absent,
and an infinite bound is refused outright by both closed forms.

### Implemented, but not reachable from outside

Expand Down
12 changes: 10 additions & 2 deletions Sources/AngouriMath/Docs/Usage/Syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,16 @@ rather than a hundred thousand terms. The answer carries the condition it needs
— because below that the range is empty and the polynomial is not zero; where the bounds are
concrete that condition is decidable and the answer is a number. A bound that is a number and not
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 `product` stays as written
whatever its body.
`sum(k, k, 1, 5/2)` is `1 + 2` rather than the polynomial at `5/2`.

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
`product(c, k, m, n)` is `c ^ (n - m + 1)`. Its condition is `to >= from` rather than
`to >= from - 1`, because at the empty range itself the closed form would be `c ^ 0` — undefined
at `c = 0`, where the empty product is `1`. Where the index appears in the body the lower bound
must be a concrete integer of at least one, `factorial` having no value at the negative integers;
`product(k, k, 0, n)` therefore stays as written.

**The declared name means the name, and only inside the operator that declares it.** Two names
show this, because both mean something else in this language:
Expand Down
108 changes: 108 additions & 0 deletions Sources/AngouriMath/Functions/Algebra/Polynomials/MonomialProduct.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// 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 PeterO.Numbers;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Functions
{
/// <summary>
/// A product whose body is a monomial in the index, written in closed form:
/// <c>product(k, k, 1, n)</c> is <c>factorial(n)</c>.
/// </summary>
/// <remarks>
/// <para>
/// The sibling of <see cref="PolynomialSummation"/> and a narrower thing than it, because a
/// product has no linearity to take apart: a sum of two terms is the sum of their sums, and a
/// product of two terms is not the product of their products in any way that helps. What is
/// left is the body that <em>is</em> a single term — <c>c * k^p</c> — over which the product
/// separates into a power of the constant and a power of the factorial.
/// </para>
/// <para>
/// <b>The condition is the same one, for the same reason.</b> An empty range multiplies to
/// <c>1</c>, so <c>product(k, k, 1, n)</c> is <c>1</c> at every <c>n &lt; 1</c>, while
/// <c>factorial(n)</c> is not — it is undefined at the negative integers, being the gamma
/// function's poles. Answering the one with the other unconditionally would turn a value into
/// an undefinedness, which is the failure the contract's O4 is about. The identity holds
/// where <c>to >= from - 1</c>, and that is what is attached.
/// </para>
/// <para>
/// <b>A positive lower bound where the index is in the body</b>, and that one cannot go in the
/// condition. <c>product(k, k, a, b)</c> is <c>b! / (a-1)!</c> only where <c>a >= 1</c>; below
/// that the range runs through zero and the product is <c>0</c>, while <c>(a-1)!</c> is
/// undefined. But <c>a &lt; 1</c> does not mean the range is empty, so it cannot share a
/// branch with the empty-range case — a piecewise saying "identity otherwise" would be wrong
/// there. So it is decided before anything is built, and a lower bound that is not a concrete
/// integer of at least one is declined instead. A constant body has no such restriction,
/// there being no factorial in its answer.
/// </para>
/// </remarks>
internal static class MonomialProduct
{
/// <summary>
/// The largest power of the index this will take. The answer carries the factorial to
/// that power, so the ceiling is about how big an expression is worth returning.
/// </summary>
private const int MaxPower = 16;

/// <summary>
/// <c>product(expression, index, from, to)</c> written in closed form, or
/// <see langword="null"/> where the body is not a monomial in the index, or the bounds
/// are not ones the identity speaks about.
/// </summary>
internal static Entity? ClosedForm(Entity expression, Entity var, Entity from, Entity to)
{
if (var is not Variable index)
return null;
if (!PolynomialSummation.IsWholeOrSymbolic(from) || !PolynomialSummation.IsWholeOrSymbolic(to))
return null;
if (!TreeAnalyzer.TryGetPolynomial(expression, index, out var monomials))
return null;

// One term, or this is not a monomial and the product does not separate.
if (monomials.Count != 1)
return null;
var only = System.Linq.Enumerable.Single(monomials);
if (only.Key.Sign < 0 || !only.Key.CanFitInInt32())
return null;
var power = only.Key.ToInt32Checked();
if (power > MaxPower)
return null;
var coefficient = only.Value;
if (coefficient.ContainsNode(index))
return null;

// How many times the body is multiplied: to - from + 1, which the condition below
// guarantees is not negative.
var count = (to - from + 1).InnerSimplified;
Entity closed = MathS.Pow(coefficient, count);

if (power > 0)
{
// b! / (a-1)! needs a - 1 to be a whole number that factorial is defined at,
// which is decided here rather than asked in the condition -- see the remarks.
if (from.Evaled is not Integer lower || lower.EInteger.Sign < 1)
return null;
var factorials = (MathS.Factorial(to) / MathS.Factorial(from - 1)).InnerSimplified;
closed *= power == 1 ? factorials : MathS.Pow(factorials, Integer.Create(power));
}

// `to >= from` and not `to >= from - 1`, which is where the sum's condition sits.
// The two agree everywhere but the empty range itself, and there the closed form is
// c^0 -- which is 1 for every c except 0, where it is undefined, while the empty
// product is 1 for every c including 0. Handing the empty range to the identity
// branch instead keeps a value from becoming an undefinedness, and costs nothing:
// both branches say 1 there.
return MathS.Piecewise(new[]
{
new Providedf(closed.InnerSimplified, new GreaterOrEqualf(to, from)),
new Providedf(Integer.Create(1), Entity.Boolean.True),
}).InnerSimplified;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Entity AtBound(Entity bound)
/// anything symbolic to go through — the symbolic case being the one the closed form
/// exists for.
/// </remarks>
private static bool IsWholeOrSymbolic(Entity bound)
internal static bool IsWholeOrSymbolic(Entity bound)
=> bound.Evaled is Integer || bound.Evaled is not Number;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,15 @@ public partial record Productf
private protected override Entity IntrinsicCondition => Boolean.True;

/// <inheritdoc/>
/// <remarks>
/// The expansion first and the closed form after, as for the sum — and the closed
/// form is narrower here, a product having no linearity to take a general polynomial
/// apart with. A monomial body is what separates.
/// </remarks>
protected override Entity InnerSimplify(bool isExact) =>
Summationf.Expanded(this, Expression, Var, From, To, static (a, b) => a * b, 1, isExact) ?? this;
Summationf.Expanded(this, Expression, Var, From, To, static (a, b) => a * b, 1, isExact)
?? Functions.MonomialProduct.ClosedForm(Expression, Var, From, To)
?? this;
}

public partial record Limitf
Expand Down
Loading
Loading