pyMOTO 1.3.0__py3-none-any.whl → 1.4.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/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_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,6 +247,7 @@ 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
@@ -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:
@@ -440,6 +345,7 @@ class EigenSolve(Module):
440
345
  self.sigma = sigma
441
346
  self.mode = mode
442
347
  self.Ainv = None
348
+ self.do_solve = False
443
349
 
444
350
  def _response(self, A, *args):
445
351
  B = args[0] if len(args) > 0 else None
@@ -500,9 +406,14 @@ class EigenSolve(Module):
500
406
  # Use shift-and-invert, so make inverse operator
501
407
  if self.Ainv is None:
502
408
  self.Ainv = auto_determine_solver(mat_shifted, ishermitian=self.is_hermitian)
503
- self.Ainv.update(mat_shifted)
504
-
505
- AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve, rmatvec=self.Ainv.adjoint)
409
+ self.do_solve = True
410
+ if self.sigma != 0:
411
+ self.do_solve = True
412
+ if self.do_solve:
413
+ self.Ainv.update(mat_shifted)
414
+
415
+ AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve,
416
+ rmatvec=lambda b: self.Ainv.solve(b, trans='H'))
506
417
 
507
418
  if self.is_hermitian:
508
419
  return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma, mode=self.mode)
pymoto/modules/scaling.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from pymoto import Module
2
-
2
+ import numpy as np
3
3
 
4
4
  class Scaling(Module):
5
5
  r""" Scales (scalar) input for different response functions in optimization (objective / constraints).
6
6
  This is useful, for instance, for MMA where the objective must be scaled in a certain way for good convergence.
7
7
 
8
- Objective scaling (`minval` and `maxval` are both undefined):
9
- :math:`y^{(i)} = s \frac{x^{(i)}}{x^{(0)}}`
8
+ Objective scaling using absolute value or vector norm (`minval` and `maxval` are both undefined):
9
+ :math:`y^{(i)} = s \frac{x^{(i)}}{||x^{(0)}||}`
10
10
 
11
11
  For the constraints, the negative null-form convention is used, which means the constraint is :math:`y(x) \leq 0`.
12
12
 
@@ -39,7 +39,7 @@ class Scaling(Module):
39
39
 
40
40
  def _response(self, x):
41
41
  if not hasattr(self, 'sf'):
42
- self.sf = self.scaling/x
42
+ self.sf = self.scaling/np.linalg.norm(x)
43
43
  if self.minval is not None:
44
44
  g = 1 - x/self.minval
45
45
  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)
@@ -171,7 +173,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
171
173
  fp = Sout.state
172
174
 
173
175
  # Finite difference sensitivity
174
- df = (fp - f0[Iout])/(dx*sf)
176
+ if issparse(fp):
177
+ df = (fp.toarray() - f0[Iout].toarray()) / (dx * sf)
178
+ else:
179
+ df = (fp - f0[Iout])/(dx*sf)
175
180
 
176
181
  dgdx_fd = np.real(np.sum(df*df_an[Iout]))
177
182
 
@@ -183,7 +188,7 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
183
188
  else:
184
189
  dgdx_an = 0.0
185
190
 
186
- if abs(dgdx_an) < tol:
191
+ if abs(dgdx_an) == 0:
187
192
  error = abs(dgdx_fd - dgdx_an)
188
193
  else:
189
194
  error = abs(dgdx_fd - dgdx_an)/max(abs(dgdx_fd), abs(dgdx_an))
@@ -223,7 +228,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
223
228
  fp = Sout.state
224
229
 
225
230
  # Finite difference sensitivity
226
- df = (fp - f0[Iout])/(dx*1j*sf)
231
+ if issparse(fp):
232
+ df = (fp.toarray() - f0[Iout].toarray()) / (dx * 1j * sf)
233
+ else:
234
+ df = (fp - f0[Iout])/(dx*1j*sf)
227
235
  dgdx_fd = np.imag(np.sum(df*df_an[Iout]))
228
236
 
229
237
  if dx_an[Iout][Iin] is not None:
@@ -335,8 +343,9 @@ def minimize_oc(function, variables, objective: Signal,
335
343
  function.sensitivity()
336
344
  dfdx, _ = _concatenate_to_array(obtain_sensitivities(variables))
337
345
  maxdfdx = max(dfdx)
338
- if maxdfdx > 0:
339
- raise RuntimeError(f"OC only works for negative sensitivities: max(dfdx) = {maxdfdx}")
346
+ if maxdfdx > 1e-15:
347
+ warnings.warn(f"OC only works for negative sensitivities: max(dfdx) = {maxdfdx}. Clipping positive values.")
348
+ dfdx = np.minimum(dfdx, 0)
340
349
 
341
350
  # Do OC update
342
351
  l1, l2 = l1init, l2init
@@ -374,7 +383,12 @@ def minimize_mma(function, variables, responses, **kwargs):
374
383
  move: Move limit on relative variable change per iteration
375
384
  xmin: Minimum design variable (can be a vector)
376
385
  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
386
+ verbosity: Level of information to print
387
+ 0 - No prints
388
+ 1 - Only convergence message
389
+ 2 - Convergence and iteration info (default)
390
+ 3 - Additional info on variables
391
+ 4 - Additional info on sensitivity information
378
392
 
379
393
  """
