Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<double, bool>("x")(6)` | `UncompilableNodeException` | `True` |

### `mod` compiles over `Complex`

**Not silent: what was refused is compiled.** `Compile<Complex, ...>` refused `mod` with `UncompilableNodeException`,
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ public virtual Expression ConvertOtherNode(IEnumerable<Expression> 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
Expand All @@ -335,6 +336,15 @@ public virtual Expression ConvertOtherNode(IEnumerable<Expression> 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<Expression> en)
{
var elements = en.ToArray();
Expand Down Expand Up @@ -529,6 +539,10 @@ private static MethodInfo Conv<TFrom, TTo>(Expression<Func<TFrom, TTo>> e)
((MethodCallExpression)((Expression<Func<double, int, bool?>>)((x, set) => AngouriMath.Numerics.NumberStatements.In(x, set))).Body).Method;
[ConstantField] private static readonly MethodInfo complexDivisibility =
((MethodCallExpression)((Expression<Func<System.Numerics.Complex, System.Numerics.Complex, bool?>>)((a, b) => AngouriMath.Numerics.NumberStatements.Divides(a, b))).Body).Method;
[ConstantField] private static readonly MethodInfo complexCongruence =
((MethodCallExpression)((Expression<Func<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex, bool?>>)((a, b, n) => AngouriMath.Numerics.NumberStatements.Congruent(a, b, n))).Body).Method;
[ConstantField] private static readonly MethodInfo realCongruence =
((MethodCallExpression)((Expression<Func<double, double, double, bool?>>)((a, b, n) => AngouriMath.Numerics.NumberStatements.Congruent(a, b, n))).Body).Method;
[ConstantField] private static readonly MethodInfo realDivisibility =
((MethodCallExpression)((Expression<Func<double, double, bool?>>)((a, b) => AngouriMath.Numerics.NumberStatements.Divides(a, b))).Body).Method;

Expand Down
17 changes: 17 additions & 0 deletions Sources/AngouriMath/Numerics/NumberStatements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

/// <summary>
/// Whether <paramref name="left"/> and <paramref name="right"/> are congruent modulo <paramref name="modulus"/>:
/// for whole numbers, whether the modulus divides their difference, exactly, a modulus of 0 meaning they are
/// equal; for any other, no truth value.
/// </summary>
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;
Expand Down
75 changes: 75 additions & 0 deletions Sources/Tests/UnitTests/Common/CongruenceCompilationTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A congruence <c>a = b (mod n)</c> compiles, in FastExpression and in the LINQ compiler, to the truth value
/// evaluation gives: between whole numbers whether <c>n</c> divides <c>a - b</c>, a modulus of 0 meaning equality,
/// and none for any other number. It was refused.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2195">#2195</a>
/// </summary>
[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<Complex, Complex, Complex, bool?>("x", "y", "z");
var real = statement.Compile<double, double, double, bool?>("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");
}

/// <summary>
/// Past 2^53 the difference is taken in whole numbers: <c>2^60 + 256</c> less 1 is 3 modulo 4, where the
/// difference in doubles rounds back to <c>2^60 + 256</c>, which 4 divides.
/// </summary>
[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<double, bool?>("x")(1152921504606847232.0));
}

[Fact]
public void AChainOfCongruencesCompiles()
=> Assert.Equal(new Complex(1, 0), "x = 1 = 11 (mod 5)".ToEntity().Compile("x").Call(6));
}
}
Loading