Basic Operations

Why Linear Algebra Is the Engine of Scientific Computing

Every major algorithm in scientific computing β€” from solving PDEs to training neural networks to decoding wireless signals β€” reduces to linear algebra at its core. NumPy and SciPy provide thin Python wrappers around battle-tested Fortran/C libraries (BLAS and LAPACK), giving you near-C performance with Python convenience.

In wireless communications (Book 1), the channel equation y=Hx+n\mathbf{y} = \mathbf{H}\mathbf{x} + \mathbf{n} is the starting point for everything: detection, estimation, beamforming, and precoding are all linear algebra operations on H\mathbf{H}.

Definition:

Matrix Multiplication with the @ Operator

For A∈CmΓ—n\mathbf{A} \in \mathbb{C}^{m \times n} and B∈CnΓ—p\mathbf{B} \in \mathbb{C}^{n \times p}, the product C=AB\mathbf{C} = \mathbf{A}\mathbf{B} has entries:

Cij=βˆ‘k=1nAikBkjC_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj}

In NumPy, this is computed via the @ operator (PEP 465):

C = A @ B           # matrix-matrix
y = A @ x           # matrix-vector
s = x.conj() @ y    # inner product (scalar)

The @ operator dispatches to BLAS gemm (general matrix multiply), which achieves near-peak hardware throughput via cache-optimal blocking and SIMD vectorization.

For 1D arrays, @ computes the inner product. For 2D arrays, it computes the matrix product. For higher dimensions, it broadcasts over batch dimensions β€” a pattern used heavily in batched MIMO processing.

Definition:

Transpose and Conjugate Transpose

The transpose AT\mathbf{A}^T swaps rows and columns: (AT)ij=Aji(A^T)_{ij} = A_{ji}.

The conjugate transpose (Hermitian transpose) AH\mathbf{A}^H additionally conjugates each element: (AH)ij=Ajiβˆ—(A^H)_{ij} = A_{ji}^*.

A_T = A.T                    # transpose
A_H = A.conj().T             # conjugate transpose
A_H = np.conj(A.T)           # equivalent

For real matrices, AH=AT\mathbf{A}^H = \mathbf{A}^T. In wireless communications, channel matrices are complex, so HH\mathbf{H}^H appears in matched filtering, MMSE estimation, and beamforming.

Definition:

Trace of a Matrix

The trace of a square matrix A∈CnΓ—n\mathbf{A} \in \mathbb{C}^{n \times n} is:

tr(A)=βˆ‘i=1nAii\mathrm{tr}(\mathbf{A}) = \sum_{i=1}^{n} A_{ii}

Key properties:

  • tr(A+B)=tr(A)+tr(B)\mathrm{tr}(\mathbf{A} + \mathbf{B}) = \mathrm{tr}(\mathbf{A}) + \mathrm{tr}(\mathbf{B})
  • tr(AB)=tr(BA)\mathrm{tr}(\mathbf{A}\mathbf{B}) = \mathrm{tr}(\mathbf{B}\mathbf{A}) (cyclic property)
  • tr(A)=βˆ‘iΞ»i\mathrm{tr}(\mathbf{A}) = \sum_i \lambda_i (sum of eigenvalues)
t = np.trace(A)

In MIMO, tr(Rxx)\mathrm{tr}(\mathbf{R}_{xx}) gives the total transmit power when Rxx\mathbf{R}_{xx} is the transmit covariance matrix.

Definition:

Determinant

The determinant of a square matrix A\mathbf{A} is a scalar that encodes the signed volume scaling factor of the linear transformation:

det⁑(A)=∏i=1nλi\det(\mathbf{A}) = \prod_{i=1}^{n} \lambda_i

d = np.linalg.det(A)
sign, logdet = np.linalg.slogdet(A)  # numerically stable

For large matrices, use slogdet to avoid overflow/underflow: it returns (sign,log⁑∣det⁑∣)(\mathrm{sign}, \log|\det|) so that det⁑=signβ‹…elog⁑∣det⁑∣\det = \mathrm{sign} \cdot e^{\log|\det|}.

