pyMOTO 1.1.0__tar.gz → 1.2.1__tar.gz

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.
Files changed (43) hide show
  1. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/PKG-INFO +1 -1
  2. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyMOTO.egg-info/PKG-INFO +1 -1
  3. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyMOTO.egg-info/SOURCES.txt +4 -1
  4. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/__init__.py +15 -12
  5. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/common/domain.py +3 -3
  6. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/assembly.py +14 -4
  7. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/linalg.py +90 -7
  8. pyMOTO-1.2.1/tests/test_assembly.py +91 -0
  9. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_automod.py +4 -4
  10. pyMOTO-1.2.1/tests/test_linsolve_sparse.py +479 -0
  11. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_solvers_sparse.py +7 -208
  12. pyMOTO-1.2.1/tests/test_static_condenstation.py +55 -0
  13. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/LICENSE +0 -0
  14. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/README.md +0 -0
  15. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyMOTO.egg-info/dependency_links.txt +0 -0
  16. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyMOTO.egg-info/requires.txt +0 -0
  17. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyMOTO.egg-info/top_level.txt +0 -0
  18. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyMOTO.egg-info/zip-safe +0 -0
  19. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/common/dyadcarrier.py +0 -0
  20. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/common/mma.py +0 -0
  21. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/common/solvers.py +0 -0
  22. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/common/solvers_dense.py +0 -0
  23. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/common/solvers_sparse.py +0 -0
  24. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/core_objects.py +0 -0
  25. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/autodiff.py +0 -0
  26. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/complex.py +0 -0
  27. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/filter.py +0 -0
  28. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/generic.py +0 -0
  29. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/io.py +0 -0
  30. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/modules/scaling.py +0 -0
  31. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/routines.py +0 -0
  32. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pymoto/utils.py +0 -0
  33. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/pyproject.toml +0 -0
  34. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/setup.cfg +0 -0
  35. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_complex.py +0 -0
  36. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_core.py +0 -0
  37. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_dyadcarrier.py +0 -0
  38. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_module_concatsignal.py +0 -0
  39. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_module_eigensolve.py +0 -0
  40. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_module_einsum.py +0 -0
  41. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_module_mathgeneral.py +0 -0
  42. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_scaling.py +0 -0
  43. {pyMOTO-1.1.0 → pyMOTO-1.2.1}/tests/test_solvers_dense.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyMOTO
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Summary: A modular approach for topology optimization
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyMOTO
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Summary: A modular approach for topology optimization
5
5
  Home-page: https://github.com/aatmdelissen/pyMOTO
6
6
  Author: Arnoud Delissen
@@ -26,14 +26,17 @@ pymoto/modules/generic.py
26
26
  pymoto/modules/io.py
27
27
  pymoto/modules/linalg.py
28
28
  pymoto/modules/scaling.py
29
+ tests/test_assembly.py
29
30
  tests/test_automod.py
30
31
  tests/test_complex.py
31
32
  tests/test_core.py
32
33
  tests/test_dyadcarrier.py
34
+ tests/test_linsolve_sparse.py
33
35
  tests/test_module_concatsignal.py
34
36
  tests/test_module_eigensolve.py
35
37
  tests/test_module_einsum.py
36
38
  tests/test_module_mathgeneral.py
37
39
  tests/test_scaling.py
38
40
  tests/test_solvers_dense.py
39
- tests/test_solvers_sparse.py
41
+ tests/test_solvers_sparse.py
42
+ tests/test_static_condenstation.py
@@ -1,27 +1,30 @@
1
- __version__ = '1.1.0'
1
+ __version__ = '1.2.1'
2
2
 
3
- from .core_objects import Signal, Module, Network, make_signals
4
- from .routines import finite_difference, minimize_oc, minimize_mma
3
+ from .common.domain import DomainDefinition
5
4
 
6
5
  # Imports from common
7
6
  from .common.dyadcarrier import DyadCarrier
8
- from .common.domain import DomainDefinition
9
7
  from .common.mma import MMA
10
- from .common.solvers import matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian, \
11
- LinearSolver, LDAWrapper
8
+ from .common.solvers import matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian, LinearSolver, LDAWrapper
12
9
  from .common.solvers_dense import SolverDiagonal, SolverDenseQR, SolverDenseLU, SolverDenseCholesky, SolverDenseLDL
13
10
  from .common.solvers_sparse import SolverSparsePardiso, SolverSparseLU, SolverSparseCholeskyScikit, SolverSparseCholeskyCVXOPT
14
11
 
15
- # Import from modules
16
- from .modules.generic import MathGeneral, EinSum, ConcatSignal
17
- from .modules.linalg import Inverse, LinSolve, EigenSolve, SystemOfEquations
12
+ # Modular inports
13
+ from .core_objects import Signal, Module, Network, make_signals
14
+
15
+ # Import modules
18
16
  from .modules.assembly import AssembleGeneral, AssembleStiffness, AssembleMass
17
+ from .modules.autodiff import AutoMod
18
+ from .modules.complex import MakeComplex, RealPart, ImagPart, ComplexNorm
19
19
  from .modules.filter import FilterConv, Filter, DensityFilter, OverhangFilter
20
+ from .modules.generic import MathGeneral, EinSum, ConcatSignal
20
21
  from .modules.io import PlotDomain, PlotGraph, PlotIter, WriteToVTI
21
- from .modules.complex import MakeComplex, RealPart, ImagPart, ComplexNorm
22
- from .modules.autodiff import AutoMod
22
+ from .modules.linalg import Inverse, LinSolve, EigenSolve, SystemOfEquations, StaticCondensation
23
23
  from .modules.scaling import Scaling
24
24
 
