pyMOTO 1.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyMOTO
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: A modular approach for topology optimization
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -0,0 +1,29 @@
1
+ pymoto/__init__.py,sha256=YLMAiO2PZHAC6nYWXVh03rhZnZkc_Rc2z7SGQj1T8I4,2058
2
+ pymoto/core_objects.py,sha256=88AOo041wrcRSPoLCRwBXUoYANGX-b2SAA0Nuf6sn2Y,25252
3
+ pymoto/routines.py,sha256=yjvcQDcWU47ZM6ZpZoX8VwrJoN9JDO_25DSa9oVcKec,15582
4
+ pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
+ pymoto/common/domain.py,sha256=-eFuYRLehQ17Ai-cV59f4I9FbEM-DJAj6kjjVfj31X0,18120
6
+ pymoto/common/dyadcarrier.py,sha256=VwMbqPr0NMDPfpsH0BwvXp8M1dmh8ijFDpF6yoyTmto,19394
7
+ pymoto/common/mma.py,sha256=Pof3clOHA8PG51TmUjs11dkjSP96kovZjsPv62tI2Ec,24055
8
+ pymoto/modules/aggregation.py,sha256=Oi17hIJ6dic4lOPw16zmjbdC72MjB6XK34H80bnbWAI,7580
9
+ pymoto/modules/assembly.py,sha256=quuR8QpB2w-O0zly-xS6PK6wZQMY6S5TWh15Y9wuh14,22974
10
+ pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
11
+ pymoto/modules/complex.py,sha256=B_Obk-ABdV66lEudZ5s8o6qG9NsmYlBsX-PbWvbphhc,4429
12
+ pymoto/modules/filter.py,sha256=6X9FaQMWYZ_TpHVTFiEibzlmAwmSWbydYM93LFrJ0Wo,25490
13
+ pymoto/modules/generic.py,sha256=YzsGZ8J0oLCORt78Bf2p0v4GuqpWRI77NLoCk7gqidw,10666
14
+ pymoto/modules/io.py,sha256=LcFvJ-cPgg5ee-aag8kaxHw5RzQ-ggxOM5jk7PeJ1r8,13140
15
+ pymoto/modules/linalg.py,sha256=BNkih4nvvkYuQpm4bG5U38dAjkfD5EFi3MjANpBEPPI,21927
16
+ pymoto/modules/scaling.py,sha256=uq88HHW9rP16XLz7UGc3CNBBpY2Z1glo8yjYxZEnXUg,2327
17
+ pymoto/solvers/__init__.py,sha256=9JUeD2SgZbkYFullA7s7s6SuAVv0onqAqJ8hFvNOs2g,1033
18
+ pymoto/solvers/auto_determine.py,sha256=X8MEG7h6jLfAV1inpja45_-suG8qQFMfLMDfW2ryQqQ,5134
19
+ pymoto/solvers/dense.py,sha256=9fKPCwNxRKAEk5k1A7fdLrr9ngeVssGlw-sbjWCm4iU,11235
20
+ pymoto/solvers/iterative.py,sha256=CIxJHjGnCaIjXbtO2NxV60yeDpcCbSD6Bp0xR-7vOf0,12944
21
+ pymoto/solvers/matrix_checks.py,sha256=bbrfjpTSWWnuQW3xY0_CYE8yrh5gA9K5b1LzHEOFAxI,1663
22
+ pymoto/solvers/solvers.py,sha256=RwHjZYYlE3oA0U9k7ukla2gOdmq57rSSJQvHqjaM7JU,10626
23
+ pymoto/solvers/sparse.py,sha256=w8XBlFBIfOpNnfRdLWhLzzqtD8YVxMnDBuhIabFfQQc,16664
24
+ pyMOTO-1.5.0.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
25
+ pyMOTO-1.5.0.dist-info/METADATA,sha256=hC38SdgeKEK5NkNDh-gwc4Gz2JOFSymJB-eAMXl7HX4,5006
26
+ pyMOTO-1.5.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
27
+ pyMOTO-1.5.0.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
28
+ pyMOTO-1.5.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
+ pyMOTO-1.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pymoto/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '1.4.0'
1
+ __version__ = '1.5.0'
2
2
 
3
3
  from .common.domain import DomainDefinition
4
4
 
@@ -14,12 +14,12 @@ from .core_objects import Signal, Module, Network, make_signals
14
14
 
15
15
  # Import modules
16
16
  from .modules.assembly import AssembleGeneral, AssembleStiffness, AssembleMass, AssemblePoisson
17
- from .modules.assembly import ElementOperation, Strain, Stress
17
+ from .modules.assembly import ElementOperation, Strain, Stress, ElementAverage, NodalOperation, ThermoMechanical
18
18
  from .modules.autodiff import AutoMod
19
19
  from .modules.complex import MakeComplex, RealPart, ImagPart, ComplexNorm
20
20
  from .modules.filter import FilterConv, Filter, DensityFilter, OverhangFilter
21
21
  from .modules.generic import MathGeneral, EinSum, ConcatSignal
22
- from .modules.io import FigModule, PlotDomain, PlotGraph, PlotIter, WriteToVTI
22
+ from .modules.io import FigModule, PlotDomain, PlotGraph, PlotIter, WriteToVTI, ScalarToFile
23
23
  from .modules.linalg import Inverse, LinSolve, EigenSolve, SystemOfEquations, StaticCondensation
24
24
  from .modules.aggregation import AggScaling, AggActiveSet, Aggregation, PNorm, SoftMinMax, KSFunction
25
25
  from .modules.scaling import Scaling
@@ -44,9 +44,9 @@ __all__ = [
44
44
  "MathGeneral", "EinSum", "ConcatSignal",
45
45
  "Inverse", "LinSolve", "EigenSolve", "SystemOfEquations", "StaticCondensation",
46
46
  "AssembleGeneral", "AssembleStiffness", "AssembleMass", "AssemblePoisson",
47
- "ElementOperation", "Strain", "Stress",
47
+ "ElementOperation", "Strain", "Stress", "ElementAverage", "NodalOperation", "ThermoMechanical",
48
48
  "FilterConv", "Filter", "DensityFilter", "OverhangFilter",
49
- "FigModule", "PlotDomain", "PlotGraph", "PlotIter", "WriteToVTI",
49
+ "FigModule", "PlotDomain", "PlotGraph", "PlotIter", "WriteToVTI", "ScalarToFile",
50
50
  "MakeComplex", "RealPart", "ImagPart", "ComplexNorm",
51
51
  "AutoMod",
52
52
  "Aggregation", "PNorm", "SoftMinMax", "KSFunction",
pymoto/common/domain.py CHANGED
@@ -126,9 +126,10 @@ class DomainDefinition:
126
126
  self.conn[el, :] = self.get_elemconnectivity(elx, ely, elz)
127
127
 
128
128
  # Helper for element slicing
129
- eli, elj, elk = np.meshgrid(np.arange(self.nelx), np.arange(self.nely), np.arange(self.nelz), indexing='ij')
129
+ eli, elj, elk = np.meshgrid(np.arange(self.nelx), np.arange(self.nely), np.arange(max(self.nelz, 1)), indexing='ij')
130
130
  self.elements = self.get_elemnumber(eli, elj, elk)
131
131
 
132
+ # Helper for node slicing
132
133
  ndi, ndj, ndk = np.meshgrid(np.arange(self.nelx+1), np.arange(self.nely+1), np.arange(self.nelz+1), indexing='ij')
133
134
  self.nodes = self.get_nodenumber(ndi, ndj, ndk)
134
135
 
@@ -1,4 +1,4 @@
1
- from typing import Union, Iterable, List
1
+ from typing import Union, Iterable, List, Tuple
2
2
  import warnings
3
3
  import numpy as np
4
4
  from numpy.typing import NDArray
@@ -37,19 +37,20 @@ class DyadCarrier(object):
37
37
  :math:`\mathbf{A} = \sum_k^N \mathbf{u}_k\otimes\mathbf{v}_k`
38
38
  or in index notation :math:`A_{ij} = \sum_k^N u_{ki} v_{kj}`. This saves a lot of memory for low :math:`N`.
39
39
 
40
- Args:
41
- u : (optional) List of vectors
42
- v : (optional) List of vectors (if ``u`` is given and ``v`` not, a symmetric dyad is assumed with ``v = u``)
40
+ Keyword Args:
41
+ u: List of vectors
42
+ v: List of vectors (if ``u`` is given and ``v`` not, a symmetric dyad is assumed with ``v = u``)
43
+ shape: Shape of the matrix
43
44
  """
44
45
 
45
46
  __array_priority__ = 11.0 # For overriding numpy's ufuncs
46
47
  ndim = 2 # Number of dimensions
47
48
 
48
- def __init__(self, u: Iterable = None, v: Iterable = None):
49
+ def __init__(self, u: Iterable = None, v: Iterable = None, shape: Tuple[int, int] = (-1, -1)):
49
50
  self.u = []
50
51
  self.v = []
51
- self.ulen = -1
52
- self.vlen = -1
52
+ self.ulen = shape[0]
53
+ self.vlen = shape[1]
53
54
  self.dtype = np.dtype('float64') # Standard data type
54
55
  self.add_dyad(u, v)
55
56
 
@@ -66,6 +67,12 @@ class DyadCarrier(object):
66
67
  else:
67
68
  return self.ulen * self.vlen
68
69
 
70
+ @property
71
+ def n_dyads(self):
72
+ """ Number of dyads stored """
73
+ assert len(self.u) == len(self.v)
74
+ return len(self.u)
75
+
69
76
  def add_dyad(self, u: Iterable, v: Iterable = None, fac: float = None):
70
77
  r""" Adds a list of vectors to the dyad carrier
71
78
 
@@ -92,9 +99,7 @@ class DyadCarrier(object):
92
99
  if len(ulist) != len(vlist):
93
100
  raise TypeError("Number of vectors in u ({}) and v({}) should be equal".format(len(ulist), len(vlist)))
94
101
 
95
- n = len(ulist)
96
-
97
- for i, ui, vi in zip(range(n), ulist, vlist):
102
+ for i, (ui, vi) in enumerate(zip(ulist, vlist)):
98
103
  # Make sure they are numpy arrays
99
104
  if not isinstance(ui, np.ndarray):
100
105
  ui = np.array(ui)
@@ -141,14 +146,22 @@ class DyadCarrier(object):
141
146
 
142
147
  def __getitem__(self, subscript):
143
148
  assert len(subscript) == self.ndim, "Invalid number of slices, must be 2"
144
- usub = [ui[subscript[0]] for ui in self.u]
145
- vsub = [vi[subscript[1]] for vi in self.v]
149
+ if self.shape[0] < 0 and self.shape[1] < 0:
150
+ return DyadCarrier()
151
+
152
+ usample = np.zeros(self.shape[0])[subscript[0]]
153
+ vsample = np.zeros(self.shape[1])[subscript[1]]
146
154
 
147
- is_uni_slice = isscalarlike(usub[0]) or isscalarlike(vsub[0])
155
+ is_uni_slice = isscalarlike(usample) or isscalarlike(vsample)
148
156
  is_np_slice = isinstance(subscript[0], np.ndarray) and isinstance(subscript[1], np.ndarray)
157
+
149
158
  if is_np_slice and subscript[0].shape != subscript[1].shape:
150
159
  raise IndexError(f"shape mismatch: indexing arrays could not be broadcast together "
151
160
  f"with shapes {subscript[0].shape} {subscript[1].shape}")
161
+
162
+ usub = [ui[subscript[0]] for ui in self.u]
163
+ vsub = [vi[subscript[1]] for vi in self.v]
164
+
152
165
  if is_uni_slice or is_np_slice:
153
166
  res = 0
154
167
  for (ui, vi) in zip(usub, vsub):
@@ -156,7 +169,7 @@ class DyadCarrier(object):
156
169
 
157
170
  return res
158
171
  else:
159
- return DyadCarrier(usub, vsub)
172
+ return DyadCarrier(usub, vsub, shape=(np.size(usample), np.size(vsample)))
160
173
 
161
174
  def __setitem__(self, subscript, value):
162
175
  assert len(subscript) == self.ndim, "Invalid number of slices, must be 2"
@@ -171,10 +184,10 @@ class DyadCarrier(object):
171
184
  vi[subscript[1]] = value
172
185
 
173
186
  def __pos__(self):
174
- return DyadCarrier(self.u, self.v)
187
+ return self.copy()
175
188
 
176
189
  def __neg__(self):
177
- return DyadCarrier([-uu for uu in self.u], self.v)
190
+ return DyadCarrier([-uu for uu in self.u], self.v, shape=self.shape)
178
191
 
179
192
  def __iadd__(self, other):
180
193
  self.add_dyad(other.u, other.v)
@@ -189,7 +202,7 @@ class DyadCarrier(object):
189
202
  elif isdyad(other):
190
203
  if other.shape != self.shape and (self.size > 0 and other.size > 0):
191
204
  raise ValueError(f"Inconsistent shapes {self.shape} and {other.shape}")
192
- return DyadCarrier(self.u, self.v).__iadd__(other)
205
+ return self.copy().__iadd__(other)
193
206
  elif isdense(other):
194
207
  other = np.broadcast_to(other, self.shape)
195
208
  return other + self.todense()
@@ -219,28 +232,44 @@ class DyadCarrier(object):
219
232
  return NotImplemented
220
233
 
221
234
  def __rmul__(self, other): # other * self
222
- return DyadCarrier([other*ui for ui in self.u], self.v)
235
+ return DyadCarrier([other*ui for ui in self.u], self.v, shape=self.shape)
223
236
 
224
237
  def __mul__(self, other): # self * other
225
- return DyadCarrier(self.u, [vi*other for vi in self.v])
238
+ return DyadCarrier(self.u, [vi*other for vi in self.v], shape=self.shape)
226
239
 
227
240
  def copy(self):
228
241
  """ Returns a deep copy of the DyadCarrier """
229
- return DyadCarrier(self.u, self.v)
242
+ return DyadCarrier(self.u, self.v, shape=self.shape)
230
243
 
231
244
  def conj(self):
232
245
  """ Returns (a deep copied) complex conjugate of the DyadCarrier """
233
- return DyadCarrier([u.conj() for u in self.u], [v.conj() for v in self.v])
246
+ return DyadCarrier([u.conj() for u in self.u], [v.conj() for v in self.v], shape=self.shape)
234
247
 
235
248
  @property
236
249
  def real(self):
237
250
  """ Returns a deep copy of the real part of the DyadCarrier """
238
- return DyadCarrier([*[u.real for u in self.u], *[-u.imag for u in self.u]], [*[v.real for v in self.v], *[v.imag for v in self.v]])
251
+ return DyadCarrier([*[u.real for u in self.u], *[-u.imag for u in self.u]], [*[v.real for v in self.v], *[v.imag for v in self.v]], shape=self.shape)
239
252
 
240
253
  @property
241
254
  def imag(self):
242
255
  """ Returns a deep copy of the imaginary part of the DyadCarrier """
243
- return DyadCarrier([*[u.real for u in self.u], *[u.imag for u in self.u]], [*[v.imag for v in self.v], *[v.real for v in self.v]])
256
+ return DyadCarrier([*[u.real for u in self.u], *[u.imag for u in self.u]], [*[v.imag for v in self.v], *[v.real for v in self.v]], shape=self.shape)
257
+
258
+ def min(self):
259
+ minval = 0.0
260
+ for u, v in zip(self.u, self.v):
261
+ minval += u.min() * v.min()
262
+ if len(self.u) >= 2:
263
+ warnings.warn("The minimum is an approximation")
264
+ return minval
265
+
266
+ def max(self):
267
+ maxval = 0.0
268
+ for u, v in zip(self.u, self.v):
269
+ maxval += u.max() * v.max()
270
+ if len(self.u) >= 2:
271
+ warnings.warn("The maximum is an approximation")
272
+ return maxval
244
273
 
245
274
  # flake8: noqa: C901
246
275
  def contract(self, mat: Union[NDArray, spmatrix] = None, rows: NDArray[int] = None, cols: NDArray[int] = None):
@@ -446,7 +475,7 @@ class DyadCarrier(object):
446
475
 
447
476
  def transpose(self):
448
477
  """ Returns a deep copy of the transposed DyadCarrier matrix"""
449
- return DyadCarrier(self.v, self.u)
478
+ return DyadCarrier(self.v, self.u, shape=(self.shape[1], self.shape[0]))
450
479
 
451
480
  def dot(self, other):
452
481
  """ Inner product """
@@ -474,9 +503,9 @@ class DyadCarrier(object):
474
503
  if other.ndim == 1:
475
504
  return self.__dot__(other)
476
505
 
477
- return DyadCarrier(self.u, [vi@other for vi in self.v])
506
+ return DyadCarrier(self.u, [vi@other for vi in self.v], shape=(self.shape[0], other.shape[1]))
478
507
 
479
508
  def __rmatmul__(self, other): # other @ self
480
509
  if other.ndim == 1:
481
510
  return self.__rdot__(other)
482
- return DyadCarrier([other@ui for ui in self.u], self.v)
511
+ return DyadCarrier([other@ui for ui in self.u], self.v, shape=(other.shape[0], self.shape[1]))
pymoto/common/mma.py CHANGED
@@ -28,20 +28,50 @@ def residual(x, y, z, lam, xsi, eta, mu, zet, s, upp, low, P0, P1, Q0, Q1, epsi,
28
28
  ])
