Skip to content

panchi.algorithms

from panchi.algorithms import ref, rref, lu, qr_decomposition
from panchi.algorithms import RowSwap, RowScale, RowAdd
from panchi.algorithms import Reduction, LUDecomposition, QRDecomposition
from panchi.algorithms import InverseResult, Solution, EigenResult
from panchi.algorithms import inverse, solve, determinant, determinant_lu, eigen
from panchi.algorithms import rank, nullity, is_invertible, is_symmetric
from panchi.algorithms import span, standard_basis, column_space, row_space, null_space
from panchi.algorithms import dot, cross, orthogonal_complement
from panchi.algorithms import gram_schmidt, vector_projection

Row operations

panchi.algorithms.RowSwap

Bases: RowOperation

Elementary row operation: swap two rows.

Represents the operation R_a <-> R_b. The corresponding elementary matrix is the identity matrix with rows a and b exchanged.

Applying this operation twice returns the original matrix.

Parameters:

Name Type Description Default
row_a int

Index of the first row to swap (0-based).

required
row_b int

Index of the second row to swap (0-based).

required

Examples:

>>> m = Matrix([[1, 2], [3, 4], [5, 6]])
>>> op = RowSwap(0, 2)
>>> print(op.apply(m))
[[5, 6],
 [3, 4],
 [1, 2]]
>>> v = Vector([1, 2, 3])
>>> print(op.apply(v))
[3, 2, 1]
>>> print(op.elementary_matrix(3))
[[0, 0, 1],
 [0, 1, 0],
 [1, 0, 0]]
>>> print(op)
R0 <-> R2
>>> repr(op)
'RowSwap(row_a=0, row_b=2)'
Source code in panchi/algorithms/row_operations.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
class RowSwap(RowOperation):
    """
    Elementary row operation: swap two rows.

    Represents the operation R_a <-> R_b. The corresponding elementary
    matrix is the identity matrix with rows a and b exchanged.

    Applying this operation twice returns the original matrix.

    Parameters
    ----------
    row_a : int
        Index of the first row to swap (0-based).
    row_b : int
        Index of the second row to swap (0-based).

    Examples
    --------
    >>> m = Matrix([[1, 2], [3, 4], [5, 6]])
    >>> op = RowSwap(0, 2)
    >>> print(op.apply(m))
    [[5, 6],
     [3, 4],
     [1, 2]]
    >>> v = Vector([1, 2, 3])
    >>> print(op.apply(v))
    [3, 2, 1]
    >>> print(op.elementary_matrix(3))
    [[0, 0, 1],
     [0, 1, 0],
     [1, 0, 0]]
    >>> print(op)
    R0 <-> R2
    >>> repr(op)
    'RowSwap(row_a=0, row_b=2)'
    """

    def __init__(self, row_a: int, row_b: int) -> None:
        self.a = row_a
        self.b = row_b
        self._validate_index_types()

    def _validate_index_types(self) -> None:
        """
        Validate that both row indices are integers.

        Raises
        ------
        TypeError
            If either row index is not an integer.
        """
        if not isinstance(self.a, int):
            raise TypeError(
                f"Row index must be an integer. Got {type(self.a).__name__} for row_a."
            )

        if not isinstance(self.b, int):
            raise TypeError(
                f"Row index must be an integer. Got {type(self.b).__name__} for row_b."
            )

    def _validate_indices(self, n: int) -> None:
        """
        Validate that both row indices are in range for a matrix of size n.

        Parameters
        ----------
        n : int
            Number of rows in the target matrix or vector.

        Raises
        ------
        ValueError
            If either row index is outside [0, n - 1].
        """
        if not (0 <= self.a < n):
            raise ValueError(
                f"Row index {self.a} is out of range for a matrix with {n} rows. "
                f"Valid indices are 0 to {n - 1}."
            )

        if not (0 <= self.b < n):
            raise ValueError(
                f"Row index {self.b} is out of range for a matrix with {n} rows. "
                f"Valid indices are 0 to {n - 1}."
            )

    def elementary_matrix(self, n: int) -> Matrix:
        """
        Return the n×n elementary matrix for this row swap.

        Constructed by swapping rows a and b in the n×n identity matrix.
        This matrix has determinant -1, reflecting that row swaps reverse
        the orientation of the row space.

        Parameters
        ----------
        n : int
            Size of the elementary matrix. Must be at least 2, and large
            enough so that both row indices are in range.

        Returns
        -------
        Matrix
            An n×n matrix identical to the identity except that rows a
            and b are exchanged.

        Raises
        ------
        TypeError
            If n is not an integer.
        ValueError
            If n < 2 or either row index is out of range for n.

        Examples
        --------
        >>> op = RowSwap(0, 1)
        >>> print(op.elementary_matrix(3))
        [[0, 1, 0],
         [1, 0, 0],
         [0, 0, 1]]
        """
        self._validate_n(n)
        self._validate_indices(n)

        grid: Matrix = identity(n)
        row_a: list[Scalar] = grid[self.a].copy()
        row_b: list[Scalar] = grid[self.b].copy()
        grid[self.a] = row_b
        grid[self.b] = row_a

        return grid

    def apply(self, target: Matrix | Vector) -> Matrix | Vector:
        """
        Swap rows a and b of a matrix or vector, returning the result.

        Parameters
        ----------
        target : Matrix | Vector
            The matrix or vector to operate on.

        Returns
        -------
        Matrix | Vector
            A new matrix or vector with the two entries exchanged.

        Raises
        ------
        TypeError
            If target is not a Matrix or Vector instance.
        ValueError
            If either row index is out of range for this target.

        Examples
        --------
        >>> m = Matrix([[1, 2], [3, 4], [5, 6]])
        >>> print(RowSwap(0, 2).apply(m))
        [[5, 6],
         [3, 4],
         [1, 2]]
        >>> v = Vector([1, 2, 3])
        >>> print(RowSwap(0, 2).apply(v))
        [3, 2, 1]
        """
        self._validate_target(target)
        n = target.dims if isinstance(target, Vector) else target.rows
        self._validate_indices(n)

        if isinstance(target, Vector):
            result = target.copy()
            result[self.a], result[self.b] = target[self.b], target[self.a]
            return result

        return self.elementary_matrix(target.rows) @ target

    def inverse(self) -> RowSwap:
        """
        Return the inverse of this row swap.

        A row swap is its own inverse: swapping the same two rows a second
        time restores the original matrix.

        Returns
        -------
        RowSwap
            A new RowSwap with the same row indices.

        Examples
        --------
        >>> op = RowSwap(0, 2)
        >>> op.inverse()
        RowSwap(row_a=0, row_b=2)
        """
        return RowSwap(self.a, self.b)

    def __str__(self) -> str:
        return f"R{self.a} <-> R{self.b}"

    def __repr__(self) -> str:
        return f"RowSwap(row_a={self.a}, row_b={self.b})"

apply(target)

Swap rows a and b of a matrix or vector, returning the result.

Parameters:

Name Type Description Default
target Matrix | Vector

The matrix or vector to operate on.

required

Returns:

Type Description
Matrix | Vector

A new matrix or vector with the two entries exchanged.

Raises:

Type Description
TypeError

If target is not a Matrix or Vector instance.

ValueError

If either row index is out of range for this target.

Examples:

>>> m = Matrix([[1, 2], [3, 4], [5, 6]])
>>> print(RowSwap(0, 2).apply(m))
[[5, 6],
 [3, 4],
 [1, 2]]
>>> v = Vector([1, 2, 3])
>>> print(RowSwap(0, 2).apply(v))
[3, 2, 1]
Source code in panchi/algorithms/row_operations.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def apply(self, target: Matrix | Vector) -> Matrix | Vector:
    """
    Swap rows a and b of a matrix or vector, returning the result.

    Parameters
    ----------
    target : Matrix | Vector
        The matrix or vector to operate on.

    Returns
    -------
    Matrix | Vector
        A new matrix or vector with the two entries exchanged.

    Raises
    ------
    TypeError
        If target is not a Matrix or Vector instance.
    ValueError
        If either row index is out of range for this target.

    Examples
    --------
    >>> m = Matrix([[1, 2], [3, 4], [5, 6]])
    >>> print(RowSwap(0, 2).apply(m))
    [[5, 6],
     [3, 4],
     [1, 2]]
    >>> v = Vector([1, 2, 3])
    >>> print(RowSwap(0, 2).apply(v))
    [3, 2, 1]
    """
    self._validate_target(target)
    n = target.dims if isinstance(target, Vector) else target.rows
    self._validate_indices(n)

    if isinstance(target, Vector):
        result = target.copy()
        result[self.a], result[self.b] = target[self.b], target[self.a]
        return result

    return self.elementary_matrix(target.rows) @ target

elementary_matrix(n)

Return the n×n elementary matrix for this row swap.

Constructed by swapping rows a and b in the n×n identity matrix. This matrix has determinant -1, reflecting that row swaps reverse the orientation of the row space.

Parameters:

Name Type Description Default
n int

Size of the elementary matrix. Must be at least 2, and large enough so that both row indices are in range.

required

Returns:

Type Description
Matrix

An n×n matrix identical to the identity except that rows a and b are exchanged.

Raises:

Type Description
TypeError

If n is not an integer.

ValueError

If n < 2 or either row index is out of range for n.

Examples:

>>> op = RowSwap(0, 1)
>>> print(op.elementary_matrix(3))
[[0, 1, 0],
 [1, 0, 0],
 [0, 0, 1]]
Source code in panchi/algorithms/row_operations.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def elementary_matrix(self, n: int) -> Matrix:
    """
    Return the n×n elementary matrix for this row swap.

    Constructed by swapping rows a and b in the n×n identity matrix.
    This matrix has determinant -1, reflecting that row swaps reverse
    the orientation of the row space.

    Parameters
    ----------
    n : int
        Size of the elementary matrix. Must be at least 2, and large
        enough so that both row indices are in range.

    Returns
    -------
    Matrix
        An n×n matrix identical to the identity except that rows a
        and b are exchanged.

    Raises
    ------
    TypeError
        If n is not an integer.
    ValueError
        If n < 2 or either row index is out of range for n.

    Examples
    --------
    >>> op = RowSwap(0, 1)
    >>> print(op.elementary_matrix(3))
    [[0, 1, 0],
     [1, 0, 0],
     [0, 0, 1]]
    """
    self._validate_n(n)
    self._validate_indices(n)

    grid: Matrix = identity(n)
    row_a: list[Scalar] = grid[self.a].copy()
    row_b: list[Scalar] = grid[self.b].copy()
    grid[self.a] = row_b
    grid[self.b] = row_a

    return grid

inverse()

Return the inverse of this row swap.

A row swap is its own inverse: swapping the same two rows a second time restores the original matrix.

Returns:

Type Description
RowSwap

A new RowSwap with the same row indices.

Examples:

>>> op = RowSwap(0, 2)
>>> op.inverse()
RowSwap(row_a=0, row_b=2)
Source code in panchi/algorithms/row_operations.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def inverse(self) -> RowSwap:
    """
    Return the inverse of this row swap.

    A row swap is its own inverse: swapping the same two rows a second
    time restores the original matrix.

    Returns
    -------
    RowSwap
        A new RowSwap with the same row indices.

    Examples
    --------
    >>> op = RowSwap(0, 2)
    >>> op.inverse()
    RowSwap(row_a=0, row_b=2)
    """
    return RowSwap(self.a, self.b)

panchi.algorithms.RowScale

Bases: RowOperation

Elementary row operation: multiply a row by a non-zero scalar.

Represents the operation R_i -> scalar * R_i. The corresponding elementary matrix is the identity matrix with the diagonal entry at position [i, i] replaced by scalar.

Scaling a row by scalar multiplies the determinant of the matrix by scalar. To invert this operation, scale by 1 / scalar.

Parameters:

Name Type Description Default
row int

Index of the row to scale (0-based).

required
scalar int | float | Fraction

The non-zero value to multiply the row by.

required

Examples:

>>> m = Matrix([[1, 2], [3, 4]])
>>> op = RowScale(1, 3)
>>> print(op.apply(m))
[[1, 2],
 [9, 12]]
>>> v = Vector([1, 2])
>>> print(op.apply(v))
[1, 6]
>>> print(op.elementary_matrix(2))
[[1, 0],
 [0, 3]]
>>> print(op)
R1 -> 3 * R1
>>> repr(op)
'RowScale(row=1, scalar=3)'
Source code in panchi/algorithms/row_operations.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
class RowScale(RowOperation):
    """
    Elementary row operation: multiply a row by a non-zero scalar.

    Represents the operation R_i -> scalar * R_i. The corresponding
    elementary matrix is the identity matrix with the diagonal entry
    at position [i, i] replaced by scalar.

    Scaling a row by scalar multiplies the determinant of the matrix
    by scalar. To invert this operation, scale by 1 / scalar.

    Parameters
    ----------
    row : int
        Index of the row to scale (0-based).
    scalar : int | float | Fraction
        The non-zero value to multiply the row by.

    Examples
    --------
    >>> m = Matrix([[1, 2], [3, 4]])
    >>> op = RowScale(1, 3)
    >>> print(op.apply(m))
    [[1, 2],
     [9, 12]]
    >>> v = Vector([1, 2])
    >>> print(op.apply(v))
    [1, 6]
    >>> print(op.elementary_matrix(2))
    [[1, 0],
     [0, 3]]
    >>> print(op)
    R1 -> 3 * R1
    >>> repr(op)
    'RowScale(row=1, scalar=3)'
    """

    def __init__(self, row: int, scalar: Scalar) -> None:
        self.row = row
        self.scalar = scalar
        self._validate_row_type()
        self._validate_scalar()

    def _validate_row_type(self) -> None:
        """
        Validate that the row index is an integer.

        Raises
        ------
        TypeError
            If row is not an integer.
        """
        if not isinstance(self.row, int):
            raise TypeError(
                f"Row index must be an integer. Got {type(self.row).__name__}."
            )

    def _validate_row(self, n: int) -> None:
        """
        Validate that the row index is in range for a matrix or vector of size n.

        Parameters
        ----------
        n : int
            Number of rows in the target matrix or vector.

        Raises
        ------
        ValueError
            If the row index is outside [0, n - 1].
        """
        if not (0 <= self.row < n):
            raise ValueError(
                f"Row index {self.row} is out of range for a matrix with {n} rows. "
                f"Valid indices are 0 to {n - 1}."
            )

    def _validate_scalar(self) -> None:
        """
        Validate that the scalar is a non-zero number.

        Raises
        ------
        TypeError
            If scalar is not an int, float, or Fraction.
        ValueError
            If scalar is zero.
        """
        if not isinstance(self.scalar, SCALAR_TYPES):
            raise TypeError(
                f"Scalar must be a number (int, float, or Fraction). "
                f"Got {type(self.scalar).__name__}."
            )

        if self.scalar == 0:
            raise ValueError(
                "Scalar must be non-zero. Scaling a row by zero would make "
                "the matrix singular and the operation non-invertible."
            )

    def elementary_matrix(self, n: int) -> Matrix:
        """
        Return the n×n elementary matrix for this row scale.

        Constructed from the identity matrix with the diagonal entry at
        position [row, row] replaced by scalar.

        Parameters
        ----------
        n : int
            Size of the elementary matrix. Must be at least 2, and large
            enough so that the row index is in range.

        Returns
        -------
        Matrix
            An n×n matrix identical to the identity except that entry
            [row, row] equals scalar.

        Raises
        ------
        TypeError
            If n is not an integer, or scalar is not a number.
        ValueError
            If n < 2, scalar is zero, or the row index is out of range.

        Examples
        --------
        >>> op = RowScale(0, 5)
        >>> print(op.elementary_matrix(3))
        [[5, 0, 0],
         [0, 1, 0],
         [0, 0, 1]]
        """
        self._validate_n(n)
        self._validate_row(n)
        self._validate_scalar()

        grid: Matrix = identity(n)
        grid[self.row, self.row] = self.scalar

        return grid

    def apply(self, target: Matrix | Vector) -> Matrix | Vector:
        """
        Multiply a row of a matrix or vector by the scalar, returning the result.

        Parameters
        ----------
        target : Matrix | Vector
            The matrix or vector to operate on.

        Returns
        -------
        Matrix | Vector
            A new matrix or vector with the specified row multiplied by scalar.

        Raises
        ------
        TypeError
            If target is not a Matrix or Vector instance, or scalar is not a number.
        ValueError
            If scalar is zero or the row index is out of range.

        Examples
        --------
        >>> m = Matrix([[1, 2], [3, 4]])
        >>> print(RowScale(0, -1).apply(m))
        [[-1, -2],
         [3, 4]]
        >>> v = Vector([1, 2])
        >>> print(RowScale(0, -1).apply(v))
        [-1, 2]
        """
        self._validate_target(target)
        n = target.dims if isinstance(target, Vector) else target.rows
        self._validate_row(n)
        self._validate_scalar()

        if isinstance(target, Vector):
            result = target.copy()
            result[self.row] = target[self.row] * self.scalar
            return result

        return self.elementary_matrix(target.rows) @ target

    def inverse(self) -> RowScale:
        """
        Return the inverse of this row scale.

        The inverse scales the same row by 1 / scalar, which restores
        the original values.

        Returns
        -------
        RowScale
            A new RowScale on the same row with scalar 1 / self.scalar.

        Examples
        --------
        >>> op = RowScale(1, 3)
        >>> op.inverse()
        RowScale(row=1, scalar=0.3333333333333333)
        """
        return RowScale(self.row, 1 / self.scalar)

    def __str__(self) -> str:
        return f"R{self.row} -> {self.scalar} * R{self.row}"

    def __repr__(self) -> str:
        return f"RowScale(row={self.row}, scalar={self.scalar})"

