pyMOTO 1.2.1__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,8 +49,8 @@ class AssembleGeneral(Module):
49
49
  self.dofconn = domain.get_dofconnectivity(self.ndof)
50
50
 
51
51
  # Row and column indices for the matrix
52
- self.rows = np.kron(self.dofconn, np.ones((domain.elemnodes*self.ndof, 1), dtype=int)).flatten()
53
- self.cols = np.kron(self.dofconn, np.ones((1, domain.elemnodes*self.ndof), dtype=int)).flatten()
52
+ self.rows = np.kron(self.dofconn, np.ones((1, domain.elemnodes*self.ndof), dtype=int)).flatten()
53
+ self.cols = np.kron(self.dofconn, np.ones((domain.elemnodes * self.ndof, 1), dtype=int)).flatten()
54
54
  self.matrix_type = matrix_type
55
55
 
56
56
  # Boundary conditions
@@ -68,6 +68,8 @@ class AssembleGeneral(Module):
68
68
  self.add_constant = add_constant
69
69
 
70
70
  def _response(self, xscale: np.ndarray):
71
+ nel = self.dofconn.shape[0]
72
+ assert xscale.size == nel, f"Input vector wrong size ({xscale.size}), must be of size #nel ({nel})"
71
73
  scaled_el = ((self.elmat.flatten()[np.newaxis]).T * xscale).flatten(order='F')
72
74
 
73
75
  # Set boundary conditions
@@ -104,15 +106,17 @@ class AssembleGeneral(Module):
104
106
  return dgdmat.contract(self.elmat, self.dofconn, self.dofconn)
105
107
 
106
108
 
107
- def get_B(dN_dx):
109
+ def get_B(dN_dx, voigt=True):
108
110
  """ Gets the strain-displacement relation (Cook, eq 3.1-9, P.80)
109
111
 
110
112
  - 1D : [ε_x]_i = B [u]_i
111
113
  - 2D : [ε_x; ε_y; γ_xy]_i = B [u, v]_i
112
- - 3D : [ε_x; ε_y; ε_z; γ_xy; γ_yz; γ_zx]_i = B [u, v, w]_i
114
+ - 3D : [ε_x; ε_y; ε_z; γ_xy; γ_yz; γ_zx]_i = B [u, v, w]_i (standard notation)
115
+ [ε_x; ε_y; ε_z; γ_yz; γ_zx; γ_xy]_i = B [u, v, w]_i (Voigt notation)
113
116
 
114
117
  Args:
115
118
  dN_dx: Shape function derivatives [dNi_dxj] of size (#shapefn. x #dimensions)
119
+ voigt(optional): Use Voigt notation for the shear terms [yz, zx, xy] or standard notation [xy, yz, zx]
116
120
 
117
121
  Returns:
118
122
  B strain-displacement relation of size (#strains x #shapefn.*#dimensions)
@@ -130,12 +134,21 @@ def get_B(dN_dx):
130
134
  [dN_dx[1, i], dN_dx[0, i]]])
131
135
  elif n_dim == 3:
132
136
  for i in range(n_shapefn):
133
- B[:, i*n_dim:(i+1)*n_dim] = np.array([[dN_dx[0, i], 0, 0],
134
- [0, dN_dx[1, i], 0],
135
- [0, 0, dN_dx[2, i]],
136
- [dN_dx[1, i], dN_dx[0, i], 0],
137
- [0, dN_dx[2, i], dN_dx[1, i]],
138
- [dN_dx[2, i], 0, dN_dx[0, i]]])
137
+ if voigt:
138
+ B[:, i*n_dim:(i+1)*n_dim] = np.array([[dN_dx[0, i], 0, 0],
139
+ [0, dN_dx[1, i], 0],
140
+ [0, 0, dN_dx[2, i]],
141
+ [0, dN_dx[2, i], dN_dx[1, i]],
142
+ [dN_dx[2, i], 0, dN_dx[0, i]],
143
+ [dN_dx[1, i], dN_dx[0, i], 0],
144
+ ])
145
+ else:
146
+ B[:, i * n_dim:(i + 1) * n_dim] = np.array([[dN_dx[0, i], 0, 0],
147
+ [0, dN_dx[1, i], 0],
148
+ [0, 0, dN_dx[2, i]],
149
+ [dN_dx[1, i], dN_dx[0, i], 0],
150
+ [0, dN_dx[2, i], dN_dx[1, i]],
151
+ [dN_dx[2, i], 0, dN_dx[0, i]]])
139
152
  else:
140
153
  raise ValueError(f"Number of dimensions ({n_dim}) cannot be greater than 3")
141
154
  return B
@@ -209,7 +222,7 @@ class AssembleStiffness(AssembleGeneral):
209
222
  ndof = nnode*domain.dim
210
223
 
211
224
  # Element stiffness matrix
212
- self.KE = np.zeros((ndof, ndof))
225
+ self.stiffness_element = np.zeros((ndof, ndof))
213
226
 
214
227
  # Numerical integration
215
228
  siz = domain.element_size
@@ -218,13 +231,13 @@ class AssembleStiffness(AssembleGeneral):
218
231
  pos = n*(siz/2)/np.sqrt(3) # Sampling point
219
232
  dN_dx = domain.eval_shape_fun_der(pos)
220
233
  B = get_B(dN_dx)
221
- self.KE += w * B.T @ D @ B # Add contribution
234
+ self.stiffness_element += w * B.T @ D @ B # Add contribution
222
235
 
223
- super()._prepare(domain, self.KE, *args, **kwargs)
236
+ super()._prepare(domain, self.stiffness_element, *args, **kwargs)
224
237
 
225
238
 
226
239
  class AssembleMass(AssembleGeneral):
227
- r""" Consistent mass matrix assembly by scaling elements
240
+ r""" Consistent mass matrix or equivalents assembly by scaling elements
228
241
  :math:`\mathbf{M} = \sum_e x_e \mathbf{M}_e`
229
242
 
230
243
  Input Signal:
@@ -235,38 +248,186 @@ class AssembleMass(AssembleGeneral):
235
248
 
236
249
  Args:
237
250
  domain: The domain to assemble for -- this determines the element size and dimensionality
251
+ ndof: Amount of dofs per node (for mass and damping: ndof = domain.dim; else ndof=1)
238
252
  *args: Other arguments are passed to AssembleGeneral
239
253
 
240
254
  Keyword Args:
241
- rho: Base density
255
+ material_property: Material property to use in the element matrix (for mass matrix the material density is used;
256
+ for damping the damping parameter, and for a thermal capacity matrix the thermal capacity multiplied with
257
+ density)
242
258
  bcdiagval: The value to put on the diagonal in case of boundary conditions (bc)
243
- **kwargs : Other keyword-arguments are passed to AssembleGeneral
259
+ **kwargs: Other keyword-arguments are passed to AssembleGeneral
244
260
  """
245
261
 
246
- def _prepare(self, domain: DomainDefinition, *args, rho: float = 1.0, bcdiagval=0.0, **kwargs):
247
- # Element mass matrix
248
- # 1/36 Mass of one element
249
- mel = rho * np.prod(domain.element_size)
262
+ def _prepare(self, domain: DomainDefinition, *args, material_property: float = 1.0, ndof: int = 1,
263
+ bcdiagval: float = 0.0, **kwargs):
264
+ # Element mass (or equivalent) matrix
265
+ self.el_mat = np.zeros((domain.elemnodes * ndof, domain.elemnodes * ndof))
250
266
 
267
+ # Numerical integration
268
+ siz = domain.element_size
269
+ w = np.prod(siz[:domain.dim] / 2)
270
+ if domain.dim != 3:
271
+ material_property *= np.prod(siz[domain.dim:])
272
+ Nmat = np.zeros((ndof, domain.elemnodes * ndof))
273
+
274
+ for n in domain.node_numbering:
275
+ pos = n * (siz / 2) / np.sqrt(3) # Sampling point
276
+ N = domain.eval_shape_fun(pos)
277
+ for d in range(domain.elemnodes):
278
+ Nmat[0:ndof, ndof * d:ndof * d + ndof] = np.identity(ndof) * N[d] # fill up shape function matrix according to ndof
279
+ self.el_mat += w * material_property * Nmat.T @ Nmat # Add contribution
280
+
281
+ super()._prepare(domain, self.el_mat, *args, bcdiagval=bcdiagval, **kwargs)
282
+
283
+
284
+ class AssemblePoisson(AssembleGeneral):
285
+ r""" Assembly of matrix to solve Poisson equation (e.g. Thermal conductivity, Electric permittivity)
286
+ :math:`\mathbf{P} = \sum_e x_e \mathbf{P}_e`
287
+
288
+ Input Signal:
289
+ - ``x``: Scaling vector of size ``(Nel)``
290
+
291
+ Output Signal:
292
+ - ``P``: Poisson matrix of size ``(n, n)``
293
+
294
+ Args:
295
+ domain: The domain to assemble for -- this determines the element size and dimensionality
296
+ args (optional): Other arguments are passed to AssembleGeneral
297
+
298
+ Keyword Args:
299
+ material_property: Material property (e.g. thermal conductivity, electric permittivity)
300
+ bcdiagval: The value to put on the diagonal in case of boundary conditions (bc)
301
+ kwargs: Other keyword-arguments are passed to AssembleGeneral
302
+ """
303
+
304
+ def _prepare(self, domain: DomainDefinition, *args, material_property: float = 1.0, **kwargs):
305
+ # Prepare material properties and element matrices
306
+ self.material_property = material_property
307
+ self.poisson_element = np.zeros((domain.elemnodes, domain.elemnodes))
308
+
309
+ # Numerical Integration
310
+ siz = domain.element_size
311
+ w = np.prod(siz[:domain.dim]/2)
312
+ if domain.dim != 3:
313
+ self.material_property *= siz[domain.dim:]
314
+
315
+ for n in domain.node_numbering:
316
+ pos = n*(siz/2)/np.sqrt(3) # Sampling point
317
+ Bn = domain.eval_shape_fun_der(pos)
318
+ self.poisson_element += w * self.material_property * Bn.T @ Bn # Add contribution
319
+
320
+ super()._prepare(domain, self.poisson_element, *args, **kwargs)
321
+
322
+
323
+ class ElementOperation(Module):
324
+ r""" Generic module for element-wise operations based on nodal information
325
+
326
+ :math:`y_e = \mathbf{B} \mathbf{u}_e`
327
+
328
+ Input Signal:
329
+ - ``u``: Nodal vector of size ``(n_dof_per_element * #nodes)``
330
+
331
+ Output Signal:
332
+ - ``y``: Elemental output data of size ``(..., #elements)``
333
+
334
+ Args:
335
+ domain: The domain defining element and nodal connectivity
336
+ element_matrix: The element operator matrix :math:`\mathbf{B}` of size ``(..., n_dof_per_element)``
337
+ """
338
+ def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray):
339
+ if element_matrix.shape[-1] % domain.elemnodes != 0:
340
+ raise IndexError("Size of last dimension of element operator matrix is not compatible with mesh. "
341
+ "Must be dividable by the number of nodes.")
342
+
343
+ ndof = element_matrix.shape[-1] // domain.elemnodes
344
+
345
+ self.element_matrix = element_matrix
346
+ self.dofconn = domain.get_dofconnectivity(ndof)
347
+ self.usiz = ndof * domain.nnodes
348
+
349
+ def _response(self, u):
350
+ assert u.size == self.usiz
351
+ return einsum('...k, lk -> ...l', self.element_matrix, u[self.dofconn], optimize=True)
352
+
353
+ def _sensitivity(self, dy):
354
+ du_el = einsum('...k, ...l -> lk', self.element_matrix, dy, optimize=True)
355
+ du = np.zeros_like(self.sig_in[0].state)
356
+ np.add.at(du, self.dofconn, du_el)
357
+ return du
358
+
359
+
360
+ class Strain(ElementOperation):
361
+ r""" Evaluate average mechanical strains in solid elements based on deformation
362
+
363
+ The strains are returned in Voigt notation.
364
+ :math:`\mathbf{\epsilon}_e = \mathbf{B} \mathbf{u}_e`
365
+
366
+ Each integration point in the element has different strain values. Here, the average is returned.
367
+
368
+ The returned strain is either
369
+ :math:`\mathbf{\epsilon} = \begin{bmatrix}\epsilon_{xx} & \epsilon_{yy} & \epsilon_{xy} \end{bmatrix}`
370
+ in case ``voigt = False`` or
371
+ :math:`\mathbf{\epsilon} = \begin{bmatrix}\epsilon_{xx} & \epsilon_{yy} & \gamma_{xy} \end{bmatrix}`
372
+ in case ``voigt = True``, for which :math:`\gamma_{xy}=2\epsilon_{xy}`.
373
+
374
+ Input Signal:
375
+ - ``u``: Nodal vector of size ``(#dof per element * #nodes)``
376
+
377
+ Output Signal:
378
+ - ``e``: Strain matrix of size ``(#strains per element, #elements)``
379
+
380
+ Args:
381
+ domain: The domain defining element and nodal connectivity
382
+
383
+ Keyword Args:
384
+ voigt: Use Voigt strain notation (2x off-diagonal strain contribution)
385
+ """
386
+ def _prepare(self, domain: DomainDefinition, voigt: bool = True):
387
+ # Numerical integration
388
+ B = None
389
+ siz = domain.element_size
390
+ w = 1/domain.elemnodes # Average strain at the integration points
391
+ for n in domain.node_numbering:
392
+ pos = n * (siz / 2) / np.sqrt(3) # Sampling point
393
+ dN_dx = domain.eval_shape_fun_der(pos)
394
+ B_add = w*get_B(dN_dx) # Add contribution
395
+ if B is None:
396
+ B = B_add
397
+ else:
398
+ B += B_add
399
+
400
+ if voigt:
401
+ idx_shear = np.count_nonzero(B, axis=1) == 2*domain.elemnodes # Shear is combination of two displacements
402
+ B[idx_shear, :] *= 2 # Use engineering strain
403
+
404
+ super()._prepare(domain, B)
405
+
406
+
407
+ class Stress(Strain):
408
+ """ Calculate the average stresses per element
409
+
410
+ Input Signal:
411
+ - ``u``: Nodal vector of size ``(#dof per element * #nodes)``
412
+
413
+ Output Signal:
414
+ - ``s``: Stress matrix of size ``(#stresses per element, #elements)``
415
+
416
+ Args:
417
+ domain: The domain defining element and nodal connectivity
418
+
419
+ Keyword Args:
420
+ e_modulus: Young's modulus
421
+ poisson_ratio: Poisson ratio
422
+ plane: Plane 'strain' or 'stress'
423
+ """
424
+ def _prepare(self, domain: DomainDefinition,
425
+ e_modulus: float = 1.0, poisson_ratio: float = 0.3, plane: str = 'strain'):
426
+
427
+ super()._prepare(domain, voigt=True)
428
+
429
+ # Get material relation
430
+ D = get_D(e_modulus, poisson_ratio, '3d' if domain.dim == 3 else plane.lower())
251
431
  if domain.dim == 2:
252
- # Consistent mass matrix
253
- ME = mel / 36 * np.array([[4.0, 0.0, 2.0, 0.0, 2.0, 0.0, 1.0, 0.0],
254
- [0.0, 4.0, 0.0, 2.0, 0.0, 2.0, 0.0, 1.0],
255
- [2.0, 0.0, 4.0, 0.0, 1.0, 0.0, 2.0, 0.0],
256
- [0.0, 2.0, 0.0, 4.0, 0.0, 1.0, 0.0, 2.0],
257
- [2.0, 0.0, 1.0, 0.0, 4.0, 0.0, 2.0, 0.0],
258
- [0.0, 2.0, 0.0, 1.0, 0.0, 4.0, 0.0, 2.0],
259
- [1.0, 0.0, 2.0, 0.0, 2.0, 0.0, 4.0, 0.0],
260
- [0.0, 1.0, 0.0, 2.0, 0.0, 2.0, 0.0, 4.0]])
261
- elif domain.dim == 3:
262
- ME = np.zeros((domain.elemnodes*domain.dim, domain.elemnodes*domain.dim))
263
- weights = np.array([8.0, 4.0, 2.0, 1.0])
264
- for n1 in range(domain.elemnodes):
265
- for n2 in range(domain.elemnodes):
266
- dist = round(np.sum(abs(np.array(domain.node_numbering[n1]) - np.array(domain.node_numbering[n2])))/2)
267
- ME[n1*domain.dim+np.arange(domain.dim), n2*domain.dim+np.arange(domain.dim)] = weights[dist]
268
-
269
- ME *= mel / 216
270
- else:
271
- raise RuntimeError("Only for 2D and 3D")
272
- super()._prepare(domain, ME, *args, bcdiagval=bcdiagval, **kwargs)
432
+ D *= domain.element_size[2]
433
+ self.element_matrix = D @ self.element_matrix
pymoto/modules/complex.py CHANGED
@@ -73,7 +73,7 @@ class MakeComplex(Module):
73
73
 
