pyMOTO 1.0.1__py3-none-any.whl → 1.0.2rc1__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.0.1
3
+ Version: 1.0.2rc1
4
4
  Summary: Example package description
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -25,6 +25,8 @@ Requires-Dist: scikit-sparse ; extra == 'dev'
25
25
  Requires-Dist: pypardiso ; extra == 'dev'
26
26
  Requires-Dist: jax[cpu] ; extra == 'dev'
27
27
 
28
+ [![10.5281/zenodo.7708738](https://zenodo.org/badge/DOI/10.5281/zenodo.7708738.svg)](https://doi.org/10.5281/zenodo.7708738)
29
+
28
30
  # pyMOTO
29
31
 
30
32
  * [Link to Documentation](https://pymoto.readthedocs.io)
@@ -64,35 +66,19 @@ A local installation for development in `pyMOTO` can be done by first downloadin
64
66
  * **SymPy** - Symbolic differentiation for `MathGeneral` module
65
67
  * **Matplotlib** - Plotting and visualisation
66
68
  * (optional) **SAO** - Sequential approximated optimizers
67
- * (optional) **opt_einsum** - Optimized function for `EinSum` module
69
+ * (optional) [**opt_einsum**](https://optimized-einsum.readthedocs.io/en/stable/install.html) - Optimized function for `EinSum` module
68
70
 
69
71
  For fast linear solvers for sparse matrices:
70
- * (optional) **scikit-umfpack** - Fast LU linear solver based on UMFPACK
71
- * (optional) **sksparse** - Fast Cholesky solver based on CHOLMOD
72
- * (optional) **CVXopt** - Another fast Cholesky solver based on CHOLMOD
73
- * (optional) **Intel OneAPI** - Non-python library with a fast PARDISO solver
72
+ * (optional) [**pypardiso**](https://github.com/haasad/PyPardisoProject) - Uses the Intel OneAPI PARDISO solver (recommended)
73
+ * (optional) [**scikit-umfpack**](https://scikit-umfpack.github.io/scikit-umfpack/install.html) - Fast LU linear solver based on UMFPACK
74
+ * (optional) [**scikit-sparse**](https://github.com/scikit-sparse/scikit-sparse) - Fast Cholesky solver based on CHOLMOD
75
+ * (optional) [**cvxopt**](https://cvxopt.org/install/index.html) - Another fast Cholesky solver based on CHOLMOD
74
76
 
75
77
  __Note on linear solvers for sparse matrices:__ Scipy implements a version of LU which is quite slow. To increase the
76
- speed of the optimization, `Intel OneAPI` is recommended as it contains a very robust and flexible solver for symmetric
78
+ speed of the optimization, `pypardiso` is recommended as it contains a very robust and flexible solver for symmetric
77
79
  and asymmetric matrices. An alternative is `scikit-umfpack` which provides a fast LU factorization. For symmetric
78
80
  matrices a Cholesky factorization is recommended (not provided with Scipy), which can be used by either installing
79
- `sksparse` or `cvxopt`.
80
-
81
-
82
- ## How to make Python fast with Intel OneAPI
83
- Intel provides a toolkit with many fast math operations and solvers called OneAPI (basekit).
84
- It can easily be installed on Linux by for instance following the steps described in https://www.intel.com/content/www/us/en/develop/documentation/installation-guide-for-intel-oneapi-toolkits-linux/top/installation/install-using-package-managers/apt.html
85
- For other OSes installation can be found in https://www.intel.com/content/www/us/en/developer/articles/guide/installation-guide-for-oneapi-toolkits.html
86
-
87
- The nice thing about OneAPI is that it also includes an optimized version of Python. To use it follow the next steps (Linux)
88
-
89
- 1. `source <intel install location>/intel/oneapi/setvars.sh` (usually installed in `/opt/intel` or `/opt/ud/intel`). This loads the Intel OneAPI package.
90
- 2. `conda create --name <venv_name> --clone base` to create a new conda virtual environment to work in.
91
- 3. `conda activate <venv_name>` to activate the virtual environment.
92
-
93
- ### Usage of multi-thread linear solvers
94
- Intel has a Pardiso type linear solver for fast solution of large systems.
95
- To use it.....
81
+ `scikit-sparse` or `cvxopt`.
96
82
 
97
83
  # License
98
84
  pyMOTO is available under te [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,23 @@
1
+ pymoto/__init__.py,sha256=owmlGqvdF7Gc00pZRuSX5G_lt05F34xtgsVWP4QijPA,2031
2
+ pymoto/core_objects.py,sha256=V870S_G8xTArGoIM9VjMGCEXzeaolCJ_HJ2lbFzLE58,23491
3
+ pymoto/routines.py,sha256=WHOeTu5NF6kCiVYIT2pmACUyP_rCxdRU5dF9I8VyQWQ,13972
4
+ pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
+ pymoto/common/domain.py,sha256=Bo5Qqnben3Ih0IuprnoO2CmG09shEVUWt1hyiVFnB9U,15136
6
+ pymoto/common/dyadcarrier.py,sha256=iszzJMLiHbnLn4_UoCa8PkhdEQPKytLlDxXWsGCmze8,17206
7
+ pymoto/common/mma.py,sha256=7KhJJM48taB0EkTpQWPXNSPNL4tqFNdQhI4qwf1LNgE,23304
8
+ pymoto/common/solvers.py,sha256=92brZkAXc-8jNM3YfL24CCV99uuz0u4weIgyrn8__aw,8143
9
+ pymoto/common/solvers_dense.py,sha256=vuBUp3y4qJLwmsXbFQ_tEb-7LqqCEemFunTn1z_Qu0U,9901
10
+ pymoto/common/solvers_sparse.py,sha256=QVbGTwGtbhOqRUyg2gHmY-K5haiPbskGA6uj_g-dKz8,15776
11
+ pymoto/modules/assembly.py,sha256=j5m-Qq9wKbvKauhWpwtGKHDElStMIbXacZxxapWMj6I,11313
12
+ pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
13
+ pymoto/modules/complex.py,sha256=vwzqRo5W319mVf_RqbB7LpYe7jXruVxa3ZV560Iq39k,4421
14
+ pymoto/modules/filter.py,sha256=8A-dmWSFEqFyQcutjFv__pfgAwszCVZeZgLxuG9hi0g,18840
15
+ pymoto/modules/generic.py,sha256=UsTIYeRINjQUyLKieian1lYHSsigTNFDdPSPIK-p8PA,9386
16
+ pymoto/modules/io.py,sha256=4k5S-YQHKhw_HwmqOoYQWFEzdcL5nMJ5fVD2FJFqpFg,10532
17
+ pymoto/modules/linalg.py,sha256=afB_NHfX2hvkVHI2U2DCEvg4kgfu3QwzlgUeV3DoR1o,16185
18
+ pyMOTO-1.0.2rc1.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
19
+ pyMOTO-1.0.2rc1.dist-info/METADATA,sha256=YIavv2fQxY2fz79TPmAaWzlxmbo6uzx3Dswg0kQUjgc,4471
20
+ pyMOTO-1.0.2rc1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
21
+ pyMOTO-1.0.2rc1.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
22
+ pyMOTO-1.0.2rc1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
+ pyMOTO-1.0.2rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.40.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.0.1'
1
+ __version__ = '1.0.2-rc1'
2
2
 
3
3
  from .core_objects import Signal, Module, Network, make_signals
4
4
  from .routines import finite_difference, minimize_oc, minimize_mma
@@ -6,13 +6,14 @@ from .routines import finite_difference, minimize_oc, minimize_mma
6
6
  # Imports from common
7
7
  from .common.dyadcarrier import DyadCarrier
8
8
  from .common.domain import DomainDefinition
9
+ from .common.mma import MMA
9
10
  from .common.solvers import matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian, \
10
11
  LinearSolver, LDAWrapper
11
12
  from .common.solvers_dense import SolverDiagonal, SolverDenseQR, SolverDenseLU, SolverDenseCholesky, SolverDenseLDL
12
13
  from .common.solvers_sparse import SolverSparsePardiso, SolverSparseLU, SolverSparseCholeskyScikit, SolverSparseCholeskyCVXOPT
13
14
 
14
15
  # Import from modules
15
- from .modules.generic import MathGeneral, EinSum
16
+ from .modules.generic import MathGeneral, EinSum, ConcatSignal
16
17
  from .modules.linalg import Inverse, LinSolve, EigenSolve
17
18
  from .modules.assembly import AssembleGeneral, AssembleStiffness, AssembleMass
18
19
  from .modules.filter import FilterConv, Filter, DensityFilter, OverhangFilter
@@ -24,6 +25,7 @@ __all__ = [
24
25
  'Signal', 'Module', 'Network', 'make_signals',
25
26
  'finite_difference', 'minimize_oc', 'minimize_mma',
26
27
  # Common
28
+ 'MMA',
27
29
  'DyadCarrier',
28
30
  'DomainDefinition',
29
31
  'matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
@@ -31,7 +33,7 @@ __all__ = [
31
33
  'SolverDiagonal', 'SolverDenseQR', 'SolverDenseLU', 'SolverDenseCholesky', 'SolverDenseLDL',
32
34
  'SolverSparsePardiso', 'SolverSparseLU', 'SolverSparseCholeskyScikit', 'SolverSparseCholeskyCVXOPT',
33
35
  # Modules
34
- "MathGeneral", "EinSum",
36
+ "MathGeneral", "EinSum", "ConcatSignal",
35
37
  "Inverse", "LinSolve", "EigenSolve",
36
38
  "AssembleGeneral", "AssembleStiffness", "AssembleMass",
37
39
  "FilterConv", "Filter", "DensityFilter", "OverhangFilter",
pymoto/common/mma.py ADDED
@@ -0,0 +1,554 @@
1
+ import numpy as np
2
+ from pymoto.utils import _parse_to_list, _concatenate_to_array
3
+
4
+
5
+ def residual(x, y, z, lam, xsi, eta, mu, zet, s, upp, low, P0, P1, Q0, Q1, epsi, a0, a, b, c, d, alfa, beta):
6
+ # upcoming lines determine the left hand sides, i.e. the resiudals of all constraints
7
+ ux1 = upp - x
8
+ xl1 = x - low
9
+
10
+ plam = P0 + np.dot(lam, P1)
11
+ qlam = Q0 + np.dot(lam, Q1)
12
+ gvec = np.dot(P1, 1/ux1) + np.dot(Q1, 1/xl1)
13
+
14
+ # gradient of approximation function wrt x
15
+ dpsidx = plam / (ux1**2) - qlam / (xl1**2)
16
+
17
+ # put all residuals in one line
18
+ return np.concatenate([
19
+ dpsidx - xsi + eta, # rex [n]
20
+ c + d * y - mu - lam, # rey [m]
21
+ np.array([a0 - zet - np.dot(a, lam)]), # rez [1]
22
+ gvec - a * z - y + s - b, # relam [m]
23
+ xsi * (x - alfa) - epsi, # rexsi [n]
24
+ eta * (beta - x) - epsi, # reeta [n]
25
+ mu * y - epsi, # remu [m]
26
+ np.array([zet * z - epsi]), # rezet [1]
27
+ lam * s - epsi, # res [m]
28
+ ])
29
+
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.
39
+ """
40
+
41
+ n, m = len(alfa), len(a)
42
+ epsi = 1.0
43
+ maxittt = 400
44
+ x = 0.5 * (alfa + beta)
45
+ y = np.ones(m)
46
+ z = 1.0
47
+ lam = np.ones(m)
48
+ GG = np.empty((m, n))
49
+ xsi = np.maximum((1.0 / (x - alfa)), 1)
50
+ eta = np.maximum((1.0 / (beta - x)), 1)
51
+ mu = np.maximum(1, 0.5 * c)
52
+ zet = 1.0
53
+ s = np.ones(m)
54
+ bb = np.empty(m+1)
55
+ AA = np.empty((m+1, m+1))
56
+
57
+ P0 = np.ascontiguousarray(P[0, :])
58
+ Q0 = np.ascontiguousarray(Q[0, :])
59
+ P1 = np.ascontiguousarray(P[1:, :])
60
+ Q1 = np.ascontiguousarray(Q[1:, :])
61
+
62
+ itera = 0
63
+ while epsi > epsimin:
64
+ # main loop + 1
65
+ itera = itera + 1
66
+
67
+ # upcoming lines determine the left hand sides, i.e. the resiudals of all constraints
68
+ 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)
69
+ residunorm = np.linalg.norm(residu)
70
+ residumax = np.max(np.abs(residu))
71
+
72
+ ittt = 0
73
+ # the algorithm is terminated when the maximum residual has become smaller than 0.9*epsilon
74
+ # and epsilon has become sufficiently small (and not too many iterations are used)
75
+ while residumax > 0.9 * epsi and ittt < maxittt:
76
+ ittt = ittt + 1
77
+
78
+ # Newton's method: first create the variable steps
79
+
80
+ # precalculations for PSIjj (or diagx)
81
+ ux1 = upp - x
82
+ xl1 = x - low
83
+ ux2 = ux1 ** 2
84
+ xl2 = xl1 ** 2
85
+ ux3 = ux1 * ux2
86
+ xl3 = xl1 * xl2
87
+
88
+ uxinv1 = 1.0 / ux1
89
+ xlinv1 = 1.0 / xl1
90
+ uxinv2 = 1.0 / ux2
91
+ xlinv2 = 1.0 / xl2
92
+
93
+ plam = P0 + np.dot(lam, P1)
94
+ qlam = Q0 + np.dot(lam, Q1)
95
+ gvec = np.dot(P1, uxinv1) + np.dot(Q1, xlinv1)
96
+
97
+ # CG is an m x n matrix with values equal to partial derivative of constraints wrt variables
98
+ GG[:, :] = P1 * uxinv2 - Q1 * xlinv2
99
+
100
+ # derivative of PSI wrt x
101
+ dpsidx = plam / ux2 - qlam / xl2
102
+
103
+ # calculation of right hand sides dx, dy, dz, dlam
104
+ delx = dpsidx - epsi / (x - alfa) + epsi / (beta - x)
105
+ dely = c + d * y - lam - epsi / y
106
+ delz = a0 - np.dot(a, lam) - epsi / z
107
+ dellam = gvec - a * z - y - b + epsi / lam
108
+
109
+ # calculation of diagonal matrices Dx Dy Dlam
110
+ diagx = 2 * (plam / ux3 + qlam / xl3) + xsi / (x - alfa) + eta / (beta - x)
111
+ diagy = d + mu / y
112
+
113
+ diaglam = s / lam
114
+ diaglamyi = diaglam + 1.0 / diagy
115
+
116
+ # different options depending on the number of constraints
117
+ # considering the fact I will probably not use local constraints I removed the option
118
+
119
+ # normally here is a statement if m < n
120
+ bb[:-1] = dellam + dely / diagy - np.dot(GG, (delx / diagx))
121
+ bb[-1] = delz
122
+
123
+ AA[:-1, :-1] = np.diag(diaglamyi) + np.dot((GG / diagx), GG.T)
124
+ AA[-1, :-1] = a
125
+ AA[:-1, -1] = a
126
+ AA[-1, -1] = -zet/z
127
+ # solve system for delta lambda and delta z
128
+ solut = np.linalg.solve(AA, bb)
129
+
130
+ # solution of delta vars
131
+ dlam = solut[0:m]
132
+ dz = solut[m]
133
+ dx = -delx / diagx - np.dot(dlam, GG) / diagx
134
+ dy = -dely / diagy + dlam / diagy
135
+ dxsi = -xsi + epsi / (x - alfa) - (xsi * dx) / (x - alfa)
136
+ deta = -eta + epsi / (beta - x) + (eta * dx) / (beta - x)
137
+ dmu = -mu + epsi / y - (mu * dy) / y
138
+ dzet = -zet + epsi / z - zet * dz / z
139
+ ds = -s + epsi / lam - (s * dlam) / lam
140
+
141
+ # calculate the step size
142
+ stmy = -1.01*np.min(dy/y)
143
+ stmz = -1.01 * dz / z
144
+ stmlam = -1.01*np.min(dlam / lam)
145
+ stmxsi = -1.01*np.min(dxsi / xsi)
146
+ stmeta = -1.01 * np.min(deta / eta)
147
+ stmmu = -1.01 * np.min(dmu / mu)
148
+ stmzet = -1.01 * dzet / zet
149
+ stms = -1.01 * np.min(ds / s)
150
+ stmxx = max(stmy, stmz, stmlam, stmxsi, stmeta, stmmu, stmzet, stms)
151
+
152
+ # put variables and accompanying changes in alist
153
+ stmalfa = -1.01 * np.min(dx / (x - alfa))
154
+ stmbeta = 1.01 * np.max(dx / (beta - x))
155
+
156
+ # Initial step size
157
+ steg = 1.0 / max(stmalfa, stmbeta, stmxx, 1.0)
158
+
159
+ # set old variables
160
+ xold = x.copy()
161
+ yold = y.copy()
162
+ zold = z
163
+ lamold = lam.copy()
164
+ xsiold = xsi.copy()
165
+ etaold = eta.copy()
166
+ muold = mu.copy()
167
+ zetold = zet
168
+ sold = s.copy()
169
+
170
+ # Do linesearch
171
+ itto = 0
172
+ while itto < maxittt:
173
+ # Find new set of variables with stepsize
174
+ x[:] = xold + steg * dx
175
+ y[:] = yold + steg * dy
176
+ z = zold + steg * dz
177
+ lam[:] = lamold + steg * dlam
178
+ xsi[:] = xsiold + steg * dxsi
179
+ eta[:] = etaold + steg * deta
180
+ mu[:] = muold + steg * dmu
181
+ zet = zetold + steg * dzet
182
+ s[:] = sold + steg * ds
183
+
184
+ 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
+ if np.linalg.norm(residu) < residunorm:
186
+ break
187
+ itto += 1
188
+ steg /= 2 # Reduce stepsize
189
+
190
+ residunorm = np.linalg.norm(residu)
191
+ residumax = np.max(np.abs(residu))
192
+
193
+ if ittt > maxittt - 2:
194
+ print(f"MMA Subsolver: itt = {ittt}, at epsi = {epsi}")
195
+ # decrease epsilon with factor 10
196
+ epsi /= 10
197
+
198
+ # ## END OF SUBSOLVE
199
+ return x, y, z, lam, xsi, eta, mu, zet, s
200
+
201
+
202
+ class MMA:
203
+ """
204
+ Block for the MMA algorithm
205
+ The design variables are set by keyword <variables> accepting a list of variables.
206
+ The responses are set by keyword <responses> accepting a list of signals.
207
+ If none are given, the internal sig_in and sig_out are used.
208
+
209
+ Args:
210
+ function: The Network defining the optimization problem
211
+ variables: The Signals defining the design variables
212
+ responses: A list of Signals, where the first is to be minimized and the others are constraints.
213
+
214
+ Keyword Args:
215
+ tolx: Stopping criterium for relative design change
216
+ tolf: Stopping criterium for relative objective change
217
+ maxit: Maximum number of iteration
218
+ move: Move limit on relative variable change per iteration
219
+ xmin: Minimum design variable (can be a vector)
220
+ xmax: Maximum design variable (can be a vector)
221
+ fn_callback: A function that is called just before calling the response() in each iteration
222
+ verbosity: 0 - No prints, 1 - Only convergence message, 2 - Convergence and iteration info, 3 - Extended info
223
+
224
+ """
225
+
226
+ def __init__(self, function, variables, responses, tolx=1e-4, tolf=0.0, move=0.1, maxit=100, xmin=0.0, xmax=1.0, fn_callback=None, verbosity=0, **kwargs):
227
+ self.funbl = function
228
+ self.verbosity = verbosity
229
+
230
+ self.variables = _parse_to_list(variables)
231
+ self.responses = _parse_to_list(responses)
232
+
233
+ self.iter = 0
234
+
235
+ # Convergence options
236
+ self.tolX = tolx
237
+ self.tolf = tolf
238
+ self.maxIt = maxit
239
+
240
+ # Operational options
241
+ self.xmax = xmax
242
+ self.xmin = xmin
243
+ self.move = move
244
+
245
+ self.pijconst = kwargs.get("pijconst", 1e-3)
246
+
247
+ self.a0 = kwargs.get("a0", 1.0)
248
+
249
+ self.epsimin = kwargs.get("epsimin", 1e-7) # Or 1e-7 ?? witout sqrt(m+n) or 1e-9
250
+ self.raa0 = kwargs.get("raa0", 1e-5)
251
+
252
+ self.cCoef = kwargs.get("cCoef", 1e3) # Svanberg uses 1e3 in example? Old code had 1e7
253
+
254
+ # Not used
255
+ self.dxmin = kwargs.get("dxmin", 1e-5)
256
+
257
+ self.albefa = kwargs.get("albefa", 0.1)
258
+ self.asyinit = kwargs.get("asyinit", 0.5)
259
+ self.asyincr = kwargs.get("asyincr", 1.2)
260
+ self.asydecr = kwargs.get("asydecr", 0.7)
261
+ self.asybound = kwargs.get("asybound", 10.0)
262
+
263
+ self.ittomax = kwargs.get("ittomax", 400)
264
+
265
+ self.iterinitial = kwargs.get("iterinitial", 2.5)
266
+
267
+ self.fn_callback = fn_callback
268
+
269
+ # Numbers
270
+ self.n = None # len(x0)
271
+ self.dx = None
272
+ self.xold1 = None
273
+ self.xold2 = None
274
+ self.low = None
275
+ self.upp = None
276
+ self.offset = None
277
+
278
+ # Setting up for constriants
279
+ self.m = len(self.responses) - 1
280
+ self.a = np.zeros(self.m)
281
+ self.c = self.cCoef * np.ones(self.m)
282
+ self.d = np.ones(self.m)
283
+ self.gold1 = np.zeros(self.m + 1)
284
+ self.gold2 = self.gold1.copy()
285
+ self.rho = self.raa0 * np.ones(self.m + 1)
286
+
287
+ def response(self):
288
+ change = 1
289
+
290
+ # Save initial state
291
+ xval, self.cumlens = _concatenate_to_array([s.state for s in self.variables])
292
+ self.n = len(xval)
293
+
294
+ # Set outer bounds
295
+ if not hasattr(self.xmin, '__len__'):
296
+ self.xmin = self.xmin * np.ones_like(xval)
297
+ elif len(self.xmin) == len(self.variables):
298
+ xminvals = self.xmin
299
+ self.xmin = np.zeros_like(xval)
300
+ for i in range(len(xminvals)):
301
+ self.xmin[self.cumlens[i]:self.cumlens[i+1]] = xminvals[i]
302
+
303
+ if len(self.xmin) != self.n:
304
+ raise RuntimeError(
305
+ "Length of the xmin vector not correct ({} != {})".format(len(self.xmin), self.n))
306
+
307
+ if not hasattr(self.xmax, '__len__'):
308
+ self.xmax = self.xmax * np.ones_like(xval)
309
+ elif len(self.xmax) == len(self.variables):
310
+ xmaxvals = self.xmax
311
+ self.xmax = np.zeros_like(xval)
312
+ for i in range(len(xmaxvals)):
313
+ self.xmax[self.cumlens[i]:self.cumlens[i + 1]] = xmaxvals[i]
314
+
315
+ if len(self.xmax) != self.n:
316
+ raise RuntimeError("Length of the xmax vector not correct ({} != {})".format(len(self.xmax), self.n))
317
+
318
+ # Set movelimit in case of multiple
319
+ if hasattr(self.move, '__len__'):
320
+ if len(self.move) == len(self.variables):
321
+ movevals = self.move
322
+ self.move = np.zeros_like(xval)
323
+ for i in range(len(movevals)):
324
+ self.move[self.cumlens[i]:self.cumlens[i + 1]] = movevals[i]
325
+ elif len(self.move) != self.n:
326
+ raise RuntimeError("Length of the move vector not correct ({} != {})".format(len(self.move), self.n))
327
+
328
+ fcur = 0.0
329
+ while self.iter < self.maxIt:
330
+ # Reset all signals in function block
331
+ self.funbl.reset()
332
+
333
+ # Set the new states
334
+ for i, s in enumerate(self.variables):
335
+ if self.cumlens[i+1]-self.cumlens[i] == 1:
336
+ try:
337
+ s.state[:] = xval[self.cumlens[i]]
338
+ except TypeError:
339
+ s.state = xval[self.cumlens[i]]
340
+ else:
341
+ s.state[:] = xval[self.cumlens[i]:self.cumlens[i+1]]
342
+
343
+ if self.fn_callback is not None:
344
+ self.fn_callback()
345
+
346
+ # Calculate response
347
+ self.funbl.response()
348
+
349
+ # Update the states
350
+ for i, s in enumerate(self.variables):
351
+ if self.cumlens[i+1]-self.cumlens[i] == 1:
352
+ try:
353
+ xval[self.cumlens[i]] = s.state[:]
354
+ except TypeError:
355
+ xval[self.cumlens[i]] = s.state
356
+ else:
357
+ xval[self.cumlens[i]:self.cumlens[i+1]] = s.state[:]
358
+
359
+ # Save response
360
+ f = ()
361
+ for s in self.responses:
362
+ f += (s.state, )
363
+
364
+ # Check function change convergence criterion
365
+ fprev, fcur = fcur, self.responses[0].state
366
+ rel_fchange = abs(fcur-fprev)/abs(fcur)
367
+ if rel_fchange < self.tolf:
368
+ if self.verbosity >= 1:
369
+ print(f"MMA converged: Relative function change |Δf|/|f| ({rel_fchange}) below tolerance ({self.tolf})")
370
+ break
371
+
372
+ # Calculate and save sensitivities
373
+ df = ()
374
+ for i, s_out in enumerate(self.responses):
375
+ for s in self.responses:
376
+ s.reset()
377
+
378
+ s_out.sensitivity = s_out.state*0 + 1.0
379
+
380
+ self.funbl.sensitivity()
381
+
382
+ sens_list = []
383
+ for v in self.variables:
384
+ sens_list.append(v.sensitivity if v.sensitivity is not None else 0*v.state)
385
+ dff, _ = _concatenate_to_array(sens_list)
386
+ df += (dff, )
387
+
388
+ # Reset sensitivities for the next response
389
+ self.funbl.reset()
390
+
391
+ # Display info on variables
392
+ if self.verbosity >= 3:
393
+ for i, s in enumerate(self.variables):
394
+ isscal = self.cumlens[i + 1] - self.cumlens[i] == 1
395
+ msg = "{0:>10s} = ".format(s.tag)
396
+ if isscal:
397
+ try:
398
+ msg += " {0: .3e} ".format(s.state)
399
+ except TypeError:
400
+ msg += " {0: .3e} ".format(s.state[0])
401
+ else:
402
+ msg += "[{0: .3e} ... {1: .3e}] ".format(min(s.state), max(s.state))
403
+ for j, s_out in enumerate(self.responses):
404
+ msg += "| {0:>10s}/{1:10s} = ".format("d"+s_out.tag, "d"+s.tag)
405
+ if isscal:
406
+ msg += " {0: .3e} ".format(df[j][self.cumlens[i]])
407
+ else:
408
+ msg += "[{0: .3e} ... {1: .3e}] ".format(min(df[j][self.cumlens[i]:self.cumlens[i+1]]), max(df[j][self.cumlens[i]:self.cumlens[i+1]]))
409
+ print(msg)
410
+
411
+ self.iter += 1
412
+ xnew, change = self.mmasub(xval.copy(), np.hstack(f), np.vstack(df))
413
+
414
+ # Stopping criteria on step size
415
+ rel_stepsize = np.linalg.norm((xval - xnew)/self.dx) / np.linalg.norm(xval/self.dx)
416
+ if rel_stepsize < self.tolX:
417
+ if self.verbosity >= 1:
418
+ print(f"MMA converged: Relative stepsize |Δx|/|x| ({rel_stepsize}) below tolerance ({self.tolX})")
419
+ break
420
+
421
+ xval = xnew
422
+
423
+ def mmasub(self, xval, g, dg):
424
+ if self.dx is None:
425
+ self.dx = self.xmax - self.xmin
426
+ if self.offset is None:
427
+ self.offset = self.asyinit * np.ones(self.n)
428
+
429
+ # Minimize f_0(x) + a_0*z + sum( c_i*y_i + 0.5*d_i*(y_i)^2 )
430
+ # subject to f_i(x) - a_i*z - y_i <= 0, i = 1,...,m
431
+ # xmin_j <= x_j <= xmax_j, j = 1,...,n
432
+ # z >= 0, y_i >= 0, i = 1,...,m
433
+ # *** INPUT:
434
+ #
435
+ # m = The number of general constraints.
436
+ # n = The number of variables x_j.
437
+ # iter = Current iteration number ( =1 the first time mmasub is called).
438
+ # xval = Column vector with the current values of the variables x_j.
439
+ # xmin = Column vector with the lower bounds for the variables x_j.
440
+ # xmax = Column vector with the upper bounds for the variables x_j.
441
+ # xold1 = xval, one iteration ago (provided that iter>1).
442
+ # xold2 = xval, two iterations ago (provided that iter>2).
443
+ # f0val = The value of the objective function f_0 at xval.
444
+ # df0dx = Column vector with the derivatives of the objective function
445
+ # f_0 with respect to the variables x_j, calculated at xval.
446
+ # fval = Column vector with the values of the constraint functions f_i,
447
+ # calculated at xval.
448
+ # dfdx = (m x n)-matrix with the derivatives of the constraint functions
449
+ # f_i with respect to the variables x_j, calculated at xval.
450
+ # dfdx(i,j) = the derivative of f_i with respect to x_j.
451
+ # low = Column vector with the lower asymptotes from the previous
452
+ # iteration (provided that iter>1).
453
+ # upp = Column vector with the upper asymptotes from the previous
454
+ # iteration (provided that iter>1).
455
+ # a0 = The constants a_0 in the term a_0*z.
456
+ # a = Column vector with the constants a_i in the terms a_i*z.
457
+ # c = Column vector with the constants c_i in the terms c_i*y_i.
458
+ # d = Column vector with the constants d_i in the terms 0.5*d_i*(y_i)^2.
459
+ #
460
+
461
+ # *** OUTPUT:
462
+ #
463
+ # xmma = Column vector with the optimal values of the variables x_j
464
+ # in the current MMA subproblem.
465
+ # ymma = Column vector with the optimal values of the variables y_i
466
+ # in the current MMA subproblem.
467
+ # zmma = Scalar with the optimal value of the variable z
468
+ # in the current MMA subproblem.
469
+ # lam = Lagrange multipliers for the m general MMA constraints.
470
+ # xsi = Lagrange multipliers for the n constraints alfa_j - x_j <= 0.
471
+ # eta = Lagrange multipliers for the n constraints x_j - beta_j <= 0.
472
+ # mu = Lagrange multipliers for the m constraints -y_i <= 0.
473
+ # zet = Lagrange multiplier for the single constraint -z <= 0.
474
+ # s = Slack variables for the m general MMA constraints.
475
+ # low = Column vector with the lower asymptotes, calculated and used
476
+ # in the current MMA subproblem.
477
+ # upp = Column vector with the upper asymptotes, calculated and used
478
+ # in the current MMA subproblem.
479
+
480
+ # # ASYMPTOTES
481
+ # Calculation of the asymptotes low and upp :
482
+ # For iter = 1,2 the asymptotes are fixed depending on asyinit
483
+ if self.xold1 is not None and self.xold2 is not None:
484
+ # depending on if the signs of xval - xold and xold - xold2 are opposite, indicating an oscillation
485
+ # in the variable xi
486
+ # if the signs are equal the asymptotes are slowing down the convergence and should be relaxed
487
+
488
+ # check for oscillations in variables
489
+ # if zzz positive no oscillations, if negative --> oscillations
490
+ zzz = (xval - self.xold1) * (self.xold1 - self.xold2)
491
+ # decrease those variables that are oscillating equal to asydecr
492
+ self.offset[zzz > 0] *= self.asyincr
493
+ self.offset[zzz < 0] *= self.asydecr
494
+
495
+ # check with minimum and maximum bounds of asymptotes, as they cannot be to close or far from the variable
496
+ # give boundaries for upper and lower asymptotes
497
+ self.offset = np.clip(self.offset, 1/(self.asybound**2), self.asybound)
498
+
499
+ # Update asymptotes
500
+ shift = self.offset * self.dx
501
+ self.low = xval - shift
502
+ self.upp = xval + shift
503
+
504
+ # # VARIABLE BOUNDS
505
+ # Calculation of the bounds alfa and beta :
506
+ # use albefa to limit the maximum change of variables wrt the lower and upper asymptotes
507
+ # as it should remain within both asymptotes
508
+ zzl1 = self.low + self.albefa * shift
509
+ # use movelimit to limit the maximum change of variables
510
+ zzl2 = xval - self.move * self.dx
511
+ # minimum variable bounds
512
+ alfa = np.maximum.reduce([zzl1, zzl2, self.xmin])
513
+
514
+ zzu1 = self.upp - self.albefa * shift
515
+ zzu2 = xval + self.move * self.dx
516
+ # maximum variable bounds
517
+ beta = np.minimum.reduce([zzu1, zzu2, self.xmax])
518
+
519
+ # # APPROXIMATE CONVEX SEPARABLE FUNCTIONS
520
+ # Calculations of p0, q0, P, Q and b.
521
+ # calculate the constant factor in calculations of pij and qij
522
+ dx2 = shift**2
523
+ P = dx2 * np.maximum(+dg, 0)
524
+ Q = dx2 * np.maximum(-dg, 0)
525
+
526
+ rhs = np.dot(P, 1 / shift) + np.dot(Q, 1 / shift) - g
527
+ b = rhs[1:]
528
+
529
+ # Solving the subproblem by a primal-dual Newton method
530
+ epsimin_scaled = self.epsimin*np.sqrt(self.m + self.n)
531
+ 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)
532
+
533
+ self.gold2, self.gold1 = self.gold1, g.copy()
534
+ self.xold2, self.xold1 = self.xold1, xval.copy()
535
+ change = np.average(abs(xval - xmma))
536
+
537
+ if self.verbosity >= 2:
538
+ msgs = ["g{0:d}({1:s}): {2:+.4e}".format(i, s.tag, g[i]) for i, s in enumerate(self.responses)]
539
+ print("It. {0: 4d}, {1}".format(self.iter, ", ".join(msgs)))
540
+
541
+ if self.verbosity >=3:
542
+ # Print changes
543
+ printstr = "Changes: "
544
+ for i, s in enumerate(self.variables):
545
+ isscal = self.cumlens[i + 1] - self.cumlens[i] == 1
546
+ if isscal:
547
+ chg = abs(xval[self.cumlens[i]] - xmma[self.cumlens[i]])
548
+ else:
549
+ chg = np.average(abs(xval[self.cumlens[i]:self.cumlens[i + 1]] - xmma[self.cumlens[i]:self.cumlens[i + 1]]))
550
+
551
+ printstr += "{0:s} = {1:.3e} ".format("Δ_"+s.tag, chg)
552
+ print(printstr)
553
+
554
+ return xmma, change
pymoto/core_objects.py CHANGED
@@ -2,7 +2,7 @@ from typing import Union, List, Any
2
2
  import warnings
3
3
  import inspect
4
4
  import time
5
- from .utils import _parse_to_list
5
+ from .utils import _parse_to_list, _concatenate_to_array, _split_from_array
6
6
  from abc import ABC, abstractmethod
7
7
 
8
8
 
@@ -35,7 +35,7 @@ def get_init_loc():
35
35
  frame = fr
36
36
  break
37
37
  if frame is None:
38
- return ""
38
+ return "N/A", "N/A", "N/A"
39
39
  _, filename, line, func, _, _ = frame
40
40
  return filename, line, func
41
41
 
@@ -145,6 +145,10 @@ class Signal:
145
145
 
146
146
 
147
147
  class SignalSlice(Signal):
148
+ """ Slice operator for a Signal
149
+ The sliced values are referenced to their original source Signal, such that they can be used and updated in modules.
150
+ This means that updating the values in this SignalSlice changes the data in its source Signal.
151
+ """
148
152
  def __init__(self, orig_signal, sl, tag=None):
149
153
  self.orig_signal = orig_signal
150
154
  self.slice = sl
@@ -165,7 +169,7 @@ class SignalSlice(Signal):
165
169
  return None if self.orig_signal.state is None else self.orig_signal.state[self.slice]
166
170
  except Exception as e:
167
171
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
168
- raise type(e)("Get state - " + e.args[0] + self._err_str(), *e.args[1:]) from None
172
+ raise type(e)("SignalSlice.state (getter)" + self._err_str()) from e
169
173
 
170
174
  @state.setter
171
175
  def state(self, new_state):
@@ -173,7 +177,7 @@ class SignalSlice(Signal):
173
177
  self.orig_signal.state[self.slice] = new_state
174
178
  except Exception as e:
175
179
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
176
- raise type(e)("Set state - " + e.args[0] + self._err_str(), *e.args[1:]) from None
180
+ raise type(e)("SignalSlice.state (setter)" + self._err_str()) from e
177
181
 
178
182
  @property
179
183
  def sensitivity(self):
@@ -181,29 +185,29 @@ class SignalSlice(Signal):
181
185
  return None if self.orig_signal.sensitivity is None else self.orig_signal.sensitivity[self.slice]
182
186
  except Exception as e:
183
187
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
184
- raise type(e)("Get sensitivity - " + e.args[0] + self._err_str(), *e.args[1:]) from None
188
+ raise type(e)("SignalSlice.sensitivity (getter)" + self._err_str()) from e
185
189
 
186
190
  @sensitivity.setter
187
191
  def sensitivity(self, new_sens):
188
- if self.orig_signal.sensitivity is None:
189
- if new_sens is None:
190
- return # Sensitivity doesn't need to be initialized when it is set to None
191
- try:
192
- self.orig_signal.sensitivity = self.orig_signal.state*0 # Make a new copy with 0 values
193
- except TypeError:
194
- if self.orig_signal.state is None:
195
- raise TypeError("Could not initialize sensitivity because state is not set" + self._err_str()) from None
196
- else:
197
- raise TypeError(f"Could not initialize sensitivity for type \'{type(self.orig_signal.state).__name__}\'" + self._err_str()) from None
192
+ try:
193
+ if self.orig_signal.sensitivity is None:
194
+ if new_sens is None:
195
+ return # Sensitivity doesn't need to be initialized when it is set to None
196
+ try:
197
+ self.orig_signal.sensitivity = self.orig_signal.state * 0 # Make a new copy with 0 values
198
+ except TypeError:
199
+ if self.orig_signal.state is None:
200
+ raise TypeError("Could not initialize sensitivity because state is not set" + self._err_str())
201
+ else:
202
+ raise TypeError(f"Could not initialize sensitivity for type \'{type(self.orig_signal.state).__name__}\'")
198
203
 
199
- if new_sens is None:
200
- new_sens = 0 # reset() uses this
204
+ if new_sens is None:
205
+ new_sens = 0 # reset() uses this
201
206
 
202
- try:
203
207
  self.orig_signal.sensitivity[self.slice] = new_sens
204
208
  except Exception as e:
205
209
  # Possibilities: Unslicable object (TypeError) or Wrong dimensions or out of range (IndexError)
206
- raise type(e)("Set sensitivity - " + e.args[0] + self._err_str(), *e.args[1:]) from None
210
+ raise type(e)("SignalSlice.sensitivity (setter)" + self._err_str()) from e
207
211
 
208
212
 
209
213
  def make_signals(*args):
@@ -237,14 +237,26 @@ class AssembleMass(AssembleGeneral):
237
237
  # 1/36 Mass of one element
238
238
  mel = rho * np.prod(domain.element_size)
239
239
 
240
- # Consistent mass matrix
241
- ME = mel / 36 * np.array([[4.0, 0.0, 2.0, 0.0, 2.0, 0.0, 1.0, 0.0],
242
- [0.0, 4.0, 0.0, 2.0, 0.0, 2.0, 0.0, 1.0],
243
- [2.0, 0.0, 4.0, 0.0, 1.0, 0.0, 2.0, 0.0],
244
- [0.0, 2.0, 0.0, 4.0, 0.0, 1.0, 0.0, 2.0],
245
- [2.0, 0.0, 1.0, 0.0, 4.0, 0.0, 2.0, 0.0],
246
- [0.0, 2.0, 0.0, 1.0, 0.0, 4.0, 0.0, 2.0],
247
- [1.0, 0.0, 2.0, 0.0, 2.0, 0.0, 4.0, 0.0],
248
- [0.0, 1.0, 0.0, 2.0, 0.0, 2.0, 0.0, 4.0]])
249
-
240
+ if domain.dim == 2:
241
+ # Consistent mass matrix
242
+ ME = mel / 36 * np.array([[4.0, 0.0, 2.0, 0.0, 2.0, 0.0, 1.0, 0.0],
243
+ [0.0, 4.0, 0.0, 2.0, 0.0, 2.0, 0.0, 1.0],
244
+ [2.0, 0.0, 4.0, 0.0, 1.0, 0.0, 2.0, 0.0],
245
+ [0.0, 2.0, 0.0, 4.0, 0.0, 1.0, 0.0, 2.0],
246
+ [2.0, 0.0, 1.0, 0.0, 4.0, 0.0, 2.0, 0.0],
247
+ [0.0, 2.0, 0.0, 1.0, 0.0, 4.0, 0.0, 2.0],
248
+ [1.0, 0.0, 2.0, 0.0, 2.0, 0.0, 4.0, 0.0],
249
+ [0.0, 1.0, 0.0, 2.0, 0.0, 2.0, 0.0, 4.0]])
250
+ elif domain.dim == 3:
251
+ ME = np.zeros((domain.elemnodes*domain.dim, domain.elemnodes*domain.dim))
252
+ weights = np.array([8.0, 4.0, 2.0, 1.0])
253
+ for n1 in range(domain.elemnodes):
254
+ for n2 in range(domain.elemnodes):
255
+ dist = round(np.sum(abs(np.array(domain.node_numbering[n1]) - np.array(domain.node_numbering[n2])))/2)
256
+ ME[n1*domain.dim+np.arange(domain.dim), n2*domain.dim+np.arange(domain.dim)] = weights[dist]
257
+
258
+ ME *= mel / 216
259
+ else:
260
+ raise RuntimeError("Only for 2D and 3D")
250
261
  super()._prepare(domain, ME, *args, bcdiagval=bcdiagval, **kwargs)
262
+
pymoto/modules/generic.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """ Generic modules, valid for general mathematical operations """
2
2
  import numpy as np
3
3
  from pymoto.core_objects import Module
4
+ from pymoto.utils import _concatenate_to_array, _split_from_array
4
5
  try:
5
6
  from opt_einsum import contract as einsum # Faster einsum
6
7
  except ModuleNotFoundError:
@@ -200,3 +201,23 @@ class EinSum(Module):
200
201
  einsum(op, df_in, *arg_in, out=da_i)
201
202
  df_out.append(da_i)
202
203
  return df_out
204
+
205
+
206
+ class ConcatSignal(Module):
207
+ """ Concatenates data of multiple signals into one big vector """
208
+ def _response(self, *args):
209
+ state, self.cumlens = _concatenate_to_array(list(args))
210
+ return state
211
+
212
+ def _sensitivity(self, dy):
213
+ dsens = [np.zeros_like(s.state) for s in self.sig_in]
214
+ dx = _split_from_array(dy, self.cumlens)
215
+ for i, s in enumerate(self.sig_in):
216
+ if not isinstance(dsens[i], type(s.state)):
217
+ dsens[i] = type(s.state)(dx[i])
218
+ continue
219
+ try:
220
+ dsens[i][...] = dx[i]
221
+ except TypeError:
222
+ dsens[i] = type(s.state)(dx[i])
223
+ return dsens
pymoto/modules/linalg.py CHANGED
@@ -9,6 +9,7 @@ from inspect import currentframe, getframeinfo
9
9
  from pymoto import Module, DyadCarrier, LDAWrapper
10
10
  import numpy as np
11
11
  import scipy.sparse as sps
12
+ import scipy.sparse.linalg as spsla
12
13
  import scipy.linalg as spla # Dense matrix solvers
13
14
 
14
15
  from pymoto import SolverDenseLU, SolverDenseLDL, SolverDenseCholesky, SolverDiagonal, SolverDenseQR
@@ -242,22 +243,34 @@ class EigenSolve(Module):
242
243
  sorting_func: Sorting function to sort the eigenvalues, which must have signature ``func(λ,Q)``
243
244
  (default = ``numpy.argsort``)
244
245
  hermitian: Flag to omit the automatic detection for Hermitian matrix, saves some work for large matrices
246
+ nmodes: Number of modes to calculate (only for sparse matrices, default = ``0``)
247
+ sigma: Shift value for the eigenvalue problem (only for sparse matrices). Eigenvalues around the shift are
248
+ calculated first (default = ``0.0``)
245
249
  """
246
- def _prepare(self, sorting_func=lambda W, Q: np.argsort(W), hermitian=None):
250
+ def _prepare(self, sorting_func=lambda W, Q: np.argsort(W), hermitian=None, nmodes=None, sigma=None):
247
251
  self.sorting_fn = sorting_func
248
252
  self.is_hermitian = hermitian
253
+ self.nmodes = nmodes
254
+ self.sigma = sigma
255
+ self.Ainv = None
256
+
249
257
 
250
258
  def _response(self, A, *args):
251
259
  B = args[0] if len(args) > 0 else None
252
260
  if self.is_hermitian is None:
253
261
  self.is_hermitian = (matrix_is_hermitian(A) and (B is None or matrix_is_hermitian(B)))
254
- W, Q = spla.eigh(A, b=B) if self.is_hermitian else spla.eig(A, b=B)
262
+ self.is_sparse = sps.issparse(A) and (B is None or sps.issparse(B))
263
+ if self.is_sparse:
264
+ W, Q = self._sparse_eigs(A, B=B)
265
+ else:
266
+ W, Q = spla.eigh(A, b=B) if self.is_hermitian else spla.eig(A, b=B)
267
+
255
268
  isort = self.sorting_fn(W, Q)
256
269
  W = W[isort]
257
270
  Q = Q[:, isort]
258
271
  for i in range(W.size):
259
272
  qi, wi = Q[:, i], W[i]
260
- qi *= np.sign(np.real(qi[0]))
273
+ qi *= np.sign(np.real(qi[np.argmax(abs(qi)>0)]))
261
274
  Bqi = qi if B is None else B@qi
262
275
  qi /= np.sqrt(qi@Bqi)
263
276
  assert (abs(qi@(qi if B is None else B@qi) - 1.0) < 1e-5)
@@ -267,6 +280,49 @@ class EigenSolve(Module):
267
280
  def _sensitivity(self, dW, dQ):
268
281
  A = self.sig_in[0].state
269
282
  B = self.sig_in[1].state if len(self.sig_in) > 1 else np.eye(*A.shape)
283
+ dA, dB = None, None
284
+ if not self.is_sparse:
285
+ dA, dB = self._dense_sens(A, B, dW, dQ)
286
+ else:
287
+ if dQ is not None:
288
+ raise NotImplementedError('Sparse eigenvector sensitivities not implemented')
289
+ elif dW is not None:
290
+ dA, dB = self._sparse_eigval_sens(A, B, dW)
291
+
292
+ if len(self.sig_in) == 1:
293
+ return dA
294
+ elif len(self.sig_in) == 2:
295
+ return dA, dB
296
+
297
+
298
+ def _sparse_eigs(self, A, B=None):
299
+ if self.nmodes is None:
300
+ self.nmodes = 6
301
+ if self.sigma is None:
302
+ self.sigma = 0.0
303
+
304
+ if self.sigma == 0.0: # No shift
305
+ mat_shifted = A
306
+ else:
307
+ if B is None: # If no B is given, use identity to shift
308
+ B = sps.eye(*A.shape)
309
+ mat_shifted = A - self.sigma * B
310
+
311
+ # Use shift-and-invert, so make inverse operator
312
+ if self.Ainv is None:
313
+ self.Ainv = auto_determine_solver(mat_shifted, ishermitian=self.is_hermitian)
314
+ self.Ainv.update(mat_shifted)
315
+
316
+ AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve, rmatvec=self.Ainv.adjoint)
317
+
318
+ if self.is_hermitian:
319
+ return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma)
320
+ else:
321
+ # TODO
322
+ raise NotImplementedError('Non-Hermitian sparse matrix not supported')
323
+
324
+ def _dense_sens(self, A, B, dW, dQ):
325
+ """ Calculates all (eigenvector and eigenvalue) sensitivities for dense matrix """
270
326
  dA = np.zeros_like(A)
271
327
  dB = np.zeros_like(B) if len(self.sig_in) > 1 else None
272
328
  W, Q = [s.state for s in self.sig_out]
@@ -281,17 +337,39 @@ class EigenSolve(Module):
281
337
  continue
282
338
  qi, wi = Q[:, i], W[i]
283
339
 
284
- P = np.block([[(A - wi*B).T, -((B + B.T)/2)@qi[..., np.newaxis]], [-B@qi.T, 0]])
340
+ P = np.block([[(A - wi * B).T, -((B + B.T) / 2) @ qi[..., np.newaxis]], [-B @ qi.T, 0]])
285
341
  adj = np.linalg.solve(P, np.block([dqi, dwi])) # Adjoint solve Lee(1999)
286
342
  nu = adj[:-1]
287
343
  alpha = adj[-1]
288
344
  dAi = np.outer(-nu, qi)
289
345
  dA += dAi if np.iscomplexobj(A) else np.real(dAi)
290
346
  if dB is not None:
291
- dBi = np.outer(wi*nu + alpha/2*qi, qi)
347
+ dBi = np.outer(wi * nu + alpha / 2 * qi, qi)
292
348
  dB += dBi if np.iscomplexobj(B) else np.real(dBi)
349
+ return dA, dB
350
+
351
+ def _sparse_eigval_sens(self, A, B, dW):
352
+ if dW is None:
353
+ return None, None
354
+ W, Q = [s.state for s in self.sig_out]
355
+ dA, dB = DyadCarrier(), None if B is None else DyadCarrier()
356
+ for i in range(W.size):
357
+ wi, dwi = W[i], dW[i]
358
+ if dwi == 0:
359
+ continue
360
+ qi = Q[:, i]
361
+ qmq = qi@qi if B is None else qi @ (B @ qi)
362
+ dA_u = (dwi/qmq) * qi
363
+ if np.isrealobj(A):
364
+ dA += DyadCarrier([np.real(dA_u), -np.imag(dA_u)], [np.real(qi), np.imag(qi)])
365
+ else:
366
+ dA += DyadCarrier(dA_u, qi)
367
+
368
+ if dB is not None:
369
+ dB_u = (wi*dwi/qmq) * qi
370
+ if np.isrealobj(B):
371
+ dB -= DyadCarrier([np.real(dB_u), -np.imag(dB_u)], [np.real(qi), np.imag(qi)])
372
+ else:
373
+ dB -= DyadCarrier(dB_u, qi)
374
+ return dA, dB
293
375
 
294
- if len(self.sig_in) == 1:
295
- return dA
296
- elif len(self.sig_in) == 2:
297
- return dA, dB
pymoto/routines.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  from .utils import _parse_to_list, _concatenate_to_array
3
3
  from .core_objects import Signal, Module, Network
4
+ from .common.mma import MMA
4
5
  from typing import List, Iterable, Union, Callable
5
6
 
6
7
 
@@ -271,7 +272,7 @@ def obtain_sensitivities(signals: Iterable[Signal]) -> List:
271
272
 
272
273
  def minimize_oc(function, variables, objective: Signal,
273
274
  tolx=1e-4, tolf=1e-4, maxit=100, xmin=0.0, xmax=1.0, move=0.2,
274
- l1init=0, l2init=100000, l1l2tol=1e-4, maxvol=None):
275
+ l1init=0, l2init=100000, l1l2tol=1e-4, maxvol=None, verbosity=2):
275
276
  """ Execute minimization using the OC-method
276
277
 
277
278
  Args:
@@ -289,7 +290,10 @@ def minimize_oc(function, variables, objective: Signal,
289
290
  l1init: OC internal parameter
290
291
  l2init: OC internal parameter
291
292
  l1l2tol: OC internal parameter
293
+ verbosity: 0 - No prints, 1 - Only convergence message, 2 - Convergence and iteration info
294
+
292
295
  """
296
+ variables = _parse_to_list(variables)
293
297
  xval, cumlens = _concatenate_to_array([s.state for s in variables])
294
298
 
295
299
  if maxvol is None:
@@ -302,16 +306,21 @@ def minimize_oc(function, variables, objective: Signal,
302
306
  fprev, f = f, objective.state
303
307
  rel_fchange = abs(f-fprev)/abs(f)
304
308
  if rel_fchange < tolf:
305
- print(f"OC converged: Relative function change |Δf|/|f| ({rel_fchange}) below tolerance ({tolf})")
309
+ if verbosity >= 1:
310
+ print(f"OC converged: Relative function change |Δf|/|f| ({rel_fchange}) below tolerance ({tolf})")
306
311
  break
307
312
 
308
- print("It. {0: 4d}, f0 = {1: .2e}, Δf = {2: .2e}".format(it, f, f-fprev))
313
+ if verbosity >= 2:
314
+ print("It. {0: 4d}, f0 = {1: .2e}, Δf = {2: .2e}".format(it, f, f-fprev))
309
315
 
310
316
  # Calculate sensitivity of the objective
311
317
  function.reset()
312
318
  objective.sensitivity = 1.0
313
319
  function.sensitivity()
314
320
  dfdx, _ = _concatenate_to_array(obtain_sensitivities(variables))
321
+ maxdfdx = max(dfdx)
322
+ if maxdfdx > 0:
323
+ raise RuntimeError(f"OC only works for negative sensitivities: max(dfdx) = {maxdfdx}")
315
324
 
316
325
  # Do OC update
317
326
  l1, l2 = l1init, l2init
@@ -323,7 +332,8 @@ def minimize_oc(function, variables, objective: Signal,
323
332
  # Stopping criteria on step size
324
333
  rel_stepsize = np.linalg.norm(xval - xnew)/np.linalg.norm(xval)
325
334
  if rel_stepsize < tolx:
326
- print(f"OC converged: Relative stepsize |Δx|/|x| ({rel_stepsize}) below tolerance ({tolx})")
335
+ if verbosity >= 1:
336
+ print(f"OC converged: Relative stepsize |Δx|/|x| ({rel_stepsize}) below tolerance ({tolx})")
327
337
  break
328
338
 
329
339
  xval = xnew
@@ -332,13 +342,9 @@ def minimize_oc(function, variables, objective: Signal,
332
342
  s.state = xnew[cumlens[i]:cumlens[i + 1]]
333
343
 
334
344
 
335
- def minimize_mma(function, variables, responses, tolx=1e-4, tolf=1e-4, maxit=1000, xmin=0.0, xmax=1.0):
345
+ def minimize_mma(function, variables, responses, tolx=1e-4, tolf=1e-4, maxit=1000, move=0.1, xmin=0.0, xmax=1.0, verbosity=2):
336
346
  """ Execute minimization using the MMA-method
337
-
338
- Uses the ``nlopt`` version of MMA.
339
-
340
- Todo:
341
- Improve the MMA implementation as the ``nlopt`` version is not very good
347
+ Svanberg (1987), The method of moving asymptotes - a new method for structural optimization
342
348
 
343
349
  Args:
344
350
  function: The Network defining the optimization problem
@@ -349,59 +355,12 @@ def minimize_mma(function, variables, responses, tolx=1e-4, tolf=1e-4, maxit=100
349
355
  tolx: Stopping criterium for relative design change
350
356
  tolf: Stopping criterium for relative objective change
351
357
  maxit: Maximum number of iteration
358
+ move: Move limit on relative variable change per iteration
352
359
  xmin: Minimum design variable (can be a vector)
353
360
  xmax: Maximum design variable (can be a vector)
361
+ verbosity: 0 - No prints, 1 - Only convergence message, 2 - Convergence and iteration info, 3 - Extended info
362
+
354
363
  """
355
364
  # Save initial state
356
- xval, cumlens = _concatenate_to_array([s.state for s in variables])
357
- n = len(xval)
358
-
359
- def fi(_, grad, i):
360
-
361
- if grad.size > 0:
362
- # Calculate sensitivities
363
- function.reset()
364
-
365
- responses[i].sensitivity = 1.0
366
-
367
- function.sensitivity()
368
-
369
- grad[:], _ = _concatenate_to_array(obtain_sensitivities(variables))
370
-
371
- return responses[i].state
372
-
373
- global it
374
- it = 0
375
-
376
- # Objective function
377
- def f0(x, grad):
378
- global it
379
- # Set the new states
380
- for i, s in enumerate(variables):
381
- s.state = x[cumlens[i]:cumlens[i + 1]]
382
-
383
- # Calculate response
384
- function.response()
385
-
386
- print("It. {0: 4d}, f0 = {1: .2e}, f1 = {2: .2e}".format(it, responses[0].state, responses[1].state))
387
- it += 1
388
-
389
- return fi(x, grad, 0)
390
-
391
- # Create optimization
392
- import nlopt
393
-
394
- opt = nlopt.opt(nlopt.LD_MMA, n)
395
-
396
- opt.set_min_objective(f0)
397
- for ri in range(1, len(responses)):
398
- opt.add_inequality_constraint(lambda x, grad: fi(x, grad, ri))
399
-
400
- opt.set_lower_bounds(np.ones_like(xval) * xmin)
401
- opt.set_upper_bounds(np.ones_like(xval) * xmax)
402
-
403
- opt.set_maxeval(maxit)
404
- opt.set_xtol_rel(tolx)
405
- opt.set_ftol_rel(tolf)
406
-
407
- opt.optimize(xval)
365
+ mma = MMA(function, variables, responses, tolx=tolx, move=move, maxit=maxit, xmin=xmin, xmax=xmax, verbosity=verbosity)
366
+ mma.response()
pymoto/utils.py CHANGED
@@ -32,3 +32,11 @@ def _concatenate_to_array(var_list: list):
32
32
  cumulative_inds[i+1] = len(values)
33
33
 
34
34
  return values, cumulative_inds
35
+
36
+
37
+ def _split_from_array(values: np.ndarray, cumulative_inds: np.ndarray):
38
+ assert cumulative_inds[-1] == values.size, "Size of the array does not match the indices"
39
+ var_list = list()
40
+ for i in range(cumulative_inds.size-1):
41
+ var_list.append(values[cumulative_inds[i]:cumulative_inds[i+1]])
42
+ return var_list
@@ -1,22 +0,0 @@
1
- pymoto/__init__.py,sha256=djteoo_1T-w76W-FH4xo_-t1qbfkNTA6AixYkZVOfws,1958
2
- pymoto/core_objects.py,sha256=iOgJSjJYAiY2vOJzQPSIgtgZv5XtSUOV90l0KoXnzmo,23212
3
- pymoto/routines.py,sha256=NJur-TG4hHuY7Vm8X7BqQ9t8PIwmZaYvv_awRDNs7zo,14490
4
- pymoto/utils.py,sha256=wsqK7CQhqVBzyYRKDa-yIFTN_2ldeHSvy8TzaLMomjo,795
5
- pymoto/common/domain.py,sha256=Bo5Qqnben3Ih0IuprnoO2CmG09shEVUWt1hyiVFnB9U,15136
6
- pymoto/common/dyadcarrier.py,sha256=iszzJMLiHbnLn4_UoCa8PkhdEQPKytLlDxXWsGCmze8,17206
7
- pymoto/common/solvers.py,sha256=92brZkAXc-8jNM3YfL24CCV99uuz0u4weIgyrn8__aw,8143
8
- pymoto/common/solvers_dense.py,sha256=vuBUp3y4qJLwmsXbFQ_tEb-7LqqCEemFunTn1z_Qu0U,9901
9
- pymoto/common/solvers_sparse.py,sha256=QVbGTwGtbhOqRUyg2gHmY-K5haiPbskGA6uj_g-dKz8,15776
10
- pymoto/modules/assembly.py,sha256=0jjzrIhMpvrIll2Fy0QLTBNVxYX2fr2FdqqHtib3D8A,10650
11
- pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
12
- pymoto/modules/complex.py,sha256=vwzqRo5W319mVf_RqbB7LpYe7jXruVxa3ZV560Iq39k,4421
13
- pymoto/modules/filter.py,sha256=8A-dmWSFEqFyQcutjFv__pfgAwszCVZeZgLxuG9hi0g,18840
14
- pymoto/modules/generic.py,sha256=mqSHf0CMc0UwitzRgcDtybnk4tuR0obeiO6QB7iEv-o,8632
15
- pymoto/modules/io.py,sha256=4k5S-YQHKhw_HwmqOoYQWFEzdcL5nMJ5fVD2FJFqpFg,10532
16
- pymoto/modules/linalg.py,sha256=D_gwWPl3nrR3zFW1i9d4WobQ9YoUkkPEM_05kiixuEs,13201
17
- pyMOTO-1.0.1.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
18
- pyMOTO-1.0.1.dist-info/METADATA,sha256=k1ImayKsQwlx4ZDnERCrZZJUUCkZLfNTgKWZyJ638VM,5201
19
- pyMOTO-1.0.1.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
20
- pyMOTO-1.0.1.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
21
- pyMOTO-1.0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
22
- pyMOTO-1.0.1.dist-info/RECORD,,