Skip to content

Field plots

Everything here is a method of array.plasma.plot, where array is a labeled xarray.DataArray produced by Struphy’s Output.evaluate(...). Full signatures are in the Plotting reference.

field.plasma.plot.lineout(x="eta1", t=-1, eta2=0.5)

Selects every dimension except x (via keyword selection) and plots the remaining 1-D profile.

Lineout of a scalar field along one axis

Several profiles in one axes, e.g. at a few times, against the minor radius:

phi.plasma.plot.profiles(
x="eta1",
at=[0, 100, 200, 299],
x_of=lambda eta1: 0.1 + 0.9 * eta1,
eta2=0.0,
eta3=0.0,
)

at picks the times (integers are positions, floats nearest values; the default is four evenly spaced ones), and over names another dimension to sweep instead of t.

x_of maps the coordinate for one plot. To work in physical units throughout, replace the coordinate itself: analysis.map_coordinate takes a function of it (or a factor, for a length), and a new name and units, so that selections and every plot use them:

r_T = T.plasma.analysis.map_coordinate(
"eta1", lambda eta1: 0.1 + 0.9 * eta1, name="r", units="m"
)
r_T.plasma.plot.profiles(x="r", at=[0, 20, 40])
# a 2 m long interval
T.plasma.analysis.map_coordinate("eta1", 2.0, name="x", units="m")

Temperature profiles over x in meters

Radial profiles at four times

lineout and profiles take a reference: the exact or expected profile, drawn dashed in the same color as its profile. It can be a function of the plotted x and of t (the profile’s time), an (x, y) pair, or a dict of labels to these. For a diffusing temperature against the heat kernel:

def exact(x, t, D=0.01, width=0.05):
s2 = width**2 + 2 * D * t
return width / np.sqrt(s2) * np.exp(-((x - 0.5) ** 2) / (2 * s2))
T.plasma.plot.profiles(
x="eta1", at=[0, 10, 20, 40], reference={"exact": exact}
)
T.plasma.plot.lineout(
x="eta1", t=-1, reference=exact, x_of=lambda eta1: L * eta1
)

Here the run’s diffusion coefficient is 10 % too large, so its profiles fall below the exact ones:

Temperature profiles and the exact solution at four times

line_animation animates a 1-D profile over time with the exact profile of each frame. The value axis stays fixed over the whole animation:

anim = T.plasma.plot.line_animation(reference={"exact": exact}, step=2)
anim.save("heat.gif", writer="pillow")

A profile animated with the exact solution

alongside adds panels below the profile that run in sync with it: another profile over the same sweep (e.g. the density next to the velocity), or time series, drawn whole with a marker at each frame’s time (a list of series shares one panel; alongside_logy=True puts them on log axes). Time series may be on another time grid, e.g. scalars saved every step; each frame marks the nearest time. Here the peak temperature of the run below its profile, against the exact one:

peak = T.max("eta1")
anim = T.plasma.plot.line_animation(
reference={"exact": exact}, alongside=[[peak, exact_peak]], step=2
)

A profile animated with its peak temperature below, against the exact peak

max_frames=100 keeps at most that many evenly spaced frames (the first and the last included), as for the 2-D animations.

The same plot with Plotly

The same call with backend="plotly": an interactive figure with the same data, fits and labels (see Interactive plots with Plotly).

T.plasma.plot.line_animation(
reference={"exact": exact},
alongside=[[peak, exact_peak]],
step=2,
backend="plotly",
)

Loading interactive chart…

For the error itself, see errors against exact solutions.

plot.view(...) configures a reusable 2-D slice — pick the two axes to plot (x, y) and which dimension to sweep over (sweep, default "t"):

view = field.plasma.plot.view(x="eta1", y="eta2", sweep="t")
view.slice(t=-1) # one snapshot
view.panels(nrows=3, ncols=4) # a grid of snapshots
view.viewer() # interactive slider (Jupyter/IPython)
view.animation(interval=100) # matplotlib.animation.FuncAnimation
view.save_frames("frames/", step=2, prefix="phi")

plot.slice(...), plot.panels(...), plot.viewer(...), plot.animation(...) and plot.frames(...) are shortcuts that build a view and immediately render it, taking the same keyword arguments as view(...).

A single 2-D slice snapshot

A grid of snapshots swept over time

The data behind this plot: field.plasma.data.slice(x="eta1", y="eta2", t=-1) returns the selected 2-D array itself, ordered (x, y), with its coordinates and units, for your own analysis or another plotting library (see Selecting data).

The same plot with Plotly

The same call with backend="plotly": an interactive figure with the same data, fits and labels (see Interactive plots with Plotly).

view.slice(t=-1, backend="plotly")

Loading interactive chart…

plot.animation(...) returns a matplotlib.animation.FuncAnimation; save it with anim.save("orbit.gif", writer="pillow"):

An animation swept over time, saved as a GIF