74
74
 
75
75
  class RealPart(Module):
76
- """ Takes the real part of a complex value :math:`x = \\text{Re}(z)`
76
+ r""" Takes the real part of a complex value :math:`x = \text{Re}(z)`
77
77
 
78
78
  Input Signal:
79
79
  - ``z``: Complex value
@@ -89,7 +89,7 @@ class RealPart(Module):
89
89
 
90
90
 
91
91
  class ImagPart(Module):
92
- """ Takes the imaginary part of a complex value :math:`y = \\text{Im}(z)`
92
+ r""" Takes the imaginary part of a complex value :math:`y = \text{Im}(z)`
93
93
 
94
94
  Input Signal:
95
95
  - ``z``: Complex value
@@ -105,7 +105,7 @@ class ImagPart(Module):
105
105
 
106
106
 
107
107
  class ComplexNorm(Module):
108
- """ Takes the complex norm :math:`A = z z^*`
108
+ r""" Takes the complex norm :math:`A = \sqrt(z z^*)`
109
109
 
110
110
  Input Signal:
111
111
  - ``z``: Complex value
pymoto/modules/filter.py CHANGED
@@ -1,46 +1,191 @@
1
- from pymoto import Module
2
- from .assembly import DomainDefinition
1
+ from pymoto import Module, DomainDefinition
3
2
  import numpy as np
