Skip to content

A sum over a set: sum(f, x in S) - #1624

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
a-sum-over-a-set
Sep 30, 2026
Merged

Rafael-SOWNet merged 2 commits into
masterfrom
a-sum-over-a-set

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

sum(f, x in S) is the sum of f over the members of a set, each member counted once. It is a binder, like max(f, t in S) (#1222), and is built by MathS.Sum(f, x, S). The range form sum(f, k, from, to) is untouched. A sum over the roots of a polynomial is a sum over its set of roots, sum(f(w), w in { w : p(w) = 0 }), with no index and no order. That is the shape agreed on #1285, and it lets the rational integrator write an answer over the roots of an irreducible denominator.

What it answers. The terms are added up where the set is finite and its members are known:

  • a listed set of numbers, distinct by value;
  • the roots of a polynomial where every one is rational, a root of a quadratic or a root of a binomial. That is checked by counting them against the degree of the square-free part, so a root the solver missed leaves the sum as written rather than one term short. The roots of an irreducible cubic or quartic are not written out: in radicals they read no more simply than the sum, and for a real cubic with three real roots Cardano's formula writes them with complex radicals. So sum(w^2, w in { w : w^3 + w + 1 = 0 }) stays as it is, and evaluates to -2. (Changed in review, second commit.)

Evaluated to a number, the roots of a root set are found to the working precision by the Durand–Kerner iteration, without asking the solver (NumericalSolving/DurandKerner.cs, one square-free factor at a time), all of them or none. It respects the precision scope and downcasting off. A set with a symbol among its members, which may coincide, and a set that is not finite are left as written.

Measured on this branch and on v2.5.0, where every set form threw sum should have exactly 4 arguments but 2 arguments are provided:

this branch
sum(x^2, x in {1, 2, 3}), simplified 14
sum(w^2, w in { w : w^3 - 2w + 1 = 0 }), simplified 4
sum(w^5, w in { w : w^5 + w + 3 = 0 }), evaluated -15, from the five roots found numerically
sum(k, k in ZZ intersect [1; 10]), simplified 55, as sum(k, k, 1, 10) is
sum(x, x in {a, b}), simplified as written: a and b may be equal

Evaluated with downcasting off, sum(1/w, w in { w : w^5 + w + 3 = 0 }) is -0.333…3336 + 4E-100i at 100 digits: -1/3, with rounding at the last digit.

The name is bound over the term and the set, as in the other binders: it is not a free variable of the sum, and substitution renames it first. Differentiation is termwise where the set does not depend on the variable, and gives zero in the bound name. SymPy export: a listed set goes over as a sum of substitutions, and a root set as RootSum of the square-free part, since RootSum counts a repeated root as often as it is repeated. Anything else is refused.

The node's interval evaluation, and the Rothstein–Trager rule that answers 1/(x^3 + x + 1), come next in a separate PR by the session working on the integrator.

BREAKING-CHANGES.md: sum(f, x in S) was a parse error in 2.5.0. Measured rows are in the entry.

Suite, net10.0, at 7b7f5e11 on master 8cb650e2: 14100 passed, 0 failed. Every target framework builds.
Gate: allocation is what the baseline says on all 19 gated benchmarks.

Part of #1285: the Rothstein–Trager rule in the integrator closes it.

🤖 Generated with Claude Code

https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura

sum took a range and nothing else, so a sum over a set had no spelling:
sum(f, x in S) was a parse error asking for the range form's four
arguments. SumOverSetf reads the second argument as `variable in set` and
binds the variable over the term and the set, as max(f, t in S) does; it
prints \sum_{x \in S} f, and MathS.Sum(f, x, S) builds it. The range form
is untouched. A sum over the roots of a polynomial is a sum over its set
of roots, sum(f(w), w in { w : p(w) = 0 }), with no index and no order.

The terms are added up where the set is finite and its members are known:
a listed set of numbers, distinct by value, and the roots of a polynomial
where the solver writes all of them -- counted against the degree of the
square-free part, so a root the solver missed leaves the sum as written
rather than a term short. The solver is asked only where every
irreducible factor has degree at most four or is a binomial. Evaluated
to a number, the roots it cannot write are found to the working
precision by Durand-Kerner, one square-free factor at a time, all of
them or none; the polynomial is read off the tree, so that holds with
downcasting off. A set whose members may coincide, and one that is not
finite, are left as written.

Termwise differentiation where the set does not depend on the variable,
zero in the bound name; SymPy export as a sum of substitutions or a
RootSum of the square-free part.

Part of #1285.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet Rafael-SOWNet added this to the 2.6.0 milestone Sep 30, 2026
Rational roots, the roots of a quadratic and the roots of a binomial are
written and added; the roots of an irreducible cubic or quartic are not.
In radicals they read no more simply than the sum over them, and for a
real cubic with three real roots Cardano's formula writes them with
complex radicals, so sum(w^2, w in { w : w^3 + w + 1 = 0 }) is kept as it
is and evaluates to -2.

A numeric value goes to the numeric roots at once rather than asking the
solver first, since a check evaluates at many points and the roots do not
depend on them. SquareFreeParts and NumericRoots are internal, for the
interval evaluation that reads them next.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet
Rafael-SOWNet merged commit ad02ebb into master Sep 30, 2026
32 checks passed
Rafael-SOWNet added a commit that referenced this pull request Sep 30, 2026
The Rothstein-Trager resultant declined a rational function whose residues lie in a field of degree
above two: 1/(x^3 + x + 1) and #1285's sqrt(x)/(1 + x + x^4), 2u^2/(1 + u^2 + u^8) under
u = sqrt(x), were left unevaluated. Their logarithms are written now as a sum over the roots of the
factor of the denominator they belong to, sum(A(r)/D'(r) ln(x - r), r in { r : E(r) = 0 }), the
form #1285 asked for, with the node #1624 added. The poles whose residues are roots of R are the
common roots of D and D'^n R(A/D'), so E is their gcd over the rationals, and no arithmetic in the
residues' field is needed.

Only as a last resort. Asked beside the other rules, the sum answered each of Jeffrey's three terms
of (-1 + 4 cos(x) + 5 cos(x)^2)/(-1 - 4 cos(x) - 3 cos(x)^2 + 4 cos(x)^3) with a sum over the roots
of a sextic, where the whole is 2 arctan(tan(x/2)^3 - 2 tan(x/2)). So the rule declines as before
and notes that a sum would answer, and only where the question comes back unanswered and that was
noted is it asked again with sums allowed, under a memo of its own; the answer found then is
remembered for the question.

The integrator checks an answer in double intervals and then in decimals, and neither read the sum
node, so every such answer would have been declined by its own check. PreciseEvaluation encloses
each root now: Durand-Kerner's approximations, each widened by its Weierstrass correction in
intervals, n |w_k| about z_k, which holds exactly one root where the rectangles are apart
(Braess and Hadeler), and the summand worked out on each rectangle.

Measured on the Rubi corpus, master at ad05a79 and this change on it, run side by side: the
independent suites 1753 -> 1756 of 1814 (Bondarenko 22 and 23, and Hearn 66, 1/(1 - x^4 + x^8));
family 1 at ten problems a file, 295 -> 301 of 377 (1/(x^2 (1 - x^3 + x^6)), (1 + x^4)/(1 - x^4 +
x^8), (1 + x^4)/(1 - 5x^4 + x^8), (-1 + 2x^4 + sqrt(3))/(1 - x^4 + x^8), (1 - x^4)/(1 - x^4 + x^8)
and x^3 (5 + x + 3x^2 + 2x^3)/(2 + x + 5x^2 + x^3 + 2x^4)); families 2 to 7 at the usual sample, 663
of 722 on both. 0 wrong everywhere. Run alone, those nine are 0 of 9 on master and 9 of 9 here, and
the tenth problem that moved, 1.1.4.3 #231, is 17.9 s unevaluated on both: its master timeout was
the load of the side-by-side run. A question still declined costs what it did: the problems
unevaluated in both arms took 14.2 s against 13.4 s in the independent suites, 65.6 s against 64.7 s
in families 2 to 7 and 89.9 s against 92.3 s in the family 1 sample.

The performance gate passes on 34c548a8, whose library code is this commit's: allocation is what
the baseline says on all 19 gated benchmarks.

RothsteinTragerTest: the cubic, quartic and quintic rows that were pinned as declined are sums now,
and #1285's integral in PowerSubstitutionIntegralTest; that test's declined witnesses are the
elliptic x^2/sqrt(x^4 + x + 1) and x^2/sqrt(x^4 + x^3 + 1), which nothing answers.
BinomialDenominatorIntegralTest, PartialFractionsTest and RationalIntegralsTest pinned
1/(x^3 + x + 1), 1/(x^3 + x^2 + x + 2), 1/(x^4 + x + 1) and 1/(x^4 + x^3 + 1) as declined: each is
the sum now, checked by differentiating back, and the boundary pinned in their place is the same
kind of denominator with a symbol among its coefficients, which the sum is not written for.
DecliningStaysCheap keeps its ten-second bound as FindingThatNothingSplitsStaysCheap: the second
pass starts only once the first has declined, so the decline it guards is still inside the bound.

Closes #1285.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant