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.
- {pyMOTO-1.0.1.dist-info → pyMOTO-1.0.2rc1.dist-info}/METADATA +10 -24
- pyMOTO-1.0.2rc1.dist-info/RECORD +23 -0
- {pyMOTO-1.0.1.dist-info → pyMOTO-1.0.2rc1.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +5 -3
- pymoto/common/mma.py +554 -0
- pymoto/core_objects.py +23 -19
- pymoto/modules/assembly.py +22 -10
- pymoto/modules/generic.py +21 -0
- pymoto/modules/linalg.py +87 -9
- pymoto/routines.py +21 -62
- pymoto/utils.py +8 -0
- pyMOTO-1.0.1.dist-info/RECORD +0 -22
- {pyMOTO-1.0.1.dist-info → pyMOTO-1.0.2rc1.dist-info}/LICENSE +0 -0
- {pyMOTO-1.0.1.dist-info → pyMOTO-1.0.2rc1.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.0.1.dist-info → pyMOTO-1.0.2rc1.dist-info}/zip-safe +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pyMOTO
|
3
|
-
Version: 1.0.
|
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
|
+
[](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) **
|
71
|
-
* (optional) **
|
72
|
-
* (optional) **
|
73
|
-
* (optional) **
|
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, `
|
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
|
-
`
|
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,,
|
pymoto/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
__version__ = '1.0.
|
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)("
|
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)("
|
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)("
|
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
|
-
|
189
|
-
if
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
200
|
-
|
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)("
|
210
|
+
raise type(e)("SignalSlice.sensitivity (setter)" + self._err_str()) from e
|
207
211
|
|
208
212
|
|
209
213
|
def make_signals(*args):
|
pymoto/modules/assembly.py
CHANGED
@@ -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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
357
|
-
|
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
|
pyMOTO-1.0.1.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|