25
+ # Further helper routines
26
+ from .routines import finite_difference, minimize_oc, minimize_mma
27
+
25
28
  __all__ = [
26
29
  'Signal', 'Module', 'Network', 'make_signals',
27
30
  'finite_difference', 'minimize_oc', 'minimize_mma',
@@ -35,7 +38,7 @@ __all__ = [
35
38
  'SolverSparsePardiso', 'SolverSparseLU', 'SolverSparseCholeskyScikit', 'SolverSparseCholeskyCVXOPT',
36
39
  # Modules
37
40
  "MathGeneral", "EinSum", "ConcatSignal",
38
- "Inverse", "LinSolve", "EigenSolve", "SystemOfEquations",
41
+ "Inverse", "LinSolve", "EigenSolve", "SystemOfEquations", "StaticCondensation",
39
42
  "AssembleGeneral", "AssembleStiffness", "AssembleMass",
40
43
  "FilterConv", "Filter", "DensityFilter", "OverhangFilter",
41
44
  "PlotDomain", "PlotGraph", "PlotIter", "WriteToVTI",
@@ -184,10 +184,10 @@ class DomainDefinition:
184
184
  """
185
185
  v = np.prod(self.element_size[:self.dim])
186
186
  assert v > 0.0, 'Element volume needs to be positive'
187
- ret = np.ones(self.nnodes)/v
187
+ shapefn = np.ones(self.elemnodes)/v
188
188
  for i in range(self.dim):
189
- ret *= np.array([self.element_size[i] + n[i]*pos[i] for n in self.node_numbering])
190
- return ret
189
+ shapefn *= np.array([self.element_size[i]/2 + n[i]*pos[i] for n in self.node_numbering])
190
+ return shapefn
191
191
 
192
192
  def eval_shape_fun_der(self, pos: np.ndarray):
193
193
  """ Evaluates the shape function derivatives in x, y, and optionally z-direction.
@@ -1,9 +1,12 @@
1
1
  """ Assembly modules for finite element analysis """
2
2
  import sys
3
- from pymoto import Module, DyadCarrier, DomainDefinition
3
+ from typing import Union
4
+
4
5
  import numpy as np
5
6
  from scipy.sparse import csc_matrix
6
- from typing import Union
7
+
8
+ from pymoto import Module, DyadCarrier, DomainDefinition
9
+
7
10
  try:
8
11
  from opt_einsum import contract as einsum
9
12
  except ModuleNotFoundError:
@@ -33,8 +36,11 @@ class AssembleGeneral(Module):
33
36
  bcdiagval (optional): Value to put on the diagonal of the matrix at dofs where boundary conditions are active.
34
37
  matrix_type (optional): The matrix type to construct. This is a constructor which must accept the arguments
35
38
  ``matrix_type((vals, (row_idx, col_idx)), shape=(n, n))``
39
+ add_constant (optional): A constant (e.g. matrix) to add.
36
40
  """
37
- def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray, bc=None, bcdiagval=None, matrix_type=csc_matrix):
41
+
42
+ def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray, bc=None, bcdiagval=None,
43
+ matrix_type=csc_matrix, add_constant=None):
38
44
  self.elmat = element_matrix
39
45
  self.ndof = self.elmat.shape[-1] // domain.elemnodes # Number of dofs per node
40
46
  self.n = self.ndof * domain.nnodes # Matrix size
@@ -59,6 +65,8 @@ class AssembleGeneral(Module):
59
65
  else:
60
66
  self.bcselect = None
61
67
 
68
+ self.add_constant = add_constant
69
+
62
70
  def _response(self, xscale: np.ndarray):
63
71
  scaled_el = ((self.elmat.flatten()[np.newaxis]).T * xscale).flatten(order='F')
64
72
 
@@ -76,6 +84,8 @@ class AssembleGeneral(Module):
76
84
  "scipy.sparse.csrmatrix are supported"
77
85
  .format(self.matrix_type)).with_traceback(sys.exc_info()[2]) from None
78
86
 
87
+ if self.add_constant is not None:
88
+ mat += self.add_constant
79
89
  return mat
80
90
 
81
91
  def _sensitivity(self, dgdmat: Union[DyadCarrier, np.ndarray]):
@@ -232,6 +242,7 @@ class AssembleMass(AssembleGeneral):
232
242
  bcdiagval: The value to put on the diagonal in case of boundary conditions (bc)
233
243
  **kwargs : Other keyword-arguments are passed to AssembleGeneral
234
244
  """
245
+
235
246
  def _prepare(self, domain: DomainDefinition, *args, rho: float = 1.0, bcdiagval=0.0, **kwargs):
236
247
  # Element mass matrix
237
248
  # 1/36 Mass of one element
@@ -259,4 +270,3 @@ class AssembleMass(AssembleGeneral):
259
270
  else:
260
271
  raise RuntimeError("Only for 2D and 3D")
261
272
  super()._prepare(domain, ME, *args, bcdiagval=bcdiagval, **kwargs)
262
-
@@ -13,6 +13,71 @@ from pymoto import SolverSparseLU, SolverSparseCholeskyCVXOPT, SolverSparsePardi
13
13
  from pymoto import matrix_is_symmetric, matrix_is_hermitian, matrix_is_diagonal
14
14
 
15
15
 
16
+ class StaticCondensation(Module):
17
+ r"""Static condensation of a linear system of equations
18
+
19
+ The partitioned system of equations
20
+
21
+ :math:`\begin{bmatrix} \mathbf{A}_\text{mm} & \mathbf{A}_\text{ms} \\ \mathbf{A}_\text{sm} & \mathbf{A}_\text{ss}
22
+ \end{bmatrix}
23
+ \begin{bmatrix} \mathbf{x}_\text{m} \\ \mathbf{x}_\text{s} \end{bmatrix} =
24
+ \begin{bmatrix} \mathbf{b}_\text{m} \\ \mathbf{b}_\text{s} \end{bmatrix}
25
+ ,`
26
+ with subscripts ``(m)`` and ``(s)`` referring to the main and secondary dofs, respectively.
27
+
28
+ The system is solved in two steps:
29
+
30
+ :math:`\begin{aligned}
31
+ \mathbf{A}_\text{ss} \mathbf{x}_\text{sm} &= \mathbf{A}_\text{sm} \\
32
+ \tilde{\mathbf{A}} &= \mathbf{A}_\text{mm} - \mathbf{A}_\text{ms} \mathbf{x}_\text{sm}.
33
+ \end{aligned}`
34
+
35
+ Assumptions:
36
+ (i) It is assumed the prescribed DOFs (all dof - main dof - free dof) are prescribed to zero.
37
+ (ii) It is assumed the applied load on the free DOFs is zero; there is no reduced load.
38
+
39
+ Implemented by @artofscience (s.koppen@tudelft.nl).
40
+
41
+ References:
42
+
43
+ Koppen, S., Langelaar, M., & van Keulen, F. (2022).
44
+ Efficient multi-partition topology optimization.
45
+ Computer Methods in Applied Mechanics and Engineering, 393, 114829.
46
+ DOI: https://doi.org/10.1016/j.cma.2022.114829
47
+
48
+ Input Signals:
49
+ - ``A`` (`dense or sparse matrix`): The system matrix :math:`\mathbf{A}` of size ``(n, n)``
50
+
51
+ Output Signal:
52
+ - ``Ared`` (`dense or sparse matrix`): The reduced system matrix :math:`\tilde{\mathbf{A}}` of size ``(m, m)``
53
+
54
+ Args:
55
+ free: The indices corresponding to the free degrees of freedom
56
+ main: The indices corresponding to the main degrees of freedom
57
+ **kwargs: See `pymoto.LinSolve`, as they are directly passed into the `LinSolve` module
58
+ """
59
+
60
+ def _prepare(self, main, free, **kwargs):
61
+ self.module_LinSolve = LinSolve([self.sig_in[0], Signal()], **kwargs)
62
+ self.module_LinSolve.use_lda_solver = False
63
+ self.m = main
64
+ self.f = free
65
+
66
+ def _response(self, A):
67
+ self.n = np.shape(A)[0]
68
+ self.module_LinSolve.sig_in[0].state = A[self.f, ...][..., self.f]
69
+ self.module_LinSolve.sig_in[1].state = A[self.f, ...][..., self.m].todense()
70
+ self.module_LinSolve.response()
71
+ self.X = self.module_LinSolve.sig_out[0].state
72
+ return A[self.m, ...][..., self.m] - A[self.m, ...][..., self.f] @ self.X
73
+
74
+ def _sensitivity(self, dfdB):
75
+ C = np.zeros((self.n, len(self.m)), dtype=float)
76
+ C[self.m, ...] = np.eye(len(self.m))
77
+ C[self.f, ...] = -self.X
78
+ return C @ dfdB @ C.T if isinstance(dfdB, DyadCarrier) else DyadCarrier(list(C.T), list(np.asarray(dfdB @ C.T)))
79
+
80
+
16
81
  class SystemOfEquations(Module):
17
82
  r""" Solve a partitioned linear system of equations
18
83
 
@@ -63,6 +128,7 @@ class SystemOfEquations(Module):
63
128
  assert bf.shape[0] + xp.shape[0] == A.shape[0], "Dimensions of applied force and displacement must match matrix"
64
129
  assert bf.ndim == xp.ndim, "Number of loadcases for applied force and displacement must match"
65
130
  self.n = np.shape(A)[0]
131
+ self.dim = xp.ndim
66
132
 
67
133
  if self.f is None:
68
134
  all_dofs = np.arange(self.n)
@@ -73,7 +139,8 @@ class SystemOfEquations(Module):
73
139
  assert self.f.size + self.p.size == self.n, "Size of free and prescribed indices must match the matrix size"
74
140
 
75
141
  # create empty output
76
- self.x = np.zeros((self.n, *bf.shape[1:]), dtype=float)
142
+ self.x = np.zeros((self.n, *bf.shape[1:]), dtype=complex) if np.iscomplexobj(A) else np.zeros(
143
+ (self.n, *bf.shape[1:]), dtype=float)
77
144
  self.x[self.p, ...] = xp
78
145
 
79
146
  b = np.zeros_like(self.x)
@@ -97,13 +164,19 @@ class SystemOfEquations(Module):
97
164
  return self.x, b
98
165
 
99
166
  def _sensitivity(self, dgdx, dgdb):
100
- adjoint_load = dgdx[self.f, ...] + self.Afp * dgdb[self.p, ...]
167
+ adjoint_load = np.zeros_like(self.x[self.f, ...])
168
+
169
+ if dgdx is not None:
170
+ adjoint_load += dgdx[self.f, ...]
171
+ if dgdb is not None:
172
+ adjoint_load += self.Afp * dgdb[self.p, ...]
101
173
 
102
- # adjoint equation
103
174
  lam = np.zeros_like(self.x)
104
175
  lamf = -1.0 * self.module_LinSolve.solver.adjoint(adjoint_load)
105
176
  lam[self.f, ...] = lamf
106
- lam[self.p, ...] = dgdb[self.p, ...]
177
+
178
+ if dgdb is not None:
179
+ lam[self.p, ...] = dgdb[self.p, ...]
107
180
 
108
181
  # sensitivities to system matrix
109
182
  if self.x.ndim > 1:
@@ -112,10 +185,19 @@ class SystemOfEquations(Module):
112
185
  dgdA = DyadCarrier(lam, self.x)
113
186
 
114
187
  # sensitivities to applied load and prescribed state
115
- dgdff = dgdb[self.f, ...] - lam[self.f, ...]
116
- dgdup = dgdx[self.p, ...] + self.App * dgdb[self.p, ...] + self.Afp.T * lam[self.f, ...]
188
+ dgdbf = np.zeros_like(adjoint_load)
189
+ dgdup = np.zeros_like(self.x[self.p, ...])
190
+ dgdbf -= lam[self.f, ...]
191
+ dgdup += self.Afp.T * lam[self.f, ...]
192
+
193
+ if dgdx is not None:
194
+ dgdup += dgdx[self.p, ...]
117
195
 
118
- return dgdA, dgdff, dgdup
196
+ if dgdb is not None:
197
+ dgdbf += dgdb[self.f, ...]
198
+ dgdup += self.App * dgdb[self.p, ...]
199
+
200
+ return dgdA, dgdbf, dgdup
119
201
 
120
202
 
121
203
  class Inverse(Module):
@@ -257,6 +339,7 @@ class LinSolve(Module):
257
339
  Attributes:
258
340
  use_lda_solver: Use the linear-dependency-aware solver :class:`LDAWrapper` to prevent redundant computations
259
341
  """
342
+
260
343
  use_lda_solver = True
261
344
 
262
345
  def _prepare(self, dep_tol=1e-5, hermitian=None, symmetric=None, solver=None):
@@ -0,0 +1,91 @@
1
+ import unittest
2
+ import numpy as np
3
+ import pymoto as pym
4
+ import numpy.testing as npt
5
+
6
+
7
+ class TestAssembleStiffness(unittest.TestCase):
8
+ def test_FEA_pure_tensile_2d_one_element(self):
9
+ Lx, Ly, Lz = 0.1, 0.2, 0.3
10
+ domain = pym.DomainDefinition(1, 1, unitx=Lx, unity=Ly, unitz=Lz)
11
+ nodidx_left = domain.get_nodenumber(0, np.arange(domain.nely + 1))
12
+ # Fixed at bottom, roller at the top in y-direction
13
+ nodidx_right = domain.get_nodenumber(domain.nelx, np.arange(domain.nely + 1))
14
+ dofidx_left = np.concatenate([nodidx_left*2, np.array([nodidx_left[0]*2 + 1])])
15
+
16
+ E, nu = 210e+9, 0.3
17
+
18
+ s_x = pym.Signal('x', state=np.ones(domain.nel))
19
+
20
+ # Assemble stiffness matrix
21
+ m_K = pym.AssembleStiffness(s_x, domain=domain, bc=dofidx_left, e_modulus=E, poisson_ratio=nu, plane='stress')
22
+ s_K = m_K.sig_out[0]
23
+
24
+ m_K.response()
25
+ F = 1.5
26
+ f = np.zeros(domain.nnodes*2)
27
+ f[nodidx_right * 2] = F/nodidx_right.size
28
+ x = np.linalg.solve(s_K.state.toarray(), f)
29
+
30
+ # Bottom y displacement should be zero
31
+ npt.assert_allclose(x[nodidx_right[0]*2+1], 0, atol=1e-10)
32
+
33
+ # Analytical axial displacement using stiffness k = EA/L
34
+ ux_chk = F * Lx / (E * Ly * Lz)
35
+ npt.assert_allclose(x[nodidx_right*2], ux_chk, rtol=1e-10)
36
+
37
+ # Transverse displacement using Poisson's effect
38
+ e_xx = ux_chk / Lx # Strain in x-direction
39
+ e_yy = - nu * e_xx
40
+ uy_chk = e_yy * Ly
41
+ npt.assert_allclose(x[nodidx_right[1]*2+1], uy_chk, rtol=1e-10)
42
+
43
+ def test_FEA_pure_tensile_3d_one_element(self):
44
+ Lx, Ly, Lz = 0.1, 0.2, 0.3
45
+ domain = pym.DomainDefinition(1, 1, 1, unitx=Lx, unity=Ly, unitz=Lz)
46
+ nodidx_left = domain.get_nodenumber(*np.meshgrid(0, range(domain.nely + 1), range(domain.nelz + 1))).flatten()
47
+ # Fixed at (0,0,0), roller in z-direction at (0, 1, 0), roller in y-direction at (0, 0, 1)
48
+ nod_00 = domain.get_nodenumber(0, 0, 0)
49
+ nod_10 = domain.get_nodenumber(0, 1, 0)
50
+ nod_01 = domain.get_nodenumber(0, 0, 1)
51
+ dofidx_left = np.concatenate([nodidx_left * 3, np.array([nod_00, nod_01]) * 3 + 1, np.array([nod_00, nod_10]) * 3 + 2])
52
+ nodidx_right = domain.get_nodenumber(*np.meshgrid(1, range(domain.nely + 1), range(domain.nelz + 1))).flatten()
53
+
54
+ E, nu = 210e+9, 0.3
55
+
56
+ s_x = pym.Signal('x', state=np.ones(domain.nel))
57
+
58
+ # Assemble stiffness matrix
59
+ m_K = pym.AssembleStiffness(s_x, domain=domain, bc=dofidx_left, e_modulus=E, poisson_ratio=nu)
60
+ s_K = m_K.sig_out[0]
61
+
62
+ m_K.response()
63
+ F = 1.5e+3
64
+ f = np.zeros(domain.nnodes * 3)
65
+ f[nodidx_right * 3] = F / nodidx_right.size
66
+ x = np.linalg.solve(s_K.state.toarray(), f)
67
+
68
+ # y and z displacements at (1, 0, 0) should be zero
69
+ npt.assert_allclose(x[domain.get_nodenumber(1, 0, 0) * 3 + 1], 0, atol=1e-10)
70
+ npt.assert_allclose(x[domain.get_nodenumber(1, 0, 0) * 3 + 2], 0, atol=1e-10)
71
+
72
+ # Z displacement at (1, 1, 0) should be zero
73
+ npt.assert_allclose(x[domain.get_nodenumber(1, 1, 0) * 3 + 2], 0, atol=1e-10)
74
+
75
+ # Y displacement at (1, 0, 1) should be zero
76
+ npt.assert_allclose(x[domain.get_nodenumber(1, 0, 1) * 3 + 1], 0, atol=1e-10)
77
+
78
+ # Analytical axial displacement using stiffness k = EA/L
79
+ ux_chk = F * Lx / (E * Ly * Lz)
80
+ npt.assert_allclose(x[nodidx_right * 3], ux_chk, rtol=1e-10)
81
+
82
+ # Transverse displacement using Poisson's effect
83
+ e_xx = ux_chk / Lx # Strain in x-direction
84
+ e_trans = - nu * e_xx
85
+ uy_chk = e_trans * Ly
86
+ npt.assert_allclose(x[domain.get_nodenumber(1, 1, 0) * 3 + 1], uy_chk, rtol=1e-10)
87
+ npt.assert_allclose(x[domain.get_nodenumber(1, 1, 1) * 3 + 1], uy_chk, rtol=1e-10)
88
+
89
+ uz_chk = e_trans * Lz
90
+ npt.assert_allclose(x[domain.get_nodenumber(1, 0, 1) * 3 + 2], uz_chk, rtol=1e-10)
91
+ npt.assert_allclose(x[domain.get_nodenumber(1, 1, 1) * 3 + 2], uz_chk, rtol=1e-10)
@@ -6,10 +6,10 @@ import pymoto as pym
6
6
  class TestAutoMod(unittest.TestCase):
7
7
  @classmethod
8
8
  def setUpClass(cls) -> None:
9
- """ Skip test if no function is given """
10
- import importlib.util
11
- jax_spec = importlib.util.find_spec("jax")
12
- if jax_spec is None:
9
+ """ Skip test if JAX is not installed """
10
+ try:
11
+ import jax
12
+ except ImportError:
13
13
  raise unittest.SkipTest(f"Skipping test {cls}")
14
14
 
15
15
  def test_automod_scalar(self):
@@ -0,0 +1,479 @@
1
+ import unittest
2
+ import numpy as np
3
+ import pymoto as pym
4
+ import numpy.testing as npt
5
+ from scipy.sparse import csc_matrix
6
+
7
+
8
+ class DynamicMatrix(pym.Module):
9
+ alpha = 0.5
10
+ beta = 0.5
11
+
12
+ def _response(self, K, M, omega):
13
+ return K + 1j * omega * (self.alpha * M + self.beta * K) - omega ** 2 * M
14
+
15
+ def _sensitivity(self, dZ):
16
+ K, M, omega = [s.state for s in self.sig_in]
17
+ dK = np.real(dZ) - (omega * self.beta) * np.imag(dZ)
18
+ dM = (-omega ** 2) * np.real(dZ) - (omega * self.alpha) * np.imag(dZ)
19
+ dZrM = np.real(dZ).contract(M)
20
+ dZiK = np.imag(dZ).contract(K)
21
+ dZiM = np.imag(dZ).contract(M)
22
+ domega = -self.beta * dZiK - self.alpha * dZiM - 2 * omega * dZrM
23
+ return dK, dM, domega
24
+
25
+
26
+ class TestLinSolveModuleSparse(unittest.TestCase):
27
+ # # ------------- Symmetric -------------
28
+ def test_symmetric_real_compliance2d(self):
29
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
30
+ N = 10 # Number of elements
31
+ dom = pym.DomainDefinition(N, N)
32
+ sx = pym.Signal('x', np.random.rand(dom.nel))
33
+ fixed_nodes = dom.get_nodenumber(0, np.arange(0, N+1))
34
+ bc = np.concatenate((fixed_nodes*2, fixed_nodes*2+1))
35
+ # Setup different rhs types
36
+ iforce_x = dom.get_nodenumber(N, np.arange(0, N + 1)) * 2 # Force in x-direction
37
+ iforce_y = dom.get_nodenumber(N, np.arange(0, N + 1)) * 2 + 1 # Force in y-direction
38
+
39
+ force_vecs = dict()
40
+
41
+ # Single force
42
+ f = np.zeros(dom.nnodes*2)
43
+ f[iforce_x] = 1.0
44
+ force_vecs['single_real'] = f
45
+
46
+ # Multiple rhs
47
+ f = np.zeros((dom.nnodes * 2, 2))
48
+ f[iforce_x, 0] = 1.0
49
+ f[iforce_y, 1] = 1.0
50
+ force_vecs['multiple_real'] = f
51
+
52
+ for k, f in force_vecs.items():
53
+ with self.subTest(f"RHS-{k}"):
54
+ sf = pym.Signal('f', f)
55
+
56
+ fn = pym.Network()
57
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), dom, bc=bc))
58
+ su = fn.append(pym.LinSolve([sK, sf], pym.Signal('u')))
59
+
60
+ fn.response()
61
+
62
+ self.assertTrue(np.allclose(sK.state@su.state, sf.state)) # Check residual
63
+ # Check finite difference
64
+ # def tfn(x0, dx, df_an, df_fd): np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5)
65
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
66
+ pym.finite_difference(fn, [sx, sf], su, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
67
+
68
+ def test_symmetric_real_compliance3d(self):
69
+ """ Test symmetric real sparse matrix (compliance in 3D)"""
70
+ N = 3 # Number of elements
71
+ dom = pym.DomainDefinition(N, N, N)
72
+ sx = pym.Signal('x', np.random.rand(dom.nel))
73
+ jfix, kfix = np.meshgrid(np.arange(0, N+1), np.arange(0, N+1), indexing='ij')
74
+ fixed_nodes = dom.get_nodenumber(0, jfix, kfix).flatten()
75
+ bc = np.concatenate((fixed_nodes*3, fixed_nodes*3+1, fixed_nodes*3+2))
76
+ iforce = dom.get_nodenumber(N, np.arange(0, N+1), np.arange(0, N+1))*3 + 1
77
+ sf = pym.Signal('f', np.zeros(dom.nnodes*3))
78
+ sf.state[iforce] = 1.0
79
+
80
+ fn = pym.Network()
81
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), dom, bc=bc))
82
+ su = fn.append(pym.LinSolve([sK, sf], pym.Signal('u')))
83
+
84
+ fn.response()
85
+
86
+ self.assertTrue(np.allclose(sK.state@su.state, sf.state)) # Check residual
87
+ # Check finite difference
88
+ # def tfn(x0, dx, df_an, df_fd): np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5)
89
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=2e-3, atol=1e-5))
90
+ pym.finite_difference(fn, [sx, sf], su, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
91
+
92
+ def test_symmetric_complex_dyncompliance2d(self):
93
+ """ Test symmetric complex sparse matrix (dynamic compliance in 2D)"""
94
+ N = 5 # Number of elements
95
+ dom = pym.DomainDefinition(N, N)
96
+ sx = pym.Signal('x', np.random.rand(dom.nel))
97
+ fixed_nodes = dom.get_nodenumber(0, np.arange(0, N+1))
98
+ bc = np.concatenate((fixed_nodes*2, fixed_nodes*2+1))
99
+ iforce = dom.get_nodenumber(N, np.arange(0, N+1))*2 + 1
100
+ sf = pym.Signal('f', np.zeros(dom.nnodes*2))
101
+ sf.state[iforce] = 1.0
102
+
103
+ sOmega = pym.Signal('omega', 0.1)
104
+ fn = pym.Network()
105
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), dom, bc=bc))
106
+ sM = fn.append(pym.AssembleMass(sx, pym.Signal('M'), dom, bc=bc))
107
+ sZ = fn.append(DynamicMatrix([sK, sM, sOmega], pym.Signal('Z')))
108
+
109
+ su = fn.append(pym.LinSolve([sZ, sf], pym.Signal('u')))
110
+
111
+ fn.response()
112
+
113
+ # spspla.eigsh(sK.state, M=sM.state, k=6, sigma=0.0)
114
+
115
+ self.assertTrue(np.allclose(sZ.state@su.state, sf.state)) # Check residual
116
+ # Check finite difference
117
+ # def tfn(x0, dx, df_an, df_fd): np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5)
118
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
119
+ pym.finite_difference(fn, [sx, sf, sOmega], su, test_fn=tfn, dx=1e-7, tol=1e-4, verbose=False)
120
+
121
+
122
+ class TestAssemblyAddValues(unittest.TestCase):
123
+ def test_finite_difference(self):
124
+ N = 2
125
+ # Set up the domain
126
+ domain = pym.DomainDefinition(N, N)
127
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
128
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
129
+
130
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
131
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
132
+ dofs_left_x = dofs_left[0::2]
133
+ dofs_left_y = dofs_left[1::2]
134
+ dof_input = dofs_left_y[0] # Input dofs for mechanism
135
+ dof_output = dofs_left_y[-1] # Output dofs for mechanism
136
+
137
+ prescribed_dofs = np.union1d(dofs_left_x, dofs_right)
138
+
139
+ # Setup rhs for two loadcases
140
+ f = np.zeros(domain.nnodes * 2, dtype=float)
141
+ f[dof_input] = 1.0
142
+
143
+ # Initial design
144
+ sx = pym.Signal('x', np.random.rand(domain.nel))
145
+ signal_force = pym.Signal('f', state=f)
146
+ # Setup optimization problem
147
+ network = pym.Network()
148
+
149
+ # Assembly
150
+ istiff = np.array([dof_input, dof_output])
151
+ sstiff = np.array([10.0, 10.0])
152
+
153
+ K_const = csc_matrix((sstiff, (istiff, istiff)), shape=(domain.nnodes * 2, domain.nnodes * 2))
154
+ signal_stiffness = network.append(pym.AssembleStiffness(sx, domain=domain, bc=prescribed_dofs, add_constant=K_const))
155
+ su = network.append(pym.LinSolve([signal_stiffness, signal_force], pym.Signal('u')))
156
+ sc = network.append(pym.EinSum(su, expression='i->'))
157
+ network.response()
158
+
159
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
160
+
161
+ pym.finite_difference(network, [sx], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
162
+
163
+ def test_added_stiffness_on_ground(self):
164
+ N = 2
165
+ # Set up the domain
166
+ domain = pym.DomainDefinition(N, N)
167
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
168
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
169
+
170
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
171
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
172
+ dofs_left_x = dofs_left[0::2]
173
+ dofs_left_y = dofs_left[1::2]
174
+ dof_input = dofs_left_y[0] # Input dofs for mechanism
175
+ dof_output = dofs_left_y[-1] # Output dofs for mechanism
176
+
177
+ prescribed_dofs = np.union1d(dofs_left_x, dofs_right)
178
+
179
+ # Setup rhs for two loadcases
180
+ f = np.zeros(domain.nnodes * 2, dtype=float)
181
+ f[dof_input] = 1.0
182
+
183
+ # Initial design
184
+ sx = pym.Signal('x', np.random.rand(domain.nel))
185
+ signal_force = pym.Signal('f', state=f)
186
+ # Setup optimization problem
187
+ network = pym.Network()
188
+
189
+ # Assembly
190
+ istiff = np.array([dof_input, dof_output])
191
+ sstiff = np.array([10.0, 10.0])
192
+
193
+ K_const = csc_matrix((sstiff, (istiff, istiff)), shape=(domain.nnodes * 2, domain.nnodes * 2))
194
+ signal_stiffness = network.append(
195
+ pym.AssembleStiffness(sx, domain=domain, bc=prescribed_dofs, add_constant=K_const))
196
+ su = network.append(pym.LinSolve([signal_stiffness, signal_force], pym.Signal('u')))
197
+ sc = network.append(pym.EinSum(su, expression='i->'))
198
+
199
+ network.response()
200
+
201
+ network2 = pym.Network()
202
+ # Assembly
203
+ istiff = np.array([dof_input, dof_output, 4, 5])
204
+ sstiff = np.array([10.0, 10.0, 100.0, 100.0])
205
+
206
+ K_const1 = csc_matrix((sstiff, (istiff, istiff)), shape=(domain.nnodes * 2, domain.nnodes * 2))
207
+ signal_stiffness = network2.append(
208
+ pym.AssembleStiffness(sx, domain=domain, bc=prescribed_dofs, add_constant=K_const1))
209
+ su = network2.append(pym.LinSolve([signal_stiffness, signal_force], pym.Signal('u')))
210
+ sc2 = network2.append(pym.EinSum(su, expression='i->'))
211
+
212
+ network2.response()
213
+
214
+ npt.assert_allclose(sc.state, sc2.state)
215
+
216
+
217
+ class TestSystemOfEquations(unittest.TestCase):
218
+ def test_sparse_symmetric_real_compliance2d_single_load(self):
219
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
220
+ N=10
221
+ # Set up the domain
222
+ domain = pym.DomainDefinition(N, N)
223
+
224
+ # node groups
225
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
226
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
227
+
228
+ # dof groups
229
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
230
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
231
+ dofs_left_horizontal = dofs_left[0::2]
232
+ dofs_left_vertical = dofs_left[1::2]
233
+
234
+ # free and prescribed dofs
235
+ all_dofs = np.arange(0, 2 * domain.nnodes)
236
+ prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
237
+ free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
238
+
239
+ # Setup solution vectors and rhs
240
+ ff = np.zeros_like(free_dofs, dtype=float)
241
+ ff[:] = np.random.rand(len(free_dofs))
242
+ u = np.zeros_like(all_dofs, dtype=float)
243
+
244
+ u[dofs_left_vertical] = np.random.rand(len(dofs_left_vertical))
245
+ up = u[prescribed_dofs]
246
+
247
+ sff = pym.Signal('ff', ff)
248
+ sup = pym.Signal('up', up)
249
+
250
+ fn = pym.Network()
251
+ sx = pym.Signal('x', np.random.rand(domain.nel))
252
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
253
+ su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
254
+ sc = fn.append(pym.EinSum([su[0], su[1]], expression='i,i->'))
255
+ fn.response()
256
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
257
+ pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
258
+
259
+ def test_sparse_symmetric_real_compliance2d_single_load_u(self):
260
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
261
+ N = 10
262
+ # Set up the domain
263
+ domain = pym.DomainDefinition(N, N)
264
+
265
+ # node groups
266
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
267
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
268
+
269
+ # dof groups
270
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
271
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
272
+ dofs_left_horizontal = dofs_left[0::2]
273
+ dofs_left_vertical = dofs_left[1::2]
274
+
275
+ # free and prescribed dofs
276
+ all_dofs = np.arange(0, 2 * domain.nnodes)
277
+ prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
278
+ free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
279
+
280
+ # Setup solution vectors and rhs
281
+ ff = np.zeros_like(free_dofs, dtype=float)
282
+ ff[:] = np.random.rand(len(free_dofs))
283
+ u = np.zeros_like(all_dofs, dtype=float)
284
+
285
+ u[dofs_left_vertical] = np.random.rand(len(dofs_left_vertical))
286
+ up = u[prescribed_dofs]
287
+
288
+ sff = pym.Signal('ff', ff)
289
+ sup = pym.Signal('up', up)
290
+
291
+ fn = pym.Network()
292
+ sx = pym.Signal('x', np.random.rand(domain.nel))
293
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
294
+ su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
295
+ sc = fn.append(pym.EinSum([su[0]], expression='i->'))
296
+ fn.response()
297
+
298
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
299
+
300
+ pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
301
+
302
+ def test_sparse_symmetric_real_compliance2d_single_load_f(self):
303
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
304
+ N = 10
305
+ # Set up the domain
306
+ domain = pym.DomainDefinition(N, N)
307
+
308
+ # node groups
309
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
310
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
311
+
312
+ # dof groups
313
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
314
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
315
+ dofs_left_horizontal = dofs_left[0::2]
316
+ dofs_left_vertical = dofs_left[1::2]
317
+
318
+ # free and prescribed dofs
319
+ all_dofs = np.arange(0, 2 * domain.nnodes)
320
+ prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
321
+ free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
322
+
323
+ # Setup solution vectors and rhs
324
+ ff = np.zeros_like(free_dofs, dtype=float)
325
+ ff[:] = np.random.rand(len(free_dofs))
326
+ u = np.zeros_like(all_dofs, dtype=float)
327
+
328
+ u[dofs_left_vertical] = np.random.rand(len(dofs_left_vertical))
329
+ up = u[prescribed_dofs]
330
+
331
+ sff = pym.Signal('ff', ff)
332
+ sup = pym.Signal('up', up)
333
+
334
+ fn = pym.Network()
335
+ sx = pym.Signal('x', np.random.rand(domain.nel))
336
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
337
+ su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
338
+ sc = fn.append(pym.EinSum([su[1]], expression='i->'))
339
+ fn.response()
340
+
341
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
342
+
343
+ pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
344
+
345
+ def test_sparse_symmetric_real_compliance2d_single_multi_load(self):
346
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
347
+ N=10
348
+ # Set up the domain
349
+ domain = pym.DomainDefinition(N, N)
350
+
351
+ # node groups
352
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
353
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
354
+
355
+ # dof groups
356
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
357
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
358
+ dofs_left_horizontal = dofs_left[0::2]
359
+ dofs_left_vertical = dofs_left[1::2]
360
+
361
+ # free and prescribed dofs
362
+ all_dofs = np.arange(0, 2 * domain.nnodes)
363
+ prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
364
+ free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
365
+
366
+ # Setup solution vectors and rhs
367
+ ff = np.zeros((len(free_dofs), 2), dtype=float)
368
+ ff[:, :] = np.random.rand(np.shape(ff)[0], np.shape(ff)[1])
369
+ u = np.zeros((len(all_dofs), 2), dtype=float)
370
+
371
+ u[dofs_left_vertical, 0] = np.random.rand(len(dofs_left_vertical))
372
+ u[dofs_left_horizontal, 1] = np.random.rand(len(dofs_left_vertical))
373
+ up = u[prescribed_dofs, :]
374
+
375
+ sff = pym.Signal('ff', ff)
376
+ sup = pym.Signal('up', up)
377
+
378
+ fn = pym.Network()
379
+ sx = pym.Signal('x', np.random.rand(domain.nel))
380
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
381
+ su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
382
+ sc1 = fn.append(pym.EinSum([su[0][:, 0], su[1][:, 0]], expression='i,i->'))
383
+ sc2 = fn.append(pym.EinSum([su[0][:, 1], su[1][:, 1]], expression='i,i->'))
384
+ sc = fn.append(pym.MathGeneral([sc1, sc2], expression='inp0 + inp1'))
385
+ fn.response()
386
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
387
+ pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
388
+
389
+ def test_sparse_symmetric_real_compliance2d_single_multi_load_u(self):
390
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
391
+ N = 10
392
+ # Set up the domain
393
+ domain = pym.DomainDefinition(N, N)
394
+
395
+ # node groups
396
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
397
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
398
+
399
+ # dof groups
400
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
401
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
402
+ dofs_left_horizontal = dofs_left[0::2]
403
+ dofs_left_vertical = dofs_left[1::2]
404
+
405
+ # free and prescribed dofs
406
+ all_dofs = np.arange(0, 2 * domain.nnodes)
407
+ prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
408
+ free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
409
+
410
+ # Setup solution vectors and rhs
411
+ ff = np.zeros((len(free_dofs), 2), dtype=float)
412
+ ff[:, :] = np.random.rand(np.shape(ff)[0], np.shape(ff)[1])
413
+ u = np.zeros((len(all_dofs), 2), dtype=float)
414
+
415
+ u[dofs_left_vertical, 0] = np.random.rand(len(dofs_left_vertical))
416
+ u[dofs_left_horizontal, 1] = np.random.rand(len(dofs_left_vertical))
417
+ up = u[prescribed_dofs, :]
418
+
419
+ sff = pym.Signal('ff', ff)
420
+ sup = pym.Signal('up', up)
421
+
422
+ fn = pym.Network()
423
+ sx = pym.Signal('x', np.random.rand(domain.nel))
424
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
425
+ su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
426
+ sc = fn.append(pym.EinSum(su[0], expression='ij->'))
427
+ fn.response()
428
+
429
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
430
+
431
+ pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
432
+
433
+ def test_sparse_symmetric_real_compliance2d_single_multi_load_f(self):
434
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
435
+ N = 10
436
+ # Set up the domain
437
+ domain = pym.DomainDefinition(N, N)
438
+
439
+ # node groups
440
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
441
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
442
+
443
+ # dof groups
444
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
445
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
446
+ dofs_left_horizontal = dofs_left[0::2]
447
+ dofs_left_vertical = dofs_left[1::2]
448
+
449
+ # free and prescribed dofs
450
+ all_dofs = np.arange(0, 2 * domain.nnodes)
451
+ prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
452
+ free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
453
+
454
+ # Setup solution vectors and rhs
455
+ ff = np.zeros((len(free_dofs), 2), dtype=float)
456
+ ff[:, :] = np.random.rand(np.shape(ff)[0], np.shape(ff)[1])
457
+ u = np.zeros((len(all_dofs), 2), dtype=float)
458
+
459
+ u[dofs_left_vertical, 0] = np.random.rand(len(dofs_left_vertical))
460
+ u[dofs_left_horizontal, 1] = np.random.rand(len(dofs_left_vertical))
461
+ up = u[prescribed_dofs, :]
462
+
463
+ sff = pym.Signal('ff', ff)
464
+ sup = pym.Signal('up', up)
465
+
466
+ fn = pym.Network()
467
+ sx = pym.Signal('x', np.random.rand(domain.nel))
468
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
469
+ su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
470
+ sc = fn.append(pym.EinSum(su[1], expression='ij->'))
471
+ fn.response()
472
+
473
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
474
+
475
+ pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
476
+
477
+
478
+ if __name__ == '__main__':
479
+ unittest.main()
@@ -1,14 +1,16 @@
1
+ import inspect
2
+ import pathlib # For importing files
3
+ import sys
1
4
  import unittest
5
+ from math import isclose
6
+
2
7
  import numpy as np
3
8
  import scipy as sp
4
- from scipy.io import mmread # For importing files
5
- import pathlib # For importing files
6
9
  import scipy.sparse as spsp
7
10
  import scipy.sparse.linalg as spspla
8
- import scipy
11
+ from scipy.io import mmread # For importing files
12
+
9
13
  import pymoto as pym
10
- import sys
11
- import inspect
12
14
 
13
15
  try:
14
16
  import cvxopt
@@ -408,208 +410,5 @@ class TestPardiso(GenericTestSolvers):
408
410
  ]