Every slice presentation takes symmetric=True, which centers the color limits on zero, as a diverging colormap such as "RdBu_r" for a perturbation needs. robust=True takes the limits from the 1st and 99th percentiles, so a few extreme values do not wash out the rest. The 3-D views take the same two options.

view = u.plasma.plot.view(
x="eta1",
y="eta2",
coords="physical",
plane="RZ",
cmap="RdBu_r",
symmetric=True,
eta3=0,
)
view.animation()

Physical slices of Struphy’s cell-centered fields close the seam of periodic directions (e.g. θ = 0) automatically, so a poloidal cross-section has no gap.

The axis and color bar labels come from the coordinates’ and the array’s names and units; xlabel=, ylabel= and colorbar_label= replace them, in every slice presentation:

f_v.plasma.plot.slice(
x="t", y="v1", xlabel="time [ms]", ylabel="v", colorbar_label="f(v, t)"
)

levels draws contour lines of the field on top of any slice presentation: slice, panels, viewer, animation and frames. It takes a number of evenly spaced levels or explicit values. Use it for an interface, flux surfaces, or the streamlines of a stream function. With fill=False, only the lines are drawn, colored by cmap.

# the ring's edges
n.plasma.plot.slice(coords="physical", plane="XY", t=-1, eta3=0, levels=[0.2])
psi.plasma.plot.slice(levels=12, fill=False, cmap="Greys") # streamlines only
# the interface in motion
n.plasma.plot.animation(coords="physical", plane="XY", eta3=0, levels=[0.2])

Charge density of a ring with the interface n = 0.2 as a contour line

overlays draws more on top of any slice presentation, as a dict:

Key Draws
contours_of contour lines of a second field, e.g. the flux function over the current; with contour_levels and contour_color
boundary=True the outline of the grid, e.g. the wall of a mapped domain; with boundary_color
grid_lines=n every n-th grid line, to show the mesh or the mapping
lines labels to (x, y) pairs or functions y(x), e.g. characteristics on a space-time map
points labels to (x, y), e.g. O- and X-points

Lines and points are white by default, for dark colormaps; set line_color and point_color otherwise. In an animation, a contours_of field with a t dimension follows the frames.

Magnetic islands: |B| with the flux surfaces from flux_function(), and the O- and X-points:

flux = B.plasma.analysis.flux_function()
absB.plasma.plot.slice(
coords="physical",
plane="XY",
eta3=0,
cmap="magma",
overlays={
"contours_of": flux,
"contour_levels": 14,
"contour_color": "w",
"boundary": True,
"points": {
"O-point": (np.pi, np.pi),
"X-points": ([0, 2 * np.pi], [np.pi, np.pi]),
},
},
)

|B| with flux surfaces, an O-point and X-points

A wave packet on a space-time map (x="eta1", y="t"), with the line x = x0 + v_g t it should follow:

phi.plasma.plot.slice(
x="eta1",
y="t",
cmap="RdBu_r",
symmetric=True,
overlays={
"lines": {"group velocity 0.4": lambda x: (x - 0.2) / 0.4},
"line_color": "k",
},
)

A wave packet on a space-time map with its group-velocity line

animation(alongside=[...]) animates further fields next to the first one, frame by frame in sync. Each field keeps its own color limits and color bar. The selection and options apply to all of them:

omega.plasma.plot.animation(
coords="physical",
plane="XY",
eta3=0,
alongside=[density],
cmap="RdBu_r",
symmetric=True,
robust=True,
)

Vorticity and density animated side by side

b_field.plasma.plot.vector(x="eta1", y="eta2", components=(0, 1), stride=4)

A quiver plot of two vector components

The data behind this plot: b_field.plasma.data.vector(x="eta1", y="eta2", components=(0, 1), stride=4) returns the selected, strided components (see Selecting data).

The same plot with Plotly

The same call with backend="plotly": an interactive figure with the same data, fits and labels (see Interactive plots with Plotly).

b_field.plasma.plot.vector(
x="eta1", y="eta2", components=(0, 1), stride=4, backend="plotly"
)

Loading interactive chart…

Three orthogonal midpoint slices need nothing extra:

density.plasma.plot.volume_slices()

Three orthogonal slices through a 3-D scalar field

The data behind this plot: density.plasma.data.volume_slices() returns the three planes as a dict keyed by the fixed dimension (see Selecting data).

The same plot with Plotly

The same call with backend="plotly": an interactive figure with the same data, fits and labels (see Interactive plots with Plotly).

density.plasma.plot.volume_slices(backend="plotly")

Loading interactive chart…

A full interactive volume render needs the optional PyVista extra (pip install "plasma-plots[pyvista]"):

plotter = density.plasma.plot.volume(cmap="viridis", opacity="linear")
plotter.show()

A PyVista volume render of a 3-D scalar field

For isosurfaces, cuts through mapped domains, field lines, orbits and movies, in 3-D and 2-D runs, see 3-D views.

field.plasma.plot.compare(reference_field, mode="difference")

See Time series & comparisons for compare() figures — it’s a 1-D lineout under the hood, so it fits either page; we keep the write-up there next to timeseries().