Skip to main content

Creating and Manipulating Arrays and Matrices

This guide introduces the core containers provided by the Linear Algebra module, how to inspect them, and the day-to-day tools you will reach for when preparing data. It complements the Quickstart introduction by diving deeper into the object model and the rich API that both arrays and matrices expose.

Containers at a glance

Linear Algebra exposes two closely related container families:

  • Arrays (RealArray, ComplexArray, ...) favour element-wise semantics. Operators such as *, /, and functions like sin() act independently on each entry.
  • Matrices (RealMatrix, ComplexMatrix, ...) adopt linear-algebra semantics. The binary * performs matrix multiplication and matrix-oriented routines (factorisations, solving systems, ...) live here.

We refer to both as dense containers because they store all elements explicitly in memory (as opposed to sparse representations). We will use the term dense container interchangeably with arrays and matrices in this document.

You can move freely between the two worlds:

var A = matrix([[1, 2], [3, 4]])
var elementwise = A.array() // intrinsic converter to array semantics
var same_array = array(A) // intrinsic converter to array semantics
var squared = elementwise * elementwise
var back = matrix(squared) // regain matrix semantics when needed

Every constructor accepts type specifiers that combine a container shape with a scalar domain:

  • Containers: as_array, as_matrix
  • Scalars: as_bool, as_integer, as_real, as_complex

They compose through bitwise-or:

var real_grid = zeros([3, 4], as_array | as_real)
var complex_eye = identity([3, 3], as_matrix | as_complex)
var int_counts = array([[1, 2], [3, 4]], as_integer)

Factory helpers default to complex matrices, so be explicit whenever you care about the scalar domain.

tip

When should you use arrays versus matrices? Choose arrays when you need element-wise operations (e.g., applying functions to each element, computing element-wise products). Choose matrices when you need linear algebra operations (e.g., matrix multiplication, solving linear systems, eigenvalue decomposition). You can always convert between them using array() and matrix() methods, so don't worry too much about making the wrong choice initially.

Inspecting a container

Arrays and matrices shared a common set inspection facilities. The following attributes and methods enable learning more about the kind and type of a container:

PurposePropertyNotes
Scalar domainx.is_bool, x.is_integer, x.is_real, x.is_complexBoolean checks exposing the compile-time scalar type
Scalar descriptorx.scalar_typeReturns the Aleph type tag (as_real, as_complex, ...)
Container descriptorx.container_typeDistinguishes array vs matrix storage
Shape and size informationx.rows(), x.cols(), x.size(), x.shape()size = product(shape); vectors are stored as column matrices
Container kindx.is_array(), x.is_matrix()Reflect element-wise vs linear-algebra semantics
Vector testsx.is_vector(), x.is_column_vector(), x.is_row_vector()Check if 1D matrices
Scalar testx.is_scalar()True for 1×1 containers

Clarifications

The scalar kind predicates are mutually exclusive. Exactly one of is_bool, is_integer, is_real, or is_complex is true for a given container. For instance, if is_integer is true then is_real is false. Integers are not treated as a subtype of reals in these checks, even though they are a subset of the real numbers mathematically speaking.

var Y = ones([4, 4], as_array | as_integer)
print(Y.is_integer()) // true
print(Y.is_real()) // false

scalar_type yields a type tag like as_bool, as_integer, as_real, or as_complex, and container_type yields a tag like as_array or as_matrix. You can use these tags with the factory helpers to recreate containers with the same kind and scalar type.

var X = random([2, 3], as_array | as_real)
print(X.is_array()) // true
print(X.scalar_type) // as_real
print(X.container_type) // as_array
var Z = zeros([2, 3], X.container_type | X.scalar_type) // same container and scalar kind as X

shape() returns the shape of the container. For matrices, this is always a two-element list equal to [rows, cols]. This is also true for arrays at the moment, although it shall depend on dimensionality in the near future when we introduce multi-dimensional arrays. In any case, the product of the shape elements equals size().

var A = random([2, 3], as_array | as_real)
print(A.shape()) // [2, 3]
print(A.size()) // 6

We refer loosely to matrices with a single column or row as vectors. Vectors are really n×1 matrices (column vectors), which means they have matrix semantics (e.g., they can be multiplied by other matrices). Vector tests (is_vector, is_column_vector, is_row_vector) therefore apply to matrices only; arrays always return false for these predicates.