29
29
 
30
30
 
31
- def subsolv(epsimin, low, upp, alfa, beta, P, Q, a0, a, b, c, d):
32
- """ This function subsolv solves the MMA subproblem
33
- minimize SUM[ p0j/(uppj-xj) + q0j/(xj-lowj) ] + a0*z +
34
- + SUM[ ci*yi + 0.5*di*(yi)^2 ],
35
- subject to SUM[ pij/(uppj-xj) + qij/(xj-lowj) ] - ai*z - yi <= bi,
36
- alfaj <= xj <= betaj, yi >= 0, z >= 0.
37
- Input: m, n, low, upp, alfa, beta, p0, q0, P, Q, a0, a, b, c, d.
38
- Output: xmma,ymma,zmma, slack variables and Lagrange multiplers.
31
+ def subsolv(epsimin, low, upp, alfa, beta, P, Q, a0, a, b, c, d, x0=None):
32
+ r""" This function solves the MMA subproblem
33
+
34
+ minimize f_0(\vec{x}) + a_0*z + \sum_i^m[ c_i*y_i + 1/2*d_i*y_i^2 ],
35
+ subject to f_i(\vec{x}) - a_i*z - y_i <= b_i, for i = 1, ..., m
36
+ alfa_j <= x_j <= beta_j, for j = 1, ..., n
37
+ y_i >= 0, for i = 1, ..., m
38
+ z >= 0.
39
+
40
+ where:
41
+ MMA approximation: :math:`f_i(\vec{x}) = \sum_j\left( p_{ij}/(upp_j-x_j) + q_{ij}/(x_j-low_j) \right)`
42
+ m: The number of general constraints
43
+ n: The number of variables in :math:`\vec{x}`
44
+
45
+ Args:
46
+ epsimin: Solution tolerance on maximum residual
47
+ low: Column vector with the lower asymptotes
48
+ upp: Column vector with the upper asymptotes
49
+ alfa: Vector with the lower bounds for the variables :math:`\vec{x}`
50
+ beta: Vector with the upper bounds for the variables :math:`\vec{x}`
51
+ P: Upper asymptotic amplitudes
52
+ Q: Lower asymptotic amplitudes
53
+ a0: The constants :math:`a_0` in the term :math:`a_0\cdot z`
54
+ a: Vector with the constants :math:`a_i` in the terms :math:`a_i \cdot z`
55
+ c: Vector with the constants :math:`c_i` in the terms :math:`c_i \cdot y_i`
56
+ d: Vector with the constants :math:`d_i` in the terms :math:`0.5 \cdot d_i \cdot y_i^2`
57
+ x0 (optional): Initial guess, in case not given :math:`x_0 = (\alpha + \beta)/2` is used
58
+
59
+ Returns:
60
+ x: Vector with the optimal values of the variables :math:`\vec{x}` in the current MMA subproblem
61
+ y: Vector with the optimal values of the variables :math:`y_i` in the current MMA subproblem
62
+ z: Scalar with the optimal value of the variable :math:`z` in the current MMA subproblem
63
+ lam: Lagrange multipliers for the :math:`m` general MMA constraints
64
+ xsi: Lagrange multipliers for the :math:'n' constraints :math:`alfa_j - x_j <= 0`
65
+ eta: Lagrange multipliers for the :math:'n' constraints :math:`x_j - beta_j <= 0`
66
+ mu: Lagrange multipliers for the :math:`m` constraints :math:`-y_i <= 0`
67
+ zet: Lagrange multiplier for the single constraint :math:`-z <= 0`
68
+ s: Slack variables for the m general MMA constraints
39
69
  """
40
70
 
41
71
  n, m = len(alfa), len(a)
42
72
  epsi = 1.0
43
73
  maxittt = 400
44
- x = 0.5 * (alfa + beta)
74
+ x = 0.5 * (alfa + beta) if x0 is None else np.clip(x0, alfa+1e-10, beta-1e-10) # Design variables
45
75
  y = np.ones(m)
46
76
  z = 1.0
47
77
  lam = np.ones(m)
@@ -168,8 +198,7 @@ def subsolv(epsimin, low, upp, alfa, beta, P, Q, a0, a, b, c, d):
168
198
  sold = s.copy()
169
199
 
170
200
  # Do linesearch
171
- itto = 0
172
- while itto < maxittt:
201
+ for itto in range(maxittt):
173
202
  # Find new set of variables with stepsize
174
203
  x[:] = xold + steg * dx
175
204
  y[:] = yold + steg * dy
@@ -184,14 +213,13 @@ def subsolv(epsimin, low, upp, alfa, beta, P, Q, a0, a, b, c, d):
184
213
  residu = residual(x, y, z, lam, xsi, eta, mu, zet, s, upp, low, P0, P1, Q0, Q1, epsi, a0, a, b, c, d, alfa, beta)
185
214
  if np.linalg.norm(residu) < residunorm:
186
215
  break
187
- itto += 1
188
216
  steg /= 2 # Reduce stepsize
189
217
 
190
218
  residunorm = np.linalg.norm(residu)
191
219
  residumax = np.max(np.abs(residu))
192
220
 
193
221
  if ittt > maxittt - 2:
194
- print(f"MMA Subsolver: itt = {ittt}, at epsi = {epsi}")
222
+ print(f"MMA Subsolver: itt = {ittt}, at epsi = {'%.3e'%epsi}")
195
223
  # decrease epsilon with factor 10
196
224
  epsi /= 10
197
225
 
@@ -200,8 +228,7 @@ def subsolv(epsimin, low, upp, alfa, beta, P, Q, a0, a, b, c, d):
200
228
 
201
229
 
202
230
  class MMA:
203
- """
204
- Block for the MMA algorithm
231
+ r""" Class for the MMA optimization algorithm
205
232
  The design variables are set by keyword <variables> accepting a list of variables.
206
233
  The responses are set by keyword <responses> accepting a list of signals.
207
234
  If none are given, the internal sig_in and sig_out are used.
@@ -251,19 +278,15 @@ class MMA:
251
278
 
252
279
  self.a0 = kwargs.get("a0", 1.0)
253
280
 
254
- self.epsimin = kwargs.get("epsimin", 1e-7) # Or 1e-7 ?? witout sqrt(m+n) or 1e-9
255
- self.raa0 = kwargs.get("raa0", 1e-5)
256
-
281
+ self.epsimin = kwargs.get("epsimin", 1e-10) # Or 1e-7 ?? witout sqrt(m+n) or 1e-9
257
282
  self.cCoef = kwargs.get("cCoef", 1e3) # Svanberg uses 1e3 in example? Old code had 1e7
258
283
 
259
- # Not used
260
- self.dxmin = kwargs.get("dxmin", 1e-5)
261
-
262
284
  self.albefa = kwargs.get("albefa", 0.1)
263
285
  self.asyinit = kwargs.get("asyinit", 0.5)
264
286
  self.asyincr = kwargs.get("asyincr", 1.2)
265
287
  self.asydecr = kwargs.get("asydecr", 0.7)
266
288
  self.asybound = kwargs.get("asybound", 10.0)
289
+ self.mmaversion = kwargs.get("mmaversion", "Svanberg2007") # Options are Svanberg1987, Svanberg2007
267
290
 
268
291
  self.ittomax = kwargs.get("ittomax", 400)
269
292
 
@@ -287,7 +310,6 @@ class MMA:
287
310
  self.d = np.ones(self.m)
288
311
  self.gold1 = np.zeros(self.m + 1)
289
312
  self.gold2 = self.gold1.copy()
290
- self.rho = self.raa0 * np.ones(self.m + 1)
291
313
 
292
314
  def response(self):
293
315
  change = 1
@@ -352,7 +374,7 @@ class MMA:
352
374
  # Save response
353
375
  f = ()
354
376
  for s in self.responses:
355
- if not np.isscalar(s.state):
377
+ if np.size(s.state) != 1:
356
378
  raise TypeError("State of responses must be scalar.")
357
379
  f += (s.state, )
358
380
 
@@ -443,57 +465,6 @@ class MMA:
443
465
  if self.offset is None:
444
466
  self.offset = self.asyinit * np.ones(self.n)
445
467
 
446
- # Minimize f_0(x) + a_0*z + sum( c_i*y_i + 0.5*d_i*(y_i)^2 )
447
- # subject to f_i(x) - a_i*z - y_i <= 0, i = 1,...,m
448
- # xmin_j <= x_j <= xmax_j, j = 1,...,n
449
- # z >= 0, y_i >= 0, i = 1,...,m
450
- # *** INPUT:
451
- #
452
- # m = The number of general constraints.
453
- # n = The number of variables x_j.
454
- # iter = Current iteration number ( =1 the first time mmasub is called).
455
- # xval = Column vector with the current values of the variables x_j.
456
- # xmin = Column vector with the lower bounds for the variables x_j.
457
- # xmax = Column vector with the upper bounds for the variables x_j.
458
- # xold1 = xval, one iteration ago (provided that iter>1).
459
- # xold2 = xval, two iterations ago (provided that iter>2).
460
- # f0val = The value of the objective function f_0 at xval.
461
- # df0dx = Column vector with the derivatives of the objective function
462
- # f_0 with respect to the variables x_j, calculated at xval.
463
- # fval = Column vector with the values of the constraint functions f_i,
464
- # calculated at xval.
465
- # dfdx = (m x n)-matrix with the derivatives of the constraint functions
466
- # f_i with respect to the variables x_j, calculated at xval.
467
- # dfdx(i,j) = the derivative of f_i with respect to x_j.
468
- # low = Column vector with the lower asymptotes from the previous
469
- # iteration (provided that iter>1).
470
- # upp = Column vector with the upper asymptotes from the previous
471
- # iteration (provided that iter>1).
472
- # a0 = The constants a_0 in the term a_0*z.
473
- # a = Column vector with the constants a_i in the terms a_i*z.
474
- # c = Column vector with the constants c_i in the terms c_i*y_i.
475
- # d = Column vector with the constants d_i in the terms 0.5*d_i*(y_i)^2.
476
- #
477
-
478
- # *** OUTPUT:
479
- #
480
- # xmma = Column vector with the optimal values of the variables x_j
481
- # in the current MMA subproblem.
482
- # ymma = Column vector with the optimal values of the variables y_i
483
- # in the current MMA subproblem.
484
- # zmma = Scalar with the optimal value of the variable z
485
- # in the current MMA subproblem.
486
- # lam = Lagrange multipliers for the m general MMA constraints.
487
- # xsi = Lagrange multipliers for the n constraints alfa_j - x_j <= 0.
488
- # eta = Lagrange multipliers for the n constraints x_j - beta_j <= 0.
489
- # mu = Lagrange multipliers for the m constraints -y_i <= 0.
490
- # zet = Lagrange multiplier for the single constraint -z <= 0.
491
- # s = Slack variables for the m general MMA constraints.
492
- # low = Column vector with the lower asymptotes, calculated and used
493
- # in the current MMA subproblem.
494
- # upp = Column vector with the upper asymptotes, calculated and used
495
- # in the current MMA subproblem.
496
-
497
468
  # # ASYMPTOTES
498
469
  # Calculation of the asymptotes low and upp :
499
470
  # For iter = 1,2 the asymptotes are fixed depending on asyinit
@@ -536,16 +507,27 @@ class MMA:
536
507
  # # APPROXIMATE CONVEX SEPARABLE FUNCTIONS
537
508
  # Calculations of p0, q0, P, Q and b.
538
509
  # calculate the constant factor in calculations of pij and qij
510
+ # From: Svanberg(2007) - MMA and GCMMA, two methods for nonlinear optimization
511
+ dg_plus = np.maximum(+dg, 0)
512
+ dg_min = np.maximum(-dg, 0)
539
513
  dx2 = shift**2
540
- P = dx2 * np.maximum(+dg, 0)
541
- Q = dx2 * np.maximum(-dg, 0)
514
+ if '1987' in self.mmaversion:
515
+ # Original version
516
+ P = dx2 * dg_plus
517
+ Q = dx2 * dg_min
518
+ elif '2007' in self.mmaversion:
519
+ # Improved version -> Allows to use higher epsimin to get design variables closer to the bound.
520
+ P = dx2 * (1.001*dg_plus + 0.001*dg_min + 1e-5/self.dx)
521
+ Q = dx2 * (0.001*dg_plus + 1.001*dg_min + 1e-5/self.dx)
522
+ else:
523
+ raise ValueError("Only \"Svanberg1987\" or \"Svanberg2007\" are valid options")
542
524
 
543
525
  rhs = np.dot(P, 1 / shift) + np.dot(Q, 1 / shift) - g
544
526
  b = rhs[1:]
545
527
 
546
528
  # Solving the subproblem by a primal-dual Newton method
547
529
  epsimin_scaled = self.epsimin*np.sqrt(self.m + self.n)
548
- xmma, ymma, zmma, lam, xsi, eta, mu, zet, s = subsolv(epsimin_scaled, self.low, self.upp, alfa, beta, P, Q, self.a0, self.a, b, self.c, self.d)
530
+ xmma, ymma, zmma, lam, xsi, eta, mu, zet, s = subsolv(epsimin_scaled, self.low, self.upp, alfa, beta, P, Q, self.a0, self.a, b, self.c, self.d, x0=xval)
549
531
 
550
532
  self.gold2, self.gold1 = self.gold1, g.copy()
551
533
  self.xold2, self.xold1 = self.xold1, xval.copy()
@@ -582,3 +564,4 @@ class MMA:
582
564
  print(f" | Changes: {', '.join(change_msgs)}")
583
565
 
584
566
  return xmma, change
567
+
pymoto/core_objects.py CHANGED
@@ -107,6 +107,9 @@ class Signal:
107
107
  return
108
108
  if self.sensitivity is None:
109
109
  self.sensitivity = copy.deepcopy(ds)
110
+ elif hasattr(self.sensitivity, "add_sensitivity"):
111
+ # Allow user to implement a custom add_sensitivity function instead of __iadd__
112
+ self.sensitivity.add_sensitivity(ds)
110
113
  else:
111
114
  self.sensitivity += ds
112
115
  return self
@@ -593,19 +596,24 @@ class Network(Module):
593
596
 
594
597
  self.print_timing = print_timing
595
598
 
596
- def timefn(self, fn):
599
+ def timefn(self, fn, prefix='Evaluation'):
597
600
  start_t = time.time()
598
601
  fn()
599
- print(f"Evaluating {fn} took {time.time() - start_t} s")
602
+ duration = time.time() - start_t
603
+ if duration > .5:
604
+ print(f"{prefix} {fn} took {time.time() - start_t} s")
600
605
 
601
606
  def response(self):
602
607
  if self.print_timing:
603
- [self.timefn(b.response) for b in self.mods]
608
+ [self.timefn(b.response, prefix='Response') for b in self.mods]
604
609
  else:
605
610
  [b.response() for b in self.mods]
606
611
 
607
612
  def sensitivity(self):
608
- [b.sensitivity() for b in reversed(self.mods)]
613
+ if self.print_timing:
614
+ [self.timefn(b.sensitivity, 'Sensitivity') for b in reversed(self.mods)]
615
+ else:
616
+ [b.sensitivity() for b in reversed(self.mods)]
609
617
 
610
618
  def reset(self):
611
619
  [b.reset() for b in reversed(self.mods)]
@@ -325,29 +325,47 @@ class ElementOperation(Module):
325
325
 
326
326
  :math:`y_e = \mathbf{B} \mathbf{u}_e`
327
327
 
328
+ This module is the reverse of :py:class:`pymoto.NodalOperation`.
329
+
328
330
  Input Signal:
329
- - ``u``: Nodal vector of size ``(n_dof_per_element * #nodes)``
331
+ - ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
330
332
 
331
333
  Output Signal:
332
- - ``y``: Elemental output data of size ``(..., #elements)``
334
+ - ``y``: Elemental output data of size ``(..., #elements)`` or ``(#dofs, ..., #elements)``
333
335
 
334
336
  Args:
335
337
  domain: The domain defining element and nodal connectivity
336
- element_matrix: The element operator matrix :math:`\mathbf{B}` of size ``(..., n_dof_per_element)``
338
+ element_matrix: The element operator matrix :math:`\mathbf{B}` of size ``(..., #dofs_per_element)`` or ``(..., #nodes_per_element)``
337
339
  """
