File: diag.mac

package info (click to toggle)
maxima 5.42.1-1
  • links: PTS
  • area: main
  • in suites: buster
  • size: 150,192 kB
  • sloc: lisp: 382,565; fortran: 14,666; perl: 14,365; tcl: 11,123; sh: 4,622; makefile: 2,688; ansic: 444; xml: 23; awk: 17; sed: 17
file content (391 lines) | stat: -rw-r--r-- 13,814 bytes parent folder | download | duplicates (10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
/*
   diag.mac

   Contains the functions:
     (1) diag
     (2) JF
     (3) jordan
     (4) minimalPoly
     (5) dispJordan
     (6) ModeMatrix
     (7) mat_function

   plus some internal utility funcions. If you make use of one of
   them, consider asking on the mailing list: we can try to make a
   more useful API and give it a sensible name (not prefixed by
   "diag_"), that we won't change.

   See the manual or the comments above the relevant function for more
   information on how each works.

   As with the rest of Maxima, this code is distributed under the GPL,
   version 2+
*/
load("eigen")$

/*
  If EXPR is a matrix then return it unchanged. Otherwise, return a
  1x1 matrix whose sole element is EXPR.
*/
diag_matrixify (expr) :=
  if matrixp (expr) then expr else matrix ([expr])$

/*
  Pad R with zeros before and after to make it WIDTH wide using INDENT
  zeros on the left hand side.
*/
diag_zeropad_row (r, indent, width) :=
  block([right: width - (left + length(r))],
    if is(right < 0) then
      error(r, "does not fit in a row of length", width,
            "with indent", indent),
    append (makelist (0, indent), r, makelist (0, right)))$

/*
  Return the direct sum of the elements of LST, made into 1x1 matrices
  if they weren't already matrices.
*/
diag (lst) :=
  block ([lst: map (diag_matrixify, lst),
          width, height, left: 0, rows: []],
    width: lsum (length (first (A)), A, lst),
    height: lsum (length (A), A, lst),
    for A in lst do (
      for r in args(A) do
        rows: cons (diag_zeropad_row (r, left, width), rows),
      left: left + length (first (A))),
    apply (matrix, reverse (rows)))$

/*
  Construct a Jordan block of size N x N and eigenvalue EIVAL.
*/
JF (eival, n) :=
  if not (numberp (n)) then
    'JF (eival, n)
  elseif not (integerp (n)) then
    error (n, "is not an integer, so not a valid Jordan block size")
  elseif is (n <= 0) then
    error ("Cannot construct matrix with negative size", n)
  else
    genmatrix (
      lambda ([i,j],
              if is (i = j) then eival
              elseif is (j = i + 1) then 1
              else 0),
      n, n)$
	  
/*
  Calculate the correct partition of multiplicity for the matrix A at
  the eigenvalue eival.
*/
diag_calculate_mult_partition (A, multiplicity, eival) :=
  if is (multiplicity = 1) then [1] else
  block ([Abar: A - eival * ident (length (A)),
          n: length (A),
          nullity],
    nullity: n - rank (Abar),
    if is (nullity = 1) then [multiplicity]
    elseif is (nullity = multiplicity) then makelist (1, nullity)
    else block ([blocks_left: nullity, mults: [], Abarpow: Abar, dnull],
      for k: 1 do (
        Abarpow: Abarpow . Abar,
        dnull: n - rank (Abarpow) - nullity,
        nullity: nullity + dnull,
        if (dnull < blocks_left) then (
          mults: append (makelist (k, blocks_left - dnull), mults),
          multiplicity: multiplicity - k * (blocks_left - dnull),
          blocks_left: dnull),
        if is (blocks_left * (k + 1) = multiplicity) then
          return (append (makelist (k+1, blocks_left), mults)),
        if is (blocks_left * (k + 1) > multiplicity) then
          error ("Unexpected blocks left over!"))))$

/*
  Calculate the JCF of A. Returns a list of eigenvalues and their
  multiplicities.
*/
jordan (A) :=
  if not (matrixp (A)) then
    'jordan(A)
  else block([eigenlist: eigenvectors (A)],
    map (lambda ([eival, mult],
                 cons (eival, diag_calculate_mult_partition (A, mult, eival))),
         eigenlist[1][1], eigenlist[1][2]))$

/*
  A simple sanity check that arguments are of the form returned by
  jordan().
*/
diag_jordan_info_check (lst) :=
  if not (listp (lst)) then false
  else block([ret: true],
    for pair in lst do
      if not (listp (pair)) then (ret: false, return (false))
      elseif is (length (pair) < 2) then
        error ("Found a surprisingly short jordan_info list:",
               pair),
    ret)$

/*
  Calculate the minimal polynomial, expressed in the symbol "x", of
  any matrix with the given list of eigenvalues and their
  multiplicities.

  NOTE: This assumes that the first multiplicity for an eigenvalue is
        the largest and that the given eigenvalues are distinct. This
        is the case for the output of jordan().
*/
minimalPoly (jordan_info) :=
  if not (diag_jordan_info_check (jordan_info)) then
    'minimalPoly (jordan_info)
  else
  lreduce ("*",
    map (lambda ([eival_lst], ('x - first (eival_lst))^(second (eival_lst))),
         jordan_info))$

/*
  Take a list of eigenvalues and their multiplicities and build the
  corresponding Jordan matrix.
*/
dispJordan (jordan_info) := block ([blocks: []],
  if not (diag_jordan_info_check (jordan_info)) then
    'dispJordan (jordan_info)
  else
  for eival_lst in jordan_info do
    for size in rest (eival_lst) do
      blocks: cons (JF (first (eival_lst), size), blocks),
  diag (reverse (blocks)))$

/*
  Takes a "sorted list" and produces a histogram of elements and their
  frequencies:

     [3,3,2,2,2,1,1,1] => [[3,2],[2,3],[1,3]]

  The "sorted" requirement is merely that any object only occurs in
  one block. Thus, we will do the right thing on [1,1,0,0,2,2], but
  not on [1,1,0,0,1,1].
*/
diag_sorted_list_histogram (list) :=
  if is (emptyp (list)) then [] else
  block([pairs: [], current: first (list), count: 1],
    for lst: rest (list) next rest (lst) while not (emptyp (lst))
    do
      if is (current = first (lst)) then
        count: count + 1
      else (
        pairs: cons ([current, count], pairs),
        current: first (lst),
        count: 1),
    reverse (cons ([current, count], pairs)))$

/*
  Find a formula for a general element of the kernel of A.

  Returns the solution as a row vector of (linear) expressions in the
  variables of %rnum_list.
*/
diag_kernel_element (A) :=
  block ([vars: makelist (gensym (), length (first (A))), eqns, soln],
    if length (vars) = 1 then
      eqns: [first(vars) * first (first (A))]
    else
      eqns: map (first, args (A . apply (matrix, map("[", vars)))),
    soln: algsys (eqns, vars),
    if emptyp (soln) then error ("Could not solve for the kernel of ", A),
    map (rhs, first (soln)))$

/*
  Calculate the general form for a chain of generalized eigenvectors
  for A, of the form [v1, v2, ..., vn] such that (A-eival*I).v1 = 0 and
  (A-eival*I).vk = v(k-1) where n = degree.

  If degree is not correct, throws an error.

  Otherwise returns [chain, freevars] where chain is a list of the
  eigenvectors, each of which is represented as a list, and freevars
  is the list of free variables.
*/
diag_general_jordan_chain (A, eival, degree) :=
  block ([n: length (A), Abar, ret],
    if (is (n < 1) or not (length (A[1]) = n)) then
    error ("Invalid input matrix: ", A),
    Abar: A - eival * ident (n),
    ret: [diag_kernel_element (Abar^^degree)],
    for i : 2 thru degree do
      ret: cons (args (map (first, Abar . first(ret))), ret),
    [ret, %rnum_list])$

/*
  LI_ROWS should be a list of lists representing row vectors that are
  linearly independent. Then CHAIN_EXPR is a list of row vectors in
  VARIABLES representing a Jordan chain. The function tries to find a
  choice of variables such that CHAIN_EXPR is linearly independent of
  LI_ROWS.

  (Note: Because chain_expr represents a Jordan chain, it suffices to
         check linear independence for the first and last elements of
         CHAIN_EXPR)
*/
diag_find_li_chain (li_rows, chain_expr, variables) :=
  block ([dict: false, extra_rows],
    for vals in ident (length (variables)) do (
      dict: map ("=", variables, vals),
      extra_rows: subst (dict,
                         if is (length (chain_expr) > 1) then
                         [first (chain_expr), last (chain_expr)]
                         else [first (chain_expr)]),
      if is (rank (apply (matrix, append (li_rows, extra_rows))) =
             length (li_rows) + length (extra_rows))
      then return (true)
      else dict: false),
    if is (dict = false) then
      error ("Could not find a linearly independent vector"),
    /* Rectform since complex eigenvalues result in horrible
       expressions here otherwise */
    rectform (subst (dict, chain_expr)))$

/*
   Calculates the mode matrix of A, which is a matrix T such that
      T^^(-1) . A . T = JordanForm (A)

   F should be a list of the eigenvalues of A together with their
   multiplicities, as returned by jordan(A).

   Notes on algorithm:

   rest(ev_lst) is a list of chain lengths, which we convert to
   multiplicities. To ensure we get the correct answer, when we hunt
   for chains we need to start with those of maximal
   length. Fortunately, jordan() returns lists where rest(ev_lst) is
   (weakly) decreasing. As such, we can just work down the lengths in
   the given order and we'll get the right answer.

   For each chain length, we find a general form for a Jordan chain
   that length then we evaluate the formula, varying the parameters to
   make sure we end up with a linearly independent chain. Note that it
   suffices to check that the first term of the chain list (the actual
   eigenvector) is linearly independent from the rows we've got so
   far.

   Since generalised eigenvectors for different eigenvalues are
   linearly independent, we don't bother checking there.
*/
diag_mode_matrix (a, F) :=
  if not (diag_jordan_info_check (F)) or not (matrixp (a)) then
    'ModeMatrix (a, F)
  else block([msize: length(a), all_rows: []],
    for ev_pair in F do
      block ([eival: first (ev_pair),
              multiplist: diag_sorted_list_histogram (rest (ev_pair)),
              mat_rows: [], mat_rank: 0],
        for degree_pair in multiplist do
          block ([mindeg: first (degree_pair),
                  genev_pair, genevs, free_vars],
            genev_pair: diag_general_jordan_chain (a, eival,
                                                   first (degree_pair)),
            for k : 1 thru second (degree_pair) do
              mat_rows:
                append (mat_rows,
                        diag_find_li_chain (mat_rows,
                                            first (genev_pair),
                                            second (genev_pair)))),
        all_rows: append (all_rows, mat_rows)),
    transpose (apply (matrix, all_rows)))$

/*
  Finds a matrix T such that T^^(-1) . A . T is the Jordan form of A.
*/
ModeMatrix (A, [jordan_info]) :=
  if is (length (jordan_info) > 1) then
    error ("Too many arguments for ModeMatrix. (Expects 1 or 2)")
  elseif not (matrixp (A)) then
    if is (length (jordan_info) = 1) then
      'ModeMatrix (A, first (jordan_info))
    else
      'ModeMatrix (A)
  else
    diag_mode_matrix (A,
                      if is (length (jordan_info) = 1)
                      then first (jordan_info) else jordan (A))$

/*
  Return a list of taylor coefficients for the function f expanded
  around some arbitrary point VAR where f(var) = EXPR. The first
  element of the list is f(var) and the last is
  diff(f(var),var,maxpow).
*/
diag_taylor_coefficients (expr, var, maxpow) :=
  block ([coeffs: [expr]],
    for k:1 thru maxpow do
      (expr: diff (expr, var), coeffs: cons (expr/k!, coeffs)),
    reverse (coeffs))$

/*
  Build a matrix of the form:

        [ f(0)  f'(0)  f''(0) ]
        [  0    f(0)   f'(0)  ]
        [  0      0     f(0)  ]

  except with coefficients taken from the COEFFS list, substituting
  EIGENVALUE for VAR, where SIZE is the size of the matrix. This is
  the result of Taylor expanding a f(A), where A is a Jordan block,
  around its eigenvalue.
*/
diag_taylor_expand_block (coeffs, var, eigenvalue, size) := (
  coeffs: makelist (subst (eigenvalue, var, coeffs[i]), i, 1, size),
  apply (matrix,
         makelist (makelist (if is (i<k) then 0 else coeffs[i-k+1], i, 1, size),
                   k, 1, size)))$

/*
  Calculate the value of an analytic function on the matrix
  represented by the Jordan list JORDAN. The function is given by EXPR
  in VAR.
*/
diag_mat_function_jordan (jordan, expr, var) :=
  block ([coeffs, blocks: [],
          max_degree: lmax (map (second, jordan)) - 1],
    /*
      Expand f(var) about some arbitrary point as a Taylor series. The
      Jordan matrix is diagonal plus a nilpotent matrix order one less
      than the largest block. We need coefficients the same order as
      that maximum block.
    */
    coeffs: diag_taylor_coefficients (expr, var, max_degree),
    /*
      We calculate the value of EXPR on each Jordan block using
      DIAG_TAYLOR_EXPAND_BLOCK. The degrees in JORDAN_LST are known to
      be decreasing, so we can be slightly clever about not computing
      things repeatedly.
    */
    for jordan_lst in jordan do
      block ([cached_block:
                diag_taylor_expand_block (coeffs, var,
                                          first (jordan_lst),
                                          second (jordan_lst)),
              cached_size: second(jordan_lst)],
        for size in rest (jordan_lst) do
          (if is (size # cached_size) then
             (cached_size: size,
              cached_block:
                apply (matrix,
                       makelist (
                         makelist (cached_block[i,j], j, 1, size),
                         i, 1, size))),
           blocks: cons (cached_block, blocks))),
    diag (reverse (blocks)))$

/*
  Take an analytic function, f, and a matrix, A, and calculate f(A) by
  means of the associated Taylor series.
*/
mat_function (f, A) :=
  if not (matrixp (A)) then
    'mat_function (f, A)
  else
  block ([jj: jordan (A), var: gensym(), modemat],
    modemat: ModeMatrix (A, jj),
    modemat . diag_mat_function_jordan (jj, apply (f, [var]), var)
            . (modemat)^^(-1))$