1//////////////////////////////////////////////////////////////////////
2// LibFile: linalg.scad
3// This file provides linear algebra, with support for matrix construction,
4// solutions to linear systems of equations, QR and Cholesky factorizations, and
5// matrix inverse.
6// Includes:
7// include <BOSL2/std.scad>
8// FileGroup: Math
9// FileSummary: Linear Algebra: solve linear systems, construct and modify matrices.
10// FileFootnotes: STD=Included in std.scad
11//////////////////////////////////////////////////////////////////////
12
13// Section: Matrices
14// The matrix, a rectangular array of numbers which represents a linear transformation,
15// is the fundamental object in linear algebra. In OpenSCAD a matrix is a list of lists of numbers
16// with a rectangular structure. Because OpenSCAD treats all data the same, most of the functions that
17// index matrices or construct them will work on matrices (lists of lists) whose elements are not numbers but may be
18// arbitrary data: strings, booleans, or even other lists. It may even be acceptable in some cases if the structure is non-rectangular.
19// Of course, linear algebra computations and solutions require true matrices with rectangular structure, where all the entries are
20// finite numbers.
21// .
22// Matrices in OpenSCAD are lists of row vectors. However, a potential source of confusion is that OpenSCAD
23// treats vectors as either column vectors or row vectors as demanded by
24// context. Thus both `v*M` and `M*v` are valid if `M` is square and `v` has the right length. If you want to multiply
25// `M` on the left by `v` and `w` you can do this with `[v,w]*M` but if you want to multiply on the right side with `v` and `w` as
26// column vectors, you now need to use {{transpose()}} because OpenSCAD doesn't adjust matrices
27// contextually: `A=M*transpose([v,w])`. The solutions are now columns of A and you must extract
28// them with {{column()}} or take the transpose of `A`.
29
30
31// Section: Matrix testing and display
32
33// Function: is_matrix()
34// Usage:
35// test = is_matrix(A, [m], [n], [square])
36// Description:
37// Returns true if A is a numeric matrix of height m and width n with finite entries. If m or n
38// are omitted or set to undef then true is returned for any positive dimension.
39// Arguments:
40// A = The matrix to test.
41// m = If given, requires the matrix to have this height.
42// n = Is given, requires the matrix to have this width.
43// square = If true, matrix must have height equal to width. Default: false
44function is_matrix(A,m,n,square=false) =
45 is_list(A)
46 && (( is_undef(m) && len(A) ) || len(A)==m)
47 && (!square || len(A) == len(A[0]))
48 && is_vector(A[0],n)
49 && is_consistent(A);
50
51
52// Function: is_matrix_symmetric()
53// Usage:
54// b = is_matrix_symmetric(A, [eps])
55// Description:
56// Returns true if the input matrix is symmetric, meaning it approximately equals its transpose.
57// The matrix can have arbitrary entries.
58// Arguments:
59// A = matrix to test
60// eps = epsilon for comparing equality. Default: 1e-12
61function is_matrix_symmetric(A,eps=1e-12) =
62 approx(A,transpose(A), eps);
63
64
65// Function: is_rotation()
66// Usage:
67// b = is_rotation(A, [dim], [centered])
68// Description:
69// Returns true if the input matrix is a square affine matrix that is a rotation around any point,
70// or around the origin if `centered` is true.
71// The matrix must be 3x3 (representing a 2d transformation) or 4x4 (representing a 3d transformation).
72// You can set `dim` to 2 to require a 2d transform (3x3 matrix) or to 3 to require a 3d transform (4x4 matrix).
73// Arguments:
74// A = matrix to test
75// dim = if set, specify dimension in which the transform operates (2 or 3)
76// centered = if true then require rotation to be around the origin. Default: false
77function is_rotation(A,dim,centered=false) =
78 let(n=len(A))
79 is_matrix(A,square=true)
80 && ( n==3 || n==4 && (is_undef(dim) || dim==n-1))
81 &&
82 (
83 let(
84 rotpart = [for(i=[0:n-2]) [for(j=[0:n-2]) A[j][i]]]
85 )
86 approx(determinant(rotpart),1)
87 )
88 &&
89 (!centered || [for(row=[0:n-2]) if (!approx(A[row][n-1],0)) row]==[]);
90
91
92// Function&Module: echo_matrix()
93// Usage:
94// echo_matrix(M, [description], [sig], [sep], [eps]);
95// dummy = echo_matrix(M, [description], [sig], [sep], [eps]),
96// Description:
97// Display a numerical matrix in a readable columnar format with `sig` significant
98// digits. Values smaller than eps display as zero. If you give a description
99// it is displayed at the top. You can change the space between columns by
100// setting `sep` to a number of spaces, which will use wide figure spaces the same
101// width as digits, or you can set it to any string to separate the columns.
102// Values that are NaN or INF will display as "nan" and "inf". Values which are
103// otherwise non-numerica display as two dashes. Note that this includes lists, so
104// a 3D array will display as a list of dashes.
105// Arguments:
106// M = matrix to display, which should be numerical
107// description = optional text to print before the matrix
108// sig = number of digits to display. Default: 4
109// sep = number of spaces between columns or a text string to separate columns. Default: 1
110// eps = numbers smaller than this display as zero. Default: 1e-9
111function echo_matrix(M,description,sig=4,sep=1,eps=1e-9) =
112 let(
113 horiz_line = chr(8213),
114 matstr = _format_matrix(M,sig=sig,sep=sep,eps=eps),
115 separator = str_join(repeat(horiz_line,10)),
116 dummy=echo(str(separator,is_def(description) ? str(" ",description) : ""))
117 [for(row=matstr) echo(row)]
118 )
119 echo(separator);
120
121module echo_matrix(M,description,sig=4,sep=1,eps=1e-9)
122{
123 dummy = echo_matrix(M,description,sig,sep,eps);
124}
125
126
127// Section: Matrix indexing
128
129// Function: column()
130// Usage:
131// list = column(M, i);
132// Topics: Matrices, List Handling
133// See Also: select(), slice()
134// Description:
135// Extracts entry `i` from each list in M, or equivalently column i from the matrix M, and returns it as a vector.
136// This function will return `undef` at all entry positions indexed by i not found in M.
137// Arguments:
138// M = The given list of lists.
139// i = The index to fetch
140// Example:
141// M = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]];
142// a = column(M,2); // Returns [3, 7, 11, 15]
143// b = column(M,0); // Returns [1, 5, 9, 13]
144// N = [ [1,2], [3], [4,5], [6,7,8] ];
145// c = column(N,1); // Returns [1,undef,5,7]
146// data = [[1,[3,4]], [3, [9,3]], [4, [3,1]]]; // Matrix with non-numeric entries
147// d = column(data,0); // Returns [1,3,4]
148// e = column(data,1); // Returns [[3,4],[9,3],[3,1]]
149function column(M, i) =
150 assert( is_list(M), "The input is not a list." )
151 assert( is_int(i) && i>=0, "Invalid index")
152 [for(row=M) row[i]];
153
154
155// Function: submatrix()
156// Usage:
157// mat = submatrix(M, idx1, idx2);
158// Topics: Matrices
159// See Also: column(), block_matrix(), submatrix_set()
160// Description:
161// The input must be a list of lists (a matrix or 2d array). Returns a submatrix by selecting the rows listed in idx1 and columns listed in idx2.
162// Arguments:
163// M = Given list of lists
164// idx1 = rows index list or range
165// idx2 = column index list or range
166// Example:
167// M = [[ 1, 2, 3, 4, 5],
168// [ 6, 7, 8, 9,10],
169// [11,12,13,14,15],
170// [16,17,18,19,20],
171// [21,22,23,24,25]];
172// submatrix(M,[1:2],[3:4]); // Returns [[9, 10], [14, 15]]
173// submatrix(M,[1], [3,4])); // Returns [[9,10]]
174// submatrix(M,1, [3,4])); // Returns [[9,10]]
175// submatrix(M,1,3)); // Returns [[9]]
176// submatrix(M, [3,4],1); // Returns [[17],[22]]);
177// submatrix(M, [1,3],[2,4]); // Returns [[8,10],[18,20]]);
178// A = [[true, 17, "test"],
179// [[4,2], 91, false],
180// [6, [3,4], undef]];
181// submatrix(A,[0,2],[1,2]); // Returns [[17, "test"], [[3, 4], undef]]
182function submatrix(M,idx1,idx2) =
183 [for(i=idx1) [for(j=idx2) M[i][j] ] ];
184
185
186// Section: Matrix construction and modification
187
188// Function: ident()
189// Usage:
190// mat = ident(n);
191// Topics: Affine, Matrices
192// Description:
193// Create an `n` by `n` square identity matrix.
194// Arguments:
195// n = The size of the identity matrix square, `n` by `n`.
196// Example:
197// mat = ident(3);
198// // Returns:
199// // [
200// // [1, 0, 0],
201// // [0, 1, 0],
202// // [0, 0, 1]
203// // ]
204// Example:
205// mat = ident(4);
206// // Returns:
207// // [
208// // [1, 0, 0, 0],
209// // [0, 1, 0, 0],
210// // [0, 0, 1, 0],
211// // [0, 0, 0, 1]
212// // ]
213function ident(n) = [
214 for (i = [0:1:n-1]) [
215 for (j = [0:1:n-1]) (i==j)? 1 : 0
216 ]
217];
218
219
220// Function: diagonal_matrix()
221// Usage:
222// mat = diagonal_matrix(diag, [offdiag]);
223// Topics: Matrices
224// See Also: column(), submatrix()
225// Description:
226// Creates a square matrix with the items in the list `diag` on
227// its diagonal. The off diagonal entries are set to offdiag,
228// which is zero by default.
229// Arguments:
230// diag = A list of items to put in the diagnal cells of the matrix.
231// offdiag = Value to put in non-diagonal matrix cells.
232function diagonal_matrix(diag, offdiag=0) =
233 assert(is_list(diag) && len(diag)>0)
234 [for(i=[0:1:len(diag)-1]) [for(j=[0:len(diag)-1]) i==j?diag[i] : offdiag]];
235
236
237// Function: transpose()
238// Usage:
239// M = transpose(M, [reverse]);
240// Topics: Matrices
241// See Also: submatrix(), block_matrix(), hstack(), flatten()
242// Description:
243// Returns the transpose of the given input matrix. The input can be a matrix with arbitrary entries or
244// a numerical vector. If you give a vector then transpose returns it unchanged.
245// When reverse=true, the transpose is done across to the secondary diagonal. (See example below.)
246// By default, reverse=false.
247// Example:
248// M = [
249// [1, 2, 3],
250// [4, 5, 6],
251// [7, 8, 9]
252// ];
253// t = transpose(M);
254// // Returns:
255// // [
256// // [1, 4, 7],
257// // [2, 5, 8],
258// // [3, 6, 9]
259// // ]
260// Example:
261// M = [
262// [1, 2, 3],
263// [4, 5, 6]
264// ];
265// t = transpose(M);
266// // Returns:
267// // [
268// // [1, 4],
269// // [2, 5],
270// // [3, 6],
271// // ]
272// Example:
273// M = [
274// [1, 2, 3],
275// [4, 5, 6],
276// [7, 8, 9]
277// ];
278// t = transpose(M, reverse=true);
279// // Returns:
280// // [
281// // [9, 6, 3],
282// // [8, 5, 2],
283// // [7, 4, 1]
284// // ]
285// Example: Transpose on a list of numbers returns the list unchanged
286// transpose([3,4,5]); // Returns: [3,4,5]
287// Example: Transpose on non-numeric input
288// arr = [
289// [ "a", "b", "c"],
290// [ "d", "e", "f"],
291// [[1,2],[3,4],[5,6]]
292// ];
293// t = transpose(arr);
294// // Returns:
295// // [
296// // ["a", "d", [1,2]],
297// // ["b", "e", [3,4]],
298// // ["c", "f", [5,6]],
299// // ]
300
301function transpose(M, reverse=false) =
302 assert( is_list(M) && len(M)>0, "Input to transpose must be a nonempty list.")
303 is_list(M[0])
304 ? let( len0 = len(M[0]) )
305 assert([for(a=M) if(!is_list(a) || len(a)!=len0) 1 ]==[], "Input to transpose has inconsistent row lengths." )
306 reverse
307 ? [for (i=[0:1:len0-1])
308 [ for (j=[0:1:len(M)-1]) M[len(M)-1-j][len0-1-i] ] ]
309 : [for (i=[0:1:len0-1])
310 [ for (j=[0:1:len(M)-1]) M[j][i] ] ]
311 : assert( is_vector(M), "Input to transpose must be a vector or list of lists.")
312 M;
313
314
315// Function: outer_product()
316// Usage:
317// x = outer_product(u,v);
318// Description:
319// Compute the outer product of two vectors, a matrix.
320// Usage:
321// M = outer_product(u,v);
322function outer_product(u,v) =
323 assert(is_vector(u) && is_vector(v), "The inputs must be vectors.")
324 [for(ui=u) ui*v];
325
326// Function: submatrix_set()
327// Usage:
328// mat = submatrix_set(M, A, [m], [n]);
329// Topics: Matrices
330// See Also: column(), submatrix()
331// Description:
332// Sets a submatrix of M equal to the matrix A. By default the top left corner of M is set to A, but
333// you can specify offset coordinates m and n. If A (as adjusted by m and n) extends beyond the bounds
334// of M then the extra entries are ignored. You can pass in A=[[]], a null matrix, and M will be
335// returned unchanged. This function works on arbitrary lists of lists and the input M need not be rectangular in shape.
336// Arguments:
337// M = Original matrix.
338// A = Submatrix of new values to write into M
339// m = Row number of upper-left corner to place A at. Default: 0
340// n = Column number of upper-left corner to place A at. Default: 0
341function submatrix_set(M,A,m=0,n=0) =
342 assert(is_list(M))
343 assert(is_list(A))
344 assert(is_int(m))
345 assert(is_int(n))
346 let( badrows = [for(i=idx(A)) if (!is_list(A[i])) i])
347 assert(badrows==[], str("Input submatrix malformed rows: ",badrows))
348 [for(i=[0:1:len(M)-1])
349 assert(is_list(M[i]), str("Row ",i," of input matrix is not a list"))
350 [for(j=[0:1:len(M[i])-1])
351 i>=m && i <len(A)+m && j>=n && j<len(A[0])+n ? A[i-m][j-n] : M[i][j]]];
352
353
354// Function: hstack()
355// Usage:
356// A = hstack(M1, M2)
357// A = hstack(M1, M2, M3)
358// A = hstack([M1, M2, M3, ...])
359// Topics: Matrices
360// See Also: column(), submatrix(), block_matrix()
361// Description:
362// Constructs a matrix by horizontally "stacking" together compatible matrices or vectors. Vectors are treated as columsn in the stack.
363// This command is the inverse of `column`. Note: strings given in vectors are broken apart into lists of characters. Strings given
364// in matrices are preserved as strings. If you need to combine vectors of strings use {{list_to_matrix()}} as shown below to convert the
365// vector into a column matrix. Also note that vertical stacking can be done directly with concat.
366// Arguments:
367// M1 = If given with other arguments, the first matrix (or vector) to stack. If given alone, a list of matrices/vectors to stack.
368// M2 = Second matrix/vector to stack
369// M3 = Third matrix/vector to stack.
370// Example:
371// M = ident(3);
372// v1 = [2,3,4];
373// v2 = [5,6,7];
374// v3 = [8,9,10];
375// a = hstack(v1,v2); // Returns [[2, 5], [3, 6], [4, 7]]
376// b = hstack(v1,v2,v3); // Returns [[2, 5, 8],
377// // [3, 6, 9],
378// // [4, 7, 10]]
379// c = hstack([M,v1,M]); // Returns [[1, 0, 0, 2, 1, 0, 0],
380// // [0, 1, 0, 3, 0, 1, 0],
381// // [0, 0, 1, 4, 0, 0, 1]]
382// d = hstack(column(M,0), submatrix(M,idx(M),[1 2])); // Returns M
383// strvec = ["one","two"];
384// strmat = [["three","four"], ["five","six"]];
385// e = hstack(strvec,strvec); // Returns [["o", "n", "e", "o", "n", "e"],
386// // ["t", "w", "o", "t", "w", "o"]]
387// f = hstack(list_to_matrix(strvec,1), list_to_matrix(strvec,1));
388// // Returns [["one", "one"],
389// // ["two", "two"]]
390// g = hstack(strmat,strmat); // Returns: [["three", "four", "three", "four"],
391// // [ "five", "six", "five", "six"]]
392function hstack(M1, M2, M3) =
393 (M3!=undef)? hstack([M1,M2,M3]) :
394 (M2!=undef)? hstack([M1,M2]) :
395 assert(all([for(v=M1) is_list(v)]), "One of the inputs to hstack is not a list")
396 let(
397 minlen = min_length(M1),
398 maxlen = max_length(M1)
399 )
400 assert(minlen==maxlen, "Input vectors to hstack must have the same length")
401 [for(row=[0:1:minlen-1])
402 [for(matrix=M1)
403 each matrix[row]
404 ]
405 ];
406
407
408// Function: block_matrix()
409// Usage:
410// bmat = block_matrix([[M11, M12,...],[M21, M22,...], ... ]);
411// Topics: Matrices
412// See Also: column(), submatrix()
413// Description:
414// Create a block matrix by supplying a matrix of matrices, which will
415// be combined into one unified matrix. Every matrix in one row
416// must have the same height, and the combined width of the matrices
417// in each row must be equal. Strings will stay strings.
418// Example:
419// A = [[1,2],
420// [3,4]];
421// B = ident(2);
422// C = block_matrix([[A,B],[B,A],[A,B]]);
423// // Returns:
424// // [[1, 2, 1, 0],
425// // [3, 4, 0, 1],
426// // [1, 0, 1, 2],
427// // [0, 1, 3, 4],
428// // [1, 2, 1, 0],
429// // [3, 4, 0, 1]]);
430// D = block_matrix([[A,B], ident(4)]);
431// // Returns:
432// // [[1, 2, 1, 0],
433// // [3, 4, 0, 1],
434// // [1, 0, 0, 0],
435// // [0, 1, 0, 0],
436// // [0, 0, 1, 0],
437// // [0, 0, 0, 1]]);
438// E = [["one", "two"], [3,4]];
439// F = block_matrix([[E,E]]);
440// // Returns:
441// // [["one", "two", "one", "two"],
442// // [ 3, 4, 3, 4]]
443function block_matrix(M) =
444 let(
445 bigM = [for(bigrow = M) each hstack(bigrow)],
446 len0 = len(bigM[0]),
447 badrows = [for(row=bigM) if (len(row)!=len0) 1]
448 )
449 assert(badrows==[], "Inconsistent or invalid input")
450 bigM;
451
452
453// Section: Solving Linear Equations and Matrix Factorizations
454
455// Function: linear_solve()
456// Usage:
457// solv = linear_solve(A,b,[pivot])
458// Description:
459// Solves the linear system Ax=b. If `A` is square and non-singular the unique solution is returned. If `A` is overdetermined
460// the least squares solution is returned. If `A` is underdetermined, the minimal norm solution is returned.
461// If `A` is rank deficient or singular then linear_solve returns `[]`. If `b` is a matrix that is compatible with `A`
462// then the problem is solved for the matrix valued right hand side and a matrix is returned. Note that if you
463// want to solve Ax=b1 and Ax=b2 that you need to form the matrix `transpose([b1,b2])` for the right hand side and then
464// transpose the returned value. The solution is computed using QR factorization. If `pivot` is set to true (the default) then
465// pivoting is used in the QR factorization, which is slower but expected to be more accurate.
466// Usage:
467// A = Matrix describing the linear system, which need not be square
468// b = right hand side for linear system, which can be a matrix to solve several cases simultaneously. Must be consistent with A.
469// pivot = if true use pivoting when computing the QR factorization. Default: true
470function linear_solve(A,b,pivot=true) =
471 assert(is_matrix(A), "Input should be a matrix.")
472 let(
473 m = len(A),
474 n = len(A[0])
475 )
476 assert(is_vector(b,m) || is_matrix(b,m),"Invalid right hand side or incompatible with the matrix")
477 let (
478 qr = m<n? qr_factor(transpose(A),pivot) : qr_factor(A,pivot),
479 maxdim = max(n,m),
480 mindim = min(n,m),
481 Q = submatrix(qr[0],[0:maxdim-1], [0:mindim-1]),
482 R = submatrix(qr[1],[0:mindim-1], [0:mindim-1]),
483 P = qr[2],
484 zeros = [for(i=[0:mindim-1]) if (approx(R[i][i],0)) i]
485 )
486 zeros != [] ? [] :
487 m<n ? Q*back_substitute(R,transpose(P)*b,transpose=true) // Too messy to avoid input checks here
488 : P*_back_substitute(R, transpose(Q)*b); // Calling internal version skips input checks
489
490
491// Function: linear_solve3()
492// Usage:
493// x = linear_solve3(A,b)
494// Description:
495// Fast solution to a 3x3 linear system using Cramer's rule (which appears to be the fastest
496// method in OpenSCAD). The input `A` must be a 3x3 matrix. Returns undef if `A` is singular.
497// The input `b` must be a 3-vector. Note that Cramer's rule is not a stable algorithm, so for
498// the highest accuracy on ill-conditioned problems you may want to use the general solver, which is about ten times slower.
499// Arguments:
500// A = 3x3 matrix for linear system
501// b = length 3 vector, right hand side of linear system
502function linear_solve3(A,b) =
503 // Arg sanity checking adds 7% overhead
504 assert(b*0==[0,0,0], "Input b must be a 3-vector")
505 assert(A*0==[[0,0,0],[0,0,0],[0,0,0]],"Input A must be a 3x3 matrix")
506 let(
507 Az = [for(i=[0:2])[A[i][0], A[i][1], b[i]]],
508 Ay = [for(i=[0:2])[A[i][0], b[i], A[i][2]]],
509 Ax = [for(i=[0:2])[b[i], A[i][1], A[i][2]]],
510 detA = det3(A)
511 )
512 detA==0 ? undef : [det3(Ax), det3(Ay), det3(Az)] / detA;
513
514
515// Function: matrix_inverse()
516// Usage:
517// mat = matrix_inverse(A)
518// Description:
519// Compute the matrix inverse of the square matrix `A`. If `A` is singular, returns `undef`.
520// Note that if you just want to solve a linear system of equations you should NOT use this function.
521// Instead use {{linear_solve()}}, or use {{qr_factor()}}. The computation
522// will be faster and more accurate.
523function matrix_inverse(A) =
524 assert(is_matrix(A) && len(A)==len(A[0]),"Input to matrix_inverse() must be a square matrix")
525 linear_solve(A,ident(len(A)));
526
527
528// Function: rot_inverse()
529// Usage:
530// B = rot_inverse(A)
531// Description:
532// Inverts a 2d (3x3) or 3d (4x4) rotation matrix. The matrix can be a rotation around any center,
533// so it may include a translation. This is faster and likely to be more accurate than using `matrix_inverse()`.
534function rot_inverse(T) =
535 assert(is_matrix(T,square=true),"Matrix must be square")
536 let( n = len(T))
537 assert(n==3 || n==4, "Matrix must be 3x3 or 4x4")
538 let(
539 rotpart = [for(i=[0:n-2]) [for(j=[0:n-2]) T[j][i]]],
540 transpart = [for(row=[0:n-2]) T[row][n-1]]
541 )
542 assert(approx(determinant(T),1),"Matrix is not a rotation")
543 concat(hstack(rotpart, -rotpart*transpart),[[for(i=[2:n]) 0, 1]]);
544
545
546
547
548// Function: null_space()
549// Usage:
550// x = null_space(A)
551// Description:
552// Returns an orthonormal basis for the null space of `A`, namely the vectors {x} such that Ax=0.
553// If the null space is just the origin then returns an empty list.
554function null_space(A,eps=1e-12) =
555 assert(is_matrix(A))
556 let(
557 Q_R = qr_factor(transpose(A),pivot=true),
558 R = Q_R[1],
559 zrows = [for(i=idx(R)) if (all_zero(R[i],eps)) i]
560 )
561 len(zrows)==0 ? [] :
562 select(transpose(Q_R[0]), zrows);
563
564// Function: qr_factor()
565// Usage:
566// qr = qr_factor(A,[pivot]);
567// Description:
568// Calculates the QR factorization of the input matrix A and returns it as the list [Q,R,P]. This factorization can be
569// used to solve linear systems of equations. The factorization is `A = Q*R*transpose(P)`. If pivot is false (the default)
570// then P is the identity matrix and A = Q*R. If pivot is true then column pivoting results in an R matrix where the diagonal
571// is non-decreasing. The use of pivoting is supposed to increase accuracy for poorly conditioned problems, and is necessary
572// for rank estimation or computation of the null space, but it may be slower.
573function qr_factor(A, pivot=false) =
574 assert(is_matrix(A), "Input must be a matrix." )
575 let(
576 m = len(A),
577 n = len(A[0])
578 )
579 let(
580 qr = _qr_factor(A, Q=ident(m),P=ident(n), pivot=pivot, col=0, m = m, n = n),
581 Rzero = let( R = qr[1]) [
582 for(i=[0:m-1]) [
583 let( ri = R[i] )
584 for(j=[0:n-1]) i>j ? 0 : ri[j]
585 ]
586 ]
587 ) [qr[0], Rzero, qr[2]];
588
589function _qr_factor(A,Q,P, pivot, col, m, n) =
590 col >= min(m-1,n) ? [Q,A,P] :
591 let(
592 swap = !pivot ? 1
593 : _swap_matrix(n,col,col+max_index([for(i=[col:n-1]) sqr([for(j=[col:m-1]) A[j][i]])])),
594 A = pivot ? A*swap : A,
595 x = [for(i=[col:1:m-1]) A[i][col]],
596 alpha = (x[0]<=0 ? 1 : -1) * norm(x),
597 u = x - concat([alpha],repeat(0,m-1)),
598 v = alpha==0 ? u : u / norm(u),
599 Qc = ident(len(x)) - 2*outer_product(v,v),
600 Qf = [for(i=[0:m-1]) [for(j=[0:m-1]) i<col || j<col ? (i==j ? 1 : 0) : Qc[i-col][j-col]]]
601 )
602 _qr_factor(Qf*A, Q*Qf, P*swap, pivot, col+1, m, n);
603
604// Produces an n x n matrix that swaps column i and j (when multiplied on the right)
605function _swap_matrix(n,i,j) =
606 assert(i<n && j<n && i>=0 && j>=0, "Swap indices out of bounds")
607 [for(y=[0:n-1]) [for (x=[0:n-1])
608 x==i ? (y==j ? 1 : 0)
609 : x==j ? (y==i ? 1 : 0)
610 : x==y ? 1 : 0]];
611
612
613
614// Function: back_substitute()
615// Usage:
616// x = back_substitute(R, b, [transpose]);
617// Description:
618// Solves the problem Rx=b where R is an upper triangular square matrix. The lower triangular entries of R are
619// ignored. If transpose==true then instead solve transpose(R)*x=b.
620// You can supply a compatible matrix b and it will produce the solution for every column of b. Note that if you want to
621// solve Rx=b1 and Rx=b2 you must set b to transpose([b1,b2]) and then take the transpose of the result. If the matrix
622// is singular (e.g. has a zero on the diagonal) then it returns [].
623function back_substitute(R, b, transpose = false) =
624 assert(is_matrix(R, square=true))
625 let(n=len(R))
626 assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b)))
627 transpose
628 ? reverse(_back_substitute(transpose(R, reverse=true), reverse(b)))
629 : _back_substitute(R,b);
630
631function _back_substitute(R, b, x=[]) =
632 let(n=len(R))
633 len(x) == n ? x
634 : let(ind = n - len(x) - 1)
635 R[ind][ind] == 0 ? []
636 : let(
637 newvalue = len(x)==0
638 ? b[ind]/R[ind][ind]
639 : (b[ind]-list_tail(R[ind],ind+1) * x)/R[ind][ind]
640 )
641 _back_substitute(R, b, concat([newvalue],x));
642
643
644
645// Function: cholesky()
646// Usage:
647// L = cholesky(A);
648// Description:
649// Compute the cholesky factor, L, of the symmetric positive definite matrix A.
650// The matrix L is lower triangular and `L * transpose(L) = A`. If the A is
651// not symmetric then an error is displayed. If the matrix is symmetric but
652// not positive definite then undef is returned.
653function cholesky(A) =
654 assert(is_matrix(A,square=true),"A must be a square matrix")
655 assert(is_matrix_symmetric(A),"Cholesky factorization requires a symmetric matrix")
656 _cholesky(A,ident(len(A)), len(A));
657
658function _cholesky(A,L,n) =
659 A[0][0]<0 ? undef : // Matrix not positive definite
660 len(A) == 1 ? submatrix_set(L,[[sqrt(A[0][0])]], n-1,n-1):
661 let(
662 i = n+1-len(A)
663 )
664 let(
665 sqrtAii = sqrt(A[0][0]),
666 Lnext = [for(j=[0:n-1])
667 [for(k=[0:n-1])
668 j<i-1 || k<i-1 ? (j==k ? 1 : 0)
669 : j==i-1 && k==i-1 ? sqrtAii
670 : j==i-1 ? 0
671 : k==i-1 ? A[j-(i-1)][0]/sqrtAii
672 : j==k ? 1 : 0]],
673 Anext = submatrix(A,[1:n-1], [1:n-1]) - outer_product(list_tail(A[0]), list_tail(A[0]))/A[0][0]
674 )
675 _cholesky(Anext,L*Lnext,n);
676
677
678// Section: Matrix Properties: Determinants, Norm, Trace
679
680// Function: det2()
681// Usage:
682// d = det2(M);
683// Description:
684// Rturns the determinant for the given 2x2 matrix.
685// Arguments:
686// M = The 2x2 matrix to get the determinant of.
687// Example:
688// M = [ [6,-2], [1,8] ];
689// det = det2(M); // Returns: 50
690function det2(M) =
691 assert(is_def(M) && M*0==[[0,0],[0,0]], "Expected square matrix (2x2)")
692 cross(M[0],M[1]);
693
694
695// Function: det3()
696// Usage:
697// d = det3(M);
698// Description:
699// Returns the determinant for the given 3x3 matrix.
700// Arguments:
701// M = The 3x3 square matrix to get the determinant of.
702// Example:
703// M = [ [6,4,-2], [1,-2,8], [1,5,7] ];
704// det = det3(M); // Returns: -334
705function det3(M) =
706 assert(is_def(M) && M*0==[[0,0,0],[0,0,0],[0,0,0]], "Expected square matrix (3x3).")
707 M[0][0] * (M[1][1]*M[2][2]-M[2][1]*M[1][2]) -
708 M[1][0] * (M[0][1]*M[2][2]-M[2][1]*M[0][2]) +
709 M[2][0] * (M[0][1]*M[1][2]-M[1][1]*M[0][2]);
710
711// Function: det4()
712// Usage:
713// d = det4(M);
714// Description:
715// Returns the determinant for the given 4x4 matrix.
716// Arguments:
717// M = The 4x4 square matrix to get the determinant of.
718// Example:
719// M = [ [6,4,-2,1], [1,-2,8,-3], [1,5,7,4], [2,3,4,7] ];
720// det = det4(M); // Returns: -1773
721function det4(M) =
722 assert(is_def(M) && M*0==[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]], "Expected square matrix (4x4).")
723 M[0][0]*M[1][1]*M[2][2]*M[3][3] + M[0][0]*M[1][2]*M[2][3]*M[3][1] + M[0][0]*M[1][3]*M[2][1]*M[3][2]
724 + M[0][1]*M[1][0]*M[2][3]*M[3][2] + M[0][1]*M[1][2]*M[2][0]*M[3][3] + M[0][1]*M[1][3]*M[2][2]*M[3][0]
725 + M[0][2]*M[1][0]*M[2][1]*M[3][3] + M[0][2]*M[1][1]*M[2][3]*M[3][0] + M[0][2]*M[1][3]*M[2][0]*M[3][1]
726 + M[0][3]*M[1][0]*M[2][2]*M[3][1] + M[0][3]*M[1][1]*M[2][0]*M[3][2] + M[0][3]*M[1][2]*M[2][1]*M[3][0]
727 - M[0][0]*M[1][1]*M[2][3]*M[3][2] - M[0][0]*M[1][2]*M[2][1]*M[3][3] - M[0][0]*M[1][3]*M[2][2]*M[3][1]
728 - M[0][1]*M[1][0]*M[2][2]*M[3][3] - M[0][1]*M[1][2]*M[2][3]*M[3][0] - M[0][1]*M[1][3]*M[2][0]*M[3][2]
729 - M[0][2]*M[1][0]*M[2][3]*M[3][1] - M[0][2]*M[1][1]*M[2][0]*M[3][3] - M[0][2]*M[1][3]*M[2][1]*M[3][0]
730 - M[0][3]*M[1][0]*M[2][1]*M[3][2] - M[0][3]*M[1][1]*M[2][2]*M[3][0] - M[0][3]*M[1][2]*M[2][0]*M[3][1];
731
732// Function: determinant()
733// Usage:
734// d = determinant(M);
735// Description:
736// Returns the determinant for the given square matrix.
737// Arguments:
738// M = The NxN square matrix to get the determinant of.
739// Example:
740// M = [ [6,4,-2,9], [1,-2,8,3], [1,5,7,6], [4,2,5,1] ];
741// det = determinant(M); // Returns: 2267
742function determinant(M) =
743 assert(is_list(M), "Input must be a square matrix." )
744 len(M)==1? M[0][0] :
745 len(M)==2? det2(M) :
746 len(M)==3? det3(M) :
747 len(M)==4? det4(M) :
748 assert(is_matrix(M, square=true), "Input must be a square matrix." )
749 sum(
750 [for (col=[0:1:len(M)-1])
751 ((col%2==0)? 1 : -1) *
752 M[col][0] *
753 determinant(
754 [for (r=[1:1:len(M)-1])
755 [for (c=[0:1:len(M)-1])
756 if (c!=col) M[c][r]
757 ]
758 ]
759 )
760 ]
761 );
762
763
764// Function: norm_fro()
765// Usage:
766// norm_fro(A)
767// Description:
768// Computes frobenius norm of input matrix. The frobenius norm is the square root of the sum of the
769// squares of all of the entries of the matrix. On vectors it is the same as the usual 2-norm.
770// This is an easily computed norm that is convenient for comparing two matrices.
771function norm_fro(A) =
772 assert(is_matrix(A) || is_vector(A))
773 norm(flatten(A));
774
775
776// Function: matrix_trace()
777// Usage:
778// matrix_trace(M)
779// Description:
780// Computes the trace of a square matrix, the sum of the entries on the diagonal.
781function matrix_trace(M) =
782 assert(is_matrix(M,square=true), "Input to trace must be a square matrix")
783 [for(i=[0:1:len(M)-1])1] * [for(i=[0:1:len(M)-1]) M[i][i]];