var V = vector([1, 2, 3])
print(V.is_vector()) // true
print(V.is_column_vector()) // true
print(V.is_row_vector()) // false
print(V.transposed().is_row_vector()) // true
print(array(V).is_vector()) // false

In any case, one can always query whether a container is an array or a matrix with is_array() and is_matrix():

var X = random([2, 3], as_array | as_real)
print(X.is_array()) // true
print(X.is_matrix()) // false
var M = matrix(X)
print(M.is_array()) // false
print(M.is_matrix()) // true

And finally, it is sometimes useful to turn a 1×1 container into a scalar value, for instance after computing the matrix product of a row-vector and a column-vector (dot product). You can check whether the container has a single element with is_scalar() and extract the value with the value() method:

var a = vector([1, 2, 3])
var b = vector([4, 5, 6]).transposed()
var dot_product = b * a // 1x1 matrix
if (dot_product.is_scalar()) {
print(dot_product.value()) // 32
}
tip

The last example is not the most efficient way to compute a dot product. Use the dedicated dot(a, b) function instead.

Constructing arrays and matrices

An important aspect of working with dense containers is creating them in the first place. The Linear Algebra module offers several complementary approaches to build arrays and matrices.

From literals

When learning a framework, it is often convenient to create small arrays and matrices from hard-coded values. This is the most straightforward approach for prototyping and testing. Suppose you want to learn how the various options and overloads of the sum function behave on different container types. You will likely want to create small test cases by hand and then experiment summing the whole array, summing along rows or columns, etc. These small test cases will have simple values that you can easily verify by hand to gain confidence in your understanding of the interface.

Aleph has a List container that stores a sequence of any objects. It is natural to use lists of homogeneous elements to create vectors, as shown below:

var v = [1, 2, 3]
var vec = vector(v)
tip

You can also create a vector directly from a literal without the intermediate variable:

var vec = vector([-100..101]);  // range literal

Here we use the range literal syntax start..end to create a list of integers from -100 to 100 (included).

As mentioned earlier, vectors are simply n×1 matrices, so the vector constructor is a thin wrapper over the matrix constructor. It is therefore natural to extend the previous example to matrices by using nested lists:

var arr = array([[1, 2, 3], [4, 5, 6]], as_real)
var mat = matrix([[1, 2], [3, 4]], as_real)

Think of each set of brackets as introducing a dimension. Since the inner list represents a column (not a row), [[1, 2, 3], [4, 5, 6]] builds a 3×2 container with the first column (1, 2, 3) and the second column (4, 5, 6). To create a 2×3 matrix instead, you would write:

var mat = matrix([[1, 4], [2, 5], [3, 6]], as_real)
tip

If you are used to row-major languages like Python or C, you can build matrices in row-major order and then transpose them to get the desired layout. For instance, the previous 2×3 matrix can also be created as:

var mat = matrix([[1, 2, 3], [4, 5, 6]], as_real).transposed()

This approach is particularly helpful when copying data from other environments or when thinking about the matrix row by row feels more natural.

info

The matrix created with matrix([[1, 2, 3], [4, 5, 6]], as_real) is a real matrix (scalar type as_real) with 2 rows and 3 columns. It will be printed as:

[[1, 2, 3],
[4, 5, 6]]

i.e. the same way you wrote it. This allows copying the input back into the factory function. If you would like to display the matrix row by row you can transpose it before printing:

var mat = matrix([[1, 2, 3], [4, 5, 6]], as_real)
print(mat.transpose())

Factory helpers

When you need to create containers of a specific shape filled with a pattern, the factory functions are your go-to tools. They are more convenient than building from literals when working with larger dimensions or when the initial values follow a simple rule (all zeros, all ones, random, etc.).

Each factory function accepts a shape argument and an optional type argument. The shape can be either [n] (creating an n×1 column vector) or [rows, cols] (creating an array or matrix with the specified dimensions). The type argument combines container and scalar type using the bitwise-or operator:

  • If you specify only the scalar type (e.g., as_real), the container defaults to a matrix.
  • If you specify only the container type (e.g., as_array), the scalar type defaults to complex.
  • If you omit the type entirely, you get a complex matrix by default.
var col = zeros([10], as_real)           // 10x1 column vector
var mat = zeros([10, 5], as_real) // 10x5 matrix
var default_z = zeros([2, 2]) // ComplexMatrix (default)
var explicit_z = zeros([2, 2], as_matrix | as_complex) // same thing

