pyMOTO 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -305,3 +318,230 @@ class AssemblePoisson(AssembleGeneral):
305
318
  self.poisson_element += w * self.material_property * Bn.T @ Bn # Add contribution
306
319
 
307
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
+ This module is the reverse of :py:class:`pymoto.NodalOperation`.
329
+
330
+ Input Signal:
331
+ - ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
332
+
333
+ Output Signal:
334
+ - ``y``: Elemental output data of size ``(..., #elements)`` or ``(#dofs, ..., #elements)``
335
+
336
+ Args:
337
+ domain: The domain defining element and nodal connectivity
338
+ element_matrix: The element operator matrix :math:`\mathbf{B}` of size ``(..., #dofs_per_element)`` or ``(..., #nodes_per_element)``
339
+ """
340
+ def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray):
341
+ if element_matrix.shape[-1] % domain.elemnodes != 0:
342
+ raise IndexError(
343
+ f"Size of last dimension of element operator matrix ({element_matrix.shape[-1]}) is not compatible "
344
+ f"with mesh. Must be dividable by the number of nodes per element ({domain.elemnodes})."
345
+ )
346
+ self.domain = domain
347
+ self.element_matrix = element_matrix
348
+ self.dofconn = None
349
+
350
+ def _response(self, u):
351
+ if u.size % self.domain.nnodes != 0:
352
+ raise IndexError(f"Size of input vector ({u.size}) does not match number of nodes ({self.domain.nnodes})")
353
+ ndof = u.size // self.domain.nnodes
354
+
355
+ if self.element_matrix.shape[-1] != self.domain.elemnodes * ndof:
356
+ # Initialize only after first call to response(), because the number of dofs may not yet be known
357
+ em = self.element_matrix.copy()
358
+ assert em.shape[-1] == self.domain.elemnodes, f"Size of element matrix must match #dofs_per_element ({ndof*self.domain.elemnodes}) or #nodes_per_element ({self.domain.elemnodes})."
359
+
360
+ # Element matrix is repeated for each dof
361
+ self.element_matrix = np.zeros((ndof, *self.element_matrix.shape[:-1], ndof * self.domain.elemnodes))
362
+ for i in range(ndof):
363
+ self.element_matrix[i, ..., i::ndof] = em
364
+
365
+ if self.dofconn is None:
366
+ self.dofconn = self.domain.get_dofconnectivity(ndof)
367
+
368
+ assert self.element_matrix.shape[-1] == ndof * self.domain.elemnodes
369
+ return einsum('...k, lk -> ...l', self.element_matrix, u[self.dofconn], optimize=True)
370
+
371
+ def _sensitivity(self, dy):
372
+ du_el = einsum('...k, ...l -> lk', self.element_matrix, dy, optimize=True)
373
+ du = np.zeros_like(self.sig_in[0].state)
374
+ np.add.at(du, self.dofconn, du_el)
375
+ return du
376
+
377
+
378
+ class Strain(ElementOperation):
379
+ r""" Evaluate average mechanical strains in solid elements based on deformation
380
+
381
+ The strains are returned in Voigt notation.
382
+ :math:`\mathbf{\epsilon}_e = \mathbf{B} \mathbf{u}_e`
383
+
384
+ Each integration point in the element has different strain values. Here, the average is returned.
385
+
386
+ The returned strain is either
387
+ :math:`\mathbf{\epsilon} = \begin{bmatrix}\epsilon_{xx} & \epsilon_{yy} & \epsilon_{xy} \end{bmatrix}`
388
+ in case ``voigt = False`` or
389
+ :math:`\mathbf{\epsilon} = \begin{bmatrix}\epsilon_{xx} & \epsilon_{yy} & \gamma_{xy} \end{bmatrix}`
390
+ in case ``voigt = True``, for which :math:`\gamma_{xy}=2\epsilon_{xy}`.
391
+
392
+ Input Signal:
393
+ - ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
394
+
395
+ Output Signal:
396
+ - ``e``: Strain matrix of size ``(#strains_per_element, #elements)``
397
+
398
+ Args:
399
+ domain: The domain defining element and nodal connectivity
400
+
401
+ Keyword Args:
402
+ voigt: Use Voigt strain notation (2x off-diagonal strain contribution)
403
+ """
404
+ def _prepare(self, domain: DomainDefinition, voigt: bool = True):
405
+ # Numerical integration
406
+ B = None
407
+ siz = domain.element_size
408
+ w = 1/domain.elemnodes # Average strain at the integration points
409
+ for n in domain.node_numbering:
410
+ pos = n * (siz / 2) / np.sqrt(3) # Sampling point
411
+ dN_dx = domain.eval_shape_fun_der(pos)
412
+ B_add = w*get_B(dN_dx) # Add contribution
413
+ if B is None:
414
+ B = B_add
415
+ else:
416
+ B += B_add
417
+
418
+ if voigt:
419
+ idx_shear = np.count_nonzero(B, axis=1) == 2*domain.elemnodes # Shear is combination of two displacements
420
+ B[idx_shear, :] *= 2 # Use engineering strain
421
+
422
+ super()._prepare(domain, B)
423
+
424
+
425
+ class Stress(Strain):
426
+ """ Calculate the average stresses per element
427
+
428
+ Input Signal:
429
+ - ``u``: Nodal vector of size ``(#dofs_per_node * #nodes)``
430
+
431
+ Output Signal:
432
+ - ``s``: Stress matrix of size ``(#stresses_per_element, #elements)``
433
+
434
+ Args:
435
+ domain: The domain defining element and nodal connectivity
436
+
437
+ Keyword Args:
438
+ e_modulus: Young's modulus
439
+ poisson_ratio: Poisson ratio
440
+ plane: Plane 'strain' or 'stress'
441
+ """
442
+ def _prepare(self, domain: DomainDefinition,
443
+ e_modulus: float = 1.0, poisson_ratio: float = 0.3, plane: str = 'strain'):
444
+
445
+ super()._prepare(domain, voigt=True)
446
+
447
+ # Get material relation
448
+ D = get_D(e_modulus, poisson_ratio, '3d' if domain.dim == 3 else plane.lower())
449
+ if domain.dim == 2:
450
+ D *= domain.element_size[2]
451
+ self.element_matrix = D @ self.element_matrix
452
+
453
+
454
+ class ElementAverage(ElementOperation):
455
+ r""" Determine average value in element of input nodal values
456
+
457
+ Input Signal:
458
+ - ``v``: Nodal vector of size ``(#dofs_per_node * #nodes)``
459
+
460
+ Output Signal:
461
+ - ``v_el``: Elemental vector of size ``(#elements)`` or ``(#dofs, #elements)`` if ``#dofs_per_node>1``
462
+
463
+ Args:
464
+ domain: The domain defining element and nodal connectivity
465
+ """
466
+ def _prepare(self, domain: DomainDefinition):
467
+ shapefuns = domain.eval_shape_fun(pos=np.array([0, 0, 0]))
468
+ super()._prepare(domain, shapefuns)
469
+
470
+
471
+ class NodalOperation(Module):
472
+ r""" Generic module for nodal operations based on elemental information
473
+
474
+ :math:`u_e = \mathbf{A} x_e`
475
+
476
+ This module is the reverse of :py:class:`pymoto.ElementOperation`.
477
+
478
+ Input Signal:
479
+ - ``x``: Elemental vector of size ``(#elements)``
480
+
481
+ Output Signal:
482
+ - ``u``: nodal output data of size ``(..., #dofs_per_node * #nodes)``
483
+
484
+ Args:
485
+ domain: The domain defining element and nodal connectivity
486
+ element_matrix: The element operator matrix :math:`\mathbf{A}` of size ``(..., #dofs_per_element)``
487
+ """
488
+ def _prepare(self, domain: DomainDefinition, element_matrix: np.ndarray):
489
+ if element_matrix.shape[-1] % domain.elemnodes != 0:
490
+ raise IndexError("Size of last dimension of element operator matrix is not compatible with mesh. "
491
+ "Must be dividable by the number of nodes.")
492
+
493
+ ndof = element_matrix.shape[-1] // domain.elemnodes
494
+
495
+ self.element_matrix = element_matrix
496
+ self.dofconn = domain.get_dofconnectivity(ndof)
497
+ self.ndofs = ndof*domain.nnodes
498
+
499
+ def _response(self, x):
500
+ dofs_el = einsum('...k, ...l -> lk', self.element_matrix, x, optimize=True)
501
+ dofs = np.zeros(self.ndofs)
502
+ np.add.at(dofs, self.dofconn, dofs_el)
503
+ return dofs
504
+
505
+ def _sensitivity(self, dx):
506
+ return einsum('...k, lk -> ...l', self.element_matrix, dx[self.dofconn], optimize=True)
507
+
508
+
509
+ class ThermoMechanical(NodalOperation):
510
+ r""" Determine equivalent thermo-mechanical load from design vector and elemental temperature difference
511
+
512
+ :math:`f_thermal = \mathbf{A} (x*t_delta)_e`
513
+
514
+ Input Signal:
515
+ - ``x*t_delta``: Elemental vector of size ``(#elements)`` containing elemental densities multiplied by
516
+ elemental temperature difference
517
+
518
+ Output Signal:
519
+ - ``f_thermal``: nodal equivalent thermo-mechanical load of size ``(#dofs_per_node * #nodes)``
520
+
521
+ Args:
522
+ domain: The domain defining element and nodal connectivity
523
+ e_modulus (optional): Young's modulus
524
+ poisson_ratio (optional): Poisson ratio
525
+ alpha (optional): Coefficient of thermal expansion
526
+ plane (optional): Plane 'strain' or 'stress'
527
+ """
528
+ def _prepare(self, domain: DomainDefinition, e_modulus: float = 1.0, poisson_ratio: float = 0.3, alpha: float = 1e-6, plane: str = 'strain'):
529
+ dim = domain.dim
530
+ D = get_D(e_modulus, poisson_ratio, '3d' if dim == 3 else plane.lower())
531
+ if dim == 2:
532
+ Phi = np.array([1, 1, 0])
533
+ D *= domain.element_size[2]
534
+ elif dim == 3:
535
+ Phi = np.array([1, 1, 1, 0, 0, 0])
536
+
537
+ # Numerical integration
538
+ BDPhi = np.zeros(domain.elemnodes * dim)
539
+ siz = domain.element_size
540
+ w = np.prod(siz[:domain.dim] / 2)
541
+ for n in domain.node_numbering:
542
+ pos = n * (siz / 2) / np.sqrt(3) # Sampling point
543
+ dN_dx = domain.eval_shape_fun_der(pos)
544
+ B = get_B(dN_dx)
545
+ BDPhi += w * B.T @ D @ Phi # Add contribution
546
+
547
+ super()._prepare(domain, alpha*BDPhi)
pymoto/modules/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