338
340
  def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray):
339
341
  if element_matrix.shape[-1] % domain.elemnodes != 0:
340
- raise IndexError("Size of last dimension of element operator matrix is not compatible with mesh. "
341
- "Must be dividable by the number of nodes.")
342
-
343
- ndof = element_matrix.shape[-1] // domain.elemnodes
344
-
342
+ raise IndexError(
343
+ f"Size of last dimension of element operator matrix ({element_matrix.shape[-1]}) is not compatible "
344
+ f"with mesh. Must be dividable by the number of nodes per element ({domain.elemnodes})."
345
+ )
346
+ self.domain = domain
345
347
  self.element_matrix = element_matrix
346
- self.dofconn = domain.get_dofconnectivity(ndof)
347
- self.usiz = ndof * domain.nnodes
348
+ self.dofconn = None
348
349
 
349
350
  def _response(self, u):
350
- assert u.size == self.usiz
351
+ if u.size % self.domain.nnodes != 0:
352
+ raise IndexError(f"Size of input vector ({u.size}) does not match number of nodes ({self.domain.nnodes})")
353
+ ndof = u.size // self.domain.nnodes
354
+
355
+ if self.element_matrix.shape[-1] != self.domain.elemnodes * ndof:
356
+ # Initialize only after first call to response(), because the number of dofs may not yet be known
357
+ em = self.element_matrix.copy()
358
+ assert em.shape[-1] == self.domain.elemnodes, f"Size of element matrix must match #dofs_per_element ({ndof*self.domain.elemnodes}) or #nodes_per_element ({self.domain.elemnodes})."
359
+
360
+ # Element matrix is repeated for each dof
361
+ self.element_matrix = np.zeros((ndof, *self.element_matrix.shape[:-1], ndof * self.domain.elemnodes))
362
+ for i in range(ndof):
363
+ self.element_matrix[i, ..., i::ndof] = em
364
+
365
+ if self.dofconn is None:
366
+ self.dofconn = self.domain.get_dofconnectivity(ndof)
367
+
368
+ assert self.element_matrix.shape[-1] == ndof * self.domain.elemnodes
351
369
  return einsum('...k, lk -> ...l', self.element_matrix, u[self.dofconn], optimize=True)