apply(target)

Multiply a row of a matrix or vector by the scalar, returning the result.

Parameters:

Name Type Description Default
target Matrix | Vector

The matrix or vector to operate on.

required

Returns:

Type Description
Matrix | Vector

A new matrix or vector with the specified row multiplied by scalar.

Raises:

Type Description
TypeError

If target is not a Matrix or Vector instance, or scalar is not a number.

ValueError

If scalar is zero or the row index is out of range.

Examples:

>>> m = Matrix([[1, 2], [3, 4]])
>>> print(RowScale(0, -1).apply(m))
[[-1, -2],
 [3, 4]]
>>> v = Vector([1, 2])
>>> print(RowScale(0, -1).apply(v))
[-1, 2]
Source code in panchi/algorithms/row_operations.py
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
def apply(self, target: Matrix | Vector) -> Matrix | Vector:
    """
    Multiply a row of a matrix or vector by the scalar, returning the result.

    Parameters
    ----------
    target : Matrix | Vector
        The matrix or vector to operate on.

    Returns
    -------
    Matrix | Vector
        A new matrix or vector with the specified row multiplied by scalar.

    Raises
    ------
    TypeError
        If target is not a Matrix or Vector instance, or scalar is not a number.
    ValueError
        If scalar is zero or the row index is out of range.

    Examples
    --------
    >>> m = Matrix([[1, 2], [3, 4]])
    >>> print(RowScale(0, -1).apply(m))
    [[-1, -2],
     [3, 4]]
    >>> v = Vector([1, 2])
    >>> print(RowScale(0, -1).apply(v))
    [-1, 2]
    """
    self._validate_target(target)
    n = target.dims if isinstance(target, Vector) else target.rows
    self._validate_row(n)
    self._validate_scalar()

    if isinstance(target, Vector):
        result = target.copy()
        result[self.row] = target[self.row] * self.scalar
        return result

    return self.elementary_matrix(target.rows) @ target

elementary_matrix(n)

Return the n×n elementary matrix for this row scale.

Constructed from the identity matrix with the diagonal entry at position [row, row] replaced by scalar.

Parameters:

Name Type Description Default
n int

Size of the elementary matrix. Must be at least 2, and large enough so that the row index is in range.

required

Returns:

Type Description
Matrix

An n×n matrix identical to the identity except that entry [row, row] equals scalar.

Raises:

Type Description
TypeError

If n is not an integer, or scalar is not a number.

ValueError

If n < 2, scalar is zero, or the row index is out of range.

Examples:

>>> op = RowScale(0, 5)
>>> print(op.elementary_matrix(3))
[[5, 0, 0],
 [0, 1, 0],
 [0, 0, 1]]
Source code in panchi/algorithms/row_operations.py
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def elementary_matrix(self, n: int) -> Matrix:
    """
    Return the n×n elementary matrix for this row scale.

    Constructed from the identity matrix with the diagonal entry at
    position [row, row] replaced by scalar.

    Parameters
    ----------
    n : int
        Size of the elementary matrix. Must be at least 2, and large
        enough so that the row index is in range.

    Returns
    -------
    Matrix
        An n×n matrix identical to the identity except that entry
        [row, row] equals scalar.

    Raises
    ------
    TypeError
        If n is not an integer, or scalar is not a number.
    ValueError
        If n < 2, scalar is zero, or the row index is out of range.

    Examples
    --------
    >>> op = RowScale(0, 5)
    >>> print(op.elementary_matrix(3))
    [[5, 0, 0],
     [0, 1, 0],
     [0, 0, 1]]
    """
    self._validate_n(n)
    self._validate_row(n)
    self._validate_scalar()

    grid: Matrix = identity(n)
    grid[self.row, self.row] = self.scalar

    return grid

inverse()

Return the inverse of this row scale.

The inverse scales the same row by 1 / scalar, which restores the original values.

Returns:

Type Description
RowScale

A new RowScale on the same row with scalar 1 / self.scalar.

Examples:

>>> op = RowScale(1, 3)
>>> op.inverse()
RowScale(row=1, scalar=0.3333333333333333)
Source code in panchi/algorithms/row_operations.py
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
def inverse(self) -> RowScale:
    """
    Return the inverse of this row scale.

    The inverse scales the same row by 1 / scalar, which restores
    the original values.

    Returns
    -------
    RowScale
        A new RowScale on the same row with scalar 1 / self.scalar.

    Examples
    --------
    >>> op = RowScale(1, 3)
    >>> op.inverse()
    RowScale(row=1, scalar=0.3333333333333333)
    """
    return RowScale(self.row, 1 / self.scalar)

panchi.algorithms.RowAdd

Bases: RowOperation

Elementary row operation: add a scalar multiple of one row to another.

Represents the operation R_target -> R_target + scalar * R_source. The corresponding elementary matrix is the identity with scalar placed at position [target, source].

This is the core operation of Gaussian elimination. When scalar is chosen to eliminate an entry, the result is a zero in position [target, source_col] of the transformed matrix.

The inverse of this operation is RowAdd(target, source, -scalar).

Parameters:

Name Type Description Default
target int

Index of the row being modified (0-based).

required
source int

Index of the row being added (0-based). Must differ from target.

required
scalar int | float | Fraction

The value to multiply the source row by before adding.

required

Examples:

>>> m = Matrix([[1, 2], [3, 4]])
>>> op = RowAdd(target=1, source=0, scalar=-3)
>>> print(op.apply(m))
[[1,  2],
 [0, -2]]
>>> v = Vector([1, 2])
>>> print(op.apply(v))
[1, -1]
>>> print(op.elementary_matrix(2))
[[1,  0],
 [-3, 1]]
>>> print(op)
R1 -> R1 + (-3) * R0
>>> repr(op)
'RowAdd(target=1, source=0, scalar=-3)'
Source code in panchi/algorithms/row_operations.py
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
class RowAdd(RowOperation):
    """
    Elementary row operation: add a scalar multiple of one row to another.

    Represents the operation R_target -> R_target + scalar * R_source.
    The corresponding elementary matrix is the identity with scalar placed
    at position [target, source].

    This is the core operation of Gaussian elimination. When scalar is
    chosen to eliminate an entry, the result is a zero in position
    [target, source_col] of the transformed matrix.

    The inverse of this operation is RowAdd(target, source, -scalar).

    Parameters
    ----------
    target : int
        Index of the row being modified (0-based).
    source : int
        Index of the row being added (0-based). Must differ from target.
    scalar : int | float | Fraction
        The value to multiply the source row by before adding.

    Examples
    --------
    >>> m = Matrix([[1, 2], [3, 4]])
    >>> op = RowAdd(target=1, source=0, scalar=-3)
    >>> print(op.apply(m))
    [[1,  2],
     [0, -2]]
    >>> v = Vector([1, 2])
    >>> print(op.apply(v))
    [1, -1]
    >>> print(op.elementary_matrix(2))
    [[1,  0],
     [-3, 1]]
    >>> print(op)
    R1 -> R1 + (-3) * R0
    >>> repr(op)
    'RowAdd(target=1, source=0, scalar=-3)'
    """

    def __init__(self, target: int, source: int, scalar: Scalar) -> None:
        self.target = target
        self.source = source
        self.scalar = scalar
        self._validate_index_types()
        self._validate_scalar()

    def _validate_index_types(self) -> None:
        """
        Validate that both row indices are integers.

        Raises
        ------
        TypeError
            If either row index is not an integer.
        """
        if not isinstance(self.target, int):
            raise TypeError(
                f"Row index must be an integer. Got {type(self.target).__name__} for target."
            )

        if not isinstance(self.source, int):
            raise TypeError(
                f"Row index must be an integer. Got {type(self.source).__name__} for source."
            )

    def _validate_indices(self, n: int) -> None:
        """
        Validate that both row indices are in range and distinct.

        Parameters
        ----------
        n : int
            Number of rows in the target matrix or vector.

        Raises
        ------
        ValueError
            If either index is out of range, or if target equals source.
        """
        if not (0 <= self.target < n):
            raise ValueError(
                f"Target row index {self.target} is out of range for a matrix "
                f"with {n} rows. Valid indices are 0 to {n - 1}."
            )

        if not (0 <= self.source < n):
            raise ValueError(
                f"Source row index {self.source} is out of range for a matrix "
                f"with {n} rows. Valid indices are 0 to {n - 1}."
            )

        if self.target == self.source:
            raise ValueError(
                f"Target and source rows must be different. Both are row {self.target}. "
                f"To scale a row, use RowScale instead."
            )

    def _validate_scalar(self) -> None:
        """
        Validate that the scalar is a number.

        Raises
        ------
        TypeError
            If scalar is not an int, float, or Fraction.
        """
        if not isinstance(self.scalar, SCALAR_TYPES):
            raise TypeError(
                f"Scalar must be a number (int, float, or Fraction). "
                f"Got {type(self.scalar).__name__}."
            )

    def elementary_matrix(self, n: int) -> Matrix:
        """
        Return the n×n elementary matrix for this row addition.

        Constructed from the identity matrix with scalar placed at position
        [target, source]. This encodes the fact that left-multiplying by E
        replaces row target with row target plus scalar times row source.

        Parameters
        ----------
        n : int
            Size of the elementary matrix. Must be at least 2 and large
            enough so that both row indices are in range.

        Returns
        -------
        Matrix
            An n×n matrix identical to the identity except that entry
            [target, source] equals scalar.

        Raises
        ------
        TypeError
            If n is not an integer, or scalar is not a number.
        ValueError
            If n < 2, indices are out of range, or target equals source.

        Examples
        --------
        >>> op = RowAdd(target=2, source=0, scalar=4)
        >>> print(op.elementary_matrix(3))
        [[1, 0, 0],
         [0, 1, 0],
         [4, 0, 1]]
        """
        self._validate_n(n)
        self._validate_indices(n)
        self._validate_scalar()

        grid: Matrix = identity(n)
        grid[self.target, self.source] = self.scalar

        return grid

    def apply(self, target: Matrix | Vector) -> Matrix | Vector:
        """
        Add scalar times the source row to the target row, returning the result.

        Parameters
        ----------
        target : Matrix | Vector
            The matrix or vector to operate on.

        Returns
        -------
        Matrix | Vector
            A new matrix or vector where the target row has been replaced by
            target row + scalar * source row.

        Raises
        ------
        TypeError
            If target is not a Matrix or Vector instance, or scalar is not a number.
        ValueError
            If indices are out of range or target equals source.

        Examples
        --------
        >>> m = Matrix([[2, 1], [6, 4]])
        >>> print(RowAdd(target=1, source=0, scalar=-3).apply(m))
        [[2, 1],
         [0, 1]]
        >>> v = Vector([2, 6])
        >>> print(RowAdd(target=1, source=0, scalar=-3).apply(v))
        [2, 0]
        """
        self._validate_target(target)
        n = target.dims if isinstance(target, Vector) else target.rows
        self._validate_indices(n)
        self._validate_scalar()

        if isinstance(target, Vector):
            result = target.copy()
            result[self.target] = (
                target[self.target] + self.scalar * target[self.source]
            )
            return result

        return self.elementary_matrix(target.rows) @ target

    def inverse(self) -> RowAdd:
        """
        Return the inverse of this row addition.

        The inverse subtracts the same scalar multiple of the source row
        from the target row, which restores the original values.

        Returns
        -------
        RowAdd
            A new RowAdd with the same rows and negated scalar.

        Examples
        --------
        >>> op = RowAdd(target=1, source=0, scalar=-3)
        >>> op.inverse()
        RowAdd(target=1, source=0, scalar=3)
        """
        return RowAdd(self.target, self.source, -self.scalar)

    def __str__(self) -> str:
        return f"R{self.target} -> R{self.target} + ({self.scalar}) * R{self.source}"

    def __repr__(self) -> str:
        return (
            f"RowAdd(target={self.target}, source={self.source}, scalar={self.scalar})"
        )

apply(target)

Add scalar times the source row to the target row, returning the result.

Parameters:

Name Type Description Default
target Matrix | Vector

The matrix or vector to operate on.

required

Returns:

Type Description
Matrix | Vector

A new matrix or vector where the target row has been replaced by target row + scalar * source row.

Raises:

Type Description
TypeError

If target is not a Matrix or Vector instance, or scalar is not a number.

ValueError

If indices are out of range or target equals source.

Examples:

>>> m = Matrix([[2, 1], [6, 4]])
>>> print(RowAdd(target=1, source=0, scalar=-3).apply(m))
[[2, 1],
 [0, 1]]
>>> v = Vector([2, 6])
>>> print(RowAdd(target=1, source=0, scalar=-3).apply(v))
[2, 0]
Source code in panchi/algorithms/row_operations.py
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
def apply(self, target: Matrix | Vector) -> Matrix | Vector:
    """
    Add scalar times the source row to the target row, returning the result.

    Parameters
    ----------
    target : Matrix | Vector
        The matrix or vector to operate on.

    Returns
    -------
    Matrix | Vector
        A new matrix or vector where the target row has been replaced by
        target row + scalar * source row.

    Raises
    ------
    TypeError
        If target is not a Matrix or Vector instance, or scalar is not a number.
    ValueError
        If indices are out of range or target equals source.

    Examples
    --------
    >>> m = Matrix([[2, 1], [6, 4]])
    >>> print(RowAdd(target=1, source=0, scalar=-3).apply(m))
    [[2, 1],
     [0, 1]]
    >>> v = Vector([2, 6])
    >>> print(RowAdd(target=1, source=0, scalar=-3).apply(v))
    [2, 0]
    """
    self._validate_target(target)
    n = target.dims if isinstance(target, Vector) else target.rows
    self._validate_indices(n)
    self._validate_scalar()

    if isinstance(target, Vector):
        result = target.copy()
        result[self.target] = (
            target[self.target] + self.scalar * target[self.source]
        )
        return result

    return self.elementary_matrix(target.rows) @ target

elementary_matrix(n)

Return the n×n elementary matrix for this row addition.

Constructed from the identity matrix with scalar placed at position [target, source]. This encodes the fact that left-multiplying by E replaces row target with row target plus scalar times row source.

Parameters:

Name Type Description Default
n int

Size of the elementary matrix. Must be at least 2 and large enough so that both row indices are in range.

required

Returns:

Type Description
Matrix

An n×n matrix identical to the identity except that entry [target, source] equals scalar.

Raises:

Type Description
TypeError

If n is not an integer, or scalar is not a number.

ValueError

If n < 2, indices are out of range, or target equals source.

Examples:

>>> op = RowAdd(target=2, source=0, scalar=4)
>>> print(op.elementary_matrix(3))
[[1, 0, 0],
 [0, 1, 0],
 [4, 0, 1]]
