Skip to content

Commit c19393e

Browse files
A monomial body is multiplied in closed form (#1153)
The same for `product` as #1152 did for `sum`, over the narrower class a product's shape allows: 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) was carried is piecewise(n! provided n >= 1, 1) product(2, k, 1, 500) was carried is the 151-digit integer `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 included. This closes the last of the three `sum`/`product` rows in #717's survey. The condition is `to >= from` where the sum's is `to >= from - 1`, and the one point between them is the reason. At the empty range itself the closed form is `c^0`, which is 1 for every `c` but 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. It cost nothing: both branches say 1 there. Found by comparing the closed form against the expansion at bounds either side of the boundary, which is why the check is written that way rather than against a printed form. 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. `b!/(a-1)!` holds only for `a >= 1`; below that the range runs through zero so the product is 0 while `(a-1)!` is undefined -- and `a < 1` does not make the range empty, so it cannot share a branch with the empty-range case. A piecewise reading "identity otherwise" would be wrong there. Three tests asserted the product is carried, two of them added by #1152 with the reason it could not yet be answered. They now assert what is still carried -- a body that is not one term. Syntax.md, MathS.Product's summary and the #1152 entry in BREAKING-CHANGES.md are corrected the same way. Comparison.md's cell is again left as measured, its note now naming two moved rows rather than one. Full suite 9487 passed, 0 failed. Part of #717. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent a169363 commit c19393e

11 files changed

Lines changed: 373 additions & 32 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ read first.
109109
| | `"a => a + 3".ToEntity()`, and every lambda written with an arrow | `UnhandledParseException` | `lambda(a, a + 3)` |
110110
| | `"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)` |
111111
| | `"sum(k, k, 1, 100000)".ToEntity().Simplify()`, and every concrete range past a hundred terms | `sum(k, k, 1, 100000)` — carried | `5000050000` |
112+
| | `"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)` |
112113

113114
### A polynomial summand is summed in closed form
114115

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

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

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

160+
### A monomial body is multiplied in closed form
161+
162+
The same for `product`, with the narrower reach a product has: a sum of two terms is the sum of
163+
their sums, and a product of two terms is not the product of their products in any way that
164+
helps. What separates is the body that **is** one term.
165+
166+
```
167+
"product(k, k, 1, n)".Simplify() was product(k, k, 1, n)
168+
is piecewise(n! provided n >= 1, 1)
169+
170+
"product(2, k, 1, 500)".Simplify() was product(2, k, 1, 500)
171+
is 3273390607896141870013189696827599152216642046043064789483291368096133796404674554883270092325904157150886684127560071009217256545885393053328527589376
172+
```
173+
174+
`product(k^2, k, 1, n)` is `(n!)^2`, `product(2 * k, k, 1, n)` is `n! * 2^n`, and a constant body
175+
needs no factorial at all — `product(c, k, m, n)` is `c^(n - m + 1)`, symbolic lower bound and
176+
all.
177+
178+
**The condition is `to >= from`, where the sum's is `to >= from - 1`,** and the one point between
179+
them is the whole reason. At the empty range the closed form is `c^0`, which is `1` for every `c`
180+
except zero and undefined there, while the empty product is `1` for every `c` including zero.
181+
Giving that point to the identity branch keeps a value from becoming an undefinedness, and costs
182+
nothing, since both branches say `1` there.
183+
184+
**A lower bound that is not a concrete integer of at least one is declined** where the index is in
185+
the body, rather than conditioned. `product(k, k, a, b)` is `b!/(a-1)!` only for `a >= 1`; below
186+
that the range runs through zero so the product is `0`, while `(a-1)!` is undefined. That cannot
187+
share a branch with the empty-range case, because `a < 1` does not make the range empty — a
188+
piecewise reading "identity otherwise" would be wrong there. So `product(k, k, 0, n)` and
189+
`product(k, k, m, n)` stay as written.
190+
191+
**What is not one term is carried**, `product(k + 1, k, 1, n)` included. As for the sum, a bound
192+
that is a number and not a whole one is carried too.
193+
194+
[#717](https://github.com/asc-community/AngouriMath/issues/717).
195+
158196
### A lambda is written with an arrow as well as a call
159197

160198
`a => a + 3` was a parse error and is now the same entity as `lambda(a, a + 3)`. Several

‎Sources/.editorconfig‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,8 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
181181
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
182182
[Tests/UnitTests/Core/ClosedFormSummationTest.cs]
183183
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
184+
185+
[AngouriMath/Functions/Algebra/Polynomials/MonomialProduct.cs]
186+
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
187+
[Tests/UnitTests/Core/ClosedFormProductTest.cs]
188+
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

‎Sources/AngouriMath/Convenience/MathS.cs‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6999,14 +6999,27 @@ public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
69996999
/// <summary>
70007000
/// A product of <paramref name="expr"/> as <paramref name="var"/> runs from
70017001
/// <paramref name="from"/> to <paramref name="to"/> inclusive. Mirrors
7002-
/// <see cref="Sum(Entity, Entity, Entity, Entity)"/> exactly, with an empty range
7003-
/// multiplying to <c>1</c>.
7002+
/// <see cref="Sum(Entity, Entity, Entity, Entity)"/>, with an empty range multiplying to
7003+
/// <c>1</c>.
70047004
/// </summary>
70057005
/// <param name="expr">The factor. It may mention <paramref name="var"/>.</param>
70067006
/// <param name="var">The index, which this binds.</param>
70077007
/// <param name="from">The first value of the index.</param>
70087008
/// <param name="to">The last value of the index, inclusive.</param>
7009-
/// <returns>The product written out where it can be, and an unevaluated node otherwise.</returns>
7009+
/// <returns>
7010+
/// The product written out where the bounds are concrete integers and there are not too
7011+
/// many terms; a closed form where the factor is a <b>monomial</b> in the index — a
7012+
/// narrower class than the sum's, a product having no linearity to take a sum of terms
7013+
/// apart with; and an unevaluated node otherwise.
7014+
/// </returns>
7015+
/// <remarks>
7016+
/// The closed form's condition is <c>to &gt;= from</c> rather than the sum's
7017+
/// <c>to &gt;= from - 1</c>: at the empty range itself it would be <c>c ^ 0</c>, which is
7018+
/// undefined at <c>c = 0</c> where the empty product is <c>1</c>. Where the index is in
7019+
/// the factor the lower bound must be a concrete integer of at least one, since the
7020+
/// answer is a ratio of factorials and <c>factorial</c> has no value at the negative
7021+
/// integers.
7022+
/// </remarks>
70107023
/// <example>
70117024
/// <code>
70127025
/// using System;

‎Sources/AngouriMath/Docs/Usage/Comparison.md‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,20 +248,19 @@ Long answers are truncated in the table below at 64 characters, with `…`.
248248
| holonomic | holonomic functions | `expr_to_holonomic exists: True` | `<no member at all matching "Holonomic">` | absent |
249249
| codegen | compile to a callable | `9` | `9` | answers |
250250

251-
### One row has moved since the run
252-
253-
**concrete / symbolic summation** was measured as `declines` at `6b93b401` and is answered now:
254-
`sum(k, k, 1, n)` is `(n + n^2)/2`, carrying the condition `n >= 0` that this library's
255-
empty-range convention requires and SymPy's reversed-range convention does not. The row is left
256-
as it was measured rather than edited, because the table is a record of one run at one commit and
257-
a hand-corrected cell would be a measurement nobody took; it will read `answers` when
258-
`sympyparity` is next run.
259-
260-
The two rows either side of it have **not** moved. *Infinite series* is still declined —
261-
`sum(1 / k ^ 2, k, 1, +oo)` needs the zeta function, which the row four below records as absent.
262-
*Symbolic product* is still declined, and deliberately: `factorial(n)` is undefined at the
263-
negative integers where the empty product is `1`, so answering it needs a condition of the same
264-
kind the sum now carries.
251+
### Two rows have moved since the run
252+
253+
**concrete / symbolic summation** and **concrete / symbolic product** were both measured as
254+
`declines` at `6b93b401` and are answered now: `sum(k, k, 1, n)` is `(n + n^2)/2` and
255+
`product(k, k, 1, n)` is `factorial(n)`, each carrying a condition on the range being non-empty
256+
that this library's empty-range convention requires and SymPy's reversed-range convention does
257+
not. The rows are left as they were measured rather than edited, because the table is a record of
258+
one run at one commit and a hand-corrected cell would be a measurement nobody took; they will
259+
read `answers` when `sympyparity` is next run.
260+
261+
The row between them has **not** moved. *Infinite series* is still declined —
262+
`sum(1 / k ^ 2, k, 1, +oo)` needs the zeta function, which the row four below records as absent,
263+
and an infinite bound is refused outright by both closed forms.
265264

266265
### Implemented, but not reachable from outside
267266

‎Sources/AngouriMath/Docs/Usage/Syntax.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,16 @@ rather than a hundred thousand terms. The answer carries the condition it needs
255255
— because below that the range is empty and the polynomial is not zero; where the bounds are
256256
concrete that condition is decidable and the answer is a number. A bound that is a number and not
257257
a whole one still stays as written, since the index runs over the integers and
258-
`sum(k, k, 1, 5/2)` is `1 + 2` rather than the polynomial at `5/2`. A `product` stays as written
259-
whatever its body.
258+
`sum(k, k, 1, 5/2)` is `1 + 2` rather than the polynomial at `5/2`.
259+
260+
A `product` gets the same treatment over the narrower class its shape allows: a **monomial** in
261+
the index, since a product has no linearity to take a sum of terms apart with. So
262+
`product(k, k, 1, n)` is `factorial(n)`, `product(k ^ 2, k, 1, n)` is `factorial(n) ^ 2`, and
263+
`product(c, k, m, n)` is `c ^ (n - m + 1)`. Its condition is `to >= from` rather than
264+
`to >= from - 1`, because at the empty range itself the closed form would be `c ^ 0` — undefined
265+
at `c = 0`, where the empty product is `1`. Where the index appears in the body the lower bound
266+
must be a concrete integer of at least one, `factorial` having no value at the negative integers;
267+
`product(k, k, 0, n)` therefore stays as written.
260268

261269
**The declared name means the name, and only inside the operator that declares it.** Two names
262270
show this, because both mean something else in this language:
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// Copyright (c) 2019-2026 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using PeterO.Numbers;
9+
using static AngouriMath.Entity;
10+
using static AngouriMath.Entity.Number;
11+
12+
namespace AngouriMath.Functions
13+
{
14+
/// <summary>
15+
/// A product whose body is a monomial in the index, written in closed form:
16+
/// <c>product(k, k, 1, n)</c> is <c>factorial(n)</c>.
17+
/// </summary>
18+
/// <remarks>
19+
/// <para>
20+
/// The sibling of <see cref="PolynomialSummation"/> and a narrower thing than it, because a
21+
/// product has no linearity to take apart: a sum of two terms is the sum of their sums, and a
22+
/// product of two terms is not the product of their products in any way that helps. What is
23+
/// left is the body that <em>is</em> a single term — <c>c * k^p</c> — over which the product
24+
/// separates into a power of the constant and a power of the factorial.
25+
/// </para>
26+
/// <para>
27+
/// <b>The condition is the same one, for the same reason.</b> An empty range multiplies to
28+
/// <c>1</c>, so <c>product(k, k, 1, n)</c> is <c>1</c> at every <c>n &lt; 1</c>, while
29+
/// <c>factorial(n)</c> is not — it is undefined at the negative integers, being the gamma
30+
/// function's poles. Answering the one with the other unconditionally would turn a value into
31+
/// an undefinedness, which is the failure the contract's O4 is about. The identity holds
32+
/// where <c>to >= from - 1</c>, and that is what is attached.
33+
/// </para>
34+
/// <para>
35+
/// <b>A positive lower bound where the index is in the body</b>, and that one cannot go in the
36+
/// condition. <c>product(k, k, a, b)</c> is <c>b! / (a-1)!</c> only where <c>a >= 1</c>; below
37+
/// that the range runs through zero and the product is <c>0</c>, while <c>(a-1)!</c> is
38+
/// undefined. But <c>a &lt; 1</c> does not mean the range is empty, so it cannot share a
39+
/// branch with the empty-range case — a piecewise saying "identity otherwise" would be wrong
40+
/// there. So it is decided before anything is built, and a lower bound that is not a concrete
41+
/// integer of at least one is declined instead. A constant body has no such restriction,
42+
/// there being no factorial in its answer.
43+
/// </para>
44+
/// </remarks>
45+
internal static class MonomialProduct
46+
{
47+
/// <summary>
48+
/// The largest power of the index this will take. The answer carries the factorial to
49+
/// that power, so the ceiling is about how big an expression is worth returning.
50+
/// </summary>
51+
private const int MaxPower = 16;
52+
53+
/// <summary>
54+
/// <c>product(expression, index, from, to)</c> written in closed form, or
55+
/// <see langword="null"/> where the body is not a monomial in the index, or the bounds
56+
/// are not ones the identity speaks about.
57+
/// </summary>
58+
internal static Entity? ClosedForm(Entity expression, Entity var, Entity from, Entity to)
59+
{
60+
if (var is not Variable index)
61+
return null;
62+
if (!PolynomialSummation.IsWholeOrSymbolic(from) || !PolynomialSummation.IsWholeOrSymbolic(to))
63+
return null;
64+
if (!TreeAnalyzer.TryGetPolynomial(expression, index, out var monomials))
65+
return null;
66+
67+
// One term, or this is not a monomial and the product does not separate.
68+
if (monomials.Count != 1)
69+
return null;
70+
var only = System.Linq.Enumerable.Single(monomials);
71+
if (only.Key.Sign < 0 || !only.Key.CanFitInInt32())
72+
return null;
73+
var power = only.Key.ToInt32Checked();
74+
if (power > MaxPower)
75+
return null;
76+
var coefficient = only.Value;
77+
if (coefficient.ContainsNode(index))
78+
return null;
79+
80+
// How many times the body is multiplied: to - from + 1, which the condition below
81+
// guarantees is not negative.
82+
var count = (to - from + 1).InnerSimplified;
83+
Entity closed = MathS.Pow(coefficient, count);
84+
85+
if (power > 0)
86+
{
87+
// b! / (a-1)! needs a - 1 to be a whole number that factorial is defined at,
88+
// which is decided here rather than asked in the condition -- see the remarks.
89+
if (from.Evaled is not Integer lower || lower.EInteger.Sign < 1)
90+
return null;
91+
var factorials = (MathS.Factorial(to) / MathS.Factorial(from - 1)).InnerSimplified;
92+
closed *= power == 1 ? factorials : MathS.Pow(factorials, Integer.Create(power));
93+
}
94+
95+
// `to >= from` and not `to >= from - 1`, which is where the sum's condition sits.
96+
// The two agree everywhere but the empty range itself, and there the closed form is
97+
// c^0 -- which is 1 for every c except 0, where it is undefined, while the empty
98+
// product is 1 for every c including 0. Handing the empty range to the identity
99+
// branch instead keeps a value from becoming an undefinedness, and costs nothing:
100+
// both branches say 1 there.
101+
return MathS.Piecewise(new[]
102+
{
103+
new Providedf(closed.InnerSimplified, new GreaterOrEqualf(to, from)),
104+
new Providedf(Integer.Create(1), Entity.Boolean.True),
105+
}).InnerSimplified;
106+
}
107+
}
108+
}

‎Sources/AngouriMath/Functions/Algebra/Polynomials/PolynomialSummation.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Entity AtBound(Entity bound)
121121
/// anything symbolic to go through — the symbolic case being the one the closed form
122122
/// exists for.
123123
/// </remarks>
124-
private static bool IsWholeOrSymbolic(Entity bound)
124+
internal static bool IsWholeOrSymbolic(Entity bound)
125125
=> bound.Evaled is Integer || bound.Evaled is not Number;
126126

127127
/// <summary>

‎Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,15 @@ public partial record Productf
226226
private protected override Entity IntrinsicCondition => Boolean.True;
227227

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

233240
public partial record Limitf

0 commit comments

Comments
 (0)