diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 03e1be289..9e8a0d10b 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -1608,6 +1608,36 @@ the ends are about, `n >= 0`. With two names it is left as it was, `n >= m - 1`
|---|---|---|
| `"sum(2 k + 1, k, 0, n - 1)".Simplify()` | `piecewise((2 * n + (n - 1) ^ 2 - 1) provided (n - 1 >= -1), 0 provided True)` | `piecewise((n ^ 2) provided (n >= 0), 0 provided True)` |
+### A sum over a set: `sum(f, x in S)`
+
+`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 four arguments of the range form. `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: spelt `sum(f, x in S)`, printed `\sum_{x \in S} f` in LaTeX, and 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.
+
+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 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, which read no more simply than the
+sum. Evaluated to a number, the roots it cannot
+write are found to the working precision by the Durand–Kerner iteration, all of them or none. A set
+with a symbol among its members, whose members may coincide, and a set that is not finite are left
+as written. [#1285](https://github.com/asc-community/AngouriMath/issues/1285). Both columns
+measured on a build, `v2.5.0` against this change.
+
+| | Was (2.5.0) | Is |
+|---|---|---|
+| `"sum(x^2, x in {1, 2, 3})".ToEntity()` | `FunctionArgumentCountException`: *sum should have exactly 4 arguments but 2 arguments are provided* | `14` once simplified |
+| `"sum(w^2, w in { w : w^3 - 2w + 1 = 0 })".ToEntity().Simplify()` | the same exception | `4` |
+| `"sum(w^5, w in { w : w^5 + w + 3 = 0 })".ToEntity().Evaled` | the same exception | `-15`, from the five roots found numerically |
+| `"sum(k, k in ZZ intersect [1; 10])".ToEntity().Simplify()` | the same exception | `55`, as `sum(k, k, 1, 10)` is |
+| `"sum(x, x in {a, b})".ToEntity().Simplify()` | the same exception | `sum(x, x in { a, b })`, since `a` and `b` may be equal |
+| `"sum(k, k, 1, 10)".ToEntity().Simplify()` | `55` | the same |
+
### `(a + b asech(c x))/(d + e x)^2` is integrated, and a root written apart with `|x|` no longer needs a parity
Rubi's 7.5.1 with a symbolic linear below the bar ran for ten minutes without an answer. By
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 9a89de0e1..d964644f0 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -279,6 +279,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[AngouriMath/Functions/Continuous/ExtremumOverSet.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/SumOverSet.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/Continuous/Solvers/NumericalSolving/DurandKerner.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/Transformations/LogarithmSpellingsTest.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/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs
index 4ba860f7d..6252e36a3 100644
--- a/Sources/AngouriMath/Convenience/MathS.cs
+++ b/Sources/AngouriMath/Convenience/MathS.cs
@@ -7476,6 +7476,40 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
=> new Summationf(expr, var, from, to);
+ ///
+ /// The sum of over the members of as
+ /// ranges over it, each member counted once: written
+ /// sum(expr, var in over). 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 term, as a function of .
+ /// The name that ranges over the set, bound by the sum.
+ /// The set.
+ ///
+ /// The terms added up where the set is finite and its members are known: a listed set of
+ /// numbers, or roots of a polynomial that are rational, roots of a quadratic or roots of a
+ /// binomial -- those of an irreducible cubic or quartic are kept as the sum, radicals
+ /// reading no more simply than it. Evaluated to a
+ /// number, the roots it cannot write are found numerically, all of them or none. Left as
+ /// written otherwise: a set with a symbol among its members, which may coincide, or a set
+ /// that is not finite.
+ ///
+ /// #1285
+ ///
+ ///
+ /// using System;
+ /// using static AngouriMath.MathS;
+ ///
+ /// Console.WriteLine(Sum("w^2", "w", "{ w : w^3 - 2w + 1 = 0 }").Simplify());
+ ///
+ /// Prints
+ ///
+ /// 4
+ ///
+ ///
+ public static Entity Sum(Entity expr, Entity var, Entity over)
+ => new SumOverSetf(expr, var, over);
+
///
/// The largest value takes as ranges over
/// , written max(expr, var in over). Answered over a finite
diff --git a/Sources/AngouriMath/Core/Antlr/AngouriMath.g b/Sources/AngouriMath/Core/Antlr/AngouriMath.g
index 26183810f..e4576d85c 100644
--- a/Sources/AngouriMath/Core/Antlr/AngouriMath.g
+++ b/Sources/AngouriMath/Core/Antlr/AngouriMath.g
@@ -571,7 +571,7 @@ atom returns[Entity value]
| 'limit(' args = function_arguments ')' { Assert("limit", 3, $args.list.Count); $value = MathS.Limit($args.list[0], $args.list[1], $args.list[2]); }
| 'limitleft(' args = function_arguments ')' { Assert("limitleft", 3, $args.list.Count); $value = MathS.Limit($args.list[0], $args.list[1], $args.list[2], AngouriMath.Core.ApproachFrom.Left); }
| 'limitright(' args = function_arguments ')' { Assert("limitright", 3, $args.list.Count); $value = MathS.Limit($args.list[0], $args.list[1], $args.list[2], AngouriMath.Core.ApproachFrom.Right); }
- | 'sum(' args = function_arguments ')' { Assert("sum", 4, $args.list.Count); $value = MathS.Sum($args.list[0], $args.list[1], $args.list[2], $args.list[3]); }
+ | 'sum(' args = function_arguments ')' { if ($args.list.Count == 2 && AngouriMath.Functions.ExtremumOverSet.AsRange($args.list[1]) is var (sumVar, sumOver)) $value = MathS.Sum($args.list[0], sumVar, sumOver); else { Assert("sum", 4, $args.list.Count); $value = MathS.Sum($args.list[0], $args.list[1], $args.list[2], $args.list[3]); } }
| 'product(' args = function_arguments ')' { Assert("product", 4, $args.list.Count); $value = MathS.Product($args.list[0], $args.list[1], $args.list[2], $args.list[3]); }
| 'signum(' args = function_arguments ')' { Assert("signum", 1, $args.list.Count); $value = MathS.Signum($args.list[0]); }
| 'sgn(' args = function_arguments ')' { Assert("sgn", 1, $args.list.Count); $value = MathS.Signum($args.list[0]); }
diff --git a/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs b/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
index 7fd0ac2a4..1e11d2dc1 100644
--- a/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
+++ b/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
@@ -4449,7 +4449,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 916;
Match(T__30);
- Assert("sum", 4, _localctx.args.list.Count); _localctx.value = MathS.Sum(_localctx.args.list[0], _localctx.args.list[1], _localctx.args.list[2], _localctx.args.list[3]);
+ if (_localctx.args.list.Count == 2 && AngouriMath.Functions.ExtremumOverSet.AsRange(_localctx.args.list[1]) is var (sumVar, sumOver)) _localctx.value = MathS.Sum(_localctx.args.list[0], sumVar, sumOver); else { Assert("sum", 4, _localctx.args.list.Count); _localctx.value = MathS.Sum(_localctx.args.list[0], _localctx.args.list[1], _localctx.args.list[2], _localctx.args.list[3]); }
}
break;
case 100:
diff --git a/Sources/AngouriMath/Core/Domains.Classes.cs b/Sources/AngouriMath/Core/Domains.Classes.cs
index adc24a380..1da51eaa3 100644
--- a/Sources/AngouriMath/Core/Domains.Classes.cs
+++ b/Sources/AngouriMath/Core/Domains.Classes.cs
@@ -280,6 +280,14 @@ partial record Productf
internal override Domain DefaultCodomain => Domain.Complex;
}
+ partial record SumOverSetf
+ {
+ ///
+ public override Domain Codomain { get; protected init; } = Domain.Complex;
+ ///
+ internal override Domain DefaultCodomain => Domain.Complex;
+ }
+
partial record Maximumf
{
///
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs
index 0bd9c7fbc..bf85c70b2 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs
@@ -119,6 +119,29 @@ public override Entity Replace(Func func) =>
protected override Entity[] InitDirectChildren() => new[] { Expression, Var, From, To };
}
+ ///
+ /// A sum over a set: sum(f(x), x in S) is the sum of f(x) over the members
+ /// x of S, each counted once. A binder, like , whose
+ /// range is a set rather than two bounds, and so has no index and no order: the sum over
+ /// the roots of a polynomial is sum(f(w), w in { w : p(w) = 0 }), which needs no
+ /// first root.
+ /// https://github.com/asc-community/AngouriMath/issues/1285
+ ///
+ public sealed partial record SumOverSetf(Entity Expression, Entity Var, Entity Over) : CalculusOperator(Expression, Var)
+ {
+ /// The set the variable ranges over.
+ public Entity Over { get; init; } = Binding.Of(Var).In(Over);
+
+ private SumOverSetf New(Entity expression, Entity var, Entity over) =>
+ ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var) && ReferenceEquals(Over, over)
+ ? this : new(expression, var, over) { Codomain = Codomain };
+ ///
+ public override Entity Replace(Func func) =>
+ func(New(Expression.Replace(func), Var, Over.Replace(func)));
+ ///
+ protected override Entity[] InitDirectChildren() => new[] { Expression, Var, Over };
+ }
+
///
/// A node of limit
///
diff --git a/Sources/AngouriMath/Core/Entity/Entity.Definition.cs b/Sources/AngouriMath/Core/Entity/Entity.Definition.cs
index 21ad2c884..ff904fe04 100644
--- a/Sources/AngouriMath/Core/Entity/Entity.Definition.cs
+++ b/Sources/AngouriMath/Core/Entity/Entity.Definition.cs
@@ -694,6 +694,9 @@ public IReadOnlyList Vars
=> BoundBy(index, body, from, to),
Productf(var body, var index, var from, var to)
=> BoundBy(index, body, from, to),
+ // A sum over a set binds the name that ranges over the set:
+ // sum(w * a, w in { w : w^2 = 2 }) is a function of a alone.
+ SumOverSetf(var body, var bound, var over) => BoundBy(bound, body, over),
// An extremum over a set binds the variable that ranges over the set:
// max(t^2 + a, t in [0; 1]) is a function of a alone.
Maximumf(var body, var bound, var over) => BoundBy(bound, body, over),
diff --git a/Sources/AngouriMath/Core/Serialization/Entity.Serialization.cs b/Sources/AngouriMath/Core/Serialization/Entity.Serialization.cs
index 6cbef6aa1..c0f7a6f05 100644
--- a/Sources/AngouriMath/Core/Serialization/Entity.Serialization.cs
+++ b/Sources/AngouriMath/Core/Serialization/Entity.Serialization.cs
@@ -135,6 +135,7 @@ [EntityJsonConverter] partial record SpecialSet
[EntityJsonConverter] partial record Sumf;
[EntityJsonConverter] partial record Summationf;
[EntityJsonConverter] partial record Maximumf;
+ [EntityJsonConverter] partial record SumOverSetf;
[EntityJsonConverter] partial record Minimumf;
[EntityJsonConverter] partial record Argmaxf;
[EntityJsonConverter] partial record Argminf;
diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md
index 0674a0f60..ab54245aa 100644
--- a/Sources/AngouriMath/Docs/Usage/Syntax.md
+++ b/Sources/AngouriMath/Docs/Usage/Syntax.md
@@ -328,8 +328,9 @@ undefined at `0`; `Si(+oo)` is `pi / 2` and `Ci(+oo)` is `0`.
**Calculus** — `derivative(expr, var, order)`, `integral(expr, var)`,
`integral(expr, var, from, to)`, `limit(expr, var, dest)`, `limitleft(...)`, `limitright(...)`;
-`max(expr, var in set)` and `min` for the extremum of an expression over a set, `argmax` and
-`argmin` for the set of points where it is taken — `max(a, b)` of two values is still the larger.
+`sum(expr, var in set)` for a sum over a set; `max(expr, var in set)` and `min` for the extremum of
+an expression over a set, `argmax` and `argmin` for the set of points where it is taken —
+`max(a, b)` of two values is still the larger.
`min(S)` with one argument that is a set is its least member, `min(x, x in S)`, and the range
may carry conditions, `min(x, x in PP and x > 14)` being the least of `{ x in PP : x > 14 }`;
decided for `PP`, `ZZ+`, `ZZ*` and their cuts by an interval or bounds (`min(ZZ+)` is `1`,
@@ -487,6 +488,18 @@ at `c = 0`, where the empty product is `1`. Where the index appears in the body
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.
+`sum(body, name in set)` is a sum **over a set**: the body at each member of the set, each member
+counted once, added up. `sum(x^2, x in {1, 2, 3})` is `14`. A sum over the roots of a polynomial is a
+sum over its set of roots, `sum(w^2, w in { w : w^3 - 2w + 1 = 0 })`, which is `4`, and it needs no
+index and no order, a set having neither. The terms are added up where the set is finite and its
+members are known: a listed set of numbers, or roots that are rational, roots of a quadratic or roots
+of a binomial, which is checked by counting them against the polynomial's number of distinct roots.
+The roots of an irreducible cubic or quartic are not written out in radicals, which read no more
+simply than the sum. A set whose members might coincide,
+such as `{ a, b }`, and a set that is not finite are left as written. Evaluated to a number, roots
+the solver cannot write are found numerically, all of them to the working precision or none, so
+`sum(w^5, w in { w : w^5 + w + 3 = 0 })` evaluates to `-15`.
+
**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:
diff --git a/Sources/AngouriMath/Functions/Algebra/SumOverSet.cs b/Sources/AngouriMath/Functions/Algebra/SumOverSet.cs
new file mode 100644
index 000000000..ccb8764b1
--- /dev/null
+++ b/Sources/AngouriMath/Functions/Algebra/SumOverSet.cs
@@ -0,0 +1,232 @@
+//
+// 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;
+using static AngouriMath.Entity.Set;
+
+namespace AngouriMath.Functions
+{
+ ///
+ /// The value of a sum over a set, sum(f(x), x in S): the terms written out and added
+ /// where the members of S are known and there are finitely many of them, and the sum
+ /// left as written otherwise.
+ ///
+ ///
+ ///
+ /// A sum over a set counts each member once, which is what makes it well defined without an
+ /// order: a set has no first member, and addition does not care. So the members have to be
+ /// known to be distinct before they are added, and a listed set is read only where its
+ /// elements are numbers. sum(x, x in { a, b }) is a + b when a and
+ /// b differ and a when they are equal, and nothing here can tell which, so it is
+ /// left as written.
+ ///
+ ///
+ /// The sum over the roots of a polynomial is the case the node exists for:
+ /// sum(f(w), w in { w : p(w) = 0 }). Where every root is rational, a root of a
+ /// quadratic or a root of a binomial, the solver writes them and they are added; every
+ /// is checked, by counting the distinct roots it gave against the degree of the square-free
+ /// part of p, which is how many distinct roots p has. The roots of an
+ /// irreducible cubic or quartic are not written out in radicals, which read no more simply
+ /// than the sum. A root set the solver answers only in part is left as written, since a sum
+ /// that misses a term is a wrong answer. Evaluated to a number, the roots the solver cannot
+ /// write are found to the working precision, all of them or none, by
+ /// . And a sum over a set that is not finite -- an
+ /// interval, the integers -- is a series, which is not this node's to answer.
+ /// #1285
+ ///
+ ///
+ internal static class SumOverSet
+ {
+ ///
+ /// The sum, or where the members of the set are not known or not
+ /// finitely many.
+ ///
+ internal static Entity? Value(Entity expression, Entity var, Entity over, bool isExact)
+ {
+ if (var is not Variable x || over.InnerSimplified is not Set set)
+ return null;
+ // A numeric value needs the roots as numbers, and those do not need the solver: asking
+ // it first would be a solve per evaluation, and a check evaluates at many points.
+ var members = isExact ? Members(set) : NumericMembers(set) ?? Members(set);
+ if (members is null || members.Count > Summationf.MaxExpandedTerms)
+ return null;
+ Entity? sum = null;
+ foreach (var member in members)
+ {
+ var term = expression.Substitute(x, member);
+ sum = sum is null ? term : sum + term;
+ }
+ sum ??= Integer.Zero;
+ return isExact ? sum.InnerSimplified : sum.Evaled;
+ }
+
+ ///
+ /// The members of , each once, or where they
+ /// are not known to be distinct or there are not finitely many.
+ ///
+ private static IReadOnlyList? Members(Set set)
+ => set switch
+ {
+ FiniteSet finite => Distinct(finite.Elements),
+ // { w in S : p(w) = 0 }, which the set builder holds as w in S and p(w) = 0: the
+ // roots that are members of S.
+ ConditionalSet { Var: Variable w } builder when builder.DeclaredMembership is (Set universe, Equalsf(var left, var right))
+ => Roots(left - right, w) is { } roots ? Within(roots, universe) : null,
+ ConditionalSet { Var: Variable w, Predicate: Equalsf(var left, var right) } => Roots(left - right, w),
+ _ => null,
+ };
+
+ ///
+ /// The members of that are in , or
+ /// where that is not decided for one of them.
+ ///
+ private static IReadOnlyList? Within(IReadOnlyList members, Set universe)
+ {
+ var inside = new List();
+ foreach (var member in members)
+ {
+ if (!universe.TryContains(member, out var contains))
+ return null;
+ if (contains)
+ inside.Add(member);
+ }
+ return inside;
+ }
+
+ ///
+ /// The elements, where each is a number and no two are equal;
+ /// otherwise. Two numbers written differently can be the same number, so they are
+ /// compared by value, and a pair that agrees to the working precision is taken for one
+ /// that may be equal and the set is not read.
+ ///
+ private static IReadOnlyList? Distinct(IEnumerable elements)
+ {
+ var members = new List();
+ var values = new List();
+ foreach (var element in elements)
+ {
+ if (element.Evaled is not Complex value || !value.IsFinite)
+ return null;
+ foreach (var seen in values)
+ if (seen == value)
+ return null;
+ members.Add(element);
+ values.Add(value);
+ }
+ return members;
+ }
+
+ ///
+ /// The roots of the polynomial in , each
+ /// once, where the solver writes all of them; otherwise.
+ ///
+ ///
+ /// Only roots that read as simply as the sum does are written out: rational ones, the
+ /// roots of a quadratic, and the roots of a binomial. The roots of an irreducible cubic or
+ /// quartic in radicals are no simpler than the sum over them, and for a real polynomial
+ /// with three real roots Cardano's formula writes them with complex radicals, so that sum
+ /// is kept as it is; its value is still found numerically. The solver is not asked for
+ /// anything past that, which also spares a search every time a simplification meets the
+ /// sum.
+ ///
+ private static IReadOnlyList? Roots(Entity polynomial, Variable w)
+ {
+ if (SquareFreeParts(polynomial, w) is not { } parts)
+ return null;
+ var expected = 0;
+ foreach (var part in parts)
+ expected += part.Factor.Degree;
+ if (expected > 2 && !(PolynomialFactorization.FactorComplete(polynomial, w) is { } factored
+ && factored.Parts.All(static part => part.Factor.Degree <= 2 || IsBinomial(part.Factor))))
+ return null;
+ if (polynomial.SolveEquation(w) is not FiniteSet solutions
+ || Distinct(solutions.Elements.Select(static root => root.InnerSimplified)) is not { } roots)
+ return null;
+ return roots.Count == expected ? roots : null;
+ }
+
+ ///
+ /// The members of a set of roots to the working precision, where the solver cannot write
+ /// them: each square-free part of the polynomial by ,
+ /// all its roots or none. The parts share no root, so the roots are distinct.
+ ///
+ private static IReadOnlyList? NumericMembers(Set set)
+ => set switch
+ {
+ ConditionalSet { Var: Variable w } builder when builder.DeclaredMembership is (Set universe, Equalsf(var left, var right))
+ => NumericRoots(left - right, w) is { } roots ? Within(roots, universe) : null,
+ ConditionalSet { Var: Variable w, Predicate: Equalsf(var left, var right) } => NumericRoots(left - right, w),
+ _ => null,
+ };
+
+ ///
+ /// The roots of in to the working
+ /// precision, each once, all of them or .
+ ///
+ internal static IReadOnlyList? NumericRoots(Entity polynomial, Variable w)
+ {
+ if (SquareFreeParts(polynomial, w) is not { } parts)
+ return null;
+ var context = MathS.Settings.DecimalPrecisionContext.Value;
+ var roots = new List();
+ foreach (var part in parts)
+ {
+ if (part.Factor.Degree < 1)
+ continue;
+ if (Algebra.NumericalSolving.DurandKerner.Roots(part.Factor, context) is not { } found)
+ return null;
+ roots.AddRange(found);
+ }
+ return roots;
+ }
+
+ ///
+ /// The square-free decomposition of in ,
+ /// where it is a polynomial with rational coefficients of degree at least one; its parts'
+ /// degrees add up to the number of distinct roots. otherwise.
+ ///
+ ///
+ /// Read as reads a polynomial, off the tree, so the
+ /// coefficients are the rationals written rather than values evaluated: with downcasting
+ /// off an evaluated 3 is a decimal and not the rational 3, and a reading
+ /// through evaluation would refuse the polynomial -- in the setting numerical checks
+ /// run in.
+ ///
+ internal static IReadOnlyList? SquareFreeParts(Entity polynomial, Variable w)
+ {
+ if (MultivariatePolynomial.TryParse(polynomial, new Dictionary { [w] = 0 }) is not { } parsed)
+ return null;
+ var degree = parsed.DegreeIn(0);
+ if (degree < 1 || degree > IntegerPolynomial.MaxDegree)
+ return null;
+ var rational = new ERational[degree + 1];
+ for (var i = 0; i < rational.Length; i++)
+ rational[i] = ERational.Zero;
+ foreach (var term in parsed.Terms)
+ rational[MultivariatePolynomial.PowerOf(term.Key, 0)] = term.Value;
+ var denominator = EInteger.One;
+ foreach (var coefficient in rational)
+ denominator = denominator.Divide(denominator.Gcd(coefficient.Denominator)).Multiply(coefficient.Denominator);
+ var whole = new EInteger[rational.Length];
+ for (var i = 0; i < whole.Length; i++)
+ whole[i] = rational[i].Numerator.Multiply(denominator.Divide(rational[i].Denominator));
+ var poly = IntegerPolynomial.Create(whole);
+ return poly.Degree < 1 ? null : SquareFreeDecomposition.Decompose(poly.PrimitivePart());
+ }
+
+ private static bool IsBinomial(IntegerPolynomial poly)
+ {
+ var terms = 0;
+ for (var power = 0; power <= poly.Degree; power++)
+ if (!poly[power].IsZero)
+ terms++;
+ return terms == 2;
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
index 5a6a72fd7..9d16d3b6d 100644
--- a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
@@ -476,6 +476,22 @@ protected override Entity InnerDifferentiate(Variable variable) =>
}
#pragma warning restore IDE0054 // Use compound assignment
+ partial record SumOverSetf
+ {
+ ///
+ ///
+ /// Term by term, where the set does not depend on the variable: the derivative of a
+ /// sum of finitely many terms is the sum of their derivatives, and a sum over a set
+ /// that is not finite is not evaluated in the first place. The name the sum binds is
+ /// not a free variable of it, so nothing varies with it.
+ /// https://github.com/asc-community/AngouriMath/issues/1285
+ ///
+ protected override Entity InnerDifferentiate(Variable variable) =>
+ Var == variable ? Integer.Zero
+ : Over.ContainsNode(variable) ? base.InnerDifferentiate(variable)
+ : New(Expression.InnerDifferentiate(variable), Var, Over);
+ }
+
partial record Limitf
{
///
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs
index 3b1a9a60d..9f84d96cf 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs
@@ -329,6 +329,13 @@ partial record Integralf
: Enumerable.Empty();
}
+ partial record SumOverSetf
+ {
+ // The unknown sits under a binder; see Summationf below.
+ private protected override IEnumerable? InvertNode(Entity value, Entity x) =>
+ null;
+ }
+
partial record Maximumf
{
// The unknown sits under a binder; see Summationf below.
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/DurandKerner.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/DurandKerner.cs
new file mode 100644
index 000000000..e11a1f7be
--- /dev/null
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/DurandKerner.cs
@@ -0,0 +1,136 @@
+//
+// 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 System;
+using PeterO.Numbers;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Functions.Algebra.NumericalSolving
+{
+ ///
+ /// Every root of a square-free polynomial with whole coefficients, to the working precision,
+ /// by the Durand–Kerner iteration: each approximation moves by p(z_i) / prod_{j != i}
+ /// (z_i - z_j), which is Newton's step for the root it is nearest once the others are
+ /// close to theirs, so the iteration converges to all the roots at once and quadratically at
+ /// the end.
+ ///
+ ///
+ ///
+ /// All the roots or none: a sum over the roots of a polynomial that is missing one is a wrong
+ /// answer, so where the iteration does not settle, or settles on two approximations too close
+ /// to tell apart, there is no answer. A square-free polynomial has no repeated root, so two
+ /// approximations that close mean the iteration has not separated them.
+ ///
+ ///
+ /// The coefficients are real, so the non-real roots come in conjugate pairs, and a root whose
+ /// imaginary part is smaller than half the separation that was checked is real: were it not,
+ /// it and its conjugate would be closer together than that. It is written as a real number
+ /// then, rather than as one with an imaginary part of 1e-110 that no root has.
+ ///
+ /// #1285
+ ///
+ internal static class DurandKerner
+ {
+ /// Digits carried beyond the working precision, and dropped at the end.
+ private const int GuardDigits = 10;
+
+ /// How many sweeps over all the roots before the iteration is taken not to settle.
+ private const int MaxSweeps = 1000;
+
+ ///
+ /// The roots of , which has degree at least one and no repeated
+ /// root, at the precision of ; where they
+ /// are not settled.
+ ///
+ internal static IReadOnlyList? Roots(IntegerPolynomial squareFree, EContext context)
+ {
+ var degree = squareFree.Degree;
+ if (degree < 1)
+ return null;
+ var digits = context.Precision.ToInt32Checked();
+ var work = context.WithPrecision(digits + GuardDigits);
+ var lead = EDecimal.FromEInteger(squareFree[degree]);
+ var monic = new EDecimal[degree];
+ for (var i = 0; i < degree; i++)
+ monic[i] = EDecimal.FromEInteger(squareFree[i]).Divide(lead, work);
+
+ // Cauchy's bound: every root is within 1 + max |a_i / a_n| of zero. The approximations
+ // start spread round that circle, turned off the axes so that no two start as a
+ // conjugate pair or on a line of symmetry the iteration cannot leave.
+ var radius = EDecimal.One;
+ foreach (var coefficient in monic)
+ radius = EDecimal.Max(radius, EDecimal.One.Add(coefficient.Abs(), work));
+ var r = radius.ToDouble();
+ var re = new EDecimal[degree];
+ var im = new EDecimal[degree];
+ for (var i = 0; i < degree; i++)
+ {
+ var angle = 2 * Math.PI * i / degree + 0.4;
+ re[i] = EDecimal.FromDouble(r * Math.Cos(angle));
+ im[i] = EDecimal.FromDouble(r * Math.Sin(angle));
+ }
+
+ var tolerance = EDecimal.Create(EInteger.One, EInteger.FromInt32(-(digits + 2))).Multiply(radius, work);
+ var settled = false;
+ for (var sweep = 0; sweep < MaxSweeps && !settled; sweep++)
+ {
+ var largest = EDecimal.Zero;
+ for (var i = 0; i < degree; i++)
+ {
+ // p(z_i) by Horner's scheme, on the monic polynomial.
+ var (pRe, pIm) = (EDecimal.One, EDecimal.Zero);
+ for (var k = degree - 1; k >= 0; k--)
+ {
+ (pRe, pIm) = Times(pRe, pIm, re[i], im[i], work);
+ pRe = pRe.Add(monic[k], work);
+ }
+ var (qRe, qIm) = (EDecimal.One, EDecimal.Zero);
+ for (var j = 0; j < degree; j++)
+ if (j != i)
+ (qRe, qIm) = Times(qRe, qIm, re[i].Subtract(re[j], work), im[i].Subtract(im[j], work), work);
+ if (qRe.IsZero && qIm.IsZero)
+ return null;
+ var (stepRe, stepIm) = Over(pRe, pIm, qRe, qIm, work);
+ re[i] = re[i].Subtract(stepRe, work);
+ im[i] = im[i].Subtract(stepIm, work);
+ largest = EDecimal.Max(largest, stepRe.Abs().Add(stepIm.Abs(), work));
+ }
+ settled = largest.CompareTo(tolerance) <= 0;
+ }
+ if (!settled)
+ return null;
+
+ // Distinct to half the working precision, which a square-free polynomial's roots are
+ // unless the iteration has not separated two of them.
+ var separation = EDecimal.Create(EInteger.One, EInteger.FromInt32(-(digits / 2)));
+ for (var i = 0; i < degree; i++)
+ for (var j = i + 1; j < degree; j++)
+ if (re[i].Subtract(re[j], work).Abs().Add(im[i].Subtract(im[j], work).Abs(), work).CompareTo(separation) <= 0)
+ return null;
+
+ var half = separation.Divide(EDecimal.FromInt32(2), work);
+ var roots = new List(degree);
+ for (var i = 0; i < degree; i++)
+ {
+ var imaginary = im[i].Abs().CompareTo(half) < 0 ? EDecimal.Zero : im[i].RoundToPrecision(context);
+ roots.Add(Complex.Create(re[i].RoundToPrecision(context), imaginary));
+ }
+ return roots;
+ }
+
+ private static (EDecimal Re, EDecimal Im) Times(EDecimal aRe, EDecimal aIm, EDecimal bRe, EDecimal bIm, EContext work)
+ => (aRe.Multiply(bRe, work).Subtract(aIm.Multiply(bIm, work), work),
+ aRe.Multiply(bIm, work).Add(aIm.Multiply(bRe, work), work));
+
+ private static (EDecimal Re, EDecimal Im) Over(EDecimal aRe, EDecimal aIm, EDecimal bRe, EDecimal bIm, EContext work)
+ {
+ var norm = bRe.Multiply(bRe, work).Add(bIm.Multiply(bIm, work), work);
+ return (aRe.Multiply(bRe, work).Add(aIm.Multiply(bIm, work), work).Divide(norm, work),
+ aIm.Multiply(bRe, work).Subtract(aRe.Multiply(bIm, work), work).Divide(norm, work));
+ }
+ }
+}
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 dba589426..877c145a0 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
@@ -208,6 +208,25 @@ protected override Entity InnerSimplify(bool isExact)
=> Functions.ExtremumOverSet.Points(Expression, Var, Over, largest: false) ?? this;
}
+ partial record SumOverSetf
+ {
+ ///
+ /// A sum over a set is a well-formed expression whatever the set, as
+ /// is whatever its bounds; adding its terms up is an
+ /// evaluation, not a claim about where it is defined.
+ ///
+ private protected override Entity IntrinsicCondition => Boolean.True;
+ ///
+ ///
+ /// Where the terms are not added up, the sum is still written in its simplest parts, so
+ /// that a derivative taken term by term reads as one: 2 y w and not
+ /// 0 * y ^ 2 + 2 * y ^ 1 * 1 * w.
+ ///
+ protected override Entity InnerSimplify(bool isExact)
+ => Functions.SumOverSet.Value(Expression, Var, Over, isExact)
+ ?? New(Expression.InnerSimplified, Var, Over.InnerSimplified);
+ }
+
// TODO: rewrite this part too
public partial record Summationf
{
diff --git a/Sources/AngouriMath/Functions/Output/Latex/Latex.Calculus.Classes.cs b/Sources/AngouriMath/Functions/Output/Latex/Latex.Calculus.Classes.cs
index 195cde5c7..2bd05f539 100644
--- a/Sources/AngouriMath/Functions/Output/Latex/Latex.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/Latex/Latex.Calculus.Classes.cs
@@ -66,6 +66,14 @@ private protected override string LatexizeNode()
}
}
+ public partial record SumOverSetf
+ {
+ ///
+ private protected override string LatexizeNode() =>
+ @"\sum_{" + Var.Latexize() + @" \in " + Over.Latexize() + "} "
+ + Expression.Latexize(Expression.Priority < Priority.Sum);
+ }
+
public partial record Maximumf
{
///
diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Calculus.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Calculus.Classes.cs
index 6f8ab28a0..5962c76d2 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Calculus.Classes.cs
@@ -36,6 +36,15 @@ Range is var (from, to)
public override string ToString() => Stringize();
}
+ public partial record SumOverSetf
+ {
+ ///
+ private protected override string StringizeNode() =>
+ $"sum({Expression.Stringize()}, {Var.Stringize()} in {Over.Stringize()})";
+ ///
+ public override string ToString() => Stringize();
+ }
+
public partial record Maximumf
{
///
diff --git a/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Calculus.Classes.cs b/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Calculus.Classes.cs
index f6025d275..c98eb5f8e 100644
--- a/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Calculus.Classes.cs
@@ -21,6 +21,20 @@ public partial record Integralf
internal override string ToSymPy() => $"sympy.integrate({Expression.ToSymPy()}, {(Range is var (from, to) ? $"({Var.ToSymPy()}, {from.ToSymPy()}, {to.ToSymPy()})" : Var.ToSymPy())})";
}
+ public partial record SumOverSetf
+ {
+ // SymPy's RootSum counts a repeated root as often as it is repeated, and a set counts
+ // it once, so the polynomial goes over as its square-free part.
+ internal override string ToSymPy() => Over switch
+ {
+ Set.FiniteSet finite =>
+ $"sympy.Add(*[({Expression.ToSymPy()}).subs({Var.ToSymPy()}, member) for member in [{string.Join(", ", finite.Elements.Select(static e => e.ToSymPy()))}]])",
+ Set.ConditionalSet { Var: Variable w, Predicate: Equalsf(var left, var right) } when w == Var =>
+ $"sympy.RootSum(sympy.sqf_part(sympy.Poly({(left - right).ToSymPy()}, {Var.ToSymPy()})), sympy.Lambda({Var.ToSymPy()}, {Expression.ToSymPy()}))",
+ _ => throw new NotSufficientlySupportedException("SymPy has no sum over a set; a listed set or the roots of a polynomial can be exported"),
+ };
+ }
+
public partial record Maximumf
{
internal override string ToSymPy() =>
diff --git a/Sources/AngouriMath/Functions/Substitute.cs b/Sources/AngouriMath/Functions/Substitute.cs
index a77cf72d5..af6b6af44 100644
--- a/Sources/AngouriMath/Functions/Substitute.cs
+++ b/Sources/AngouriMath/Functions/Substitute.cs
@@ -637,6 +637,20 @@ public override Entity Substitute(Entity x, Entity value)
}
}
+ partial record SumOverSetf
+ {
+ ///
+ // The variable is bound; renamed first, as a summation's index is.
+ public override Entity Substitute(Entity x, Entity value)
+ {
+ if (this == x)
+ return value;
+ var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
+ var renamed = Expression.Substitute(Var, replacement).Substitute(x, value).Substitute(replacement, Var);
+ return New(renamed, Var, Over.Substitute(x, value));
+ }
+ }
+
partial record Maximumf
{
///
diff --git a/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs b/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs
index a8c5f4c5d..bd4346add 100644
--- a/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs
+++ b/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs
@@ -201,6 +201,11 @@ public partial record Productf
private protected override string SortHashName(SortLevel level) => "productf_";
}
+ public partial record SumOverSetf
+ {
+ private protected override string SortHashName(SortLevel level) => "sumoversetf_";
+ }
+
public partial record Maximumf
{
private protected override string SortHashName(SortLevel level) => "maximumf_";
diff --git a/Sources/Tests/UnitTests/Calculus/SumOverSetTest.cs b/Sources/Tests/UnitTests/Calculus/SumOverSetTest.cs
new file mode 100644
index 000000000..c845783d1
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/SumOverSetTest.cs
@@ -0,0 +1,139 @@
+//
+// 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 PeterO.Numbers;
+using Xunit;
+using static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// sum(f(x), x in S): the sum of an expression over the members of a set, each counted
+ /// once, as a binder. https://github.com/asc-community/AngouriMath/issues/1285
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class SumOverSetTest
+ {
+ [Fact]
+ public void ItIsWrittenAsItIsRead()
+ {
+ var sum = "sum(x^2, x in {1, 2, 3})".ToEntity();
+ Assert.IsType(sum);
+ Assert.Equal("sum(x ^ 2, x in { 1, 2, 3 })", sum.Stringize());
+ Assert.Equal(sum, sum.Stringize().ToEntity());
+ }
+
+ /// The range form keeps its meaning, and two arguments that are not a range are the error they were.
+ [Fact]
+ public void TheRangeFormIsUnchanged()
+ {
+ Assert.IsType("sum(k, k, 1, 10)".ToEntity());
+ Assert.Equal(Integer.Create(55), "sum(k, k, 1, 10)".ToEntity().Simplify());
+ Assert.ThrowsAny(() => "sum(x, y)".ToEntity());
+ }
+
+ [Theory]
+ [InlineData("sum(x^2, x in {1, 2, 3})", "14")]
+ [InlineData("sum(x, x in {})", "0")]
+ [InlineData("sum(7, x in {1, 2, 3})", "21")]
+ // An integer range is listed, so the set form agrees with the range form.
+ [InlineData("sum(k, k in ZZ intersect [1; 10])", "55")]
+ [InlineData("sum(w^2, w in { w : w^3 - 2w + 1 = 0 })", "4")]
+ [InlineData("sum(w, w in { w : w^2 + 1 = 0 })", "0")]
+ [InlineData("sum(1/w, w in { w : w^2 - 3w + 2 = 0 })", "3/2")]
+ // Over the reals only the real roots count: w^4 - 1 has 1 and -1 there, and i and -i besides.
+ [InlineData("sum(w^2, w in { w in RR : w^4 - 1 = 0 })", "2")]
+ [InlineData("sum(w^2, w in { w : w^4 - 1 = 0 })", "0")]
+ public void TheTermsAreAddedUp(string sum, string value)
+ => Assert.Equal(value.ToEntity(), sum.ToEntity().Simplify());
+
+ /// A set counts a member once, so a double root is one term.
+ [Fact]
+ public void ARepeatedRootIsOneMember()
+ => Assert.Equal(Integer.Create(3), "sum(w, w in { w : (w - 1)^2 (w - 2) = 0 })".ToEntity().InnerSimplified);
+
+ ///
+ /// Left as written: members that may coincide, a set that is not finite, and roots the
+ /// solver cannot write, which are not added up symbolically.
+ ///
+ [Theory]
+ [InlineData("sum(x, x in {a, b})")]
+ [InlineData("sum(x, x in [0; 1])")]
+ [InlineData("sum(k, k in ZZ)")]
+ [InlineData("sum(w, w in { w : w^5 + w + 3 = 0 })")]
+ // An irreducible cubic: Cardano would write its roots in complex radicals, which read no
+ // more simply than the sum.
+ [InlineData("sum(w^2, w in { w : w^3 + w + 1 = 0 })")]
+ public void WhatItCannotListIsLeftAsWritten(string sum)
+ => Assert.IsType(sum.ToEntity().InnerSimplified);
+
+ ///
+ /// Evaluated to a number, the roots the solver cannot write are found numerically, all of
+ /// them, and the sum agrees with the exact value the coefficients give: for
+ /// w^5 + w + 3 the roots add to 0, their reciprocals to -1/3, and
+ /// their fifth powers to -15, since each root has w^5 = -w - 3.
+ ///
+ [Theory]
+ [InlineData("sum(w, w in { w : w^5 + w + 3 = 0 })", "0")]
+ [InlineData("sum(1/w, w in { w : w^5 + w + 3 = 0 })", "-1/3")]
+ [InlineData("sum(w^5, w in { w : w^5 + w + 3 = 0 })", "-15")]
+ // The cubic kept as a sum still has its value: the squares of its roots add to -2.
+ [InlineData("sum(w^2, w in { w : w^3 + w + 1 = 0 })", "-2")]
+ public void NumericallyOverRootsTheSolverCannotWrite(string sum, string exact)
+ {
+ AssertClose(exact.ToEntity().Evaled, sum.ToEntity().Evaled);
+ // And evaluated with downcasting off, as a numerical check evaluates an answer that was
+ // built as usual: parsed afresh, since a cached entity would hand back the value it was
+ // evaluated to with downcasting on, and parsed outside the scope, since there a literal
+ // 3 is a decimal and nothing is a polynomial with rational coefficients.
+ var (fresh, reference) = (MathS.FromString(sum, useCache: false), MathS.FromString(exact, useCache: false));
+ using var _ = MathS.Settings.DowncastingEnabled.Set(false);
+ AssertClose(reference.Evaled, fresh.Evaled);
+ }
+
+ private static void AssertClose(Entity expected, Entity actual)
+ {
+ var e = Assert.IsAssignableFrom(expected);
+ var a = Assert.IsAssignableFrom(actual);
+ var tolerance = EDecimal.FromString("1E-50");
+ Assert.True(e.RealPart.EDecimal.Subtract(a.RealPart.EDecimal).Abs().CompareTo(tolerance) < 0, $"{a} is not {e}");
+ Assert.True(e.ImaginaryPart.EDecimal.Subtract(a.ImaginaryPart.EDecimal).Abs().CompareTo(tolerance) < 0, $"{a} is not {e}");
+ }
+
+ /// The name ranges over the set, so the sum is not a function of it.
+ [Fact]
+ public void TheNameIsBound()
+ {
+ var sum = "sum(w * a, w in { w : w^5 + w + 3 = 0 })".ToEntity();
+ Assert.Equal(new[] { MathS.Var("a") }, sum.FreeVariables);
+ Assert.Equal(sum, sum.Substitute("w", 5));
+ Assert.Equal("sum(w * 2, w in { w : w ^ 5 + w + 3 = 0 })".ToEntity(), sum.Substitute("a", 2));
+ }
+
+ ///
+ /// Term by term where the set does not depend on the variable, and zero in the bound name:
+ /// the derivative of y^2 times the fifth powers of the roots, which add to
+ /// -15, is a sum over the same set with the value -30 y.
+ ///
+ [Fact]
+ public void ItIsDifferentiatedTermByTerm()
+ {
+ var sum = "sum(w^5 * y^2, w in { w : w^5 + w + 3 = 0 })".ToEntity();
+ var derivative = sum.Differentiate("y");
+ Assert.IsType(derivative);
+ AssertClose(Integer.Create(-30), derivative.Substitute("y", 1).Evaled);
+ Assert.Equal(Integer.Zero, sum.Differentiate("w"));
+ }
+
+ [Fact]
+ public void ItPrintsAsASumUnderItsRange()
+ => Assert.Equal(@"\sum_{x \in \left\{ 1, 2, 3 \right\}} {x}^{2}", "sum(x^2, x in {1, 2, 3})".ToEntity().Latexize());
+ }
+}
diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt
index d7302cae4..a7e36a4ca 100644
--- a/Sources/Tests/UnitTests/Common/PublicApi.txt
+++ b/Sources/Tests/UnitTests/Common/PublicApi.txt
@@ -2618,6 +2618,25 @@ AngouriMath.Entity+Statement.ctor()
AngouriMath.Entity+Statement.ctor(AngouriMath.Entity+Statement)
AngouriMath.Entity+Statement.op_Equality(AngouriMath.Entity+Statement, AngouriMath.Entity+Statement) : System.Boolean
AngouriMath.Entity+Statement.op_Inequality(AngouriMath.Entity+Statement, AngouriMath.Entity+Statement) : System.Boolean
+AngouriMath.Entity+SumOverSetf.$() : AngouriMath.Entity+SumOverSetf
+AngouriMath.Entity+SumOverSetf.Codomain { } : AngouriMath.Core.Domain
+AngouriMath.Entity+SumOverSetf.Deconstruct(AngouriMath.Entity&, AngouriMath.Entity&, AngouriMath.Entity&) : System.Void
+AngouriMath.Entity+SumOverSetf.EqualityContract { } : System.Type
+AngouriMath.Entity+SumOverSetf.Equals(AngouriMath.Entity+CalculusOperator) : System.Boolean
+AngouriMath.Entity+SumOverSetf.Equals(AngouriMath.Entity+SumOverSetf) : System.Boolean
+AngouriMath.Entity+SumOverSetf.Equals(System.Object) : System.Boolean
+AngouriMath.Entity+SumOverSetf.GetHashCode() : System.Int32
+AngouriMath.Entity+SumOverSetf.InitDirectChildren() : AngouriMath.Entity[]
+AngouriMath.Entity+SumOverSetf.InnerDifferentiate(AngouriMath.Entity+Variable) : AngouriMath.Entity
+AngouriMath.Entity+SumOverSetf.InnerSimplify(System.Boolean) : AngouriMath.Entity
+AngouriMath.Entity+SumOverSetf.Over { } : AngouriMath.Entity
+AngouriMath.Entity+SumOverSetf.PrintMembers(System.Text.StringBuilder) : System.Boolean
+AngouriMath.Entity+SumOverSetf.Replace(System.Func) : AngouriMath.Entity
+AngouriMath.Entity+SumOverSetf.Substitute(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity
+AngouriMath.Entity+SumOverSetf.ToString() : System.String
+AngouriMath.Entity+SumOverSetf.ctor(AngouriMath.Entity, AngouriMath.Entity, AngouriMath.Entity)
+AngouriMath.Entity+SumOverSetf.op_Equality(AngouriMath.Entity+SumOverSetf, AngouriMath.Entity+SumOverSetf) : System.Boolean
+AngouriMath.Entity+SumOverSetf.op_Inequality(AngouriMath.Entity+SumOverSetf, AngouriMath.Entity+SumOverSetf) : System.Boolean
AngouriMath.Entity+Sumf.$() : AngouriMath.Entity+Sumf
AngouriMath.Entity+Sumf.Addend { } : AngouriMath.Entity
AngouriMath.Entity+Sumf.Augend { } : AngouriMath.Entity
@@ -3235,6 +3254,7 @@ AngouriMath.MathS.SolveEquation(AngouriMath.Entity, AngouriMath.Entity+Variable)
AngouriMath.MathS.SolveOde(AngouriMath.Entity, AngouriMath.Entity+Variable, AngouriMath.Entity+Variable) : AngouriMath.Entity
AngouriMath.MathS.Sqr(AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.MathS.Sqrt(AngouriMath.Entity) : AngouriMath.Entity
+AngouriMath.MathS.Sum(AngouriMath.Entity, AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.MathS.Sum(AngouriMath.Entity, AngouriMath.Entity, AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.MathS.Tan(AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.MathS.ToBaseN(AngouriMath.Entity+Number+Real, System.Int32) : System.String
@@ -3425,6 +3445,7 @@ class AngouriMath.Entity+Sif
class AngouriMath.Entity+Signumf
class AngouriMath.Entity+Sinf
class AngouriMath.Entity+Statement
+class AngouriMath.Entity+SumOverSetf
class AngouriMath.Entity+Sumf
class AngouriMath.Entity+Summationf
class AngouriMath.Entity+Tanf