From ef0fb020195cce14b19d966a95794ebd1dc0222b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 22:39:07 +0000 Subject: [PATCH] A scalar over a matrix argument is not compiled Compiled, x / A with a matrix argument A divided x by every element: [[2, 1], [0.667, 0.5]] for 2 / [[1, 2], [3, 4]], where evaluation takes a scalar over a matrix as the scalar times the inverse, [[-4, 2], [3, -1]]. The inverse is not compiled, as a negative matrix power is not, so the quotient is now refused with UncompilableNodeException. A matrix written into the expression is inverted before it is compiled, and that is unchanged. DivideFromScalar stays public, for what it says it is, and the compiler no longer uses it. AScalarOverAMatrixArgumentTest takes the refusal, which fails on master, a matrix argument over a scalar and a written matrix inverted, which pass on both. The suite, the trio, F#, AOT and the eight harnesses pass. Fixes #2287. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 12 +++++ Sources/.editorconfig | 3 ++ .../Compilation/IntoLinq/TensorCompilation.cs | 19 +++++--- .../Common/AScalarOverAMatrixArgumentTest.cs | 47 +++++++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 10896a986..04bb60337 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -300,6 +300,18 @@ are now also found wherever the `1` and the square sit in a sum, under the condi | `"1 + tan(x)^2 - sec(x)^2".Simplify()` | `1 + tan(x) ^ 2 - sec(x) ^ 2` | `0 provided not cos(x) = 0` | | `"a + 1 + tan(y)^2 - sec(y)^2".Simplify()` | `a + 1 + tan(y) ^ 2 - sec(y) ^ 2` | `a provided not cos(y) = 0` | +### A scalar over a matrix argument is not compiled + +**Not silent: what compiled to a wrong value now throws `UncompilableNodeException`.** A scalar over a matrix that is an +argument of the compiled function was compiled as the scalar over every element, where evaluation takes it as the scalar +times the inverse. The inverse is not compiled, as a negative matrix power is not, so the quotient is refused. A matrix +written into the expression is inverted before it is compiled, as before +([#2287](https://github.com/asc-community/AngouriMath/issues/2287)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x / A"`, compiled over `GenTensor` and called at `A = [[1, 2], [3, 4]]`, `x = 2` | `[[2, 1], [0.667, 0.5]]` | throws `UncompilableNodeException` | + ### A square system is not answered with rows that are no solution **Silent: a row that is no solution is gone, and a system with a row that fails for some values is `null`.** diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 986ac8813..2e7b9a242 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -164,6 +164,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/ListArgumentOverloadTest.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/AScalarOverAMatrixArgumentTest.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/Multithreading/SettingsAcrossAsync.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/IntoLinq/TensorCompilation.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs index 287671e14..8e8af054e 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs @@ -249,14 +249,13 @@ private sealed class Kit internal MethodInfo SubtractFromScalar { get; } internal MethodInfo MultiplyScalar { get; } internal MethodInfo DivideScalar { get; } - internal MethodInfo DivideFromScalar { get; } internal MethodInfo Power { get; } private Kit(Type tensor, MethodInfo empty, MethodInfo set, MethodInfo add, MethodInfo subtract, MethodInfo multiply, MethodInfo addScalar, MethodInfo subtractScalar, MethodInfo subtractFromScalar, MethodInfo multiplyScalar, - MethodInfo divideScalar, MethodInfo divideFromScalar, MethodInfo power) - => (Tensor, Empty, Set, Add, Subtract, Multiply, AddScalar, SubtractScalar, SubtractFromScalar, MultiplyScalar, DivideScalar, DivideFromScalar, Power) - = (tensor, empty, set, add, subtract, multiply, addScalar, subtractScalar, subtractFromScalar, multiplyScalar, divideScalar, divideFromScalar, power); + MethodInfo divideScalar, MethodInfo power) + => (Tensor, Empty, Set, Add, Subtract, Multiply, AddScalar, SubtractScalar, SubtractFromScalar, MultiplyScalar, DivideScalar, Power) + = (tensor, empty, set, add, subtract, multiply, addScalar, subtractScalar, subtractFromScalar, multiplyScalar, divideScalar, power); // An expression tree is how C# names a method without a string: the call inside it // is resolved at compile time, closed over T and TWrapper, and rooted for the trimmer. @@ -274,7 +273,6 @@ internal static Kit For() where TWrapper : struct, IOperations Named, GenTensor>>((s, a) => TensorCompilation.SubtractFromScalar(s, a)), Named, T, GenTensor>>((a, s) => TensorCompilation.MultiplyScalar(a, s)), Named, T, GenTensor>>((a, s) => TensorCompilation.DivideScalar(a, s)), - Named, GenTensor>>((s, a) => TensorCompilation.DivideFromScalar(s, a)), Named, int, GenTensor>>((a, n) => TensorCompilation.Power(a, n))); } @@ -380,6 +378,10 @@ public static GenTensor MultiplyScalar(GenTensor DivideScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations => Elementwise(a, x => default(TWrapper).Divide(x, scalar)); /// The scalar over every element. + /// + /// Not what a scalar over a matrix means, which is the scalar times the inverse, and so not what the compiler + /// makes of one. https://github.com/asc-community/AngouriMath/issues/2287 + /// public static GenTensor DivideFromScalar(T scalar, GenTensor a) where TWrapper : struct, IOperations => Elementwise(a, x => default(TWrapper).Divide(scalar, x)); /// The matrix power, a non-negative whole exponent, by repeated squaring. @@ -452,7 +454,12 @@ internal static Expression Build(Entity.Matrix matrix, IReadOnlyList Entity.Sumf => Expression.Call(rightKit!.AddScalar, right, value), Entity.Minusf => Expression.Call(rightKit!.SubtractFromScalar, value, right), Entity.Mulf => Expression.Call(rightKit!.MultiplyScalar, right, value), - Entity.Divf => Expression.Call(rightKit!.DivideFromScalar, value, right), + // A scalar over a matrix is the scalar times its inverse, as evaluation has it, and the + // inverse is not compiled, as a negative power is not: dividing by every element + // instead gave 2 / [[1, 2], [3, 4]] as [[2, 1], [2/3, 1/2]]. + // https://github.com/asc-community/AngouriMath/issues/2287 + Entity.Divf => throw new UncompilableNodeException( + "A scalar over a matrix is the scalar times its inverse, which is not compiled: invert the matrix first"), _ => null, }; } diff --git a/Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs b/Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs new file mode 100644 index 000000000..d1c216f5f --- /dev/null +++ b/Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs @@ -0,0 +1,47 @@ +// +// 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.Core.Compilation.IntoLinq; +using AngouriMath.Core.Exceptions; +using AngouriMath.Extensions; +using GenericTensor.Core; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A scalar over a matrix argument is not compiled, where it was compiled as the scalar over every element: evaluation + /// takes it as the scalar times the inverse, and the inverse is not compiled, as a negative matrix power is not. A matrix + /// written into the expression is inverted before it is compiled, and that is unchanged. + /// #2287 + /// + [Trait("Area", "Common")] + public sealed class AScalarOverAMatrixArgumentTest + { + [Fact] + public void ItIsRefused() + => Assert.Throws(() => "x / A".ToEntity() + .Compile, double, GenTensor>("A", "x")); + + [Fact] + public void AMatrixArgumentOverAScalarIsStillCompiled() + { + var matrix = GenTensor.CreateMatrix(new double[,] { { 1, 2 }, { 3, 4 } }); + var divided = "A / x".ToEntity() + .Compile, double, GenTensor>("A", "x")(matrix, 2); + Assert.Equal(new[] { 0.5, 1, 1.5, 2 }, new[] { divided[0, 0], divided[0, 1], divided[1, 0], divided[1, 1] }); + } + + [Fact] + public void AWrittenMatrixIsInvertedAsBefore() + { + var inverse = "x / [[1, 0], [2, 4]]".ToEntity().Compile>("x")(1); + Assert.Equal(new[] { 1, 0, -0.5, 0.25 }, new[] { inverse[0, 0], inverse[0, 1], inverse[1, 0], inverse[1, 1] }); + } + } +}