Source code in panchi/algorithms/row_operations.py
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
def elementary_matrix(self, n: int) -> Matrix:
    """
    Return the n×n elementary matrix for this row addition.

    Constructed from the identity matrix with scalar placed at position
    [target, source]. This encodes the fact that left-multiplying by E
    replaces row target with row target plus scalar times row source.

    Parameters
    ----------
    n : int
        Size of the elementary matrix. Must be at least 2 and large
        enough so that both row indices are in range.

    Returns
    -------
    Matrix
        An n×n matrix identical to the identity except that entry
        [target, source] equals scalar.

    Raises
    ------
    TypeError
        If n is not an integer, or scalar is not a number.
    ValueError
        If n < 2, indices are out of range, or target equals source.

    Examples
    --------
    >>> op = RowAdd(target=2, source=0, scalar=4)
    >>> print(op.elementary_matrix(3))
    [[1, 0, 0],
     [0, 1, 0],
     [4, 0, 1]]
    """
    self._validate_n(n)
    self._validate_indices(n)
    self._validate_scalar()

    grid: Matrix = identity(n)
    grid[self.target, self.source] = self.scalar

    return grid

inverse()

Return the inverse of this row addition.

The inverse subtracts the same scalar multiple of the source row from the target row, which restores the original values.

Returns:

Type Description
RowAdd

A new RowAdd with the same rows and negated scalar.

Examples:

>>> op = RowAdd(target=1, source=0, scalar=-3)
>>> op.inverse()
RowAdd(target=1, source=0, scalar=3)
Source code in panchi/algorithms/row_operations.py
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
def inverse(self) -> RowAdd:
    """
    Return the inverse of this row addition.

    The inverse subtracts the same scalar multiple of the source row
    from the target row, which restores the original values.

    Returns
    -------
    RowAdd
        A new RowAdd with the same rows and negated scalar.

    Examples
    --------
    >>> op = RowAdd(target=1, source=0, scalar=-3)
    >>> op.inverse()
    RowAdd(target=1, source=0, scalar=3)
    """
    return RowAdd(self.target, self.source, -self.scalar)

Reductions

panchi.algorithms.ref(matrix, tolerance=0.0)

Reduce a matrix to row echelon form using Gaussian elimination.

Applies a sequence of elementary row operations to produce an upper triangular form where each pivot is to the right of the pivot in the row above it, and all entries below each pivot are zero. The pivot values are not normalised to 1.

Parameters:

Name Type Description Default
matrix Matrix

The matrix to reduce. Not modified by this function.

required
tolerance float

Column entries with magnitude at or below this value are treated as zero when selecting pivots, so a column with only near-zero entries becomes a free (non-pivot) column. The default of 0.0 means exact comparison and reproduces the standard exact reduction. A positive tolerance is useful for floating-point matrices that are only approximately rank-deficient (e.g. A - λI for an estimated λ).

0.0

Returns:

Type Description
Reduction

A Reduction object containing the original matrix, the REF result, the ordered list of row operations applied, the pivot positions as (row, col) tuples, and the form label 'REF'.

Examples:

>>> m = Matrix([[1, 2, 3], [2, 5, 7], [0, 1, 2]])
>>> reduction = ref(m)
>>> print(reduction.result)
[[1, 2, 3],
 [0, 1, 1],
 [0, 0, 1]]
>>> reduction.rank
3
>>> reduction.pivots
[(0, 0), (1, 1), (2, 2)]
Source code in panchi/algorithms/reductions.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def ref(matrix: Matrix, tolerance: float = 0.0) -> Reduction:
    """
    Reduce a matrix to row echelon form using Gaussian elimination.

    Applies a sequence of elementary row operations to produce an upper
    triangular form where each pivot is to the right of the pivot in the
    row above it, and all entries below each pivot are zero. The pivot
    values are not normalised to 1.

    Parameters
    ----------
    matrix : Matrix
        The matrix to reduce. Not modified by this function.
    tolerance : float, optional
        Column entries with magnitude at or below this value are treated as
        zero when selecting pivots, so a column with only near-zero entries
        becomes a free (non-pivot) column. The default of 0.0 means exact
        comparison and reproduces the standard exact reduction. A positive
        tolerance is useful for floating-point matrices that are only
        approximately rank-deficient (e.g. A - λI for an estimated λ).

    Returns
    -------
    Reduction
        A Reduction object containing the original matrix, the REF result,
        the ordered list of row operations applied, the pivot positions as
        (row, col) tuples, and the form label 'REF'.

    Examples
    --------
    >>> m = Matrix([[1, 2, 3], [2, 5, 7], [0, 1, 2]])
    >>> reduction = ref(m)
    >>> print(reduction.result)
    [[1, 2, 3],
     [0, 1, 1],
     [0, 0, 1]]
    >>> reduction.rank
    3
    >>> reduction.pivots
    [(0, 0), (1, 1), (2, 2)]
    """
    result = matrix.copy()
    operations = []
    pivots = []
    i = 0
    for j in range(matrix.cols):
        if i >= matrix.rows:
            break
        result, swap_operations = _swap_pivot(i, j, result, tolerance)
        operations += swap_operations
        if abs(result[i][j]) <= tolerance:
            continue

        result, addition_operations = _add_below_pivot(i, j, result, tolerance)
        operations += addition_operations
        pivots.append((i, j))
        i += 1

    return Reduction(matrix, result, operations, pivots, "REF")

panchi.algorithms.rref(matrix, tolerance=0.0)

Reduce a matrix to reduced row echelon form using Gauss-Jordan elimination.

First reduces to REF via Gaussian elimination, then applies back-substitution to clear all entries above each pivot and scales each pivot row so that the pivot value equals 1. The result is unique for any given matrix.

Parameters:

Name Type Description Default
matrix Matrix

The matrix to reduce. Not modified by this function.

required
tolerance float

Entries with magnitude at or below this value are treated as zero when selecting pivots. The default of 0.0 means exact comparison. See ref() for when a positive tolerance is useful.

0.0

Returns:

Type Description
Reduction

A Reduction object containing the original matrix, the RREF result, the complete ordered list of row operations applied (including those from the initial REF step), the pivot positions as (row, col) tuples, and the form label 'RREF'.

Examples:

>>> m = Matrix([[1, 2, 3], [2, 5, 7], [0, 1, 2]])
>>> reduction = rref(m)
>>> print(reduction.result)
[[1, 0, 0],
 [0, 1, 0],
 [0, 0, 1]]
>>> reduction.rank
3
>>> reduction.nullity
0
Source code in panchi/algorithms/reductions.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def rref(matrix: Matrix, tolerance: float = 0.0) -> Reduction:
    """
    Reduce a matrix to reduced row echelon form using Gauss-Jordan elimination.

    First reduces to REF via Gaussian elimination, then applies back-substitution
    to clear all entries above each pivot and scales each pivot row so that the
    pivot value equals 1. The result is unique for any given matrix.

    Parameters
    ----------
    matrix : Matrix
        The matrix to reduce. Not modified by this function.
    tolerance : float, optional
        Entries with magnitude at or below this value are treated as zero when
        selecting pivots. The default of 0.0 means exact comparison. See ref()
        for when a positive tolerance is useful.

    Returns
    -------
    Reduction
        A Reduction object containing the original matrix, the RREF result,
        the complete ordered list of row operations applied (including those
        from the initial REF step), the pivot positions as (row, col) tuples,
        and the form label 'RREF'.

    Examples
    --------
    >>> m = Matrix([[1, 2, 3], [2, 5, 7], [0, 1, 2]])
    >>> reduction = rref(m)
    >>> print(reduction.result)
    [[1, 0, 0],
     [0, 1, 0],
     [0, 0, 1]]
    >>> reduction.rank
    3
    >>> reduction.nullity
    0
    """
    gaussian_step = ref(matrix, tolerance)
    result = gaussian_step.result
    operations = gaussian_step.steps
    pivots = gaussian_step.pivots
    for i, j in pivots:
        result, scale_operations = _scale_pivot(i, j, result)
        operations += scale_operations
        result, addition_operations = _add_above_pivot(i, j, result, tolerance)
        operations += addition_operations

    return Reduction(matrix, result, operations, pivots, "RREF")

Decompositions

panchi.algorithms.lu(matrix)

Compute the LU decomposition of a square matrix with partial pivoting.

Factors the matrix into a lower triangular matrix L, an upper triangular matrix U, and a permutation matrix P such that P @ matrix == L @ U.

Partial pivoting swaps rows before each elimination step to place the largest available entry in the pivot column at the pivot position. This improves numerical stability and avoids division by zero or near-zero values. The swaps are recorded in P so the factorisation relationship holds exactly.

L is lower triangular with ones on the diagonal. Its off-diagonal entries are the elimination multipliers used during Gaussian elimination. U is the row echelon form of P @ matrix.

Parameters:

Name Type Description Default
matrix Matrix

The square matrix to decompose.

required

Returns:

Type Description
LUDecomposition

A result object containing the original matrix, L, U, P, and the ordered list of row operations applied during elimination.

Examples:

>>> A = Matrix([[2, 1], [4, 3]])
>>> decomp = lu(A)
>>> decomp.permutation @ A == decomp.lower @ decomp.upper
True
>>> print(decomp.lower)
[[1, 0],
 [2.0, 1]]
>>> print(decomp.upper)
[[2, 1],
 [0.0, 1.0]]
Source code in panchi/algorithms/decompositions.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def lu(matrix: Matrix) -> LUDecomposition:
    """
    Compute the LU decomposition of a square matrix with partial pivoting.

    Factors the matrix into a lower triangular matrix L, an upper triangular
    matrix U, and a permutation matrix P such that P @ matrix == L @ U.

    Partial pivoting swaps rows before each elimination step to place the
    largest available entry in the pivot column at the pivot position. This
    improves numerical stability and avoids division by zero or near-zero
    values. The swaps are recorded in P so the factorisation relationship
    holds exactly.

    L is lower triangular with ones on the diagonal. Its off-diagonal
    entries are the elimination multipliers used during Gaussian elimination.
    U is the row echelon form of P @ matrix.

    Parameters
    ----------
    matrix : Matrix
        The square matrix to decompose.

    Returns
    -------
    LUDecomposition
        A result object containing the original matrix, L, U, P, and the
        ordered list of row operations applied during elimination.

    Examples
    --------
    >>> A = Matrix([[2, 1], [4, 3]])
    >>> decomp = lu(A)
    >>> decomp.permutation @ A == decomp.lower @ decomp.upper
    True
    >>> print(decomp.lower)
    [[1, 0],
     [2.0, 1]]
    >>> print(decomp.upper)
    [[2, 1],
     [0.0, 1.0]]
    """
    matrix_ref = ref(matrix)
    n = matrix.rows
    steps = matrix_ref.steps
    l = _calculate_l(n, steps)
    u = matrix_ref.result
    p = _calculate_p(n, steps)
    return LUDecomposition(matrix, l, u, p, steps)

panchi.algorithms.qr_decomposition(matrix)

Compute the (thin) QR decomposition of a matrix.

Factors the matrix into a matrix Q with orthonormal columns and an upper triangular matrix R such that matrix == Q @ R. Q is built by applying Gram-Schmidt to the columns of the matrix, and R is recovered as Q.T @ matrix.

This is the reduced ("thin") QR decomposition and assumes the columns of the matrix are linearly independent. Dependent columns produce a zero orthogonal vector during Gram-Schmidt, which cannot be normalized and raises ZeroDivisionError.

Parameters:

Name Type Description Default
matrix Matrix

The matrix to decompose. Its columns are expected to be linearly independent.

required

Returns:

Type Description
QRDecomposition

A result object containing the original matrix, Q, R, and the ordered list of Gram-Schmidt steps used to build Q.

Raises:

Type Description
ZeroDivisionError

If the columns of the matrix are linearly dependent.

Examples:

>>> A = Matrix([[1, 0], [0, 1]])
>>> decomp = qr_decomposition(A)
>>> decomp.q @ decomp.r == A
True
Source code in panchi/algorithms/decompositions.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def qr_decomposition(matrix: Matrix) -> QRDecomposition:
    """
    Compute the (thin) QR decomposition of a matrix.

    Factors the matrix into a matrix Q with orthonormal columns and an
    upper triangular matrix R such that matrix == Q @ R. Q is built by
    applying Gram-Schmidt to the columns of the matrix, and R is recovered
    as Q.T @ matrix.

    This is the reduced ("thin") QR decomposition and assumes the columns
    of the matrix are linearly independent. Dependent columns produce a zero
    orthogonal vector during Gram-Schmidt, which cannot be normalized and
    raises ZeroDivisionError.

    Parameters
    ----------
    matrix : Matrix
        The matrix to decompose. Its columns are expected to be linearly
        independent.

    Returns
    -------
    QRDecomposition
        A result object containing the original matrix, Q, R, and the
        ordered list of Gram-Schmidt steps used to build Q.

    Raises
    ------
    ZeroDivisionError
        If the columns of the matrix are linearly dependent.

    Examples
    --------
    >>> A = Matrix([[1, 0], [0, 1]])
    >>> decomp = qr_decomposition(A)
    >>> decomp.q @ decomp.r == A
    True
    """
    steps = _gram_schmidt_steps(matrix.col_vectors)
    orthonormal_vectors = [step.orthonormal for step in steps]
    q = vector_column_matrix(orthonormal_vectors)
    r = q.T @ matrix
    return QRDecomposition(matrix, q, r, steps)

Eigenvalues

panchi.algorithms.eigen(matrix, max_iterations=1000, tolerance=1e-12)

Compute the eigenvalues of a square matrix using the QR algorithm.

Starting from the original matrix, repeatedly computes a QR decomposition and reassembles the matrix as R @ Q. For matrices with real eigenvalues this sequence converges to an upper triangular matrix whose diagonal entries are the eigenvalues.

The iteration stops once the strictly-below-diagonal entries sum to less than tolerance (recorded as converged), or once max_iterations is reached (recorded as not converged). Eigenvalues are the diagonal of the final iterate. Eigenvectors are computed separately and are only populated when the iteration converges.

Only real eigenvalues are supported. Matrices with complex eigenvalues or eigenvalues of equal magnitude may fail to converge, in which case the returned result has converged set to False and no eigenvectors.

Parameters:

Name Type Description Default
matrix Matrix

The square matrix whose eigenvalues will be computed.

required
max_iterations int

The maximum number of QR iterations to perform. Defaults to 1000.

1000
tolerance float

The convergence threshold on the below-diagonal mass. Defaults to 1e-12.

1e-12

Returns:

Type Description
EigenResult

A result object containing the eigenvalues, eigenvectors, the number of iterations performed, whether the iteration converged, and the final (near) upper-triangular matrix.

Raises:

Type Description
TypeError

If matrix is not a Matrix instance.

ValueError

If matrix is not square.

Examples:

>>> result = eigen(Matrix([[2, 1], [1, 2]]))
>>> sorted(round(v, 6) for v in result.eigenvalues)
[1.0, 3.0]
Source code in panchi/algorithms/matrix_operations.py
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
def eigen(
    matrix: Matrix,
    max_iterations: int = 1000,
    tolerance: float = 1e-12,
) -> EigenResult:
    """
    Compute the eigenvalues of a square matrix using the QR algorithm.

    Starting from the original matrix, repeatedly computes a QR
    decomposition and reassembles the matrix as R @ Q. For matrices with
    real eigenvalues this sequence converges to an upper triangular matrix
    whose diagonal entries are the eigenvalues.

    The iteration stops once the strictly-below-diagonal entries sum to less
    than ``tolerance`` (recorded as converged), or once ``max_iterations`` is
    reached (recorded as not converged). Eigenvalues are the diagonal of the
    final iterate. Eigenvectors are computed separately and are only
    populated when the iteration converges.

    Only real eigenvalues are supported. Matrices with complex eigenvalues
    or eigenvalues of equal magnitude may fail to converge, in which case the
    returned result has ``converged`` set to False and no eigenvectors.

    Parameters
    ----------
    matrix : Matrix
        The square matrix whose eigenvalues will be computed.
    max_iterations : int, optional
        The maximum number of QR iterations to perform. Defaults to 1000.
    tolerance : float, optional
        The convergence threshold on the below-diagonal mass. Defaults to
        1e-12.

    Returns
    -------
    EigenResult
        A result object containing the eigenvalues, eigenvectors, the number
        of iterations performed, whether the iteration converged, and the
        final (near) upper-triangular matrix.

    Raises
    ------
    TypeError
        If matrix is not a Matrix instance.
    ValueError
        If matrix is not square.

    Examples
    --------
    >>> result = eigen(Matrix([[2, 1], [1, 2]]))
    >>> sorted(round(v, 6) for v in result.eigenvalues)
    [1.0, 3.0]
    """
    if not isinstance(matrix, Matrix):
        raise TypeError(
            f"Expected a Matrix, but got {type(matrix).__name__}. "
            f"Eigenvalues are only defined for Matrix objects."
        )
    if not matrix.is_square:
        raise ValueError(
            f"Cannot compute the eigenvalues of a non-square matrix. "
            f"Your matrix is {matrix.rows}×{matrix.cols}. "
            f"Eigenvalues are only defined for square matrices (n×n)."
        )

    n = matrix.rows
    current = matrix
    converged = False
    iterations = 0
    for iterations in range(1, max_iterations + 1):
        if _below_diagonal_mass(current) < tolerance:
            converged = True
            iterations -= 1
            break
        try:
            decomposition = qr_decomposition(current)
        except ZeroDivisionError:
            # A singular iterate cannot be orthonormalized by Gram-Schmidt;
            # treat this as failure to converge rather than crashing.
            break
        current = decomposition.r @ decomposition.q

    eigenvalues = [current[i][i] for i in range(n)]
    eigenvectors: list[Vector] = []
    if converged:
        for eigenvalue in eigenvalues:
            vector = _eigenvector(matrix, eigenvalue, n)
            if vector is not None:
                eigenvectors.append(vector)
    return EigenResult(
        matrix, eigenvalues, eigenvectors, iterations, converged, current
    )

