Skip to content

Commit 86af577

Browse files
A congruence is solved to a residue class, two classes meet by the Chinese remainder theorem, the modular inverse is a class representative, lcm is a function, and a quantified statement over the whole numbers is decided by its residues (#1424)
Item 3 of the #1409 docket, chapter 6's number theory on top of the congruence node, and #180's "proper modular arithmetic". A residue class is the set { x in ZZ : x = r (mod n) }, r in [0, n), and everything here is arithmetic on those. Solve reads a x + b = c (mod n) through the gcd: solutions exactly when gcd(a, n) divides c - b, one class modulo n / gcd -- 3 x = 11 (mod 7) is { x in ZZ : x = 6 (mod 7) }, 2 x = 4 (mod 6) is x = 2 (mod 3), 2 x = 3 (mod 6) has none -- and a congruence of higher degree by the residues that satisfy it. Two classes intersect by the Chinese remainder theorem, with the coprime case as the case where the consistency condition a = b (mod gcd) is empty: x = 3 (mod 4) and x = 5 (mod 6) is x = 11 (mod 12), x = 3 (mod 4) and x = 2 (mod 6) is nothing, x = 2 (mod 6) and x = 5 (mod 9) is x = 14 (mod 18). A class cut down by a bounded interval is the finite set of its members, so the book's coins -- x = 1 (2), 1 (3), 2 (5), between 250 and 300 -- come out as { 277 }; an intersection A /\ [a; +oo) /\ (-oo; b] is regrouped so that the two intervals meet first. MathS.NumberTheory.ModularInverse(a, n) is the one representative in [1, n - 1], or null where a and n share a factor: a class, never 1/a. lcm(a, b, ...) is a node by gcd's conventions -- non-negative, 0 where either is 0, lcm(a/b, c/d) = lcm(a, c) / gcd(b, d) -- where it was refused by name. Two more routes for a quantifier over ZZ, ZZ* or ZZ+. A body that is a divisibility by a whole number or a congruence modulo one, of polynomials with whole coefficients, joined by the connectives, repeats with the modulus, so its residues decide it: forall n in ZZ : 6 divides n^3 + 5 n is six cases (Ex 6.5.14), forall p in ZZ : (not 2 divides p and not 3 divides p) implies p^2 = 1 (mod 24) twenty-four, and a nested quantifier over whole numbers is decided at each residue of the outer one. And a polynomial equation with no solution modulo some small m has none in the whole numbers -- exists x, y in ZZ : 3 x^2 - 5 y^2 = 1 is False because x^2 = 2 (mod 5) has no solution (Ex 6.5.27), x^2 - y^2 = 14 by parity -- while a solution modulo every m tried proves nothing and is not read as one. Every spelling but lcm was a parse error in 2.5.0; recorded in BREAKING-CHANGES.md. Part of #1409. Part of #180. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 89d3b0a commit 86af577

35 files changed

Lines changed: 807 additions & 45 deletions

‎BREAKING-CHANGES.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,29 @@ now, which is the one silent change: a variable spelled `forall` or `exists` no
520520
| `forall + 1` | `forall + 1`, a variable named `forall` | `UnhandledParseException` |
521521
| `exists(x)` | `exists * x` | `UnhandledParseException` |
522522

523+
### `lcm` is a function, and a congruence is solved to a residue class
524+
525+
`lcm(a, b, ...)` is a node, `Entity.Lcmf`, by the conventions of `gcd`: non-negative, `0` where
526+
either argument is `0`, `lcm(a/b, c/d) = lcm(a, c) / gcd(b, d)`, and left as written for symbols.
527+
It was refused by name. A linear congruence in one unknown is solved by `Solve` to the residue
528+
class `{ x in ZZ : x = r (mod n) }`, two classes meet by the Chinese remainder theorem — the
529+
non-coprime case included — and a class cut down by a bounded interval is the finite set of its
530+
members; `MathS.NumberTheory.ModularInverse(a, n)` is the representative in `[1, n - 1]` or
531+
`null`. A quantified statement over the whole numbers whose body is a divisibility or a congruence
532+
of polynomials is decided by its residues, and a polynomial equation with no solution modulo a
533+
small `m` is decided to have none ([#1409](https://github.com/asc-community/AngouriMath/issues/1409)).
534+
Every spelling but `lcm` was a parse error in 2.5.0, the congruence node being new since.
535+
536+
| Input | Was (2.5.0) | Now |
537+
|---|---|---|
538+
| `lcm(4, 6)` | `UnrecognizedFunctionParseException` | `12` |
539+
| `lcm(1/2, 1/3)`, `lcm(0, 5)`, `lcm(x, y)` | `UnrecognizedFunctionParseException` | `1`, `0`, `lcm(x, y)` |
540+
| `"3 x = 11 (mod 7)".ToEntity().Solve("x")` | `UnhandledParseException` | `{ x in ZZ : x = 6 (mod 7) }` |
541+
| `"x = 1 (mod 2) and x = 1 (mod 3) and x = 2 (mod 5) and 250 <= x and x <= 300".ToEntity().Solve("x")` | `UnhandledParseException` | `{ 277 }` |
542+
| `"x = 3 (mod 4) and x = 2 (mod 6)".ToEntity().Solve("x")` | `UnhandledParseException` | `{}` |
543+
| `forall n in ZZ : 6 divides n^3 + 5 n` | `UnhandledParseException` | `True` |
544+
| `exists x, y in ZZ : 3 x^2 - 5 y^2 = 1` | `UnhandledParseException` | `False` |
545+
523546
### `binomial(n, k)` is a function
524547

525548
**Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled

‎Sources/.editorconfig‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
276276
[AngouriMath/Functions/Boolean/Quantifiers.cs]
277277
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
278278

279+
[AngouriMath/Functions/NumberTheory/ResidueClasses.cs]
280+
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
281+
282+
[Tests/UnitTests/Core/ResidueClassTest.cs]
283+
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
284+
279285
[Tests/UnitTests/Core/QuantifierTest.cs]
280286
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
281287

‎Sources/AngouriMath/Convenience/MathS.cs‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ public static class NumberTheory
8787
/// </example>
8888
public static Entity Congruent(Entity left, Entity right, Entity modulus) => new Congruentf(left, right, modulus);
8989

90+
/// <summary>
91+
/// The multiplicative inverse of <paramref name="a"/> modulo <paramref name="n"/>:
92+
/// the one representative in <c>[1, n - 1]</c> of the class of <c>x</c> with
93+
/// <c>a x = 1 (mod n)</c>, or <see langword="null"/> where there is none, which is
94+
/// exactly when <c>a</c> and <c>n</c> share a factor. A class, never the number
95+
/// <c>1/a</c>. https://github.com/asc-community/AngouriMath/issues/1409
96+
/// </summary>
97+
/// <example>
98+
/// <code>
99+
/// Console.WriteLine(MathS.NumberTheory.ModularInverse(3, 10));
100+
/// Console.WriteLine(MathS.NumberTheory.ModularInverse(6, 15) is null);
101+
/// </code>
102+
/// Prints
103+
/// <code>
104+
/// 7
105+
/// True
106+
/// </code>
107+
/// </example>
108+
public static Integer? ModularInverse(Integer a, Integer n)
109+
=> Functions.ResidueClasses.Inverse(a.EInteger, n.EInteger) is { } inverse ? Integer.Create(inverse) : null;
110+
90111
/// <summary>
91112
/// Count of all divisors of an integer, including 1 or itself.
92113
/// </summary>
@@ -1280,6 +1301,24 @@ public static Entity VectorLength(Matrix v)
12801301
[MethodImpl(MethodImplOptions.AggressiveInlining)]
12811302
public static Entity Gcd(Entity a, Entity b) => new Gcdf(a, b);
12821303

1304+
/// <summary>
1305+
/// The least common multiple of two numbers, <c>lcm(a, b)</c>: computed over integers
1306+
/// and rationals, non-negative, <c>0</c> where either is <c>0</c>, and left as a node
1307+
/// otherwise. https://github.com/asc-community/AngouriMath/issues/1409
1308+
/// </summary>
1309+
/// <example>
1310+
/// <code>
1311+
/// Console.WriteLine(MathS.Lcm(4, 6).Evaled);
1312+
/// Console.WriteLine("lcm(2, 3, 4)".ToEntity().Evaled);
1313+
/// </code>
1314+
/// Prints
1315+
/// <code>
1316+
/// 12
1317+
/// 12
1318+
/// </code>
1319+
/// </example>
1320+
public static Entity Lcm(Entity a, Entity b) => new Lcmf(a, b);
1321+
12831322
/// <summary>The binomial coefficient <c>binomial(n, k)</c>, "n choose k"</summary>
12841323
/// <param name="n">The upper argument, which need not be a whole number</param>
12851324
/// <param name="k">The lower argument</param>

‎Sources/AngouriMath/Core/Antlr/AngouriMath.g‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ atom returns[Entity value]
558558
| 'argmax(' args = function_arguments ')' { Assert("argmax", 2, $args.list.Count); $value = $args.list[1] is Entity.Set.Inf { Element: Variable } argmaxRange ? MathS.Argmax($args.list[0], argmaxRange.Element, argmaxRange.SupSet) : throw new InvalidArgumentParseException("argmax expects its second argument to say which variable ranges over which set, as in argmax(f(t), t in S)"); }
559559
| 'argmin(' args = function_arguments ')' { Assert("argmin", 2, $args.list.Count); $value = $args.list[1] is Entity.Set.Inf { Element: Variable } argminRange ? MathS.Argmin($args.list[0], argminRange.Element, argminRange.SupSet) : throw new InvalidArgumentParseException("argmin expects its second argument to say which variable ranges over which set, as in argmin(f(t), t in S)"); }
560560
| 'gcd(' args = function_arguments ')' { AssertAtLeast("gcd", 1, $args.list.Count); $value = $args.list.Aggregate((a, b) => MathS.Gcd(a, b)); }
561+
| 'lcm(' args = function_arguments ')' { AssertAtLeast("lcm", 1, $args.list.Count); $value = $args.list.Aggregate((a, b) => MathS.Lcm(a, b)); }
561562
| 'binomial(' args = function_arguments ')' { Assert("binomial", 2, $args.list.Count); $value = MathS.Binomial($args.list[0], $args.list[1]); }
562563
563564
/* Names the library does not have. Each is a function every other CAS spells this way, so
@@ -566,7 +567,6 @@ atom returns[Entity value]
566567
between a missing function and a wrong answer. */
567568
568569
| 'trunc(' args = function_arguments ')' { $value = NotImplementedFunction("trunc", "rounding functions"); }
569-
| 'lcm(' args = function_arguments ')' { $value = NotImplementedFunction("lcm", "least common multiple as a symbolic function"); }
570570
| 'erf(' args = function_arguments ')' { $value = NotImplementedFunction("erf", "error function"); }
571571
| 'conjugate(' args = function_arguments ')' { $value = NotImplementedFunction("conjugate", "complex conjugate as a symbolic function"); }
572572
| 'domain(' args = function_arguments ')'

‎Sources/AngouriMath/Core/Antlr/AngouriMath.interp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,9 @@ null
150150
'argmax('
151151
'argmin('
152152
'gcd('
153+
'lcm('
153154
'binomial('
154155
'trunc('
155-
'lcm('
156156
'erf('
157157
'conjugate('
158158
'domain('

‎Sources/AngouriMath/Core/Antlr/AngouriMath.tokens‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ WS=167
315315
'argmax('=148
316316
'argmin('=149
317317
'gcd('=150
318-
'binomial('=151
319-
'trunc('=152
320-
'lcm('=153
318+
'lcm('=151
319+
'binomial('=152
320+
'trunc('=153
321321
'erf('=154
322322
'conjugate('=155
323323
'domain('=156

‎Sources/AngouriMath/Core/Antlr/AngouriMathLexer.cs‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public AngouriMathLexer(ICharStream input, TextWriter output, TextWriter errorOu
132132
"'limit('", "'limitleft('", "'limitright('", "'sum('", "'product('", "'signum('",
133133
"'sgn('", "'sign('", "'abs('", "'phi('", "'card('", "'floor('", "'ceil('",
134134
"'ceiling('", "'round('", "'min('", "'max('", "'argmax('", "'argmin('",
135-
"'gcd('", "'binomial('", "'trunc('", "'lcm('", "'erf('", "'conjugate('",
135+
"'gcd('", "'lcm('", "'binomial('", "'trunc('", "'erf('", "'conjugate('",
136136
"'domain('", "'piecewise('", "'apply('", "'lambda('", null, null, null,
137137
null, "'NaN'"
138138
};
@@ -273,8 +273,8 @@ static AngouriMathLexer() {
273273
1,144,1,144,1,144,1,144,1,144,1,145,1,145,1,145,1,145,1,145,1,146,1,146,
274274
1,146,1,146,1,146,1,147,1,147,1,147,1,147,1,147,1,147,1,147,1,147,1,148,
275275
1,148,1,148,1,148,1,148,1,148,1,148,1,148,1,149,1,149,1,149,1,149,1,149,
276-
1,150,1,150,1,150,1,150,1,150,1,150,1,150,1,150,1,150,1,150,1,151,1,151,
277-
1,151,1,151,1,151,1,151,1,151,1,152,1,152,1,152,1,152,1,152,1,153,1,153,
276+
1,150,1,150,1,150,1,150,1,150,1,151,1,151,1,151,1,151,1,151,1,151,1,151,
277+
1,151,1,151,1,151,1,152,1,152,1,152,1,152,1,152,1,152,1,152,1,153,1,153,
278278
1,153,1,153,1,153,1,154,1,154,1,154,1,154,1,154,1,154,1,154,1,154,1,154,
279279
1,154,1,154,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,156,1,156,
280280
1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,157,1,157,1,157,
@@ -379,7 +379,7 @@ static AngouriMathLexer() {
379379
0,277,1133,1,0,0,0,279,1138,1,0,0,0,281,1143,1,0,0,0,283,1149,1,0,0,0,
380380
285,1156,1,0,0,0,287,1162,1,0,0,0,289,1171,1,0,0,0,291,1178,1,0,0,0,293,
381381
1183,1,0,0,0,295,1188,1,0,0,0,297,1196,1,0,0,0,299,1204,1,0,0,0,301,1209,
382-
1,0,0,0,303,1219,1,0,0,0,305,1226,1,0,0,0,307,1231,1,0,0,0,309,1236,1,
382+
1,0,0,0,303,1214,1,0,0,0,305,1224,1,0,0,0,307,1231,1,0,0,0,309,1236,1,
383383
0,0,0,311,1247,1,0,0,0,313,1255,1,0,0,0,315,1266,1,0,0,0,317,1273,1,0,
384384
0,0,319,1285,1,0,0,0,321,1291,1,0,0,0,323,1333,1,0,0,0,325,1351,1,0,0,
385385
0,327,1371,1,0,0,0,329,1373,1,0,0,0,331,1378,1,0,0,0,333,1416,1,0,0,0,
@@ -600,19 +600,19 @@ static AngouriMathLexer() {
600600
1197,5,97,0,0,1197,1198,5,114,0,0,1198,1199,5,103,0,0,1199,1200,5,109,
601601
0,0,1200,1201,5,105,0,0,1201,1202,5,110,0,0,1202,1203,5,40,0,0,1203,298,
602602
1,0,0,0,1204,1205,5,103,0,0,1205,1206,5,99,0,0,1206,1207,5,100,0,0,1207,
603-
1208,5,40,0,0,1208,300,1,0,0,0,1209,1210,5,98,0,0,1210,1211,5,105,0,0,
604-
1211,1212,5,110,0,0,1212,1213,5,111,0,0,1213,1214,5,109,0,0,1214,1215,
605-
5,105,0,0,1215,1216,5,97,0,0,1216,1217,5,108,0,0,1217,1218,5,40,0,0,1218,
606-
302,1,0,0,0,1219,1220,5,116,0,0,1220,1221,5,114,0,0,1221,1222,5,117,0,
607-
0,1222,1223,5,110,0,0,1223,1224,5,99,0,0,1224,1225,5,40,0,0,1225,304,1,
608-
0,0,0,1226,1227,5,108,0,0,1227,1228,5,99,0,0,1228,1229,5,109,0,0,1229,
609-
1230,5,40,0,0,1230,306,1,0,0,0,1231,1232,5,101,0,0,1232,1233,5,114,0,0,
610-
1233,1234,5,102,0,0,1234,1235,5,40,0,0,1235,308,1,0,0,0,1236,1237,5,99,
611-
0,0,1237,1238,5,111,0,0,1238,1239,5,110,0,0,1239,1240,5,106,0,0,1240,1241,
612-
5,117,0,0,1241,1242,5,103,0,0,1242,1243,5,97,0,0,1243,1244,5,116,0,0,1244,
613-
1245,5,101,0,0,1245,1246,5,40,0,0,1246,310,1,0,0,0,1247,1248,5,100,0,0,
614-
1248,1249,5,111,0,0,1249,1250,5,109,0,0,1250,1251,5,97,0,0,1251,1252,5,
615-
105,0,0,1252,1253,5,110,0,0,1253,1254,5,40,0,0,1254,312,1,0,0,0,1255,1256,
603+
1208,5,40,0,0,1208,300,1,0,0,0,1209,1210,5,108,0,0,1210,1211,5,99,0,0,
604+
1211,1212,5,109,0,0,1212,1213,5,40,0,0,1213,302,1,0,0,0,1214,1215,5,98,
605+
0,0,1215,1216,5,105,0,0,1216,1217,5,110,0,0,1217,1218,5,111,0,0,1218,1219,
606+
5,109,0,0,1219,1220,5,105,0,0,1220,1221,5,97,0,0,1221,1222,5,108,0,0,1222,
607+
1223,5,40,0,0,1223,304,1,0,0,0,1224,1225,5,116,0,0,1225,1226,5,114,0,0,
608+
1226,1227,5,117,0,0,1227,1228,5,110,0,0,1228,1229,5,99,0,0,1229,1230,5,
609+
40,0,0,1230,306,1,0,0,0,1231,1232,5,101,0,0,1232,1233,5,114,0,0,1233,1234,
610+
5,102,0,0,1234,1235,5,40,0,0,1235,308,1,0,0,0,1236,1237,5,99,0,0,1237,
611+
1238,5,111,0,0,1238,1239,5,110,0,0,1239,1240,5,106,0,0,1240,1241,5,117,
612+
0,0,1241,1242,5,103,0,0,1242,1243,5,97,0,0,1243,1244,5,116,0,0,1244,1245,
613+
5,101,0,0,1245,1246,5,40,0,0,1246,310,1,0,0,0,1247,1248,5,100,0,0,1248,
614+
1249,5,111,0,0,1249,1250,5,109,0,0,1250,1251,5,97,0,0,1251,1252,5,105,
615+
0,0,1252,1253,5,110,0,0,1253,1254,5,40,0,0,1254,312,1,0,0,0,1255,1256,
616616
5,112,0,0,1256,1257,5,105,0,0,1257,1258,5,101,0,0,1258,1259,5,99,0,0,1259,
617617
1260,5,101,0,0,1260,1261,5,119,0,0,1261,1262,5,105,0,0,1262,1263,5,115,
618618
0,0,1263,1264,5,101,0,0,1264,1265,5,40,0,0,1265,314,1,0,0,0,1266,1267,

‎Sources/AngouriMath/Core/Antlr/AngouriMathLexer.interp‎

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

‎Sources/AngouriMath/Core/Antlr/AngouriMathLexer.tokens‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ WS=167
315315
'argmax('=148
316316
'argmin('=149
317317
'gcd('=150
318-
'binomial('=151
319-
'trunc('=152
320-
'lcm('=153
318+
'lcm('=151
319+
'binomial('=152
320+
'trunc('=153
321321
'erf('=154
322322
'conjugate('=155
323323
'domain('=156

‎Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public const int
114114
"'limit('", "'limitleft('", "'limitright('", "'sum('", "'product('", "'signum('",
115115
"'sgn('", "'sign('", "'abs('", "'phi('", "'card('", "'floor('", "'ceil('",
116116
"'ceiling('", "'round('", "'min('", "'max('", "'argmax('", "'argmin('",
117-
"'gcd('", "'binomial('", "'trunc('", "'lcm('", "'erf('", "'conjugate('",
117+
"'gcd('", "'lcm('", "'binomial('", "'trunc('", "'erf('", "'conjugate('",
118118
"'domain('", "'piecewise('", "'apply('", "'lambda('", null, null, null,
119119
null, "'NaN'"
120120
};
@@ -3756,7 +3756,7 @@ public AtomContext atom() {
37563756
_localctx.args = function_arguments();
37573757
State = 933;
37583758
Match(T__24);
3759-
Assert("binomial", 2, _localctx.args.list.Count); _localctx.value = MathS.Binomial(_localctx.args.list[0], _localctx.args.list[1]);
3759+
AssertAtLeast("lcm", 1, _localctx.args.list.Count); _localctx.value = _localctx.args.list.Aggregate((a, b) => MathS.Lcm(a, b));
37603760
}
37613761
break;
37623762
case 117:
@@ -3768,7 +3768,7 @@ public AtomContext atom() {
37683768
_localctx.args = function_arguments();
37693769
State = 938;
37703770
Match(T__24);
3771-
_localctx.value = NotImplementedFunction("trunc", "rounding functions");
3771+
Assert("binomial", 2, _localctx.args.list.Count); _localctx.value = MathS.Binomial(_localctx.args.list[0], _localctx.args.list[1]);
37723772
}
37733773
break;
37743774
case 118:
@@ -3780,7 +3780,7 @@ public AtomContext atom() {
37803780
_localctx.args = function_arguments();
37813781
State = 943;
37823782
Match(T__24);
3783-
_localctx.value = NotImplementedFunction("lcm", "least common multiple as a symbolic function");
3783+
_localctx.value = NotImplementedFunction("trunc", "rounding functions");
37843784
}
37853785
break;
37863786
case 119:

0 commit comments

Comments
 (0)