In MIMO capacity calculations, the log-determinant appears directly: C=log⁑2det⁑ ⁣(I+SNRNtHHH)C = \log_2 \det\!\left(\mathbf{I} + \frac{\mathrm{SNR}}{N_t} \mathbf{H}\mathbf{H}^H\right).

Definition:

Matrix Inverse and Solving Linear Systems

The inverse Aβˆ’1\mathbf{A}^{-1} of a nonsingular matrix satisfies AAβˆ’1=I\mathbf{A}\mathbf{A}^{-1} = \mathbf{I}. To solve Ax=b\mathbf{A}\mathbf{x} = \mathbf{b}:

# BAD: computes full inverse (slow, numerically unstable)
x_bad = np.linalg.inv(A) @ b

# GOOD: solves directly via LU decomposition
x = np.linalg.solve(A, b)

np.linalg.solve calls LAPACK gesv, which performs LU factorization with partial pivoting: PA=LU\mathbf{P}\mathbf{A} = \mathbf{L}\mathbf{U}. Cost: 23n3\frac{2}{3}n^3 flops for the factorization, 2n22n^2 flops for the solve. Computing and applying Aβˆ’1\mathbf{A}^{-1} costs 2n32n^3 flops and doubles the numerical error.

Definition:

Vector and Matrix Norms

The pp-norm of a vector x∈Cn\mathbf{x} \in \mathbb{C}^n is: βˆ₯xβˆ₯p=(βˆ‘i=1n∣xi∣p)1/p\|\mathbf{x}\|_p = \left(\sum_{i=1}^{n} |x_i|^p\right)^{1/p}

Common choices: p=2p=2 (Euclidean), p=1p=1 (Manhattan), p=∞p=\infty (max).

The matrix pp-norm is the induced operator norm: βˆ₯Aβˆ₯p=max⁑xβ‰ 0βˆ₯Axβˆ₯pβˆ₯xβˆ₯p\|\mathbf{A}\|_p = \max_{\mathbf{x} \neq \mathbf{0}} \frac{\|\mathbf{A}\mathbf{x}\|_p}{\|\mathbf{x}\|_p}

The Frobenius norm uses all entries: βˆ₯Aβˆ₯F=βˆ‘ij∣Aij∣2\|\mathbf{A}\|_F = \sqrt{\sum_{ij} |A_{ij}|^2}.

np.linalg.norm(x, ord=2)      # vector 2-norm
np.linalg.norm(A, ord=2)      # largest singular value
np.linalg.norm(A, ord='fro')  # Frobenius norm

Theorem: Solve Is Faster and More Accurate Than Inverse

For a nonsingular A∈CnΓ—n\mathbf{A} \in \mathbb{C}^{n \times n} and b∈Cn\mathbf{b} \in \mathbb{C}^n, computing x\mathbf{x} via np.linalg.solve(A, b) requires 23n3+2n2\frac{2}{3}n^3 + 2n^2 flops and produces a relative error bounded by βˆ₯xβˆ’x^βˆ₯βˆ₯xβˆ₯≀cβ‹…ΞΊ(A)β‹…Ο΅mach\frac{\|\mathbf{x} - \hat{\mathbf{x}}\|}{\|\mathbf{x}\|} \le c \cdot \kappa(\mathbf{A}) \cdot \epsilon_{\mathrm{mach}}.

Computing x=Aβˆ’1b\mathbf{x} = \mathbf{A}^{-1}\mathbf{b} via np.linalg.inv(A) @ b requires 2n3+2n22n^3 + 2n^2 flops and the error bound has an additional factor of ΞΊ(A)\kappa(\mathbf{A}).

solve performs LU decomposition and forward/back substitution, while inv computes all nn columns of Aβˆ’1\mathbf{A}^{-1} even though you only need one linear combination (Aβˆ’1b\mathbf{A}^{-1}\mathbf{b}).

Theorem: Transpose and Trace Identities