4
3
  from scipy.sparse import coo_matrix
5
- from scipy.ndimage import convolve
4
+ from scipy.signal import convolve, correlate
5
+ from numbers import Number
6
6
 
7
7
 
8
8
  class FilterConv(Module):
9
- def _prepare(self, domain: DomainDefinition, weights: np.ndarray, mode: str = 'reflect'):
10
- self.domain = domain
11
- self.weights = weights
12
- self.mode = mode
9
+ r""" Density filter based on convolution
10
+
11
+ Either the argument filter radius (`radius`) or a filtering kernel `weights` needs to be provided. If a filter
12
+ radius is passed, the standard linear density filter will be used (see :py:class:`pymoto.DensityFilter`).
13
+
14
+ For the boundaries, a padded effect can be selected from the following options:
15
+ 'symmetric' (default)
16
+ Pads with the reflection of the vector mirrored
17
+ along the edge of the array.
18
+ value (e.g. `1.0` or `0.0`)
19
+ Pads with a constant value.
20
+ 'edge'
21
+ Pads with the edge values of array.
22
+ 'wrap'
23
+ Pads with the wrap of the vector along the axis.
24
+ The first values are used to pad the end and the
25
+ end values are used to pad the beginning.
13
26
 
14
- def set_filter_radius(self, radius: float, relative_units: bool = False):
27
+ Args:
28
+ domain: The DomainDefinition
29
+ radius (optional): Filter radius
30
+ relative_units(optional): Indicate if the filter radius is in relative units with respect to the element-size or
31
+ is given as an absolute size
32
+ weights(optional): Filtering kernel (2D or 3D array)
33
+ xmin_bc(optional): Boundary condition for the boundary at minimum x-value
34
+ xmax_bc(optional): Boundary condition for the boundary at maximum x-value
35
+ ymin_bc(optional): Boundary condition at minimum y
36
+ ymax_bc(optional): Boundary condition at maximum y
37
+ zmin_bc(optional): Boundary condition at minimum z (only in 3D)
38
+ zmax_bc(optional): Bounadry condition at maximum z (only in 3D)
39
+ """
40
+ def _prepare(self, domain: DomainDefinition, radius: float = None, relative_units: bool = True, weights: np.ndarray = None,
41
+ xmin_bc='symmetric', xmax_bc='symmetric',
42
+ ymin_bc='symmetric', ymax_bc='symmetric',
43
+ zmin_bc='symmetric', zmax_bc='symmetric'):
44
+
45
+ self.domain = domain
46
+ self.weights = None
47
+ if (weights is None and radius is None) or (weights is not None and radius is not None):
48
+ raise ValueError("Only one of arguments 'filter_radius' or 'weights' must be provided.")
49
+ elif weights is not None:
50
+ self.weights = weights.copy()
51
+ while self.weights.ndim < 3:
52
+ self.weights = np.expand_dims(self.weights, axis=-1)
53
+ for i in range(self.weights.ndim):
54
+ assert self.weights.shape[i] % 2 == 1, "Size of weights must be uneven"
55
+ elif radius is not None:
56
+ self.set_filter_radius(radius, relative_units)
57
+
58
+ # Process padding
59
+ self.overrides = []
60
+ self.pad_sizes = [v//2 for v in self.weights.shape]
61
+
62
+ domain_sizes = [self.domain.nelx, self.domain.nely, self.domain.nelz]
63
+ el_x, el_y, el_z = np.meshgrid(*[np.arange(max(1, s)) for s in domain_sizes], indexing='ij')
64
+ self.el3d_orig = self.domain.get_elemnumber(el_x, el_y, el_z)
65
+
66
+ padx = self._process_padding(self.el3d_orig, xmin_bc, xmax_bc, 0, self.pad_sizes[0])
67
+ pady = self._process_padding(padx, ymin_bc, ymax_bc, 1, self.pad_sizes[1])
68
+ self.el3d_pad = self._process_padding(pady, zmin_bc, zmax_bc, 2, self.pad_sizes[2])
69
+
70
+ def _process_padding(self, indices, type_edge0, type_edge1, direction: int, pad_size: int):
71
+ # First process wrapped padding
72
+ wrap_size = (0, 0)
73
+ do_wrap = False
74
+ if type_edge0 == 'wrap':
75
+ do_wrap = True
76
+ wrap_size = (pad_size, wrap_size[1])
77
+ if type_edge1 == 'wrap':
78
+ do_wrap = True
79
+ wrap_size = (wrap_size[0], pad_size)
80
+
81
+ if do_wrap:
82
+ pad_width = [(0, 0) for _ in range(indices.ndim)]
83
+ pad_width[direction] = wrap_size
84
+ pad1a = np.pad(indices, pad_width, mode='wrap')
85
+ else:
86
+ pad1a = indices
87
+
88
+ domain_sizes = [self.domain.nelx, self.domain.nely, self.domain.nelz]
89
+ padded_sizes = [self.domain.nelx + 2 * self.pad_sizes[0],
90
+ self.domain.nely + 2 * self.pad_sizes[1],
91
+ self.domain.nelz + 2 * self.pad_sizes[2]]
92
+
93
+ # Process edge 1
94
+ pad_width = [(0, 0) for _ in range(indices.ndim)]
95
+ pad_width[direction] = (0, pad_size)
96
+ if type_edge1 == 'edge':
97
+ pad1b = np.pad(pad1a, pad_width, mode='edge')
98
+ elif type_edge1 == 'symmetric':
99
+ pad1b = np.pad(pad1a, pad_width, mode='symmetric')
100
+ elif isinstance(type_edge1, Number): # Constant
101
+ value = type_edge1
102
+ pad1b = np.pad(pad1a, pad_width, mode='constant', constant_values=0)
103
+
104
+ n_range = [np.arange(max(1, s)) for s in padded_sizes]
105
+ n_range[direction] = pad_size + domain_sizes[direction] + np.arange(pad_size)
106
+
107
+ el_nos = np.meshgrid(*n_range, indexing='ij')
108
+ self.override_padded_values(tuple(el_nos), value)
109
+ else:
110
+ pad1b = pad1a
111
+
112
+ # Process edge 0
113
+ pad_width = [(0, 0) for _ in range(indices.ndim)]
114
+ pad_width[direction] = (pad_size, 0)
115
+ if type_edge0 == 'edge':
116
+ pad1 = np.pad(pad1b, pad_width, mode='edge')
117
+ elif type_edge0 == 'symmetric':
118
+ pad1 = np.pad(pad1b, pad_width, mode='symmetric')
119
+ elif isinstance(type_edge0, Number):
120
+ value = type_edge0
121
+ pad1 = np.pad(pad1b, pad_width, mode='constant', constant_values=0)
122
+
123
+ n_range = [np.arange(max(1, s)) for s in padded_sizes]
124
+ n_range[direction] = np.arange(pad_size)
125
+
126
+ el_nos = np.meshgrid(*n_range, indexing='ij')
127
+ self.override_padded_values(tuple(el_nos), value)
128
+ else:
129
+ pad1 = pad1b
130
+ return pad1
131
+
132
+ @property
133
+ def padded_domain(self):
134
+ domain_sizes = [self.domain.nelx, self.domain.nely, self.domain.nelz]
135
+ nx, ny, nz = [n + 2*p for n, p in zip(domain_sizes, self.pad_sizes)]
136
+ lx, ly, lz = self.domain.element_size
137
+ return DomainDefinition(nx, ny, nz, unitx=lx, unity=ly, unitz=lz)
138
+
139
+ def override_padded_values(self, index, value):
140
+ if all([np.asarray(i).size == 0 for i in index]):
141
+ # Don't add empty sets
142
+ return
143
+ self.overrides.append((index, value))
144
+
145
+ def override_values(self, index, value):
146
+ # Change index to extended domain
147
+ xrange = self.pad_sizes[0] + np.arange(max(1, self.domain.nelx))
148
+ yrange = self.pad_sizes[1] + np.arange(max(1, self.domain.nely))
149
+ zrange = self.pad_sizes[2] + np.arange(max(1, self.domain.nelz))
150
+ el_x, el_y, el_z = np.meshgrid(xrange, yrange, zrange, indexing='ij')
151
+ self.overrides.append(((el_x[index], el_y[index], el_z[index]), value))
152
+
153
+ def get_padded_vector(self, x):
154
+ xpad = x[self.el3d_pad]
155
+ for index, value in self.overrides:
156
+ xpad[index] = value
157
+ return xpad
158
+
159
+ def set_filter_radius(self, radius: float, relative_units: bool = True):
15
160
  if relative_units:
16
161
  dx, dy, dz = 1.0, 1.0, 1.0
17
162
  else:
18
163
  dx, dy, dz = self.domain.element_size
19
- delemx, delemy, delemz = int((radius-1e-10*dx)/dx), int((radius-1e-10*dy)/dy), int((radius-1e-10*dz)/dz)
20
- if self.domain.dim < 3:
21
- delemz = 0
164
+ nx, ny, nz = self.domain.nelx, self.domain.nely, self.domain.nelz
165
+ delemx = min(nx, int((radius-1e-10*dx)/dx))
166
+ delemy = min(ny, int((radius-1e-10*dy)/dy))
167
+ delemz = min(nz, int((radius-1e-10*dz)/dz))
22
168
  xrange = np.arange(-delemx, delemx+1)*dx
23
169
  yrange = np.arange(-delemy, delemy+1)*dy
24
170
  zrange = np.arange(-delemz, delemz+1)*dz
25
- coords_x, coords_y, coords_z = np.meshgrid(xrange, yrange, zrange)
171
+ coords_x, coords_y, coords_z = np.meshgrid(xrange, yrange, zrange, indexing='ij')
26
172
  self.weights = np.maximum(0.0, radius - np.sqrt(coords_x*coords_x + coords_y*coords_y + coords_z*coords_z))
27
173
  self.weights /= np.sum(self.weights) # Volume preserving
28
- if self.domain.dim < 3:
29
- self.weights = self.weights[:, :, 0]
30
174
 
31
175
  def _response(self, x):
32
- domain_sizes = [self.domain.nelx, self.domain.nely]
33
- if self.domain.dim >= 3:
34
- domain_sizes.append(self.domain.nely)
35
- xbox = x.reshape(*domain_sizes, order='F').T # TODO 3d?
36
- return convolve(xbox, self.weights, mode=self.mode).flatten()
176
+ xpad = self.get_padded_vector(x)
177
+ y3d = convolve(xpad, self.weights, mode='valid')
178
+ y = np.zeros_like(x)
179
+ np.add.at(y, self.el3d_orig, y3d)
180
+ return y
37
181
 
38
182
  def _sensitivity(self, dfdv):
39
- domain_sizes = [self.domain.nelx, self.domain.nely]
40
- if self.domain.dim == 3:
41
- domain_sizes.append(self.domain.nely)
42
- ybox = dfdv.reshape(*domain_sizes, order='F').T # TODO 3d
43
- return convolve(ybox, self.weights, mode=self.mode).flatten()
183
+ dx3d = correlate(dfdv[self.el3d_orig], self.weights, mode='full')
184
+ for index, _ in self.overrides:
185
+ dx3d[index] = 0
186
+ dx = np.zeros_like(self.sig_in[0].state)
187
+ np.add.at(dx, self.el3d_pad, dx3d)
188
+ return dx
44
189
 
45
190
 
46
191
  class Filter(Module):
@@ -167,6 +312,8 @@ class DensityFilter(Filter):
167
312
  nwindy = yupp - ylow + 1
168
313
  nwindz = zupp - zlow + 1
169
314
  nwind = nwindx * nwindy * nwindz
315
+ if np.sum(nwind) < 0:
316
+ raise OverflowError("Filter size too large for this mesh size")
170
317
 
171
318
  # Total number of window elements
172
319
  ncum = np.cumsum(nwind)
pymoto/modules/generic.py CHANGED
@@ -105,10 +105,21 @@ class MathGeneral(Module):
105
105
  if np.isrealobj(dg_dx[i]) and np.iscomplexobj(dg_dx_add):
106
106
  dg_dx_add = np.real(dg_dx_add)
107
107
 
108
- # Add the contribution
108
+ # Add the contribution according to broadcasting rules of NumPy
109
+ # https://numpy.org/doc/stable/user/basics.broadcasting.html
109
110
  if (not hasattr(dg_dx[i], '__len__')) or (hasattr(dg_dx[i], 'ndim') and dg_dx[i].ndim == 0):
110
111
  # Scalar type or 0-dimensional array
111
112
  dg_dx[i] += np.sum(dg_dx_add)
113
+ elif dg_dx[i].shape != dg_dx_add.shape:
114
+ # Reverse broadcast https://stackoverflow.com/questions/76002989/numpy-is-there-a-reverse-broadcast
115
+ n_leading_dims = dg_dx_add.ndim - dg_dx[i].ndim
116
+ broadcasted_dims = tuple(range(n_leading_dims))
117
+ for ii in range(dg_dx_add.ndim - n_leading_dims):
118
+ if dg_dx[i].shape[ii] == 1 and dg_dx_add.shape[ii+n_leading_dims] != 1:
119
+ broadcasted_dims = (*broadcasted_dims, n_leading_dims+ii)
120
+
121
+ dg_dx_add1 = np.add.reduce(dg_dx_add, axis=broadcasted_dims, keepdims=True) # Sum broadcasted axis
122
+ dg_dx[i] += np.squeeze(dg_dx_add1, axis=tuple(range(n_leading_dims))) # Squeeze out singleton axis
112
123
  else:
113
124
  dg_dx[i] += dg_dx_add
114
125
 
pymoto/modules/io.py CHANGED
@@ -14,7 +14,7 @@ from pymoto import Module
14
14
  from .assembly import DomainDefinition
15
15
 
16
16
 
17
- class _FigModule(Module):
17
+ class FigModule(Module):
18
18
  """ Abstract base class for any module which produces a figure
19
19
 
20
20
  Keyword Args:
@@ -55,7 +55,7 @@ class _FigModule(Module):
55
55
  plt.close(self.fig)
56
56
 
57
57
 
58
- class PlotDomain(_FigModule):
58
+ class PlotDomain(FigModule):
59
59
  """ Plots the densities of a domain (2D or 3D)
60
60
 
61
61
  Input Signal:
@@ -96,10 +96,16 @@ class PlotDomain(_FigModule):
96
96
  self.im.set_data(data)
97
97
  else:
98
98
  ax = self.fig.add_subplot(111)
99
- self.im = ax.imshow(data, origin='lower', cmap=self.cmap)
99
+ Lx = self.domain.nelx * self.domain.unitx
100
+ Ly = self.domain.nely * self.domain.unity
101
+ self.im = ax.imshow(data, cmap=self.cmap, origin='lower', extent=(0.0, Lx, 0.0, Ly))
100
102
  self.cbar = self.fig.colorbar(self.im, orientation='horizontal')
101
103
  ax.set(xlabel='x', ylabel='y')
102
- clim = [np.min(data), np.max(data)] if self.clim is None else self.clim
104
+ vmin, vmax = np.min(data), np.max(data)
105
+ if vmin < 0:
106
+ vabs = max(abs(vmin), abs(vmax))
107
+ vmin, vmax = -vabs, vabs
108
+ clim = [vmin, vmax] if self.clim is None else self.clim
103
109
  self.im.set_clim(vmin=clim[0], vmax=clim[1])
104
110
 
105
111
  def _plot_3d(self, x):
@@ -138,7 +144,7 @@ class PlotDomain(_FigModule):
138
144
  self.fac = ax.voxels(sel, facecolors=colors, linewidth=0.5, edgecolors='k')
139
145
 
140
146
 
141
- class PlotGraph(_FigModule):
147
+ class PlotGraph(FigModule):
142
148
  """ Plot an X-Y graph
143
149
 
144
150
  Input Signals:
@@ -175,14 +181,15 @@ class PlotGraph(_FigModule):
175
181
  for i, y in enumerate(ys):
176
182
  self.line[i].set_xdata(x)
177
183
  self.line[i].set_ydata(y)
178
- ymin, ymax = min(ymin, min(y)), max(ymax, max(y))
179
- self.ax.set_xlim([min(x), max(x)])
180
- self.ax.set_ylim([ymin, ymax])
184
+ ymin, ymax = min(ymin, np.min(y)), max(ymax, np.max(y))
185
+ self.ax.set_xlim([np.min(x), np.max(x)])
186
+ dy = ymax - ymin
187
+ self.ax.set_ylim([ymin-0.05*dy, ymax+0.05*dy])
181
188
 
182
189
  self._update_fig()
183
190
 
184
191
 
185
- class PlotIter(_FigModule):
192
+ class PlotIter(FigModule):
186
193
  """ Plot iteration history of one or more variables
187
194
 
188
195
  Input Signals:
@@ -193,10 +200,12 @@ class PlotIter(_FigModule):
193
200
  overwrite (bool): Overwrite saved image every time the figure is updated, else prefix ``_0000`` is added to the
194
201
  filename (default = ``False``)
195
202
  show (bool): Show the figure on the screen
203
+ ylim: Provide y-axis limits for the plot
196
204
  """
197
- def _prepare(self):
205
+ def _prepare(self, ylim=None):
198
206
  self.minlim = 1e+200
199
207
  self.maxlim = -1e+200
208
+ self.ylim = ylim
200
209
 
201
210
  def _response(self, *args):
202
211
  if not hasattr(self, 'ax'):
@@ -227,7 +236,9 @@ class PlotIter(_FigModule):
227
236
  dy = max((self.maxlim - self.minlim)*0.05, sys.float_info.min)
228
237
 
229
238
  self.ax.set_xlim([-0.5, self.iter+0.5])
230
- if np.isfinite(self.minlim) and np.isfinite(self.maxlim):
239
+ if self.ylim is not None:
240
+ self.ax.set_ylim(self.ylim)
241
+ elif np.isfinite(self.minlim) and np.isfinite(self.maxlim):
231
242
  self.ax.set_ylim([self.minlim - dy, self.maxlim + dy])
232
243
 
233
244
  self._update_fig()