409
411
 
410
412
 
411
- """ ------------------ TEST LINSOLVE MODULE -------------------- """
412
-
413
-
414
- class DynamicMatrix(pym.Module):
415
- alpha = 0.5
416
- beta = 0.5
417
-
418
- def _response(self, K, M, omega):
419
- return K + 1j * omega * (self.alpha * M + self.beta * K) - omega ** 2 * M
420
-
421
- def _sensitivity(self, dZ):
422
- K, M, omega = [s.state for s in self.sig_in]
423
- dK = np.real(dZ) - (omega * self.beta) * np.imag(dZ)
424
- dM = (-omega ** 2) * np.real(dZ) - (omega * self.alpha) * np.imag(dZ)
425
- dZrM = np.real(dZ).contract(M)
426
- dZiK = np.imag(dZ).contract(K)
427
- dZiM = np.imag(dZ).contract(M)
428
- domega = -self.beta * dZiK - self.alpha * dZiM - 2 * omega * dZrM
429
- return dK, dM, domega
430
-
431
- class TestLinSolveModule_sparse(unittest.TestCase):
432
- # # ------------- Symmetric -------------
433
- def test_symmetric_real_compliance2d(self):
434
- """ Test symmetric real sparse matrix (compliance in 2D)"""
435
- N = 10 # Number of elements
436
- dom = pym.DomainDefinition(N, N)
437
- sx = pym.Signal('x', np.random.rand(dom.nel))
438
- fixed_nodes = dom.get_nodenumber(0, np.arange(0, N+1))
439
- bc = np.concatenate((fixed_nodes*2, fixed_nodes*2+1))
440
- # Setup different rhs types
441
- iforce_x = dom.get_nodenumber(N, np.arange(0, N + 1)) * 2 # Force in x-direction
442
- iforce_y = dom.get_nodenumber(N, np.arange(0, N + 1)) * 2 + 1 # Force in y-direction
443
-
444
- force_vecs = dict()
445
-
446
- # Single force
447
- f = np.zeros(dom.nnodes*2)
448
- f[iforce_x] = 1.0
449
- force_vecs['single_real'] = f
450
-
451
- # Multiple rhs
452
- f = np.zeros((dom.nnodes * 2, 2))
453
- f[iforce_x, 0] = 1.0
454
- f[iforce_y, 1] = 1.0
455
- force_vecs['multiple_real'] = f
456
-
457
- for k, f in force_vecs.items():
458
- with self.subTest(f"RHS-{k}"):
459
- sf = pym.Signal('f', f)
460
-
461
- fn = pym.Network()
462
- sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), dom, bc=bc))
463
- su = fn.append(pym.LinSolve([sK, sf], pym.Signal('u')))
464
-
465
- fn.response()
466
-
467
- self.assertTrue(np.allclose(sK.state@su.state, sf.state)) # Check residual
468
- # Check finite difference
469
- # def tfn(x0, dx, df_an, df_fd): np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5)
470
- def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
471
- pym.finite_difference(fn, [sx, sf], su, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
472
-
473
- def test_symmetric_real_compliance3d(self):
474
- """ Test symmetric real sparse matrix (compliance in 3D)"""
475
- N = 3 # Number of elements
476
- dom = pym.DomainDefinition(N, N, N)
477
- sx = pym.Signal('x', np.random.rand(dom.nel))
478
- jfix, kfix = np.meshgrid(np.arange(0, N+1), np.arange(0, N+1), indexing='ij')
479
- fixed_nodes = dom.get_nodenumber(0, jfix, kfix).flatten()
480
- bc = np.concatenate((fixed_nodes*3, fixed_nodes*3+1, fixed_nodes*3+2))
481
- iforce = dom.get_nodenumber(N, np.arange(0, N+1), np.arange(0, N+1))*3 + 1
482
- sf = pym.Signal('f', np.zeros(dom.nnodes*3))
483
- sf.state[iforce] = 1.0
484
-
485
- fn = pym.Network()
486
- sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), dom, bc=bc))
487
- su = fn.append(pym.LinSolve([sK, sf], pym.Signal('u')))
488
-
489
- fn.response()
490
-
491
- self.assertTrue(np.allclose(sK.state@su.state, sf.state)) # Check residual
492
- # Check finite difference
493
- # def tfn(x0, dx, df_an, df_fd): np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5)
494
- def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=2e-3, atol=1e-5))
495
- pym.finite_difference(fn, [sx, sf], su, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
496
-
497
- def test_symmetric_complex_dyncompliance2d(self):
498
- """ Test symmetric complex sparse matrix (dynamic compliance in 2D)"""
499
- N = 5 # Number of elements
500
- dom = pym.DomainDefinition(N, N)
501
- sx = pym.Signal('x', np.random.rand(dom.nel))
502
- fixed_nodes = dom.get_nodenumber(0, np.arange(0, N+1))
503
- bc = np.concatenate((fixed_nodes*2, fixed_nodes*2+1))
504
- iforce = dom.get_nodenumber(N, np.arange(0, N+1))*2 + 1
505
- sf = pym.Signal('f', np.zeros(dom.nnodes*2))
506
- sf.state[iforce] = 1.0
507
-
508
- sOmega = pym.Signal('omega', 0.1)
509
- fn = pym.Network()
510
- sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), dom, bc=bc))
511
- sM = fn.append(pym.AssembleMass(sx, pym.Signal('M'), dom, bc=bc))
512
- sZ = fn.append(DynamicMatrix([sK, sM, sOmega], pym.Signal('Z')))
513
-
514
- su = fn.append(pym.LinSolve([sZ, sf], pym.Signal('u')))
515
-
516
- fn.response()
517
-
518
- # spspla.eigsh(sK.state, M=sM.state, k=6, sigma=0.0)
519
-
520
- self.assertTrue(np.allclose(sZ.state@su.state, sf.state)) # Check residual
521
- # Check finite difference
522
- # def tfn(x0, dx, df_an, df_fd): np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5)
523
- def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
524
- pym.finite_difference(fn, [sx, sf, sOmega], su, test_fn=tfn, dx=1e-7, tol=1e-4, verbose=False)
525
-
526
-
527
- class TestSystemOfEquations(unittest.TestCase):
528
- def test_sparse_symmetric_real_compliance2d_single_load(self):
529
- """ Test symmetric real sparse matrix (compliance in 2D)"""
530
- N=10
531
- # Set up the domain
532
- domain = pym.DomainDefinition(N, N)
533
-
534
- # node groups
535
- nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
536
- nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
537
-
538
- # dof groups
539
- dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
540
- dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
541
- dofs_left_horizontal = dofs_left[0::2]
542
- dofs_left_vertical = dofs_left[1::2]
543
-
544
- # free and prescribed dofs
545
- all_dofs = np.arange(0, 2 * domain.nnodes)
546
- prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
547
- free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
548
-
549
- # Setup solution vectors and rhs
550
- ff = np.zeros_like(free_dofs, dtype=float)
551
- ff[:] = np.random.rand(len(free_dofs))
552
- u = np.zeros_like(all_dofs, dtype=float)
553
-
554
- u[dofs_left_vertical] = np.random.rand(len(dofs_left_vertical))
555
- up = u[prescribed_dofs]
556
-
557
- sff = pym.Signal('ff', ff)
558
- sup = pym.Signal('up', up)
559
-
560
- fn = pym.Network()
561
- sx = pym.Signal('x', np.random.rand(domain.nel))
562
- sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
563
- su = fn.append(pym.SystemOfEquations([sK, sff, sup], free=free_dofs, prescribed=prescribed_dofs))
564
- sc = fn.append(pym.EinSum([su[0], su[1]], expression='i,i->'))
565
- fn.response()
566
- def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
567
- pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
568
-
569
- def test_sparse_symmetric_real_compliance2d_single_multi_load(self):
570
- """ Test symmetric real sparse matrix (compliance in 2D)"""
571
- N=10
572
- # Set up the domain
573
- domain = pym.DomainDefinition(N, N)
574
-
575
- # node groups
576
- nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
577
- nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
578
-
579
- # dof groups
580
- dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
581
- dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
582
- dofs_left_horizontal = dofs_left[0::2]
583
- dofs_left_vertical = dofs_left[1::2]
584
-
585
- # free and prescribed dofs
586
- all_dofs = np.arange(0, 2 * domain.nnodes)
587
- prescribed_dofs = np.unique(np.hstack([dofs_left_horizontal, dofs_right, dofs_left_vertical]))
588
- free_dofs = np.setdiff1d(all_dofs, prescribed_dofs)
589
-
590
- # Setup solution vectors and rhs
591
- ff = np.zeros((len(free_dofs), 2), dtype=float)
592
- ff[:, :] = np.random.rand(np.shape(ff)[0], np.shape(ff)[1])
593
- u = np.zeros((len(all_dofs), 2), dtype=float)
594
-
595
- u[dofs_left_vertical, 0] = np.random.rand(len(dofs_left_vertical))
596
- u[dofs_left_horizontal, 1] = np.random.rand(len(dofs_left_vertical))
597
- up = u[prescribed_dofs, :]
598
-
599
- sff = pym.Signal('ff', ff)
600
- sup = pym.Signal('up', up)
601
-
602
- fn = pym.Network()
603
- sx = pym.Signal('x', np.random.rand(domain.nel))
604
- sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
605
- su = fn.append(pym.SystemOfEquations([sK, sff, sup], prescribed=prescribed_dofs))
606
- sc1 = fn.append(pym.EinSum([su[0][:, 0], su[1][:, 0]], expression='i,i->'))
607
- sc2 = fn.append(pym.EinSum([su[0][:, 1], su[1][:, 1]], expression='i,i->'))
608
- sc = fn.append(pym.MathGeneral([sc1, sc2], expression='inp0 + inp1'))
609
- fn.response()
610
- def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
611
- pym.finite_difference(fn, [sx, sff, sup], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
612
-
613
-
614
413
  if __name__ == '__main__':
615
414
  unittest.main()
@@ -0,0 +1,55 @@
1
+ import unittest
2
+ import numpy as np
3
+ import pymoto as pym
4
+ import numpy.testing as npt
5
+
6
+ np.random.seed(0)
7
+
8
+
9
+ class TestStaticCondensation(unittest.TestCase):
10
+ def test_real_symmetric(self):
11
+ """ Test symmetric real sparse matrix (compliance in 2D)"""
12
+ N = 20
13
+ # Set up the domain
14
+ domain = pym.DomainDefinition(N, N)
15
+
16
+ # node groups
17
+ nodes_left = domain.get_nodenumber(0, np.arange(N + 1))
18
+ nodes_right = domain.get_nodenumber(N, np.arange(N + 1))
19
+
20
+ # dof groups
21
+ dofs_left = np.repeat(nodes_left * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
22
+ dofs_right = np.repeat(nodes_right * 2, 2, axis=-1) + np.tile(np.arange(2), N + 1)
23
+
24
+ # free and prescribed dofs
25
+ all_dofs = np.arange(0, 2 * domain.nnodes)
26
+ prescribed_dofs = dofs_right
27
+ main_dofs = dofs_left[0::2]
28
+ free_dofs = np.setdiff1d(all_dofs, np.unique(np.hstack([main_dofs, prescribed_dofs])))
29
+
30
+ fn = pym.Network()
31
+ sx = pym.Signal('x', np.random.rand(domain.nel))
32
+ sK = fn.append(pym.AssembleStiffness(sx, pym.Signal('K'), domain))
33
+ su = fn.append(pym.StaticCondensation([sK], free=free_dofs, main=main_dofs))
34
+ sc = fn.append(pym.EinSum([su], expression='ij->'))
35
+ fn.response()
36
+
37
+ # Check result
38
+ fn1 = pym.Network()
39
+ sK1 = fn1.append(pym.AssembleStiffness(sx, domain=domain, bc=np.concatenate([main_dofs[1:], prescribed_dofs])))
40
+ sf = pym.Signal('f', np.zeros(domain.nnodes*2))
41
+ sf.state[main_dofs[0]] = 1.0
42
+ su1 = fn1.append(pym.LinSolve([sK1, sf]))
43
+ suKu1 = fn1.append(pym.EinSum([su1, sf], expression='i,i->'))
44
+ fn1.response()
45
+
46
+ npt.assert_allclose(suKu1.state, 1/su.state[0, 0])
47
+
48
+ def tfn(x0, dx, df_an, df_fd): self.assertTrue(np.allclose(df_an, df_fd, rtol=1e-3, atol=1e-5))
49
+
50
+ pym.finite_difference(fn, [sx], sc, test_fn=tfn, dx=1e-5, tol=1e-4, verbose=False)
51
+
52
+
53
+
54
+ if __name__ == '__main__':
55
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes