Skip to content

Commit a733dea

Browse files
committed
spi pellets reader
1 parent 6bdb8e0 commit a733dea

File tree

2 files changed

+318
-0
lines changed

2 files changed

+318
-0
lines changed

imas_paraview/plugins/pellets.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"""ParaView plugin to visualize SPI fragment positions"""
2+
3+
import logging
4+
from dataclasses import dataclass
5+
6+
import numpy as np
7+
from imas.ids_primitive import IDSNumericArray
8+
from imas.ids_struct_array import IDSStructArray
9+
from imas.ids_structure import IDSStructure
10+
from paraview.util.vtkAlgorithm import smhint, smproxy
11+
from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet, vtkPolyData
12+
from vtkmodules.vtkFiltersCore import vtkAppendPolyData
13+
14+
from imas_paraview.paraview_support.servermanager_tools import (
15+
doublevector,
16+
propertygroup,
17+
)
18+
from imas_paraview.plugins.base_class import GGDVTKPluginBase
19+
from imas_paraview.util import (
20+
create_vtk_arrows,
21+
create_vtk_spheres,
22+
find_closest_indices,
23+
pol_to_cart,
24+
vel_pol_to_cart,
25+
)
26+
27+
28+
@dataclass
29+
class Fragments:
30+
fragments: IDSStructArray # injector.fragment
31+
32+
33+
@dataclass
34+
class VelMassCentres:
35+
vel_r: IDSNumericArray # injector.velocity_mass_centre_fragments_r
36+
vel_phi: IDSNumericArray # injector.velocity_mass_centre_fragments_phi
37+
vel_z: IDSNumericArray # injector.velocity_mass_centre_fragments_z
38+
shatter_cone_origin: IDSStructure # injector.shatter_cone.origin
39+
40+
41+
logger = logging.getLogger("imas_paraview")
42+
43+
# TODO: Also visualize 'pellets' IDS
44+
# TODO: Currently only the shattered pellet fragments are visualized, we can also
45+
# - spi.injector(i1).pellet position/velocity over time
46+
# - injector(i1).injection_direction
47+
# - injector(i1).shatter_cone
48+
# - injector(i1)/pellet/core/species(i2).name
49+
50+
SUPPORTED_IDS_NAMES = ["spi"]
51+
52+
53+
@smproxy.source(label="(Shattered) Pellets Reader")
54+
@smhint.xml("""<ShowInMenu category="IMAS Tools" />""")
55+
class PelletReader(GGDVTKPluginBase, is_time_dependent=True):
56+
def __init__(self):
57+
super().__init__("vtkMultiBlockDataSet", SUPPORTED_IDS_NAMES)
58+
self.selectable_map = {}
59+
self.frag_vel_scaling_factor = 1.0
60+
"""Scaling factor for the velocity arrow glyphs of shattered pellet fragments"""
61+
self.frag_scaling_factor = 1.0
62+
"""Scaling factor for the spheres of shattered pellet fragments"""
63+
64+
@doublevector(
65+
label="VelocityScalingFactor",
66+
name="frag_vel_scaling_factor",
67+
default_values=1.0,
68+
)
69+
def P98_SetVelocityScalingFactor(self, val):
70+
self._update_property("frag_vel_scaling_factor", val)
71+
72+
@doublevector(label="ScalingFactor", name="frag_scaling_factor", default_values=1.0)
73+
def P99_SetScalingFactor(self, val):
74+
self._update_property("frag_scaling_factor", val)
75+
76+
@propertygroup(
77+
"Pellets Reader Settings", ["frag_scaling_factor", "frag_vel_scaling_factor"]
78+
)
79+
def PG3_PelletReaderGroup(self):
80+
"""Dummy function to define a PropertyGroup."""
81+
82+
def setup_ids(self):
83+
"""Populate the array selector with injector names."""
84+
assert self._ids is not None, "IDS cannot be empty during setup."
85+
86+
self.selectable_map = {}
87+
88+
for i, injector in enumerate(self._ids.injector):
89+
injector_name = injector.name
90+
if not injector_name:
91+
injector_name = f"injector {i}"
92+
logger.warning(
93+
"Found an injector without a name, loading as '%s'.", injector_name
94+
)
95+
self.populate_fragments(injector, injector_name)
96+
self.populate_vel_mass_centre(injector, injector_name)
97+
self._selectable = list(self.selectable_map.keys())
98+
99+
def populate_fragments(self, injector, injector_name):
100+
if len(injector.fragment) > 0:
101+
injector_name = f"Shattered fragments ({injector_name})"
102+
self.selectable_map[injector_name] = Fragments(fragments=injector.fragment)
103+
else:
104+
logger.warning("'%s' has no fragments, skipping.", injector_name)
105+
106+
def populate_vel_mass_centre(self, injector, injector_name):
107+
vel_r = injector.velocity_mass_centre_fragments_r
108+
109+
try:
110+
vel_phi = injector.velocity_mass_centre_fragments_phi
111+
except AttributeError:
112+
vel_phi = injector.velocity_mass_centre_fragments_tor
113+
vel_z = injector.velocity_mass_centre_fragments_z
114+
origin = injector.shatter_cone.origin
115+
116+
if (
117+
vel_r.has_value
118+
and vel_phi.has_value
119+
and vel_z.has_value
120+
and origin.r.has_value
121+
and origin.phi.has_value
122+
and origin.z.has_value
123+
):
124+
injector_name = f"Fragment centre of mass velocity ({injector_name})"
125+
self.selectable_map[injector_name] = VelMassCentres(
126+
vel_r=vel_r,
127+
vel_phi=vel_phi,
128+
vel_z=vel_z,
129+
shatter_cone_origin=origin,
130+
)
131+
else:
132+
logger.warning(
133+
"'%s' does not have both the 'velocity_mass_centre' and "
134+
"'shatter_cone.origin' defined. The velocity centre of mass of this "
135+
"injector will not be loaded.",
136+
injector_name,
137+
)
138+
139+
def RequestData(self, request, inInfo, outInfo):
140+
if self._dbentry is None or not self._ids_and_occurrence or self._ids is None:
141+
return 1
142+
143+
time = self._get_selected_time_step(outInfo)
144+
if time is None:
145+
return 1
146+
147+
time_idx = find_closest_indices([time], self._ids.time)[0]
148+
output = vtkMultiBlockDataSet.GetData(outInfo)
149+
150+
for block_id, selection_name in enumerate(self._selected):
151+
selected = self.selectable_map[selection_name]
152+
vtk_object = None
153+
if isinstance(selected, Fragments):
154+
spheres_poly, arrows_poly = self._create_fragments_geom(
155+
selected, time_idx
156+
)
157+
appended = vtkAppendPolyData()
158+
appended.AddInputData(spheres_poly)
159+
appended.AddInputData(arrows_poly)
160+
appended.Update()
161+
vtk_object = appended.GetOutput()
162+
elif isinstance(selected, VelMassCentres):
163+
vtk_object = self._build_vel_mass_centre_geom(selected)
164+
165+
if vtk_object is not None:
166+
output.GetMetaData(block_id).Set(selection_name)
167+
output.SetBlock(block_id, vtk_object)
168+
return 1
169+
170+
def _build_vel_mass_centre_geom(self, selectable: VelMassCentres) -> vtkPolyData:
171+
try:
172+
vel_phi = selectable.vel_phi
173+
except AttributeError:
174+
vel_phi = selectable.vel_tor
175+
176+
pos_phi = selectable.shattering_position.phi
177+
pos_x, pos_y = pol_to_cart(selectable.shattering_position.r, pos_phi)
178+
179+
position = np.array([[pos_x, pos_y, selectable.shattering_position.z]])
180+
velocity = vel_pol_to_cart(
181+
np.array([selectable.vel_r]),
182+
np.array([vel_phi]),
183+
np.array([selectable.vel_z]),
184+
np.array([pos_phi]),
185+
)
186+
187+
return create_vtk_arrows(position, velocity)
188+
189+
def _create_fragments_geom(self, selectable, time_idx):
190+
"""Create sphere and arrow polydata for all fragments of one injector.
191+
192+
Args:
193+
injector: The injector IDS node.
194+
time_idx: The time index to query.
195+
196+
Returns:
197+
Tuple of (spheres_polydata, arrows_polydata).
198+
"""
199+
fragments = selectable.fragments
200+
num_fragments = len(fragments)
201+
202+
r = np.empty(num_fragments)
203+
phi = np.empty(num_fragments)
204+
z = np.empty(num_fragments)
205+
volumes = np.empty(num_fragments)
206+
v_r = np.empty(num_fragments)
207+
v_phi = np.empty(num_fragments)
208+
v_z = np.empty(num_fragments)
209+
210+
for i, f in enumerate(fragments):
211+
pos = f.position
212+
r[i] = pos.r[time_idx]
213+
phi[i] = pos.phi[time_idx]
214+
z[i] = pos.z[time_idx]
215+
volumes[i] = f.volume[time_idx]
216+
v_r[i] = f.velocity_r[time_idx]
217+
v_z[i] = f.velocity_z[time_idx]
218+
try:
219+
v_phi[i] = f.velocity_phi[time_idx]
220+
except AttributeError:
221+
v_phi[i] = f.velocity_tor[time_idx]
222+
223+
pos_x, pos_y = pol_to_cart(r, phi)
224+
positions = np.stack([pos_x, pos_y, z], axis=1)
225+
radii = (3.0 * volumes / (4.0 * np.pi)) ** (1.0 / 3.0)
226+
velocities = vel_pol_to_cart(v_r, v_phi, v_z, phi)
227+
228+
spheres = create_vtk_spheres(positions, radii)
229+
arrows = create_vtk_arrows(positions, velocities)
230+
return spheres, arrows

