Coverage for dragonfly_sim/core/pod.py: 88%
274 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 03:37 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 03:37 +0000
1"""
2Proper orthogonal decomposition (POD) of solution snapshots, distributed over
3MPI with PETSc.
5SnapshotMatrix keeps the snapshot matrix S = [x_1, ..., x_n] only as its
6incrementally updated, optionally truncated SVD, in a weighted inner product
7(x, y)_W = x^T W y. PODBasis turns that SVD into a reduced basis for the
8current design point::
10 global the leading rb_size left-singular vectors of S;
11 local_weighted the leading left-singular vectors of S diag(w), with
12 partition-of-unity weights w that decay with the distance
13 between each snapshot's parameter vector and the current
14 one (compact-support cubic by default).
16Both bases are W-orthonormal (V^T W V = I). core/reduced_order_model.py
17builds W and drives the reduced solves.
18"""
19from copy import deepcopy
20from time import perf_counter
22import numpy as np
23import scipy.linalg as sp_la
24from scipy.spatial.distance import cdist
25from mpi4py import MPI
26from petsc4py import PETSc
28from dragonfly_sim.utils.pod_utils import (petsc_append_dense_column, petsc_dense_from_local_block,
29 petsc_small_mat_from_numpy, petsc_small_vec_from_numpy,
30 petsc_small_mat_to_numpy, cubic_dist_hat,
31 cubic_dist_hat_fixed, basis_size_floor, cubic_weights)
34class SnapshotMatrix:
35 """
36 Incremental weighted QR and SVD of the snapshot matrix.
38 Each snapshot x_k (a PETSc Vec in the state layout) updates a weighted QR
39 decomposition S = Q R with Q^T W Q = I, then the SVD of the small R; Q is
40 rotated into R's left-singular basis every update (Q <- Q U_R), so Q IS
41 the SVD's U and only one dense n_dofs x rank matrix is stored. The
42 rotation is also what makes truncation to max_rank the standard truncated
43 incremental SVD.
45 Storage: Q distributed (MATDENSE, rows in the state layout); R, Sigma,
46 VT small and replicated on PETSc.COMM_SELF. W is any object with
47 W.mult(x, y) (a PETSc Mat or utils/pod_utils.DiagonalWeight), or None
48 for the Euclidean inner product.
50 A snapshot is rejected (no new column) when its parameter vector
51 duplicates an earlier one; its location and the weights reconstructing it
52 from the existing snapshots are kept, and the local_weighted basis
53 redistributes its weight onto those. A rank-deficient snapshot -- one in
54 the span of Q up to a weighted residual ||x - Q Q^T W x||_W below
55 qr_rank_tol ||x||_W, a test independent of the snapshot's scale -- adds a
56 column to R/VT but not to Q, so row i of the parameter array matches
57 column i of R/VT, never of Q.
58 """
60 def __init__(self, comm, n_dofs=None, W=None, qr_rank_tol=1e-10, reorthogonalize=True,
61 max_rank=None):
62 self.comm = comm
63 self.n_dofs = n_dofs # expected global size, checked on the first snapshot
64 self.W = W
65 self.qr_rank_tol = qr_rank_tol # relative weighted residual needed for a new basis direction
66 self.reorthogonalize = reorthogonalize
67 self.max_rank = max_rank # None: Q gains a column per accepted snapshot, unbounded
69 self.Q = None # n_dofs x r, distributed, W-orthonormal columns (== U)
70 self.R = None # r x n
71 self.Sigma = None # r
72 self.VT = None # r x n
74 self.rank = 0
75 self.n_snapshots = 0
77 self.snapshot_parameter_array = np.array([])
78 self.snapshot_list_parameters = []
79 self.rejected_snapshot_parameters = []
80 # rejected_snapshot_reconstruction_weights[j] has the length n_snapshots
81 # had when rejection j happened; consumers zero-pad it. Valid because
82 # columns of R are only ever appended, never reordered or dropped
83 # (truncation trims rows).
84 self.rejected_snapshot_reconstruction_weights = []
86 # ------------------------------------------------------------------
87 # Parameter bookkeeping
88 # ------------------------------------------------------------------
89 def construct_snapshot_parameter_array(self):
90 self.snapshot_parameter_array = np.vstack(self.snapshot_list_parameters)
92 def _is_duplicate_parameter(self, par_vec):
93 if len(self.snapshot_list_parameters) == 0:
94 return False
95 self.construct_snapshot_parameter_array()
96 return cdist(par_vec[None, :], self.snapshot_parameter_array).min() <= 1e-10
98 # ------------------------------------------------------------------
99 # Snapshot update
100 # ------------------------------------------------------------------
101 def _apply_W(self, x):
102 """(W x, owned): W x as a fresh Vec, or x itself when unweighted --
103 `owned` says whether the caller must destroy it."""
104 if self.W is None:
105 return x, False
106 Wx = x.duplicate()
107 self.W.mult(x, Wx)
108 return Wx, True
110 def process_and_append_to_snapshot_lists(self, x_k, par_vec):
111 """
112 Incorporate snapshot `x_k` (not modified) with parameter vector
113 `par_vec`. Returns True if it added a new basis direction, False if
114 it was a duplicate parameter or rank-deficient within qr_rank_tol.
115 """
116 if self._is_duplicate_parameter(par_vec):
117 PETSc.Sys.Print("Snapshot parameter vector already present; keeping its location and "
118 "reconstruction weights, adding no column.")
119 Wx, owns_Wx = self._apply_W(x_k)
120 r_vec_np, _, rho = self._project_onto_Q(x_k, Wx)
121 x_norm = self._weighted_norm(x_k, Wx)
122 if owns_Wx:
123 Wx.destroy()
124 d = np.linalg.lstsq(petsc_small_mat_to_numpy(self.R), r_vec_np, rcond=None)[0]
125 if rho > self.qr_rank_tol * x_norm:
126 PETSc.Sys.Print("WARNING: the snapshot at a duplicate parameter location is not well "
127 "reconstructed by the existing basis (relative weighted residual "
128 "{:.6e} > qr_rank_tol {:.6e})".format(rho / x_norm, self.qr_rank_tol))
129 self.rejected_snapshot_parameters += [deepcopy(par_vec)]
130 self.rejected_snapshot_reconstruction_weights += [d]
131 return False
133 if self.n_dofs is not None and self.Q is None:
134 assert x_k.getSize() == self.n_dofs, (
135 "snapshot size {} does not match n_dofs {}".format(x_k.getSize(), self.n_dofs))
137 t0 = perf_counter()
138 new_direction_found = self._update_weighted_QR_and_SVD(x_k)
139 self.snapshot_list_parameters += [deepcopy(par_vec)]
140 PETSc.Sys.Print("Snapshot append time: {}".format(perf_counter() - t0))
142 assert len(self.snapshot_list_parameters) == self.n_snapshots
143 if not new_direction_found:
144 PETSc.Sys.Print("Snapshot added no new basis direction (relative weighted residual below "
145 "{}); rank remains {}.".format(self.qr_rank_tol, self.rank))
146 return new_direction_found
148 def _project_onto_Q(self, x_k, Wx):
149 """
150 W-orthogonal projection of x_k onto Q: (r, e_local, rho) with r =
151 Q^T W x_k (replicated), e_local this rank's slice of x_k - Q r, and
152 rho = sqrt(e^T W e). `Wx` is W x_k (or x_k when unweighted).
154 With reorthogonalize, a second projection pass (CGS2) removes the
155 round-off a single pass leaves when x_k is nearly in span(Q), as
156 snapshots from nearby design points are; without it orthogonality
157 degrades catastrophically over many updates.
158 """
159 comm = self.comm
160 x_local = x_k.getArray(readonly=True)
161 Wx_local = Wx.getArray(readonly=True)
163 if self.Q is None:
164 r_vec_np = np.zeros((0,))
165 e_local = np.array(x_local, copy=True)
166 else:
167 Q_local = self.Q.getDenseArray()
168 r_vec_np = comm.allreduce(Q_local.T @ Wx_local, op=MPI.SUM)
169 e_local = x_local - Q_local @ r_vec_np
170 if self.reorthogonalize:
171 e_tmp = x_k.duplicate()
172 e_tmp.setArray(e_local)
173 We_tmp, owns = self._apply_W(e_tmp)
174 r_vec2_np = comm.allreduce(Q_local.T @ We_tmp.getArray(readonly=True), op=MPI.SUM)
175 if owns:
176 We_tmp.destroy()
177 e_tmp.destroy()
178 e_local = e_local - Q_local @ r_vec2_np
179 r_vec_np = r_vec_np + r_vec2_np
181 e_vec = x_k.duplicate()
182 e_vec.setArray(e_local)
183 We_vec, owns = self._apply_W(e_vec)
184 rho = float(np.sqrt(max(np.real(e_vec.dot(We_vec)), 0.0)))
185 if owns:
186 We_vec.destroy()
187 e_vec.destroy()
188 return r_vec_np, e_local, rho
190 def _weighted_norm(self, x, Wx):
191 """||x||_W = sqrt(x^T W x), given W x. Collective."""
192 return float(np.sqrt(max(np.real(x.dot(Wx)), 0.0)))
194 def _update_weighted_QR_and_SVD(self, x_k):
195 comm = self.comm
196 Wx, owns_Wx = self._apply_W(x_k)
197 r_vec_np, e_local, rho = self._project_onto_Q(x_k, Wx)
198 x_norm = self._weighted_norm(x_k, Wx)
199 if owns_Wx:
200 Wx.destroy()
201 # relative test: the snapshot's part outside span(Q) must exceed
202 # qr_rank_tol times its own size (a zero snapshot never qualifies)
203 new_direction_found = rho > self.qr_rank_tol * x_norm
205 # R (small, replicated)
206 R_old_np = petsc_small_mat_to_numpy(self.R)
207 if new_direction_found:
208 if R_old_np.shape[0] > 0:
209 R_new_np = np.block([[R_old_np, r_vec_np[:, None]],
210 [np.zeros((1, R_old_np.shape[1])), rho]])
211 else:
212 R_new_np = np.array([[rho]])
213 elif R_old_np.shape[0] > 0:
214 R_new_np = np.column_stack([R_old_np, r_vec_np])
215 else:
216 PETSc.Sys.Print("WARNING: first snapshot has weighted norm below tolerance {}; basis "
217 "remains empty.".format(self.qr_rank_tol))
218 R_new_np = np.zeros((0, R_old_np.shape[1] + 1))
220 # Q extended by the new direction
221 Q_ext = petsc_append_dense_column(comm, self.Q, e_local / rho) if new_direction_found else self.Q
223 # SVD of the small R, computed redundantly (and identically) on every rank
224 if R_new_np.shape[0] > 0 and R_new_np.shape[1] > 0:
225 U_R_np, Sigma_np, VT_np = sp_la.svd(R_new_np, full_matrices=False)
226 else:
227 U_R_np, Sigma_np, VT_np = np.zeros((0, 0)), np.zeros((0,)), np.zeros((0, R_new_np.shape[1]))
229 # Rotate Q into R's left-singular basis, truncated to max_rank:
230 # Q' = Q U_R[:, :k], R' = diag(Sigma[:k]) VT[:k]. Q' stays W-orthonormal,
231 # Q' R' is the rank-k truncation of the snapshot matrix, and the SVD of
232 # R' has U = I, so U = Q'.
233 if Q_ext is not None:
234 k = Sigma_np.shape[0] if self.max_rank is None else min(Sigma_np.shape[0], self.max_rank)
235 Q_new = petsc_dense_from_local_block(comm, Q_ext.getDenseArray() @ U_R_np[:, :k],
236 Q_ext.getSize()[0])
237 Sigma_np = Sigma_np[:k]
238 VT_np = VT_np[:k, :]
239 R_new_np = Sigma_np[:, None] * VT_np
240 Q_ext.destroy()
241 else:
242 Q_new = None
243 if self.Q is not None and self.Q is not Q_ext:
244 self.Q.destroy()
245 for obj in (self.R, self.Sigma, self.VT):
246 if obj is not None:
247 obj.destroy()
249 self.Q = Q_new
250 self.R = petsc_small_mat_from_numpy(R_new_np)
251 self.Sigma = petsc_small_vec_from_numpy(Sigma_np)
252 self.VT = petsc_small_mat_from_numpy(VT_np)
253 self.rank = 0 if self.Q is None else self.Q.getSize()[1]
254 self.n_snapshots = self.R.getSize()[1]
255 return new_direction_found
257 # ------------------------------------------------------------------
258 # Read-only surface
259 # ------------------------------------------------------------------
260 @property
261 def U(self):
262 """The left-singular vectors: the same object as self.Q."""
263 return self.Q
265 @property
266 def full_SVD_triplet(self):
267 return (self.U, self.Sigma, self.VT)
269 def print_info(self):
270 if self.Q is not None:
271 PETSc.Sys.Print("Snapshot SVD: basis {} x {}, R {}, {} snapshots".format(
272 self.Q.getSize()[0], self.Q.getSize()[1], self.R.getSize(), self.n_snapshots))
273 else:
274 PETSc.Sys.Print("No snapshots processed yet.")
276 def gather_dense_to_rank0_numpy(self, mat, root=0):
277 """A distributed dense Mat as one numpy array on `root` (diagnostics)."""
278 if mat is None:
279 return None
280 gathered = self.comm.gather(mat.getDenseArray().copy(), root=root)
281 return np.vstack(gathered) if self.comm.Get_rank() == root else None
283 def destroy(self):
284 for obj in (self.Q, self.R, self.Sigma, self.VT):
285 if obj is not None:
286 obj.destroy()
287 self.Q = self.R = self.Sigma = self.VT = None
288 self.rank = 0
291class PODBasis:
292 """
293 A reduced basis from the snapshots in a SnapshotMatrix (see the module
294 docstring for the basis modes). `basis` is the current basis, a
295 distributed MATDENSE in the state layout, rebuilt (destroyed and
296 reallocated) by every construct_basis call.
297 """
298 SUPPORTED_BASIS_MODES = ('global', 'local_weighted')
299 WEIGHT_FUNCTIONS = ('Cubic', 'Linear', 'Quadratic', 'InverseDistance')
301 def __init__(self, comm, n_dofs, basis_mode='global', subtract_mean=False,
302 mean_weighting='snapshot_weights', IP_matrix=None, max_rank=None, qr_rank_tol=1e-10):
303 if basis_mode not in self.SUPPORTED_BASIS_MODES:
304 raise ValueError("basis_mode must be one of {}, got {!r}".format(
305 self.SUPPORTED_BASIS_MODES, basis_mode))
306 if subtract_mean and basis_mode != 'local_weighted':
307 # the snapshots exist only as their SVD, so centring composes into
308 # the small weighted factor that only local_weighted forms
309 raise NotImplementedError("subtract_mean=True needs basis_mode='local_weighted'")
310 if mean_weighting not in ('uniform', 'snapshot_weights'):
311 raise ValueError("mean_weighting must be 'uniform' or 'snapshot_weights', got {!r}".format(
312 mean_weighting))
313 self.comm = comm
314 self.n_dofs = n_dofs
315 self.basis_mode = basis_mode
316 self.subtract_mean = subtract_mean
317 self.mean_weighting = mean_weighting
318 self.IP_matrix = IP_matrix
319 self.snapshot_matrix = SnapshotMatrix(comm, n_dofs, W=IP_matrix, qr_rank_tol=qr_rank_tol,
320 reorthogonalize=True, max_rank=max_rank)
321 self.basis = None
322 self.mean = None
324 def _set_basis_from_local_block(self, basis_local_block):
325 """
326 Rebuild self.basis from this rank's row block. (MatCreateSubMatrix is
327 not used to slice columns of U: for MATDENSE it fails on a width
328 change, and under MPI returns replicated columns or corrupts the heap,
329 depending on how the column IS is split.)
330 """
331 if self.basis is not None:
332 self.basis.destroy()
333 self.basis = petsc_dense_from_local_block(self.comm, np.ascontiguousarray(basis_local_block))
334 return self.basis
336 def _local_weighted_snapshot_weights(self, local_parametervector, weightfunction,
337 parameter_weights, n_nonzero_weights, rb_size,
338 cubic_cutoff=None):
339 """
340 Partition-of-unity weights over the accepted snapshots (one per column
341 of VT), from the distances between their parameter vectors and
342 `local_parametervector`. Duplicate-parameter (rejected) snapshots take
343 part in the distance weighting like accepted ones, and their weight is
344 then redistributed onto the accepted snapshots that reconstruct them.
346 Returns (weights, distances of the accepted snapshots).
347 """
348 sm = self.snapshot_matrix
349 sm.construct_snapshot_parameter_array()
350 n_snapshots = sm.n_snapshots
351 rejected = sm.rejected_snapshot_parameters
352 n_rej = len(rejected)
353 params = sm.snapshot_parameter_array
354 if n_rej > 0:
355 params = np.vstack([params, np.vstack(rejected)])
357 parameter_distance = params - local_parametervector[None, :]
358 if parameter_weights is not None:
359 parameter_distance = parameter_distance * np.asarray(parameter_weights)[None, :]
360 parameter_distnorm = np.linalg.norm(parameter_distance, ord=2, axis=1)
361 min_dist = parameter_distnorm.min()
362 max_dist = parameter_distnorm.max()
364 # Every weighting divides by the spread of the distances; with none
365 # (all snapshots equidistant, e.g. two symmetric perturbations) they
366 # all reduce to uniform weights.
367 if (max_dist - min_dist) <= 1e-12 * max(max_dist, 1.0):
368 PETSc.Sys.Print("All snapshots are equidistant from the query point (distance {:.6e}); "
369 "using uniform snapshot weights.".format(min_dist))
370 snapshot_weights = np.ones_like(parameter_distnorm)
371 elif weightfunction == "Cubic":
372 if cubic_cutoff is not None:
373 # c sets the support radius, but never below the radius that
374 # gives the rb_size nearest accepted snapshots a nonzero weight:
375 # the basis then always has rb_size columns (when the snapshots
376 # span that many directions), and c can only widen the set of
377 # snapshots that contribute
378 dist_hat_c = cubic_dist_hat_fixed(min_dist, max_dist, cubic_cutoff)
379 dist_floor = basis_size_floor(parameter_distnorm[:n_snapshots], rb_size,
380 max_dist - min_dist)
381 dist_hat = max(dist_hat_c, dist_floor)
382 PETSc.Sys.Print("cubic support radius: c = {} gives {:.6e}, rb_size = {} needs {:.6e}; "
383 "using {:.6e}".format(cubic_cutoff, dist_hat_c, rb_size, dist_floor, dist_hat))
384 else:
385 k_target = rb_size if n_nonzero_weights is None else n_nonzero_weights
386 dist_hat = cubic_dist_hat(parameter_distnorm, min_dist, max_dist, k_target)
387 snapshot_weights = cubic_weights(parameter_distnorm, min_dist, dist_hat)
388 elif weightfunction == "Linear":
389 avg_dist = (3 * min_dist + max_dist) / 4
390 snapshot_weights = avg_dist / (avg_dist - min_dist) - parameter_distnorm / (avg_dist - min_dist)
391 snapshot_weights[snapshot_weights < 0.] = 0.
392 elif weightfunction == "Quadratic":
393 denominator = max_dist**2 - min_dist**2
394 snapshot_weights = max_dist**2 / denominator - parameter_distnorm**2 / denominator
395 elif weightfunction == "InverseDistance":
396 inv = 1.0 / parameter_distnorm**2
397 snapshot_weights = inv / inv.max()
398 else:
399 raise ValueError("weightfunction must be one of {}, got {!r}".format(
400 self.WEIGHT_FUNCTIONS, weightfunction))
402 w_acc = snapshot_weights[:n_snapshots]
403 if n_rej > 0:
404 D = np.zeros((n_snapshots, n_rej))
405 for j, d in enumerate(sm.rejected_snapshot_reconstruction_weights):
406 D[:d.shape[0], j] = d
407 w_acc = w_acc + D @ snapshot_weights[n_snapshots:]
408 # clip after redistribution: negative reconstruction weights can
409 # push an accepted snapshot's total below zero
410 w_acc[w_acc < 0.] = 0.
412 return w_acc / np.sum(w_acc), parameter_distnorm[:n_snapshots]
414 def construct_basis(self, rb_size=10, local_parametervector=None, parameter_weights=None,
415 weightfunction='Cubic', n_nonzero_weights=None, cubic_cutoff=None,
416 return_distance_norms=False, layout_vec=None):
417 """
418 (Re)build self.basis from the current snapshot SVD; with no snapshots
419 self.basis becomes None.
421 local_weighted only::
423 local_parametervector the current design point
424 parameter_weights per-parameter factors on the distance (None: 1)
425 weightfunction 'Cubic' (compact support), 'Linear',
426 'Quadratic' or 'InverseDistance'
427 n_nonzero_weights Cubic: number of nearest snapshots with a
428 nonzero weight (default rb_size)
429 cubic_cutoff Cubic: instead of n_nonzero_weights, a
430 c in [0, 1) with support radius
431 max(c min_dist + (1 - c) max_dist, r_rb),
432 r_rb the smallest radius that leaves the
433 rb_size nearest accepted snapshots a nonzero
434 weight (utils/pod_utils.basis_size_floor)
436 `layout_vec` (the state vector) guards the one invariant the reduced
437 solve depends on: the basis rows are distributed exactly like it.
438 Returns the accepted snapshots' distances with return_distance_norms
439 (local_weighted), else None.
440 """
441 U, Sig_vec, VT = self.snapshot_matrix.full_SVD_triplet
442 n_snapshots = self.snapshot_matrix.n_snapshots
443 basis_rank = self.snapshot_matrix.rank
445 if layout_vec is not None and U is not None and U.getLocalSize()[0] != layout_vec.getLocalSize():
446 raise ValueError("POD basis row layout ({} local rows) does not match the state vector "
447 "layout ({} local rows)".format(U.getLocalSize()[0], layout_vec.getLocalSize()))
449 if n_snapshots == 0 or U is None:
450 if self.basis is not None:
451 self.basis.destroy()
452 self.basis = None
453 self.mean = None
454 return None
456 parameter_distnorm = None
457 if self.basis_mode == 'global' or n_snapshots < 2:
458 # bounded by the rank, not n_snapshots: a rank-deficient snapshot
459 # adds a column to R/VT but none to U
460 self._set_basis_from_local_block(U.getDenseArray()[:, :min(basis_rank, rb_size)])
461 else:
462 if local_parametervector is None:
463 raise ValueError("the local_weighted basis needs local_parametervector")
464 weights, parameter_distnorm = self._local_weighted_snapshot_weights(
465 local_parametervector, weightfunction, parameter_weights, n_nonzero_weights, rb_size,
466 cubic_cutoff=cubic_cutoff)
467 PETSc.Sys.Print("snapshot weights: {}".format(weights))
469 # diagonal weight matrix restricted to the columns above 1e-10
470 valid_idxs = np.where(weights >= 1e-10)[0]
471 num_rows, num_cols = len(weights), len(valid_idxs)
472 weight_mat = np.zeros((num_rows, num_cols))
473 weight_mat[valid_idxs, np.arange(num_cols)] = weights[valid_idxs]
475 Sig_np = np.asarray(Sig_vec.getArray()).ravel()
476 VT_np = VT.getDenseArray()
477 if self.subtract_mean:
478 # centring is the rank-1 right factor H = I - c 1^T: S_c = S H,
479 # so H W = W - c (1^T W), and the mean S c = U (Sig * (VT c))
480 c_vec = weights.copy() if self.mean_weighting == 'snapshot_weights' \
481 else np.full(num_rows, 1.0 / max(num_rows, 1))
482 if self.mean is not None:
483 self.mean.destroy()
484 self.mean = U.createVecLeft()
485 self.mean.getArray()[:] = U.getDenseArray() @ (Sig_np * (VT_np @ c_vec))
486 self.mean.assemble()
487 weight_mat = weight_mat - np.outer(c_vec, weight_mat.sum(axis=0))
489 # SVD of diag(Sig) VT W: small (rank x num_cols), replicated, so dense scipy
490 t0 = perf_counter()
491 U_tilde_np, _, _ = sp_la.svd(Sig_np[:, None] * (VT_np @ weight_mat), full_matrices=False)
492 t1 = perf_counter()
494 # the basis is U U_tilde truncated to rb_size; truncate U_tilde first
495 # so the intermediate never exceeds rb_size columns
496 k = min(rb_size, U_tilde_np.shape[1])
497 self._set_basis_from_local_block(U.getDenseArray() @ U_tilde_np[:, :k])
498 PETSc.Sys.Print("local_weighted basis: {} of {} snapshots carry nonzero weight -> {} basis "
499 "vectors (rb_size = {}); small SVD time {}".format(
500 num_cols, n_snapshots, k, rb_size, t1 - t0))
502 PETSc.Sys.Print("Computed POD basis: {} rows (global) x {} columns".format(*self.basis.getSize()))
503 return parameter_distnorm if return_distance_norms else None
505 def destroy(self):
506 if self.basis is not None:
507 self.basis.destroy()
508 self.basis = None
509 if self.mean is not None:
510 self.mean.destroy()
511 self.mean = None
512 self.snapshot_matrix.destroy()