pyMOTO 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/METADATA +7 -8
- pyMOTO-1.5.0.dist-info/RECORD +29 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +17 -11
- pymoto/common/domain.py +61 -5
- pymoto/common/dyadcarrier.py +87 -29
- pymoto/common/mma.py +142 -129
- pymoto/core_objects.py +129 -117
- pymoto/modules/aggregation.py +209 -0
- pymoto/modules/assembly.py +250 -10
- pymoto/modules/complex.py +3 -3
- pymoto/modules/filter.py +171 -24
- pymoto/modules/generic.py +12 -1
- pymoto/modules/io.py +85 -12
- pymoto/modules/linalg.py +92 -120
- pymoto/modules/scaling.py +5 -4
- pymoto/routines.py +34 -9
- pymoto/solvers/__init__.py +14 -0
- pymoto/solvers/auto_determine.py +108 -0
- pymoto/{common/solvers_dense.py → solvers/dense.py} +90 -70
- pymoto/solvers/iterative.py +361 -0
- pymoto/solvers/matrix_checks.py +60 -0
- pymoto/solvers/solvers.py +253 -0
- pymoto/{common/solvers_sparse.py → solvers/sparse.py} +42 -29
- pyMOTO-1.3.0.dist-info/RECORD +0 -24
- pymoto/common/solvers.py +0 -236
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/LICENSE +0 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/zip-safe +0 -0
pymoto/modules/io.py
CHANGED
@@ -14,7 +14,7 @@ from pymoto import Module
|
|
14
14
|
from .assembly import DomainDefinition
|
15
15
|
|
16
16
|
|
17
|
-
class
|
17
|
+
class FigModule(Module):
|
18
18
|
""" Abstract base class for any module which produces a figure
|
19
19
|
|
20
20
|
Keyword Args:
|
@@ -55,7 +55,7 @@ class _FigModule(Module):
|
|
55
55
|
plt.close(self.fig)
|
56
56
|
|
57
57
|
|
58
|
-
class PlotDomain(
|
58
|
+
class PlotDomain(FigModule):
|
59
59
|
""" Plots the densities of a domain (2D or 3D)
|
60
60
|
|
61
61
|
Input Signal:
|
@@ -96,10 +96,16 @@ class PlotDomain(_FigModule):
|
|
96
96
|
self.im.set_data(data)
|
97
97
|
else:
|
98
98
|
ax = self.fig.add_subplot(111)
|
99
|
-
|
99
|
+
Lx = self.domain.nelx * self.domain.unitx
|
100
|
+
Ly = self.domain.nely * self.domain.unity
|
101
|
+
self.im = ax.imshow(data, cmap=self.cmap, origin='lower', extent=(0.0, Lx, 0.0, Ly))
|
100
102
|
self.cbar = self.fig.colorbar(self.im, orientation='horizontal')
|
101
103
|
ax.set(xlabel='x', ylabel='y')
|
102
|
-
|
104
|
+
vmin, vmax = np.min(data), np.max(data)
|
105
|
+
if vmin < 0:
|
106
|
+
vabs = max(abs(vmin), abs(vmax))
|
107
|
+
vmin, vmax = -vabs, vabs
|
108
|
+
clim = [vmin, vmax] if self.clim is None else self.clim
|
103
109
|
self.im.set_clim(vmin=clim[0], vmax=clim[1])
|
104
110
|
|
105
111
|
def _plot_3d(self, x):
|
@@ -138,7 +144,7 @@ class PlotDomain(_FigModule):
|
|
138
144
|
self.fac = ax.voxels(sel, facecolors=colors, linewidth=0.5, edgecolors='k')
|
139
145
|
|
140
146
|
|
141
|
-
class PlotGraph(
|
147
|
+
class PlotGraph(FigModule):
|
142
148
|
""" Plot an X-Y graph
|
143
149
|
|
144
150
|
Input Signals:
|
@@ -175,14 +181,15 @@ class PlotGraph(_FigModule):
|
|
175
181
|
for i, y in enumerate(ys):
|
176
182
|
self.line[i].set_xdata(x)
|
177
183
|
self.line[i].set_ydata(y)
|
178
|
-
ymin, ymax = min(ymin, min(y)), max(ymax, max(y))
|
179
|
-
self.ax.set_xlim([min(x), max(x)])
|
180
|
-
|
184
|
+
ymin, ymax = min(ymin, np.min(y)), max(ymax, np.max(y))
|
185
|
+
self.ax.set_xlim([np.min(x), np.max(x)])
|
186
|
+
dy = ymax - ymin
|
187
|
+
self.ax.set_ylim([ymin-0.05*dy, ymax+0.05*dy])
|
181
188
|
|
182
189
|
self._update_fig()
|
183
190
|
|
184
191
|
|
185
|
-
class PlotIter(
|
192
|
+
class PlotIter(FigModule):
|
186
193
|
""" Plot iteration history of one or more variables
|
187
194
|
|
188
195
|
Input Signals:
|
@@ -193,10 +200,12 @@ class PlotIter(_FigModule):
|
|
193
200
|
overwrite (bool): Overwrite saved image every time the figure is updated, else prefix ``_0000`` is added to the
|
194
201
|
filename (default = ``False``)
|
195
202
|
show (bool): Show the figure on the screen
|
203
|
+
ylim: Provide y-axis limits for the plot
|
196
204
|
"""
|
197
|
-
def _prepare(self):
|
205
|
+
def _prepare(self, ylim=None):
|
198
206
|
self.minlim = 1e+200
|
199
207
|
self.maxlim = -1e+200
|
208
|
+
self.ylim = ylim
|
200
209
|
|
201
210
|
def _response(self, *args):
|
202
211
|
if not hasattr(self, 'ax'):
|
@@ -227,7 +236,9 @@ class PlotIter(_FigModule):
|
|
227
236
|
dy = max((self.maxlim - self.minlim)*0.05, sys.float_info.min)
|
228
237
|
|
229
238
|
self.ax.set_xlim([-0.5, self.iter+0.5])
|
230
|
-
if
|
239
|
+
if self.ylim is not None:
|
240
|
+
self.ax.set_ylim(self.ylim)
|
241
|
+
elif np.isfinite(self.minlim) and np.isfinite(self.maxlim):
|
231
242
|
self.ax.set_ylim([self.minlim - dy, self.maxlim + dy])
|
232
243
|
|
233
244
|
self._update_fig()
|
@@ -244,7 +255,7 @@ class WriteToVTI(Module):
|
|
244
255
|
accepted, which get the suffixed as ``_00``.
|
245
256
|
|
246
257
|
Input Signals:
|
247
|
-
- ``*args`` (`numpy.
|
258
|
+
- ``*args`` (`numpy.ndarray`): Vectors to write to VTI. The signal tags are used as name.
|
248
259
|
|
249
260
|
Args:
|
250
261
|
domain: The domain layout
|
@@ -271,3 +282,65 @@ class WriteToVTI(Module):
|
|
271
282
|
filen = pth[0] + '.{0:04d}'.format(self.iter) + pth[1]
|
272
283
|
self.domain.write_to_vti(data, filename=filen, scale=self.scale)
|
273
284
|
self.iter += 1
|
285
|
+
|
286
|
+
|
287
|
+
class ScalarToFile(Module):
|
288
|
+
""" Writes iteration data to a log file
|
289
|
+
|
290
|
+
This function can also handle small vectors of scalars, i.e. eigenfrequencies or multiple constraints.
|
291
|
+
|
292
|
+
Input Signals:
|
293
|
+
- ``*args`` (`Numeric` or `np.ndarray`): Values to write to file. The signal tags are used as name.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
saveto: Location to save the log file, supports .txt or .csv
|
297
|
+
fmt (optional): Value format (e.g. 'e', 'f', '.3e', '.5g', '.3f')
|
298
|
+
separator (optional): Value separator, .csv files will automatically use a comma
|
299
|
+
"""
|
300
|
+
def _prepare(self, saveto: str, fmt: str = '.10e', separator: str = '\t'):
|
301
|
+
self.saveto = saveto
|
302
|
+
Path(saveto).parent.mkdir(parents=True, exist_ok=True)
|
303
|
+
self.iter = 0
|
304
|
+
|
305
|
+
# Test the format
|
306
|
+
3.14.__format__(fmt)
|
307
|
+
self.format = fmt
|
308
|
+
|
309
|
+
self.separator = "," if ".csv" in self.saveto else separator
|
310
|
+
|
311
|
+
def _response(self, *args):
|
312
|
+
tags = [] if self.iter == 0 else None
|
313
|
+
|
314
|
+
# Add iteration as first column
|
315
|
+
dat = [self.iter.__format__('d')]
|
316
|
+
if tags is not None:
|
317
|
+
tags.append('Iteration')
|
318
|
+
|
319
|
+
# Add all signals
|
320
|
+
for s in self.sig_in:
|
321
|
+
if np.size(np.asarray(s.state)) > 1:
|
322
|
+
it = np.nditer(s.state, flags=['multi_index'])
|
323
|
+
while not it.finished:
|
324
|
+
dat.append(it.value.__format__(self.format))
|
325
|
+
if tags is not None:
|
326
|
+
tags.append(f"{s.tag}{list(it.multi_index)}")
|
327
|
+
it.iternext()
|
328
|
+
else:
|
329
|
+
dat.append(s.state.__format__(self.format))
|
330
|
+
if tags is not None:
|
331
|
+
tags.append(s.tag)
|
332
|
+
|
333
|
+
# Write to file
|
334
|
+
if tags is not None:
|
335
|
+
assert len(tags) == len(dat)
|
336
|
+
with open(self.saveto, "w+") as f:
|
337
|
+
# Write header line
|
338
|
+
f.write(self.separator.join(tags))
|
339
|
+
f.write("\n")
|
340
|
+
|
341
|
+
with open(self.saveto, "a+") as f:
|
342
|
+
# Write data
|
343
|
+
f.write(self.separator.join(dat))
|
344
|
+
f.write("\n")
|
345
|
+
|
346
|
+
self.iter += 1
|
pymoto/modules/linalg.py
CHANGED
@@ -7,10 +7,9 @@ import scipy.linalg as spla # Dense matrix solvers
|
|
7
7
|
import scipy.sparse as sps
|
8
8
|
import scipy.sparse.linalg as spsla
|
9
9
|
|
10
|
-
from pymoto import Signal, Module, DyadCarrier
|
11
|
-
from pymoto import
|
12
|
-
from pymoto import
|
13
|
-
from pymoto import matrix_is_symmetric, matrix_is_hermitian, matrix_is_diagonal
|
10
|
+
from pymoto import Signal, Module, DyadCarrier
|
11
|
+
from pymoto.solvers import auto_determine_solver
|
12
|
+
from pymoto.solvers import matrix_is_sparse, matrix_is_complex, matrix_is_hermitian, LDAWrapper
|
14
13
|
|
15
14
|
|
16
15
|
class StaticCondensation(Module):
|
@@ -172,7 +171,7 @@ class SystemOfEquations(Module):
|
|
172
171
|
adjoint_load += self.Afp * dgdb[self.p, ...]
|
173
172
|
|
174
173
|
lam = np.zeros_like(self.x)
|
175
|
-
lamf = -1.0 * self.module_LinSolve.solver.
|
174
|
+
lamf = -1.0 * self.module_LinSolve.solver.solve(adjoint_load, trans='T')
|
176
175
|
lam[self.f, ...] = lamf
|
177
176
|
|
178
177
|
if dgdb is not None:
|
@@ -219,105 +218,6 @@ class Inverse(Module):
|
|
219
218
|
return dA if np.iscomplexobj(A) else np.real(dA)
|
220
219
|
|
221
220
|
|
222
|
-
# flake8: noqa: C901
|
223
|
-
def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertriangular=None,
|
224
|
-
ishermitian=None, issymmetric=None, ispositivedefinite=None):
|
225
|
-
"""
|
226
|
-
Uses parts of Matlab's scheme https://nl.mathworks.com/help/matlab/ref/mldivide.html
|
227
|
-
:param A: The matrix
|
228
|
-
:param isdiagonal: Manual override for diagonal matrix
|
229
|
-
:param islowertriangular: Override for lower triangular matrix
|
230
|
-
:param isuppertriangular: Override for upper triangular matrix
|
231
|
-
:param ishermitian: Override for hermitian matrix (prevents check)
|
232
|
-
:param issymmetric: Override for symmetric matrix (prevents check). Is the same as hermitian for a real matrix
|
233
|
-
:param ispositivedefinite: Manual override for positive definiteness
|
234
|
-
:return: LinearSolver which should be 'best' for the matrix
|
235
|
-
"""
|
236
|
-
issparse = sps.issparse(A) # Check if the matrix is sparse
|
237
|
-
issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
|
238
|
-
|
239
|
-
if not issquare:
|
240
|
-
if issparse:
|
241
|
-
sps.SparseEfficiencyWarning("Only a dense version of QR solver is available") # TODO
|
242
|
-
return SolverDenseQR()
|
243
|
-
|
244
|
-
# l_bw, u_bw = spla.bandwidth(A) # TODO Get bandwidth (implemented in scipy version > 1.8.0)
|
245
|
-
|
246
|
-
if isdiagonal is None: # Check if matrix is diagonal
|
247
|
-
# TODO: This could be improved to check other sparse matrix types as well
|
248
|
-
isdiagonal = matrix_is_diagonal(A)
|
249
|
-
if isdiagonal:
|
250
|
-
return SolverDiagonal()
|
251
|
-
|
252
|
-
# Check if the matrix is triangular
|
253
|
-
# TODO Currently only for dense matrices
|
254
|
-
if islowertriangular is None: # Check if matrix is lower triangular
|
255
|
-
islowertriangular = False if issparse else np.allclose(A, np.tril(A))
|
256
|
-
if islowertriangular:
|
257
|
-
warnings.WarningMessage("Lower triangular solver not implemented",
|
258
|
-
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
259
|
-
|
260
|
-
if isuppertriangular is None: # Check if matrix is upper triangular
|
261
|
-
isuppertriangular = False if issparse else np.allclose(A, np.triu(A))
|
262
|
-
if isuppertriangular:
|
263
|
-
warnings.WarningMessage("Upper triangular solver not implemented",
|
264
|
-
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
265
|
-
|
266
|
-
ispermutedtriangular = False
|
267
|
-
if ispermutedtriangular:
|
268
|
-
warnings.WarningMessage("Permuted triangular solver not implemented",
|
269
|
-
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
270
|
-
|
271
|
-
# Check if the matrix is complex-valued
|
272
|
-
iscomplex = np.iscomplexobj(A)
|
273
|
-
if iscomplex:
|
274
|
-
# Detect if the matrix is hermitian and/or symmetric
|
275
|
-
if ishermitian is None:
|
276
|
-
ishermitian = matrix_is_hermitian(A)
|
277
|
-
if issymmetric is None:
|
278
|
-
issymmetric = matrix_is_symmetric(A)
|
279
|
-
else:
|
280
|
-
if ishermitian is None and issymmetric is None:
|
281
|
-
# Detect if the matrix is symmetric
|
282
|
-
issymmetric = matrix_is_symmetric(A)
|
283
|
-
ishermitian = issymmetric
|
284
|
-
elif ishermitian is not None and issymmetric is not None:
|
285
|
-
assert ishermitian == issymmetric, "For real-valued matrices, symmetry and hermitian must be equal"
|
286
|
-
elif ishermitian is None:
|
287
|
-
ishermitian = issymmetric
|
288
|
-
elif issymmetric is None:
|
289
|
-
issymmetric = ishermitian
|
290
|
-
|
291
|
-
if issparse:
|
292
|
-
# Prefer Intel Pardiso solver as it can solve any matrix TODO: Check for complex matrix
|
293
|
-
if SolverSparsePardiso.defined and not iscomplex:
|
294
|
-
# TODO check for positive definiteness? np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
|
295
|
-
return SolverSparsePardiso(symmetric=issymmetric, hermitian=ishermitian, positive_definite=ispositivedefinite)
|
296
|
-
|
297
|
-
if ishermitian:
|
298
|
-
# Check if diagonal is all positive or all negative -> Cholesky
|
299
|
-
if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0): # TODO what about the complex case?
|
300
|
-
if SolverSparseCholeskyScikit.defined:
|
301
|
-
return SolverSparseCholeskyScikit()
|
302
|
-
if SolverSparseCholeskyCVXOPT.defined:
|
303
|
-
return SolverSparseCholeskyCVXOPT()
|
304
|
-
|
305
|
-
return SolverSparseLU() # Default to LU, which should be possible for any non-singular square matrix
|
306
|
-
|
307
|
-
else: # Dense branch
|
308
|
-
if ishermitian:
|
309
|
-
# Check if diagonal is all positive or all negative
|
310
|
-
if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0):
|
311
|
-
return SolverDenseCholesky()
|
312
|
-
else:
|
313
|
-
return SolverDenseLDL(hermitian=ishermitian)
|
314
|
-
elif issymmetric:
|
315
|
-
return SolverDenseLDL(hermitian=ishermitian)
|
316
|
-
else:
|
317
|
-
# TODO: Detect if the matrix is Hessenberg
|
318
|
-
return SolverDenseLU()
|
319
|
-
|
320
|
-
|
321
221
|
class LinSolve(Module):
|
322
222
|
r""" Solves linear system of equations :math:`\mathbf{A}\mathbf{x}=\mathbf{b}`
|
323
223
|
|
@@ -347,11 +247,12 @@ class LinSolve(Module):
|
|
347
247
|
self.ishermitian = hermitian
|
348
248
|
self.issymmetric = symmetric
|
349
249
|
self.solver = solver
|
250
|
+
self.u = None # Solution storage
|
350
251
|
|
351
252
|
def _response(self, mat, rhs):
|
352
253
|
# Do some detections on the matrix type
|
353
|
-
self.issparse =
|
354
|
-
self.iscomplex =
|
254
|
+
self.issparse = matrix_is_sparse(mat) # Check if it is a sparse matrix
|
255
|
+
self.iscomplex = matrix_is_complex(mat) # Check if it is a complex-valued matrix
|
355
256
|
if not self.iscomplex and self.issymmetric is not None:
|
356
257
|
self.ishermitian = self.issymmetric
|
357
258
|
if self.ishermitian is None:
|
@@ -365,19 +266,23 @@ class LinSolve(Module):
|
|
365
266
|
if self.solver is None:
|
366
267
|
self.solver = auto_determine_solver(mat, ishermitian=self.ishermitian)
|
367
268
|
if not isinstance(self.solver, LDAWrapper) and self.use_lda_solver:
|
368
|
-
|
269
|
+
lda_kwargs = dict(hermitian=self.ishermitian, symmetric=self.issymmetric)
|
270
|
+
if hasattr(self.solver, 'tol'):
|
271
|
+
lda_kwargs['tol'] = self.solver.tol * 2
|
272
|
+
self.solver = LDAWrapper(self.solver, **lda_kwargs)
|
369
273
|
|
370
274
|
# Update solver with new matrix
|
371
275
|
self.solver.update(mat)
|
372
276
|
|
373
277
|
# Solution
|
374
|
-
self.u = self.solver.solve(rhs)
|
278
|
+
self.u = self.solver.solve(rhs, x0=self.u)
|
375
279
|
|
376
280
|
return self.u
|
377
281
|
|
378
282
|
def _sensitivity(self, dfdv):
|
379
283
|
mat, rhs = [s.state for s in self.sig_in]
|
380
|
-
lam = self.solver.
|
284
|
+
# lam = self.solver.solve(dfdv.conj(), trans='H').conj()
|
285
|
+
lam = self.solver.solve(dfdv, trans='T')
|
381
286
|
|
382
287
|
if self.issparse:
|
383
288
|
if self.u.ndim > 1:
|
@@ -413,9 +318,6 @@ class EigenSolve(Module):
|
|
413
318
|
Mode tracking algorithms can be implemented by the user by providing the argument ``sorting_func``, which is a
|
414
319
|
function with arguments (``λ``, ``Q``).
|
415
320
|
|
416
|
-
Todo:
|
417
|
-
Support for sparse matrix
|
418
|
-
|
419
321
|
Input Signal(s):
|
420
322
|
- ``A`` (`dense matrix`): The system matrix of size ``(n, n)``
|
421
323
|
- ``B`` (`dense matrix, optional`): Second system matrix (must be positive-definite) of size ``(n, n)``
|
@@ -440,12 +342,15 @@ class EigenSolve(Module):
|
|
440
342
|
self.sigma = sigma
|
441
343
|
self.mode = mode
|
442
344
|
self.Ainv = None
|
345
|
+
self.do_solve = False
|
346
|
+
self.adjoint_solvers_need_update = True
|
443
347
|
|
444
348
|
def _response(self, A, *args):
|
445
349
|
B = args[0] if len(args) > 0 else None
|
446
350
|
if self.is_hermitian is None:
|
447
351
|
self.is_hermitian = (matrix_is_hermitian(A) and (B is None or matrix_is_hermitian(B)))
|
448
|
-
self.is_sparse =
|
352
|
+
self.is_sparse = matrix_is_sparse(A) and (B is None or matrix_is_sparse(B))
|
353
|
+
self.adjoint_solvers_need_update = True
|
449
354
|
|
450
355
|
# Solve the eigenvalue problem
|
451
356
|
if self.is_sparse:
|
@@ -458,12 +363,18 @@ class EigenSolve(Module):
|
|
458
363
|
W = W[isort]
|
459
364
|
Q = Q[:, isort]
|
460
365
|
|
461
|
-
# Normalize the eigenvectors
|
366
|
+
# Normalize the eigenvectors with the following conditions:
|
367
|
+
# 1) Flip sign such that the average (real) value is positive
|
368
|
+
# 2) Normalize the eigenvector v⋅v or v⋅Bv to unity
|
462
369
|
for i in range(W.size):
|
463
370
|
qi, wi = Q[:, i], W[i]
|
464
|
-
qi *= np.sign(np.real(qi[np.argmax(abs(qi) > 0)])) # Set first value positive for orientation
|
465
371
|
Bqi = qi if B is None else B@qi
|
466
|
-
|
372
|
+
|
373
|
+
normval = np.sqrt(qi @ Bqi)
|
374
|
+
avgval = np.average(qi)/normval
|
375
|
+
|
376
|
+
sf = np.sign(np.real(avgval)) / normval
|
377
|
+
qi *= sf
|
467
378
|
return W, Q
|
468
379
|
|
469
380
|
def _sensitivity(self, dW, dQ):
|
@@ -474,7 +385,8 @@ class EigenSolve(Module):
|
|
474
385
|
dA, dB = self._dense_sens(A, B, dW, dQ)
|
475
386
|
else:
|
476
387
|
if dQ is not None:
|
477
|
-
raise NotImplementedError('Sparse eigenvector sensitivities not implemented')
|
388
|
+
# raise NotImplementedError('Sparse eigenvector sensitivities not implemented')
|
389
|
+
dA, dB = self._sparse_eigvec_sens(A, B, dW, dQ)
|
478
390
|
elif dW is not None:
|
479
391
|
dA, dB = self._sparse_eigval_sens(A, B, dW)
|
480
392
|
|
@@ -483,7 +395,6 @@ class EigenSolve(Module):
|
|
483
395
|
elif len(self.sig_in) == 2:
|
484
396
|
return dA, dB
|
485
397
|
|
486
|
-
|
487
398
|
def _sparse_eigs(self, A, B=None):
|
488
399
|
if self.nmodes is None:
|
489
400
|
self.nmodes = 6
|
@@ -500,9 +411,14 @@ class EigenSolve(Module):
|
|
500
411
|
# Use shift-and-invert, so make inverse operator
|
501
412
|
if self.Ainv is None:
|
502
413
|
self.Ainv = auto_determine_solver(mat_shifted, ishermitian=self.is_hermitian)
|
503
|
-
|
414
|
+
self.do_solve = True
|
415
|
+
if self.sigma != 0:
|
416
|
+
self.do_solve = True
|
417
|
+
if self.do_solve:
|
418
|
+
self.Ainv.update(mat_shifted)
|
504
419
|
|
505
|
-
AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve,
|
420
|
+
AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve,
|
421
|
+
rmatvec=lambda b: self.Ainv.solve(b, trans='H'))
|
506
422
|
|
507
423
|
if self.is_hermitian:
|
508
424
|
return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma, mode=self.mode)
|
@@ -562,3 +478,59 @@ class EigenSolve(Module):
|
|
562
478
|
dB -= DyadCarrier(dB_u, qi)
|
563
479
|
return dA, dB
|
564
480
|
|
481
|
+
def _sparse_eigvec_sens(self, A, B, dW, dQ):
|
482
|
+
""" Calculate eigenvector sensitivities for a sparse eigenvalue problem
|
483
|
+
References:
|
484
|
+
Delissen (2022), Topology optimization for dynamic and controlled systems,
|
485
|
+
doi: https://doi.org/10.4233/uuid:c9ed8f61-efe1-4dc8-bb56-e353546cf247
|
486
|
+
|
487
|
+
Args:
|
488
|
+
A: System matrix
|
489
|
+
B: Mass matrix
|
490
|
+
dW: Adjoint eigenvalue sensitivities
|
491
|
+
dQ: Adjoint eigenvector sensitivities
|
492
|
+
|
493
|
+
Returns:
|
494
|
+
dA: Adjoint system matrix sensitivities
|
495
|
+
dB: Adjoint mass matrix sensitivities
|
496
|
+
"""
|
497
|
+
if dQ is None:
|
498
|
+
return self._sparse_eigval_sens(A, B, dW)
|
499
|
+
W, Q = [s.state for s in self.sig_out]
|
500
|
+
if dW is not None:
|
501
|
+
dA, dB = self._sparse_eigval_sens(A, B, dW)
|
502
|
+
else:
|
503
|
+
dA, dB = DyadCarrier(), None if B is None else DyadCarrier()
|
504
|
+
for i in range(W.size):
|
505
|
+
phi = Q[:, i]
|
506
|
+
dphi = dQ[:, i]
|
507
|
+
if dphi.min() == dphi.max() == 0.0:
|
508
|
+
continue
|
509
|
+
lam = W[i]
|
510
|
+
|
511
|
+
alpha = - phi @ dphi
|
512
|
+
r = dphi + alpha * B.T @ phi
|
513
|
+
|
514
|
+
# Solve particular solution
|
515
|
+
if self.adjoint_solvers_need_update or self.solvers[i] is None:
|
516
|
+
Z = A - lam * B
|
517
|
+
if not hasattr(self, 'solvers'):
|
518
|
+
self.solvers = [None for _ in range(W.size)]
|
519
|
+
if self.solvers[i] is None: # Solver must be able to solve indefinite system
|
520
|
+
self.solvers[i] = auto_determine_solver(Z, ispositivedefinite=False)
|
521
|
+
if self.adjoint_solvers_need_update:
|
522
|
+
self.solvers[i].update(Z)
|
523
|
+
|
524
|
+
vp = self.solvers[i].solve(r, trans='T')
|
525
|
+
|
526
|
+
# Calculate total ajoint by adding homogeneous solution
|
527
|
+
c = - vp @ B @ phi
|
528
|
+
v = vp + c * phi
|
529
|
+
|
530
|
+
# Add to mass and stiffness matrix
|
531
|
+
dAi = - DyadCarrier(v, phi)
|
532
|
+
dA += np.real(dAi) if np.isrealobj(A) else dAi
|
533
|
+
if B is not None:
|
534
|
+
dBi = DyadCarrier(alpha / 2 * phi + lam * v, phi)
|
535
|
+
dB += np.real(dBi) if np.isrealobj(B) else dBi
|
536
|
+
return dA, dB
|
pymoto/modules/scaling.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
from pymoto import Module
|
2
|
+
import numpy as np
|
2
3
|
|
3
4
|
|
4
5
|
class Scaling(Module):
|
5
6
|
r""" Scales (scalar) input for different response functions in optimization (objective / constraints).
|
6
7
|
This is useful, for instance, for MMA where the objective must be scaled in a certain way for good convergence.
|
7
8
|
|
8
|
-
Objective scaling (`minval` and `maxval` are both undefined):
|
9
|
-
:math:`y^{(i)} = s \frac{x^{(i)}}{x^{(0)}}`
|
9
|
+
Objective scaling using absolute value or vector norm (`minval` and `maxval` are both undefined):
|
10
|
+
:math:`y^{(i)} = s \frac{x^{(i)}}{||x^{(0)}||}`
|
10
11
|
|
11
12
|
For the constraints, the negative null-form convention is used, which means the constraint is :math:`y(x) \leq 0`.
|
12
13
|
|
@@ -25,7 +26,7 @@ class Scaling(Module):
|
|
25
26
|
Keyword Args:
|
26
27
|
scaling: Value :math:`s` to scale with
|
27
28
|
minval: Minimum value :math:`x_\text{min}` for negative-null-form constraint
|
28
|
-
|
29
|
+
maxval: Maximum value :math:`x_\text{max}` for negative-null-form constraint
|
29
30
|
"""
|
30
31
|
def _prepare(self, scaling: float = 100.0, minval: float = None, maxval: float = None):
|
31
32
|
self.minval = minval
|
@@ -39,7 +40,7 @@ class Scaling(Module):
|
|
39
40
|
|
40
41
|
def _response(self, x):
|
41
42
|
if not hasattr(self, 'sf'):
|
42
|
-
self.sf = self.scaling/x
|
43
|
+
self.sf = self.scaling/np.linalg.norm(x)
|
43
44
|
if self.minval is not None:
|
44
45
|
g = 1 - x/self.minval
|
45
46
|
elif self.maxval is not None:
|
pymoto/routines.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
import warnings
|
1
2
|
import numpy as np
|
2
3
|
from .utils import _parse_to_list, _concatenate_to_array
|
3
4
|
from .core_objects import Signal, SignalSlice, Module, Network
|
4
5
|
from .common.mma import MMA
|
5
6
|
from typing import List, Iterable, Union, Callable
|
7
|
+
from scipy.sparse import issparse
|
6
8
|
|
7
9
|
|
8
10
|
def _has_signal_overlap(sig1: List[Signal], sig2: List[Signal]):
|
@@ -84,15 +86,15 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
84
86
|
blk.response()
|
85
87
|
|
86
88
|
print("Outputs:")
|
87
|
-
if
|
89
|
+
if verbose:
|
90
|
+
[print("{}\t{} = {}".format(i, s.tag, s.state)) for i, s in enumerate(outps)]
|
91
|
+
else:
|
88
92
|
print(", ".join([s.tag for s in outps]))
|
89
93
|
|
90
94
|
# Get analytical response and sensitivities, by looping over all outputs
|
91
95
|
for Iout, Sout in enumerate(outps):
|
92
96
|
# Obtain the output state
|
93
97
|
output = Sout.state
|
94
|
-
if verbose:
|
95
|
-
print("{}\t{} = {}".format(Iout, Sout.tag, output))
|
96
98
|
|
97
99
|
# Store the output value
|
98
100
|
f0[Iout] = (output.copy() if hasattr(output, "copy") else output)
|
@@ -100,6 +102,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
100
102
|
# Get the output state shape
|
101
103
|
shape = (output.shape if hasattr(output, "shape") else ())
|
102
104
|
|
105
|
+
if output is None:
|
106
|
+
warnings.warn(f"Output {Iout} of {Sout.tag} is None")
|
107
|
+
continue
|
108
|
+
|
103
109
|
# Generate a (random) sensitivity for output signal
|
104
110
|
if use_df is not None:
|
105
111
|
df_an[Iout] = use_df[Iout]
|
@@ -169,9 +175,15 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
169
175
|
for Iout, Sout in enumerate(outps):
|
170
176
|
# Obtain perturbed response
|
171
177
|
fp = Sout.state
|
178
|
+
if fp is None:
|
179
|
+
warnings.warn(f"Output {Iout} of {Sout.tag} is None")
|
180
|
+
continue
|
172
181
|
|
173
182
|
# Finite difference sensitivity
|
174
|
-
|
183
|
+
if issparse(fp):
|
184
|
+
df = (fp.toarray() - f0[Iout].toarray()) / (dx * sf)
|
185
|
+
else:
|
186
|
+
df = (fp - f0[Iout])/(dx*sf)
|
175
187
|
|
176
188
|
dgdx_fd = np.real(np.sum(df*df_an[Iout]))
|
177
189
|
|
@@ -183,7 +195,7 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
183
195
|
else:
|
184
196
|
dgdx_an = 0.0
|
185
197
|
|
186
|
-
if abs(dgdx_an)
|
198
|
+
if abs(dgdx_an) == 0:
|
187
199
|
error = abs(dgdx_fd - dgdx_an)
|
188
200
|
else:
|
189
201
|
error = abs(dgdx_fd - dgdx_an)/max(abs(dgdx_fd), abs(dgdx_an))
|
@@ -222,8 +234,15 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
222
234
|
# Obtain perturbed response
|
223
235
|
fp = Sout.state
|
224
236
|
|
237
|
+
if fp is None:
|
238
|
+
warnings.warn(f"Output {Iout} of {Sout.tag} is None")
|
239
|
+
continue
|
240
|
+
|
225
241
|
# Finite difference sensitivity
|
226
|
-
|
242
|
+
if issparse(fp):
|
243
|
+
df = (fp.toarray() - f0[Iout].toarray()) / (dx * 1j * sf)
|
244
|
+
else:
|
245
|
+
df = (fp - f0[Iout])/(dx*1j*sf)
|
227
246
|
dgdx_fd = np.imag(np.sum(df*df_an[Iout]))
|
228
247
|
|
229
248
|
if dx_an[Iout][Iin] is not None:
|
@@ -335,8 +354,9 @@ def minimize_oc(function, variables, objective: Signal,
|
|
335
354
|
function.sensitivity()
|
336
355
|
dfdx, _ = _concatenate_to_array(obtain_sensitivities(variables))
|
337
356
|
maxdfdx = max(dfdx)
|
338
|
-
if maxdfdx >
|
339
|
-
|
357
|
+
if maxdfdx > 1e-15:
|
358
|
+
warnings.warn(f"OC only works for negative sensitivities: max(dfdx) = {maxdfdx}. Clipping positive values.")
|
359
|
+
dfdx = np.minimum(dfdx, 0)
|
340
360
|
|
341
361
|
# Do OC update
|
342
362
|
l1, l2 = l1init, l2init
|
@@ -374,7 +394,12 @@ def minimize_mma(function, variables, responses, **kwargs):
|
|
374
394
|
move: Move limit on relative variable change per iteration
|
375
395
|
xmin: Minimum design variable (can be a vector)
|
376
396
|
xmax: Maximum design variable (can be a vector)
|
377
|
-
verbosity:
|
397
|
+
verbosity: Level of information to print
|
398
|
+
0 - No prints
|
399
|
+
1 - Only convergence message
|
400
|
+
2 - Convergence and iteration info (default)
|
401
|
+
3 - Additional info on variables
|
402
|
+
4 - Additional info on sensitivity information
|
378
403
|
|
379
404
|
"""
|
380
405
|
# Save initial state
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from .solvers import LinearSolver, LDAWrapper
|
2
|
+
from .matrix_checks import matrix_is_sparse, matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian
|
3
|
+
from .dense import SolverDiagonal, SolverDenseQR, SolverDenseLU, SolverDenseCholesky, SolverDenseLDL
|
4
|
+
from .sparse import SolverSparsePardiso, SolverSparseLU, SolverSparseCholeskyScikit, SolverSparseCholeskyCVXOPT
|
5
|
+
from .iterative import Preconditioner, CG, DampedJacobi, SOR, ILU, GeometricMultigrid
|
6
|
+
from .auto_determine import auto_determine_solver
|
7
|
+
|
8
|
+
__all__ = ['matrix_is_sparse', 'matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
|
9
|
+
'LinearSolver', 'LDAWrapper',
|
10
|
+
'SolverDiagonal', 'SolverDenseQR', 'SolverDenseLU', 'SolverDenseCholesky', 'SolverDenseLDL',
|
11
|
+
'SolverSparsePardiso', 'SolverSparseLU', 'SolverSparseCholeskyScikit', 'SolverSparseCholeskyCVXOPT',
|
12
|
+
'Preconditioner', 'CG', 'DampedJacobi', 'SOR', 'ILU', 'GeometricMultigrid',
|
13
|
+
'auto_determine_solver',
|
14
|
+
]
|