352
370
 
353
371
  def _sensitivity(self, dy):
@@ -372,10 +390,10 @@ class Strain(ElementOperation):
372
390
  in case ``voigt = True``, for which :math:`\gamma_{xy}=2\epsilon_{xy}`.
373
391
 
374
392
  Input Signal:
375
- - ``u``: Nodal vector of size ``(#dof per element * #nodes)``
393
+ - ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
376
394
 
377
395
  Output Signal:
378
- - ``e``: Strain matrix of size ``(#strains per element, #elements)``
396
+ - ``e``: Strain matrix of size ``(#strains_per_element, #elements)``
379
397
 
380
398
  Args:
381
399
  domain: The domain defining element and nodal connectivity
@@ -408,10 +426,10 @@ class Stress(Strain):
408
426
  """ Calculate the average stresses per element
409
427
 
410
428
  Input Signal:
411
- - ``u``: Nodal vector of size ``(#dof per element * #nodes)``
429
+ - ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
412
430
 
413
431
  Output Signal:
414
- - ``s``: Stress matrix of size ``(#stresses per element, #elements)``
432
+ - ``s``: Stress matrix of size ``(#stresses_per_element, #elements)``
415
433
 
416
434
  Args:
417
435
  domain: The domain defining element and nodal connectivity
@@ -431,3 +449,99 @@ class Stress(Strain):
431
449
  if domain.dim == 2:
432
450
  D *= domain.element_size[2]
433
451
  self.element_matrix = D @ self.element_matrix
452
+
453
+
454
+ class ElementAverage(ElementOperation):
455
+ r""" Determine average value in element of input nodal values
456
+
457
+ Input Signal:
458
+ - ``v``: Nodal vector of size ``(#dofs_per_node * #nodes)``
459
+
460
+ Output Signal:
461
+ - ``v_el``: Elemental vector of size ``(#elements)`` or ``(#dofs, #elements)`` if ``#dofs_per_node>1``
462
+
463
+ Args:
464
+ domain: The domain defining element and nodal connectivity
465
+ """
466
+ def _prepare(self, domain: DomainDefinition):
467
+ shapefuns = domain.eval_shape_fun(pos=np.array([0, 0, 0]))
468
+ super()._prepare(domain, shapefuns)
469
+
470
+
471
+ class NodalOperation(Module):
472
+ r""" Generic module for nodal operations based on elemental information
473
+
474
+ :math:`u_e = \mathbf{A} x_e`
475
+
476
+ This module is the reverse of :py:class:`pymoto.ElementOperation`.
477
+
478
+ Input Signal:
479
+ - ``x``: Elemental vector of size ``(#elements)``
480
+
481
+ Output Signal:
482
+ - ``u``: nodal output data of size ``(..., #dofs_per_node * #nodes)``
483
+
484
+ Args:
485
+ domain: The domain defining element and nodal connectivity
486
+ element_matrix: The element operator matrix :math:`\mathbf{A}` of size ``(..., #dofs_per_element)``
487
+ """
488
+ def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray):
489
+ if element_matrix.shape[-1] % domain.elemnodes != 0:
490
+ raise IndexError("Size of last dimension of element operator matrix is not compatible with mesh. "
491
+ "Must be dividable by the number of nodes.")
492
+
493
+ ndof = element_matrix.shape[-1] // domain.elemnodes
494
+
495
+ self.element_matrix = element_matrix
496
+ self.dofconn = domain.get_dofconnectivity(ndof)
497
+ self.ndofs = ndof*domain.nnodes
498
+
499
+ def _response(self, x):
500
+ dofs_el = einsum('...k, ...l -> lk', self.element_matrix, x, optimize=True)
501
+ dofs = np.zeros(self.ndofs)
502
+ np.add.at(dofs, self.dofconn, dofs_el)
503
+ return dofs
504
+
505
+ def _sensitivity(self, dx):
506
+ return einsum('...k, lk -> ...l', self.element_matrix, dx[self.dofconn], optimize=True)
507
+
508
+
509
+ class ThermoMechanical(NodalOperation):
510
+ r""" Determine equivalent thermo-mechanical load from design vector and elemental temperature difference
511
+
512
+ :math:`f_thermal = \mathbf{A} (x*t_delta)_e`
513
+
514
+ Input Signal:
515
+ - ``x*t_delta``: Elemental vector of size ``(#elements)`` containing elemental densities multiplied by
516
+ elemental temperature difference
517
+
518
+ Output Signal:
519
+ - ``f_thermal``: nodal equivalent thermo-mechanical load of size ``(#dofs_per_node * #nodes)``
520
+
521
+ Args:
522
+ domain: The domain defining element and nodal connectivity
523
+ e_modulus (optional): Young's modulus
524
+ poisson_ratio (optional): Poisson ratio
525
+ alpha (optional): Coefficient of thermal expansion
526
+ plane (optional): Plane 'strain' or 'stress'
527
+ """
528
+ def _prepare(self, domain: DomainDefinition, e_modulus: float = 1.0, poisson_ratio: float = 0.3, alpha: float = 1e-6, plane: str = 'strain'):
529
+ dim = domain.dim
530
+ D = get_D(e_modulus, poisson_ratio, '3d' if dim == 3 else plane.lower())
531
+ if dim == 2:
532
+ Phi = np.array([1, 1, 0])
533
+ D *= domain.element_size[2]
534
+ elif dim == 3:
535
+ Phi = np.array([1, 1, 1, 0, 0, 0])
536
+
537
+ # Numerical integration
538
+ BDPhi = np.zeros(domain.elemnodes * dim)
539
+ siz = domain.element_size
540
+ w = np.prod(siz[:domain.dim] / 2)
541
+ for n in domain.node_numbering:
542
+ pos = n * (siz / 2) / np.sqrt(3) # Sampling point
543
+ dN_dx = domain.eval_shape_fun_der(pos)
544
+ B = get_B(dN_dx)
545
+ BDPhi += w * B.T @ D @ Phi # Add contribution
546
+
547
+ super()._prepare(domain, alpha*BDPhi)
pymoto/modules/io.py CHANGED
@@ -255,7 +255,7 @@ class WriteToVTI(Module):
255
255
  accepted, which get the suffixed as ``_00``.
256
256
 
257
257
  Input Signals:
258
- - ``*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.
259
259
 
260
260
  Args:
261
261
  domain: The domain layout
@@ -282,3 +282,65 @@ class WriteToVTI(Module):
282
282
  filen = pth[0] + '.{0:04d}'.format(self.iter) + pth[1]
283
283
  self.domain.write_to_vti(data, filename=filen, scale=self.scale)
284
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
@@ -9,7 +9,7 @@ import scipy.sparse.linalg as spsla
9
9
 
10
10
  from pymoto import Signal, Module, DyadCarrier
11
11
  from pymoto.solvers import auto_determine_solver
12
- from pymoto.solvers import matrix_is_hermitian, LDAWrapper
12
+ from pymoto.solvers import matrix_is_sparse, matrix_is_complex, matrix_is_hermitian, LDAWrapper
13
13
 
14
14
 
15
15
  class StaticCondensation(Module):
@@ -251,8 +251,8 @@ class LinSolve(Module):
251
251
 
252
252
  def _response(self, mat, rhs):
253
253
  # Do some detections on the matrix type
254
- self.issparse = sps.issparse(mat) # Check if it is a sparse matrix
255
- 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
256
256
  if not self.iscomplex and self.issymmetric is not None:
257
257
  self.ishermitian = self.issymmetric
258
258
  if self.ishermitian is None:
@@ -318,9 +318,6 @@ class EigenSolve(Module):
318
318
  Mode tracking algorithms can be implemented by the user by providing the argument ``sorting_func``, which is a
319
319
  function with arguments (``λ``, ``Q``).
320
320
 
321
- Todo:
322
- Support for sparse matrix
323
-
324
321
  Input Signal(s):
325
322
  - ``A`` (`dense matrix`): The system matrix of size ``(n, n)``
326
323
  - ``B`` (`dense matrix, optional`): Second system matrix (must be positive-definite) of size ``(n, n)``
@@ -346,12 +343,14 @@ class EigenSolve(Module):
346
343
  self.mode = mode
347
344
  self.Ainv = None
348
345
  self.do_solve = False
346
+ self.adjoint_solvers_need_update = True
349
347
 
350
348
  def _response(self, A, *args):
351
349
  B = args[0] if len(args) > 0 else None
352
350
  if self.is_hermitian is None:
353
351
  self.is_hermitian = (matrix_is_hermitian(A) and (B is None or matrix_is_hermitian(B)))
354
- 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
355
354
 
356
355
  # Solve the eigenvalue problem
357
356
  if self.is_sparse:
@@ -364,12 +363,18 @@ class EigenSolve(Module):
364
363
  W = W[isort]
365
364
  Q = Q[:, isort]
366
365
 
367
- # 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
368
369
  for i in range(W.size):
369
370
  qi, wi = Q[:, i], W[i]
370
- qi *= np.sign(np.real(qi[np.argmax(abs(qi) > 0)])) # Set first value positive for orientation
371
371
  Bqi = qi if B is None else B@qi
372
- 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
373
378
  return W, Q
374
379
 
375
380
  def _sensitivity(self, dW, dQ):
@@ -380,7 +385,8 @@ class EigenSolve(Module):
380
385
  dA, dB = self._dense_sens(A, B, dW, dQ)
381
386
  else:
382
387
  if dQ is not None:
383
- 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)
384
390
  elif dW is not None:
385
391
  dA, dB = self._sparse_eigval_sens(A, B, dW)
386
392
 
@@ -389,7 +395,6 @@ class EigenSolve(Module):
389
395
  elif len(self.sig_in) == 2:
390
396
  return dA, dB
391
397
 
392
-
393
398
  def _sparse_eigs(self, A, B=None):
394
399
  if self.nmodes is None:
395
400
  self.nmodes = 6
@@ -473,3 +478,59 @@ class EigenSolve(Module):
473
478
  dB -= DyadCarrier(dB_u, qi)
474
479
  return dA, dB
475
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,6 +1,7 @@
1
1
  from pymoto import Module
2
2
  import numpy as np
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.
@@ -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
pymoto/routines.py CHANGED
@@ -102,6 +102,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
102
102
  # Get the output state shape
103
103
  shape = (output.shape if hasattr(output, "shape") else ())
104
104
 
105
+ if output is None:
106
+ warnings.warn(f"Output {Iout} of {Sout.tag} is None")
107
+ continue
108
+
105
109
  # Generate a (random) sensitivity for output signal
106
110
  if use_df is not None:
107
111
  df_an[Iout] = use_df[Iout]
@@ -171,6 +175,9 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
171
175
  for Iout, Sout in enumerate(outps):
172
176
  # Obtain perturbed response
173
177
  fp = Sout.state
178
+ if fp is None:
179
+ warnings.warn(f"Output {Iout} of {Sout.tag} is None")
180
+ continue
174
181
 
175
182
  # Finite difference sensitivity
176
183
  if issparse(fp):
@@ -227,6 +234,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
227
234
  # Obtain perturbed response
228
235
  fp = Sout.state
229
236
 
237
+ if fp is None:
238
+ warnings.warn(f"Output {Iout} of {Sout.tag} is None")
239
+ continue
240
+
230
241
  # Finite difference sensitivity
231
242
  if issparse(fp):
232
243
  df = (fp.toarray() - f0[Iout].toarray()) / (dx * 1j * sf)
@@ -1,11 +1,11 @@
1
1
  from .solvers import LinearSolver, LDAWrapper
2
- from .matrix_checks import matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian
2
+ from .matrix_checks import matrix_is_sparse, matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian
3
3
  from .dense import SolverDiagonal, SolverDenseQR, SolverDenseLU, SolverDenseCholesky, SolverDenseLDL
4
4
  from .sparse import SolverSparsePardiso, SolverSparseLU, SolverSparseCholeskyScikit, SolverSparseCholeskyCVXOPT
5
5
  from .iterative import Preconditioner, CG, DampedJacobi, SOR, ILU, GeometricMultigrid
6
6
  from .auto_determine import auto_determine_solver
7
7
 
8
- __all__ = ['matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
8
+ __all__ = ['matrix_is_sparse', 'matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
9
9
  'LinearSolver', 'LDAWrapper',
10
10
  'SolverDiagonal', 'SolverDenseQR', 'SolverDenseLU', 'SolverDenseCholesky', 'SolverDenseLDL',
11
11
  'SolverSparsePardiso', 'SolverSparseLU', 'SolverSparseCholeskyScikit', 'SolverSparseCholeskyCVXOPT',
@@ -21,7 +21,7 @@ def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertri
21
21
  :param ispositivedefinite: Manual override for positive definiteness
22
22
  :return: LinearSolver which should be 'best' for the matrix
23
23
  """
24
- issparse = sps.issparse(A) # Check if the matrix is sparse
24
+ issparse = matrix_is_sparse(A) # Check if the matrix is sparse
25
25
  issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
26
26
 
27
27
  if not issquare:
@@ -12,6 +12,10 @@ def is_cvxopt_spmatrix(A):
12
12
  return isinstance(A, cvxopt.spmatrix) if _has_cvxopt else False
13
13
 
14
14
 
15
+ def matrix_is_sparse(A):
16
+ return sps.issparse(A)
17
+
18
+
15
19
  def matrix_is_complex(A):
16
20
  """ Checks if the matrix is complex """
17
21
  if is_cvxopt_spmatrix(A):
@@ -22,7 +26,7 @@ def matrix_is_complex(A):
22
26
 
23
27
  def matrix_is_diagonal(A):
24
28
  """ Checks if the matrix is diagonal"""
25
- if sps.issparse(A):
29
+ if matrix_is_sparse(A):
26
30
  if isinstance(A, sps.dia_matrix):
27
31
  return len(A.offsets) == 1 and A.offsets[0] == 0
28
32
  else:
@@ -35,7 +39,7 @@ def matrix_is_diagonal(A):
35
39
 
36
40
  def matrix_is_symmetric(A):
37
41
  """ Checks whether a matrix is numerically symmetric """
38
- if sps.issparse(A):
42
+ if matrix_is_sparse(A):
39
43
  return np.allclose((A-A.T).data, 0)
40
44
  elif is_cvxopt_spmatrix(A):
41
45
  return np.isclose(max(abs(A-A.T)), 0.0)
@@ -46,7 +50,7 @@ def matrix_is_symmetric(A):
46
50
  def matrix_is_hermitian(A):
47
51
  """ Checks whether a matrix is numerically Hermitian """
48
52
  if matrix_is_complex(A):
49
- if sps.issparse(A):
53
+ if matrix_is_sparse(A):
50
54
  return np.allclose((A-A.T.conj()).data, 0)
51
55
  elif is_cvxopt_spmatrix(A):
52
56
  return np.isclose(max(abs(A-A.ctrans())), 0.0)
pymoto/solvers/sparse.py CHANGED
@@ -126,6 +126,7 @@ class SolverSparsePardiso(LinearSolver):
126
126
  self._pardiso_solver = PyPardisoSolver(mtype=self._mtype)
127
127
 
128
128
  self._pardiso_solver.factorize(A)
129
+ self._pardiso_solver.set_phase(33)
129
130
 
130
131
  def solve(self, b, x0=None, trans='N'):
131
132
  """ solve Ax=b for x
@@ -1,29 +0,0 @@
1
- pymoto/__init__.py,sha256=jsqGNDDpd3Tde1w46C5GqQCuAqfgN3faVjReYMQFiLw,1922
2
- pymoto/core_objects.py,sha256=mgn5etKHwAOINvu9qqTU89yLs0CDPvxi0anb7q76bNI,24798
3
- pymoto/routines.py,sha256=KlP6kgY-817i51Z9EotDclaRnOjrSD4SdymD0Rt7yX0,15186
4
- pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
- pymoto/common/domain.py,sha256=QJmdRXzexc1KbdLHNP6X6v5whZWtn0f8b4Rv_wlRBT8,18078
6
- pymoto/common/dyadcarrier.py,sha256=b8Ji8einZ4c0lMDuwxd4ZHwRckb2pkuZtAl4Sbg9q4w,18273
7
- pymoto/common/mma.py,sha256=sO4PzG2SBeanK82v-Lyw0c29uPmh2q40nXQVt2NCIBU,24838
8
- pymoto/modules/aggregation.py,sha256=Oi17hIJ6dic4lOPw16zmjbdC72MjB6XK34H80bnbWAI,7580
9
- pymoto/modules/assembly.py,sha256=UpTzp4KGKVcWruEpNarmNRK5lp6CK82QB-7d4Wk2xTg,18138
10
- pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
11
- pymoto/modules/complex.py,sha256=B_Obk-ABdV66lEudZ5s8o6qG9NsmYlBsX-PbWvbphhc,4429
12
- pymoto/modules/filter.py,sha256=6X9FaQMWYZ_TpHVTFiEibzlmAwmSWbydYM93LFrJ0Wo,25490
13
- pymoto/modules/generic.py,sha256=YzsGZ8J0oLCORt78Bf2p0v4GuqpWRI77NLoCk7gqidw,10666
14
- pymoto/modules/io.py,sha256=huqF2A54Tk0f36WRDLaRR7_CdeahmQc25OfvqqQ2hT4,11012
15
- pymoto/modules/linalg.py,sha256=kvfPTblFXGANWzCxEliTMi8qKxgKiejh2zGp_60iL_Q,19358
16
- pymoto/modules/scaling.py,sha256=FN6WsqJME-bkJ5nhoKziS_utFCserpRmRDKlJ8F3MEo,2326
17
- pymoto/solvers/__init__.py,sha256=CuEJMqnZ2QWgj96n4mB18LWonT4D3cq7FgnubrZsxXg,995
18
- pymoto/solvers/auto_determine.py,sha256=Q4TcTT49SYmPLVG0LMmzOx2Tnly--fLXAQVVYjDzGG0,5130
19
- pymoto/solvers/dense.py,sha256=9fKPCwNxRKAEk5k1A7fdLrr9ngeVssGlw-sbjWCm4iU,11235
20
- pymoto/solvers/iterative.py,sha256=CIxJHjGnCaIjXbtO2NxV60yeDpcCbSD6Bp0xR-7vOf0,12944
21
- pymoto/solvers/matrix_checks.py,sha256=gmq7vwFQaXjH3QG-2bW5LcMJF0abTejzw1vEVXCfLPc,1597
22
- pymoto/solvers/solvers.py,sha256=RwHjZYYlE3oA0U9k7ukla2gOdmq57rSSJQvHqjaM7JU,10626
23
- pymoto/solvers/sparse.py,sha256=VtAdscg7USR8oG_RlS21k64J7VQDa5Rk0ByJ9fzvb_0,16621
24
- pyMOTO-1.4.0.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
25
- pyMOTO-1.4.0.dist-info/METADATA,sha256=JpafKYREI7mIGlwLQ3v5sFyskvPYPTJBt9u7H-mIhi8,5006
26
- pyMOTO-1.4.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
27
- pyMOTO-1.4.0.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
28
- pyMOTO-1.4.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
- pyMOTO-1.4.0.dist-info/RECORD,,