Solvers

panchi.algorithms.inverse(matrix)

Compute the inverse of a square, invertible matrix.

Reduces the matrix to RREF using Gauss-Jordan elimination and replays the recorded row operations on the identity matrix to construct A⁻¹. The matrix must be square and have full rank; otherwise it is singular and no inverse exists.

Parameters:

Name Type Description Default
matrix Matrix

The matrix to invert. Must be square and have full rank.

required

Returns:

Type Description
InverseResult

An object containing the original matrix, the computed inverse, and the sequence of row operations used.

Raises:

Type Description
TypeError

If matrix is not a Matrix instance.

ValueError

If matrix is not square, or if matrix is singular (rank < n).

Examples:

>>> m = Matrix([[1, 2], [3, 4]])
>>> result = inverse(m)
>>> print(result.inverse)
[[-2.0, 1.0],
 [1.5, -0.5]]
Source code in panchi/algorithms/matrix_operations.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def inverse(matrix: Matrix) -> InverseResult:
    """
    Compute the inverse of a square, invertible matrix.

    Reduces the matrix to RREF using Gauss-Jordan elimination and replays
    the recorded row operations on the identity matrix to construct A⁻¹.
    The matrix must be square and have full rank; otherwise it is singular
    and no inverse exists.

    Parameters
    ----------
    matrix : Matrix
        The matrix to invert. Must be square and have full rank.

    Returns
    -------
    InverseResult
        An object containing the original matrix, the computed inverse, and
        the sequence of row operations used.

    Raises
    ------
    TypeError
        If matrix is not a Matrix instance.
    ValueError
        If matrix is not square, or if matrix is singular (rank < n).

    Examples
    --------
    >>> m = Matrix([[1, 2], [3, 4]])
    >>> result = inverse(m)
    >>> print(result.inverse)
    [[-2.0, 1.0],
     [1.5, -0.5]]
    """
    if not isinstance(matrix, Matrix):
        raise TypeError(
            f"Expected a Matrix, but got {type(matrix).__name__}. "
            f"Inverse is only defined for Matrix objects."
        )
    if not matrix.is_square:
        raise ValueError(
            f"Cannot compute the inverse of a non-square matrix. "
            f"Your matrix is {matrix.rows}×{matrix.cols}. "
            f"Inverse is only defined for square matrices (n×n)."
        )
    n = matrix.rows
    matrix_rref = rref(matrix)
    if matrix_rref.rank != n:
        raise ValueError(
            f"Cannot compute the inverse of a singular matrix. "
            f"Your matrix has rank {matrix_rref.rank}, but must have rank {n}. "
            f"Only matrices with full rank are invertible."
        )
    steps = matrix_rref.steps
    inv = _calculate_inverse(n, steps)
    return InverseResult(matrix, inv, steps)

panchi.algorithms.solve(A, b, tolerance=0.0)

Solve the linear system Ax = b.

Reduces A to RREF and applies the same row operations to b. The system's status is determined by inspecting the reduced forms: an inconsistent row (zero row in A with a non-zero corresponding entry in b) means no solution exists; fewer pivots than variables means infinitely many solutions exist; otherwise a unique solution is extracted from the pivot rows of the transformed b.

Parameters:

Name Type Description Default
A Matrix

The coefficient matrix in the system Ax = b.

required
b Vector

The right-hand side vector in the system Ax = b.

required
tolerance float

Pivot and right-hand-side magnitudes at or below this value are treated as zero during reduction. The default of 0.0 performs an exact solve. A small positive tolerance lets the solver treat a matrix that is only approximately rank-deficient as singular, which is how eigenvectors are recovered as the null space of A - λI for a floating-point eigenvalue estimate λ.

0.0

Returns:

Type Description
Solution

An object containing the original matrix and vector, the status ('unique', 'infinite', or 'inconsistent'), the solution vector if unique, and the row operations applied.

Raises:

Type Description
TypeError

If A is not a Matrix instance, or b is not a Vector instance.

ValueError

If the number of rows in A does not match the length of b.

Examples:

>>> A = Matrix([[2, 1], [5, 3]])
>>> b = Vector([1, 2])
>>> result = solve(A, b)
>>> result.status
'unique'
>>> result.solution
[1.0, -1.0]
Source code in panchi/algorithms/matrix_operations.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
def solve(A: Matrix, b: Vector, tolerance: float = 0.0) -> Solution:
    """
    Solve the linear system Ax = b.

    Reduces A to RREF and applies the same row operations to b. The
    system's status is determined by inspecting the reduced forms: an
    inconsistent row (zero row in A with a non-zero corresponding entry
    in b) means no solution exists; fewer pivots than variables means
    infinitely many solutions exist; otherwise a unique solution is
    extracted from the pivot rows of the transformed b.

    Parameters
    ----------
    A : Matrix
        The coefficient matrix in the system Ax = b.
    b : Vector
        The right-hand side vector in the system Ax = b.
    tolerance : float, optional
        Pivot and right-hand-side magnitudes at or below this value are
        treated as zero during reduction. The default of 0.0 performs an
        exact solve. A small positive tolerance lets the solver treat a
        matrix that is only approximately rank-deficient as singular, which
        is how eigenvectors are recovered as the null space of A - λI for a
        floating-point eigenvalue estimate λ.

    Returns
    -------
    Solution
        An object containing the original matrix and vector, the status
        ('unique', 'infinite', or 'inconsistent'), the solution vector
        if unique, and the row operations applied.

    Raises
    ------
    TypeError
        If A is not a Matrix instance, or b is not a Vector instance.
    ValueError
        If the number of rows in A does not match the length of b.

    Examples
    --------
    >>> A = Matrix([[2, 1], [5, 3]])
    >>> b = Vector([1, 2])
    >>> result = solve(A, b)
    >>> result.status
    'unique'
    >>> result.solution
    [1.0, -1.0]
    """
    if not isinstance(A, Matrix):
        raise TypeError(
            f"Expected a Matrix for A, but got {type(A).__name__}. "
            f"Solve is only defined for Matrix objects."
        )
    if not isinstance(b, Vector):
        raise TypeError(
            f"Expected a Vector for b, but got {type(b).__name__}. "
            f"Solve is only defined for Vector objects."
        )
    if A.rows != b.dims:
        raise ValueError(
            f"The number of rows in A must match the length of b. "
            f"A has {A.rows} rows but b has {b.dims} entries."
        )

    matrix_rref = rref(A, tolerance)
    steps = matrix_rref.steps

    applied_b = b
    for step in steps:
        applied_b = step.apply(applied_b)

    if _inconsistent_rows(matrix_rref, applied_b, tolerance):
        return Solution(A, b, "inconsistent", None, steps)

    if matrix_rref.nullity > 0:
        n = A.cols
        pivot_map = {col: row for row, col in matrix_rref.pivots}
        null_vectors = _null_space_basis(matrix_rref)

        particular_components: list[Scalar] = [0] * n
        for pc, pr in pivot_map.items():
            particular_components[pc] = applied_b[pr]
        particular = Vector(particular_components)

        null_space = VectorSpace(null_vectors)
        return Solution(A, b, "infinite", None, steps, particular, null_space)

    pivot_row_indices = [
        row for row, _ in sorted(matrix_rref.pivots, key=lambda p: p[1])
    ]
    solution = Vector([applied_b[row] for row in pivot_row_indices])
    return Solution(A, b, "unique", solution, steps)

panchi.algorithms.determinant(matrix)

Compute the determinant of a square matrix using cofactor expansion.

The determinant is a scalar that encodes properties of the matrix, including whether it is invertible (det ≠ 0) and how it scales areas or volumes under transformation. Only defined for square matrices.