This default is convenient for quick prototyping but can be surprising if you forget to specify the type.

tip

When writing production code, it's good practice to always specify the type explicitly.

Zeros, ones, and filled

The most common patterns are zeros(shape, type), which fills a container with 0, and ones(shape, type), which fills it with 1. For other constants, use filled(shape, value, type):

var z = zeros([3, 4], as_matrix | as_real)
var o = ones([3, 4], as_array | as_complex)
var pi_grid = filled([2, 2], 3.14159, as_real)

Note that zeros and ones work for all scalar types (bool, integer, real, complex), while filled requires a value compatible with the target scalar type. For instance, you cannot fill a real container with a complex value, but you can fill an integer container with a real value (it will be truncated).

Creating uninitialized containers

The uninitialized(shape, type) function allocates memory without writing any initial values. This is the fastest way to create a container when you plan to immediately overwrite all elements anyway, for example in a loop or through an assignment. The contents are undefined until you set them.

var buffer = uninitialized([100, 100], as_array | as_real);
// buffer now holds 10,000 elements with undefined values
// typically you'd fill it immediately
buffer.set_constant(42.0);
warning

If you are not yet sure what values your container will hold, zeros is the safest way to initialize it. Unlike uninitialized, which leaves memory in an undefined state, zeros gives you a predictable starting point that won't cause undefined behavior if accidentally read before being overwritten.

Random values

The random(shape, type) function fills a container with pseudo-random values. For real types, values are uniformly distributed in [-1, 1]. For complex types, both the real and imaginary parts are drawn from [-1, 1].

var noise = random([5, 5], as_array | as_real)
// Each element is a random real number in [-1, 1]

Random containers are useful for testing, initialization of iterative algorithms, and generating synthetic data.

Identity matrices

The identity(shape, type) function creates an identity matrix (or rectangular matrix with ones on the diagonal and zeros elsewhere). This is particularly useful in linear algebra operations.

var I = identity([3, 3], as_matrix | as_real)  // 3x3 identity
var R = identity([3, 5], as_real) // 3x5 rectangular identity

For square matrices, the identity has ones along the main diagonal. For rectangular matrices, ones appear wherever the row index equals the column index.

Like-constructors

Once you have a reference container, you can create new containers with the same shape and type using the *_like family of methods. These are convenience methods that clone the metadata (shape, container type, and scalar type) while filling the new container according to different patterns:

var pattern = random([3, 2], as_array | as_real)
var uninit = pattern.uninitialized_like() // fastest, undefined values
var zeros_again = pattern.zeros_like() // filled with zeros
var ones_again = pattern.ones_like() // filled with ones
var threes = pattern.filled_like(3.0) // filled with a custom value
var rand_again = pattern.random_like() // filled with random values
var identity = pattern.identity_like() // identity pattern (diagonal ones)

These methods save you from having to manually extract and recombine the shape(), container_type, and scalar_type properties. They are particularly useful when writing generic code that needs to allocate temporary containers matching an input's characteristics.

info

The identity_like() method works on rectangular (non-square) matrices too, placing ones along the main diagonal wherever the row and column indices are equal, just like the identity() factory function.

Setters

Once a container has been allocated (for example, with zeros, uninitialized, or any other factory function), you can reset its contents in-place using the set_* family of methods. These methods modify the existing container without reallocating memory, making them efficient for reusing buffers in loops or iterative algorithms.

var buffer = uninitialized([3, 3], as_array | as_real)
buffer.set_zero() // all elements now zero
buffer.set_ones() // all elements now one
buffer.set_constant(42) // all elements now 42
buffer.fill(3.14159) // fill is a synonym for set_constant
buffer.set_random() // all elements now random in [0, 1]

The set_constant(value) method and its synonym fill(value) broadcast a scalar value to all elements of the container. This is particularly useful when initializing data to a specific non-zero, non-one value. For more details on constructing arrays with random numbers see here.

For matrices, you can also reset the container to an identity pattern using set_identity():

var mat = zeros([4, 4], as_matrix | as_real)
mat.set_identity() // diagonal elements now 1, others 0
info

Vectors can use set_unit(i) to be set to a particular basis vector (all zeros except element i which is set to 1):

var v = zeros([5], as_real)
v.set_unit(2) // v is now [0, 0, 1, 0, 0]

