pyMOTO 1.3.0__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
@@ -305,3 +318,116 @@ 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
+ 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())
431
+ if domain.dim == 2:
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()