diff --git a/stack/maxima/contrib/linearalgebra.mac b/stack/maxima/contrib/linearalgebra.mac
new file mode 100644
index 00000000000..5485af05a3d
--- /dev/null
+++ b/stack/maxima/contrib/linearalgebra.mac
@@ -0,0 +1,1249 @@
+/* Author Luke Longworth
+ University of Canterbury
+ Copyright (C) 2024 Luke Longworth
+
+ This program is free software: you can redistribute it or modify
+ it under the terms of the GNU General Public License version two.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see . */
+
+/****************************************************************/
+/* Linear algebra functions for STACK */
+/* */
+/* V0.2.4 May 2024 */
+/* */
+/****************************************************************/
+
+/*******************************************************************************/
+/* Provides convenience functions for column and row vectors for student input */
+/*******************************************************************************/
+texput(c,
+ lambda([ex], block([ns,str,ii],
+ ns: args(ex),
+ str: ["\\begin{bmatrix} "],
+ for ii: 1 thru length(ns) do (str: append(str, [ev(tex1(ns[ii]),simp), " \\\\ "])),
+ str[length(str)]: " \\end{bmatrix}",
+ simplode(str)
+ ))
+);
+
+texput(r,
+ lambda([ex], block([ns,str,ii],
+ ns: args(ex),
+ str: ["\\begin{bmatrix} "],
+ for ii: 1 thru length(ns) do (str: append(str, [ev(tex1(ns[ii]),simp), " & "])),
+ str[length(str)]: " \\end{bmatrix}",
+ simplode(str)
+ ))
+);
+
+declare([c,r],nonscalar);
+
+/**
+ * Converts c and r into matrices.
+ * Works on entire expressions.
+ * Returns expression unchanged if simp:true and matrices do not conform.
+ *
+ * @param[expression] ex An expression that may contain c or r
+ * @return[scalar expression] The expression with c and r replaced with matrices, or the original expression if matrices do not conform
+ */
+vec_convert(ex):= block([ex2],
+ ex2: errcatch(ev(ex,c = lambda([[ex]],transpose(matrix(ex))),r = lambda([[ex]],matrix(ex)))),
+ if emptyp(ex2) then return(ex) else return(first(ex2))
+);
+
+/**
+ * Given a row or column vector, convert it to c() or r() form.
+ * Intended to create model answers in instances where students
+ * are expected to use these convenience functions.
+ * Does not loop through an expression, will only work on vectors
+ * as individual objects.
+ *
+ * @param[matrix] ex A vector; i.e. a 1xN or Mx1 matrix
+ * @return[expression] That vector as a c() or r() vector
+ */
+un_vec_convert(ex):= block([],
+ if col_vecp(ex) then ex: apply(c,list_matrix_entries(ex)) else
+ if row_vecp(ex) then ex: apply(r,list_matrix_entries(ex)),
+ return(ex)
+);
+
+/*******************************************************************************/
+/* A convenience function for displaying a matrix as an augmented matrix */
+/*******************************************************************************/
+texput(aug_matrix, lambda([ex], block([M,ll,rr,m,n,A,b,simp],
+ simp:true,
+ M: apply(matrix,args(ex)),
+ ll: lmxchar,
+ if is(ll="[") then rr: "]"
+ else if is(ll="(") then rr: ")"
+ else if is(ll="") then (ll: ".", rr: ".")
+ else if is(ll="{") then (ll: "\\{", rr: "\\}")
+ else if is(ll="|") then rr: "|",
+ [m, n]: matrix_size(M),
+ A: submatrix(M,n),
+ b: col(M,n),
+ sconcat("\\left",ll,block([lmxchar],lmxchar:"",tex1(A)),"\\right|\\left.",block([lmxchar],lmxchar:"",tex1(b)),"\\right",rr)
+)));
+
+/**
+ * Converts a matrix to an aug_matrix
+ * aug_matrix is an inert function that is used for displaying a matrix as an augmented matrix
+ * To convert back, use de_aug
+ *
+ * @param[matrix] M The matrix you would like to display as an augmented matrix
+ * @return[aug_matrix] An augmented matrix
+ */
+aug(M):= apply(aug_matrix,args(M));
+
+/**
+ * Converts an aug_matrix to a matrix
+ * aug_matrix is an inert function that is used for displaying a matrix as an augmented matrix
+ *
+ * @param[matrix] M The aug_matrix you would like to treat as a regular matrix
+ * @return[aug_matrix] A matrix
+ */
+de_aug(M):= apply(matrix,args(M));
+/*******************************************************************************/
+/* Predicate functions for vectors */
+/*******************************************************************************/
+
+/**
+ * A predicate to determine whether an expression has been converted to matrix form.
+ *
+ * @param[expression] ex An expression that may contain c or r
+ * @return[boolean] Does the expression contain c or r?
+ */
+vec_convertedp(ex):= block([ex_ops],
+ ex_ops: get_ops(ex),
+ if member(c,ex_ops) or member(r,ex_ops) then return(false) else return(true)
+);
+
+/**
+ * Predicate for determining whether a given object is an Mx1 matrix (a column vector)
+ * Note: does not consider c a column vector. Use vec_convert before col_vecp.
+ *
+ * @param[expression] ex An object that may be a matrix
+ * @return[boolean] Is the object an Mx1 matrix?
+ */
+col_vecp(ex):= block(
+ if not(matrixp(ex)) then return(false)
+ else return(is(second(matrix_size(ex))=1))
+);
+
+/**
+ * Predicate for determining whether a given object is a 1xN matrix (a row vector)
+ * Note: does not consider r a row vector. Use vec_convert before row_vecp.
+ *
+ * @param[expression] ex An object that may be a matrix
+ * @return[boolean] Is the object a 1xN matrix?
+ */
+row_vecp(ex):= block(
+ if not(matrixp(ex)) then return(false)
+ else return(is(first(matrix_size(ex))=1))
+);
+
+/**
+ * Predicate for determining whether a given object is a vector
+ * Note: does not consider c or r a vector. Use vec_convert before vectorp.
+ *
+ * @param[expression] ex An object that may be a matrix
+ * @return[boolean] Is the object a 1xN or Mx1 matrix?
+ */
+vectorp(ex):= col_vecp(ex) or row_vecp(ex);
+
+/* TODO write function to convert row/col vectors in matrix form to c or r form */
+/* Should be useful for creating teacher answers */
+
+/**
+ * Predicate to determine whether a given object is a unit vector.
+ * Can handle complex vectors
+ *
+ * @param[matrix] ex A vector (Mx1 or 1xN matrix)
+ * @return[boolean] Does this vector have a 2-norm of 1?
+ */
+unit_vecp(ex):= if vectorp(ex) then is(ev(ex.conjugate(ex),simp)=1) else false;
+
+/*********************************************************************************/
+/* Functions to extract parts of matrices */
+/*********************************************************************************/
+
+/**
+ * Take the upper triangular part of a matrix, leaving the remaining entries = 0
+ *
+ * @param[matrix] M An mxn matrix
+ * @return[matrix] The same matrix with all entries below the diagonal set to 0
+ */
+triu(M):= block([imax,jmax],
+ if not(matrixp(M)) then return(M),
+ [imax, jmax]: ev(matrix_size(M),simp),
+ genmatrix(lambda ([ii, jj], if ii>jj then 0 else M[ii,jj]), imax, jmax)
+);
+
+/**
+ * Take the lower triangular part of a matrix, leaving the remaining entries = 0
+ *
+ * @param[matrix] M An mxn matrix
+ * @return[matrix] The same matrix with all entries above the diagonal set to 0
+ */
+tril(M):= block([imax,jmax],
+ if not(matrixp(M)) then return(M),
+ [imax, jmax]: ev(matrix_size(M),simp),
+ genmatrix(lambda ([ii, jj], if iilength(ex)) then return(ex)
+ ),
+ if is(ex_op="matrix") then ex: transpose(apply(matrix,ex)),
+ return(ex)
+);
+
+/**
+ * Map significantfigures over a matrix
+ * Should this be core functionality? Surely when given a matrix the base sigfigsfun
+ or significantfigures function could do this by mapping itself over the arguments
+ and re-constructing the matrix.
+ * Explicitly only runs over a matrix, list, or number
+ *
+ * @param[matrix] ex A matrix of numbers (also accepts lists or numbers)
+ * @param[positive integer] n The number of significant figures desired.
+ */
+sf_map(ex,n):= block([rows],
+ if matrixp(ex) then block(
+ return(apply(matrix,map(lambda([ex2],significantfigures(ex2,n)),args(ex))))
+ ) else if listp(ex) or ev(numberp(ex),simp) then return(significantfigures(ex,n))
+ else return(ex)
+);
+
+/**
+ * Construct a diagonal matrix of size m by n with diagonal given as a list
+ * Similar to native function diag which builds a block diagonal matrix, but instead
+ is intended for numerical diagonals of rectangular matrices.
+ * Intended use case is to extend a reduced SVD into a full SVD
+ * If the whole diagonal does not fit in an mxn matrix, then it truncates d.
+ * If d is not long enough to fill an mxn matrix, remaining diagonal entries are set to 0.
+ *
+ * @param[list] d A list of numbers to go on the diagonal
+ * @param[positive integer] m The number of rows in the desired matrix
+ * @param[positive integer] n The number of columns in the desired matrix
+ * @return[matrix] A mxn matrix with d as the diagonal
+ */
+diagmatrix_like(d, m, n):= block([M,ii],
+ M: zeromatrix(m, n),
+ for ii: 1 thru ev(min(m, n, length(d)),simp) do block(
+ ii: ev(ii,simp),
+ M[ii,ii]: d[ii]
+ ),
+ return(M)
+);
+
+/**
+ * Returns the 2-norm of a matrix as a float
+ * i.e. returns the largest singular value as a float
+ * Note: I don't know if this has a good use case in STACK. I would happily remove this if this feels out of place, as I don't
+ anticipate using this in my course regularly.
+ *
+ * @param[matrix] M the matrix whose norm is desired
+ * @return[float] The 2-norm of M, or und if M is not a matrix.
+ */
+mat_norm2(M):= block([svs],
+ if matrixp(M) then block(
+ svs: ev(float(map(lambda([ex],sqrt(cabs(ex))),first(eigenvalues(transpose(M).M)))),simp),
+ return(ev(lmax(svs),simp))
+ ) else return(und)
+);
+
+/**
+ * Returns the condition number of a matrix based on the 2-norm as a float
+ * i.e. returns the ratio of the largest singular value to the smallest singular value as a float
+ * If M is singular, then und is returned instead.
+ * Note: I don't know if this has a good use case in STACK. I would happily remove this if this feels out of place, as I don't
+ anticipate using this in my course regularly.
+ *
+ * @param[matrix] M the matrix whose condition number is desired
+ * @return[float] The 2-condition number of M, or und if M is not an invertible matrix.
+ */
+mat_cond2(M):= block([svs,cond2],
+ cond2: und,
+ if invertiblep(M) then block(
+ svs: ev(float(map(lambda([ex],sqrt(cabs(ex))),first(eigenvalues(transpose(M).M)))),simp),
+ cond2: ev(lmax(svs)/lmin(svs),simp)
+ ),
+ return(cond2)
+);
+
+/**
+ * Solve the matrix equation Ax = b given matrix A and column vector (or list) b.
+ * Optionally will find a least squares solution
+ * Always returns a general solution if one exists, even in the least squares case
+ * If a single solution is required, use pseudoinverse(A) . b instead.
+ *
+ * @param[matrix] A An mxn matrix
+ * @param[matrix] b A mx1 matrix (or a list with m entries)
+ * @param[boolean] lstsq Optional: if given true then a least squares solution will be obtained. If false or omitted, only exact solutions obtained.
+ * @return[matrix] The general solution to Ax = b. If no solution exists and lstsq is not true, then matrix([]) is returned.
+ */
+mat_solve(A,b,[lstsq]):= block([m,n,vars,eqns,sol],
+ if emptyp(lstsq) then lstsq: false else lstsq:first(lstsq),
+ if listp(b) then b: transpose(b),
+ [m, n]: matrix_size(A),
+ if ev(is(first(matrix_size(b))#m),simp) then return(matrix([])),
+ vars: rest(stack_var_makelist(tmp,n)),
+ if lstsq then AT: transpose(A) else AT: ident(m),
+ eqns: list_matrix_entries(ev((AT . A) . transpose(vars) - (AT . b),simp)),
+ sol: map(rhs,linsolve(eqns,vars)),
+ if emptyp(sol) then return(matrix(sol)) else return(transpose(matrix(sol)))
+);
+
+/**
+ * Given a list of lists or a matrix, make a basis for R^m where m = length of each vector.
+ * If you don't want to expand to R^m, use remove_dep instead (it is called here too)
+ * Optionally will make this basis orthonormal, mostly useful when the existing basis is orthogonal but not R^m
+ * Arguably "basisify" is a poor name - suggestions welcome before published
+ *
+ * @param[matrix or list] M A matrix or list of lists
+ * @param[boolean] orth Optional: if true then returned basis will be orthonormal. Note that this may remove some basis vectors if they are not already orthogonal.
+ * @return[matrix or list] A basis for R^m where m is the length of each vector in M
+ */
+
+basisify(M,[orth]):= block([ex_op,m,n,vecs,new_vecs,ii],
+ if emptyp(orth) then orth: false else orth: first(orth),
+ ex_op: "matrix",
+ if listp(M) then block(M: column_stack(M), ex_op: "list"),
+ if not(lin_indp(M)) then M: remove_dep(M),
+ [m, n]: matrix_size(M),
+ vecs: args(transpose(M)),
+ new_vecs: args(ident(m)),
+ for ii: 1 thru m do block(
+ ii: ev(ii,simp),
+ if lin_indp(append(vecs,[new_vecs[ii]])) then vecs: append(vecs,[new_vecs[ii]])
+ ),
+ if orth then block(
+ vecs: ev(gramschmidt(apply(matrix,vecs)),simp),
+ vecs: ev(map(lambda([ex],ex/sqrt(ex.ex)),vecs),simp)
+ ),
+ if is(ex_op="matrix") then return(transpose(apply(matrix,vecs))) else return(vecs)
+);
+
+/**
+ * Maps the gcd (greatest common divisor) function across a list iteratively
+ *
+ * @param[list] ex A list of numbers
+ * @return[number] The greatest common divisor of all elements in ex
+ */
+lgcd(ex):= block([],
+ return(lreduce(lambda([ex1,ex2],gcd(ex1,ex2)),ex))
+);
+
+/**
+ * Given a vector (or list) return the shortest possible parallel vector with integer entries.
+ * Also multiplies by -1 if all entries are negative
+ * Very nice for eigenvector problems.
+ *
+ * @param[matrix or list] v a list or a Mx1 or 1xN matrix
+ * @return[matrix or list] v, but scaled by a constant such that all entries are the smallest possible integers
+ */
+scale_nicely(v):= block([v_op],
+ v_op: "list",
+ if vectorp(v) then (v_op: "matrix", v: list_matrix_entries(v)),
+ tmp: ev(lgcd(v),simp),
+ if ev(is(tmp#0),simp) then v: ev(v/tmp,simp),
+ if ev(every(lambda([ex],is(signum(ex)=-1)),v),simp) then v: ev(-v,simp),
+ if is(v_op="matrix") then return(transpose(v)) else return(v)
+);
+
+/* We have columnspace and nullspace functions already. The author keeps assuming that
+ rowspace must exist too, but it doesn't. The nullTspace function was added for
+ completeness' sake, and finds the nullspace of M^T. We could call it the cokernel
+ function, but since maxima uses nullspace rather than kernel this feels inappropriate. */
+/**
+ * Returns the row space of a matrix as a collection of column vectors.
+ * Output is the inert function span, the same as columnspace gives.
+ *
+ * @param[matrix] M a matrix
+ * @return[span] A span of column vectors
+ */
+rowspace(M):= ev(columnspace(transpose(M)),simp);
+
+/**
+ * Returns the cokernel of a matrix (the null space of its transpose) as a collection of column vectors.
+ * Output is the inert function span, the same as nullspace gives.
+ *
+ * @param[matrix] M a matrix
+ * @return[span] A span of column vectors
+ */
+nullTspace(M):= ev(nullspace(transpose(M)),simp);
+
+/* We have rowswap, rowop, columnswap, columnop, but no scaling versions.
+ I do acknowledge that you can recreate these with rowop, but this is
+ non-intuitive, so it's nice to have these functions lying around. */
+/**
+ * Scales row i of matrix A by alpha.
+ * A companion to rowop and rowswap.
+ * R_i <- alpha*R_i
+ *
+ * @param[matrix] M The matrix whose row you are scaling
+ * @param[integer] i The row you are scaling
+ * @param[number] alpha The amount you are scaling the row.
+ * @return[matrix] R_i <- alpha*R_i
+ */
+rowscale(M,i,alpha):= block([],
+ M[i]: map(lambda([ex],alpha*ex),M[i]),
+ return(M)
+);
+
+/**
+ * Scales column i of matrix A by alpha.
+ * A companion to columnop and columnswap.
+ * C_i <- alpha*C_i
+ *
+ * @param[matrix] M The matrix whose column you are scaling
+ * @param[integer] i The column you are scaling
+ * @param[number] alpha The amount you are scaling the column.
+ * @return[matrix] C_i <- alpha*C_i
+ */
+columnscale(M,i,alpha):= block([MT],
+ MT: transpose(M),
+ MT[i]: map(lambda([ex],alpha*ex),MT[i]),
+ return(transpose(MT))
+);
+
+/**
+ * Compute the algebraic multiplicity of an eigenvalue.
+ * Returns 0 if L is not an eigenvalue of M.
+ *
+ * @param[matrix] M a square matrix
+ * @param[number] L an eigenvalue of M
+ * @return[non-negative integer] the algebraic multiplicity of L in M. 0 if L is not an eigenvalue of M
+ */
+alg_mult(M,L):= block([evals,ii],
+ if squarep(M) then block(
+ evals: ev(eigenvalues(M),simp),
+ if not(member(L,first(evals))) then return(0),
+ ii:ev(first(sublist_indices(first(evals),lambda([ex],is(ex=L)))),simp),
+ return(second(evals)[ii])
+ )
+);
+
+/**
+ * Compute the geometric multiplicity of an eigenvalue.
+ * Returns 0 if L is not an eigenvalue of M.
+ *
+ * @param[matrix] M a square matrix
+ * @param[number] L an eigenvalue of M
+ * @return[non-negative integer] the geometric multiplicity of L in M. 0 if L is not an eigenvalue of M
+ */
+geo_mult(M,L):= block([evals,evects,ii],
+ if squarep(M) then block(
+ [evals, evects]: ev(eigenvectors(M),simp),
+ if not(member(L,first(evals))) then return(0),
+ ii:ev(first(sublist_indices(first(evals),lambda([ex],is(ex=L)))),simp),
+ return(length(evects[ii]))
+ )
+);
+
+/**
+ * Find the matrix that projects orthogonally onto the column space of M
+ * Computed by removing linearly dependent columns and then using M.(M^T.M)^^-1.M^T
+ *
+ * @param[matrix] M An mxn matrix
+ * @return[matrix] A symmetric, idempotent mxm matrix that projects mx1 vectors into the columnspace of M
+ */
+projection_matrix(M):= block([reduced_M],
+ if ev(zeromatrixp(M),simp) then return(0),
+ reduced_M: mat_unblocker(matrix(args(ev(columnspace(M),simp)))),
+ return(ev(reduced_M . invert(mat_unblocker(matrix([transpose(reduced_M) . reduced_M]))) . transpose(reduced_M),simp))
+);
+
+/*********************************************************************************/
+/* Processing vector parametric equations for lines, planes, etc. */
+/*********************************************************************************/
+
+/**
+ * Is a given algebraic expression a linear combination of column vectors with a linear offset?
+ * The use is somewhat limited; it would be nice to have support for other vector input types like lists or ntuples.
+ * e.g. c(1,2,3) + t*c(2,3,4) + s*c(1,0,1)
+ *
+ * @param[expression] ex A vector parametric equation using column vectors
+ * @return[boolean] Is the given expression in an expected form?
+ */
+linear_combinationp(ex):= block([vars,cm,dir_vecs,cons_vecs],
+ ex: ev(vec_convert(ex),expand,simp),
+ if not(matrixp(ex)) then return(false),
+ vars: listofvars(ex),
+ cm: coefmatrix(flatten(args(ex)), vars),
+ dir_vecs: maplist(lambda([ex2], col(cm,ex2)), ev(makelist(ii,ii,1,length(vars)),simp)),
+ cons_vec: ev(ex - apply("+", zip_with("*", vars, dir_vecs)),simp),
+ return(every(constantp,list_matrix_entries(cons_vec)))
+);
+
+/**
+ * Given a vector parametric equation, extract the linear offset, direction vectors, and parameters
+ * Does not explicitly check for non-linearity in its parameters.
+ * Any non-linear terms will appear in the linear offset term.
+ * Using hipow() to filter out non-linear terms only works for polynomial non-linearities, so it was rejected.
+ * Some notes:
+ * * every(constantp,list_matrix_entries(cons_vec)) can be used to detect said non-linearities. This is what linear_combinationp does.
+ * * If any direction vectors are entirely zero, check the cons_vec term for non-linearities.
+ *
+ * @param[expression] ex A vector parametric equation using column vectors
+ * @return[list] A list with three elements. The first is a column vector containing the linear offset, the second is a list containing each direction vector in order, the third is a list of parameters.
+ */
+vector_parametric_parts(ex):= block([vars,cm,dir_vecs,cons_vecs],
+ ex: ev(vec_convert(ex),expand,simp),
+ if not(matrixp(ex)) then return([]),
+ vars: listofvars(ex),
+ cm: coefmatrix(flatten(args(ex)), vars),
+ dir_vecs: maplist(lambda([ex2], col(cm,ex2)), ev(makelist(ii,ii,1,length(vars)),simp)),
+ cons_vec: ev(ex - apply("+", zip_with("*", vars, dir_vecs)),simp),
+ /*if not(every(constantp,list_matrix_entries(cons_vec))) then return([]),*/
+ return([cons_vec,dir_vecs,vars])
+);
+
+/**
+ * Given the output of vector_parametric_parts, produce a string of TeX output that formats the answer in the following "standard" form:
+ * r0 + t*d1 + s*d2 + ...
+ * where r0 is a vector of constants; t and s (etc) are parameters; d1 and d2 (etc) are direction vectors.
+ *
+ * @param[list] parts A list with three elements (see vector_parametric_parts) [mx1 matrix of constants, a list of mx1 matrices of constants, a list of variable names]
+ * @return[string] TeX output of a vector parametric equation in a "standard" form.
+ */
+vector_parametric_display(parts):= block([simp:false,cons_vec,dir_vecs,vars],
+ [cons_vec,dir_vecs,vars]: parts,
+ return(sconcat(tex1(cons_vec),"+",tex1(apply("+", zip_with("*", vars, dir_vecs)))))
+);
+
+/**
+ * Is a given point in a given affine subspace (e.g. a line, plane, etc)?
+ * Intended for use with vector_parametric_parts function
+ *
+ * @param[matrix] p The point of interest
+ * @param[list] parts The output of vector_parametric_parts. This is a three-element list of the form [constant_vector, [list of direction vectors], [list of parameters]]. All vectors should be mx1 matrices. The third element can be omitted if building the list manually.
+ * @return[boolean] Is p in the affine subspace?
+ */
+point_in_affine_spacep(p,parts):= block([simp:true,cons_vec,dir_vecs,vars,eqns,soln],
+ cons_vec: first(parts),
+ dir_vecs: second(parts),
+ if is(length(parts)>2) then vars: third(parts) else vars: rest(stack_var_makelist(tmp,length(dir_vecs))),
+ errcatch(
+ eqns: list_matrix_entries(cons_vec - p + apply("+", zip_with("*", vars, dir_vecs))),
+ soln: linsolve(eqns,vars)
+ ),
+ if listp(soln) then if is(length(soln)>0) then return(true) else return(false)
+);
+
+/**
+ * Is a given vector in a given subspace?
+ * If v is a zero vector, returns false by default as the intended use case is determining whether a given DIRECTION vector is in a subspace.
+ *
+ * @param[matrix] v The vector of interest
+ * @param[list] dir_vecs A list of mx1 matrices that span the subspace. Does not need to be a basis.
+ * @param[boolean] allow_zero Optional: If given true, then the zero vector will return true, otherwise zero vectors will return false
+ * @return[boolean] Is v in the subspace?
+ */
+vector_in_spacep(v,dir_vecs,[allow_zero]):= block([simp:true,is_dep:false],
+ if emptyp(allow_zero) then allow_zero: false else allow_zero: first(allow_zero),
+ if zeromatrixp(v) then return(allow_zero),
+ errcatch(
+ is_dep: is(rank(mat_unblocker(matrix(dir_vecs)))=rank(mat_unblocker(matrix(append(dir_vecs,[v])))))
+ ),
+ return(is_dep)
+);
+
+/*********************************************************************************/
+/* Matrix factorisations */
+/*********************************************************************************/
+
+/* Overall notes:
+ - These are in no way efficient functions, but seem to be fine for small
+ matrices with carefully deployed variants.
+ - I'm not convinced these add much to the package, but it felt wrong to not
+ include them in a linear algebra package.
+ - In most cases, teachers should begin with the factorisation, compute the
+ original matrix, and ask students to work backwards to your KNOWN answer.
+*/
+
+/**
+ * M = QR
+ * M must have full column rank
+ * Q has orthonormal columns that span the columnspace of M
+ * R is upper triangular
+ *
+ * @param[matrix] M a matrix with full column rank
+ */
+QR(M):= block([cols,Q,R],
+ if is(rank(M)#second(matrix_size(M))) then return([]),
+ cols: ev(gramschmidt(transpose(M)),simp),
+ cols: ev(map(lambda([ex],ex/sqrt(ex.ex)),cols),simp),
+ Q: transpose(apply(matrix,cols)),
+ R: ev(transpose(Q).M,simp),
+ return([Q,R])
+);
+
+/**
+ * M = P.J.P^^-1
+ * J is in Jordan normal form
+ * P is invertible and made up of generalised eigenvectors of M
+ * This really just calls existing functions in one go and avoids annoying errors.
+ *
+ * @param[matrix] M a square matrix
+ * @return[list] A list of two matrices: [P, J] such that J is in Jordan form and M = P . J . P^^-1. Returns empty list if M is not a square matrix
+ */
+get_Jordan_form(M):= block([jordan_info,J,P],
+ if not(squarep(M)) then return([]),
+ jordan_info: ev(jordan(M),simp),
+ J: ev(dispJordan(jordan_info),simp),
+ P: ev(ModeMatrix(M,jordan_info),simp),
+ return([P,J])
+);
+
+/**
+ * M = P.D.P^^-1
+ * M must be diagonalisable (i.e. all eigenvalues must have matching geometric and algebraic multiplicities)
+ * P is invertible and contains the eigenvectors of M
+ * D is diagonal and contains the eigenvalues of M
+ * If M is symmetric it will automatically orthogonally diagonalise
+ *
+ * @param[matrix] M a diagonalisable matrix
+ * @return[list] A list of two matrices: [P, D] such that D is diagonal and M = P . D . P^^-1. Returns empty list if M is not diagonalisable
+ */
+diagonalise(M):= block([P,D],
+ if not(squarep(M)) then return([]),
+ [P, D]: get_Jordan_form(M),
+ if symp(M) then P: ev(transpose(apply(matrix,map(lambda([ex],ex/sqrt(ex.ex)),args(transpose(P))))),simp),
+ if diagp(D) then return([P,D]) else return([])
+);
+
+/**
+ * Reduced SVD
+ * M = U.S.V^T with M as a rank r mxn matrix
+ * S is an rxr invertible diagonal matrix containing the sorted non-zero singular values of M
+ * V and U have orthonormal columns, with V nxr and U mxr
+ *
+ * @param[matrix] An mxn matrix
+ * @return[list] A list of 3 matrices [U,S,VT] such that U has orthonormal columns, VT has orthonormal rows, S is invertible diagonal, and M = U.S.VT
+ */
+SVD_red(M):= block([MTM,V,S2,components,n,S,U,ii],
+ if ev(zeromatrixp(M),simp) then return([matrix([]),matrix([]),matrix([])]),
+ MTM: ev(transpose(M).M,simp),
+ if atom(MTM) then MTM: matrix([MTM]),
+ [V, S2]: diagonalise(MTM),
+ /* TODO: does this work? */
+ V: first(QR(V)),
+ components: ev(makelist([S2[ii,ii],col(V,ii)],ii,1,second(matrix_size(MTM))),simp),
+ components: ev(reverse(sort(components)),simp),
+ components: ev(sublist(components,lambda([ex],is(first(ex)#0))),simp),
+ n: length(components),
+ S: zeromatrix(n,n),
+ S[1,1]: ev(sqrt(first(first(components))),simp),
+ V: second(first(components)),
+ U: ev(M.V/S[1,1],simp),
+ if atom(U) then U: matrix([U]),
+ if is(n>1) then block(
+ for ii: 2 thru n do block(
+ ii: ev(ii,simp),
+ S[ii,ii]: ev(sqrt(first(components[ii])),simp),
+ V: addcol(V,second(components[ii])),
+ U: addcol(U,ev(M.second(components[ii])/S[ii,ii],simp))
+ )
+ ),
+ return([U,S,transpose(V)])
+);
+
+/**
+ * M^+ = V.S^+.U^T
+ * Moore-penrose pseudoinverse.
+ * I'm convinced this routine exists somewhere in a package, because I've used it before in other maxima terminals, but I was unable to find it.
+ * Most commonly used to find minimal least squares solution to Ax = b using A^+ . b
+ *
+ * @param[matrix] M
+ * @return[matrix] The moore-penrose pseudoinverse of M
+ */
+pinv(M):= block([U,S,VT],
+ if ev(zeromatrixp(M),simp) then return(M),
+ [U, S, VT]: SVD_red(M),
+ return(ev(transpose(VT) . invert(S) . transpose(U),simp))
+);
+
+/**
+ * Full SVD
+ * M = U.S.V^T with M as a rank r mxn matrix
+ * S is an mxn diagonal matrix containing the sorted singular values of M
+ * V and U are orthogonal matrices, with V nxn and U mxm
+ *
+ * @param[matrix] An mxn matrix
+ * @return[list] A list of 3 matrices [U,S,VT] such that U is mxm orthogonal, VT is nxn orthogonal, S is mxn diagonal, and M = U.S.VT
+ */
+SVD(M):= block([U,S,VT],
+ [U, S, VT]: SVD_red(M),
+ if is(U=matrix([])) then U: ident(first(matrix_size(M))) else U: basisify(U,true),
+ if is(VT=matrix([])) then VT: ident(second(matrix_size(M))) else VT: transpose(basisify(transpose(VT),true)),
+ S: diagmatrix_like(diag_entries(S),first(matrix_size(M)),second(matrix_size(M))),
+ return([U,S,VT])
+);
+
+/*********************************************************************************/
+/* Automatically formats a system of linear equations */
+/*********************************************************************************/
+
+/**
+ * Format expressions so that they can be printed as coefficients by wrapping sums
+ * and expressions with unary minus into brackets.
+ */
+format_as_coeff(expr):= block([ops, repr],
+ ops: errcatch(op(expr)),
+ repr: if emptyp(ops) or not elementp(first(ops), {"+", "-"}) then expr else simplode(["\\left(", tex1(expr), "\\right)"]),
+ return(repr)
+);
+
+/**
+ * Given a list of equations and a list of variables, produce TeX output that displays them as a system of equations.
+ * Everything will be appropriately vertically aligned with leading ones and zeros removed appropriately.
+ *
+ * @param[list] eqns A list of linear equations. Constants should be on the right hand side.
+ * @param[list] vars A list of variables in the order that they should appear.
+ * @return[string] TeX output for this system of equations
+ */
+disp_eqns(eqns,vars):= block([is_neg,s_in,s_first,format_as_positive_coeff,one_zero_remover,delete_if_zero,m,n,p,pivot,new_pivot,ii,jj,v,a],
+ is_neg(ex) := ev(is(signum(ex)=-1),simp), /* return true if ex is numerical and negative, false otherwise */
+ s_in(ex):= if ev(is_neg(ex),simp) then "-" else "+", /* returns the sign of a coefficient as a string, assuming 0 is positive */
+ s_first(ex):= if ev(is_neg(ex),simp) then "-" else "", /* Altered version of above that doesn't return + for leading coefficient */
+ format_as_positive_coeff(ex) := if is_neg(ex) then ev(abs(ex),simp) else format_as_coeff(ev(ex,simp)),
+ one_zero_remover(ex):= if ev(is(ex=1) or is(ex=0) or is(ex=-1),simp) then "" else format_as_positive_coeff(ev(ex,simp)), /* scrubs out unwanted ones and zeros */
+ delete_if_zero(ex,var):= if is(ex=0) then "" else var, /* returns nothing if the coefficient is zero, otherwise returns the coefficient */
+ n: length(eqns), /* n = number of equations */
+ m: length(vars), /* m = number of variables */
+ p: ["\\begin{array}"], /* begin the LaTeX array that will house the system of equations */
+ p: append(p,[" {r",simplode(ev(makelist("cr",ii,1,m),simp)),"}"]), /* define the column alignments */
+ for ii: 1 thru n do block(
+ ii: ev(ii,simp),
+ pivot: false, /* each row will have a pivot, assume false until we find it */
+ new_pivot: false,
+ v: vars[1], /* v is the variable we are looking at in this column */
+ a: ev(coeff(lhs(eqns[ii]),v),simp), /* find coefficient of v */
+ if is(a#0) and not(pivot) then pivot: true, /* If the coefficient is non-zero, we have found our pivot! */
+ if pivot then p: append(p, [simplode([s_first(a),one_zero_remover(a),tex1(delete_if_zero(a,v))])]), /* If this is a pivot, display normally, otherwise do nothing */
+ for jj: 2 thru m do block(
+ jj: ev(jj,simp),
+ v: vars[jj],
+ a: ev(coeff(lhs(eqns[ii]),v),simp),
+ if is(a#0) and not(pivot) then new_pivot: true,
+ if is(a#0) then p: append(p,[simplode(["& ", if pivot then s_in(a) else ""," & ",if new_pivot then s_first(a) else "",one_zero_remover(a),tex1(delete_if_zero(a,v))])]) else p: append(p,["& & "]),
+ if new_pivot then [pivot, new_pivot]: [true, false]
+ ),
+ if is(fullratsimp(lhs(eqns[ii]))=0) then p: append(p, ["0"]), /* Display "0=0" properly */
+ p: append(p,[simplode(["& = &",tex1(rhs(eqns[ii]))])]),
+ if is(ii#n) then p: append(p,["\\\\"])
+ ),
+ p: append(p,["\\end{array}"]),
+ return(simplode(p))
+);
+
+/**
+ * Given a matrix, right-hand side vector, and a list of variables, produce TeX output that displays them as a system of equations.
+ * Everything will be appropriately vertically aligned with leading ones and zeros removed appropriately.
+ *
+ * @param[matrix] A The coefficient matrix.
+ * @param[matrix] b The right hand side vector.
+ * @param[list] vars A list of variables in the order that they should appear.
+ * @return[string] TeX output for this system of equations
+ */
+mat_disp_eqns(A,b,vars):= block([],
+ eqns: ev(map("=",list_matrix_entries(A . transpose(vars)),list_matrix_entries(b)),simp),
+ return(disp_eqns(eqns,vars))
+);
diff --git a/stack/maxima/contrib/linearalgebra_test.mac b/stack/maxima/contrib/linearalgebra_test.mac
new file mode 100644
index 00000000000..598ad2a5d7b
--- /dev/null
+++ b/stack/maxima/contrib/linearalgebra_test.mac
@@ -0,0 +1,408 @@
+/* Author Luke Longworth
+ University of Canterbury
+ Copyright (C) 2024 Luke Longworth
+
+ This program is free software: you can redistribute it or modify
+ it under the terms of the GNU General Public License version two.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see . */
+
+/****************************************************************/
+/* Linear algebra functions for STACK */
+/* */
+/* Test cases. */
+/* */
+/* V0.2.4 May 2024 */
+/* */
+/****************************************************************/
+
+s_test_case(vec_convert(c(1,2,3)),matrix([1],[2],[3]));
+s_test_case(vec_convert(r(1,2,3)),matrix([1,2,3]));
+s_test_case(vec_convert(c(1,2,3) + matrix([1],[1],[1])),matrix([1],[2],[3])+matrix([1],[1],[1]));
+s_test_case(vec_convert(c(1,2) + r(3,4)),matrix([1],[2])+matrix([3,4]) );
+s_test_case(ev(vec_convert(c(1,2) + r(3,4)),simp),c(1,2) + r(3,4) );
+
+s_test_case(un_vec_convert(matrix([1],[2],[3])),c(1,2,3));
+s_test_case(un_vec_convert(matrix([1,2,3])),r(1,2,3));
+s_test_case(un_vec_convert(matrix([1])),c(1));
+
+s_test_case(aug(matrix([1,2,3,1],[4,5,6,1],[7,8,9,1])),aug_matrix([1,2,3,1],[4,5,6,1],[7,8,9,1]));
+s_test_case(de_aug(aug_matrix([1,2,3,1],[4,5,6,1],[7,8,9,1])),matrix([1,2,3,1],[4,5,6,1],[7,8,9,1]));
+
+s_test_case(vec_convertedp(c(1,2)),false);
+s_test_case(vec_convertedp(r(1,2)),false);
+s_test_case(vec_convertedp(vec_convert(c(1,2))),true);
+s_test_case(vec_convertedp(ev(vec_convert(c(1,2)+r(3,4)),simp)),false);
+
+s_test_case(col_vecp(matrix([1],[2])),true);
+s_test_case(col_vecp(matrix([1,2])),false);
+s_test_case(row_vecp(matrix([1],[2])),false);
+s_test_case(row_vecp(matrix([1,2])),true);
+s_test_case(col_vecp(c(1,2)),false);
+s_test_case(row_vecp(r(1,2)),false);
+
+s_test_case(vectorp(matrix([1],[2])),true);
+s_test_case(vectorp(matrix([1,2])),true);
+s_test_case(vectorp(c(1,2)),false);
+
+s_test_case(unit_vecp(matrix([1],[0])),true);
+s_test_case(unit_vecp(matrix([1/sqrt(2),1/sqrt(2)])),true);
+s_test_case(unit_vecp(matrix([1],[1])),false);
+s_test_case(unit_vecp(c(1,0)),false);
+
+s_test_case(triu(matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,2,3],[0,5,6],[0,0,9]));
+s_test_case(triu(matrix([1,2,3],[4,5,6],[7,8,9],[10,11,12])),matrix([1,2,3],[0,5,6],[0,0,9],[0,0,0]));
+s_test_case(triu(matrix([1,2,3,4],[4,5,6,7],[7,8,9,10])),matrix([1,2,3,4],[0,5,6,7],[0,0,9,10]));
+s_test_case(triu(matrix([1,2],[3,2+2])),matrix([1,2],[0,2+2]));
+s_test_case(triu(1),1);
+
+s_test_case(tril(matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,0,0],[4,5,0],[7,8,9]));
+s_test_case(tril(matrix([1,2,3],[4,5,6],[7,8,9],[10,11,12])),matrix([1,0,0],[4,5,0],[7,8,9],[10,11,12]));
+s_test_case(tril(matrix([1,2,3,4],[4,5,6,7],[7,8,9,10])),matrix([1,0,0,0],[4,5,0,0],[7,8,9,0]));
+s_test_case(tril(matrix([1,2],[3,2+2])),matrix([1,0],[3,2+2]));
+s_test_case(tril(1),1);
+
+s_test_case(diagonal(matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,0,0],[0,5,0],[0,0,9]));
+s_test_case(diagonal(matrix([1,2,3],[4,5,6],[7,8,9],[10,11,12])),matrix([1,0,0],[0,5,0],[0,0,9],[0,0,0]));
+s_test_case(diagonal(matrix([1,2,3,4],[4,5,6,7],[7,8,9,10])),matrix([1,0,0,0],[0,5,0,0],[0,0,9,0]));
+s_test_case(diagonal(matrix([1,2],[3,2+2])),matrix([1,0],[0,2+2]));
+s_test_case(diagonal(1),1);
+
+s_test_case(diag_entries(ident(3)),[1,1,1]);
+s_test_case(diag_entries(matrix([1,0,0],[0,2,0],[0,0,3],[0,0,0])),[1,2,3]);
+s_test_case(diag_entries(matrix([3,0,0,0],[0,2,0,0],[0,0,1,0])),[3,2,1]);
+s_test_case(diag_entries(matrix([1+2,0,0,0],[0,2,0,0],[0,0,1,0])),[1+2,2,1]);
+s_test_case(diag_entries(1),[1]);
+
+s_test_case(rowscale(matrix([1,2,3],[4,5,6],[7,8,9]),2,100),matrix([1,2,3],[100*4,100*5,100*6],[7,8,9]));
+s_test_case(columnscale(matrix([1,2,3],[4,5,6],[7,8,9]),2,100),matrix([1,100*2,3],[4,100*5,6],[7,100*8,9]));
+
+s_test_case(triup(ident(5)),true);
+s_test_case(trilp(ident(5)),true);
+s_test_case(diagp(ident(5)),true);
+s_test_case(triup(zeromatrix(5,4)),true);
+s_test_case(trilp(zeromatrix(5,4)),true);
+s_test_case(diagp(zeromatrix(5,4)),true);
+
+s_test_case(triup(matrix([1,2,3],[4,5,6],[7,8,9])),false);
+s_test_case(triup(matrix([1,2,3],[0,5,6],[0,0,9])),true);
+s_test_case(triup(matrix([1,2,3],[4,5,6],[7,8,9],[10,11,12])),false);
+s_test_case(triup(matrix([1,2,3],[0,5,6],[0,0,9],[0,0,0])),true);
+s_test_case(triup(matrix([1,2,3,4],[4,5,6,7],[7,8,9,10])),false);
+s_test_case(triup(matrix([1,2,3,4],[0,5,6,7],[0,0,9,10])),true);
+s_test_case(triup(matrix([1,2,3,4,5,6,7,8,9])),true);
+
+s_test_case(trilp(matrix([1,2,3],[4,5,6],[7,8,9])),false);
+s_test_case(trilp(matrix([1,0,0],[4,5,0],[7,8,9])),true);
+s_test_case(trilp(matrix([1,2,3],[4,5,6],[7,8,9],[10,11,12])),false);
+s_test_case(trilp(matrix([1,0,0],[4,5,0],[7,8,9],[10,11,12])),true);
+s_test_case(trilp(matrix([1,2,3,4],[4,5,6,7],[7,8,9,10])),false);
+s_test_case(trilp(matrix([1,0,0,0],[4,5,0,0],[7,8,9,0])),true);
+s_test_case(trilp(transpose([1,2,3,4,5,6,7,8,9])),true);
+
+s_test_case(diagp(matrix([1,0],[1-1,1])),false);
+s_test_case(diagp(matrix([1,1],[0,1])),false);
+s_test_case(diagp(matrix([1,0],[1,1])),false);
+s_test_case(diagp(matrix([1])),true);
+s_test_case(diagp(matrix([1,0,0,0],[0,2,0,0],[0,0,3,0])),true);
+s_test_case(diagp(matrix([1,0,0,0],[0,2,0,0],[0,0,3,4])),false);
+s_test_case(diagp(matrix([1,0,0],[0,2,0],[0,0,3],[0,0,0])),true);
+s_test_case(diagp(matrix([1,0,0],[0,2,0],[0,0,3],[0,0,4])),false);
+
+s_test_case(REFp(ident(4)),true);
+s_test_case(REFp(ev(2*ident(4),simp)),true);
+s_test_case(REFp(ev(2*ident(4),simp),true),false);
+s_test_case(REFp(matrix([2,1,1],[0,0,3],[0,0,0],[0,0,0])),true);
+s_test_case(REFp(matrix([2,1,1],[0,0,3],[0,0,0],[0,0,0]),true),false);
+s_test_case(REFp(matrix([2,1,1],[0,0,3],[0,0,0],[0,0,0]),false),true);
+s_test_case(REFp(matrix([2,1,1],[0,0,0],[0,0,3],[0,0,0])),false);
+s_test_case(REFp(matrix([1,1,1,1,1,1],[0,1,1,1,1,1],[0,0,0,1,1,1],[0,0,0,0,0,1])),true);
+s_test_case(REFp(matrix([1,1,1,1,1,1],[0,1,1,1,1,1],[0,0,0,1,1,1],[0,0,0,0,0,1]),true),true);
+s_test_case(REFp(matrix([1,1,1,1,1,1],[0,1,1,1,1,1],[0,0,1,0,1,1],[0,0,0,0,0,1])),true);
+s_test_case(REFp(matrix([1,2,3],[0,5,6])),true);
+s_test_case(REFp(matrix([1,2,3],[4,5,6])),false);
+s_test_case(REFp(matrix([1,2,3],[0,5,6],[0,8,9])),false);
+
+s_test_case(squarep(ident(4)),true);
+s_test_case(squarep(matrix([1],[2])),false);
+s_test_case(squarep(matrix([1,2],[2,3])),true);
+s_test_case(squarep(1),false);
+
+s_test_case(diagonalisablep(ident(2)),true);
+s_test_case(diagonalisablep(matrix([1,1],[0,1])),false);
+s_test_case(diagonalisablep(1),false);
+s_test_case(diagonalisablep(matrix([1,1],[1,1])),true);
+
+s_test_case(symp(ident(3)),true);
+s_test_case(symp(matrix([1,1],[0,1])),false);
+s_test_case(symp(1),false);
+
+s_test_case(invertiblep(ident(2)),true);
+s_test_case(invertiblep(matrix([1,1],[0,1])),true);
+s_test_case(invertiblep(1),false);
+s_test_case(invertiblep(matrix([1,1],[1,1])),false);
+
+s_test_case(orthogonal_columnsp(matrix([1,1],[1,-1],[1,0])),true);
+s_test_case(orthogonal_columnsp(matrix([1/sqrt(3),1/sqrt(2)],[1/sqrt(3),-1/sqrt(2)],[1/sqrt(3),0])),true);
+s_test_case(orthogonal_columnsp(matrix([1,1],[1,2],[1,0])),false);
+s_test_case(orthogonal_columnsp(matrix([1,1],[1,-1])),true);
+s_test_case(orthogonal_columnsp(matrix([1,1],[1,-1])/sqrt(2)),true);
+s_test_case(orthogonal_columnsp(1),false);
+
+s_test_case(orthonormal_columnsp(matrix([1,1],[1,-1],[1,0])),false);
+s_test_case(orthonormal_columnsp(matrix([1/sqrt(3),1/sqrt(2)],[1/sqrt(3),-1/sqrt(2)],[1/sqrt(3),0])),true);
+s_test_case(orthonormal_columnsp(matrix([1,1],[1,-1])),false);
+s_test_case(orthonormal_columnsp(ev(matrix([1,1],[1,-1])/sqrt(2),simp)),true);
+s_test_case(orthonormal_columnsp(1),false);
+
+s_test_case(orth_matrixp(matrix([1,1],[1,-1],[1,0])),false);
+s_test_case(orth_matrixp(matrix([1/sqrt(3),1/sqrt(2)],[1/sqrt(3),-1/sqrt(2)],[1/sqrt(3),0])),false);
+s_test_case(orth_matrixp(matrix([1,1],[1,-1])),false);
+s_test_case(orth_matrixp(ev(matrix([1,1],[1,-1])/sqrt(2),simp)),true);
+s_test_case(orth_matrixp(1),false);
+
+s_test_case(make_list_of_lists(1),1);
+s_test_case(make_list_of_lists(matrix([1,3,5])),[[1,3,5]]);
+s_test_case(make_list_of_lists(matrix([1,2],[3,4],[5,6])),[[1,3,5],[2,4,6]]);
+s_test_case(make_list_of_lists({c(1,2,3),[2,3,4],ntuple(3,4,5),{4,5,6}}),[[1,2,3],[2,3,4],[3,4,5],[4,5,6]]);
+s_test_case(make_list_of_lists([1,2,3,4]),[[1,2,3,4]]);
+s_test_case(make_list_of_lists([1,a,b,4]),[[1,a,b,4]]);
+s_test_case(make_list_of_lists([[1,2,3],[2,3,4],[3,4,5]]),[[1,2,3],[2,3,4],[3,4,5]]);
+s_test_case(make_list_of_lists([matrix([1],[2]),matrix([3],[4])]),[[1,2],[3,4]]);
+
+s_test_case(column_stack([[1,2,3],[4,5,6]]),matrix([1,4],[2,5],[3,6]));
+s_test_case(column_stack([[1,2,3]]),matrix([1],[2],[3]));
+s_test_case(column_stack([1,2,3]),[]);
+
+s_test_case(lin_indp(matrix([1,2],[4,5],[7,8])),true);
+s_test_case(lin_indp(matrix([1,2,3],[4,5,6],[7,8,9])),false);
+s_test_case(lin_indp(matrix([1,2,3],[4,5,6])),false);
+s_test_case(lin_indp([[1,2],[4,5],[7,8]]),false);
+s_test_case(lin_indp([[1,4,7],[2,5,8]]),true);
+s_test_case(lin_indp(make_list_of_lists({[1,2],[4,5],[7,8]})),false);
+s_test_case(lin_indp(make_list_of_lists({[1,4,7],[2,5,8]})),true);
+s_test_case(lin_indp(make_list_of_lists(ntuple([1,2],[4,5],[7,8]))),false);
+s_test_case(lin_indp(make_list_of_lists(ntuple([1,4,7],[2,5,8]))),true);
+s_test_case(lin_indp(make_list_of_lists(span([1,2],[4,5],[7,8]))),false);
+s_test_case(lin_indp(make_list_of_lists(span([1,4,7],[2,5,8]))),true);
+s_test_case(lin_indp(make_list_of_lists([transpose([1,4,7]),[2,5,8]])),true);
+s_test_case(lin_indp(make_list_of_lists({transpose([1,4,7]),matrix([2,5,8])})),true);
+
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6],[7,8,9]),matrix([1,0,-1],[0,1,2],[0,0,0])),true);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6],[7,8,9]),matrix([1,0,-1],[0,1,2])),false);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6],[7,8,9]),matrix([1,2,3],[0,-3,-6],[0,-6,-12])),true);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6],[7,8,9]),ident(3)),false);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6],[7,8,10]),ident(3)),true);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6],[7,8,9]),matrix([1,3,2],[4,6,5],[7,9,8])),false);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6]),matrix([1,0,-1],[0,1,2])),true);
+s_test_case(row_equivp(matrix([1,2],[2,3],[1,1]),matrix([1,0],[0,1],[0,0])),true);
+s_test_case(row_equivp(matrix([1,2,3],[4,5,6]),matrix([1,0,0],[0,1,0])),false);
+s_test_case(row_equivp(matrix([1,2],[2,3],[1,1]),matrix([1,0],[0,0],[0,0])),false);
+
+s_test_case(col_equivp(matrix([1,2,3],[4,5,6],[7,8,9]),ident(3)),false);
+s_test_case(col_equivp(matrix([1,2,3],[4,5,6],[7,8,10]),ident(3)),true);
+s_test_case(col_equivp(matrix([1,3,5],[1,1,0],[1,1,2],[1,3,3]),matrix([1/2,1/2,1/2],[1/2,-1/2,-1/2],[1/2,-1/2,1/2],[1/2,1/2,-1/2])),true);
+
+s_test_case(subspace_equivp([[1,2],[2,3]],[[1,0],[0,1]]),true);
+s_test_case(subspace_equivp([[1,2],[2,4]],[[1,0],[0,1]]),false);
+s_test_case(subspace_equivp([[1,2],[2,3],[3,4]],[[1,0],[0,1]]),true);
+s_test_case(subspace_equivp([[1,2],[2,3]],[[1,0]]),false);
+
+s_test_case(remove_dep(matrix([0,0])),[]);
+s_test_case(remove_dep([[1,0],[0,1],[1,1]]),[[1,0],[0,1]]);
+s_test_case(remove_dep([[1,0],[2,0],[1,1]]),[[1,0],[1,1]]);
+s_test_case(remove_dep(matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,2],[4,5],[7,8]));
+
+s_test_case(sf_map(1/3,2),0.33);
+s_test_case(sf_map(1/3,3),0.333);
+s_test_case(sf_map(12345,2),12000);
+s_test_case(sf_map(12345,3),12300);
+s_test_case(sf_map(1.5,1),2);
+s_test_case(sf_map(2.5,1),3);
+
+s_test_case(sf_map([1/3,12345],2),[0.33,12000]);
+s_test_case(sf_map(matrix([1/3,12345]),2),matrix([0.33,12000]));
+s_test_case(sf_map(matrix([1/3],[12345]),2),matrix([0.33],[12000]));
+s_test_case(sf_map(matrix([1/3,12345],[1/4,5/4]),2),matrix([0.33,12000],[0.25,1.3]));
+s_test_case(sf_map({1/3,1/4},1),{1/3,1/4});
+
+s_test_case(diagmatrix_like([1,1,1],3,3),ident(3));
+s_test_case(diagmatrix_like([1,2,3],3,4),matrix([1,0,0,0],[0,2,0,0],[0,0,3,0]));
+s_test_case(diagmatrix_like([1,2,3],4,3),matrix([1,0,0],[0,2,0],[0,0,3],[0,0,0]));
+s_test_case(diagmatrix_like([1,2,3],4,4),matrix([1,0,0,0],[0,2,0,0],[0,0,3,0],[0,0,0,0]));
+s_test_case(diagmatrix_like([1,2,3],2,3),matrix([1,0,0],[0,2,0]));
+s_test_case(diagmatrix_like([1,2,3],3,2),matrix([1,0],[0,2],[0,0]));
+
+s_test_case(mat_norm2(ident(2)),1.0);
+s_test_case(mat_norm2(matrix([sqrt(3),2],[0,sqrt(3)])),3.0);
+s_test_case(mat_norm2(matrix([1,2],[2,-2])),3.0);
+s_test_case(mat_norm2(matrix([2,2],[1,0],[0,1])),3.0);
+s_test_case(mat_norm2(matrix([1,1],[1,1])),2.0);
+s_test_case(mat_norm2(1),und);
+
+s_test_case(mat_cond2(ident(2)),1.0);
+s_test_case(mat_cond2(matrix([sqrt(3),2],[0,sqrt(3)])),3.0);
+s_test_case(mat_cond2(matrix([1,2],[2,-2])),1.5);
+s_test_case(mat_cond2(1),und);
+s_test_case(mat_cond2(matrix([1,1],[1,0],[0,1])),und);
+s_test_case(mat_cond2(matrix([1,2],[1,2])),und);
+
+s_test_case(mat_solve(matrix([1,2],[3,4]),[3,7]),matrix([1],[1]));
+s_test_case(mat_solve(matrix([1,-1],[1,-1]),[0,0]),matrix([%r1],[%r1]));
+s_test_case(mat_solve(matrix([1,-1],[1,-1]),[1,0]),matrix([]));
+s_test_case(mat_solve(matrix([1,-1],[1,-1]),[1,0],true),matrix([(2*%r2+1)/2],[%r2]));
+s_test_case(mat_solve(matrix([0,0],[1,1]),[1,0],true),matrix([-%r3],[%r3]));
+
+s_test_case(basisify(matrix([1,2],[0,0],[0,0])),ident(3));
+s_test_case(basisify(matrix([1,2],[1,2],[0,0])),matrix([1,1,0],[1,0,0],[0,0,1]));
+s_test_case(basisify([[1,1,0],[2,2,0]],true),[[1/sqrt(2),1/sqrt(2),0],[1/sqrt(2),-(1/sqrt(2)),0],[0,0,1]]);
+
+s_test_case(lgcd([9,12,27]),3);
+s_test_case(lgcd([-9,-12,-27]),3);
+s_test_case(lgcd([1/2,1/4,5/6]),1/12);
+
+s_test_case(scale_nicely([9,12,27]),[3,4,9]);
+s_test_case(scale_nicely(matrix([-9],[-12],[-27])),matrix([3],[4],[9]));
+s_test_case(scale_nicely([1/2,1/4,-5/6]),[6,3,-10]);
+s_test_case(scale_nicely([0,0,0]),[0,0,0]);
+
+s_test_case(rowspace(ident(2)),span(matrix([1],[0]),matrix([0],[1])));
+s_test_case(rowspace(matrix([1,0],[0,1],[1,1])),span(matrix([1],[0]),matrix([0],[1])));
+s_test_case(nullTspace(matrix([1,0],[0,1],[1,1])),span(matrix([-1],[-1],[1])));
+
+s_test_case(setrowmx(1,2,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,2,3],[1,1,1],[7,8,9]));
+s_test_case(setrowmx([%e,%pi,%i],2,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,2,3],[%e,%pi,%i],[7,8,9]));
+s_test_case(setrowmx(transpose([%e,%pi,%i]),2,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,2,3],[%e,%pi,%i],[7,8,9]));
+
+s_test_case(setcolmx(1,2,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,1,3],[4,1,6],[7,1,9]));
+s_test_case(setcolmx([%e,%pi,%i],2,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,%e,3],[4,%pi,6],[7,%i,9]));
+s_test_case(setcolmx(transpose([%e,%pi,%i]),2,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,%e,3],[4,%pi,6],[7,%i,9]));
+
+s_test_case(setdiagmx(1,matrix([1,2,3],[4,5,6],[7,8,9])),matrix([1,2,3],[4,1,6],[7,8,1]));
+s_test_case(setdiagmx(1,matrix([1,2,3],[4,5,6],[7,8,9]),1),matrix([1,1,3],[4,5,1],[7,8,9]));
+s_test_case(setdiagmx(1,matrix([1,2,3],[4,5,6],[7,8,9]),-2),matrix([1,2,3],[4,5,6],[1,8,9]));
+s_test_case(setdiagmx([10,20,30,40,50],matrix([1,2,3,4],[4,5,6,7],[7,8,9,10]),2),matrix([1,2,10,4],[4,5,6,20],[7,8,9,10]));
+
+s_test_case(Rayleigh(matrix([1,1],[1,1]),matrix([1],[1])),2);
+s_test_case(Rayleigh(matrix([1,1],[0,1]),matrix([1],[1])),3/2);
+s_test_case(Rayleigh(matrix([0,-1],[1,0]),matrix([%i],[2])),(4*%i)/5);
+
+s_test_case(alg_mult(matrix([1,1,0],[0,1,0],[0,0,1]),1),3);
+s_test_case(geo_mult(matrix([1,1,0],[0,1,0],[0,0,1]),1),2);
+s_test_case(alg_mult(matrix([1,1,0],[0,1,0],[0,0,2]),2),1);
+s_test_case(geo_mult(matrix([1,1,0],[0,1,0],[0,0,2]),2),1);
+s_test_case(alg_mult(matrix([2,1,0],[0,2,0],[0,0,1]),1),1);
+s_test_case(geo_mult(matrix([2,1,0],[0,2,0],[0,0,1]),1),1);
+
+s_test_case(projection_matrix(matrix([1,2,3],[4,5,6],[7,8,10])),ident(3));
+s_test_case(projection_matrix(matrix([1,2,3],[4,5,6],[7,8,9])),matrix([5/6,1/3,-(1/6)],[1/3,1/3,1/3],[-(1/6),1/3,5/6]));
+
+s_test_case(eigenvectorp(c(1,0),ident(2)),true);
+s_test_case(eigenvectorp(matrix([0],[1]),matrix([1,1],[0,1])),false);
+s_test_case(eigenvectorp(c(1,0),ident(2),1),true);
+s_test_case(eigenvectorp(c(1,0),ident(2),2),false);
+s_test_case(eigenvectorp(c(2,0),ident(2),1),true);
+s_test_case(eigenvectorp(matrix([%i],[1]),matrix([0,-1],[1,0])),true);
+s_test_case(eigenvectorp(matrix([%i],[1]),matrix([0,-1],[1,0]),%i),true);
+s_test_case(eigenvectorp(matrix([%i],[1]),matrix([0,-1],[1,0]),-%i),false);
+
+s_test_case(eigenvaluep(1,matrix([1,0],[0,2])),true);
+s_test_case(eigenvaluep(2,matrix([1,0],[0,2])),true);
+s_test_case(eigenvaluep(3,matrix([1,0],[0,2])),false);
+s_test_case(eigenvaluep(1,matrix([1,0],[0,2]),c(1,0)),true);
+s_test_case(eigenvaluep(1,matrix([1,0],[0,2]),c(2,0)),true);
+s_test_case(eigenvaluep(1,matrix([1,0],[0,2]),c(0,1)),false);
+s_test_case(eigenvaluep(%i,matrix([0,-1],[1,0])),true);
+s_test_case(eigenvaluep(%i,matrix([0,-1],[1,0]),matrix([%i],[1])),true);
+s_test_case(eigenvaluep(%i,matrix([0,-1],[1,0]),matrix([1],[%i])),false);
+
+s_test_case(get_eigenvalue(matrix([0],[0]),ident(2)),false);
+s_test_case(get_eigenvalue(matrix([0],[1]),matrix([1,1],[0,1])),false);
+s_test_case(get_eigenvalue(matrix([1],[0]),matrix([2,1],[0,1])),2);
+s_test_case(get_eigenvalue(matrix([%i],[1]),matrix([0,-1],[1,0])),%i);
+
+s_test_case(get_eigenvector(2,ident(2)),[]);
+s_test_case(get_eigenvector(2,matrix([2,1],[0,1])),[matrix([1],[0])]);
+s_test_case(get_eigenvector(1,ident(2)),[matrix([1],[0]),matrix([0],[1])]);
+s_test_case(get_eigenvector(2*%i,matrix([0,-2],[2,0])),[matrix([1],[-%i])]);
+s_test_case(get_eigenvector(1,matrix([3/2,-1,1/2],[0,1,0],[1/2,-1,3/2])),[matrix([1],[0],[-1]),matrix([0],[1],[2])]);
+s_test_case(get_eigenvector(1,matrix([3/2,-1,1/2],[0,1,0],[1/2,-1,3/2]),true),[matrix([1/sqrt(2)],[0],[-(1/sqrt(2))]),matrix([1/sqrt(3)],[1/sqrt(3)],[1/sqrt(3)])]);
+
+s_test_case(linear_combinationp(matrix([1],[2],[3]) + t*matrix([1],[1],[1]) + s*matrix([1],[0],[1])),true);
+s_test_case(linear_combinationp(matrix([1+t+s],[2+t],[3+t+s])),true);
+s_test_case(linear_combinationp(c(1,2,3) + t*c(1,1,1) + s*c(1,0,1)),true);
+s_test_case(linear_combinationp(matrix([1],[2],[3]) + t*matrix([1],[1],[1]) + s^2*matrix([1],[0],[1])),false);
+
+s_test_case(vector_parametric_parts(matrix([1],[2],[3]) + t*matrix([1],[1],[1]) + s*matrix([1],[0],[1])),[matrix([1],[2],[3]),[matrix([1],[0],[1]),matrix([1],[1],[1])],[s,t]]);
+s_test_case(vector_parametric_parts(matrix([1+t+s],[2+t],[3+t+s])),[matrix([1],[2],[3]),[matrix([1],[0],[1]),matrix([1],[1],[1])],[s,t]]);
+s_test_case(vector_parametric_parts(c(1,2,3) + t*c(1,1,1) + s*c(1,0,1)),[matrix([1],[2],[3]),[matrix([1],[0],[1]),matrix([1],[1],[1])],[s,t]]);
+s_test_case(vector_parametric_parts(matrix([1],[2],[3]) + t*matrix([1],[1],[1]) + s^2*matrix([1],[0],[1])),[matrix([1+s^2],[2],[3+s^2]),[matrix([0],[0],[0]),matrix([1],[1],[1])],[s,t]]);
+
+s_test_case(vector_parametric_display([matrix([1],[2],[3]),[matrix([1],[0],[1]),matrix([1],[1],[1])],[s,t]]),"\\left[\\begin{array}{c} 1 \\\\ 2 \\\\ 3 \\end{array}\\right]+s\\cdot \\left[\\begin{array}{c} 1 \\\\ 0 \\\\ 1 \\end{array}\\right]+t\\cdot \\left[\\begin{array}{c} 1 \\\\ 1 \\\\ 1 \\end{array}\\right]");
+s_test_case(vector_parametric_display(vector_parametric_parts(matrix([1+t+s],[2+t],[3+t+s]))),"\\left[\\begin{array}{c} 1 \\\\ 2 \\\\ 3 \\end{array}\\right]+s\\cdot \\left[\\begin{array}{c} 1 \\\\ 0 \\\\ 1 \\end{array}\\right]+t\\cdot \\left[\\begin{array}{c} 1 \\\\ 1 \\\\ 1 \\end{array}\\right]");
+
+s_test_case(point_in_affine_spacep(matrix([1],[1]),[matrix([0],[1]),[matrix([1],[-1])]]),false);
+s_test_case(point_in_affine_spacep(matrix([1],[1]),[matrix([0],[1]),[matrix([1],[-1])],[t]]),false);
+s_test_case(point_in_affine_spacep(matrix([1/2],[1/2]),[matrix([0],[1]),[matrix([1],[-1])]]),true);
+s_test_case(point_in_affine_spacep(matrix([1/2],[1/2]),[matrix([0],[1]),[matrix([1],[-1])],[t]]),true);
+s_test_case(point_in_affine_spacep(matrix([1],[102],[3]),[matrix([0],[100],[0]),[matrix([4],[5],[6]),matrix([7],[8],[9])]]),true);
+
+s_test_case(vector_in_spacep(matrix([1],[2],[3]),[matrix([4],[5],[6]),matrix([7],[8],[9])]),true);
+s_test_case(vector_in_spacep(matrix([1],[2],[3]),[matrix([4],[5],[6]),matrix([7],[8],[10])]),false);
+s_test_case(vector_in_spacep(matrix([1],[2]),[matrix([4],[5],[6]),matrix([7],[8],[9])]),false);
+s_test_case(vector_in_spacep(matrix([0],[0],[0]),[matrix([4],[5],[6]),matrix([7],[8],[9])]),false);
+s_test_case(vector_in_spacep(matrix([0],[0],[0]),[matrix([4],[5],[6]),matrix([7],[8],[9])],true),true);
+
+s_test_case(QR(matrix([1,3,5],[1,1,0],[1,1,2],[1,3,3])),[matrix([1/2,1/2,1/2],[1/2,-(1/2),-(1/2)],[1/2,-(1/2),1/2],[1/2,1/2,-(1/2)]),matrix([2,4,5],[0,2,3],[0,0,2])]);
+s_test_case(QR(matrix([1,1],[2,2])),[]);
+
+s_test_case(get_Jordan_form(1),[]);
+s_test_case(get_Jordan_form(matrix([1,2])),[]);
+s_test_case(get_Jordan_form(matrix([1,1],[0,1])),[ident(2),matrix([1,1],[0,1])]);
+s_test_case(get_Jordan_form(matrix([1,2],[2,3])),[matrix([1,1],[-((sqrt(5)-1)/2),(sqrt(5)+1)/2]),matrix([2-sqrt(5),0],[0,sqrt(5)+2])]);
+s_test_case(get_Jordan_form(matrix([8,-3],[12,-4])),[matrix([6,1],[12,0]),matrix([2,1],[0,2])]);
+
+s_test_case(diagonalise(1),[]);
+s_test_case(diagonalise(matrix([1,2])),[]);
+s_test_case(diagonalise(matrix([8,-3],[12,-4])),[]);
+s_test_case(diagonalise(matrix([1,2],[3,4])),[matrix([1,1],[-(sqrt(33)-3)/4,(sqrt(33)+3)/4]),matrix([-(sqrt(33)-5)/2,0],[0,(sqrt(33)+5)/2])]);
+s_test_case(diagonalise(matrix([1,2],[2,1])),[matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-1/sqrt(2)]),matrix([3,0],[0,-1])]);
+s_test_case(diagonalise(matrix([1,2],[1,2])),[matrix([1,1],[-1/2,1]),matrix([0,0],[0,3])]);
+s_test_case(diagonalise(matrix([1,1],[1,1])),[matrix([1/sqrt(2),1/sqrt(2)],[-1/sqrt(2),1/sqrt(2)]),matrix([0,0],[0,2])]);
+
+s_test_case(SVD_red(matrix([0,0],[0,0])),[matrix([]),matrix([]),matrix([])]);
+s_test_case(SVD_red(matrix([sqrt(3),2],[0,sqrt(3)])),[matrix([sqrt(3)/2,1/2],[1/2,-(sqrt(3)/2)]),matrix([3,0],[0,1]),matrix([1/2,sqrt(3)/2],[sqrt(3)/2,-(1/2)])]);
+s_test_case(SVD_red(matrix([1,1],[1,1])),[matrix([1/sqrt(2)],[1/sqrt(2)]),matrix([2]),matrix([1/sqrt(2),1/sqrt(2)])]);
+s_test_case(SVD_red(matrix([1,1],[1,0],[0,1])),[matrix([sqrt(2)/sqrt(3),0],[1/sqrt(6),1/sqrt(2)],[1/sqrt(6),-(1/sqrt(2))]),matrix([sqrt(3),0],[0,1]),matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-(1/sqrt(2))])]);
+s_test_case(SVD_red(matrix([1,1,0],[1,0,1])),[matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-(1/sqrt(2))]),matrix([sqrt(3),0],[0,1]),matrix([sqrt(2)/sqrt(3),1/sqrt(6),1/sqrt(6)],[0,1/sqrt(2),-1/sqrt(2)])]);
+
+s_test_case(pinv(matrix([0,0],[0,0])),matrix([0,0],[0,0]));
+s_test_case(pinv(matrix([1,1],[1,1])),matrix([1/4,1/4],[1/4,1/4]));
+s_test_case(pinv(matrix([1,0],[0,1],[1,1])),matrix([2/3,-(1/3),1/3],[-(1/3),2/3,1/3]));
+s_test_case(pinv(matrix([1,0,1],[0,1,1])),matrix([2/3,-(1/3)],[-(1/3),2/3],[1/3,1/3]));
+
+s_test_case(SVD(matrix([0,0],[0,0])),[matrix([1,0],[0,1]),matrix([0,0],[0,0]),matrix([1,0],[0,1])]);
+s_test_case(SVD(matrix([sqrt(3),2],[0,sqrt(3)])),[matrix([sqrt(3)/2,1/2],[1/2,-(sqrt(3)/2)]),matrix([3,0],[0,1]),matrix([1/2,sqrt(3)/2],[sqrt(3)/2,-(1/2)])]);
+s_test_case(SVD(matrix([1,1],[1,1])),[matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-(1/sqrt(2))]),matrix([2,0],[0,0]),matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-(1/sqrt(2))])]);
+s_test_case(SVD(matrix([1,1],[1,0],[0,1])),[matrix([sqrt(2)/sqrt(3),0,1/sqrt(3)],[1/sqrt(6),1/sqrt(2),-(1/sqrt(3))],[1/sqrt(6),-(1/sqrt(2)),-(1/sqrt(3))]),matrix([sqrt(3),0],[0,1],[0,0]),matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-(1/sqrt(2))])]);
+s_test_case(SVD(matrix([1,1,0],[1,0,1])),[matrix([1/sqrt(2),1/sqrt(2)],[1/sqrt(2),-(1/sqrt(2))]),matrix([sqrt(3),0,0],[0,1,0]),matrix([sqrt(2)/sqrt(3),1/sqrt(6),1/sqrt(6)],[0,1/sqrt(2),-1/sqrt(2)],[1/sqrt(3),-(1/sqrt(3)),-(1/sqrt(3))])]);
+
+s_test_case(format_as_coeff(2), 2);
+s_test_case(format_as_coeff(-2), "(-2)");
+s_test_case(format_as_coeff(g-h), "(g-h)");
+s_test_case(format_as_coeff(g(x)), g(x));
+s_test_case(format_as_coeff(-g(x)), "(-g(x))");
+s_test_case(format_as_coeff(2*h), 2*h);
+s_test_case(format_as_coeff(-2*h), "(-2*h)");
+s_test_case(format_as_coeff(""), "");
+s_test_case(format_as_coeff("2"), "2");
+s_test_case(format_as_coeff(ev(2*k-2,simp)), "(2*k-2)");
+s_test_case(disp_eqns([2*x+y-z+(-3)*w = 7,-x-2*y+(-7)*w = -1,3*z = 0,x+w = 0,0 = 0],[x,y,z,w]),"\\begin{array} {rcrcrcrcr}2x& + & y& - & z& - & 3w& = &7\\\\-x& - & 2y& & & - & 7w& = &-1\\\\& & & & 3z& & & = &0\\\\x& & & & & + & w& = &0\\\\& & & & & & 0& = &0\\end{array}");
+s_test_case(mat_disp_eqns(matrix([2,1,-1,-3],[-1,-2,0,-7],[0,0,3,0],[1,0,0,1],[0,0,0,0]),matrix([7],[-1],[0],[0],[0]),[x,y,z,w]),"\\begin{array} {rcrcrcrcr}2x& + & y& - & z& - & 3w& = &7\\\\-x& - & 2y& & & - & 7w& = &-1\\\\& & & & 3z& & & = &0\\\\x& & & & & + & w& = &0\\\\& & & & & & 0& = &0\\end{array}");
+s_test_case(mat_disp_eqns(matrix([-2, k-1, -1],[0, 2*k+2, 0], [-1, k, -2]),matrix([k-1],[1],[-1]),[x,y,z]), "\begin{array} {rcrcrcr}-2x& + & (k-1)y& - & z& = &k-1\\& & (2*k+2)y& & & = &1\\-x& + & ky& - & 2z& = &-1\end{array}")