These in-place setters are designed to work seamlessly with the like-constructors: you can allocate a container with the desired shape and type using a like-constructor, then immediately fill it with the pattern you need.

Concatenation utilities

When you need to assemble larger containers from smaller building blocks, the concatenation functions provide a clean way to combine multiple containers along different dimensions.

Concatenating along rows

The concatenate_rows() function stacks containers vertically (one on top of another), concatenating along the row dimension. All containers must have the same number of columns:

var a0 = zeros([2, 2], as_real)
var a1 = ones([2, 2], as_real)
var combined = concatenate_rows([a0, a1, a0])
// Result is a 6x2 matrix:
// [[0,0,1,1,0,0],
// [0,0,1,1,0,0]]

Think of this as appending rows: the first container provides the first set of rows, the second provides the next set, and so on. Concatenating n containers of shape [r, c] produces a result of shape [n*r, c].

Concatenating along columns

The concatenate_columns() function stacks containers horizontally (side by side), concatenating along the column dimension. All containers must have the same number of rows:

var a0 = zeros([2, 2], as_real)
var a1 = ones([2, 2], as_real)
var combined = concatenate_columns([a0, a1, a0])
// Result is a 2x6 matrix:
// [[0,0],
// [0,0],
// [1,1],
// [1,1],
// [0,0],
// [0,0]]

Think of this as appending columns: the first container provides the first set of columns, the second provides the next set, and so on. Concatenating n containers of shape [r, c] produces a result of shape [r, n*c].

Block matrix construction

The more general concatenate() function accepts a list of lists, allowing you to build block matrices with arbitrary 2D tiling patterns. This is equivalent to using concatenate_rows() with an extra level of brackets:

var a0 = zeros([2, 2], as_real)
var a1 = ones([2, 2], as_real)

// Simple horizontal stacking (equivalent to concatenate_rows)
var row_stack = concatenate([[a0, a1, a0]])

// 2D block tiling
var block_matrix = concatenate([
[a0, a1, a0], // first block column
[a1, a0, a1], // second block column
[a0, a1, a0] // third block column
])
// Result is a 6x6 matrix with 3x3 blocks

Each inner list represents a block column. All blocks in the same row must have the same number of rows, and all blocks in the same column position must have the same number of columns. The function validates these constraints and throws informative errors if dimensions are incompatible.

Replication and tiling

When you need to create a block matrix where all the blocks are identical, the replicated() function provides a more convenient alternative to manually building lists for concatenate(). It tiles a single container multiple times along both dimensions:

var tile = linspaced(6, 0, 5).reshaped([2, 3])
// tile is [[0, 1], [2, 3], [4, 5]]
var tiled = tile.replicated(2, 3) // 2 rows of tiles, 3 columns of tiles
// Result is a 4x9 matrix (2*2 rows, 3*3 columns)
// [[0,1,0,1],
// [2,3,2,3],
// [4,5,4,5],
// [0,1,0,1],
// [2,3,2,3],
// [4,5,4,5],
// [0,1,0,1],
// [2,3,2,3],
// [4,5,4,5]]

For vectors, a single-argument overload is available that replicates the vector along its non-singleton dimension:

var col = vector([1, 2, 3], as_real)
var repeated = col.replicated(4) // repeats the column vector 4 times along the column dimension
// Result is a 12x1 column vector: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]

var row = col.transposed()
var repeated_row = row.replicated(4) // repeats the row vector 4 times along the row dimension
// Result is a 1x12 row vector: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]

This is particularly useful when you need to broadcast a pattern across a larger container, such as initializing a grid with a repeated template or creating test data with regular structure.

Shaping and reshaping data

Dense containers provide a rich set of methods to manipulate their shape and layout. It is often convenient to initialize data in one shape and then transform it to another without manually handling index arithmetic. For instance, in the process of learning about matrix operations, you might want to reshape a flat array into a matrix or flatten a matrix into a vector for certain computations.

var flat = linspaced(6, 0, 5)
var mat = flat.reshaped([2, 3]) // reshaped to 2 rows and 3 columns
var vec = mat.flattened() // flattened back to a column vector

The following methods cover common reshaping operations.

Appending and removing data

If you're familiar with Aleph Lists, which can grow and shrink dynamically, you might wonder whether dense containers support similar operations. Unlike lists, dense containers maintain a fixed rectangular structure and cannot resize themselves dynamically. However, you can achieve similar effects by creating new containers with the desired shape.

To append a row or column, use the concatenation utilities:

var mat = matrix([[1, 2], [3, 4]], as_real)
var new_row = vector([5, 6], as_real).transposed()
var extended_mat = concatenate_rows([mat, new_row]) // appends a row

Note that you cannot append individual elements—doing so would create a non-rectangular structure, which dense containers do not support.

To remove a row or column, use slicing or block extraction to create a new container without the unwanted data:

var mat = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], as_real)
var reduced_mat = mat[0:2, 0:2] // keeps only first 2 rows and 2 columns

Individual elements cannot be removed from within the container, as this would break the rectangular structure. If you need data structures with arbitrary insertion and deletion, consider using a different representation such as a sparse matrix.

Reshaping

reshape(shape) performs an in-place reshape that preserves the element count, while reshaped(shape) returns a copy. Shapes can be [n] (producing a column vector) or [rows, cols]:

var seq = linspaced(6, 0, 5)          // returns a matrix by default
var grid = seq.reshaped([2, 3]) // copy
grid.reshape([3, 2]) // in-place
warning

The product of the new shape dimensions must equal the total number of elements in the container. For example, you cannot reshape a 6-element container to [2, 2] (4 elements) or [3, 3] (9 elements)—only to shapes like [6, 1], [3, 2], [2, 3], or [1, 6] that preserve the element count.

To create a 1D copy of the data, you can always use reshaped([grid.size()]). There is also a dedicated method for this common use case: flatten() turns any dense container into a column vector in-place, while flattened() keeps the original intact.

var grid = array([[1,1,1], [2,2,2]], as_real)
var flat = grid.flattened() // copy as column vector
grid.flatten() // in-place flattening

Resizing

The resize(new_rows, new_cols) method mutates the allocation (and may reallocate memory). The new size is independent of the previous number of elements—be prepared to refill the data afterwards.

warning

Resizing a container invalidates its contents. The new container will have undefined values until you explicitly set them.

Shifts and flips

When working with dense containers, you often need to permute or rearrange their elements in systematic ways. A permutation changes the order or position of elements without changing the underlying data values themselves. Common examples include mirroring a container along one dimension, rotating its layout, or cyclically shifting its rows or columns. The Linear Algebra module provides a suite of methods that handle these permutations efficiently without requiring manual index arithmetic:

  • udflip() / udflipped() flips columns (top ↔ bottom).
  • lrflip() / lrflipped() flips rows (left ↔ right).
  • flip(dimension) and flipped(dimension) generalise the previous two.
  • rotate() / rotated() perform a 90° counter-clockwise rotation.
  • shift([row_offset, col_offset]) / shifted(...) apply periodic shifts (wrap-around).
info

These methods follow a consistent naming convention: the present tense form (e.g., reshape, transpose, rotate) mutates the container in-place, while the past tense form (e.g., reshaped, transposed, rotated) returns a new copy and leaves the original unchanged.

Flipping

Flipping a container mirrors its elements along a specified dimension. The most common use cases are vertical and horizontal flips:

var mat = matrix([[1, 2, 3], [4, 5, 6]], as_real)
// mat is [[1,2,3],
// [4,5,6]]

var vert = mat.udflipped() // up-down flip
// vert is [[4,5,6],
// [1,2,3]]

var horiz = mat.lrflipped() // left-right flip
// horiz is [[3,2,1],
// [6,5,4]]

The flip(dimension) method generalizes these operations: flip(0) is equivalent to udflip(), and flip(1) is equivalent to lrflip().

Rotation

The rotate() method permutes the container's elements by performing a 90° counter-clockwise rotation of the layout:

var mat = matrix([[1, 2, 3], [4, 5, 6]], as_real)
// mat is [[1,2,3],
// [4,5,6]]

var rotated_once = mat.rotated()
// rotated_once is [[3,6],
// [2,5],
// [1,4]]

var rotated_twice = rotated_once.rotated()
// rotated_twice is [[6,5,4],
// [3,2,1]]
warning

The rotate() method permutes the data layout within the container; it does not perform a geometric rotation of vectors in 2D or 3D space. For spatial rotations of coordinate data, use dedicated linear algebra transformations (rotation matrices) instead.

Shifting

The shift() method applies a periodic (wrap-around) shift along rows and columns:

var mat = matrix([[1, 2, 3], [4, 5, 6]], as_real)
// mat is [[1,2,3],
// [4,5,6]]