Computed by expanding along the first row: for each entry in the first row, multiply it by its cofactor (the signed determinant of the submatrix formed by removing that entry's row and column), then sum the results. Unlike determinant_lu, this preserves exact int/Fraction arithmetic, at the cost of O(n!) work.

Parameters:

Name Type Description Default
matrix Matrix

The matrix whose determinant will be computed. Must be square.

required

Returns:

Type Description
int | float | Fraction

The determinant of the matrix.

Raises:

Type Description
TypeError

If matrix is not a Matrix instance.

ValueError

If matrix is not square.

Examples:

>>> determinant(Matrix([[1, 2], [3, 4]]))
-2
>>> determinant(Matrix([[6]]))
6
See Also

determinant_lu : Determinant via LU decomposition (float, O(n³)).

Source code in panchi/algorithms/matrix_operations.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def determinant(matrix: Matrix) -> Scalar:
    """
    Compute the determinant of a square matrix using cofactor expansion.

    The determinant is a scalar that encodes properties of the matrix,
    including whether it is invertible (det ≠ 0) and how it scales areas or
    volumes under transformation. Only defined for square matrices.

    Computed by expanding along the first row: for each entry in the first
    row, multiply it by its cofactor (the signed determinant of the submatrix
    formed by removing that entry's row and column), then sum the results.
    Unlike ``determinant_lu``, this preserves exact ``int``/``Fraction``
    arithmetic, at the cost of O(n!) work.

    Parameters
    ----------
    matrix : Matrix
        The matrix whose determinant will be computed. Must be square.

    Returns
    -------
    int | float | Fraction
        The determinant of the matrix.

    Raises
    ------
    TypeError
        If matrix is not a Matrix instance.
    ValueError
        If matrix is not square.

    Examples
    --------
    >>> determinant(Matrix([[1, 2], [3, 4]]))
    -2
    >>> determinant(Matrix([[6]]))
    6

    See Also
    --------
    determinant_lu : Determinant via LU decomposition (float, O(n³)).
    """
    if not isinstance(matrix, Matrix):
        raise TypeError(
            f"Expected a Matrix, but got {type(matrix).__name__}. "
            f"Determinant is only defined for Matrix objects."
        )
    if not matrix.is_square:
        raise ValueError(
            f"Cannot calculate determinant of non-square matrix. "
            f"Your matrix is {matrix.rows}×{matrix.cols}. "
            f"Determinants are only defined for square matrices (n×n)."
        )

    if matrix.rows == 1:
        return matrix[0][0]

    if matrix.rows == 2:
        return (matrix[0][0] * matrix[1][1]) - (matrix[0][1] * matrix[1][0])

    det = 0
    for j in range(matrix.cols):
        entry = matrix[0][j]
        if entry != 0:
            sign = (-1) ** j
            det += sign * entry * determinant(_submatrix(matrix, 0, j))

    return det

panchi.algorithms.determinant_lu(matrix)

Compute the determinant of a square matrix using LU decomposition.

Factors the matrix into P, L, and U using partial pivoting, then multiplies the main diagonal entries of U by the parity of the permutation. Each row swap in P contributes a factor of -1 to the determinant, so the sign is adjusted by counting the number of swaps performed during factorization.

Parameters:

Name Type Description Default
matrix Matrix

The matrix whose determinant will be computed. Must be square.

required

Returns:

Type Description
float

The determinant of the matrix.

Raises:

Type Description
TypeError

If matrix is not a Matrix instance.

ValueError

If matrix is not square.

Examples:

>>> determinant_lu(Matrix([[1, 2], [3, 4]]))
-2.0
>>> determinant_lu(Matrix([[0, 1], [1, 2]]))
-1.0
See Also

determinant : Determinant via cofactor expansion.

Source code in panchi/algorithms/matrix_operations.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def determinant_lu(matrix: Matrix) -> float:
    """
    Compute the determinant of a square matrix using LU decomposition.

    Factors the matrix into P, L, and U using partial pivoting, then
    multiplies the main diagonal entries of U by the parity of the
    permutation. Each row swap in P contributes a factor of -1 to the
    determinant, so the sign is adjusted by counting the number of swaps
    performed during factorization.

    Parameters
    ----------
    matrix : Matrix
        The matrix whose determinant will be computed. Must be square.

    Returns
    -------
    float
        The determinant of the matrix.

    Raises
    ------
    TypeError
        If matrix is not a Matrix instance.
    ValueError
        If matrix is not square.

    Examples
    --------
    >>> determinant_lu(Matrix([[1, 2], [3, 4]]))
    -2.0
    >>> determinant_lu(Matrix([[0, 1], [1, 2]]))
    -1.0

    See Also
    --------
    determinant : Determinant via cofactor expansion.
    """
    if not isinstance(matrix, Matrix):
        raise TypeError(
            f"Expected a Matrix, but got {type(matrix).__name__}. "
            f"Determinant is only defined for Matrix objects."
        )
    if not matrix.is_square:
        raise ValueError(
            f"Cannot compute the determinant of a non-square matrix. "
            f"Your matrix is {matrix.rows}×{matrix.cols}. "
            f"Determinants are only defined for square matrices (n×n)."
        )
    matrix_lu = lu(matrix)
    parity = _swap_parity(matrix_lu.steps)
    upper_diagonal_product = _main_diagonal_product(matrix_lu.upper)
    return parity * upper_diagonal_product

Matrix properties

panchi.algorithms.rank(obj)

Return the rank of a matrix or vector space.

Rank is fundamentally a matrix property: the number of linearly independent rows (equivalently, columns) of a matrix, computed as the number of pivots in its reduced row echelon form. A VectorSpace's rank is defined as the rank of the matrix whose rows are its spanning vectors — so the same function answers both, and rank(m) == rank(VectorSpace(m.col_vectors)).

Parameters:

Name Type Description Default
obj Matrix or VectorSpace

The matrix, or the vector space whose generators to rank.

required

Returns:

Type Description
int

The number of linearly independent rows/vectors.

Examples:

>>> rank(Matrix([[1, 2], [2, 4]]))
1
>>> rank(VectorSpace([Vector([1, 0]), Vector([0, 1]), Vector([1, 1])]))
2
Source code in panchi/algorithms/matrix_operations.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def rank(obj: Matrix | VectorSpace) -> int:
    """
    Return the rank of a matrix or vector space.

    Rank is fundamentally a matrix property: the number of linearly
    independent rows (equivalently, columns) of a matrix, computed as the
    number of pivots in its reduced row echelon form. A ``VectorSpace``'s
    rank is *defined* as the rank of the matrix whose rows are its spanning
    vectors — so the same function answers both, and
    ``rank(m) == rank(VectorSpace(m.col_vectors))``.

    Parameters
    ----------
    obj : Matrix or VectorSpace
        The matrix, or the vector space whose generators to rank.

    Returns
    -------
    int
        The number of linearly independent rows/vectors.

    Examples
    --------
    >>> rank(Matrix([[1, 2], [2, 4]]))
    1
    >>> rank(VectorSpace([Vector([1, 0]), Vector([0, 1]), Vector([1, 1])]))
    2
    """
    if isinstance(obj, VectorSpace):
        matrix = Matrix([v.to_list() for v in obj.data])
    else:
        matrix = obj
    return ref(matrix).rank

panchi.algorithms.nullity(matrix)

Return the nullity of a matrix — the dimension of its null space.

By the rank–nullity theorem, rank(matrix) + nullity(matrix) equals the number of columns of the matrix.

Parameters:

Name Type Description Default
matrix Matrix

The matrix whose nullity to compute.

required

Returns:

Type Description
int

The dimension of the null space (number of free columns).

Examples:

>>> nullity(Matrix([[1, 2, 3], [4, 5, 6]]))
1
Source code in panchi/algorithms/matrix_operations.py
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def nullity(matrix: Matrix) -> int:
    """
    Return the nullity of a matrix — the dimension of its null space.

    By the rank–nullity theorem, ``rank(matrix) + nullity(matrix)`` equals
    the number of columns of the matrix.

    Parameters
    ----------
    matrix : Matrix
        The matrix whose nullity to compute.

    Returns
    -------
    int
        The dimension of the null space (number of free columns).

    Examples
    --------
    >>> nullity(Matrix([[1, 2, 3], [4, 5, 6]]))
    1
    """
    return ref(matrix).nullity

panchi.algorithms.is_invertible(matrix)

Return True if the matrix is square and invertible (full rank).

A matrix is invertible exactly when it is square and its rank equals its number of rows (equivalently, its determinant is non-zero). This uses the rank via RREF rather than the O(n!) cofactor determinant.

Parameters:

Name Type Description Default
matrix Matrix

The matrix to test.

required

Returns:

Type Description
bool

True if the matrix is square and full rank, False otherwise.

Examples:

>>> is_invertible(Matrix([[1, 2], [3, 4]]))
True
>>> is_invertible(Matrix([[1, 2], [2, 4]]))
False
Source code in panchi/algorithms/matrix_operations.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def is_invertible(matrix: Matrix) -> bool:
    """
    Return True if the matrix is square and invertible (full rank).

    A matrix is invertible exactly when it is square and its rank equals its
    number of rows (equivalently, its determinant is non-zero). This uses the
    rank via RREF rather than the O(n!) cofactor determinant.

    Parameters
    ----------
    matrix : Matrix
        The matrix to test.

    Returns
    -------
    bool
        True if the matrix is square and full rank, False otherwise.

    Examples
    --------
    >>> is_invertible(Matrix([[1, 2], [3, 4]]))
    True
    >>> is_invertible(Matrix([[1, 2], [2, 4]]))
    False
    """
    return matrix.is_square and rank(matrix) == matrix.rows

panchi.algorithms.is_symmetric(matrix)

Return True if the matrix equals its own transpose.

Only square matrices can be symmetric. A symmetric matrix satisfies A == Aᵀ entrywise.

Parameters:

Name Type Description Default
matrix Matrix

The matrix to test.

required

Returns:

Type Description
bool

True if the matrix is square and equal to its transpose.

Examples:

>>> is_symmetric(Matrix([[1, 2], [2, 1]]))
True
>>> is_symmetric(Matrix([[1, 2], [3, 4]]))
False
Source code in panchi/algorithms/matrix_operations.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
def is_symmetric(matrix: Matrix) -> bool:
    """
    Return True if the matrix equals its own transpose.

    Only square matrices can be symmetric. A symmetric matrix satisfies
    ``A == Aᵀ`` entrywise.

    Parameters
    ----------
    matrix : Matrix
        The matrix to test.

    Returns
    -------
    bool
        True if the matrix is square and equal to its transpose.

    Examples
    --------
    >>> is_symmetric(Matrix([[1, 2], [2, 1]]))
    True
    >>> is_symmetric(Matrix([[1, 2], [3, 4]]))
    False
    """
    return matrix.is_square and matrix == matrix.transpose()

Vector space operations

panchi.algorithms.basis(space)

Return a basis for the span of the vectors in a space.

Computes a maximal linearly independent subset of the spanning set by reducing the matrix whose columns are the vectors to row echelon form. Each pivot column corresponds to a vector that is linearly independent of all preceding pivot vectors, so the original vectors at those column indices form a basis.

Parameters:

Name Type Description Default
space VectorSpace

The space whose basis to compute.

required

Returns:

Type Description
list[Vector]

A list of vectors from the original spanning set that form a basis. The order follows the original input order.

Examples:

>>> v1 = Vector([1, 2, 3])
>>> v2 = Vector([4, 5, 6])
>>> v3 = Vector([7, 8, 9])  # linearly dependent on v1 and v2
>>> vs = VectorSpace([v1, v2, v3])
>>> len(basis(vs))
2
Source code in panchi/algorithms/vector_space_operations.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def basis(space: VectorSpace) -> list[Vector]:
    """
    Return a basis for the span of the vectors in a space.

    Computes a maximal linearly independent subset of the spanning set by
    reducing the matrix whose columns are the vectors to row echelon form.
    Each pivot column corresponds to a vector that is linearly independent
    of all preceding pivot vectors, so the original vectors at those column
    indices form a basis.

    Parameters
    ----------
    space : VectorSpace
        The space whose basis to compute.

    Returns
    -------
    list[Vector]
        A list of vectors from the original spanning set that form a basis.
        The order follows the original input order.

    Examples
    --------
    >>> v1 = Vector([1, 2, 3])
    >>> v2 = Vector([4, 5, 6])
    >>> v3 = Vector([7, 8, 9])  # linearly dependent on v1 and v2
    >>> vs = VectorSpace([v1, v2, v3])
    >>> len(basis(vs))
    2
    """
    vector_col_list = [v.to_list() for v in space.data]
    vector_col_matrix = Matrix(vector_col_list).T
    matrix_ref = ref(vector_col_matrix)
    result = []
    for _, pivot_col in matrix_ref.pivots:
        result.append(space.data[pivot_col])

    return result

panchi.algorithms.is_full_rank(space)

Return True if a space spans its entire ambient space.

A space is full rank when its rank equals the ambient dimension — i.e. when the basis vectors span all of R^n.

Parameters:

Name Type Description Default
space VectorSpace

The space to test.

required

Returns:

Type Description
bool

True if rank(space) == space.ambient_dims, False otherwise.

Examples:

>>> is_full_rank(VectorSpace([Vector([1, 0]), Vector([0, 1])]))
True
>>> is_full_rank(VectorSpace([Vector([1, 0, 0]), Vector([0, 1, 0])]))
False
Source code in panchi/algorithms/vector_space_operations.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def is_full_rank(space: VectorSpace) -> bool:
    """
    Return True if a space spans its entire ambient space.

    A space is full rank when its rank equals the ambient dimension — i.e.
    when the basis vectors span all of R^n.

    Parameters
    ----------
    space : VectorSpace
        The space to test.

    Returns
    -------
    bool
        True if ``rank(space) == space.ambient_dims``, False otherwise.

    Examples
    --------
    >>> is_full_rank(VectorSpace([Vector([1, 0]), Vector([0, 1])]))
    True
    >>> is_full_rank(VectorSpace([Vector([1, 0, 0]), Vector([0, 1, 0])]))
    False
    """
    return rank(space) == space.ambient_dims

panchi.algorithms.contains(space, v)

Return True if vector v lies in a space.

Checks membership by attempting to solve the linear system Bx = v, where B is the matrix whose columns are the basis vectors. The vector v is in the span if and only if the system is consistent.

Parameters:

Name Type Description Default
space VectorSpace

The space to test membership against.

required
v Vector

The vector to test for membership.

required

Returns:

Type Description
bool

True if v is in the span of the space, False otherwise.

Raises:

Type Description
TypeError

If v is not a Vector.

ValueError

If v has a different number of components than the vectors in the space.

Examples:

>>> vs = VectorSpace([Vector([1, 0]), Vector([0, 1])])
>>> contains(vs, Vector([3, 4]))
True
>>> vs2 = VectorSpace([Vector([1, 0, 0]), Vector([0, 1, 0])])
>>> contains(vs2, Vector([0, 0, 1]))
False
Source code in panchi/algorithms/vector_space_operations.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def contains(space: VectorSpace, v: Vector) -> bool:
    """
    Return True if vector v lies in a space.

    Checks membership by attempting to solve the linear system ``Bx = v``,
    where B is the matrix whose columns are the basis vectors. The vector v
    is in the span if and only if the system is consistent.

    Parameters
    ----------
    space : VectorSpace
        The space to test membership against.
    v : Vector
        The vector to test for membership.

    Returns
    -------
    bool
        True if v is in the span of the space, False otherwise.

    Raises
    ------
    TypeError
        If v is not a Vector.
    ValueError
        If v has a different number of components than the vectors in the
        space.

    Examples
    --------
    >>> vs = VectorSpace([Vector([1, 0]), Vector([0, 1])])
    >>> contains(vs, Vector([3, 4]))
    True
    >>> vs2 = VectorSpace([Vector([1, 0, 0]), Vector([0, 1, 0])])
    >>> contains(vs2, Vector([0, 0, 1]))
    False
    """
    if not isinstance(v, Vector):
        raise TypeError(f"contains() requires a Vector. Got {type(v).__name__}.")

    if v.dims != space.ambient_dims:
        raise ValueError(
            f"Vector has {v.dims} component(s), but this space lives in "
            f"R^{space.ambient_dims}. Dimensions must match."
        )

    basis_matrix = Matrix([b.to_list() for b in basis(space)]).T
    return solve(basis_matrix, v).status != "inconsistent"

panchi.algorithms.same_subspace(a, b)

Check if two spaces span the same subspace.

Two subspaces are equal if and only if they have the same rank and every basis vector of one is contained in the other. This is a mathematical comparison — unlike VectorSpace.__eq__, which checks whether the generating sets contain the same vectors, this determines whether the two spaces cover the same region of R^n.

Parameters:

Name Type Description Default
a VectorSpace

The first space.

required
b VectorSpace

The second space.

required

Returns:

Type Description
bool

True if both spaces span the same subspace, False otherwise.

Raises:

Type Description
TypeError

If b is not a VectorSpace.

ValueError

If the two spaces live in different ambient dimensions.

Examples:

>>> v1, v2 = Vector([1, 0]), Vector([0, 1])
>>> a = VectorSpace([v1, v2])
>>> b = VectorSpace([v1, v1 + v2])
>>> same_subspace(a, b)
True
>>> a == b
False
Source code in panchi/algorithms/vector_space_operations.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def same_subspace(a: VectorSpace, b: VectorSpace) -> bool:
    """
    Check if two spaces span the same subspace.

    Two subspaces are equal if and only if they have the same rank and every
    basis vector of one is contained in the other. This is a mathematical
    comparison — unlike ``VectorSpace.__eq__``, which checks whether the
    generating sets contain the same vectors, this determines whether the two
    spaces cover the same region of R^n.

    Parameters
    ----------
    a : VectorSpace
        The first space.
    b : VectorSpace
        The second space.

    Returns
    -------
    bool
        True if both spaces span the same subspace, False otherwise.

    Raises
    ------
    TypeError
        If b is not a VectorSpace.
    ValueError
        If the two spaces live in different ambient dimensions.

    Examples
    --------
    >>> v1, v2 = Vector([1, 0]), Vector([0, 1])
    >>> a = VectorSpace([v1, v2])
    >>> b = VectorSpace([v1, v1 + v2])
    >>> same_subspace(a, b)
    True
    >>> a == b
    False
    """
    if not isinstance(b, VectorSpace):
        raise TypeError(
            f"same_subspace() requires a VectorSpace. Got {type(b).__name__}."
        )

    if a.ambient_dims != b.ambient_dims:
        raise ValueError(
            f"Cannot compare subspaces of different ambient dimensions. "
            f"The first lives in R^{a.ambient_dims}, "
            f"but the second lives in R^{b.ambient_dims}."
        )

    if rank(a) != rank(b):
        return False

    return all(contains(b, v) for v in basis(a))

panchi.algorithms.orthogonal_complement(space)

Compute the orthogonal complement of a vector space.

The orthogonal complement of a subspace W of R^n is the set of all vectors in R^n that are orthogonal to every vector in W. It is exactly the null space of the matrix whose rows are the basis vectors of W.

Parameters:

Name Type Description Default
space VectorSpace

The subspace whose orthogonal complement is to be computed.

required

Returns:

Type Description
VectorSpace

A VectorSpace representing the orthogonal complement. If the input space spans all of R^n, returns a VectorSpace containing only the zero vector (rank 0).

Raises:

Type Description
TypeError

If space is not a VectorSpace.

Examples:

>>> v1 = Vector([1, 0, 0])
>>> v2 = Vector([0, 1, 0])
>>> vs = VectorSpace([v1, v2])
>>> comp = orthogonal_complement(vs)
>>> rank(comp)
1
>>> basis(comp)[0]
Vector([0, 0, 1])
Source code in panchi/algorithms/vector_space_operations.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def orthogonal_complement(space: VectorSpace) -> VectorSpace:
    """
    Compute the orthogonal complement of a vector space.

    The orthogonal complement of a subspace W of R^n is the set of all
    vectors in R^n that are orthogonal to every vector in W. It is exactly
    the null space of the matrix whose rows are the basis vectors of W.

    Parameters
    ----------
    space : VectorSpace
        The subspace whose orthogonal complement is to be computed.

    Returns
    -------
    VectorSpace
        A VectorSpace representing the orthogonal complement. If the input
        space spans all of R^n, returns a VectorSpace containing only the
        zero vector (rank 0).

    Raises
    ------
    TypeError
        If space is not a VectorSpace.

    Examples
    --------
    >>> v1 = Vector([1, 0, 0])
    >>> v2 = Vector([0, 1, 0])
    >>> vs = VectorSpace([v1, v2])
    >>> comp = orthogonal_complement(vs)
    >>> rank(comp)
    1
    >>> basis(comp)[0]
    Vector([0, 0, 1])
    """
    if not isinstance(space, VectorSpace):
        raise TypeError(
            f"orthogonal_complement() requires a VectorSpace. "
            f"Got {type(space).__name__}."
        )

    basis_vectors = basis(space)
    if not basis_vectors:
        return standard_basis(space.ambient_dims)

    row_matrix = Matrix([v.to_list() for v in basis_vectors])
    return null_space(row_matrix)

panchi.algorithms.gram_schmidt(vectors)

Orthonormalize a list of vectors using the Gram-Schmidt process.

Given a list of linearly independent vectors, produces an orthonormal list spanning the same subspace: each output vector has unit length and is orthogonal to all the others. Each input vector has its components along the previously produced directions removed, and the remainder is normalized.

Parameters:

Name Type Description Default
vectors list[Vector]

The vectors to orthonormalize. Must be non-empty; the vectors are expected to be linearly independent.

required

Returns:

Type Description
list[Vector]

An orthonormal list of vectors spanning the same subspace, in the same order as the inputs.

Raises:

Type Description
ValueError

If vectors is empty.

ZeroDivisionError

If the vectors are linearly dependent (a vector reduces to the zero vector and cannot be normalized).

Examples:

>>> a = Vector([1, 1, 0])
>>> b = Vector([1, 0, 1])
>>> q = gram_schmidt([a, b])
>>> round(dot(q[0], q[1]), 10)
0.0
>>> round(q[0].magnitude, 10)
1.0
Source code in panchi/algorithms/vector_operations.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def gram_schmidt(vectors: list[Vector]) -> list[Vector]:
    """
    Orthonormalize a list of vectors using the Gram-Schmidt process.

    Given a list of linearly independent vectors, produces an orthonormal
    list spanning the same subspace: each output vector has unit length and
    is orthogonal to all the others. Each input vector has its components
    along the previously produced directions removed, and the remainder is
    normalized.

    Parameters
    ----------
    vectors : list[Vector]
        The vectors to orthonormalize. Must be non-empty; the vectors are
        expected to be linearly independent.

    Returns
    -------
    list[Vector]
        An orthonormal list of vectors spanning the same subspace, in the
        same order as the inputs.

    Raises
    ------
    ValueError
        If vectors is empty.
    ZeroDivisionError
        If the vectors are linearly dependent (a vector reduces to the zero
        vector and cannot be normalized).

    Examples
    --------
    >>> a = Vector([1, 1, 0])
    >>> b = Vector([1, 0, 1])
    >>> q = gram_schmidt([a, b])
    >>> round(dot(q[0], q[1]), 10)
    0.0
    >>> round(q[0].magnitude, 10)
    1.0
    """
    return [step.orthonormal for step in _gram_schmidt_steps(vectors)]

panchi.algorithms.vector_projection(projected_vector, axis_vector)

Project one vector orthogonally onto the line spanned by another.

The projection of v onto a is the component of v that lies along a, computed as (v . a) / (a . a) * a. It is the closest point to v on the line through the origin in the direction of a.

Parameters:

Name Type Description Default
projected_vector Vector

The vector being projected (v).

required
axis_vector Vector

The vector defining the direction to project onto (a). Must be non-zero.

required

Returns:

Type Description
Vector

The projection of projected_vector onto axis_vector.

Raises:

Type Description
ValueError

If the vectors have different dimensions.

ZeroDivisionError

If axis_vector is the zero vector.

Examples:

>>> v = Vector([2, 3])
>>> a = Vector([1, 0])
>>> print(vector_projection(v, a))
[2.0, 0.0]
Source code in panchi/algorithms/vector_operations.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def vector_projection(projected_vector: Vector, axis_vector: Vector) -> Vector:
    """
    Project one vector orthogonally onto the line spanned by another.

    The projection of v onto a is the component of v that lies along a,
    computed as (v . a) / (a . a) * a. It is the closest point to v on the
    line through the origin in the direction of a.

    Parameters
    ----------
    projected_vector : Vector
        The vector being projected (v).
    axis_vector : Vector
        The vector defining the direction to project onto (a). Must be
        non-zero.

    Returns
    -------
    Vector
        The projection of projected_vector onto axis_vector.

    Raises
    ------
    ValueError
        If the vectors have different dimensions.
    ZeroDivisionError
        If axis_vector is the zero vector.

    Examples
    --------
    >>> v = Vector([2, 3])
    >>> a = Vector([1, 0])
    >>> print(vector_projection(v, a))
    [2.0, 0.0]
    """
    scalar_projection = dot(projected_vector, axis_vector) / dot(
        axis_vector, axis_vector
    )
    return scalar_projection * axis_vector

Subspace factories

panchi.algorithms.span(vectors)

Construct the vector space spanned by a list of vectors.

A readable alias for VectorSpace(vectors) — the span is the set of all linear combinations of the given vectors.

Parameters:

Name Type Description Default
vectors list[Vector]

The spanning vectors.

required

Returns:

Type Description
VectorSpace

The space these vectors span.

Examples:

>>> span([Vector([1, 0]), Vector([0, 1])])
VectorSpace(ambient=2, generators=2)
Source code in panchi/algorithms/vector_space_operations.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def span(vectors: list[Vector]) -> VectorSpace:
    """
    Construct the vector space spanned by a list of vectors.

    A readable alias for ``VectorSpace(vectors)`` — the span is the set of all
    linear combinations of the given vectors.

    Parameters
    ----------
    vectors : list[Vector]
        The spanning vectors.

    Returns
    -------
    VectorSpace
        The space these vectors span.

    Examples
    --------
    >>> span([Vector([1, 0]), Vector([0, 1])])
    VectorSpace(ambient=2, generators=2)
    """
    return VectorSpace(vectors)

panchi.algorithms.standard_basis(n)

Construct the standard basis of R^n as a vector space.

The standard basis is {e_1, ..., e_n}, where e_i has a 1 in position i and 0 elsewhere. The resulting space is all of R^n.

Parameters:

Name Type Description Default
n int

The dimension of the ambient space.

required

Returns:

Type Description
VectorSpace

The full space R^n, spanned by the standard basis vectors.

Examples:

>>> rank(standard_basis(3))
3
Source code in panchi/algorithms/vector_space_operations.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def standard_basis(n: int) -> VectorSpace:
    """
    Construct the standard basis of R^n as a vector space.

    The standard basis is ``{e_1, ..., e_n}``, where ``e_i`` has a 1 in
    position i and 0 elsewhere. The resulting space is all of R^n.

    Parameters
    ----------
    n : int
        The dimension of the ambient space.

    Returns
    -------
    VectorSpace
        The full space R^n, spanned by the standard basis vectors.

    Examples
    --------
    >>> rank(standard_basis(3))
    3
    """
    return VectorSpace([unit_vector(n, i) for i in range(n)])

panchi.algorithms.column_space(matrix)

Construct the column space of a matrix.

The column space is the span of the matrix's columns — the set of all vectors reachable as matrix @ x. Its dimension equals rank(matrix).

Parameters:

Name Type Description Default
matrix Matrix

The matrix whose column space to build.

required

Returns:

Type Description
VectorSpace

The span of the matrix's columns.

Examples:

>>> A = Matrix([[1, 2], [3, 6]])
>>> rank(column_space(A))
1
Source code in panchi/algorithms/vector_space_operations.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def column_space(matrix: Matrix) -> VectorSpace:
    """
    Construct the column space of a matrix.

    The column space is the span of the matrix's columns — the set of all
    vectors reachable as ``matrix @ x``. Its dimension equals ``rank(matrix)``.

    Parameters
    ----------
    matrix : Matrix
        The matrix whose column space to build.

    Returns
    -------
    VectorSpace
        The span of the matrix's columns.

    Examples
    --------
    >>> A = Matrix([[1, 2], [3, 6]])
    >>> rank(column_space(A))
    1
    """
    return VectorSpace(matrix.col_vectors)

panchi.algorithms.row_space(matrix)

Construct the row space of a matrix.

The row space is the span of the matrix's rows, equivalently the column space of its transpose. Its dimension equals rank(matrix).

Parameters:

Name Type Description Default
matrix Matrix

The matrix whose row space to build.

required

Returns:

Type Description
VectorSpace

The span of the matrix's rows.

Examples:

>>> A = Matrix([[1, 2], [3, 6]])
>>> rank(row_space(A))
1
Source code in panchi/algorithms/vector_space_operations.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def row_space(matrix: Matrix) -> VectorSpace:
    """
    Construct the row space of a matrix.

    The row space is the span of the matrix's rows, equivalently the column
    space of its transpose. Its dimension equals ``rank(matrix)``.

    Parameters
    ----------
    matrix : Matrix
        The matrix whose row space to build.

    Returns
    -------
    VectorSpace
        The span of the matrix's rows.

    Examples
    --------
    >>> A = Matrix([[1, 2], [3, 6]])
    >>> rank(row_space(A))
    1
    """
    return column_space(matrix.transpose())

panchi.algorithms.null_space(matrix)

Construct the null space (kernel) of a matrix.

The null space is the set of all vectors x with matrix @ x == 0. Its basis is extracted parametrically from the reduced row echelon form: one basis vector per free column. When the matrix has full column rank the null space is trivial and is represented by the zero vector (rank 0).

Parameters:

Name Type Description Default
matrix Matrix

The matrix whose null space to build.

required

Returns:

Type Description
VectorSpace

The kernel of the matrix. rank(null_space(A)) == nullity(A).

Examples:

>>> A = Matrix([[1, 1], [1, 1]])
>>> rank(null_space(A))
1
Source code in panchi/algorithms/vector_space_operations.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def null_space(matrix: Matrix) -> VectorSpace:
    """
    Construct the null space (kernel) of a matrix.

    The null space is the set of all vectors x with ``matrix @ x == 0``. Its
    basis is extracted parametrically from the reduced row echelon form: one
    basis vector per free column. When the matrix has full column rank the
    null space is trivial and is represented by the zero vector (rank 0).

    Parameters
    ----------
    matrix : Matrix
        The matrix whose null space to build.

    Returns
    -------
    VectorSpace
        The kernel of the matrix. ``rank(null_space(A)) == nullity(A)``.

    Examples
    --------
    >>> A = Matrix([[1, 1], [1, 1]])
    >>> rank(null_space(A))
    1
    """
    null_vectors = _null_space_basis(rref(matrix))
    if not null_vectors:
        return VectorSpace([zero_vector(matrix.cols)])
    return VectorSpace(null_vectors)

Result types

panchi.algorithms.Reduction

The result of a row reduction performed on a matrix.

Stores the original matrix, the reduced form, every row operation applied as an ordered sequence of RowOperation objects, the pivot positions, and whether the result is in REF or RREF.

Parameters:

Name Type Description Default
original Matrix

The matrix before any row operations were applied.

required
result Matrix

The matrix after all row operations have been applied.

required
steps list[RowOperation]

The ordered sequence of elementary row operations that transforms original into result.

required
pivots list[tuple[int, int]]

The (row, col) positions of each pivot, in order of discovery.

required
form str

Either 'REF' or 'RREF', indicating which reduced form was computed.

required

Examples:

>>> A = Matrix([[1, 2], [3, 4]])
>>> reduction = ref(A)
>>> reduction.rank
2
>>> reduction.nullity
0
Source code in panchi/algorithms/results.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
class Reduction:
    """
    The result of a row reduction performed on a matrix.

    Stores the original matrix, the reduced form, every row operation
    applied as an ordered sequence of RowOperation objects, the pivot
    positions, and whether the result is in REF or RREF.

    Parameters
    ----------
    original : Matrix
        The matrix before any row operations were applied.
    result : Matrix
        The matrix after all row operations have been applied.
    steps : list[RowOperation]
        The ordered sequence of elementary row operations that transforms
        original into result.
    pivots : list[tuple[int, int]]
        The (row, col) positions of each pivot, in order of discovery.
    form : str
        Either 'REF' or 'RREF', indicating which reduced form was computed.

    Examples
    --------
    >>> A = Matrix([[1, 2], [3, 4]])
    >>> reduction = ref(A)
    >>> reduction.rank
    2
    >>> reduction.nullity
    0
    """

    def __init__(
        self,
        original: Matrix,
        result: Matrix,
        steps: list[RowOperation],
        pivots: list[tuple[int, int]],
        form: str,
    ) -> None:
        self.original = original
        self.result = result
        self.steps = steps
        self.pivots = pivots
        self.form = form

    @property
    def rank(self) -> int:
        """
        The rank of the matrix, equal to the number of pivot positions.

        Returns
        -------
        int
            Number of pivot positions found during reduction.
        """
        return len(self.pivots)

    @property
    def nullity(self) -> int:
        """
        The nullity of the matrix, equal to columns minus rank.

        By the rank-nullity theorem, rank + nullity equals the number
        of columns of the original matrix.

        Returns
        -------
        int
            Dimension of the null space.
        """
        return self.original.cols - self.rank

    def __str__(self) -> str:
        """
        Return a step-by-step walkthrough of the reduction.

        Shows the operation label and resulting matrix state after each
        step, followed by a summary of pivot positions, rank, and nullity.

        Returns
        -------
        str
            Human-readable reduction walkthrough.

        Examples
        --------
        >>> print(ref(Matrix([[1, 2], [3, 4]])))
        REF of 2×2 matrix — 1 steps, rank 2

        Step 1: R1 -> R1 + (-3.0) * R0
        [[1, 2],
         [0, -2.0]]
        ...
        """
        header: str = (
            f"{self.form} of {self.original.rows}×{self.original.cols} matrix"
            f" — {len(self.steps)} steps, rank {self.rank}\n"
        )

        current: Matrix = self.original.copy()
        steps_str: str = ""
        for i, step in enumerate(self.steps):
            current = step.apply(current)
            steps_str += f"\nStep {i + 1}: {step}\n{current}\n"

        footer: str = (
            f"\nResult:\n{self.result}\n"
            f"\nPivots: {self.pivots}\n"
            f"Rank: {self.rank}  |  Nullity: {self.nullity}"
        )

        return header + steps_str + footer

    def __repr__(self) -> str:
        """
        Return a concise data inspection string for this Reduction.

        Returns
        -------
        str
            Compact representation showing form, shape, rank, nullity,
            pivot positions, number of steps, and the result matrix.

        Examples
        --------
        >>> ref(Matrix([[1, 2], [3, 4]]))
        Reduction(form=REF, shape=2×2, rank=2, nullity=0, pivots=[(0, 0), (1, 1)], steps=1)
        [[1, 2],
         [0, -2.0]]
        """
        summary: str = (
            f"Reduction("
            f"form={self.form}, "
            f"shape={self.original.rows}×{self.original.cols}, "
            f"rank={self.rank}, "
            f"nullity={self.nullity}, "
            f"pivots={self.pivots}, "
            f"steps={len(self.steps)})"
        )

        return f"{summary}\n{self.result}"

    def _repr_latex_(self) -> str:
        """Render the reduction as a stacked LaTeX arrow sequence."""
        current = self.original.copy()
        lines = [f"& {matrix_to_latex(current)}"]
        for step in self.steps:
            current = step.apply(current)
            lines.append(
                f"&\\xrightarrow{{{row_op_to_latex(step)}}} {matrix_to_latex(current)}"
            )
        body = " \\\\\n".join(lines)
        return f"$$\\begin{{aligned}}\n{body}\n\\end{{aligned}}$$"

nullity property

The nullity of the matrix, equal to columns minus rank.

By the rank-nullity theorem, rank + nullity equals the number of columns of the original matrix.

Returns:

Type Description
int

Dimension of the null space.

rank property

The rank of the matrix, equal to the number of pivot positions.

Returns:

Type Description
int

Number of pivot positions found during reduction.

__repr__()

Return a concise data inspection string for this Reduction.

Returns:

Type Description
str

Compact representation showing form, shape, rank, nullity, pivot positions, number of steps, and the result matrix.

Examples:

>>> ref(Matrix([[1, 2], [3, 4]]))
Reduction(form=REF, shape=2×2, rank=2, nullity=0, pivots=[(0, 0), (1, 1)], steps=1)
[[1, 2],
 [0, -2.0]]
Source code in panchi/algorithms/results.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def __repr__(self) -> str:
    """
    Return a concise data inspection string for this Reduction.

    Returns
    -------
    str
        Compact representation showing form, shape, rank, nullity,
        pivot positions, number of steps, and the result matrix.

    Examples
    --------
    >>> ref(Matrix([[1, 2], [3, 4]]))
    Reduction(form=REF, shape=2×2, rank=2, nullity=0, pivots=[(0, 0), (1, 1)], steps=1)
    [[1, 2],
     [0, -2.0]]
    """
    summary: str = (
        f"Reduction("
        f"form={self.form}, "
        f"shape={self.original.rows}×{self.original.cols}, "
        f"rank={self.rank}, "
        f"nullity={self.nullity}, "
        f"pivots={self.pivots}, "
        f"steps={len(self.steps)})"
    )

    return f"{summary}\n{self.result}"

__str__()

Return a step-by-step walkthrough of the reduction.

Shows the operation label and resulting matrix state after each step, followed by a summary of pivot positions, rank, and nullity.

Returns:

Type Description
str

Human-readable reduction walkthrough.

Examples:

>>> print(ref(Matrix([[1, 2], [3, 4]])))
REF of 2×2 matrix — 1 steps, rank 2

Step 1: R1 -> R1 + (-3.0) * R0 [[1, 2], [0, -2.0]] ...

Source code in panchi/algorithms/results.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def __str__(self) -> str:
    """
    Return a step-by-step walkthrough of the reduction.

    Shows the operation label and resulting matrix state after each
    step, followed by a summary of pivot positions, rank, and nullity.

    Returns
    -------
    str
        Human-readable reduction walkthrough.

    Examples
    --------
    >>> print(ref(Matrix([[1, 2], [3, 4]])))
    REF of 2×2 matrix — 1 steps, rank 2

    Step 1: R1 -> R1 + (-3.0) * R0
    [[1, 2],
     [0, -2.0]]
    ...
    """
    header: str = (
        f"{self.form} of {self.original.rows}×{self.original.cols} matrix"
        f" — {len(self.steps)} steps, rank {self.rank}\n"
    )

    current: Matrix = self.original.copy()
    steps_str: str = ""
    for i, step in enumerate(self.steps):
        current = step.apply(current)
        steps_str += f"\nStep {i + 1}: {step}\n{current}\n"

    footer: str = (
        f"\nResult:\n{self.result}\n"
        f"\nPivots: {self.pivots}\n"
        f"Rank: {self.rank}  |  Nullity: {self.nullity}"
    )

    return header + steps_str + footer

panchi.algorithms.LUDecomposition

The result of an LU decomposition with partial pivoting.

Stores the original matrix, the lower triangular matrix L, the upper triangular matrix U, and the permutation matrix P encoding any row swaps applied for numerical stability. The decomposition satisfies P @ original == L @ U.

Partial pivoting swaps rows before each elimination step so that the largest available entry in the pivot column is used as the pivot. This avoids division by small numbers and produces a more numerically stable result. The swaps are recorded in P so that the factorisation relationship is exact.

Parameters:

Name Type Description Default
original Matrix

The square matrix that was decomposed.

required
lower Matrix

The lower triangular matrix L with ones on the diagonal.

required
upper Matrix

The upper triangular matrix U produced by Gaussian elimination on P @ original.

required
permutation Matrix

The permutation matrix P encoding all row swaps performed, satisfying P @ original == L @ U.

required
steps list[RowOperation]

The ordered sequence of row operations applied to P @ original to produce U.

required

Examples:

>>> A = Matrix([[2, 1], [4, 3]])
>>> decomp = lu(A)
>>> decomp.lower @ decomp.upper == decomp.permutation @ A
True
Source code in panchi/algorithms/results.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class LUDecomposition:
    """
    The result of an LU decomposition with partial pivoting.

    Stores the original matrix, the lower triangular matrix L, the upper
    triangular matrix U, and the permutation matrix P encoding any row
    swaps applied for numerical stability. The decomposition satisfies
    P @ original == L @ U.

    Partial pivoting swaps rows before each elimination step so that the
    largest available entry in the pivot column is used as the pivot.
    This avoids division by small numbers and produces a more numerically
    stable result. The swaps are recorded in P so that the factorisation
    relationship is exact.

    Parameters
    ----------
    original : Matrix
        The square matrix that was decomposed.
    lower : Matrix
        The lower triangular matrix L with ones on the diagonal.
    upper : Matrix
        The upper triangular matrix U produced by Gaussian elimination
        on P @ original.
    permutation : Matrix
        The permutation matrix P encoding all row swaps performed,
        satisfying P @ original == L @ U.
    steps : list[RowOperation]
        The ordered sequence of row operations applied to P @ original
        to produce U.

    Examples
    --------
    >>> A = Matrix([[2, 1], [4, 3]])
    >>> decomp = lu(A)
    >>> decomp.lower @ decomp.upper == decomp.permutation @ A
    True
    """

    def __init__(
        self,
        original: Matrix,
        lower: Matrix,
        upper: Matrix,
        permutation: Matrix,
        steps: list[RowOperation],
    ) -> None:
        self.original = original
        self.lower = lower
        self.upper = upper
        self.permutation = permutation
        self.steps = steps

    def __str__(self) -> str:
        """
        Return a readable summary of the LU decomposition.

        Shows P, L, and U individually and states the factorisation
        relationship P @ A = L @ U.

        Returns
        -------
        str
            Human-readable decomposition summary.

        Examples
        --------
        >>> print(lu(Matrix([[2, 1], [4, 3]])))
        LU decomposition of 2×2 matrix — 1 steps

        P @ A = L @ U

        P:
        [[1, 0],
         [0, 1]]

        A:
        [[2, 1],
         [4, 3]]

        L:
        [[1, 0],
         [2.0, 1]]

        U:
        [[2, 1],
         [0.0, 1.0]]
        """
        header: str = (
            f"LU decomposition of "
            f"{self.original.rows}×{self.original.cols} matrix"
            f" — {len(self.steps)} steps\n"
        )

        body: str = (
            f"\nP @ A = L @ U\n"
            f"\nP:\n{self.permutation}\n"
            f"\nA:\n{self.original}\n"
            f"\nL:\n{self.lower}\n"
            f"\nU:\n{self.upper}"
        )

        return header + body

    def __repr__(self) -> str:
        """
        Return a concise data inspection string for this LUDecomposition.

        Returns
        -------
        str
            Compact representation showing shape and number of steps.

        Examples
        --------
        >>> lu(Matrix([[2, 1], [4, 3]]))
        LUDecomposition(shape=2×2, steps=1)
        """
        return (
            f"LUDecomposition("
            f"shape={self.original.rows}×{self.original.cols}, "
            f"steps={len(self.steps)})"
        )

    def _repr_latex_(self) -> str:
        """Render the factorization ``A = LU`` (or ``PA = LU`` when pivoted)."""
        n = self.permutation.rows
        is_identity = all(
            self.permutation[i, j] == (1 if i == j else 0)
            for i in range(n)
            for j in range(n)
        )
        rhs = f"{matrix_to_latex(self.lower)}\\,{matrix_to_latex(self.upper)}"
        if is_identity:
            return f"$${matrix_to_latex(self.original)} = {rhs}$$"
        return (
            f"$$P\\,{matrix_to_latex(self.original)} = {rhs}, \\quad "
            f"P = {matrix_to_latex(self.permutation)}$$"
        )

__repr__()

Return a concise data inspection string for this LUDecomposition.

Returns:

Type Description
str

Compact representation showing shape and number of steps.

Examples:

>>> lu(Matrix([[2, 1], [4, 3]]))
LUDecomposition(shape=2×2, steps=1)
Source code in panchi/algorithms/results.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def __repr__(self) -> str:
    """
    Return a concise data inspection string for this LUDecomposition.

    Returns
    -------
    str
        Compact representation showing shape and number of steps.

    Examples
    --------
    >>> lu(Matrix([[2, 1], [4, 3]]))
    LUDecomposition(shape=2×2, steps=1)
    """
    return (
        f"LUDecomposition("
        f"shape={self.original.rows}×{self.original.cols}, "
        f"steps={len(self.steps)})"
    )

__str__()

Return a readable summary of the LU decomposition.

Shows P, L, and U individually and states the factorisation relationship P @ A = L @ U.

Returns:

Type Description
str

Human-readable decomposition summary.

Examples:

>>> print(lu(Matrix([[2, 1], [4, 3]])))
LU decomposition of 2×2 matrix — 1 steps

P @ A = L @ U

P: [[1, 0], [0, 1]]

A: [[2, 1], [4, 3]]

L: [[1, 0], [2.0, 1]]

U: [[2, 1], [0.0, 1.0]]

Source code in panchi/algorithms/results.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def __str__(self) -> str:
    """
    Return a readable summary of the LU decomposition.

    Shows P, L, and U individually and states the factorisation
    relationship P @ A = L @ U.

    Returns
    -------
    str
        Human-readable decomposition summary.

    Examples
    --------
    >>> print(lu(Matrix([[2, 1], [4, 3]])))
    LU decomposition of 2×2 matrix — 1 steps

    P @ A = L @ U

    P:
    [[1, 0],
     [0, 1]]

    A:
    [[2, 1],
     [4, 3]]

    L:
    [[1, 0],
     [2.0, 1]]

    U:
    [[2, 1],
     [0.0, 1.0]]
    """
    header: str = (
        f"LU decomposition of "
        f"{self.original.rows}×{self.original.cols} matrix"
        f" — {len(self.steps)} steps\n"
    )

    body: str = (
        f"\nP @ A = L @ U\n"
        f"\nP:\n{self.permutation}\n"
        f"\nA:\n{self.original}\n"
        f"\nL:\n{self.lower}\n"
        f"\nU:\n{self.upper}"
    )

    return header + body

panchi.algorithms.QRDecomposition

The result of a (thin) QR decomposition via Gram-Schmidt.

Stores the original matrix, the matrix Q with orthonormal columns, the upper triangular matrix R, and the ordered list of Gram-Schmidt steps used to build Q. The decomposition satisfies original == Q @ R.

Q is formed by orthonormalizing the columns of the original matrix, so its columns are an orthonormal basis for the column space. R = Qᵀ @ A is upper triangular and its diagonal entries record how much of each column survived orthogonalization. The recorded steps expose the column-by-column derivation of Q, mirroring how a Reduction exposes its row operations.

Parameters:

Name Type Description Default
original Matrix

The matrix that was decomposed, with linearly independent columns.

required
q Matrix

The matrix with orthonormal columns.

required
r Matrix

The upper triangular matrix satisfying original == q @ r.

required
steps list[GramSchmidtStep]

The ordered Gram-Schmidt steps, one per column, used to build Q.

required

Examples:

>>> A = Matrix([[1, 0], [0, 1]])
>>> decomp = qr_decomposition(A)
>>> decomp.q @ decomp.r == A
True
Source code in panchi/algorithms/results.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
class QRDecomposition:
    """
    The result of a (thin) QR decomposition via Gram-Schmidt.

    Stores the original matrix, the matrix Q with orthonormal columns, the
    upper triangular matrix R, and the ordered list of Gram-Schmidt steps
    used to build Q. The decomposition satisfies original == Q @ R.

    Q is formed by orthonormalizing the columns of the original matrix, so
    its columns are an orthonormal basis for the column space. R = Qᵀ @ A is
    upper triangular and its diagonal entries record how much of each column
    survived orthogonalization. The recorded steps expose the column-by-column
    derivation of Q, mirroring how a Reduction exposes its row operations.

    Parameters
    ----------
    original : Matrix
        The matrix that was decomposed, with linearly independent columns.
    q : Matrix
        The matrix with orthonormal columns.
    r : Matrix
        The upper triangular matrix satisfying original == q @ r.
    steps : list[GramSchmidtStep]
        The ordered Gram-Schmidt steps, one per column, used to build Q.

    Examples
    --------
    >>> A = Matrix([[1, 0], [0, 1]])
    >>> decomp = qr_decomposition(A)
    >>> decomp.q @ decomp.r == A
    True
    """

    def __init__(
        self,
        original: Matrix,
        q: Matrix,
        r: Matrix,
        steps: list,
    ) -> None:
        self.original = original
        self.q = q
        self.r = r
        self.steps = steps

    def __str__(self) -> str:
        """
        Return a readable summary of the QR decomposition.

        Shows the column-by-column Gram-Schmidt walkthrough, then Q and R
        individually, and states the factorisation relationship A = Q @ R.

        Returns
        -------
        str
            Human-readable decomposition summary.
        """
        header: str = (
            f"QR decomposition of "
            f"{self.original.rows}×{self.original.cols} matrix"
            f" — {len(self.steps)} steps\n"
        )

        steps_str: str = ""
        for step in self.steps:
            steps_str += f"\n{step}\n"

        body: str = (
            f"\nA = Q @ R\n"
            f"\nA:\n{self.original}\n"
            f"\nQ:\n{self.q}\n"
            f"\nR:\n{self.r}"
        )

        return header + steps_str + body

    def __repr__(self) -> str:
        """
        Return a concise data inspection string for this QRDecomposition.

        Returns
        -------
        str
            Compact representation showing shape and number of steps.

        Examples
        --------
        >>> qr_decomposition(Matrix([[1, 0], [0, 1]]))
        QRDecomposition(shape=2×2, steps=2)
        """
        return (
            f"QRDecomposition("
            f"shape={self.original.rows}×{self.original.cols}, "
            f"steps={len(self.steps)})"
        )

    def _repr_latex_(self) -> str:
        """Render the factorization ``A = QR`` in LaTeX."""
        return (
            f"$${matrix_to_latex(self.original)} = "
            f"{matrix_to_latex(self.q)}\\,{matrix_to_latex(self.r)}$$"
        )

__repr__()

Return a concise data inspection string for this QRDecomposition.

Returns:

Type Description
str

Compact representation showing shape and number of steps.

Examples:

>>> qr_decomposition(Matrix([[1, 0], [0, 1]]))
QRDecomposition(shape=2×2, steps=2)
Source code in panchi/algorithms/results.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
def __repr__(self) -> str:
    """
    Return a concise data inspection string for this QRDecomposition.

    Returns
    -------
    str
        Compact representation showing shape and number of steps.

    Examples
    --------
    >>> qr_decomposition(Matrix([[1, 0], [0, 1]]))
    QRDecomposition(shape=2×2, steps=2)
    """
    return (
        f"QRDecomposition("
        f"shape={self.original.rows}×{self.original.cols}, "
        f"steps={len(self.steps)})"
    )

__str__()

Return a readable summary of the QR decomposition.

Shows the column-by-column Gram-Schmidt walkthrough, then Q and R individually, and states the factorisation relationship A = Q @ R.

Returns:

Type Description
str

Human-readable decomposition summary.

Source code in panchi/algorithms/results.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def __str__(self) -> str:
    """
    Return a readable summary of the QR decomposition.

    Shows the column-by-column Gram-Schmidt walkthrough, then Q and R
    individually, and states the factorisation relationship A = Q @ R.

    Returns
    -------
    str
        Human-readable decomposition summary.
    """
    header: str = (
        f"QR decomposition of "
        f"{self.original.rows}×{self.original.cols} matrix"
        f" — {len(self.steps)} steps\n"
    )

    steps_str: str = ""
    for step in self.steps:
        steps_str += f"\n{step}\n"

    body: str = (
        f"\nA = Q @ R\n"
        f"\nA:\n{self.original}\n"
        f"\nQ:\n{self.q}\n"
        f"\nR:\n{self.r}"
    )

    return header + steps_str + body

panchi.algorithms.InverseResult

The result of a matrix inversion via Gauss-Jordan elimination.

Stores the original matrix, its inverse, and the row operations applied during reduction of the augmented matrix [A | I]. The inverse satisfies original @ inverse == identity(n) == inverse @ original.

Parameters:

Name Type Description Default
original Matrix

The square invertible matrix that was inverted.

required
inverse Matrix

The inverse of the original matrix.

required
steps list[RowOperation]

The ordered sequence of row operations applied to the augmented matrix [A | I] to produce [I | A⁻¹].

required

Examples:

>>> A = Matrix([[1, 2], [3, 4]])
>>> result = inverse(A)
>>> result.original @ result.inverse == identity(2)
True
Source code in panchi/algorithms/results.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
class InverseResult:
    """
    The result of a matrix inversion via Gauss-Jordan elimination.

    Stores the original matrix, its inverse, and the row operations applied
    during reduction of the augmented matrix [A | I]. The inverse satisfies
    original @ inverse == identity(n) == inverse @ original.

    Parameters
    ----------
    original : Matrix
        The square invertible matrix that was inverted.
    inverse : Matrix
        The inverse of the original matrix.
    steps : list[RowOperation]
        The ordered sequence of row operations applied to the augmented
        matrix [A | I] to produce [I | A⁻¹].

    Examples
    --------
    >>> A = Matrix([[1, 2], [3, 4]])
    >>> result = inverse(A)
    >>> result.original @ result.inverse == identity(2)
    True
    """

    def __init__(
        self,
        original: Matrix,
        inverse: Matrix,
        steps: list[RowOperation],
    ) -> None:
        self.original = original
        self.inverse = inverse
        self.steps = steps

    def __str__(self) -> str:
        """
        Return a readable summary of the inversion.

        Shows the number of steps taken and the computed inverse matrix.

        Returns
        -------
        str
            Human-readable inversion summary.

        Examples
        --------
        >>> print(inverse(Matrix([[1, 2], [3, 4]])))
        Inverse of 2×2 matrix — 6 steps

        Inverse:
        [[-2.0, 1.0],
         [1.5, -0.5]]
        """
        header: str = (
            f"Inverse of {self.original.rows}×{self.original.cols} matrix"
            f" — {len(self.steps)} steps\n"
        )

        return header + f"\nInverse:\n{self.inverse}"

    def __repr__(self) -> str:
        """
        Return a concise data inspection string for this InverseResult.

        Returns
        -------
        str
            Compact representation showing shape, number of steps,
            and the inverse matrix.

        Examples
        --------
        >>> inverse(Matrix([[1, 2], [3, 4]]))
        InverseResult(shape=2×2, steps=6)
        [[-2.0, 1.0],
         [1.5, -0.5]]
        """
        summary: str = (
            f"InverseResult("
            f"shape={self.original.rows}×{self.original.cols}, "
            f"steps={len(self.steps)})"
        )

        return f"{summary}\n{self.inverse}"

    def _repr_latex_(self) -> str:
        """Render ``A^{-1} = ...`` in LaTeX."""
        return (
            f"$${matrix_to_latex(self.original)}^{{-1}} = "
            f"{matrix_to_latex(self.inverse)}$$"
        )

__repr__()

Return a concise data inspection string for this InverseResult.

Returns:

Type Description
str

Compact representation showing shape, number of steps, and the inverse matrix.

Examples:

>>> inverse(Matrix([[1, 2], [3, 4]]))
InverseResult(shape=2×2, steps=6)
[[-2.0, 1.0],
 [1.5, -0.5]]
Source code in panchi/algorithms/results.py
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def __repr__(self) -> str:
    """
    Return a concise data inspection string for this InverseResult.

    Returns
    -------
    str
        Compact representation showing shape, number of steps,
        and the inverse matrix.

    Examples
    --------
    >>> inverse(Matrix([[1, 2], [3, 4]]))
    InverseResult(shape=2×2, steps=6)
    [[-2.0, 1.0],
     [1.5, -0.5]]
    """
    summary: str = (
        f"InverseResult("
        f"shape={self.original.rows}×{self.original.cols}, "
        f"steps={len(self.steps)})"
    )

    return f"{summary}\n{self.inverse}"

__str__()

Return a readable summary of the inversion.

Shows the number of steps taken and the computed inverse matrix.

Returns:

Type Description
str

Human-readable inversion summary.

Examples:

>>> print(inverse(Matrix([[1, 2], [3, 4]])))
Inverse of 2×2 matrix — 6 steps

Inverse: [[-2.0, 1.0], [1.5, -0.5]]

Source code in panchi/algorithms/results.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def __str__(self) -> str:
    """
    Return a readable summary of the inversion.

    Shows the number of steps taken and the computed inverse matrix.

    Returns
    -------
    str
        Human-readable inversion summary.

    Examples
    --------
    >>> print(inverse(Matrix([[1, 2], [3, 4]])))
    Inverse of 2×2 matrix — 6 steps

    Inverse:
    [[-2.0, 1.0],
     [1.5, -0.5]]
    """
    header: str = (
        f"Inverse of {self.original.rows}×{self.original.cols} matrix"
        f" — {len(self.steps)} steps\n"
    )

    return header + f"\nInverse:\n{self.inverse}"

panchi.algorithms.Solution

The result of solving a linear system Ax = b.

Stores the coefficient matrix A, the right-hand side vector b, the solution status, the solution vector x if a unique solution exists, and the row operations applied during reduction of the augmented matrix [A | b].

The three possible statuses reflect the three fundamentally different outcomes a linear system can have:

  • 'unique': exactly one solution exists, stored in solution.
  • 'infinite': infinitely many solutions exist (underdetermined system).
  • 'inconsistent': no solution exists (the system is contradictory).

Parameters:

Name Type Description Default
original Matrix

The coefficient matrix A.

required
target Vector

The right-hand side vector b.

required
status str

One of 'unique', 'infinite', or 'inconsistent'.

required
solution Vector or None

The unique solution vector x satisfying A @ x == b, or None if the system does not have a unique solution.

required
steps list[RowOperation]

The ordered sequence of row operations applied to the augmented matrix [A | b] during reduction.

required
particular Vector or None

A particular solution for infinite-solution systems. None for unique or inconsistent systems.

None
null_space VectorSpace or None

A basis for the null space of A for infinite-solution systems. None for unique or inconsistent systems.

None

Examples:

>>> A = Matrix([[1, 2], [3, 4]])
>>> b = Vector([5, 6])
>>> result = solve(A, b)
>>> result.status
'unique'
>>> A @ result.solution == b
True
Source code in panchi/algorithms/results.py
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
class Solution:
    """
    The result of solving a linear system Ax = b.

    Stores the coefficient matrix A, the right-hand side vector b, the
    solution status, the solution vector x if a unique solution exists,
    and the row operations applied during reduction of the augmented
    matrix [A | b].

    The three possible statuses reflect the three fundamentally different
    outcomes a linear system can have:

    - 'unique': exactly one solution exists, stored in solution.
    - 'infinite': infinitely many solutions exist (underdetermined system).
    - 'inconsistent': no solution exists (the system is contradictory).

    Parameters
    ----------
    original : Matrix
        The coefficient matrix A.
    target : Vector
        The right-hand side vector b.
    status : str
        One of 'unique', 'infinite', or 'inconsistent'.
    solution : Vector or None
        The unique solution vector x satisfying A @ x == b, or None if
        the system does not have a unique solution.
    steps : list[RowOperation]
        The ordered sequence of row operations applied to the augmented
        matrix [A | b] during reduction.
    particular : Vector or None
        A particular solution for infinite-solution systems. None for
        unique or inconsistent systems.
    null_space : VectorSpace or None
        A basis for the null space of A for infinite-solution systems.
        None for unique or inconsistent systems.

    Examples
    --------
    >>> A = Matrix([[1, 2], [3, 4]])
    >>> b = Vector([5, 6])
    >>> result = solve(A, b)
    >>> result.status
    'unique'
    >>> A @ result.solution == b
    True
    """

    def __init__(
        self,
        original: Matrix,
        target: Vector,
        status: str,
        solution: Vector | None,
        steps: list[RowOperation],
        particular: Vector | None = None,
        null_space: VectorSpace | None = None,
    ) -> None:
        self.original = original
        self.target = target
        self.status = status
        self.solution = solution
        self.steps = steps
        self.particular = particular
        self.null_space = null_space

    def __str__(self) -> str:
        """
        Return a readable summary of the solution.

        Shows the system dimensions, the status, and the solution vector
        if one exists.

        Returns
        -------
        str
            Human-readable solution summary.

        Examples
        --------
        >>> print(solve(Matrix([[1, 2], [3, 4]]), Vector([5, 6])))
        Solution to 2×2 system — unique

        x = [-4.0, 4.5]
        """
        header: str = (
            f"Solution to "
            f"{self.original.rows}×{self.original.cols} system"
            f" — {self.status}\n"
        )

        if self.solution is not None:
            return header + f"\nx = {self.solution}"

        if self.particular is not None and self.null_space is not None:
            return header + "\n" + self._format_general_solution()

        return header

    def _format_general_solution(self) -> str:
        basis = list(self.null_space)
        n = len(basis)

        if n == 1:
            params = ["t"]
        elif n == 2:
            params = ["s", "t"]
        else:
            params = [f"t{i + 1}" for i in range(n)]

        is_zero = all(self.particular[i] == 0 for i in range(self.particular.dims))

        parts = []
        if not is_zero:
            parts.append(str(self.particular))
        for param, vec in zip(params, basis, strict=False):
            parts.append(f"{param}·{vec}")

        return "x = " + " + ".join(parts)

    def __repr__(self) -> str:
        """
        Return a concise data inspection string for this Solution.

        Returns
        -------
        str
            Compact representation showing shape, status, and solution.

        Examples
        --------
        >>> solve(Matrix([[1, 2], [3, 4]]), Vector([5, 6]))
        Solution(shape=2×2, status=unique, solution=[-4.0, 4.5])
        """
        return (
            f"Solution("
            f"shape={self.original.rows}×{self.original.cols}, "
            f"status={self.status}, "
            f"solution={self.solution})"
        )

    def _repr_latex_(self) -> str:
        """Render the solution (unique vector or general solution) in LaTeX."""
        if self.solution is not None:
            return f"$x = {vector_to_latex(self.solution)}$"
        if self.particular is not None and self.null_space is not None:
            return f"$${self._general_solution_latex()}$$"
        return "$\\text{No solution (inconsistent system).}$"

    def _general_solution_latex(self) -> str:
        basis = list(self.null_space)
        n = len(basis)
        if n == 1:
            params = ["t"]
        elif n == 2:
            params = ["s", "t"]
        else:
            params = [f"t_{{{i + 1}}}" for i in range(n)]

        is_zero = all(self.particular[i] == 0 for i in range(self.particular.dims))

        parts = []
        if not is_zero:
            parts.append(vector_to_latex(self.particular))
        for param, vec in zip(params, basis, strict=False):
            parts.append(f"{param}\\,{vector_to_latex(vec)}")

        return "x = " + " + ".join(parts)

__repr__()

Return a concise data inspection string for this Solution.

Returns:

Type Description
str

Compact representation showing shape, status, and solution.

Examples:

>>> solve(Matrix([[1, 2], [3, 4]]), Vector([5, 6]))
Solution(shape=2×2, status=unique, solution=[-4.0, 4.5])
Source code in panchi/algorithms/results.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
def __repr__(self) -> str:
    """
    Return a concise data inspection string for this Solution.

    Returns
    -------
    str
        Compact representation showing shape, status, and solution.

    Examples
    --------
    >>> solve(Matrix([[1, 2], [3, 4]]), Vector([5, 6]))
    Solution(shape=2×2, status=unique, solution=[-4.0, 4.5])
    """
    return (
        f"Solution("
        f"shape={self.original.rows}×{self.original.cols}, "
        f"status={self.status}, "
        f"solution={self.solution})"
    )

__str__()

Return a readable summary of the solution.

Shows the system dimensions, the status, and the solution vector if one exists.

Returns:

Type Description
str

Human-readable solution summary.

Examples:

>>> print(solve(Matrix([[1, 2], [3, 4]]), Vector([5, 6])))
Solution to 2×2 system — unique

x = [-4.0, 4.5]

Source code in panchi/algorithms/results.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
def __str__(self) -> str:
    """
    Return a readable summary of the solution.

    Shows the system dimensions, the status, and the solution vector
    if one exists.

    Returns
    -------
    str
        Human-readable solution summary.

    Examples
    --------
    >>> print(solve(Matrix([[1, 2], [3, 4]]), Vector([5, 6])))
    Solution to 2×2 system — unique

    x = [-4.0, 4.5]
    """
    header: str = (
        f"Solution to "
        f"{self.original.rows}×{self.original.cols} system"
        f" — {self.status}\n"
    )

    if self.solution is not None:
        return header + f"\nx = {self.solution}"

    if self.particular is not None and self.null_space is not None:
        return header + "\n" + self._format_general_solution()

    return header

panchi.algorithms.EigenResult

The result of an eigenvalue computation via the QR algorithm.

Stores the original matrix, the computed eigenvalues, the corresponding eigenvectors, and metadata about the iterative process: how many iterations were run, whether the iteration converged, and the final (near) upper-triangular matrix the QR algorithm produced.

Eigenvalues are read off the diagonal of the QR iterate, and each eigenvector is paired with the eigenvalue at the same index. The values are numerical approximations, not exact or symbolic results.

Only real eigenvalues are supported. Matrices with complex eigenvalues (or eigenvalues of equal magnitude) may not converge; in that case converged is False and eigenvectors is empty.

Parameters:

Name Type Description Default
original Matrix

The square matrix whose spectrum was computed.

required
eigenvalues list[float]

The computed eigenvalues, in the order they appear on the diagonal of the final iterate.

required
eigenvectors list[Vector]

The eigenvectors, paired by index with eigenvalues. Empty when the iteration did not converge.

required
iterations int

The number of QR iterations performed.

required
converged bool

Whether the below-diagonal mass fell below the tolerance before the iteration limit was reached.

required
triangular Matrix

The final (near) upper-triangular matrix produced by the iteration.

required

Examples:

>>> result = eigen(Matrix([[2, 1], [1, 2]]))
>>> sorted(round(v, 6) for v in result.eigenvalues)
[1.0, 3.0]
Source code in panchi/algorithms/results.py
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
class EigenResult:
    """
    The result of an eigenvalue computation via the QR algorithm.

    Stores the original matrix, the computed eigenvalues, the corresponding
    eigenvectors, and metadata about the iterative process: how many
    iterations were run, whether the iteration converged, and the final
    (near) upper-triangular matrix the QR algorithm produced.

    Eigenvalues are read off the diagonal of the QR iterate, and each
    eigenvector is paired with the eigenvalue at the same index. The values
    are numerical approximations, not exact or symbolic results.

    Only real eigenvalues are supported. Matrices with complex eigenvalues
    (or eigenvalues of equal magnitude) may not converge; in that case
    ``converged`` is False and ``eigenvectors`` is empty.

    Parameters
    ----------
    original : Matrix
        The square matrix whose spectrum was computed.
    eigenvalues : list[float]
        The computed eigenvalues, in the order they appear on the diagonal
        of the final iterate.
    eigenvectors : list[Vector]
        The eigenvectors, paired by index with eigenvalues. Empty when the
        iteration did not converge.
    iterations : int
        The number of QR iterations performed.
    converged : bool
        Whether the below-diagonal mass fell below the tolerance before the
        iteration limit was reached.
    triangular : Matrix
        The final (near) upper-triangular matrix produced by the iteration.

    Examples
    --------
    >>> result = eigen(Matrix([[2, 1], [1, 2]]))
    >>> sorted(round(v, 6) for v in result.eigenvalues)
    [1.0, 3.0]
    """

    def __init__(
        self,
        original: Matrix,
        eigenvalues: list[float],
        eigenvectors: list[Vector],
        iterations: int,
        converged: bool,
        triangular: Matrix,
    ) -> None:
        self.original = original
        self.eigenvalues = eigenvalues
        self.eigenvectors = eigenvectors
        self.iterations = iterations
        self.converged = converged
        self.triangular = triangular

    @property
    def pairs(self) -> list[tuple[float, Vector]]:
        """
        The eigenvalue/eigenvector pairs, zipped by index.

        Returns
        -------
        list[tuple[float, Vector]]
            One (eigenvalue, eigenvector) tuple per computed eigenvector.
            Empty when no eigenvectors were computed.
        """
        return list(zip(self.eigenvalues, self.eigenvectors, strict=False))

    def __str__(self) -> str:
        """
        Return a readable summary of the eigenvalue computation.

        Shows the iteration count and convergence status, then each
        eigenvalue together with its eigenvector when available.

        Returns
        -------
        str
            Human-readable eigendecomposition summary.
        """
        header: str = (
            f"Eigendecomposition of "
            f"{self.original.rows}×{self.original.cols} matrix"
            f" — {self.iterations} iterations (converged: {self.converged})\n"
        )

        if self.eigenvectors:
            body = "\n" + "\n".join(
                f"λ = {value}\n  v = {vector}" for value, vector in self.pairs
            )
        else:
            body = "\nEigenvalues: " + ", ".join(str(v) for v in self.eigenvalues)

        return header + body

    def __repr__(self) -> str:
        """
        Return a concise data inspection string for this EigenResult.

        Returns
        -------
        str
            Compact representation showing shape, iteration count, and
            convergence status.

        Examples
        --------
        >>> eigen(Matrix([[2, 1], [1, 2]]))
        EigenResult(shape=2×2, iterations=..., converged=True)
        """
        return (
            f"EigenResult("
            f"shape={self.original.rows}×{self.original.cols}, "
            f"iterations={self.iterations}, "
            f"converged={self.converged})"
        )

    def _repr_latex_(self) -> str:
        """Render the eigenvalue/eigenvector pairs in LaTeX."""
        values = ",\\; ".join(scalar_to_latex(round(v, 6)) for v in self.eigenvalues)
        if not self.eigenvectors:
            body = f"\\lambda \\in \\left\\{{ {values} \\right\\}}"
            if not self.converged:
                body = "\\text{did not converge; } " + body
            return f"${body}$"
        lines = []
        for i, (value, vector) in enumerate(self.pairs):
            lines.append(
                f"\\lambda_{{{i + 1}}} = {scalar_to_latex(round(value, 6))}, "
                f"\\quad v_{{{i + 1}}} = {vector_to_latex(vector)}"
            )
        body = " \\\\\n".join(lines)
        return f"$$\\begin{{aligned}}\n{body}\n\\end{{aligned}}$$"

pairs property

The eigenvalue/eigenvector pairs, zipped by index.

Returns:

Type Description
list[tuple[float, Vector]]

One (eigenvalue, eigenvector) tuple per computed eigenvector. Empty when no eigenvectors were computed.

__repr__()

Return a concise data inspection string for this EigenResult.

Returns:

Type Description
str

Compact representation showing shape, iteration count, and convergence status.

Examples:

>>> eigen(Matrix([[2, 1], [1, 2]]))
EigenResult(shape=2×2, iterations=..., converged=True)
Source code in panchi/algorithms/results.py
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
def __repr__(self) -> str:
    """
    Return a concise data inspection string for this EigenResult.

    Returns
    -------
    str
        Compact representation showing shape, iteration count, and
        convergence status.

    Examples
    --------
    >>> eigen(Matrix([[2, 1], [1, 2]]))
    EigenResult(shape=2×2, iterations=..., converged=True)
    """
    return (
        f"EigenResult("
        f"shape={self.original.rows}×{self.original.cols}, "
        f"iterations={self.iterations}, "
        f"converged={self.converged})"
    )

__str__()

Return a readable summary of the eigenvalue computation.

Shows the iteration count and convergence status, then each eigenvalue together with its eigenvector when available.

Returns:

Type Description
str

Human-readable eigendecomposition summary.

Source code in panchi/algorithms/results.py
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
def __str__(self) -> str:
    """
    Return a readable summary of the eigenvalue computation.

    Shows the iteration count and convergence status, then each
    eigenvalue together with its eigenvector when available.

    Returns
    -------
    str
        Human-readable eigendecomposition summary.
    """
    header: str = (
        f"Eigendecomposition of "
        f"{self.original.rows}×{self.original.cols} matrix"
        f" — {self.iterations} iterations (converged: {self.converged})\n"
    )

    if self.eigenvectors:
        body = "\n" + "\n".join(
            f"λ = {value}\n  v = {vector}" for value, vector in self.pairs
        )
    else:
        body = "\nEigenvalues: " + ", ".join(str(v) for v in self.eigenvalues)

    return header + body