Tutorial 6
Click the badges above to run this tutorial interactively in your browser without installing anything!
- Binder: Free cloud-based Jupyter environment
- Colab: Google's free Jupyter notebook environment
Topics Covered
This tutorial covers the following topics using Python and SymPy:
- Matrix algebra: transpose, trace, determinant, inverse
- Orthogonal matrices
- Conditioning of a system and the determinant test
- Cramer's rule and the noise sensitivity of ill-conditioned systems
- Gauss elimination with partial pivoting (GEwPP)
- REF, RREF and rank
Introduction
import sympy as sy
from sympy.abc import x, y, z, t
from sympy import Matrix, Rational, eye, zeros, sqrt, N, simplify, nsimplify
sy.init_printing(use_latex=True)
def cramer(M, b):
"""Solve M x = b with Cramer's rule. Raises if the system is singular."""
D = M.det()
if D == 0:
raise ZeroDivisionError(f"det(A) = {D}: Cramer's rule does not apply (singular system)")
cols = []
for i in range(M.cols):
Mi = M.copy()
Mi[:, i] = b # replace column i with the right-hand side
cols.append(Mi.det() / D)
return Matrix(cols)
In Python 1/4 is the float 0.25, so 1/4*(...) silently converts an exact
SymPy result into a floating-point one. Write (...)/4, or use sy.Rational(1, 4).
Question 1
Solve for in
Rearranging, .
A = Matrix([[1, 0, 2], [3, -1, -3]])
B = Matrix([[4, 6, 8], [-2, 5, 7]])
C = Matrix([[10, 11, 3], [-10, -27, -25], [70, -29, -27]])
D = Matrix([[1, 2, 0], [-1, 3, 0], [0, 0, 4]])
E = Matrix([[2, -2, 1], [1, 2, 2], [2, 1, -2]])
display(C.trace(), D.det()) # -44, 20
display(A.T * B)
First verify that really is orthogonal, so that .
E3 = E/3
display(simplify(E3 * E3.T)) # the identity matrix
display(simplify(E3.inv() - E3.T) == zeros(3, 3)) # True -> orthogonal
Now evaluate the bracket and solve for .
inner = ((220/C.trace()) * (A.T * B) - C)/4 + (20/D.det()) * D
display(inner)
F = E3.T * inner # = (E/3)^-1 * inner
display(F)
Question 2
For each case, comment on the condition of the coefficient matrix in terms of its determinant, without solving.
case1 = Matrix([[3, 7], [1, -2]]) # 3x2 + 7x1 = -5 ; x2 - 2x1 = 1/2
case2 = Matrix([[1, 2], [2, Rational(3999, 1000)]]) # x2 + 2x1 = 4 ; 2x2 + 3.999x1 = 7.999
case3 = Matrix([[5, 20], [1, 4]]) # 5x2 + 20x1 = -1 ; x2 + 4x1 = 2
display(case1.det()) # -13 -> well-conditioned, unique solution
display(case2.det()) # -1/1000 -> ill-conditioned, det is almost 0
display(case3.det()) # 0 -> singular
| Case | Condition | Geometry | Solution | |
|---|---|---|---|---|
| 1 | well-conditioned | one intersection point (very different slopes) | unique | |
| 2 | ill-conditioned | nearly parallel lines (almost equal slopes) | exists but extremely noise-sensitive | |
| 3 | singular | parallel lines (identical slopes) | none |
Question 3
Continue Q2 with Cramer's rule.
display(cramer(case1, Matrix([-5, Rational(1, 2)]))) # <-1/2, -1/2>
display(cramer(case2, Matrix([4, Rational(7999, 1000)]))) # <2, 1>
Case 1 gives and Case 2 gives , ; both check out when substituted back. Case 3 has , so Cramer's rule fails:
try:
cramer(case3, Matrix([-1, 2]))
except ZeroDivisionError as err:
display(err) # det(A) = 0 -> Cramer's rule does not apply
Because the two equations are inconsistent (they describe parallel lines), the system has no solution.
Question 4
Repeat the Cramer calculation with the measured (noisy) right-hand side.
b1_noise = Matrix([Rational(-5001, 1000), Rational(4999, 10000)])
display([N(v, 4) for v in cramer(case1, b1_noise)]) # [-0.5002, -0.5001]
b2_noise = Matrix([Rational(4001, 1000), Rational(7998, 1000)])
display([N(v, 4) for v in cramer(case2, b2_noise)]) # [-3.999, 4.000]
Case 1 is well-conditioned, so the tiny perturbation is not amplified and the answer stays close to . Case 2 is ill-conditioned: a change of only in the right-hand side moves the solution from to about . Small measurement noise is hugely magnified.
Question 5
Solve the following system by GEwPP (scaling, partial pivoting, forward elimination, backward substitution).
G = Matrix([[0, 1, 1, 2],
[1, 2, 3, 1],
[1, 2, 1, 0],
[2, 1, 1, 1]])
b = Matrix([0, 5, 4, 3])
display(G.LUsolve(b)) # LU decomposition with partial pivoting
display(G.row_join(b).rref()[0]) # augmented matrix -> RREF
The LUsolve method performs partial-pivoting elimination (SymPy does not apply the
tutorial's row-scaling step, but the answer is the same); the rref of the augmented
matrix shows the same answer in one step.
Question 6
Obtain the REF, the RREF, the number of linearly independent vectors, and the rank.
M = Matrix([[1, 3, 4],
[3, 9, 12],
[2, 6, 9]])
display(M.echelon_form()) # REF
display(M.rref()) # RREF + pivot columns
display(M.rank()) # 2
i. REF:
ii. RREF: — note this is not the same as the REF, because the entry above the second pivot must also be cleared.
iii. There are 2 linearly independent vectors (2 pivot columns).
iv. Rank = 2, so the matrix is rank deficient (it is but not full rank).
MA = Matrix([[1, Rational(1, 2), Rational(1, 2), Rational(1, 2)],
[0, Rational(3, 4), Rational(1, 4), Rational(-1, 4)],
[0, 0, Rational(2, 3), Rational(1, 3)],
[0, 0, 0, Rational(6, 7)]])
display(MA.rank()) # 4 -> full rank
Matrix A from the question table is upper triangular with no zero on the diagonal, so its rank is 4 and it is full rank, matching the printed solution.
The REF and the RREF differ here: the REF still has a non-zero entry above the second pivot, and the RREF clears it.
Question 7
An electronics company produces transistors , resistors and chips .
M = Matrix([[4, 3, 2], # copper
[1, 3, 1], # zinc
[2, 1, 3]]) # glass
b = Matrix([960, 510, 610])
weekly = M.LUsolve(b)
display(weekly) # <120, 100, 90> per week
display(weekly / 5) # <24, 20, 18> per day
So the company produces transistors, resistors and chips per week, i.e. , and per day on average.
Question 8
Gravel pits — use Cramer's rule.
M = Matrix([[Rational(52, 100), Rational(20, 100), Rational(25, 100)],
[Rational(30, 100), Rational(50, 100), Rational(20, 100)],
[Rational(18, 100), Rational(30, 100), Rational(55, 100)]])
b = Matrix([4800, 5800, 5700])
display(M.det()) # 43/500 = 0.086
display([N(v, 8) for v in cramer(M, b)]) # [4005.814, 7131.395, 5162.791]
All percentages enter as decimals: fine gravel and coarse gravel in the first numerator, so the and of the raw table are not used directly.
Question 9
Three reactors linked by pipes. The mass-balance equations are , and .
M = Matrix([[120, -20, 0],
[-80, 80, 0],
[-40, -60, 120]])
b = Matrix([400, 0, 200])
display(M.LUsolve(b)) # naive GE (no pivoting needed here)
Question 10
Three planes: , , .
M = Matrix([[3, 6, -3],
[4, -2, 6],
[3, 2, 1]])
b = Matrix([-6, 2, -2])
display(M.det()) # 0 -> singular
display(M.rank(), M.row_join(b).rank()) # 2, 2 -> consistent, infinite solutions
display(sy.linsolve((M, b), (x, y, z))) # {(-z, z - 1, z)}
Since but the rank of the coefficient matrix equals the rank of the augmented matrix, the system is consistent with infinitely many solutions. Setting :
which is the line along which all three planes intersect. Note that matrix inversion
and Cramer's rule are both unusable here — GEwPP (or linsolve) is the right tool.
Extra Learning Resources
Key Concepts to Master
- Determinant test: → unique solution; → no solution or infinitely many
- Well- vs ill-conditioned: a determinant close to zero means small input errors produce large output errors
- Cramer's rule: , where has column replaced by
- Rank: the number of pivots; rank deficient means the rank is less than the matrix size
- GEwPP: scaling to compare rows fairly, partial pivoting to avoid dividing by a tiny pivot, then forward elimination and back substitution
SymPy Resources
- SymPy Matrices —
det,inv,rank,rref,echelon_form,LUsolve,row_join - SymPy
linsolve— solve singular and underdetermined systems - SymPy Matrix expressions — symbolic matrix algebra
Common Pitfalls
- Python integer division: use
/4orsy.Rational(1, 4), never1/4, when you need an exact result rref()returns a tuple(matrix, pivot_columns)— index[0]to get the matrixrref()andechelon_form()are not the same thing- A matrix with a zero row can still be consistent — compare the ranks, don't just look at the determinant
- Floats such as
3.999are exact decimal literals in SymPy only if you useRational(3999, 1000);3.999alone becomes a binary float