380
394
  # Save initial state
@@ -0,0 +1,14 @@
1
+ from .solvers import LinearSolver, LDAWrapper
2
+ from .matrix_checks import 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_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
+ ]
@@ -0,0 +1,108 @@
1
+ import warnings
2
+ import scipy.sparse as sps
3
+ from inspect import currentframe, getframeinfo
4
+
5
+ from .dense import *
6
+ from .sparse import *
7
+ from .matrix_checks import *
8
+
9
+
10
+ # flake8: noqa: C901
11
+ def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertriangular=None,
12
+ ishermitian=None, issymmetric=None, ispositivedefinite=None):
13
+ """
14
+ Uses parts of Matlab's scheme https://nl.mathworks.com/help/matlab/ref/mldivide.html
15
+ :param A: The matrix
16
+ :param isdiagonal: Manual override for diagonal matrix
17
+ :param islowertriangular: Override for lower triangular matrix
18
+ :param isuppertriangular: Override for upper triangular matrix
19
+ :param ishermitian: Override for hermitian matrix (prevents check)
20
+ :param issymmetric: Override for symmetric matrix (prevents check). Is the same as hermitian for a real matrix
21
+ :param ispositivedefinite: Manual override for positive definiteness
22
+ :return: LinearSolver which should be 'best' for the matrix
23
+ """
24
+ issparse = sps.issparse(A) # Check if the matrix is sparse
25
+ issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
26
+
27
+ if not issquare:
28
+ if issparse:
29
+ sps.SparseEfficiencyWarning("Only a dense version of QR solver is available") # TODO
30
+ return SolverDenseQR()
31
+
32
+ # l_bw, u_bw = spla.bandwidth(A) # TODO Get bandwidth (implemented in scipy version > 1.8.0)
33
+
34
+ if isdiagonal is None: # Check if matrix is diagonal
35
+ # TODO: This could be improved to check other sparse matrix types as well
36
+ isdiagonal = matrix_is_diagonal(A)
37
+ if isdiagonal:
38
+ return SolverDiagonal()
39
+
40
+ # Check if the matrix is triangular
41
+ # TODO Currently only for dense matrices
42
+ if islowertriangular is None: # Check if matrix is lower triangular
43
+ islowertriangular = False if issparse else np.allclose(A, np.tril(A))
44
+ if islowertriangular:
45
+ warnings.WarningMessage("Lower triangular solver not implemented",
46
+ UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
47
+
48
+ if isuppertriangular is None: # Check if matrix is upper triangular
49
+ isuppertriangular = False if issparse else np.allclose(A, np.triu(A))
50
+ if isuppertriangular:
51
+ warnings.WarningMessage("Upper triangular solver not implemented",
52
+ UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
53
+
54
+ ispermutedtriangular = False
55
+ if ispermutedtriangular:
56
+ warnings.WarningMessage("Permuted triangular solver not implemented",
57
+ UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
58
+
59
+ # Check if the matrix is complex-valued
60
+ iscomplex = np.iscomplexobj(A)
61
+ if iscomplex:
62
+ # Detect if the matrix is hermitian and/or symmetric
63
+ if ishermitian is None:
64
+ ishermitian = matrix_is_hermitian(A)
65
+ if issymmetric is None:
66
+ issymmetric = matrix_is_symmetric(A)
67
+ else:
68
+ if ishermitian is None and issymmetric is None:
69
+ # Detect if the matrix is symmetric
70
+ issymmetric = matrix_is_symmetric(A)
71
+ ishermitian = issymmetric
72
+ elif ishermitian is not None and issymmetric is not None:
73
+ assert ishermitian == issymmetric, "For real-valued matrices, symmetry and hermitian must be equal"
74
+ elif ishermitian is None:
75
+ ishermitian = issymmetric
76
+ elif issymmetric is None:
77
+ issymmetric = ishermitian
78
+
79
+ if issparse:
80
+ # Prefer Intel Pardiso solver as it can solve any matrix TODO: Check for complex matrix
81
+ if SolverSparsePardiso.defined and not iscomplex:
82
+ # TODO check for positive definiteness? np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
83
+ return SolverSparsePardiso(symmetric=issymmetric, hermitian=ishermitian, positive_definite=ispositivedefinite)
84
+
85
+ if ishermitian:
86
+ # Check if diagonal is all positive or all negative -> Cholesky
87
+ if ispositivedefinite is None:
88
+ ispositivedefinite = np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
89
+ if ispositivedefinite: # TODO what about the complex case?
90
+ if SolverSparseCholeskyScikit.defined:
91
+ return SolverSparseCholeskyScikit()
92
+ if SolverSparseCholeskyCVXOPT.defined:
93
+ return SolverSparseCholeskyCVXOPT()
94
+
95
+ return SolverSparseLU() # Default to LU, which should be possible for any non-singular square matrix
96
+
97
+ else: # Dense branch
98
+ if ishermitian:
99
+ # Check if diagonal is all positive or all negative
100
+ if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0):
101
+ return SolverDenseCholesky()
102
+ else:
103
+ return SolverDenseLDL(hermitian=ishermitian)
104
+ elif issymmetric:
105
+ return SolverDenseLDL(hermitian=ishermitian)
106
+ else:
107
+ # TODO: Detect if the matrix is Hessenberg
108
+ return SolverDenseLU()