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.
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/METADATA +7 -8
- pyMOTO-1.5.0.dist-info/RECORD +29 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +17 -11
- pymoto/common/domain.py +61 -5
- pymoto/common/dyadcarrier.py +87 -29
- pymoto/common/mma.py +142 -129
- pymoto/core_objects.py +129 -117
- pymoto/modules/aggregation.py +209 -0
- pymoto/modules/assembly.py +250 -10
- pymoto/modules/complex.py +3 -3
- pymoto/modules/filter.py +171 -24
- pymoto/modules/generic.py +12 -1
- pymoto/modules/io.py +85 -12
- pymoto/modules/linalg.py +92 -120
- pymoto/modules/scaling.py +5 -4
- pymoto/routines.py +34 -9
- pymoto/solvers/__init__.py +14 -0
- pymoto/solvers/auto_determine.py +108 -0
- pymoto/{common/solvers_dense.py → solvers/dense.py} +90 -70
- pymoto/solvers/iterative.py +361 -0
- pymoto/solvers/matrix_checks.py +60 -0
- pymoto/solvers/solvers.py +253 -0
- pymoto/{common/solvers_sparse.py → solvers/sparse.py} +42 -29
- pyMOTO-1.3.0.dist-info/RECORD +0 -24
- pymoto/common/solvers.py +0 -236
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/LICENSE +0 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/zip-safe +0 -0
pymoto/modules/assembly.py
CHANGED
@@ -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
|
53
|
-
self.cols = np.kron(self.dofconn, np.ones((
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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 =
|
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 =
|
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.
|
4
|
+
from scipy.signal import convolve, correlate
|
5
|
+
from numbers import Number
|
6
6
|
|
7
7
|
|
8
8
|
class FilterConv(Module):
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
return
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
|