Skip to content

Commit d8b79ee

Browse files
authored
Merge pull request #280 from boutproject/polygon-plots
Polygonal 2D poloidal plots
2 parents d02fe5a + ba35f46 commit d8b79ee

File tree

8 files changed

+242
-19
lines changed

8 files changed

+242
-19
lines changed

xbout/boutdataarray.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,12 @@ def pcolormesh(self, ax=None, **kwargs):
10721072
"""
10731073
return plotfuncs.plot2d_wrapper(self.data, xr.plot.pcolormesh, ax=ax, **kwargs)
10741074

1075+
def polygon(self, ax=None, **kwargs):
1076+
"""
1077+
Colour-plot of a radial-poloidal slice on the R-Z plane using polygons
1078+
"""
1079+
return plotfuncs.plot2d_polygon(self.data, ax=ax, **kwargs)
1080+
10751081
def plot_regions(self, ax=None, **kwargs):
10761082
"""
10771083
Plot the regions into which xBOUT splits radial-poloidal arrays to handle

xbout/geometries.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,14 @@ def add_toroidal_geometry_coords(ds, *, coordinates=None, grid=None):
381381
"total_poloidal_distance",
382382
"zShift",
383383
"zShift_ylow",
384+
"Rxy_corners", # Lower left corners
385+
"Rxy_lower_right_corners",
386+
"Rxy_upper_left_corners",
387+
"Rxy_upper_right_corners",
388+
"Zxy_corners", # Lower left corners
389+
"Zxy_lower_right_corners",
390+
"Zxy_upper_left_corners",
391+
"Zxy_upper_right_corners",
384392
],
385393
)
386394

@@ -420,6 +428,24 @@ def add_toroidal_geometry_coords(ds, *, coordinates=None, grid=None):
420428
else:
421429
ds = ds.set_coords(("Rxy", "Zxy"))
422430

431+
# Add cell corners as coordinates for polygon plotting
432+
if "Rxy_lower_right_corners" in ds:
433+
ds = ds.rename(
434+
Rxy_corners="Rxy_lower_left_corners", Zxy_corners="Zxy_lower_left_corners"
435+
)
436+
ds = ds.set_coords(
437+
(
438+
"Rxy_lower_left_corners",
439+
"Rxy_lower_right_corners",
440+
"Rxy_upper_left_corners",
441+
"Rxy_upper_right_corners",
442+
"Zxy_lower_left_corners",
443+
"Zxy_lower_right_corners",
444+
"Zxy_upper_left_corners",
445+
"Zxy_upper_right_corners",
446+
)
447+
)
448+
423449
# Rename zShift_ylow if it was added from grid file, to be consistent with name if
424450
# it was added from dump file
425451
if "zShift_CELL_YLOW" in ds and "zShift_ylow" in ds:

xbout/plotting/animate.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def animate_poloidal(
9797
cax=None,
9898
animate_over=None,
9999
separatrix=True,
100+
separatrix_kwargs=dict(),
100101
targets=True,
101102
add_limiter_hatching=True,
102103
cmap=None,
@@ -130,6 +131,8 @@ def animate_poloidal(
130131
Dimension over which to animate, defaults to the time dimension
131132
separatrix : bool, optional
132133
Add dashed lines showing separatrices
134+
separatrix_kwargs : dict, optional
135+
Options to pass to the separatrix plotter (e.g. line color)
133136
targets : bool, optional
134137
Draw solid lines at the target surfaces
135138
add_limiter_hatching : bool, optional
@@ -277,7 +280,7 @@ def animate_poloidal(
277280
targets = False
278281

279282
if separatrix:
280-
plot_separatrices(da_regions, ax, x=x, y=y)
283+
plot_separatrices(da_regions, ax, x=x, y=y, **separatrix_kwargs)
281284

282285
if targets:
283286
plot_targets(da_regions, ax, x=x, y=y, hatching=add_limiter_hatching)

xbout/plotting/plotfuncs.py

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections.abc import Sequence
22
import matplotlib
33
import matplotlib.pyplot as plt
4+
from mpl_toolkits.axes_grid1 import make_axes_locatable
45
import numpy as np
56
from pathlib import Path
67
from tempfile import TemporaryDirectory
@@ -752,9 +753,9 @@ def create_or_update_plot(plot_objects=None, tind=None, this_save_as=None):
752753
X, Y, Z, scalars=data, vmin=vmin, vmax=vmax, **kwargs
753754
)
754755
else:
755-
plot_objects[
756-
region_name + str(i)
757-
].mlab_source.scalars = data
756+
plot_objects[region_name + str(i)].mlab_source.scalars = (
757+
data
758+
)
758759

759760
if mayavi_view is not None:
760761
mlab.view(*mayavi_view)
@@ -849,3 +850,181 @@ def animation_func():
849850
plt.show()
850851
else:
851852
raise ValueError(f"Unrecognised plot3d() 'engine' argument: {engine}")
853+
854+
855+
def plot2d_polygon(
856+
da,
857+
ax=None,
858+
cax=None,
859+
cmap="viridis",
860+
norm=None,
861+
logscale=False,
862+
antialias=False,
863+
vmin=None,
864+
vmax=None,
865+
extend="neither",
866+
add_colorbar=True,
867+
colorbar_label=None,
868+
separatrix=True,
869+
separatrix_kwargs={"color": "white", "linestyle": "-", "linewidth": 2},
870+
targets=False,
871+
add_limiter_hatching=False,
872+
grid_only=False,
873+
linewidth=0,
874+
linecolor="black",
875+
):
876+
"""
877+
Nice looking 2D plots which have no visual artifacts around the X-point.
878+
879+
Parameters
880+
----------
881+
da : xarray.DataArray
882+
A 2D (x,y) DataArray of data to plot
883+
ax : Axes, optional
884+
Axes to plot on. If not provided, will make its own.
885+
cax : Axes, optional
886+
Axes to plot colorbar on. If not provided, will plot on the same axes as the plot.
887+
cmap : str or matplotlib.colors.Colormap, default "viridis"
888+
Colormap to use for the plot
889+
norm : matplotlib.colors.Normalize, optional
890+
Normalization to use for the color scale
891+
logscale : bool, default False
892+
If True, use a symlog color scale
893+
antialias : bool, default False
894+
Enables antialiasing. Note: this also shows mesh cell edges - it's unclear how to disable this.
895+
vmin : float, optional
896+
Minimum value for the color scale
897+
vmax : float, optional
898+
Maximum value for the color scale
899+
extend : str, optional, default "neither"
900+
Extend the colorbar. Options are "neither", "both", "min", "max"
901+
add_colorbar : bool, default True
902+
Enable colorbar in figure?
903+
colorbar_label : str, optional
904+
Label for the colorbar
905+
separatrix : bool, default True
906+
Add lines showing separatrices
907+
separatrix_kwargs : dict
908+
Keyword arguments to pass custom style to the separatrices plot
909+
targets : bool, default True
910+
Draw solid lines at the target surfaces
911+
add_limiter_hatching : bool, default True
912+
Draw hatched areas at the targets
913+
grid_only : bool, default False
914+
Only plot the grid, not the data. This sets all the polygons to have a white face.
915+
linewidth : float, default 0
916+
Width of the gridlines on cell edges
917+
linecolor : str, default "black"
918+
Color of the gridlines on cell edges
919+
"""
920+
921+
if ax is None:
922+
fig, ax = plt.subplots(figsize=(3, 6), dpi=120)
923+
else:
924+
fig = ax.get_figure()
925+
926+
if cax is None:
927+
cax = ax
928+
929+
if vmin is None:
930+
vmin = np.nanmin(da.values)
931+
932+
if vmax is None:
933+
vmax = np.nanmax(da.max().values)
934+
935+
if colorbar_label == None:
936+
if "short_name" in da.attrs:
937+
colorbar_label = da.attrs["short_name"]
938+
elif da.name != None:
939+
colorbar_label = da.name
940+
else:
941+
colorbar_label = ""
942+
943+
if "units" in da.attrs:
944+
colorbar_label += f" [{da.attrs['units']}]"
945+
946+
if "Rxy_lower_right_corners" in da.coords:
947+
r_nodes = [
948+
"R",
949+
"Rxy_lower_left_corners",
950+
"Rxy_lower_right_corners",
951+
"Rxy_upper_left_corners",
952+
"Rxy_upper_right_corners",
953+
]
954+
z_nodes = [
955+
"Z",
956+
"Zxy_lower_left_corners",
957+
"Zxy_lower_right_corners",
958+
"Zxy_upper_left_corners",
959+
"Zxy_upper_right_corners",
960+
]
961+
cell_r = np.concatenate(
962+
[np.expand_dims(da[x], axis=2) for x in r_nodes], axis=2
963+
)
964+
cell_z = np.concatenate(
965+
[np.expand_dims(da[x], axis=2) for x in z_nodes], axis=2
966+
)
967+
else:
968+
raise Exception("Cell corners not present in mesh, cannot do polygon plot")
969+
970+
Nx = len(cell_r)
971+
Ny = len(cell_r[0])
972+
patches = []
973+
974+
# https://matplotlib.org/2.0.2/examples/api/patch_collection.html
975+
976+
idx = [np.array([1, 2, 4, 3, 1])]
977+
patches = []
978+
for i in range(Nx):
979+
for j in range(Ny):
980+
p = matplotlib.patches.Polygon(
981+
np.concatenate((cell_r[i][j][tuple(idx)], cell_z[i][j][tuple(idx)]))
982+
.reshape(2, 5)
983+
.T,
984+
fill=False,
985+
closed=True,
986+
facecolor=None,
987+
)
988+
patches.append(p)
989+
990+
norm = _create_norm(logscale, norm, vmin, vmax)
991+
992+
if grid_only is True:
993+
cmap = matplotlib.colors.ListedColormap(["white"])
994+
colors = da.data.flatten()
995+
polys = matplotlib.collections.PatchCollection(
996+
patches,
997+
alpha=1,
998+
norm=norm,
999+
cmap=cmap,
1000+
antialiaseds=antialias,
1001+
edgecolors=linecolor,
1002+
linewidths=linewidth,
1003+
joinstyle="bevel",
1004+
)
1005+
1006+
polys.set_array(colors)
1007+
1008+
if add_colorbar:
1009+
# This produces a "foolproof" colorbar which
1010+
# is always the height of the plot
1011+
# From https://joseph-long.com/writing/colorbars/
1012+
divider = make_axes_locatable(ax)
1013+
cax = divider.append_axes("right", size="5%", pad=0.05)
1014+
fig.colorbar(polys, cax=cax, label=colorbar_label, extend=extend)
1015+
cax.grid(which="both", visible=False)
1016+
1017+
ax.add_collection(polys)
1018+
1019+
ax.set_aspect("equal", adjustable="box")
1020+
ax.set_xlabel("R [m]")
1021+
ax.set_ylabel("Z [m]")
1022+
ax.set_ylim(cell_z.min(), cell_z.max())
1023+
ax.set_xlim(cell_r.min(), cell_r.max())
1024+
ax.set_title(da.name)
1025+
1026+
if separatrix:
1027+
plot_separatrices(da, ax, x="R", y="Z", **separatrix_kwargs)
1028+
1029+
if targets:
1030+
plot_targets(da, ax, x="R", y="Z", hatching=add_limiter_hatching)

xbout/plotting/utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ def _is_core_only(da):
7878
return ix1 >= nx and ix2 >= nx
7979

8080

81-
def plot_separatrices(da, ax, *, x="R", y="Z"):
82-
"""Plot separatrices"""
81+
def plot_separatrices(da, ax, *, x="R", y="Z", **kwargs):
82+
"""
83+
Plot separatrices. Kwargs are passed to ax.plot().
84+
"""
8385

8486
if not isinstance(da, dict):
8587
da_regions = _decompose_regions(da)
@@ -116,7 +118,13 @@ def plot_separatrices(da, ax, *, x="R", y="Z"):
116118
y_sep = 0.5 * (
117119
da_region[y].isel(**{xcoord: 0}) + da_inner[y].isel(**{xcoord: -1})
118120
)
119-
ax.plot(x_sep, y_sep, "k--")
121+
default_style = {"color": "black", "linestyle": "--"}
122+
if any(x for x in kwargs if x in ["c", "ls"]):
123+
raise ValueError(
124+
"When passing separatrix plot style kwargs, use 'color' and 'linestyle' instead lf 'c' and 'ls'"
125+
)
126+
style = {**default_style, **kwargs}
127+
ax.plot(x_sep, y_sep, **style)
120128

121129

122130
def plot_targets(da, ax, *, x="R", y="Z", hatching=True):

xbout/tests/test_against_collect.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,4 @@ def test_new_collect_indexing_slice(self, tmp_path_factory):
220220

221221

222222
@pytest.mark.skip
223-
class test_speed_against_old_collect:
224-
...
223+
class test_speed_against_old_collect: ...

xbout/tests/test_load.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,7 @@ def test_combine_along_y(self, tmp_path_factory, bout_xyt_example_files):
472472
xrt.assert_identical(actual, fake)
473473

474474
@pytest.mark.skip
475-
def test_combine_along_t(self):
476-
...
475+
def test_combine_along_t(self): ...
477476

478477
@pytest.mark.parametrize(
479478
"bout_v5,metric_3D", [(False, False), (True, False), (True, True)]
@@ -623,8 +622,7 @@ def test_drop_vars(self, tmp_path_factory, bout_xyt_example_files):
623622
assert "n" in ds.keys()
624623

625624
@pytest.mark.skip
626-
def test_combine_along_tx(self):
627-
...
625+
def test_combine_along_tx(self): ...
628626

629627
def test_restarts(self):
630628
datapath = Path(__file__).parent.joinpath(

xbout/utils.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,16 @@ def _1d_coord_from_spacing(spacing, dim, ds=None, *, origin_at=None):
167167
)
168168

169169
point_to_use = {
170-
spacing.metadata["bout_xdim"]: spacing.metadata.get("MXG", 0)
171-
if spacing.metadata["keep_xboundaries"]
172-
else 0,
173-
spacing.metadata["bout_ydim"]: spacing.metadata.get("MYG", 0)
174-
if spacing.metadata["keep_yboundaries"]
175-
else 0,
170+
spacing.metadata["bout_xdim"]: (
171+
spacing.metadata.get("MXG", 0)
172+
if spacing.metadata["keep_xboundaries"]
173+
else 0
174+
),
175+
spacing.metadata["bout_ydim"]: (
176+
spacing.metadata.get("MYG", 0)
177+
if spacing.metadata["keep_yboundaries"]
178+
else 0
179+
),
176180
spacing.metadata["bout_zdim"]: spacing.metadata.get("MZG", 0),
177181
}
178182

0 commit comments

Comments
 (0)