Skip to content

Commit 384b0a2

Browse files
committed
Cherab interface for xBOUT
Raytracing library Cherab (https://www.cherab.info/) For now only works for axisymmetric grids (x, theta). Creates a triangulation of the BOUT++ grid, then can use that to create a volumetric emitter from DataArrays.
1 parent d8b79ee commit 384b0a2

File tree

6 files changed

+410
-16
lines changed

6 files changed

+410
-16
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ calc = [
5353
"xrft",
5454
"xhistogram",
5555
]
56+
cherab = [
57+
"cherab",
58+
]
5659
docs = [
5760
"sphinx >= 5.3",
5861
"sphinx-book-theme >= 0.4.0rc1",

xbout/boutdataarray.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,3 +1101,71 @@ def plot3d(self, ax=None, **kwargs):
11011101
See plotfuncs.plot3d()
11021102
"""
11031103
return plotfuncs.plot3d(self.data, **kwargs)
1104+
1105+
def with_cherab_grid(self):
1106+
"""
1107+
Returns a new DataArray with a 'cherab_grid' attribute.
1108+
1109+
If called then the `cherab` package must be available.
1110+
"""
1111+
# Import here so Cherab is required only if this method is called
1112+
from .cherab import grid
1113+
1114+
return grid.da_with_cherab_grid(self.data)
1115+
1116+
def as_cherab_data(self):
1117+
"""
1118+
Returns a new cherab.TriangularData object.
1119+
1120+
If a Cherab grid has not been calculated then it will be created.
1121+
It is more efficient to first compute a Cherab grid for a whole
1122+
DataSet (using `with_cherab_grid`) and then call this function
1123+
on individual DataArrays.
1124+
"""
1125+
if "cherab_grid" not in self.data.attrs:
1126+
# Calculate the Cherab triangulation
1127+
da = self.with_cherab_grid()
1128+
else:
1129+
da = self.data
1130+
1131+
return da.attrs["cherab_grid"].with_data(da)
1132+
1133+
def as_cherab_emitter(
1134+
self,
1135+
parent=None,
1136+
cylinder_zmin=None,
1137+
cylinder_zmax=None,
1138+
cylinder_rmin=None,
1139+
cylinder_rmax=None,
1140+
step: float = 0.01,
1141+
):
1142+
"""
1143+
Make a Cherab emitter (RadiationFunction), rotating a 2D mesh about the Z axis
1144+
1145+
Cherab (https://www.cherab.info/) is a python library for forward
1146+
modelling diagnostics based on spectroscopic plasma emission.
1147+
It is based on the Raysect (http://www.raysect.org/) scientific
1148+
ray-tracing framework.
1149+
1150+
Parameters
1151+
----------
1152+
parent : Cherab scene (default None)
1153+
The Cherab scene to attach the emitter to
1154+
step : float (default 0.01 meters)
1155+
Volume integration step length [m]
1156+
1157+
Returns
1158+
-------
1159+
1160+
A cherab.tools.emitters.RadiationFunction
1161+
1162+
"""
1163+
1164+
return self.as_cherab_data().to_emitter(
1165+
parent=parent,
1166+
cylinder_zmin=cylinder_zmin,
1167+
cylinder_zmax=cylinder_zmax,
1168+
cylinder_rmin=cyliner_rmin,
1169+
cylinder_rmax=cyliner_rmax,
1170+
step=step,
1171+
)

xbout/boutdataset.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -609,14 +609,12 @@ def interpolate_to_cartesian(
609609
newY = newY_1d.expand_dims({"X": nX, "Z": nZ}, axis=[0, 2])
610610
newZ_1d = xr.DataArray(np.linspace(Zmin, Zmax, nZ), dims="Z")
611611
newZ = newZ_1d.expand_dims({"X": nX, "Y": nY}, axis=[0, 1])
612-
newR = np.sqrt(newX**2 + newY**2)
612+
newR = np.sqrt(newX ** 2 + newY ** 2)
613613
newzeta = np.arctan2(newY, newX)
614614
# Define newzeta in range 0->2*pi
615615
newzeta = np.where(newzeta < 0.0, newzeta + 2.0 * np.pi, newzeta)
616616

617-
from scipy.interpolate import (
618-
RegularGridInterpolator,
619-
)
617+
from scipy.interpolate import RegularGridInterpolator
620618

621619
# Create Cylindrical coordinates for intermediate grid
622620
Rcyl_min = float_type(ds["R"].min())
@@ -664,10 +662,7 @@ def interp_single_time(da):
664662
)
665663

666664
print(" do 3d interpolation")
667-
return interp(
668-
(newR, newZ, newzeta),
669-
method="linear",
670-
)
665+
return interp((newR, newZ, newzeta), method="linear")
671666

672667
for name, da in ds.data_vars.items():
673668
print(f"\ninterpolating {name}")
@@ -993,14 +988,7 @@ def to_restart(
993988
# Is this even possible without saving the guard cells?
994989
# Can they be recreated?
995990
restart_datasets, paths = _split_into_restarts(
996-
self.data,
997-
variables,
998-
savepath,
999-
nxpe,
1000-
nype,
1001-
tind,
1002-
prefix,
1003-
overwrite,
991+
self.data, variables, savepath, nxpe, nype, tind, prefix, overwrite
1004992
)
1005993

1006994
with ProgressBar():
@@ -1357,6 +1345,17 @@ def is_list(variable):
13571345

13581346
return anim
13591347

1348+
def with_cherab_grid(self):
1349+
"""
1350+
Returns a new DataSet with a 'cherab_grid' attribute.
1351+
1352+
If called then the `cherab` package must be available.
1353+
"""
1354+
# Import here so Cherab is required only if this method is called
1355+
from .cherab import grid
1356+
1357+
return grid.ds_with_cherab_grid(self.data)
1358+
13601359

13611360
def _find_major_vars(data):
13621361
"""

xbout/cherab/__init__.py

Whitespace-only changes.

xbout/cherab/grid.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import numpy as np
2+
import xarray as xr
3+
4+
from .triangulate import Triangulate
5+
6+
7+
def da_with_cherab_grid(da):
8+
"""
9+
Convert an BOUT++ DataArray to a format that Cherab can use:
10+
- A 'cell_number' coordinate
11+
- A 'cherab_grid` attribute
12+
13+
The cell_number coordinate enables the DataArray to be sliced
14+
before input to Cherab.
15+
16+
Parameters
17+
----------
18+
ds : xarray.DataArray
19+
20+
Returns
21+
-------
22+
updated_da
23+
"""
24+
if "cherab_grid" in da.attrs:
25+
# Already has required attribute
26+
return da
27+
28+
if da.attrs["geometry"] != "toroidal":
29+
raise ValueError("xhermes.plotting.cherab: Geometry must be toroidal")
30+
31+
if da.sizes["zeta"] != 1:
32+
raise ValueError("xhermes.plotting.cherab: Zeta index must have size 1")
33+
34+
nx = da.sizes["x"]
35+
ny = da.sizes["theta"]
36+
37+
# Cell corners
38+
rm = np.stack(
39+
(
40+
da.coords["Rxy_upper_right_corners"],
41+
da.coords["Rxy_upper_left_corners"],
42+
da.coords["Rxy_lower_right_corners"],
43+
da.coords["Rxy_lower_left_corners"],
44+
),
45+
axis=-1,
46+
)
47+
zm = np.stack(
48+
(
49+
da.coords["Zxy_upper_right_corners"],
50+
da.coords["Zxy_upper_left_corners"],
51+
da.coords["Zxy_lower_right_corners"],
52+
da.coords["Zxy_lower_left_corners"],
53+
),
54+
axis=-1,
55+
)
56+
57+
grid = Triangulate(rm, zm)
58+
59+
# Store the cell number as a coordinate.
60+
# This allows slicing of arrays before passing to Cherab
61+
62+
# Create a DataArray for the vertices and triangles
63+
cell_number = xr.DataArray(
64+
grid.cell_number, dims=["x", "theta"], name="cell_number"
65+
)
66+
67+
result = da.assign_coords(cell_number=cell_number)
68+
result.attrs.update(cherab_grid=grid)
69+
return result
70+
71+
72+
def ds_with_cherab_grid(ds):
73+
"""
74+
Create an xarray DataSet with a Cherab grid
75+
76+
Parameters
77+
----------
78+
ds : xarray.Dataset
79+
80+
Returns
81+
-------
82+
updated_ds
83+
"""
84+
85+
# The same operation works on a DataSet
86+
ds = da_with_cherab_grid(ds)
87+
88+
# Add the Cherab grid as an attribute to all variables
89+
grid = ds.attrs["cherab_grid"]
90+
for var in ds.data_vars:
91+
ds[var].attrs.update(cherab_grid=grid)
92+
93+
return ds

0 commit comments

Comments
 (0)