For conformable matrices A\mathbf{A}, B\mathbf{B}:

  1. (AB)T=BTAT(\mathbf{A}\mathbf{B})^T = \mathbf{B}^T \mathbf{A}^T
  2. (AB)H=BHAH(\mathbf{A}\mathbf{B})^H = \mathbf{B}^H \mathbf{A}^H
  3. tr(AB)=tr(BA)\mathrm{tr}(\mathbf{A}\mathbf{B}) = \mathrm{tr}(\mathbf{B}\mathbf{A})
  4. tr(AH)=tr(A)β€Ύ\mathrm{tr}(\mathbf{A}^H) = \overline{\mathrm{tr}(\mathbf{A})}
  5. tr(ABT)=βˆ‘ijAijBij\mathrm{tr}(\mathbf{A}\mathbf{B}^T) = \sum_{ij} A_{ij} B_{ij} (Frobenius inner product)

Theorem: LU Decomposition with Partial Pivoting

Any nonsingular A∈CnΓ—n\mathbf{A} \in \mathbb{C}^{n \times n} can be factored as PA=LU\mathbf{P}\mathbf{A} = \mathbf{L}\mathbf{U}, where P\mathbf{P} is a permutation matrix, L\mathbf{L} is unit lower triangular, and U\mathbf{U} is upper triangular. The factorization costs 23n3\frac{2}{3}n^3 flops.

LU decomposition is Gaussian elimination expressed as a matrix product. Once computed, solving Ax=b\mathbf{A}\mathbf{x} = \mathbf{b} becomes two triangular solves (O(n2)O(n^2)), so multiple right-hand sides can be solved cheaply.

Example: Solving the MIMO Channel Equation

In a 4Γ—44 \times 4 MIMO system, the received signal is y=Hx+n\mathbf{y} = \mathbf{H}\mathbf{x} + \mathbf{n}. Given a known channel H\mathbf{H} and received vector y\mathbf{y}, recover the transmitted symbol vector x\mathbf{x} using zero-forcing (ZF): x^ZF=Hβˆ’1y\hat{\mathbf{x}}_{\mathrm{ZF}} = \mathbf{H}^{-1}\mathbf{y}. Show that solve gives the same answer as inv but is faster.

Example: MIMO Capacity via Log-Determinant

Compute the ergodic capacity of a 4Γ—44 \times 4 MIMO Rayleigh channel at SNR values from 0 to 30 dB. The capacity is: C=log⁑2det⁑ ⁣(INr+SNRNtHHH)C = \log_2\det\!\left(\mathbf{I}_{N_r} + \frac{\mathrm{SNR}}{N_t}\mathbf{H}\mathbf{H}^H\right) Use slogdet for numerical stability and average over 1000 channel realizations.

Example: Batch Solving Multiple Systems

In OFDM, each subcarrier has its own channel coefficient. Given 64 subcarriers with 2Γ—22 \times 2 MIMO, solve all 64 systems Hkxk=yk\mathbf{H}_k \mathbf{x}_k = \mathbf{y}_k simultaneously using vectorized np.linalg.solve.

Solve vs Inv: Speed and Accuracy Comparison

Compare np.linalg.solve and np.linalg.inv @ b in terms of execution time and relative error as a function of matrix size and condition number.

Parameters
512
100

BLAS

Basic Linear Algebra Subprograms β€” a standardized API for vector and matrix operations (dot products, matrix multiply). OpenBLAS and Intel MKL are common high-performance implementations.

Related: LAPACK

LAPACK

Linear Algebra PACKage β€” a Fortran library providing routines for solving linear systems, eigenvalue problems, SVD, and matrix factorizations. NumPy and SciPy call LAPACK under the hood.

Related: BLAS

condition number

The ratio ΞΊ(A)=βˆ₯Aβˆ₯β‹…βˆ₯Aβˆ’1βˆ₯\kappa(\mathbf{A}) = \|\mathbf{A}\| \cdot \|\mathbf{A}^{-1}\|. A large condition number means the system is ill-conditioned: small perturbations in b\mathbf{b} cause large changes in x\mathbf{x}.

Common Mistake: Using inv Instead of solve

Mistake:

Computing x = np.linalg.inv(A) @ b to solve Ax=b\mathbf{Ax} = \mathbf{b}. This is 3Γ—3\times slower and can double the numerical error.

Correction:

Always use x = np.linalg.solve(A, b). If you need to solve multiple right-hand sides, pass a matrix B\mathbf{B}: X = np.linalg.solve(A, B) solves all columns simultaneously.

Common Mistake: Using np.dot Instead of @

Mistake:

Using np.dot(A, B) for matrix multiplication. While it works, it behaves differently for higher-dimensional arrays compared to @.

Correction:

Use the @ operator for matrix multiplication. np.dot treats the last axis of the first argument and the second-to-last of the second, which can be confusing for 3D+ arrays. @ follows the standard matmul broadcasting rules.

Common Mistake: Determinant Overflow for Large Matrices

Mistake:

Using np.linalg.det(A) for large matrices, which can return inf or 0.0 due to floating-point overflow/underflow.

Correction:

Use sign, logdet = np.linalg.slogdet(A) and work with the log-determinant directly. This is essential for MIMO capacity calculations and Gaussian log-likelihoods.

Why This Matters: Zero-Forcing Detection in MIMO

The zero-forcing (ZF) detector for a MIMO system y=Hx+n\mathbf{y} = \mathbf{H}\mathbf{x} + \mathbf{n} is:

x^ZF=(HHH)βˆ’1HHy\hat{\mathbf{x}}_{\mathrm{ZF}} = (\mathbf{H}^H\mathbf{H})^{-1}\mathbf{H}^H\mathbf{y}

This is exactly a least-squares solve. In NumPy:

x_zf = np.linalg.solve(H.conj().T @ H, H.conj().T @ y)

Or equivalently via lstsq (Section 6.6). The matrix HHH\mathbf{H}^H\mathbf{H} is Hermitian positive definite when H\mathbf{H} has full column rank, so scipy.linalg.solve with assume_a='pos' is even faster.

Historical Note: PEP 465: The @ Operator

Before Python 3.5, matrix multiplication required np.dot(A, B) or A.dot(B), making chained products like AHBC\mathbf{A}^H\mathbf{B}\mathbf{C} unreadable. Nathaniel Smith proposed PEP 465 in 2014, introducing @ as a dedicated matrix multiply operator. It was one of the most rapidly adopted PEPs in scientific Python history.

Historical Note: LAPACK: 35+ Years of Linear Algebra

LAPACK was first released in 1992 as a successor to LINPACK and EISPACK, written primarily by Jack Dongarra and collaborators. It remains the gold standard for dense linear algebra and is called by virtually every scientific computing language β€” NumPy, MATLAB, R, Julia, and Mathematica all use LAPACK routines. The naming convention (dgesv, zheev, etc.) encodes the precision (d=double, z=complex double), matrix type (ge=general, he=Hermitian), and operation (sv=solve, ev=eigenvalue).

Quick Check

What is the best way to solve Ax=b\mathbf{Ax} = \mathbf{b} in NumPy?

x = np.linalg.solve(A, b)

x = np.linalg.inv(A) @ b

x = A @ b

x = np.dot(np.linalg.inv(A), b)

Quick Check

Why should you use slogdet instead of det for large matrices?

To avoid overflow/underflow of the determinant

slogdet is faster than det

det does not work for complex matrices

slogdet returns eigenvalues as a bonus

Key Takeaway

Never compute Aβˆ’1\mathbf{A}^{-1} just to multiply. Use np.linalg.solve(A, b) instead of np.linalg.inv(A) @ b. Use np.linalg.slogdet instead of np.linalg.det for large matrices. These are not style preferences β€” they are correctness requirements for ill-conditioned systems.

Key Takeaway

The @ operator is your primary tool for matrix operations. It handles matrix-matrix, matrix-vector, and batched products. It dispatches to BLAS, which is typically the fastest code running on your machine.

Basic Linear Algebra Operations

python
Complete examples of solve, eig, svd, condition number computation, and performance comparisons with timing benchmarks.
# Code from: ch06/python/basic_linalg.py
# Load from backend supplements endpoint