diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c7f83978a..cfb539286 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -388,6 +388,18 @@ truth value at any other ([#2195](https://github.com/asc-community/AngouriMath/i | `"x in [0; 1]".ToEntity().Compile("x").Call(0.5)` | `UncompilableNodeException` | `(1, 0)` | | `"3 divides x".ToEntity().Compile("x").Call(6)` | `UncompilableNodeException` | `(1, 0)` | +### A congruence compiles + +**Not silent: what was refused is compiled.** `a = b (mod n)` was refused by both compilers with +`UncompilableNodeException`. It compiles to the truth value evaluation gives: between whole numbers whether `n` divides +`a - b`, taken in whole numbers so that it is exact past `2^53`, a modulus of `0` meaning equality, and no truth value +for any other number ([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x = 1 (mod 5)".ToEntity().Compile("x").Call(6)` | `UncompilableNodeException` | `(1, 0)` | +| `"x = 1 (mod 5)".ToEntity().Compile("x")(6)` | `UncompilableNodeException` | `True` | + ### `mod` compiles over `Complex` **Not silent: what was refused is compiled.** `Compile` refused `mod` with `UncompilableNodeException`, diff --git a/Sources/.editorconfig b/Sources/.editorconfig index cddc2c8b3..60be78b3b 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -390,6 +390,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/MembershipCompilationTest.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/Common/CongruenceCompilationTest.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/Common/CompileMatrixTest.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/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index 3d05e4ad3..ba78f1d01 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -756,6 +756,20 @@ private protected override void CompileNode(Compiler compiler) } } + // A congruence of whole numbers, with no truth value at any other. + public partial record Congruentf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Modulus.InnerCompile(compiler); + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_CONGRUENT)); + } + } + // Divisibility of whole numbers, with no truth value at any other. public partial record Dividesf { diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs index 080c78b10..878d96c78 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs @@ -98,6 +98,8 @@ internal enum InstructionType // 3-arg: a value where its predicate holds, and the one below it elsewhere. CALL_SELECT = 150, + // Whether two numbers are congruent modulo a third. + CALL_CONGRUENT, } internal sealed partial record Instruction(InstructionType Type, int Reference = -1, System.Numerics.Complex Value = default) { @@ -413,6 +415,9 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value case InstructionType.CALL_DOUBLE_FACTORIAL: stack.Push(AngouriMath.Numerics.CombinatorialFunctions.DoubleFactorial(stack.Pop())); break; + case InstructionType.CALL_CONGRUENT: + stack.Push(Truth(AngouriMath.Numerics.NumberStatements.Congruent(stack.Pop(), stack.Pop(), stack.Pop()))); + break; case InstructionType.CALL_SELECT: { var value = stack.Pop(); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 7899e432e..f63cfbff3 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -324,6 +324,7 @@ public virtual Expression ConvertOtherNode(IEnumerable en, Entity ty return typeHolder switch { Piecewise => HandlePiecewise(en), + Congruentf => HandleCongruence(en.ToArray()), // A matrix is a tensor over its elements' common type, built from the compiled // elements in row-major order, which is the order DirectChildren gives them. // https://github.com/asc-community/AngouriMath/issues/526 @@ -335,6 +336,15 @@ public virtual Expression ConvertOtherNode(IEnumerable en, Entity ty }; } + // A congruence, over Complex where any of its numbers is one and over double otherwise, with no truth value + // where they are not whole. https://github.com/asc-community/AngouriMath/issues/2195 + private Expression HandleCongruence(Expression[] numbers) + { + var complex = numbers.Any(number => (Nullable.GetUnderlyingType(number.Type) ?? number.Type) == typeof(System.Numerics.Complex)); + var type = complex ? typeof(System.Numerics.Complex) : typeof(double); + return Expression.Call(complex ? complexCongruence : realCongruence, numbers.Select(number => ConvertType(number, type))); + } + private Expression HandleMatrix(Entity.Matrix matrix, IEnumerable en) { var elements = en.ToArray(); @@ -529,6 +539,10 @@ private static MethodInfo Conv(Expression> e) ((MethodCallExpression)((Expression>)((x, set) => AngouriMath.Numerics.NumberStatements.In(x, set))).Body).Method; [ConstantField] private static readonly MethodInfo complexDivisibility = ((MethodCallExpression)((Expression>)((a, b) => AngouriMath.Numerics.NumberStatements.Divides(a, b))).Body).Method; + [ConstantField] private static readonly MethodInfo complexCongruence = + ((MethodCallExpression)((Expression>)((a, b, n) => AngouriMath.Numerics.NumberStatements.Congruent(a, b, n))).Body).Method; + [ConstantField] private static readonly MethodInfo realCongruence = + ((MethodCallExpression)((Expression>)((a, b, n) => AngouriMath.Numerics.NumberStatements.Congruent(a, b, n))).Body).Method; [ConstantField] private static readonly MethodInfo realDivisibility = ((MethodCallExpression)((Expression>)((a, b) => AngouriMath.Numerics.NumberStatements.Divides(a, b))).Body).Method; diff --git a/Sources/AngouriMath/Numerics/NumberStatements.cs b/Sources/AngouriMath/Numerics/NumberStatements.cs index 03aa2c528..44c09ed2f 100644 --- a/Sources/AngouriMath/Numerics/NumberStatements.cs +++ b/Sources/AngouriMath/Numerics/NumberStatements.cs @@ -77,6 +77,23 @@ internal enum NumberSet { Complexes, Reals, Integers, PositiveIntegers, NonNegat public static bool? Divides(double divisor, double dividend) => Divides(new NumericsComplex(divisor, 0), new NumericsComplex(dividend, 0)); + /// + /// Whether and are congruent modulo : + /// for whole numbers, whether the modulus divides their difference, exactly, a modulus of 0 meaning they are + /// equal; for any other, no truth value. + /// + public static bool? Congruent(NumericsComplex left, NumericsComplex right, NumericsComplex modulus) + { + if (!IsWhole(left) || !IsWhole(right) || !IsWhole(modulus)) + return null; + // In whole numbers, since the difference of two doubles is not exact past 2^53. + var difference = EFloat.FromDouble(left.Real).ToEInteger().Subtract(EFloat.FromDouble(right.Real).ToEInteger()); + return modulus.Real == 0 ? difference.IsZero : difference.Remainder(EFloat.FromDouble(modulus.Real).ToEInteger()).IsZero; + } + + public static bool? Congruent(double left, double right, double modulus) + => Congruent(new NumericsComplex(left, 0), new NumericsComplex(right, 0), new NumericsComplex(modulus, 0)); + // A double's remainder by another is exact, so a whole one is divided exactly however large. private static bool IsWhole(NumericsComplex z) => z.Imaginary == 0 && !double.IsNaN(z.Real) && !double.IsInfinity(z.Real) && Math.Floor(z.Real) == z.Real; diff --git a/Sources/Tests/UnitTests/Common/CongruenceCompilationTest.cs b/Sources/Tests/UnitTests/Common/CongruenceCompilationTest.cs new file mode 100644 index 000000000..fd2cfaada --- /dev/null +++ b/Sources/Tests/UnitTests/Common/CongruenceCompilationTest.cs @@ -0,0 +1,75 @@ +// +// 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.Numerics; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A congruence a = b (mod n) compiles, in FastExpression and in the LINQ compiler, to the truth value + /// evaluation gives: between whole numbers whether n divides a - b, a modulus of 0 meaning equality, + /// and none for any other number. It was refused. + /// #2195 + /// + [Trait("Area", "Common")] + public sealed class CongruenceCompilationTest + { + private static readonly string[] numbers = + { "-4", "0", "1", "3", "6", "11", "5/2", "i", "+oo", "100000000000000000000" }; + + private static readonly string[] moduli = { "5", "-5", "0", "1", "2", "5/2", "i" }; + + private static Complex Numeric(string value) => ((Entity.Number.Complex)value.ToEntity().Evaled).ToNumerics(); + + private static bool? Truth(Complex value) => value == 1 ? true : value == 0 ? false : null; + + [Fact] + public void ACongruenceIsWhatEvaluationSays() + { + var statement = "x = y (mod z)".ToEntity(); + var fast = statement.Compile("x", "y", "z"); + var linq = statement.Compile("x", "y", "z"); + var real = statement.Compile("x", "y", "z"); + var compared = 0; + foreach (var a in numbers) + foreach (var b in numbers) + foreach (var n in moduli) + { + var evaluated = statement.Substitute("x", a).Substitute("y", b).Substitute("z", n).Evaled; + if (evaluated is not Entity.Boolean && !evaluated.IsNaN) + continue; + bool? expected = evaluated is Entity.Boolean truth ? (bool)truth : null; + var (x, y, z) = (Numeric(a), Numeric(b), Numeric(n)); + Assert.True(expected == Truth(fast.Call(x, y, z)), $"{a} = {b} (mod {n}): {expected?.ToString() ?? "none"} evaluated, {fast.Call(x, y, z)} by FastExpression"); + Assert.True(expected == linq(x, y, z), $"{a} = {b} (mod {n}): {expected?.ToString() ?? "none"} evaluated, {linq(x, y, z)?.ToString() ?? "none"} by LINQ"); + if (x.Imaginary == 0 && y.Imaginary == 0 && z.Imaginary == 0) + Assert.True(expected == real(x.Real, y.Real, z.Real), $"{a} = {b} (mod {n}): {expected?.ToString() ?? "none"} evaluated, {real(x.Real, y.Real, z.Real)?.ToString() ?? "none"} over double"); + compared++; + } + Assert.True(compared >= numbers.Length * numbers.Length * moduli.Length * 3 / 4, $"only {compared} decided"); + } + + /// + /// Past 2^53 the difference is taken in whole numbers: 2^60 + 256 less 1 is 3 modulo 4, where the + /// difference in doubles rounds back to 2^60 + 256, which 4 divides. + /// + [Fact] + public void PastTwoToTheFiftyThreeTheDifferenceIsExact() + { + Assert.Equal(Entity.Boolean.False, "1152921504606847232 = 1 (mod 4)".ToEntity().Evaled); + Assert.Equal(new Complex(0, 0), "x = 1 (mod 4)".ToEntity().Compile("x").Call(1152921504606847232.0)); + Assert.Equal(false, "x = 1 (mod 4)".ToEntity().Compile("x")(1152921504606847232.0)); + } + + [Fact] + public void AChainOfCongruencesCompiles() + => Assert.Equal(new Complex(1, 0), "x = 1 = 11 (mod 5)".ToEntity().Compile("x").Call(6)); + } +}