pyMOTO 1.4.0__py3-none-any.whl → 1.5.1__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.1
4
4
  Summary: A modular approach for topology optimization
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -51,7 +51,7 @@ automatically calculated.
51
51
 
52
52
  # Quick start installation
53
53
  1. Make sure you have Python running in some kind of virtual environment (e.g.
54
- [conda](https://docs.conda.io/projects/conda/en/stable/), [miniconda](https://docs.conda.io/en/latest/miniconda.html),
54
+ [uv](https://docs.astral.sh/uv/guides/install-python/), [conda](https://docs.conda.io/projects/conda/en/stable/), [miniconda](https://docs.conda.io/en/latest/miniconda.html),
55
55
  [venv](https://realpython.com/python-virtual-environments-a-primer/))
56
56
  2. Install the pymoto Python package (and its dependencies)
57
57
  - Option A (conda): If you are working with Conda, install by `conda install -c aatmdelissen pymoto`
@@ -0,0 +1,29 @@
1
+ pymoto/__init__.py,sha256=i4B3bc954raAJs0lh0vycgWQ3n6FQCL5oKllmaHtt8M,2058
2
+ pymoto/core_objects.py,sha256=JDY5OBnzSnmXp35-gY_Gqawjh6C2x8dL1cEV8KAW944,27343
3
+ pymoto/routines.py,sha256=inz5GbSnvmbD3WKmaNKATTc_SthKsAMPuRc8Jaub4xs,15568
4
+ pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
+ pymoto/common/domain.py,sha256=1peaQJuePBZCWjW2oMhRxlhOl1FuhthEE1aWW9dMumk,18168
6
+ pymoto/common/dyadcarrier.py,sha256=Q0MCIP4b-W_RuYxynvY-voCHfeZ4oF12ykAuVUnmLq4,19433
7
+ pymoto/common/mma.py,sha256=SaFdZhpL5uoQRLWxQrElcjRMzGG4CL3BcPc77z3eMVE,24612
8
+ pymoto/modules/aggregation.py,sha256=Oi17hIJ6dic4lOPw16zmjbdC72MjB6XK34H80bnbWAI,7580
9
+ pymoto/modules/assembly.py,sha256=o_HZ4bcyK6cMNWVsrmxvv2z6oDgQcrBmB6dquQCWGC8,23216
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=BHJgS5IvFTiYYH4cXqRwSid1bfB58HOQs_jhFeLbb3A,13639
15
+ pymoto/modules/linalg.py,sha256=GIMqcIfVsMib5nIwrhx4sPT6-auLiHBJjbKCAdulLLE,22005
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=hfMRw2LChupr4sQf8qUKG6OHjhpeVAp6C9N7M4-845M,13179
21
+ pymoto/solvers/matrix_checks.py,sha256=bbrfjpTSWWnuQW3xY0_CYE8yrh5gA9K5b1LzHEOFAxI,1663
22
+ pymoto/solvers/solvers.py,sha256=Srn44oRonZIjJq-XVpJY9KoTWV0R7zBWFaSmXKWsUCw,11870
23
+ pymoto/solvers/sparse.py,sha256=BPw-2Q4lgbmtjCl8eLEmDdWhjqD7RSLrVYy_kQN_LdU,17144
24
+ pyMOTO-1.5.1.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
25
+ pyMOTO-1.5.1.dist-info/METADATA,sha256=_ZZ3mHjsLTWtdHuM2k8VcO2t3xF6gt67gFVFHibXMJ8,5062
26
+ pyMOTO-1.5.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
27
+ pyMOTO-1.5.1.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
28
+ pyMOTO-1.5.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
+ pyMOTO-1.5.1.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.1'
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
@@ -91,6 +91,7 @@ class DomainDefinition:
91
91
  if self.nelz is None:
92
92
  self.nelz = 0
93
93
  self.unitx, self.unity, self.unitz = unitx, unity, unitz
94
+ self.origin = np.array([0.0, 0.0, 0.0])
94
95
 
95
96
  self.dim = 1 if (self.nelz == 0 and self.nely == 0) else (2 if self.nelz == 0 else 3)
96
97
 
@@ -126,9 +127,10 @@ class DomainDefinition:
126
127
  self.conn[el, :] = self.get_elemconnectivity(elx, ely, elz)
127
128
 
128
129
  # Helper for element slicing
129
- eli, elj, elk = np.meshgrid(np.arange(self.nelx), np.arange(self.nely), np.arange(self.nelz), indexing='ij')
130
+ eli, elj, elk = np.meshgrid(np.arange(self.nelx), np.arange(self.nely), np.arange(max(self.nelz, 1)), indexing='ij')
130
131
  self.elements = self.get_elemnumber(eli, elj, elk)
131
132
 
133
+ # Helper for node slicing
132
134
  ndi, ndj, ndk = np.meshgrid(np.arange(self.nelx+1), np.arange(self.nely+1), np.arange(self.nelz+1), indexing='ij')
133
135
  self.nodes = self.get_nodenumber(ndi, ndj, ndk)
134
136
 
@@ -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):
@@ -364,7 +393,7 @@ class DyadCarrier(object):
364
393
  exprvars = (rowvar, colvar) if mat is None else (rowvar, matvar, colvar)
365
394
  expr = ','.join(exprvars) + '->' + batchvar
366
395
 
367
- val = 0.0 if batchsize is None else np.zeros(batchsize)
396
+ val = 0.0 if batchsize is None else np.zeros(batchsize, dtype=np.result_type(mat, self.dtype))
368
397
  for ui, vi in zip(self.u, self.v):
369
398
  uarg = ui if rows is None else ui[rows]
370
399
  varg = vi if cols is None else vi[cols]
@@ -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
 
@@ -282,12 +305,15 @@ class MMA:
282
305
 
283
306
  # Setting up for constriants
284
307
  self.m = len(self.responses) - 1
285
- self.a = np.zeros(self.m)
286
- self.c = self.cCoef * np.ones(self.m)
308
+ self.a = kwargs.get("a", np.zeros(self.m))
309
+ if len(self.a) != self.m:
310
+ raise RuntimeError(f"Length of the a vector ({len(self.a)}) should be equal to # constraints ({self.m}).")
311
+ self.c = kwargs.get("c", np.full(self.m, self.cCoef, dtype=float))
312
+ if len(self.c) != self.m:
313
+ raise RuntimeError(f"Length of the c vector ({len(self.c)}) should be equal to # constraints ({self.m}).")
287
314
  self.d = np.ones(self.m)
288
315
  self.gold1 = np.zeros(self.m + 1)
289
316
  self.gold2 = self.gold1.copy()
290
- self.rho = self.raa0 * np.ones(self.m + 1)
291
317
 
292
318
  def response(self):
293
319
  change = 1
@@ -349,11 +375,15 @@ class MMA:
349
375
  # Calculate response
350
376
  self.funbl.response()
351
377
 
378
+ xval, _ = _concatenate_to_array([s.state for s in self.variables])
379
+
352
380
  # Save response
353
381
  f = ()
354
382
  for s in self.responses:
355
- if not np.isscalar(s.state):
383
+ if np.size(s.state) != 1:
356
384
  raise TypeError("State of responses must be scalar.")
385
+ if np.iscomplexobj(s.state):
386
+ raise TypeError("Responses must be real-valued.")
357
387
  f += (s.state, )
358
388
 
359
389
  # Check function change convergence criterion
@@ -443,57 +473,6 @@ class MMA:
443
473
  if self.offset is None:
444
474
  self.offset = self.asyinit * np.ones(self.n)
445
475
 
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
476
  # # ASYMPTOTES
498
477
  # Calculation of the asymptotes low and upp :
499
478
  # For iter = 1,2 the asymptotes are fixed depending on asyinit
@@ -536,16 +515,27 @@ class MMA:
536
515
  # # APPROXIMATE CONVEX SEPARABLE FUNCTIONS
537
516
  # Calculations of p0, q0, P, Q and b.
538
517
  # calculate the constant factor in calculations of pij and qij
518
+ # From: Svanberg(2007) - MMA and GCMMA, two methods for nonlinear optimization
519
+ dg_plus = np.maximum(+dg, 0)
520
+ dg_min = np.maximum(-dg, 0)
539
521
  dx2 = shift**2
540
- P = dx2 * np.maximum(+dg, 0)
541
- Q = dx2 * np.maximum(-dg, 0)
522
+ if '1987' in self.mmaversion:
523
+ # Original version
524
+ P = dx2 * dg_plus
525
+ Q = dx2 * dg_min
526
+ elif '2007' in self.mmaversion:
527
+ # Improved version -> Allows to use higher epsimin to get design variables closer to the bound.
528
+ P = dx2 * (1.001*dg_plus + 0.001*dg_min + 1e-5/self.dx)
529
+ Q = dx2 * (0.001*dg_plus + 1.001*dg_min + 1e-5/self.dx)
530
+ else:
531
+ raise ValueError("Only \"Svanberg1987\" or \"Svanberg2007\" are valid options")
542
532
 
543
533
  rhs = np.dot(P, 1 / shift) + np.dot(Q, 1 / shift) - g
544
534
  b = rhs[1:]
545
535
 
546
536
  # Solving the subproblem by a primal-dual Newton method
547
537
  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)
538
+ 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
539
 
550
540
  self.gold2, self.gold1 = self.gold1, g.copy()
551
541
  self.xold2, self.xold1 = self.xold1, xval.copy()
@@ -582,3 +572,4 @@ class MMA:
582
572
  print(f" | Changes: {', '.join(change_msgs)}")
583
573
 
584
574
  return xmma, change
575
+