From 1b4c9d2a25209768c09877b450439bf8385620cc Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 18 Sep 2026 21:50:16 +0000 Subject: [PATCH 1/2] A matrix compiles to a GenTensor over its elements' type, and the arithmetic on it to GenericTensor's operations #526: "just run GenericTensor through our functions". Under the LINQ compiler a Matrix node is a GenTensor over the common type of its compiled elements, filled element by element, and a sum, difference, product, quotient or whole power with a matrix in it is the tensor operation of that name -- PiecewiseAdd, PiecewiseSubtract, MatrixMultiply, the piecewise operations by a scalar converted to the element type, MatrixPower -- so that "A * B" compiled over two tensors is the matrix product and "2 A + B - 1" scales, adds and shifts. GenericTensor takes its element arithmetic from a struct and ships none for the numeric types, so DoubleOperations, SingleOperations, ComplexOperations, Int32Operations, Int64Operations and BigIntegerOperations are here, in AngouriMath.Core.Compilation.IntoLinq, and GenTensor is the type to compile to and to pass a matrix in as. The methods the compiled tree calls are closed over each element type at compile time and named through expression trees, the way the protocol keeps its own tables: nothing generic is constructed at run time, which the trimming and NativeAOT analyzers refuse (#363). A matrix as a number, a number as a matrix, and a function of a matrix are UncompilableNodeException with a message that says so, where sin(A) was a KeyNotFoundException out of a type table. An expression of matrices with a scalar value still simplifies to the scalar first, as #425 made it. Closes #526. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 + Sources/.editorconfig | 6 + Sources/AngouriMath/Docs/Usage/Exceptions.md | 12 +- .../IntoLinq/CompilationProtocol.cs | 35 +- .../Compilation/IntoLinq/TensorCompilation.cs | 399 ++++++++++++++++++ .../UnitTests/Common/CompileMatrixTest.cs | 106 +++++ .../UnitTests/Common/MatrixCompilationTest.cs | 19 +- Sources/Tests/UnitTests/Common/PublicApi.txt | 98 +++++ 8 files changed, 680 insertions(+), 10 deletions(-) create mode 100644 Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs create mode 100644 Sources/Tests/UnitTests/Common/CompileMatrixTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 7010b6123..219e84b39 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -520,6 +520,21 @@ now, which is the one silent change: a variable spelled `forall` or `exists` no | `forall + 1` | `forall + 1`, a variable named `forall` | `UnhandledParseException` | | `exists(x)` | `exists * x` | `UnhandledParseException` | +### A matrix compiles + +**Addition, not silent.** Under the LINQ compiler a `Matrix` node compiles to a +`GenTensor` over its elements' type, with the wrappers `DoubleOperations`, +`SingleOperations`, `ComplexOperations`, `Int32Operations`, `Int64Operations` and +`BigIntegerOperations` in `AngouriMath.Core.Compilation.IntoLinq`, and a sum, difference, product, +quotient or whole power with a matrix in it to GenericTensor's operation of that name +([#526](https://github.com/asc-community/AngouriMath/issues/526)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"[[x, 2 x], [x + 1, x^2]]".ToEntity().Compile>("x")` | `UncompilableNodeException` | a delegate; at `3` the matrix `[[3, 6], [4, 9]]` | +| `"A * B".ToEntity().Compile, GenTensor<…>, GenTensor<…>>("A", "B")` | `UncompilableNodeException` | the matrix product | +| `"[[1, 0]] * [[a, b], [c, d]] * [[0], [1]]".ToEntity().Compile("a", "b", "c", "d")` | `2` at `(1, 2, 3, 4)` — simplified to the scalar first | unchanged | + ### `binomial(n, k)` is a function **Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled diff --git a/Sources/.editorconfig b/Sources/.editorconfig index a8c3cdf0c..241a76f6b 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -267,6 +267,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/Compilation/IntoLinq/TensorCompilation.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 + [AngouriMath/Functions/Boolean/Quantifiers.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/Docs/Usage/Exceptions.md b/Sources/AngouriMath/Docs/Usage/Exceptions.md index 7f8ac3929..fb1045c7b 100644 --- a/Sources/AngouriMath/Docs/Usage/Exceptions.md +++ b/Sources/AngouriMath/Docs/Usage/Exceptions.md @@ -154,7 +154,7 @@ take. `Compile` was asked for a function it cannot build. Two distinct causes, and the message says which: -- a node with no compiled form — a matrix, a derivative, an integral, a limit; +- a node with no compiled form — a derivative, an integral, a limit, a function of a matrix; - a variable the expression mentions that the compilation was not given. The message names the variable and lists the ones it *was* given, since `Compile("x")` on `a * x` failing used to surface as a bare `KeyNotFoundException` about some dictionary. @@ -162,6 +162,16 @@ take. Both compilers raise it: the stack-machine one (`Compile`) and the LINQ one (`Compile`). +A matrix is not on the first list any more: under the LINQ compiler it compiles to a +`GenTensor` over its elements' type — `GenTensor`, +`GenTensor`, and the same for `float`, `int`, `long`, `BigInteger`, +the wrappers living in `AngouriMath.Core.Compilation.IntoLinq` — and that is the type to compile +to and to pass a matrix in as: `"A * B".ToEntity().Compile, +GenTensor, GenTensor>("A", "B")` is the matrix +product, `2 A + B - 1` scales, adds and shifts element by element, `A ^ 3` is the matrix power +([#526](https://github.com/asc-community/AngouriMath/issues/526)). Compiling a matrix as a +number, or a number as a matrix, or `sin(A)`, is still this exception. + ### `CannotEvalException` Something did not collapse to a single number or boolean. Overwhelmingly this is diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 0a693948d..1b8a27d18 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -74,6 +74,10 @@ public virtual Expression ConvertType(Expression expr, Type type) if (expr.Type == type) return expr; + // A tensor converts to nothing but itself: a matrix over one element type is not + // a matrix over another, and it is not a number. + if (TensorCompilation.IsTensor(expr.Type) || TensorCompilation.IsTensor(type)) + throw new UncompilableNodeException($"A value of type {expr.Type} cannot be compiled as {type}"); bool exprNullable = (Nullable.GetUnderlyingType(expr.Type) is not null) || (expr.Type == typeof(object)); bool typeNullable = (Nullable.GetUnderlyingType(type) is not null) || (type == typeof(object)); @@ -107,7 +111,11 @@ public virtual Expression ConvertType(Expression expr, Type type) /// public virtual Expression ConvertUnaryNode(Expression e, Entity typeHolder) { - + // A function of a matrix has no compiled form here: the tensor operations are the + // arithmetic ones, and sin(A) is not an element-by-element sine of a matrix. + if (TensorCompilation.IsTensor(e.Type)) + throw new UncompilableNodeException( + $"There is no compiled form for {typeHolder.GetType().Name} of a matrix."); return typeHolder switch { Sinf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Sin", 1, newE.Type), newE), @@ -155,7 +163,13 @@ private Expression ShouldBeAtLeastDouble(Expression expr) /// public virtual Expression ConvertBinaryNode(Expression left, Expression right, Entity typeHolder) { - + // A matrix in either place: the tensor operation of the same name, the scalar side + // converted to the element type. https://github.com/asc-community/AngouriMath/issues/526 + if (TensorCompilation.IsTensor(left.Type) || TensorCompilation.IsTensor(right.Type)) + return TensorCompilation.Binary(typeHolder, left, right, ConvertType) + ?? throw new UncompilableNodeException( + $"The node of type {typeHolder.GetType()} has no compiled form over a matrix."); + (left, right) = EqualizeTypesIfAble(left, right); return typeHolder switch { @@ -214,6 +228,10 @@ public virtual Expression ConvertOtherNode(IEnumerable en, Entity ty return typeHolder switch { Piecewise => HandlePiecewise(en), + // 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 + Entity.Matrix matrix => HandleMatrix(matrix, en), // TODO: finite set -> hash set _ => throw new UncompilableNodeException( $"The node of type {typeHolder.GetType()} does not support compilation.") @@ -221,6 +239,19 @@ public virtual Expression ConvertOtherNode(IEnumerable en, Entity ty }; } + private Expression HandleMatrix(Entity.Matrix matrix, IEnumerable en) + { + var elements = en.ToArray(); + if (elements.Length == 0) + throw new UncompilableNodeException("An empty matrix has no compiled form"); + var common = elements[0].Type; + foreach (var element in elements) + common = MaxType(Nullable.GetUnderlyingType(common) ?? common, Nullable.GetUnderlyingType(element.Type) ?? element.Type); + for (var i = 0; i < elements.Length; i++) + elements[i] = ConvertType(elements[i], common); + return TensorCompilation.Build(matrix, elements); + } + private Expression HandlePiecewise(IEnumerable en) { Expression[] children = en.ToArray(); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs new file mode 100644 index 000000000..deacf1620 --- /dev/null +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs @@ -0,0 +1,399 @@ +// +// 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 System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Numerics; +using System.Reflection; +using AngouriMath.Core.Exceptions; +using GenericTensor.Core; +using Complex = System.Numerics.Complex; + +namespace AngouriMath.Core.Compilation.IntoLinq +{ + /// + /// The element arithmetic a compiled matrix runs on: + /// takes its operations from a struct, and these are the ones for the numeric types the + /// compiler produces. A matrix compiled over is a + /// GenTensor<double, DoubleOperations>, and that is the type to compile to + /// and to pass a matrix in as. + /// https://github.com/asc-community/AngouriMath/issues/526 + /// + public readonly struct DoubleOperations : IOperations + { + /// + public double Add(double a, double b) => a + b; + /// + public double Subtract(double a, double b) => a - b; + /// + public double Multiply(double a, double b) => a * b; + /// + public double Negate(double a) => -a; + /// + public double Divide(double a, double b) => a / b; + /// + public double CreateOne() => 1; + /// + public double CreateZero() => 0; + /// + public double Copy(double a) => a; + /// + public bool AreEqual(double a, double b) => a == b; + /// + public bool IsZero(double a) => a == 0; + /// + public string ToString(double a) => a.ToString(System.Globalization.CultureInfo.InvariantCulture); + /// + public byte[] Serialize(double a) => BitConverter.GetBytes(a); + /// + public double Deserialize(byte[] data) => BitConverter.ToDouble(data, 0); + } + + /// The element arithmetic of a matrix compiled over . See . + public readonly struct SingleOperations : IOperations + { + /// + public float Add(float a, float b) => a + b; + /// + public float Subtract(float a, float b) => a - b; + /// + public float Multiply(float a, float b) => a * b; + /// + public float Negate(float a) => -a; + /// + public float Divide(float a, float b) => a / b; + /// + public float CreateOne() => 1; + /// + public float CreateZero() => 0; + /// + public float Copy(float a) => a; + /// + public bool AreEqual(float a, float b) => a == b; + /// + public bool IsZero(float a) => a == 0; + /// + public string ToString(float a) => a.ToString(System.Globalization.CultureInfo.InvariantCulture); + /// + public byte[] Serialize(float a) => BitConverter.GetBytes(a); + /// + public float Deserialize(byte[] data) => BitConverter.ToSingle(data, 0); + } + + /// The element arithmetic of a matrix compiled over . See . + public readonly struct ComplexOperations : IOperations + { + /// + public Complex Add(Complex a, Complex b) => a + b; + /// + public Complex Subtract(Complex a, Complex b) => a - b; + /// + public Complex Multiply(Complex a, Complex b) => a * b; + /// + public Complex Negate(Complex a) => -a; + /// + public Complex Divide(Complex a, Complex b) => a / b; + /// + public Complex CreateOne() => Complex.One; + /// + public Complex CreateZero() => Complex.Zero; + /// + public Complex Copy(Complex a) => a; + /// + public bool AreEqual(Complex a, Complex b) => a == b; + /// + public bool IsZero(Complex a) => a == Complex.Zero; + /// + public string ToString(Complex a) => a.ToString(System.Globalization.CultureInfo.InvariantCulture); + /// + public byte[] Serialize(Complex a) + { + var bytes = new byte[16]; + BitConverter.GetBytes(a.Real).CopyTo(bytes, 0); + BitConverter.GetBytes(a.Imaginary).CopyTo(bytes, 8); + return bytes; + } + /// + public Complex Deserialize(byte[] data) => new(BitConverter.ToDouble(data, 0), BitConverter.ToDouble(data, 8)); + } + + /// The element arithmetic of a matrix compiled over . See . + public readonly struct Int32Operations : IOperations + { + /// + public int Add(int a, int b) => a + b; + /// + public int Subtract(int a, int b) => a - b; + /// + public int Multiply(int a, int b) => a * b; + /// + public int Negate(int a) => -a; + /// + public int Divide(int a, int b) => a / b; + /// + public int CreateOne() => 1; + /// + public int CreateZero() => 0; + /// + public int Copy(int a) => a; + /// + public bool AreEqual(int a, int b) => a == b; + /// + public bool IsZero(int a) => a == 0; + /// + public string ToString(int a) => a.ToString(System.Globalization.CultureInfo.InvariantCulture); + /// + public byte[] Serialize(int a) => BitConverter.GetBytes(a); + /// + public int Deserialize(byte[] data) => BitConverter.ToInt32(data, 0); + } + + /// The element arithmetic of a matrix compiled over . See . + public readonly struct Int64Operations : IOperations + { + /// + public long Add(long a, long b) => a + b; + /// + public long Subtract(long a, long b) => a - b; + /// + public long Multiply(long a, long b) => a * b; + /// + public long Negate(long a) => -a; + /// + public long Divide(long a, long b) => a / b; + /// + public long CreateOne() => 1; + /// + public long CreateZero() => 0; + /// + public long Copy(long a) => a; + /// + public bool AreEqual(long a, long b) => a == b; + /// + public bool IsZero(long a) => a == 0; + /// + public string ToString(long a) => a.ToString(System.Globalization.CultureInfo.InvariantCulture); + /// + public byte[] Serialize(long a) => BitConverter.GetBytes(a); + /// + public long Deserialize(byte[] data) => BitConverter.ToInt64(data, 0); + } + + /// The element arithmetic of a matrix compiled over . See . + public readonly struct BigIntegerOperations : IOperations + { + /// + public BigInteger Add(BigInteger a, BigInteger b) => a + b; + /// + public BigInteger Subtract(BigInteger a, BigInteger b) => a - b; + /// + public BigInteger Multiply(BigInteger a, BigInteger b) => a * b; + /// + public BigInteger Negate(BigInteger a) => -a; + /// + public BigInteger Divide(BigInteger a, BigInteger b) => a / b; + /// + public BigInteger CreateOne() => BigInteger.One; + /// + public BigInteger CreateZero() => BigInteger.Zero; + /// + public BigInteger Copy(BigInteger a) => a; + /// + public bool AreEqual(BigInteger a, BigInteger b) => a == b; + /// + public bool IsZero(BigInteger a) => a.IsZero; + /// + public string ToString(BigInteger a) => a.ToString(System.Globalization.CultureInfo.InvariantCulture); + /// + public byte[] Serialize(BigInteger a) => a.ToByteArray(); + /// + public BigInteger Deserialize(byte[] data) => new(data); + } + + /// + /// How a matrix and the arithmetic on it compile: a node becomes + /// a over its elements' common type, filled element by + /// element from the compiled elements, and a sum, difference, product, quotient or power + /// with a matrix in it becomes the tensor operation of the same name -- PiecewiseAdd, + /// MatrixMultiply, PiecewiseMultiply by a scalar, MatrixPower -- so + /// that the compiled delegate runs GenericTensor's arithmetic rather than a tree of scalar + /// operations. + /// + /// + /// The methods the compiled tree calls are the public generic ones below, closed over each + /// element type at compile time in the kit for that type and named through expression + /// trees, the way keeps its own tables: no generic type or + /// method is constructed at run time, which a trimmed or NativeAOT build could not do. + /// https://github.com/asc-community/AngouriMath/issues/363 + /// https://github.com/asc-community/AngouriMath/issues/526 + /// + public static class TensorCompilation + { + /// The closed methods for one element type. + private sealed class Kit + { + internal Type Tensor { get; } + internal MethodInfo Empty { get; } + internal MethodInfo Set { get; } + internal MethodInfo Add { get; } + internal MethodInfo Subtract { get; } + internal MethodInfo Multiply { get; } + internal MethodInfo AddScalar { get; } + internal MethodInfo SubtractScalar { get; } + 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); + + // 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. + private static MethodInfo Named(Expression e) => ((MethodCallExpression)e.Body).Method; + + internal static Kit For() where TWrapper : struct, IOperations + => new(typeof(GenTensor), + Named>>((r, c) => TensorCompilation.Empty(r, c)), + Named, int, T, GenTensor>>((t, i, v) => TensorCompilation.Set(t, i, v)), + Named, GenTensor, GenTensor>>((a, b) => TensorCompilation.Add(a, b)), + Named, GenTensor, GenTensor>>((a, b) => TensorCompilation.Subtract(a, b)), + Named, GenTensor, GenTensor>>((a, b) => TensorCompilation.Multiply(a, b)), + Named, T, GenTensor>>((a, s) => TensorCompilation.AddScalar(a, s)), + Named, T, GenTensor>>((a, s) => TensorCompilation.SubtractScalar(a, s)), + 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))); + } + + [ConstantField] + private static readonly Dictionary kits = new() + { + { typeof(double), Kit.For() }, + { typeof(float), Kit.For() }, + { typeof(Complex), Kit.For() }, + { typeof(int), Kit.For() }, + { typeof(long), Kit.For() }, + { typeof(BigInteger), Kit.For() }, + }; + + [ConstantField] + private static readonly Dictionary kitsByTensor = kits.Values.ToDictionary(kit => kit.Tensor); + + /// The tensor type a matrix over compiles to, or where there is none. + public static Type? TensorTypeOf(Type element) + => kits.TryGetValue(element, out var kit) ? kit.Tensor : null; + + internal static bool IsTensor(Type type) => kitsByTensor.ContainsKey(type); + + private static Type ElementOf(Type tensor) => tensor.GetGenericArguments()[0]; + + /// A matrix of the shape, to be filled. + public static GenTensor Empty(int rows, int columns) where TWrapper : struct, IOperations + => GenTensor.CreateMatrix(rows, columns); + /// The matrix with its element at a row-major index set; the same matrix, for chaining. + public static GenTensor Set(GenTensor matrix, int index, T value) where TWrapper : struct, IOperations + { + matrix[index / matrix.Shape[1], index % matrix.Shape[1]] = value; + return matrix; + } + /// Element by element. + public static GenTensor Add(GenTensor a, GenTensor b) where TWrapper : struct, IOperations + => GenTensor.PiecewiseAdd(a, b); + /// Element by element. + public static GenTensor Subtract(GenTensor a, GenTensor b) where TWrapper : struct, IOperations + => GenTensor.PiecewiseSubtract(a, b); + /// The matrix product. + public static GenTensor Multiply(GenTensor a, GenTensor b) where TWrapper : struct, IOperations + => GenTensor.MatrixMultiply(a, b); + /// The scalar added to every element. + public static GenTensor AddScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations + => GenTensor.PiecewiseAdd(a, scalar); + /// The scalar subtracted from every element. + public static GenTensor SubtractScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations + => GenTensor.PiecewiseSubtract(a, scalar); + /// Every element subtracted from the scalar. + public static GenTensor SubtractFromScalar(T scalar, GenTensor a) where TWrapper : struct, IOperations + => GenTensor.PiecewiseSubtract(scalar, a); + /// Every element times the scalar. + public static GenTensor MultiplyScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations + => GenTensor.PiecewiseMultiply(a, scalar); + /// Every element over the scalar. + public static GenTensor DivideScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations + => GenTensor.PiecewiseDivide(a, scalar); + /// The scalar over every element. + public static GenTensor DivideFromScalar(T scalar, GenTensor a) where TWrapper : struct, IOperations + => GenTensor.PiecewiseDivide(scalar, a); + /// The matrix power, a whole exponent. + public static GenTensor Power(GenTensor a, int exponent) where TWrapper : struct, IOperations + => a.MatrixPower(exponent); + + /// The compiled form of a matrix node over the given compiled elements, all of one type. + internal static Expression Build(Entity.Matrix matrix, IReadOnlyList elements) + { + var element = elements[0].Type; + if (!kits.TryGetValue(element, out var kit)) + throw new UncompilableNodeException($"A matrix over {element} has no compiled form; the elements compile to double, float, Complex, int, long or BigInteger"); + Expression tensor = Expression.Call(kit.Empty, Expression.Constant(matrix.RowCount), Expression.Constant(matrix.ColumnCount)); + for (var i = 0; i < elements.Count; i++) + tensor = Expression.Call(kit.Set, tensor, Expression.Constant(i), elements[i]); + return tensor; + } + + /// + /// The compiled form of a binary node one of whose operands is a tensor, or + /// where the node has none. + /// + internal static Expression? Binary(Entity node, Expression left, Expression right, Func convert) + { + var leftTensor = kitsByTensor.TryGetValue(left.Type, out var leftKit); + var rightTensor = kitsByTensor.TryGetValue(right.Type, out var rightKit); + if (leftTensor && rightTensor) + { + if (left.Type != right.Type) + throw new UncompilableNodeException($"Two matrices over different element types, {ElementOf(left.Type)} and {ElementOf(right.Type)}, cannot be combined"); + return node switch + { + Entity.Sumf => Expression.Call(leftKit!.Add, left, right), + Entity.Minusf => Expression.Call(leftKit!.Subtract, left, right), + Entity.Mulf => Expression.Call(leftKit!.Multiply, left, right), + _ => null, + }; + } + if (leftTensor) + { + var scalar = node is Entity.Powf ? convert(right, typeof(int)) : convert(right, ElementOf(left.Type)); + return node switch + { + Entity.Sumf => Expression.Call(leftKit!.AddScalar, left, scalar), + Entity.Minusf => Expression.Call(leftKit!.SubtractScalar, left, scalar), + Entity.Mulf => Expression.Call(leftKit!.MultiplyScalar, left, scalar), + Entity.Divf => Expression.Call(leftKit!.DivideScalar, left, scalar), + Entity.Powf => Expression.Call(leftKit!.Power, left, scalar), + _ => null, + }; + } + var value = convert(left, ElementOf(right.Type)); + return node switch + { + 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), + _ => null, + }; + } + } +} diff --git a/Sources/Tests/UnitTests/Common/CompileMatrixTest.cs b/Sources/Tests/UnitTests/Common/CompileMatrixTest.cs new file mode 100644 index 000000000..e0804436b --- /dev/null +++ b/Sources/Tests/UnitTests/Common/CompileMatrixTest.cs @@ -0,0 +1,106 @@ +// +// 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 System.Numerics; +using AngouriMath; +using AngouriMath.Core.Compilation.IntoLinq; +using AngouriMath.Core.Exceptions; +using AngouriMath.Extensions; +using GenericTensor.Core; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A matrix compiles to a over its elements' type, and + /// the arithmetic on it to GenericTensor's operations: matrices are values a compiled + /// delegate takes and returns. #526 + /// + [Trait("Area", "Common")] + public sealed class CompileMatrixTest + { + private static GenTensor Doubles(params double[][] rows) + => GenTensor.CreateMatrix(rows.Length, rows[0].Length, (r, c) => rows[r][c]); + + private static void AssertSame(GenTensor expected, GenTensor actual) + { + Assert.Equal(expected.Shape, actual.Shape); + for (var r = 0; r < expected.Shape[0]; r++) + for (var c = 0; c < expected.Shape[1]; c++) + Assert.Equal(expected[r, c], actual[r, c], 12); + } + + [Fact] + public void AMatrixOfExpressionsCompilesToATensor() + { + var f = "[[x, 2 x], [x + 1, x^2]]".ToEntity().Compile>("x"); + AssertSame(Doubles(new[] { 3.0, 6.0 }, new[] { 4.0, 9.0 }), f(3)); + } + + [Fact] + public void TheProductOfTwoMatrixArgumentsIsTheMatrixProduct() + { + var f = "A * B".ToEntity().Compile, GenTensor, GenTensor>("A", "B"); + var a = Doubles(new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 }); + var b = Doubles(new[] { 0.0, 1.0 }, new[] { 1.0, 0.0 }); + AssertSame(Doubles(new[] { 2.0, 1.0 }, new[] { 4.0, 3.0 }), f(a, b)); + } + + [Fact] + public void ScalarsAndMatricesMix() + { + var f = "2 A + B - 1".ToEntity().Compile, GenTensor, GenTensor>("A", "B"); + var a = Doubles(new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 }); + var b = Doubles(new[] { 10.0, 10.0 }, new[] { 10.0, 10.0 }); + AssertSame(Doubles(new[] { 11.0, 13.0 }, new[] { 15.0, 17.0 }), f(a, b)); + } + + [Fact] + public void AScalarVariableScalesALiteralMatrix() + { + var f = "x * [[1, 2], [3, 4]] / 2".ToEntity().Compile>("x"); + AssertSame(Doubles(new[] { 2.0, 4.0 }, new[] { 6.0, 8.0 }), f(4)); + } + + [Fact] + public void AMatrixPowerIsTheMatrixPower() + { + var f = "A ^ 3".ToEntity().Compile, GenTensor>("A"); + var a = Doubles(new[] { 1.0, 1.0 }, new[] { 0.0, 1.0 }); + AssertSame(Doubles(new[] { 1.0, 3.0 }, new[] { 0.0, 1.0 }), f(a)); + } + + [Fact] + public void AnExpressionOfMatricesWithAScalarValueStillCompilesToTheScalar() + { + // #425's case: the matrix is simplified away before compiling, as before. + var f = "[[1, 0]] * [[a, b], [c, d]] * [[0], [1]]".ToEntity().Compile("a", "b", "c", "d"); + Assert.Equal(2.0, f(1, 2, 3, 4)); + } + + [Fact] + public void ComplexAndIntegerElementsHaveTheirOwnTensors() + { + var g = "[[x, i x]]".ToEntity().Compile>("x"); + var z = g(new Complex(1, 1)); + Assert.Equal(new Complex(1, 1), z[0, 0]); + Assert.Equal(new Complex(-1, 1), z[0, 1]); + var h = "[[n, 2 n]]".ToEntity().Compile>("n"); + Assert.Equal(6L, h(3)[0, 1]); + Assert.Equal(typeof(GenTensor), TensorCompilation.TensorTypeOf(typeof(double))); + Assert.Null(TensorCompilation.TensorTypeOf(typeof(string))); + } + + [Fact] + public void AMatrixIsNotANumberAndSaysSo() + { + Assert.Throws(() => "[[x, 2 x]]".ToEntity().Compile("x")); + Assert.Throws(() => "sin(A)".ToEntity().Compile, GenTensor>("A")); + } + } +} diff --git a/Sources/Tests/UnitTests/Common/MatrixCompilationTest.cs b/Sources/Tests/UnitTests/Common/MatrixCompilationTest.cs index 10b7d0b93..b638a31ca 100644 --- a/Sources/Tests/UnitTests/Common/MatrixCompilationTest.cs +++ b/Sources/Tests/UnitTests/Common/MatrixCompilationTest.cs @@ -13,8 +13,10 @@ namespace AngouriMath.Tests.Common { /// - /// Compiling expressions that mention matrices. A matrix has no compiled form, but an - /// expression built out of matrices often has a value that is an ordinary number. + /// Compiling expressions that mention matrices. An expression built out of matrices often + /// has a value that is an ordinary number, and is simplified to it before compiling; one + /// whose value is a matrix compiles to a tensor (CompileMatrixTest) and not to a + /// number, and says so. /// [Trait("Area", "Common")] public sealed class MatrixCompilationTest @@ -38,15 +40,18 @@ public void ScalarProductOfVectorsCompiles() Assert.Equal(1 * 3 + 2 * 4, compiled(3, 4), 9); } + // A matrix-valued expression asked for as a number: the value is a tensor, and the + // message names the type asked for. https://github.com/asc-community/AngouriMath/issues/526 [Theory] - [InlineData("[[a, b], [c, d]]")] - [InlineData("[[a, b], [c, d]] * [1, 0]")] - [InlineData("[a, b]")] - public void MatrixValuedExpressionsSayTheyCannotBeCompiled(string expression) + [InlineData("[[a, 2 a], [3 a, 4 a]]")] + [InlineData("[[a, a], [a, a]] * [1, 0]")] + [InlineData("[a, 2 a]")] + public void MatrixValuedExpressionsSayTheyCannotBeCompiledAsANumber(string expression) { var thrown = Assert.Throws( () => expression.ToEntity().Compile("a")); - Assert.Contains("Matrix", thrown.Message); + Assert.Contains("GenTensor", thrown.Message); + Assert.Contains("Double", thrown.Message); } // Expressions with no matrix in them must be unaffected -- they do not go anywhere diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt index deaaf287f..4ed6908d4 100644 --- a/Sources/Tests/UnitTests/Common/PublicApi.txt +++ b/Sources/Tests/UnitTests/Common/PublicApi.txt @@ -41,6 +41,19 @@ AngouriMath.Core.Budgets.WorkBudget.Unlimited { } : AngouriMath.Core.Budgets.Wor AngouriMath.Core.Budgets.WorkBudget.ctor() AngouriMath.Core.Budgets.WorkBudget.op_Equality(AngouriMath.Core.Budgets.WorkBudget, AngouriMath.Core.Budgets.WorkBudget) : System.Boolean AngouriMath.Core.Budgets.WorkBudget.op_Inequality(AngouriMath.Core.Budgets.WorkBudget, AngouriMath.Core.Budgets.WorkBudget) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Add(System.Numerics.BigInteger, System.Numerics.BigInteger) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.AreEqual(System.Numerics.BigInteger, System.Numerics.BigInteger) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Copy(System.Numerics.BigInteger) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.CreateOne() : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.CreateZero() : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Deserialize(System.Byte[]) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Divide(System.Numerics.BigInteger, System.Numerics.BigInteger) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.IsZero(System.Numerics.BigInteger) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Multiply(System.Numerics.BigInteger, System.Numerics.BigInteger) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Negate(System.Numerics.BigInteger) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Serialize(System.Numerics.BigInteger) : System.Byte[] +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.Subtract(System.Numerics.BigInteger, System.Numerics.BigInteger) : System.Numerics.BigInteger +AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations.ToString(System.Numerics.BigInteger) : System.String AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.$() : AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.ConvertBinaryNode(System.Linq.Expressions.Expression, System.Linq.Expressions.Expression, AngouriMath.Entity) : System.Linq.Expressions.Expression AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.ConvertConstant(AngouriMath.Entity) : System.Linq.Expressions.Expression @@ -58,6 +71,84 @@ AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.ctor() AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.ctor(AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol) AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.op_Equality(AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol, AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol) : System.Boolean AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.op_Inequality(AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol, AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Add(System.Numerics.Complex, System.Numerics.Complex) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.AreEqual(System.Numerics.Complex, System.Numerics.Complex) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Copy(System.Numerics.Complex) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.CreateOne() : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.CreateZero() : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Deserialize(System.Byte[]) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Divide(System.Numerics.Complex, System.Numerics.Complex) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.IsZero(System.Numerics.Complex) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Multiply(System.Numerics.Complex, System.Numerics.Complex) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Negate(System.Numerics.Complex) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Serialize(System.Numerics.Complex) : System.Byte[] +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.Subtract(System.Numerics.Complex, System.Numerics.Complex) : System.Numerics.Complex +AngouriMath.Core.Compilation.IntoLinq.ComplexOperations.ToString(System.Numerics.Complex) : System.String +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Add(System.Double, System.Double) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.AreEqual(System.Double, System.Double) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Copy(System.Double) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.CreateOne() : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.CreateZero() : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Deserialize(System.Byte[]) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Divide(System.Double, System.Double) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.IsZero(System.Double) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Multiply(System.Double, System.Double) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Negate(System.Double) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Serialize(System.Double) : System.Byte[] +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.Subtract(System.Double, System.Double) : System.Double +AngouriMath.Core.Compilation.IntoLinq.DoubleOperations.ToString(System.Double) : System.String +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Add(System.Int32, System.Int32) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.AreEqual(System.Int32, System.Int32) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Copy(System.Int32) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.CreateOne() : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.CreateZero() : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Deserialize(System.Byte[]) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Divide(System.Int32, System.Int32) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.IsZero(System.Int32) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Multiply(System.Int32, System.Int32) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Negate(System.Int32) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Serialize(System.Int32) : System.Byte[] +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.Subtract(System.Int32, System.Int32) : System.Int32 +AngouriMath.Core.Compilation.IntoLinq.Int32Operations.ToString(System.Int32) : System.String +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Add(System.Int64, System.Int64) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.AreEqual(System.Int64, System.Int64) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Copy(System.Int64) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.CreateOne() : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.CreateZero() : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Deserialize(System.Byte[]) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Divide(System.Int64, System.Int64) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.IsZero(System.Int64) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Multiply(System.Int64, System.Int64) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Negate(System.Int64) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Serialize(System.Int64) : System.Byte[] +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.Subtract(System.Int64, System.Int64) : System.Int64 +AngouriMath.Core.Compilation.IntoLinq.Int64Operations.ToString(System.Int64) : System.String +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Add(System.Single, System.Single) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.AreEqual(System.Single, System.Single) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Copy(System.Single) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.CreateOne() : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.CreateZero() : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Deserialize(System.Byte[]) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Divide(System.Single, System.Single) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.IsZero(System.Single) : System.Boolean +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Multiply(System.Single, System.Single) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Negate(System.Single) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Serialize(System.Single) : System.Byte[] +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.Subtract(System.Single, System.Single) : System.Single +AngouriMath.Core.Compilation.IntoLinq.SingleOperations.ToString(System.Single) : System.String +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.Add(GenTensor, GenTensor) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.AddScalar(GenTensor, T) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.DivideFromScalar(T, GenTensor) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.DivideScalar(GenTensor, T) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.Empty(System.Int32, System.Int32) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.Multiply(GenTensor, GenTensor) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.MultiplyScalar(GenTensor, T) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.Power(GenTensor, System.Int32) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.Set(GenTensor, System.Int32, T) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.Subtract(GenTensor, GenTensor) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.SubtractFromScalar(T, GenTensor) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.SubtractScalar(GenTensor, T) : GenTensor +AngouriMath.Core.Compilation.IntoLinq.TensorCompilation.TensorTypeOf(System.Type) : System.Type AngouriMath.Core.CostModel.$() : AngouriMath.Core.CostModel AngouriMath.Core.CostModel.All { } : System.Collections.Generic.IReadOnlyList AngouriMath.Core.CostModel.Cost { } : System.Func @@ -2746,6 +2837,7 @@ class AngouriMath.Core.Budgets.BudgetOutcome class AngouriMath.Core.Budgets.BudgetRecording class AngouriMath.Core.Budgets.WorkBudget class AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol +class AngouriMath.Core.Compilation.IntoLinq.TensorCompilation class AngouriMath.Core.CostModel class AngouriMath.Core.EquationSystem class AngouriMath.Core.Exceptions.AngouriBugException @@ -2914,6 +3006,12 @@ interface AngouriMath.Core.ILatexizeable interface AngouriMath.Core.IScalarClosedArithmetics interface AngouriMath.Core.IUnaryNode struct AngouriMath.Convenience.Setting +struct AngouriMath.Core.Compilation.IntoLinq.BigIntegerOperations +struct AngouriMath.Core.Compilation.IntoLinq.ComplexOperations +struct AngouriMath.Core.Compilation.IntoLinq.DoubleOperations +struct AngouriMath.Core.Compilation.IntoLinq.Int32Operations +struct AngouriMath.Core.Compilation.IntoLinq.Int64Operations +struct AngouriMath.Core.Compilation.IntoLinq.SingleOperations struct AngouriMath.Core.Transformations.DerivationStep struct AngouriMath.Core.Transformations.RewriteStep struct AngouriMath.Core.Transformations.TransformationResult From 075524da078b703f0ecf640fcc2caf5cffb21561 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 18 Sep 2026 22:16:57 +0000 Subject: [PATCH 2/2] The compiled matrix arithmetic is plain loops over the wrapper, not GenericTensor's expression-compiled ones, so the NativeAOT gate passes The first push failed TrimmingAndAot on both platforms: GenTensor.PiecewiseAdd, MatrixMultiply and the rest build and compile an expression tree for their loops, which ILC reports as IL3050 the moment they are reachable -- and Kit.For roots them for six element types. Add, Subtract, Multiply, the scalar operations and Power are now loops over the wrapper's Add and Multiply, and Power is by repeated squaring, refusing a negative exponent. GenTensor stays the container. The smoke test compiles a matrix expression now, so the gate covers this path from here on. Published natively and run locally: 45 checks, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Compilation/IntoLinq/TensorCompilation.cs | 83 ++++++++++++++++--- Sources/Tests/AotSmokeTest/Program.cs | 10 +++ 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs index deacf1620..287671e14 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs @@ -309,36 +309,97 @@ public static GenTensor Set(GenTensor mat matrix[index / matrix.Shape[1], index % matrix.Shape[1]] = value; return matrix; } + + // The arithmetic is written as plain loops over the elements rather than handed to + // GenericTensor's PiecewiseAdd, MatrixMultiply and the rest: those build and compile an + // expression tree for their loops, which is exactly what a NativeAOT build cannot do, + // and the AOT smoke test refuses the assembly the moment they are reachable. A loop over + // a wrapper's Add is what they compile to anyway. + + private static void SameShape(GenTensor a, GenTensor b) where TWrapper : struct, IOperations + { + if (a.Shape[0] != b.Shape[0] || a.Shape[1] != b.Shape[1]) + throw new InvalidMatrixOperationException($"Matrices of shapes {a.Shape} and {b.Shape} cannot be combined element by element"); + } + + private static GenTensor Elementwise(GenTensor a, Func map) where TWrapper : struct, IOperations + { + var result = GenTensor.CreateMatrix(a.Shape[0], a.Shape[1]); + for (var i = 0; i < a.Shape[0]; i++) + for (var j = 0; j < a.Shape[1]; j++) + result[i, j] = map(a[i, j]); + return result; + } + + private static GenTensor Elementwise(GenTensor a, GenTensor b, Func combine) where TWrapper : struct, IOperations + { + SameShape(a, b); + var result = GenTensor.CreateMatrix(a.Shape[0], a.Shape[1]); + for (var i = 0; i < a.Shape[0]; i++) + for (var j = 0; j < a.Shape[1]; j++) + result[i, j] = combine(a[i, j], b[i, j]); + return result; + } + /// Element by element. public static GenTensor Add(GenTensor a, GenTensor b) where TWrapper : struct, IOperations - => GenTensor.PiecewiseAdd(a, b); + => Elementwise(a, b, static (x, y) => default(TWrapper).Add(x, y)); /// Element by element. public static GenTensor Subtract(GenTensor a, GenTensor b) where TWrapper : struct, IOperations - => GenTensor.PiecewiseSubtract(a, b); + => Elementwise(a, b, static (x, y) => default(TWrapper).Subtract(x, y)); /// The matrix product. public static GenTensor Multiply(GenTensor a, GenTensor b) where TWrapper : struct, IOperations - => GenTensor.MatrixMultiply(a, b); + { + if (a.Shape[1] != b.Shape[0]) + throw new InvalidMatrixOperationException($"Matrices of shapes {a.Shape} and {b.Shape} cannot be multiplied"); + var ops = default(TWrapper); + var result = GenTensor.CreateMatrix(a.Shape[0], b.Shape[1]); + for (var i = 0; i < a.Shape[0]; i++) + for (var j = 0; j < b.Shape[1]; j++) + { + var sum = ops.CreateZero(); + for (var k = 0; k < a.Shape[1]; k++) + sum = ops.Add(sum, ops.Multiply(a[i, k], b[k, j])); + result[i, j] = sum; + } + return result; + } /// The scalar added to every element. public static GenTensor AddScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations - => GenTensor.PiecewiseAdd(a, scalar); + => Elementwise(a, x => default(TWrapper).Add(x, scalar)); /// The scalar subtracted from every element. public static GenTensor SubtractScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations - => GenTensor.PiecewiseSubtract(a, scalar); + => Elementwise(a, x => default(TWrapper).Subtract(x, scalar)); /// Every element subtracted from the scalar. public static GenTensor SubtractFromScalar(T scalar, GenTensor a) where TWrapper : struct, IOperations - => GenTensor.PiecewiseSubtract(scalar, a); + => Elementwise(a, x => default(TWrapper).Subtract(scalar, x)); /// Every element times the scalar. public static GenTensor MultiplyScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations - => GenTensor.PiecewiseMultiply(a, scalar); + => Elementwise(a, x => default(TWrapper).Multiply(x, scalar)); /// Every element over the scalar. public static GenTensor DivideScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations - => GenTensor.PiecewiseDivide(a, scalar); + => Elementwise(a, x => default(TWrapper).Divide(x, scalar)); /// The scalar over every element. public static GenTensor DivideFromScalar(T scalar, GenTensor a) where TWrapper : struct, IOperations - => GenTensor.PiecewiseDivide(scalar, a); - /// The matrix power, a whole exponent. + => Elementwise(a, x => default(TWrapper).Divide(scalar, x)); + /// The matrix power, a non-negative whole exponent, by repeated squaring. public static GenTensor Power(GenTensor a, int exponent) where TWrapper : struct, IOperations - => a.MatrixPower(exponent); + { + if (a.Shape[0] != a.Shape[1]) + throw new InvalidMatrixOperationException($"A matrix of shape {a.Shape} is not square and has no power"); + if (exponent < 0) + throw new NotSupportedException("A negative matrix power is not compiled: invert the matrix first"); + var result = GenTensor.CreateIdentityMatrix(a.Shape[0]); + var square = a; + for (var remaining = exponent; remaining > 0; remaining >>= 1) + { + if ((remaining & 1) == 1) + result = Multiply(result, square); + if (remaining > 1) + square = Multiply(square, square); + } + return result; + } /// The compiled form of a matrix node over the given compiled elements, all of one type. internal static Expression Build(Entity.Matrix matrix, IReadOnlyList elements) diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs index d4f6a41ec..d57b80b36 100644 --- a/Sources/Tests/AotSmokeTest/Program.cs +++ b/Sources/Tests/AotSmokeTest/Program.cs @@ -11,7 +11,9 @@ using System.Linq; using System.Numerics; using AngouriMath; +using AngouriMath.Core.Compilation.IntoLinq; using AngouriMath.Extensions; +using GenericTensor.Core; namespace AngouriMath.AotSmokeTest { @@ -238,6 +240,14 @@ private static void Compilation() var piecewise = "piecewise(x provided x > 0, 0 provided x <= 0)".Compile(x); Check("compiled piecewise at -3 and 3", piecewise(-3) == 0 && piecewise(3) == 3, $"{piecewise(-3)}, {piecewise(3)}"); + + // A matrix, which compiles to a tensor: the arithmetic on it must stay ahead-of-time + // compilable, which GenericTensor's own expression-compiled loops are not. + // https://github.com/asc-community/AngouriMath/issues/526 + var matrix = "[[x, 2 x], [x + 1, x^2]] * [[1, 0], [0, 1]] + 1".Compile>(x); + var at3 = matrix(3); + Check("compiled matrix at 3", at3[0, 0] == 4 && at3[0, 1] == 7 && at3[1, 0] == 5 && at3[1, 1] == 10, + $"[[{at3[0, 0]}, {at3[0, 1]}], [{at3[1, 0]}, {at3[1, 1]}]]"); } } }