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/modules/io.py CHANGED
@@ -14,7 +14,7 @@ from pymoto import Module
14
14
  from .assembly import DomainDefinition
15
15
 
16
16
 
17
- class _FigModule(Module):
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(_FigModule):
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
- self.im = ax.imshow(data, origin='lower', cmap=self.cmap)
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
- clim = [np.min(data), np.max(data)] if self.clim is None else self.clim
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(_FigModule):
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
- self.ax.set_ylim([ymin, ymax])
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(_FigModule):
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 np.isfinite(self.minlim) and np.isfinite(self.maxlim):
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.ndarary`): Vectors to write to VTI. The signal tags are used as name.
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, LDAWrapper
11
- from pymoto import SolverDenseLU, SolverDenseLDL, SolverDenseCholesky, SolverDiagonal, SolverDenseQR
12
- from pymoto import SolverSparseLU, SolverSparseCholeskyCVXOPT, SolverSparsePardiso, SolverSparseCholeskyScikit
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.adjoint(adjoint_load)
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 = sps.issparse(mat) # Check if it is a sparse matrix
354
- self.iscomplex = np.iscomplexobj(mat) # Check if it is a complex-valued matrix
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
- self.solver = LDAWrapper(self.solver, hermitian=self.ishermitian, symmetric=self.issymmetric)
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.adjoint(dfdv.conj()).conj()
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 = sps.issparse(A) and (B is None or sps.issparse(B))
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
- qi /= np.sqrt(qi@Bqi) # Normalize
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
- self.Ainv.update(mat_shifted)
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, rmatvec=self.Ainv.adjoint)
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
- minval: Maximum value :math:`x_\text{max}` for negative-null-form constraint
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 not verbose:
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
- df = (fp - f0[Iout])/(dx*sf)
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) < tol:
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
- df = (fp - f0[Iout])/(dx*1j*sf)
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 > 0:
339
- raise RuntimeError(f"OC only works for negative sensitivities: max(dfdx) = {maxdfdx}")
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: 0 - No prints, 1 - Only convergence message, 2 - Convergence and iteration info, 3 - Extended info
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
+ ]