var shifted = mat.shifted([1, 1]) // shift by 1 row and 1 column
// shifted is [[6,4,5],
// [3,1,2]]

Positive offsets shift down and to the right, with elements wrapping around to the opposite edge. This is useful for implementing periodic boundary conditions or aligning data with circular symmetry.

tip

Shift operations are particularly useful in signal processing and image manipulation. For example, centering a Fourier transform typically requires shifting by half the dimensions, while implementing convolution with periodic boundaries often needs various shift amounts.

Diagonal and triangular extraction

Many linear algebra algorithms work with specific structural patterns in matrices. The Linear Algebra module provides functions to extract diagonals, create diagonal matrices, and extract triangular portions of matrices. These operations are fundamental for decompositions, solving systems of equations, and working with specialized matrix structures.

Extracting diagonals

The diagonal() method extracts a diagonal from a matrix as a column vector. Without arguments, it returns the main diagonal (index 0). You can specify an index to extract other diagonals:

var mat = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]], as_real)
// mat is [[1,2,3,4],
// [5,6,7,8],
// [9,10,11,12],
// [13,14,15,16]]

var main_diag = mat.diagonal() // [1, 6, 11, 16]
var above_diag = mat.diagonal(1) // [5, 10, 15] (one above main diagonal)
var below_diag = mat.diagonal(-1) // [2, 7, 12] (one below main diagonal)

Positive indices extract diagonals above the main diagonal, while negative indices extract diagonals below it. Remember that the indexing follows column-major conventions, where element mat[i, j] is on diagonal j - i.

Creating diagonal matrices

The as_diagonal() method takes a vector (1D matrix) and creates a square diagonal matrix with those values on the main diagonal:

var values = vector([1, 2, 3, 4], as_real)
var diag_mat = values.as_diagonal()
// diag_mat is [[1,0,0,0],
// [0,2,0,0],
// [0,0,3,0],
// [0,0,0,4]]

This is particularly useful when working with eigenvalues, singular values, or any situation where you need to construct a diagonal matrix from a list of values.

Triangular extraction

The upper_triangular() and lower_triangular() methods extract the upper and lower triangular portions of a matrix, zeroing out the elements below or above the diagonal respectively:

var mat = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], as_real)
// mat is [[1,2,3,4],
// [5,6,7,8],
// [9,10,11,12]]

var upper = mat.upper_triangular()
// upper is [[1,0,0,0],
// [5,6,0,0],
// [9,10,11,0]]

var lower = mat.lower_triangular()
// lower is [[1,2,3,4],
// [0,6,7,8],
// [0,0,11,12]]

Both methods accept an optional index argument to shift the boundary. This is useful for extracting banded matrices or creating Hessenberg forms:

var mat = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], as_real)

var upper_hessenberg = mat.upper_triangular(-1)
// upper_hessenberg is [[1,2,0,0],
// [5,6,7,0],
// [9,10,11,12]]
// One subdiagonal is kept, forming an upper Hessenberg matrix

var strict_upper = mat.upper_triangular(1)
// strict_upper is [[0,0,0,0],
// [5,0,0,0],
// [9,10,0,0]]
// Main diagonal zeroed, only strictly upper part kept

A positive index argument shifts the zero boundary upward (keeping more elements for upper_triangular, fewer for lower_triangular), while a negative index shifts it downward.

Testing for structure

The Linear Algebra module also provides predicate functions to test whether a matrix has diagonal or triangular structure. These methods accept an optional tolerance parameter for approximate comparisons:

var diag = identity([3, 3], as_real)
print(diag.is_diagonal()) // true

var upper = matrix([[1, 2, 3], [0, 4, 5], [0, 0, 6]], as_real)
print(upper.is_upper_triangular()) // true

var almost_lower = matrix([[1, 0, 0], [2, 3, 0], [4, 5, 1e-10]], as_real)
print(almost_lower.is_lower_triangular()) // false (strict check)
print(almost_lower.is_lower_triangular(1e-9)) // true (with tolerance)

These predicates complement the extraction functions, allowing you to verify assumptions about matrix structure before applying specialized algorithms that require diagonal or triangular inputs.

tip

When working with matrices resulting from numerical computations (e.g., factorizations), always use a tolerance parameter with these structure tests. Floating-point arithmetic can introduce small errors that make an algorithmically triangular matrix fail a strict structural check. A tolerance of 1e-12 is often appropriate for double-precision calculations.