imas_paraview/util.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
vtkPartitionedDataSetCollection,
1313
vtkPolyData,
1414
)
15+
from vtkmodules.vtkFiltersCore import vtkGlyph3D
16+
from vtkmodules.vtkFiltersSources import vtkArrowSource, vtkSphereSource
1517
from vtkmodules.vtkIOXML import (
1618
vtkXMLPartitionedDataSetCollectionReader,
1719
vtkXMLUnstructuredGridReader,
@@ -197,6 +199,14 @@ def find_closest_indices(values_to_extract, source_array):
197199
return list(indices[indices >= 0])
198200

199201

202+
def vel_pol_to_cart(
203+
v_r: np.ndarray, v_phi: np.ndarray, v_z: np.ndarray, phi: np.ndarray
204+
) -> np.ndarray:
205+
vel_x = v_r * np.cos(phi) - v_phi * np.sin(phi)
206+
vel_y = v_r * np.sin(phi) + v_phi * np.cos(phi)
207+
return np.stack([vel_x, vel_y, v_z], axis=1)
208+
209+
200210
def pol_to_cart(rho, phi):
201211
"""Convert from polar (or cylindrical) coordinates to cartesian.
202212
@@ -354,3 +364,81 @@ def has_imas_core():
354364
except AttributeError:
355365
# imas-python >= v2.2.0 always has IMAS-Core installed
356366
return True
367+
368+
369+
def create_vtk_spheres(positions, radii, scaling_factor=1.0):
370+
"""Create a vtkPolyData of glyph spheres scaled by fragment radius.
371+
372+
Args:
373+
positions: Array with Cartesian positions.
374+
radii: Array with sphere radii.
375+
scaling_factor: Scaling factor of the spheres.
376+
377+
Returns:
378+
vtkPolyData with all sphere glyphs appended.
379+
"""
380+
pts = vtkPoints()
381+
pts.SetData(numpy_to_vtk(positions))
382+
383+
src_poly = vtkPolyData()
384+
src_poly.SetPoints(pts)
385+
386+
scale_array = numpy_to_vtk(radii)
387+
scale_array.SetName("radius")
388+
src_poly.GetPointData().SetScalars(scale_array)
389+
390+
sphere = vtkSphereSource()
391+
sphere.SetRadius(1.0)
392+
sphere.Update()
393+
394+
glyph = vtkGlyph3D()
395+
# Apply sphere source to each point
396+
glyph.SetInputData(src_poly)
397+
glyph.SetSourceConnection(sphere.GetOutputPort())
398+
glyph.SetScaleModeToScaleByScalar()
399+
glyph.SetScaleFactor(scaling_factor)
400+
glyph.Update()
401+
return glyph.GetOutput()
402+
403+
404+
def create_vtk_arrows(positions, velocities, scaling_factor=1.0):
405+
"""Create a vtkPolyData of glyph arrows representing fragment velocities.
406+
407+
Args:
408+
positions: Array with Cartesian positions.
409+
velocities: Array with Cartesian velocities.
410+
scaling_factor: Scaling factor of the arrows.
411+
412+
Returns:
413+
vtkPolyData with all arrow glyphs appended.
414+
"""
415+
416+
pts = vtkPoints()
417+
pts.SetData(numpy_to_vtk(positions, deep=True))
418+
419+
src_poly = vtkPolyData()
420+
src_poly.SetPoints(pts)
421+
422+
norm_arr = numpy_to_vtk(velocities, deep=True)
423+
norm_arr.SetName("velocity_direction")
424+
src_poly.GetPointData().SetNormals(norm_arr)
425+
426+
# Scale arrow with speed
427+
speeds = np.linalg.norm(velocities, axis=1)
428+
speed_arr = numpy_to_vtk(speeds, deep=True)
429+
speed_arr.SetName("speed [m/s]")
430+
src_poly.GetPointData().SetScalars(speed_arr)
431+
432+
arrow = vtkArrowSource()
433+
arrow.Update()
434+
435+
glyph = vtkGlyph3D()
436+
glyph.SetInputData(src_poly)
437+
glyph.SetSourceConnection(arrow.GetOutputPort())
438+
glyph.SetVectorModeToUseNormal()
439+
glyph.SetScaleModeToScaleByScalar()
440+
glyph.SetScaleFactor(scaling_factor * 1e-3)
441+
glyph.OrientOn()
442+
glyph.Update()
443+
444+
return glyph.GetOutput()

0 commit comments

Comments
 (0)