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.
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.0.dist-info}/METADATA +1 -1
- pyMOTO-1.5.0.dist-info/RECORD +29 -0
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.0.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +5 -5
- pymoto/common/domain.py +2 -1
- pymoto/common/dyadcarrier.py +55 -26
- pymoto/common/mma.py +60 -77
- pymoto/core_objects.py +12 -4
- pymoto/modules/assembly.py +129 -15
- pymoto/modules/io.py +63 -1
- pymoto/modules/linalg.py +73 -12
- pymoto/modules/scaling.py +2 -1
- pymoto/routines.py +11 -0
- pymoto/solvers/__init__.py +2 -2
- pymoto/solvers/auto_determine.py +1 -1
- pymoto/solvers/matrix_checks.py +7 -3
- pymoto/solvers/sparse.py +1 -0
- pyMOTO-1.4.0.dist-info/RECORD +0 -29
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.0.dist-info}/LICENSE +0 -0
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.0.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.0.dist-info}/zip-safe +0 -0
@@ -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,,
|
pymoto/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
__version__ = '1.
|
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
|
|
pymoto/common/dyadcarrier.py
CHANGED
@@ -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
|
42
|
-
v
|
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 =
|
52
|
-
self.vlen =
|
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
|
-
|
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
|
-
|
145
|
-
|
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(
|
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
|
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
|
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
|
33
|
-
|
34
|
-
|
35
|
-
subject to
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
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-
|
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
|
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
|
-
|
541
|
-
|
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
|
-
|
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
|
-
|
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)]
|
pymoto/modules/assembly.py
CHANGED
@@ -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 ``(
|
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 ``(...,
|
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(
|
341
|
-
|
342
|
-
|
343
|
-
|
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 =
|
347
|
-
self.usiz = ndof * domain.nnodes
|
348
|
+
self.dofconn = None
|
348
349
|
|
349
350
|
def _response(self, u):
|
350
|
-
|
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 ``(#
|
393
|
+
- ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
|
376
394
|
|
377
395
|
Output Signal:
|
378
|
-
- ``e``: Strain matrix of size ``(#
|
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 ``(#
|
429
|
+
- ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
|
412
430
|
|
413
431
|
Output Signal:
|
414
|
-
- ``s``: Stress matrix of size ``(#
|
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.
|
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 =
|
255
|
-
self.iscomplex =
|
254
|
+
self.issparse = matrix_is_sparse(mat) # Check if it is a sparse matrix
|
255
|
+
self.iscomplex = matrix_is_complex(mat) # Check if it is a complex-valued matrix
|
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 =
|
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
|
-
|
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
|
-
|
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)
|
pymoto/solvers/__init__.py
CHANGED
@@ -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',
|
pymoto/solvers/auto_determine.py
CHANGED
@@ -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 =
|
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:
|
pymoto/solvers/matrix_checks.py
CHANGED
@@ -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
|
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
|
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
|
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
|
pyMOTO-1.4.0.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|