emerge 0.4.7__py3-none-any.whl → 0.4.9__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.
Potentially problematic release.
This version of emerge might be problematic. Click here for more details.
- emerge/__init__.py +14 -14
- emerge/_emerge/__init__.py +42 -0
- emerge/_emerge/bc.py +197 -0
- emerge/_emerge/coord.py +119 -0
- emerge/_emerge/cs.py +523 -0
- emerge/_emerge/dataset.py +36 -0
- emerge/_emerge/elements/__init__.py +19 -0
- emerge/_emerge/elements/femdata.py +212 -0
- emerge/_emerge/elements/index_interp.py +64 -0
- emerge/_emerge/elements/legrange2.py +172 -0
- emerge/_emerge/elements/ned2_interp.py +645 -0
- emerge/_emerge/elements/nedelec2.py +140 -0
- emerge/_emerge/elements/nedleg2.py +217 -0
- emerge/_emerge/geo/__init__.py +24 -0
- emerge/_emerge/geo/horn.py +107 -0
- emerge/_emerge/geo/modeler.py +449 -0
- emerge/_emerge/geo/operations.py +254 -0
- emerge/_emerge/geo/pcb.py +1244 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
- emerge/_emerge/geo/pcb_tools/macro.py +79 -0
- emerge/_emerge/geo/pmlbox.py +204 -0
- emerge/_emerge/geo/polybased.py +529 -0
- emerge/_emerge/geo/shapes.py +427 -0
- emerge/_emerge/geo/step.py +77 -0
- emerge/_emerge/geo2d.py +86 -0
- emerge/_emerge/geometry.py +510 -0
- emerge/_emerge/howto.py +214 -0
- emerge/_emerge/logsettings.py +5 -0
- emerge/_emerge/material.py +118 -0
- emerge/_emerge/mesh3d.py +730 -0
- emerge/_emerge/mesher.py +339 -0
- emerge/_emerge/mth/common_functions.py +33 -0
- emerge/_emerge/mth/integrals.py +71 -0
- emerge/_emerge/mth/optimized.py +357 -0
- emerge/_emerge/periodic.py +263 -0
- emerge/_emerge/physics/__init__.py +0 -0
- emerge/_emerge/physics/microwave/__init__.py +1 -0
- emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
- emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
- emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
- emerge/_emerge/physics/microwave/periodic.py +82 -0
- emerge/_emerge/physics/microwave/port_functions.py +53 -0
- emerge/_emerge/physics/microwave/sc.py +175 -0
- emerge/_emerge/physics/microwave/simjob.py +147 -0
- emerge/_emerge/physics/microwave/sparam.py +138 -0
- emerge/_emerge/physics/microwave/touchstone.py +140 -0
- emerge/_emerge/plot/__init__.py +0 -0
- emerge/_emerge/plot/display.py +394 -0
- emerge/_emerge/plot/grapher.py +93 -0
- emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
- emerge/_emerge/plot/pyvista/__init__.py +1 -0
- emerge/_emerge/plot/pyvista/display.py +931 -0
- emerge/_emerge/plot/pyvista/display_settings.py +24 -0
- emerge/_emerge/plot/simple_plots.py +551 -0
- emerge/_emerge/plot.py +225 -0
- emerge/_emerge/projects/__init__.py +0 -0
- emerge/_emerge/projects/_gen_base.txt +32 -0
- emerge/_emerge/projects/_load_base.txt +24 -0
- emerge/_emerge/projects/generate_project.py +40 -0
- emerge/_emerge/selection.py +596 -0
- emerge/_emerge/simmodel.py +444 -0
- emerge/_emerge/simulation_data.py +411 -0
- emerge/_emerge/solver.py +993 -0
- emerge/_emerge/system.py +54 -0
- emerge/cli.py +19 -0
- emerge/lib.py +1 -1
- emerge/plot.py +1 -1
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/METADATA +7 -6
- emerge-0.4.9.dist-info/RECORD +78 -0
- emerge-0.4.9.dist-info/entry_points.txt +2 -0
- emerge-0.4.7.dist-info/RECORD +0 -9
- emerge-0.4.7.dist-info/entry_points.txt +0 -2
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
# EMerge is an open source Python based FEM EM simulation module.
|
|
2
|
+
# Copyright (C) 2025 Robert Fennis.
|
|
3
|
+
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see
|
|
16
|
+
# <https://www.gnu.org/licenses/>.
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import time
|
|
19
|
+
from ...mesh3d import Mesh3D
|
|
20
|
+
from ...geometry import GeoObject
|
|
21
|
+
from ...selection import FaceSelection, DomainSelection, EdgeSelection, Selection, encode_data
|
|
22
|
+
from ...physics.microwave.microwave_bc import PortBC, ModalPort
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pyvista as pv
|
|
25
|
+
from typing import Iterable, Literal, Callable
|
|
26
|
+
from ..display import BaseDisplay
|
|
27
|
+
from .display_settings import PVDisplaySettings
|
|
28
|
+
from matplotlib.colors import ListedColormap
|
|
29
|
+
|
|
30
|
+
### Color scale
|
|
31
|
+
|
|
32
|
+
# Define the colors we want to use
|
|
33
|
+
col1 = np.array([57, 179, 227, 255])/255
|
|
34
|
+
col2 = np.array([22, 36, 125, 255])/255
|
|
35
|
+
col3 = np.array([33, 33, 33, 255])/255
|
|
36
|
+
col4 = np.array([173, 76, 7, 255])/255
|
|
37
|
+
col5 = np.array([250, 75, 148, 255])/255
|
|
38
|
+
|
|
39
|
+
cmap_names = Literal['bgy','bgyw','kbc','blues','bmw','bmy','kgy','gray','dimgray','fire','kb','kg','kr',
|
|
40
|
+
'bkr','bky','coolwarm','gwv','bjy','bwy','cwr','colorwheel','isolum','rainbow','fire',
|
|
41
|
+
'cet_fire','gouldian','kbgyw','cwr','CET_CBL1','CET_CBL3','CET_D1A']
|
|
42
|
+
|
|
43
|
+
def gen_cmap(mesh, N: int = 256):
|
|
44
|
+
# build a linear grid of data‐values (not strictly needed for pure colormap)
|
|
45
|
+
vmin, vmax = mesh['values'].min(), mesh['values'].max()
|
|
46
|
+
mapping = np.linspace(vmin, vmax, N)
|
|
47
|
+
|
|
48
|
+
# prepare output
|
|
49
|
+
newcolors = np.empty((N, 4))
|
|
50
|
+
|
|
51
|
+
# normalized positions of control points: start, middle, end
|
|
52
|
+
control_pos = np.array([0.0, 0.25, 0.5, 0.75, 1]) * (vmax - vmin) + vmin
|
|
53
|
+
# stack control colors
|
|
54
|
+
controls = np.vstack([col1, col2, col3, col4, col5])
|
|
55
|
+
|
|
56
|
+
# interp each RGBA channel independently
|
|
57
|
+
for chan in range(4):
|
|
58
|
+
newcolors[:, chan] = np.interp(mapping, control_pos, controls[:, chan])
|
|
59
|
+
|
|
60
|
+
return ListedColormap(newcolors)
|
|
61
|
+
|
|
62
|
+
def setdefault(options: dict, **kwargs) -> dict:
|
|
63
|
+
"""Shorthand for overwriting non-existent keyword arguments with defaults
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
options (dict): The kwargs dict
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
dict: the kwargs dict
|
|
70
|
+
"""
|
|
71
|
+
for key in kwargs.keys():
|
|
72
|
+
if options.get(key,None) is None:
|
|
73
|
+
options[key] = kwargs[key]
|
|
74
|
+
return options
|
|
75
|
+
|
|
76
|
+
def _logscale(dx, dy, dz):
|
|
77
|
+
"""
|
|
78
|
+
Logarithmically scales vector magnitudes so that the largest remains unchanged
|
|
79
|
+
and others are scaled down logarithmically.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
dx, dy, dz (np.ndarray): Components of vectors.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]: Scaled dx, dy, dz arrays.
|
|
86
|
+
"""
|
|
87
|
+
dx = np.asarray(dx)
|
|
88
|
+
dy = np.asarray(dy)
|
|
89
|
+
dz = np.asarray(dz)
|
|
90
|
+
|
|
91
|
+
# Compute original magnitudes
|
|
92
|
+
mags = np.sqrt(dx**2 + dy**2 + dz**2)
|
|
93
|
+
mags_nonzero = np.where(mags == 0, 1e-10, mags) # avoid log(0)
|
|
94
|
+
|
|
95
|
+
# Logarithmic scaling (scaled to max = original max)
|
|
96
|
+
log_mags = np.log10(mags_nonzero)
|
|
97
|
+
log_min = np.min(log_mags)
|
|
98
|
+
log_max = np.max(log_mags)
|
|
99
|
+
|
|
100
|
+
if log_max == log_min:
|
|
101
|
+
# All vectors have the same length
|
|
102
|
+
return dx, dy, dz
|
|
103
|
+
|
|
104
|
+
# Normalize log magnitudes to [0, 1]
|
|
105
|
+
log_scaled = (log_mags - log_min) / (log_max - log_min)
|
|
106
|
+
|
|
107
|
+
# Scale back to original max magnitude
|
|
108
|
+
max_mag = np.max(mags)
|
|
109
|
+
new_mags = log_scaled * max_mag
|
|
110
|
+
|
|
111
|
+
# Compute unit vectors
|
|
112
|
+
unit_dx = dx / mags_nonzero
|
|
113
|
+
unit_dy = dy / mags_nonzero
|
|
114
|
+
unit_dz = dz / mags_nonzero
|
|
115
|
+
|
|
116
|
+
# Apply scaled magnitudes
|
|
117
|
+
scaled_dx = unit_dx * new_mags
|
|
118
|
+
scaled_dy = unit_dy * new_mags
|
|
119
|
+
scaled_dz = unit_dz * new_mags
|
|
120
|
+
|
|
121
|
+
return scaled_dx, scaled_dy, scaled_dz
|
|
122
|
+
|
|
123
|
+
def _min_distance(xs, ys, zs):
|
|
124
|
+
"""
|
|
125
|
+
Compute the minimum Euclidean distance between any two points
|
|
126
|
+
defined by the 1D arrays xs, ys, zs.
|
|
127
|
+
|
|
128
|
+
Parameters:
|
|
129
|
+
xs (np.ndarray): x-coordinates of the points
|
|
130
|
+
ys (np.ndarray): y-coordinates of the points
|
|
131
|
+
zs (np.ndarray): z-coordinates of the points
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
float: The minimum Euclidean distance between any two points
|
|
135
|
+
"""
|
|
136
|
+
# Stack the coordinates into a (N, 3) array
|
|
137
|
+
points = np.stack((xs, ys, zs), axis=-1)
|
|
138
|
+
|
|
139
|
+
# Compute pairwise squared distances using broadcasting
|
|
140
|
+
diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]
|
|
141
|
+
dists_squared = np.sum(diff ** 2, axis=-1)
|
|
142
|
+
|
|
143
|
+
# Set diagonal to infinity to ignore zero distances to self
|
|
144
|
+
np.fill_diagonal(dists_squared, np.inf)
|
|
145
|
+
|
|
146
|
+
# Get the minimum distance
|
|
147
|
+
min_dist = np.sqrt(np.min(dists_squared))
|
|
148
|
+
return min_dist
|
|
149
|
+
|
|
150
|
+
def _norm(x, y, z):
|
|
151
|
+
return np.sqrt(np.abs(x)**2 + np.abs(y)**2 + np.abs(z)**2)
|
|
152
|
+
|
|
153
|
+
def _select(obj: GeoObject | Selection) -> Selection:
|
|
154
|
+
if isinstance(obj, GeoObject):
|
|
155
|
+
return obj.select
|
|
156
|
+
return obj
|
|
157
|
+
|
|
158
|
+
def _merge(lst: list[GeoObject | Selection]) -> Selection:
|
|
159
|
+
selections = [_select(item) for item in lst]
|
|
160
|
+
dim = selections[0].dim
|
|
161
|
+
all_tags = []
|
|
162
|
+
for item in lst:
|
|
163
|
+
all_tags.extend(_select(item).tags)
|
|
164
|
+
|
|
165
|
+
if dim==1:
|
|
166
|
+
return EdgeSelection(all_tags)
|
|
167
|
+
elif dim==2:
|
|
168
|
+
return FaceSelection(all_tags)
|
|
169
|
+
elif dim==3:
|
|
170
|
+
return DomainSelection(all_tags)
|
|
171
|
+
|
|
172
|
+
class _AnimObject:
|
|
173
|
+
""" A private class containing the required information for plot items in a view
|
|
174
|
+
that can be animated.
|
|
175
|
+
"""
|
|
176
|
+
def __init__(self,
|
|
177
|
+
field: np.ndarray,
|
|
178
|
+
T: Callable,
|
|
179
|
+
grid: pv.Grid,
|
|
180
|
+
actor: pv.Actor,
|
|
181
|
+
on_update: Callable):
|
|
182
|
+
self.field: np.ndarray = field
|
|
183
|
+
self.T: Callable = T
|
|
184
|
+
self.grid: pv.Grid = grid
|
|
185
|
+
self.actor: pv.Actor = actor
|
|
186
|
+
self.on_update: Callable = on_update
|
|
187
|
+
|
|
188
|
+
def update(self, phi: complex):
|
|
189
|
+
self.on_update(self, phi)
|
|
190
|
+
|
|
191
|
+
class PVDisplay(BaseDisplay):
|
|
192
|
+
|
|
193
|
+
def __init__(self, mesh: Mesh3D):
|
|
194
|
+
self._mesh: Mesh3D = mesh
|
|
195
|
+
self.set: PVDisplaySettings = PVDisplaySettings()
|
|
196
|
+
|
|
197
|
+
# Animation options
|
|
198
|
+
self._facetags: list[int] = []
|
|
199
|
+
self._stop: bool = False
|
|
200
|
+
self._objs: list[_AnimObject] = []
|
|
201
|
+
self._do_animate: bool = False
|
|
202
|
+
self._Nsteps: int = None
|
|
203
|
+
self._fps: int = 25
|
|
204
|
+
self._ruler: ScreenRuler = ScreenRuler(self, 0.001)
|
|
205
|
+
self._selector: ScreenSelector = ScreenSelector(self)
|
|
206
|
+
self._stop = False
|
|
207
|
+
self._objs = []
|
|
208
|
+
|
|
209
|
+
self._plot = pv.Plotter()
|
|
210
|
+
|
|
211
|
+
self._plot.add_key_event("m", self.activate_ruler)
|
|
212
|
+
self._plot.add_key_event("f", self.activate_object)
|
|
213
|
+
|
|
214
|
+
self._ctr: int = 0
|
|
215
|
+
def activate_ruler(self):
|
|
216
|
+
self._plot.disable_picking()
|
|
217
|
+
self._selector.turn_off()
|
|
218
|
+
self._ruler.toggle()
|
|
219
|
+
|
|
220
|
+
def activate_object(self):
|
|
221
|
+
self._plot.disable_picking()
|
|
222
|
+
self._ruler.turn_off()
|
|
223
|
+
self._selector.toggle()
|
|
224
|
+
|
|
225
|
+
def show(self):
|
|
226
|
+
""" Shows the Pyvista display. """
|
|
227
|
+
self._ruler.min_length = max(1e-3, min(self._mesh.edge_lengths))
|
|
228
|
+
self._add_aux_items()
|
|
229
|
+
if self._do_animate:
|
|
230
|
+
self._plot.show(auto_close=False, interactive_update=True, before_close_callback=self._close_callback)
|
|
231
|
+
self._animate()
|
|
232
|
+
else:
|
|
233
|
+
self._plot.show()
|
|
234
|
+
self._reset()
|
|
235
|
+
|
|
236
|
+
def set_mesh(self, mesh: Mesh3D):
|
|
237
|
+
"""Define the mesh to be used
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
mesh (Mesh3D): The mesh object
|
|
241
|
+
"""
|
|
242
|
+
self._mesh = mesh
|
|
243
|
+
|
|
244
|
+
def _reset(self):
|
|
245
|
+
""" Resets key display parameters."""
|
|
246
|
+
self._plot.close()
|
|
247
|
+
self._plot = pv.Plotter()
|
|
248
|
+
self._stop = False
|
|
249
|
+
self._objs = []
|
|
250
|
+
|
|
251
|
+
def _close_callback(self):
|
|
252
|
+
"""The private callback function that stops the animation.
|
|
253
|
+
"""
|
|
254
|
+
self._stop = True
|
|
255
|
+
|
|
256
|
+
def _animate(self) -> None:
|
|
257
|
+
"""Private function that starts the animation loop.
|
|
258
|
+
"""
|
|
259
|
+
self._plot.update()
|
|
260
|
+
while not self._stop:
|
|
261
|
+
for step in range(self._Nsteps):
|
|
262
|
+
if self._stop:
|
|
263
|
+
break
|
|
264
|
+
for aobj in self._objs:
|
|
265
|
+
phi = np.exp(1j*(step/self._Nsteps)*2*np.pi)
|
|
266
|
+
aobj.update(phi)
|
|
267
|
+
self._plot.update()
|
|
268
|
+
time.sleep(1/self._fps)
|
|
269
|
+
|
|
270
|
+
def animate(self, Nsteps: int = 35, fps: int = 25) -> PVDisplay:
|
|
271
|
+
""" Turns on the animation mode with the specified number of steps and FPS.
|
|
272
|
+
|
|
273
|
+
All subsequent plot calls will automatically be animated. This method can be
|
|
274
|
+
method chained.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
Nsteps (int, optional): The number of frames in the loop. Defaults to 35.
|
|
278
|
+
fps (int, optional): The number of frames per seocond, Defaults to 25
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
PVDisplay: The same PVDisplay object
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
>>> display.animate().surf(...)
|
|
285
|
+
>>> display.show()
|
|
286
|
+
"""
|
|
287
|
+
self._Nsteps = Nsteps
|
|
288
|
+
self._fps = fps
|
|
289
|
+
self._do_animate = True
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
## CUSTOM METHODS
|
|
293
|
+
def mesh_volume(self, volume: DomainSelection) -> pv.UnstructuredGrid:
|
|
294
|
+
tets = self._mesh.get_tetrahedra(volume.tags)
|
|
295
|
+
ntets = tets.shape[0]
|
|
296
|
+
cells = np.zeros((ntets,5), dtype=np.int64)
|
|
297
|
+
cells[:,1:] = self._mesh.tets[:,tets].T
|
|
298
|
+
cells[:,0] = 4
|
|
299
|
+
celltypes = np.full(ntets, fill_value=pv.CellType.TETRA, dtype=np.uint8)
|
|
300
|
+
points = self._mesh.nodes.T
|
|
301
|
+
return pv.UnstructuredGrid(cells, celltypes, points)
|
|
302
|
+
|
|
303
|
+
def mesh_surface(self, surface: FaceSelection) -> pv.UnstructuredGrid:
|
|
304
|
+
tris = self._mesh.get_triangles(surface.tags)
|
|
305
|
+
ntris = tris.shape[0]
|
|
306
|
+
cells = np.zeros((ntris,4), dtype=np.int64)
|
|
307
|
+
cells[:,1:] = self._mesh.tris[:,tris].T
|
|
308
|
+
cells[:,0] = 3
|
|
309
|
+
celltypes = np.full(ntris, fill_value=pv.CellType.TRIANGLE, dtype=np.uint8)
|
|
310
|
+
points = self._mesh.nodes.T
|
|
311
|
+
return pv.UnstructuredGrid(cells, celltypes, points)
|
|
312
|
+
|
|
313
|
+
def mesh(self, obj: GeoObject | Selection | Iterable) -> pv.UnstructuredGrid:
|
|
314
|
+
if isinstance(obj, Iterable):
|
|
315
|
+
obj = _merge(obj)
|
|
316
|
+
else:
|
|
317
|
+
obj = _select(obj)
|
|
318
|
+
|
|
319
|
+
if isinstance(obj, DomainSelection):
|
|
320
|
+
return self.mesh_volume(obj)
|
|
321
|
+
elif isinstance(obj, FaceSelection):
|
|
322
|
+
return self.mesh_surface(obj)
|
|
323
|
+
|
|
324
|
+
## OBLIGATORY METHODS
|
|
325
|
+
def add_object(self, obj: GeoObject | Selection | Iterable, *args, **kwargs):
|
|
326
|
+
kwargs = setdefault(kwargs, color=obj.color_rgb, opacity=obj.opacity, silhouette=True)
|
|
327
|
+
self._plot.add_mesh(self.mesh(obj), pickable=True, *args, **kwargs)
|
|
328
|
+
|
|
329
|
+
def add_scatter(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray):
|
|
330
|
+
"""Adds a scatter point cloud
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
xs (np.ndarray): The X-coordinate
|
|
334
|
+
ys (np.ndarray): The Y-coordinate
|
|
335
|
+
zs (np.ndarray): The Z-coordinate
|
|
336
|
+
"""
|
|
337
|
+
cloud = pv.PolyData(np.array([xs,ys,zs]).T)
|
|
338
|
+
self._plot.add_points(cloud)
|
|
339
|
+
|
|
340
|
+
def add_portmode(self, port: PortBC,
|
|
341
|
+
Npoints: int = 10,
|
|
342
|
+
dv=(0,0,0),
|
|
343
|
+
XYZ=None,
|
|
344
|
+
field: Literal['E','H'] = 'E',
|
|
345
|
+
k0: float = None,
|
|
346
|
+
mode_number: int = None) -> pv.UnstructuredGrid:
|
|
347
|
+
|
|
348
|
+
if XYZ:
|
|
349
|
+
X,Y,Z = XYZ
|
|
350
|
+
else:
|
|
351
|
+
tris = self._mesh.get_triangles(port.selection.tags)
|
|
352
|
+
X = self._mesh.tri_centers[0,tris]
|
|
353
|
+
Y = self._mesh.tri_centers[1,tris]
|
|
354
|
+
Z = self._mesh.tri_centers[2,tris]
|
|
355
|
+
|
|
356
|
+
X = X+dv[0]
|
|
357
|
+
Y = Y+dv[1]
|
|
358
|
+
Z = Z+dv[2]
|
|
359
|
+
xf = X.flatten()
|
|
360
|
+
yf = Y.flatten()
|
|
361
|
+
zf = Z.flatten()
|
|
362
|
+
|
|
363
|
+
d = _min_distance(xf, yf, zf)
|
|
364
|
+
|
|
365
|
+
if port.vintline is not None:
|
|
366
|
+
xs, ys, zs = port.vintline.cpoint
|
|
367
|
+
p_line = pv.Line(
|
|
368
|
+
pointa=(xs[0], ys[0], zs[0]),
|
|
369
|
+
pointb=(xs[-1], ys[-1], zs[-1]),
|
|
370
|
+
)
|
|
371
|
+
self._plot.add_mesh(
|
|
372
|
+
p_line,
|
|
373
|
+
color='red',
|
|
374
|
+
pickable=False,
|
|
375
|
+
line_width=3.0,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if k0 is None:
|
|
379
|
+
if isinstance(port, ModalPort):
|
|
380
|
+
k0 = port.get_mode(0).k0
|
|
381
|
+
else:
|
|
382
|
+
k0 = 1
|
|
383
|
+
F = port.port_mode_3d_global(xf,yf,zf,k0, which=field)
|
|
384
|
+
|
|
385
|
+
Fx = F[0,:].reshape(X.shape).T
|
|
386
|
+
Fy = F[1,:].reshape(X.shape).T
|
|
387
|
+
Fz = F[2,:].reshape(X.shape).T
|
|
388
|
+
|
|
389
|
+
if field=='H':
|
|
390
|
+
F = np.imag(F.T)
|
|
391
|
+
Fnorm = np.sqrt(Fx.imag**2 + Fy.imag**2 + Fz.imag**2).T
|
|
392
|
+
else:
|
|
393
|
+
F = np.real(F.T)
|
|
394
|
+
Fnorm = np.sqrt(Fx.real**2 + Fy.real**2 + Fz.real**2).T
|
|
395
|
+
|
|
396
|
+
if XYZ is not None:
|
|
397
|
+
grid = pv.StructuredGrid(X,Y,Z)
|
|
398
|
+
self.add_surf(X,Y,Z,Fnorm, _fieldname = 'portfield')
|
|
399
|
+
self._plot.add_mesh(grid, scalars = Fnorm.T, opacity=0.8, pickable=False)
|
|
400
|
+
|
|
401
|
+
Emag = F/np.max(Fnorm.flatten())*d*3
|
|
402
|
+
self._plot.add_arrows(np.array([xf,yf,zf]).T, Emag)
|
|
403
|
+
|
|
404
|
+
def add_surf(self,
|
|
405
|
+
x: np.ndarray,
|
|
406
|
+
y: np.ndarray,
|
|
407
|
+
z: np.ndarray,
|
|
408
|
+
field: np.ndarray,
|
|
409
|
+
scale: Literal['lin','log','symlog'] = 'lin',
|
|
410
|
+
cmap: cmap_names = 'coolwarm',
|
|
411
|
+
clim: tuple[float, float] = None,
|
|
412
|
+
opacity: float = 1.0,
|
|
413
|
+
symmetrize: bool = True,
|
|
414
|
+
_fieldname: str = None,
|
|
415
|
+
**kwargs,):
|
|
416
|
+
"""Add a surface plot to the display
|
|
417
|
+
The X,Y,Z coordinates must be a 2D grid of data points. The field must be a real field with the same size.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
x (np.ndarray): The X-grid array
|
|
421
|
+
y (np.ndarray): The Y-grid array
|
|
422
|
+
z (np.ndarray): The Z-grid array
|
|
423
|
+
field (np.ndarray): The scalar field to display
|
|
424
|
+
scale (Literal["lin","log","symlog"], optional): The colormap scaling¹. Defaults to 'lin'.
|
|
425
|
+
cmap (cmap_names, optional): The colormap. Defaults to 'coolwarm'.
|
|
426
|
+
clim (tuple[float, float], optional): Specific color limits (min, max). Defaults to None.
|
|
427
|
+
opacity (float, optional): The opacity of the surface. Defaults to 1.0.
|
|
428
|
+
symmetrize (bool, optional): Wether to force a symmetrical color limit (-A,A). Defaults to True.
|
|
429
|
+
|
|
430
|
+
(¹): lin: f(x)=x, log: f(x)=log₁₀(|x|), symlog: f(x)=sgn(x)·log₁₀(1+|x·ln(10)|)
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
grid = pv.StructuredGrid(x,y,z)
|
|
434
|
+
field_flat = field.flatten(order='F')
|
|
435
|
+
|
|
436
|
+
if scale=='log':
|
|
437
|
+
T = lambda x: np.log10(np.abs(x))
|
|
438
|
+
elif scale=='symlog':
|
|
439
|
+
T = lambda x: np.sign(x) * np.log10(1 + np.abs(x*np.log(10)))
|
|
440
|
+
else:
|
|
441
|
+
T = lambda x: x
|
|
442
|
+
|
|
443
|
+
static_field = T(np.real(field_flat))
|
|
444
|
+
if _fieldname is None:
|
|
445
|
+
name = 'anim'+str(self._ctr)
|
|
446
|
+
else:
|
|
447
|
+
name = _fieldname
|
|
448
|
+
self._ctr += 1
|
|
449
|
+
grid[name] = static_field
|
|
450
|
+
|
|
451
|
+
if clim is None:
|
|
452
|
+
fmin = np.min(static_field)
|
|
453
|
+
fmax = np.max(static_field)
|
|
454
|
+
clim = (fmin, fmax)
|
|
455
|
+
|
|
456
|
+
if symmetrize:
|
|
457
|
+
lim = max(abs(clim[0]),abs(clim[1]))
|
|
458
|
+
clim = (-lim, lim)
|
|
459
|
+
|
|
460
|
+
kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
|
|
461
|
+
actor = self._plot.add_mesh(grid, scalars=name, **kwargs)
|
|
462
|
+
|
|
463
|
+
if self._animate:
|
|
464
|
+
def on_update(obj: _AnimObject, phi: complex):
|
|
465
|
+
field = obj.T(np.real(obj.field*phi))
|
|
466
|
+
obj.grid['anim'] = field
|
|
467
|
+
self._objs.append(_AnimObject(field_flat, T, grid, actor, on_update))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def add_title(self, title: str) -> None:
|
|
471
|
+
"""Adds a title
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
title (str): The title name
|
|
475
|
+
"""
|
|
476
|
+
self._plot.add_text(
|
|
477
|
+
title,
|
|
478
|
+
position='upper_edge',
|
|
479
|
+
font_size=18)
|
|
480
|
+
|
|
481
|
+
def add_text(self, text: str,
|
|
482
|
+
color: str = 'black',
|
|
483
|
+
position: Literal['lower_left', 'lower_right', 'upper_left', 'upper_right', 'lower_edge', 'upper_edge', 'right_edge', 'left_edge']='upper_right',
|
|
484
|
+
abs_position: tuple[float, float, float] = None):
|
|
485
|
+
viewport = False
|
|
486
|
+
if abs_position is not None:
|
|
487
|
+
position = abs_position
|
|
488
|
+
viewport = True
|
|
489
|
+
self._plot.add_text(
|
|
490
|
+
text,
|
|
491
|
+
position=position,
|
|
492
|
+
color=color,
|
|
493
|
+
font_size=18,
|
|
494
|
+
viewport=viewport)
|
|
495
|
+
|
|
496
|
+
def add_quiver(self, x: np.ndarray, y: np.ndarray, z: np.ndarray,
|
|
497
|
+
dx: np.ndarray, dy: np.ndarray, dz: np.ndarray,
|
|
498
|
+
scale: float = 1,
|
|
499
|
+
color: tuple[float, float, float] = None,
|
|
500
|
+
scalemode: Literal['lin','log'] = 'lin'):
|
|
501
|
+
"""Add a quiver plot to the display
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
x (np.ndarray): The X-coordinates
|
|
505
|
+
y (np.ndarray): The Y-coordinates
|
|
506
|
+
z (np.ndarray): The Z-coordinates
|
|
507
|
+
dx (np.ndarray): The arrow X-magnitude
|
|
508
|
+
dy (np.ndarray): The arrow Y-magnitude
|
|
509
|
+
dz (np.ndarray): The arrow Z-magnitude
|
|
510
|
+
scale (float, optional): The arrow scale. Defaults to 1.
|
|
511
|
+
scalemode (Literal['lin','log'], optional): Wether to scale lin or log. Defaults to 'lin'.
|
|
512
|
+
"""
|
|
513
|
+
x = x.flatten()
|
|
514
|
+
y = y.flatten()
|
|
515
|
+
z = z.flatten()
|
|
516
|
+
dx = dx.flatten().real
|
|
517
|
+
dy = dy.flatten().real
|
|
518
|
+
dz = dz.flatten().real
|
|
519
|
+
dmin = _min_distance(x,y,z)
|
|
520
|
+
|
|
521
|
+
dmax = np.max(_norm(dx,dy,dz))
|
|
522
|
+
|
|
523
|
+
Vec = scale * np.array([dx,dy,dz]).T / dmax * dmin
|
|
524
|
+
Coo = np.array([x,y,z]).T
|
|
525
|
+
if scalemode=='log':
|
|
526
|
+
dx, dy, dz = _logscale(Vec[:,0], Vec[:,1], Vec[:,2])
|
|
527
|
+
Vec[:,0] = dx
|
|
528
|
+
Vec[:,1] = dy
|
|
529
|
+
Vec[:,2] = dz
|
|
530
|
+
|
|
531
|
+
kwargs = dict()
|
|
532
|
+
if color is not None:
|
|
533
|
+
kwargs['color'] = color
|
|
534
|
+
pl = self._plot.add_arrows(Coo, Vec, scalars=None, clim=None, cmap=None, **kwargs)
|
|
535
|
+
|
|
536
|
+
def add_contour(self,
|
|
537
|
+
X: np.ndarray,
|
|
538
|
+
Y: np.ndarray,
|
|
539
|
+
Z: np.ndarray,
|
|
540
|
+
V: np.ndarray,
|
|
541
|
+
Nlevels: int = 5,
|
|
542
|
+
symmetrize: bool = True,
|
|
543
|
+
cmap: str = 'viridis'):
|
|
544
|
+
"""Adds a 3D volumetric contourplot based on a 3D grid of X,Y,Z and field values
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
X (np.ndarray): A 3D Grid of X-values
|
|
549
|
+
Y (np.ndarray): A 3D Grid of Y-values
|
|
550
|
+
Z (np.ndarray): A 3D Grid of Z-values
|
|
551
|
+
V (np.ndarray): The scalar quantity to plot ()
|
|
552
|
+
Nlevels (int, optional): The number of contour levels. Defaults to 5.
|
|
553
|
+
symmetrize (bool, optional): Wether to symmetrize the countour levels (-V,V). Defaults to True.
|
|
554
|
+
cmap (str, optional): The color map. Defaults to 'viridis'.
|
|
555
|
+
"""
|
|
556
|
+
Vf = V.flatten()
|
|
557
|
+
vmin = np.min(np.real(Vf))
|
|
558
|
+
vmax = np.max(np.real(Vf))
|
|
559
|
+
if symmetrize:
|
|
560
|
+
level = max(np.abs(vmin),np.abs(vmax))
|
|
561
|
+
vmin, vmax = (-level, level)
|
|
562
|
+
grid = pv.StructuredGrid(X,Y,Z)
|
|
563
|
+
field = V.flatten(order='F')
|
|
564
|
+
grid['anim'] = np.real(field)
|
|
565
|
+
levels = np.linspace(vmin, vmax, Nlevels)
|
|
566
|
+
contour = grid.contour(isosurfaces=levels)
|
|
567
|
+
actor = self._plot.add_mesh(contour, opacity=0.25, cmap=cmap, pickable=False)
|
|
568
|
+
|
|
569
|
+
if self._animate:
|
|
570
|
+
def on_update(obj: _AnimObject, phi: complex):
|
|
571
|
+
new_vals = np.real(obj.field * phi)
|
|
572
|
+
obj.grid['anim'] = new_vals
|
|
573
|
+
new_contour = obj.grid.contour(isosurfaces=levels)
|
|
574
|
+
obj.actor.GetMapper().SetInputData(new_contour)
|
|
575
|
+
|
|
576
|
+
self._objs.append(_AnimObject(field, lambda x: x, grid, actor, on_update))
|
|
577
|
+
|
|
578
|
+
def _add_aux_items(self) -> None:
|
|
579
|
+
saved_camera = {
|
|
580
|
+
"position": self._plot.camera.position,
|
|
581
|
+
"focal_point": self._plot.camera.focal_point,
|
|
582
|
+
"view_up": self._plot.camera.up,
|
|
583
|
+
"view_angle": self._plot.camera.view_angle,
|
|
584
|
+
"clipping_range": self._plot.camera.clipping_range
|
|
585
|
+
}
|
|
586
|
+
#self._plot.add_logo_widget('src/_img/logo.jpeg',position=(0.89,0.89), size=(0.1,0.1))
|
|
587
|
+
bounds = self._plot.bounds
|
|
588
|
+
max_size = max([abs(dim) for dim in [bounds.x_max, bounds.x_min, bounds.y_max, bounds.y_min, bounds.z_max, bounds.z_min]])
|
|
589
|
+
length = self.set.plane_ratio*max_size*2
|
|
590
|
+
if self.set.draw_xplane:
|
|
591
|
+
plane = pv.Plane(
|
|
592
|
+
center=(0, 0, 0),
|
|
593
|
+
direction=(1, 0, 0), # normal vector pointing along +X
|
|
594
|
+
i_size=length,
|
|
595
|
+
j_size=length,
|
|
596
|
+
i_resolution=1,
|
|
597
|
+
j_resolution=1
|
|
598
|
+
)
|
|
599
|
+
self._plot.add_mesh(
|
|
600
|
+
plane,
|
|
601
|
+
color='red',
|
|
602
|
+
opacity=self.set.plane_opacity,
|
|
603
|
+
show_edges=False,
|
|
604
|
+
pickable=False,
|
|
605
|
+
)
|
|
606
|
+
self._plot.add_mesh(
|
|
607
|
+
plane,
|
|
608
|
+
edge_opacity=1.0,
|
|
609
|
+
edge_color='red',
|
|
610
|
+
color='red',
|
|
611
|
+
line_width=self.set.plane_edge_width,
|
|
612
|
+
style='wireframe',
|
|
613
|
+
pickable=False,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if self.set.draw_yplane:
|
|
617
|
+
plane = pv.Plane(
|
|
618
|
+
center=(0, 0, 0),
|
|
619
|
+
direction=(0, 1, 0), # normal vector pointing along +X
|
|
620
|
+
i_size=length,
|
|
621
|
+
j_size=length,
|
|
622
|
+
i_resolution=1,
|
|
623
|
+
j_resolution=1
|
|
624
|
+
)
|
|
625
|
+
self._plot.add_mesh(
|
|
626
|
+
plane,
|
|
627
|
+
color='green',
|
|
628
|
+
opacity=self.set.plane_opacity,
|
|
629
|
+
show_edges=False,
|
|
630
|
+
pickable=False,
|
|
631
|
+
)
|
|
632
|
+
self._plot.add_mesh(
|
|
633
|
+
plane,
|
|
634
|
+
edge_opacity=1.0,
|
|
635
|
+
edge_color='green',
|
|
636
|
+
color='green',
|
|
637
|
+
line_width=self.set.plane_edge_width,
|
|
638
|
+
style='wireframe',
|
|
639
|
+
pickable=False,
|
|
640
|
+
)
|
|
641
|
+
if self.set.draw_zplane:
|
|
642
|
+
plane = pv.Plane(
|
|
643
|
+
center=(0, 0, 0),
|
|
644
|
+
direction=(0, 0, 1), # normal vector pointing along +X
|
|
645
|
+
i_size=length,
|
|
646
|
+
j_size=length,
|
|
647
|
+
i_resolution=1,
|
|
648
|
+
j_resolution=1
|
|
649
|
+
)
|
|
650
|
+
self._plot.add_mesh(
|
|
651
|
+
plane,
|
|
652
|
+
color='blue',
|
|
653
|
+
opacity=self.set.plane_opacity,
|
|
654
|
+
show_edges=False,
|
|
655
|
+
pickable=False,
|
|
656
|
+
)
|
|
657
|
+
self._plot.add_mesh(
|
|
658
|
+
plane,
|
|
659
|
+
edge_opacity=1.0,
|
|
660
|
+
edge_color='blue',
|
|
661
|
+
color='blue',
|
|
662
|
+
line_width=self.set.plane_edge_width,
|
|
663
|
+
style='wireframe',
|
|
664
|
+
pickable=False,
|
|
665
|
+
)
|
|
666
|
+
# Draw X-axis
|
|
667
|
+
if getattr(self.set, 'draw_xax', False):
|
|
668
|
+
x_line = pv.Line(
|
|
669
|
+
pointa=(-length, 0, 0),
|
|
670
|
+
pointb=(length, 0, 0),
|
|
671
|
+
)
|
|
672
|
+
self._plot.add_mesh(
|
|
673
|
+
x_line,
|
|
674
|
+
color='red',
|
|
675
|
+
line_width=self.set.axis_line_width,
|
|
676
|
+
pickable=False,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Draw Y-axis
|
|
680
|
+
if getattr(self.set, 'draw_yax', False):
|
|
681
|
+
y_line = pv.Line(
|
|
682
|
+
pointa=(0, -length, 0),
|
|
683
|
+
pointb=(0, length, 0),
|
|
684
|
+
)
|
|
685
|
+
self._plot.add_mesh(
|
|
686
|
+
y_line,
|
|
687
|
+
color='green',
|
|
688
|
+
line_width=self.set.axis_line_width,
|
|
689
|
+
pickable=False,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Draw Z-axis
|
|
693
|
+
if getattr(self.set, 'draw_zax', False):
|
|
694
|
+
z_line = pv.Line(
|
|
695
|
+
pointa=(0, 0, -length),
|
|
696
|
+
pointb=(0, 0, length),
|
|
697
|
+
)
|
|
698
|
+
self._plot.add_mesh(
|
|
699
|
+
z_line,
|
|
700
|
+
color='blue',
|
|
701
|
+
line_width=self.set.axis_line_width,
|
|
702
|
+
pickable=False,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
exponent = np.floor(np.log10(length))
|
|
706
|
+
gs = 10 ** exponent
|
|
707
|
+
N = np.ceil(length/gs)
|
|
708
|
+
if N < 5:
|
|
709
|
+
gs = gs/10
|
|
710
|
+
L = (2*np.ceil(length/(2*gs))+1)*gs
|
|
711
|
+
|
|
712
|
+
# XY grid at Z=0
|
|
713
|
+
if self.set.show_zgrid:
|
|
714
|
+
x_vals = np.arange(-L, L+gs, gs)
|
|
715
|
+
y_vals = np.arange(-L, L+gs, gs)
|
|
716
|
+
|
|
717
|
+
# lines parallel to X
|
|
718
|
+
for y in y_vals:
|
|
719
|
+
line = pv.Line(
|
|
720
|
+
pointa=(-L, y, 0),
|
|
721
|
+
pointb=(L, y, 0)
|
|
722
|
+
)
|
|
723
|
+
self._plot.add_mesh(line, color=self.set.grid_line_color, line_width=self.set.grid_line_width, opacity=0.5, edge_opacity=0.5,pickable=False)
|
|
724
|
+
|
|
725
|
+
# lines parallel to Y
|
|
726
|
+
for x in x_vals:
|
|
727
|
+
line = pv.Line(
|
|
728
|
+
pointa=(x, -L, 0),
|
|
729
|
+
pointb=(x, L, 0)
|
|
730
|
+
)
|
|
731
|
+
self._plot.add_mesh(line, color=self.set.grid_line_color, line_width=self.set.grid_line_width, opacity=0.5, edge_opacity=0.5,pickable=False)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# YZ grid at X=0
|
|
735
|
+
if self.set.show_xgrid:
|
|
736
|
+
y_vals = np.arange(-L, L+gs, gs)
|
|
737
|
+
z_vals = np.arange(-L, L+gs, gs)
|
|
738
|
+
|
|
739
|
+
# lines parallel to Y
|
|
740
|
+
for z in z_vals:
|
|
741
|
+
line = pv.Line(
|
|
742
|
+
pointa=(0, -L, z),
|
|
743
|
+
pointb=(0, L, z)
|
|
744
|
+
)
|
|
745
|
+
self._plot.add_mesh(line, color=self.set.grid_line_color, line_width=self.set.grid_line_width, opacity=0.5, edge_opacity=0.5, pickable=False)
|
|
746
|
+
|
|
747
|
+
# lines parallel to Z
|
|
748
|
+
for y in y_vals:
|
|
749
|
+
line = pv.Line(
|
|
750
|
+
pointa=(0, y, -L),
|
|
751
|
+
pointb=(0, y, L)
|
|
752
|
+
)
|
|
753
|
+
self._plot.add_mesh(line, color=self.set.grid_line_color, line_width=self.set.grid_line_width, opacity=0.5, edge_opacity=0.5, pickable=False)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
# XZ grid at Y=0
|
|
757
|
+
if self.set.show_ygrid:
|
|
758
|
+
x_vals = np.arange(-L, L+gs, gs)
|
|
759
|
+
z_vals = np.arange(-L, L+gs, gs)
|
|
760
|
+
|
|
761
|
+
# lines parallel to X
|
|
762
|
+
for z in z_vals:
|
|
763
|
+
line = pv.Line(
|
|
764
|
+
pointa=(-length, 0, z),
|
|
765
|
+
pointb=(length, 0, z)
|
|
766
|
+
)
|
|
767
|
+
self._plot.add_mesh(line, color=self.set.grid_line_color, line_width=self.set.grid_line_width, opacity=0.5, edge_opacity=0.5, pickable=False)
|
|
768
|
+
|
|
769
|
+
# lines parallel to Z
|
|
770
|
+
for x in x_vals:
|
|
771
|
+
line = pv.Line(
|
|
772
|
+
pointa=(x, 0, -length),
|
|
773
|
+
pointb=(x, 0, length)
|
|
774
|
+
)
|
|
775
|
+
self._plot.add_mesh(line, color=self.set.grid_line_color, line_width=self.set.grid_line_width, opacity=0.5, edge_opacity=0.5, pickable=False)
|
|
776
|
+
|
|
777
|
+
if self.set.add_light:
|
|
778
|
+
light = pv.Light()
|
|
779
|
+
light.set_direction_angle(*self.set.light_angle)
|
|
780
|
+
self._plot.add_light(light)
|
|
781
|
+
|
|
782
|
+
self._plot.set_background(self.set.background_bottom, top=self.set.background_top)
|
|
783
|
+
self._plot.add_axes()
|
|
784
|
+
|
|
785
|
+
self._plot.camera.position = saved_camera["position"]
|
|
786
|
+
self._plot.camera.focal_point = saved_camera["focal_point"]
|
|
787
|
+
self._plot.camera.up = saved_camera["view_up"]
|
|
788
|
+
self._plot.camera.view_angle = saved_camera["view_angle"]
|
|
789
|
+
self._plot.camera.clipping_range = saved_camera["clipping_range"]
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def freeze(function):
|
|
794
|
+
|
|
795
|
+
def new_function(self, *args, **kwargs):
|
|
796
|
+
cam = self.disp._plot.camera_position[:]
|
|
797
|
+
self.disp._plot.suppress_rendering = True
|
|
798
|
+
function(self, *args, **kwargs)
|
|
799
|
+
self.disp._plot.camera_position = cam
|
|
800
|
+
self.disp._plot.suppress_rendering = False
|
|
801
|
+
self.disp._plot.render()
|
|
802
|
+
return new_function
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
class ScreenSelector:
|
|
806
|
+
|
|
807
|
+
def __init__(self, display: PVDisplay):
|
|
808
|
+
self.disp: PVDisplay = display
|
|
809
|
+
self.original_actors: list[pv.Actor] = []
|
|
810
|
+
self.select_actors: list[pv.Actor] = []
|
|
811
|
+
self.grids: list[pv.UnstructuredGrid] = []
|
|
812
|
+
self.surfs: dict[int, np.ndarray] = dict()
|
|
813
|
+
self.state = False
|
|
814
|
+
|
|
815
|
+
def toggle(self):
|
|
816
|
+
if self.state:
|
|
817
|
+
self.turn_off()
|
|
818
|
+
else:
|
|
819
|
+
self.activate()
|
|
820
|
+
|
|
821
|
+
def activate(self):
|
|
822
|
+
self.original_actors = list(self.disp._plot.actors.values())
|
|
823
|
+
|
|
824
|
+
for actor in self.original_actors:
|
|
825
|
+
if isinstance(actor, pv.Text):
|
|
826
|
+
continue
|
|
827
|
+
actor.pickable = False
|
|
828
|
+
|
|
829
|
+
if len(self.grids) == 0:
|
|
830
|
+
for key in self.disp._facetags:
|
|
831
|
+
tris = self.disp._mesh.get_triangles(key)
|
|
832
|
+
ntris = tris.shape[0]
|
|
833
|
+
cells = np.zeros((ntris,4), dtype=np.int64)
|
|
834
|
+
cells[:,1:] = self.disp._mesh.tris[:,tris].T
|
|
835
|
+
cells[:,0] = 3
|
|
836
|
+
nodes = np.unique(self.disp._mesh.tris[:,tris].flatten())
|
|
837
|
+
celltypes = np.full(ntris, fill_value=pv.CellType.TRIANGLE, dtype=np.uint8)
|
|
838
|
+
points = self.disp._mesh.nodes.T
|
|
839
|
+
grid = pv.UnstructuredGrid(cells, celltypes, points)
|
|
840
|
+
grid._tag = key
|
|
841
|
+
self.grids.append(grid)
|
|
842
|
+
self.surfs[key] = points[nodes,:].T
|
|
843
|
+
|
|
844
|
+
self.select_actors = []
|
|
845
|
+
for grid in self.grids:
|
|
846
|
+
actor = self.disp._plot.add_mesh(grid, opacity=0.001, color='red', pickable=True, name=f'FaceTag_{grid._tag}')
|
|
847
|
+
self.select_actors.append(actor)
|
|
848
|
+
|
|
849
|
+
def callback(actor: pv.Actor):
|
|
850
|
+
key = int(actor.name.split('_')[1])
|
|
851
|
+
points = self.surfs[key]
|
|
852
|
+
xs = points[0,:]
|
|
853
|
+
ys = points[1,:]
|
|
854
|
+
zs = points[2,:]
|
|
855
|
+
meanx = np.mean(xs)
|
|
856
|
+
meany = np.mean(ys)
|
|
857
|
+
meanz = np.mean(zs)
|
|
858
|
+
data = (meanx, meany, meanz, min(xs), min(ys), min(zs), max(xs), max(ys), max(zs))
|
|
859
|
+
encoded = encode_data(data)
|
|
860
|
+
print(f'Face code key={key}: ', encoded)
|
|
861
|
+
|
|
862
|
+
self.disp._plot.enable_mesh_picking(callback, style='surface', left_clicking=True, use_actor=True)
|
|
863
|
+
|
|
864
|
+
def turn_off(self) -> None:
|
|
865
|
+
for actor in self.select_actors:
|
|
866
|
+
self.disp._plot.remove_actor(actor)
|
|
867
|
+
self.select_actors = []
|
|
868
|
+
for actor in self.original_actors:
|
|
869
|
+
if isinstance(actor, pv.Text):
|
|
870
|
+
continue
|
|
871
|
+
actor.pickable = True
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class ScreenRuler:
|
|
875
|
+
|
|
876
|
+
def __init__(self, display: PVDisplay, min_length: float):
|
|
877
|
+
self.disp: PVDisplay = display
|
|
878
|
+
self.points: list[tuple] = [(0,0,0),(0,0,0)]
|
|
879
|
+
self.text: pv.Text = None
|
|
880
|
+
self.ruler = None
|
|
881
|
+
self.state = False
|
|
882
|
+
self.min_length: float = min_length
|
|
883
|
+
|
|
884
|
+
@freeze
|
|
885
|
+
def toggle(self):
|
|
886
|
+
if not self.state:
|
|
887
|
+
self.state = True
|
|
888
|
+
self.disp._plot.enable_point_picking(self._add_point, left_clicking=True, tolerance=self.min_length)
|
|
889
|
+
else:
|
|
890
|
+
self.state = False
|
|
891
|
+
self.disp._plot.disable_picking()
|
|
892
|
+
|
|
893
|
+
@freeze
|
|
894
|
+
def turn_off(self):
|
|
895
|
+
self.state = False
|
|
896
|
+
self.disp._plot.disable_picking()
|
|
897
|
+
|
|
898
|
+
@property
|
|
899
|
+
def dist(self) -> float:
|
|
900
|
+
p1, p2 = self.points
|
|
901
|
+
return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2 + (p1[2]-p2[2])**2)**(0.5)
|
|
902
|
+
|
|
903
|
+
@property
|
|
904
|
+
def middle(self) -> tuple[float, float, float]:
|
|
905
|
+
p1, p2 = self.points
|
|
906
|
+
return ((p1[0]+p2[0])/2, (p1[1]+p2[1])/2, (p1[2]+p2[2])/2)
|
|
907
|
+
|
|
908
|
+
@property
|
|
909
|
+
def measurement_string(self) -> str:
|
|
910
|
+
dist = self.dist
|
|
911
|
+
p1, p2 = self.points
|
|
912
|
+
dx = p2[0]-p1[0]
|
|
913
|
+
dy = p2[1]-p1[1]
|
|
914
|
+
dz = p2[2]-p1[2]
|
|
915
|
+
return f'{dist*1000:.2f}mm (dx={1000.*dx:.4f}mm, dy={1000.*dy:.4f}mm, dz={1000.*dz:.4f}mm)'
|
|
916
|
+
|
|
917
|
+
def set_ruler(self) -> None:
|
|
918
|
+
if self.ruler is None:
|
|
919
|
+
self.ruler = self.disp._plot.add_ruler(self.points[0], self.points[1], title=f'{1000*self.dist:.2f}mm')
|
|
920
|
+
else:
|
|
921
|
+
p1 = self.ruler.GetPositionCoordinate()
|
|
922
|
+
p2 = self.ruler.GetPosition2Coordinate()
|
|
923
|
+
p1.SetValue(*self.points[0])
|
|
924
|
+
p2.SetValue(*self.points[1])
|
|
925
|
+
self.ruler.SetTitle(f'{1000*self.dist:.2f}mm')
|
|
926
|
+
|
|
927
|
+
@freeze
|
|
928
|
+
def _add_point(self, point: tuple[float, float, float]):
|
|
929
|
+
self.points = [point,self.points[0]]
|
|
930
|
+
self.text = self.disp._plot.add_text(self.measurement_string, self.middle, name='RulerText')
|
|
931
|
+
self.set_ruler()
|