From 302c48c54f2efacba75b2d90a676db3708ed3bb0 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 4 Aug 2026 06:42:11 +0000 Subject: [PATCH] Take part of a matrix by range (#443) a[1.., ..] is every row of a matrix but the first, a[..2, 1..3] the first two rows of its second and third columns, and a[^1.., ^1..] its bottom right entry. The result is an ordinary matrix sharing nothing with the one it came from. Both extents have to be written out, which is what the issue's own discussion asks for. A matrix indexed by one number gives a row while a vector indexed by one number gives an element, so a single range would have to mean one thing here and the other there; there is no one-range indexer to write, so the question cannot come up. An empty range is refused rather than answered with a matrix of no rows, since there is no such matrix; a range reaching past the matrix throws from the range itself. Ten tests, 3890 in all, 127 F#. --- .../Core/Entity/Omni/Entity.Matrix.cs | 33 +++++++ .../UnitTests/Algebra/MatrixRangeTest.cs | 98 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 Sources/Tests/UnitTests/Algebra/MatrixRangeTest.cs diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs index f6731ec53..ff8786a1d 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -144,6 +144,39 @@ private static Matrix ToMatrix(Entity entity) /// public Entity this[int x, int y] => InnerMatrix[x, y]; + /// + /// The submatrix spanned by the given rows and columns, for example + /// a[1.., ..] for every row of a matrix but the first, or + /// a[..2, 1..3] for the first two rows of its second and third columns. + /// + /// + /// Both extents have to be written out. A matrix indexed by one number gives a row + /// while a vector indexed by one number gives an element, so a single range would + /// have to mean one thing here and the other there -- + /// https://github.com/asc-community/AngouriMath/issues/443. + /// + /// + /// Thrown if either range reaches past the matrix. + /// + /// + /// Thrown if either range is empty, since a matrix has at least one row and one + /// column. + /// + public Matrix this[Range rows, Range columns] + { + get + { + var (firstRow, rowCount) = rows.GetOffsetAndLength(RowCount); + var (firstColumn, columnCount) = columns.GetOffsetAndLength(ColumnCount); + if (rowCount is 0 || columnCount is 0) + throw new InvalidMatrixOperationException( + $"A {rowCount}x{columnCount} matrix cannot be created; both extents must be non-empty"); + return new Matrix( + indices => InnerMatrix[firstRow + indices[0], firstColumn + indices[1]], + rowCount, columnCount); + } + } + /// /// Checks whether the matrix only contains one /// column. diff --git a/Sources/Tests/UnitTests/Algebra/MatrixRangeTest.cs b/Sources/Tests/UnitTests/Algebra/MatrixRangeTest.cs new file mode 100644 index 000000000..2f5785954 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/MatrixRangeTest.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) 2019-2022 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 AngouriMath; +using AngouriMath.Core.Exceptions; +using Xunit; + +namespace AngouriMath.Tests.Algebra +{ + /// + /// Taking part of a matrix by range, as asked for in + /// https://github.com/asc-community/AngouriMath/issues/443. + /// + public sealed class MatrixRangeTest + { + private static readonly Entity.Matrix ThreeByThree = MathS.Matrices.Matrix(3, 3, + 1, 2, 3, + 4, 5, 6, + 7, 8, 9 + ); + + private static readonly Entity.Matrix Column = MathS.Matrices.Vector(1, 2, 3); + + [Fact] + public void EveryRowButTheFirst() => + Assert.Equal(MathS.Matrices.Matrix(2, 3, + 4, 5, 6, + 7, 8, 9), ThreeByThree[1.., ..]); + + [Fact] + public void EveryColumnButTheLast() => + Assert.Equal(MathS.Matrices.Matrix(3, 2, + 1, 2, + 4, 5, + 7, 8), ThreeByThree[.., ..2]); + + [Fact] + public void AnInnerBlock() => + Assert.Equal(MathS.Matrices.Matrix(2, 2, + 5, 6, + 8, 9), ThreeByThree[1..3, 1..3]); + + [Fact] + public void CountedFromTheEnd() => + Assert.Equal(MathS.Matrices.Matrix(1, 1, 9), ThreeByThree[^1.., ^1..]); + + [Fact] + public void TheWholeMatrix() => + Assert.Equal(ThreeByThree, ThreeByThree[.., ..]); + + // A vector is a matrix of one column, so the second extent is what a vector is cut by + // rows with, and it has only the one column to ask for. + [Fact] + public void PartOfAVector() => + Assert.Equal(MathS.Matrices.Vector(2, 3), Column[1.., ..]); + + // The result is a matrix in its own right and nothing of the original is shared with + // it, so what is read out of it does not depend on the order the two were built in. + [Fact] + public void TheResultIsAMatrixLikeAnyOther() + { + var block = ThreeByThree[..2, 1..]; + Assert.Equal(2, block.RowCount); + Assert.Equal(2, block.ColumnCount); + Assert.Equal((Entity)6, block[1, 1]); + Assert.Equal(MathS.Matrices.Matrix(2, 2, + 2, 5, + 3, 6), block.T); + } + + [Fact] + public void SymbolicEntriesAreCarriedOverUntouched() => + Assert.Equal( + MathS.Matrices.Matrix(1, 2, "y", "x + 1"), + MathS.Matrices.Matrix(2, 2, + "x", "y", + "y", "x + 1")[1.., ..][.., ..]); + + [Fact] + public void AnEmptyRangeIsRefused() + { + Assert.Throws(() => ThreeByThree[1..1, ..]); + Assert.Throws(() => ThreeByThree[.., 2..2]); + } + + [Fact] + public void ARangeReachingPastTheMatrixIsRefused() + { + Assert.Throws(() => ThreeByThree[..4, ..]); + Assert.Throws(() => ThreeByThree[.., 1..5]); + } + } +}