From 1179b1659e8237eb3f4ce9338fbe8ae6438d1256 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 5 Sep 2026 13:37:32 +0300 Subject: [PATCH 001/193] Added outer_boundary_mesh method --- src/struphy/geometry/base.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index 625705b3f..927a19d80 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -1633,6 +1633,41 @@ def create_geometry_mesh( return mesh + def outer_boundary_mesh( + self, + n2: int = 40, + n3: int = 80, + eta1: float = 1.0, + ): + """Sample the domain's boundary surface at a fixed radial coordinate. + + Useful for showing the outer flux surface (or, for cube-like + mappings, any fixed-``eta1`` cross-section) as spatial context next + to other data -- e.g. particle trajectories or field quantities -- + without the PyVista/VTK dependency of :meth:`create_geometry_mesh` + and :meth:`export_geometry`. + + Parameters + ---------- + n2 : int + Number of sample points in the second logical coordinate. + n3 : int + Number of sample points in the third logical coordinate. + eta1 : float + The (fixed) first logical coordinate to sample the surface at; + 1.0 (default) gives the outer boundary, 0.0 the magnetic axis + (or inner boundary, for a hollow domain). + + Returns + ------- + x, y, z : numpy.ndarray + Physical coordinates of the sampled surface, each of shape + ``(n2, n3)``. + """ + eta2 = xp.linspace(0.0, 1.0, n2) + eta3 = xp.linspace(0.0, 1.0, n3) + return self(eta1, eta2, eta3, squeeze_out=True) + def show_3d( self, nx: int = 32, From ea3fa55657e3be63d5f439961a2b42e19b726960 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 5 Sep 2026 14:51:47 +0300 Subject: [PATCH 002/193] Clean up equations in vlasov_ampere_one_species.py --- src/struphy/models/vlasov_ampere_one_species.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index a1a9db998..93a506685 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -228,9 +228,9 @@ def doc_pde(cls): .. math:: - \int_{\Omega} \nabla \psi^{\top} \cdot \nabla \phi \, \mathrm{d} \mathbf{x} &= \frac{\alpha^2}{\varepsilon} \int_{\Omega} \int_{\mathbb{R}^3} \psi \, (f - f_0) \, \mathrm{d}^3 \mathbf{v} \, \mathrm{d} \mathbf{x} \qquad \forall \ \psi \in H^1 + \int_{\Omega} \nabla \psi^{\top} \cdot \nabla \phi \, \mathrm{d} \mathbf{x} = \frac{\alpha^2}{\varepsilon} \int_{\Omega} \int_{\mathbb{R}^3} \psi \, (f - f_0) \, \mathrm{d}^3 \mathbf{v} \, \mathrm{d} \mathbf{x} \qquad \forall \ \psi \in H^1 \\[2mm] - \mathbf{E}(t=0) &= -\nabla \phi(t=0) + \mathbf{E}(t=0) = -\nabla \phi(t=0) """ @classmethod From 2fe9a03e3d42a876cdd60b769557ab038a37d8f1 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 5 Sep 2026 16:29:20 +0300 Subject: [PATCH 003/193] Added a few helpers --- .../two_stream/pproc_two_stream.py | 122 +-- src/struphy/diagnostics/plotting.py | 711 ++++++++++++++++++ src/struphy/post_processing/arrays.py | 337 +++++++++ .../post_processing/post_processing_tools.py | 200 ++++- 4 files changed, 1260 insertions(+), 110 deletions(-) create mode 100644 src/struphy/diagnostics/plotting.py create mode 100644 src/struphy/post_processing/arrays.py diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 9c9dbbb13..9df602b90 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,111 +1,41 @@ +import cunumpy as xp import params_two_stream as params -import os -import h5py -from feectools.ddm.mpi import mpi as MPI -from matplotlib import pyplot as plt -from struphy.physics.physics import Units -from struphy import PostProcessor, PlottingData +from struphy import PlottingData, PostProcessor +from struphy.diagnostics.plotting import PanelGridPlot, SliderPlot, TimeSeriesPlot +from struphy.post_processing.arrays import StruphyArray def main(): - ### Electric field progression ### - # get parameters - dt = params.time_opts.dt - algo = params.time_opts.split_algo - num_elements = params.grid.num_elements - degree = params.derham_opts.degree + PostProcessor(sim=params.sim).process(force=False) - env = params.env - ppc = params.loading_params.ppc - - # get units - units = Units(params.base_units) - model = params.model - model.units = units - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species.charge_number - model.units.derive_units( - velocity_scale=model.velocity_scale, - A_bulk=A_bulk, - Z_bulk=Z_bulk, - ) - unit_t = model.units.t - - #analytical solution - m, b = 0.2845/unit_t, -5.3 # 0.2845 is determined from m/c time unit - analytical = lambda x, m=m, b=b: 10**(m*x+b) - - # get scalar data (post processing not needed for scalar data) - if MPI.COMM_WORLD.Get_rank() == 0: - pa_data = os.path.join(env.path_out, "data") - with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: - time = f["time"]["value"][()]*unit_t - E = f["scalar"]["electric_energy"][()] - - # plot - plt.figure(figsize=(18, 12)) - plt.plot(time, E, label="numerical") - plt.plot(time, analytical(time), label = fr"10^({m:.2e}·x {'+' if b > 0 else '-'} {abs(b):.2})", linestyle = "--", color = "black") - plt.yscale("log") - plt.legend() - plt.title(f"{dt=}, {algo=}, {num_elements=}, {degree=}, {ppc=}") - plt.xlabel("time [s]") - plt.ylabel("electric energy $E^2/2$ [a.u.]") - - plt.show() - - ### Binning distribution progression ### - # post process raw data - path = os.path.join(os.getcwd(), "sim_data") - pp = PostProcessor(sim=params.sim) - pp.process() - - # get sim data pdata = PlottingData(sim=params.sim) pdata.load() - # plot in e1-v1 - e1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_e1 - v1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_v1 + # electric field growth against the analytical rate (0.2845 in units of m/c) + energy = pdata.scalars["electric_energy"] + t = energy.coord("t") + analytical = StruphyArray( + 10 ** (0.2845 / pdata.units.t * t - 5.3), + dims=("t",), + coords={"t": t}, + label="analytical", + ).with_coord_units(t="s") + + TimeSeriesPlot( + [energy, analytical], + params=pdata.params, + title="Electric energy", + ).show() - nrows = 3 - ncols = 4 - ntime = len(pdata.f.kinetic_ions.e1_v1_density.f_binned) - time_indices = [int( i/(nrows*ncols-1) * (ntime - 1) ) for i in range(nrows*ncols)] + # phase space evolution + f = pdata.f.kinetic_ions["e1_v1_density"]["f_binned"] - fig, axs = plt.subplots(nrows = nrows, ncols = ncols, figsize = (14,10), sharex=True, sharey=True) - for i in range(nrows): - for j in range(ncols): - ax_maxwellian = axs[i][j] - time_idx = time_indices[j + i*ncols] + PanelGridPlot(f, nrows=3, ncols=4, shared_clim=True, params=pdata.params).show() - #maxwellian distribution plot - color_mapped = pdata.f.kinetic_ions.e1_v1_density.f_binned[time_idx].T - pcm = ax_maxwellian.pcolor(e1_bins,v1_bins, color_mapped) + # interactive alternative to dumping a frame sequence + SliderPlot(f, equal_aspect=False, params=pdata.params).show() - ax_maxwellian.set_xlabel(r"$\eta_1$") - ax_maxwellian.set_ylabel(r"$v_x$") - ax_maxwellian.set_title(fr"full-$f$ at t = {pdata.t_grid[time_idx]*unit_t:4.2e} s") - fig.colorbar(pcm, ax = ax_maxwellian) - - plt.tight_layout() - plt.show() - save_video_pngs = False - if save_video_pngs: - # create .png for video - jump = 2 - fig = plt.figure(figsize=(8, 8)) - for n in range(ntime): - if n % jump == 0: - color_mapped = pdata.f.kinetic_ions.e1_v1_density.f_binned[n].T - plt.pcolor(e1_bins, v1_bins, color_mapped) - - plt.xlabel("position [a.u.]") - plt.ylabel("velocity [a.u.]") - plt.title(fr"full-$f$ at t = {pdata.t_grid[n]*unit_t:4.2e} s") - plt.savefig(f"video/fig_{n:04.0f}.png", transparent=False, bbox_inches='tight', pad_inches=0) - if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py new file mode 100644 index 000000000..8e003ff14 --- /dev/null +++ b/src/struphy/diagnostics/plotting.py @@ -0,0 +1,711 @@ +"""Standardized plots for post-processed Struphy output. + +Every plotter accepts a :class:`~struphy.post_processing.arrays.StruphyArray` and +derives its axis labels, coordinates and units from it, so a correct labeled figure +needs no further arguments. +""" + +import cunumpy as xp +from matplotlib import pyplot as plt +from matplotlib.widgets import Slider + +from struphy.post_processing.arrays import StruphyArray, orbit_columns + +#: rcParams applied by every plotter, so figures from different scripts match. +STRUPHY_STYLE = { + "figure.figsize": (8.0, 5.0), + "figure.dpi": 110, + "axes.grid": True, + "grid.alpha": 0.3, + "axes.titlesize": "medium", + "legend.frameon": False, + "image.cmap": "viridis", +} + + +def growth_rate(y: StruphyArray, *, t0: float = None, t1: float = None, of_sqrt: bool = False): + """Fit an exponential ``exp(gamma*t + b)`` over a time window. + + Parameters + ---------- + y : StruphyArray + Signal with a ``t`` dimension. Non-positive and non-finite samples are excluded. + t0, t1 : float, optional + Window bounds. Default to the full range. + of_sqrt : bool + Fit the growth rate of ``sqrt(y)`` rather than of ``y``. Use this for a + quadratic quantity such as an energy whose amplitude growth rate is wanted. + + Returns + ------- + gamma, b, window : float, float, slice + ``None`` in place of all three if fewer than two usable samples remain. + """ + t = xp.asarray(y.coord("t")) + vals = xp.asarray(y) + + lo = float(t[0]) if t0 is None else float(t0) + hi = float(t[-1]) if t1 is None else float(t1) + lo, hi = sorted((lo, hi)) + + mask = (t >= lo) & (t <= hi) & xp.isfinite(vals) & (vals > 0.0) + if xp.count_nonzero(mask) < 2: + mask = xp.isfinite(vals) & (vals > 0.0) + if xp.count_nonzero(mask) < 2: + return None, None, None + + idx = xp.nonzero(mask)[0] + window = slice(int(idx[0]), int(idx[-1]) + 1) + + signal = xp.log(xp.sqrt(vals[window])) if of_sqrt else xp.log(vals[window]) + gamma, b = xp.polyfit(t[window], signal, 1) + return float(gamma), float(b), window + + +def match_to_grid(values, xgrid): + """Return ``values`` oriented to match ``xgrid``, transposing if that is what fits.""" + values = xp.asarray(values) + if values.shape == xgrid.shape: + return values + if values.T.shape == xgrid.shape: + return values.T + raise ValueError(f"cannot match data shape {values.shape} to grid shape {xgrid.shape}") + + +def physical_grids(data: StruphyArray, domain, *, axes: str = "XY", fixed_eta=(0.5, 0.0, 0.0)): + """Map the two logical dimensions of ``data`` through ``domain`` to physical coordinates. + + Parameters + ---------- + data : StruphyArray + Must have exactly two ``e`` dimensions. + domain : Domain + Struphy domain, called as ``domain(eta1, eta2, eta3, squeeze_out=True)``. + axes : str + Which physical plane to return: ``"XY"``, ``"RZ"``, ``"XZ"`` or ``"YZ"``. + fixed_eta : tuple + Logical position along the dimension that is not binned. + + Returns + ------- + xgrid, ygrid, xlabel, ylabel + """ + logical = [d for d in data.dims if d.startswith("e") and d[1:].isdigit()] + if len(logical) != 2: + raise ValueError(f"expected two logical dims, got {logical} from dims {data.dims}") + + nums = [int(d[1]) for d in logical] + etas = [data.coord(logical[nums.index(ax)]) if ax in nums else fixed_eta[ax - 1] for ax in (1, 2, 3)] + + if axes not in PLANES: + raise ValueError(f"unknown axes {axes!r}, expected one of {sorted(PLANES)}") + + x, y, z = domain(*etas, squeeze_out=True) + fx, fy, xlabel, ylabel = PLANES[axes] + return fx(x, y, z), fy(x, y, z), xlabel, ylabel + + +def logical_grids(data: StruphyArray): + """Meshgrid of the two plotted dimensions of ``data``, for plotting without a domain map. + + The plotted dimensions are whatever remains after ``t``, so this covers phase-space + slices such as ``(e1, v1)`` as well as purely spatial ones. + """ + plotted = [d for d in data.dims if d != "t"] + if len(plotted) != 2: + raise ValueError(f"expected two non-time dims, got {plotted} from dims {data.dims}") + g0, g1 = (data.coord(d) for d in plotted) + xgrid, ygrid = xp.meshgrid(g0, g1, indexing="ij") + return xgrid, ygrid, data.axis_label(plotted[0]), data.axis_label(plotted[1]) + + +#: Physical coordinate planes, as a function of the (X, Y, Z) meshgrids. +PLANES = { + "XY": (lambda x, y, z: x, lambda x, y, z: y, "X", "Y"), + "XZ": (lambda x, y, z: x, lambda x, y, z: z, "X", "Z"), + "YZ": (lambda x, y, z: y, lambda x, y, z: z, "Y", "Z"), + "RZ": (lambda x, y, z: xp.sqrt(x**2 + y**2), lambda x, y, z: z, "R", "Z"), +} + + +def field_slice_grids(grids_phy, *, fixed_dim: str = "e3", index: int = 0, plane: str = "XY"): + """Physical grids for a 2D cut through the 3D evaluation grid. + + Companion to ``StruphyArray.isel(**{fixed_dim: index})``: pass the same ``fixed_dim`` + and ``index`` here to get grids matching the sliced field. + + Parameters + ---------- + grids_phy : list + The three 3D physical coordinate arrays from :attr:`PlottingData.grids_phy`. + fixed_dim : str + Logical dimension held constant, ``"e1"``, ``"e2"`` or ``"e3"``. + index : int + Index along ``fixed_dim``. + plane : str + Which physical plane to return, one of :data:`PLANES`. + + Returns + ------- + xgrid, ygrid, xlabel, ylabel + """ + if plane not in PLANES: + raise ValueError(f"unknown plane {plane!r}, expected one of {sorted(PLANES)}") + + axis = {"e1": 0, "e2": 1, "e3": 2} + if fixed_dim not in axis: + raise ValueError(f"fixed_dim must be one of {sorted(axis)}, got {fixed_dim!r}") + + cut = [slice(None)] * 3 + cut[axis[fixed_dim]] = index + x, y, z = (xp.asarray(g)[tuple(cut)] for g in grids_phy) + + fx, fy, xlabel, ylabel = PLANES[plane] + return fx(x, y, z), fy(x, y, z), xlabel, ylabel + + +class StruphyPlot: + """Base for the plotters: owns style, figure creation, titling and output. + + Parameters + ---------- + data : StruphyArray + The quantity to draw. + ax : matplotlib Axes, optional + Draw into an existing axes instead of creating a figure. + title : str, optional + Defaults to the quantity's label. + params : ParamsIn, optional + When given, run settings are appended to the figure as a suptitle. + """ + + #: Slider-bearing subclasses position their axes manually. + tight = True + + def __init__(self, data: StruphyArray, *, ax=None, title: str = None, params=None, **kwargs): + self.data = data + self.title = title if title is not None else (data.label or "") + self.params = params + self.options = kwargs + self._ax = ax + self.fig = None + self.ax = None + + def _make_axes(self, **subplot_kw): + if self._ax is not None: + self.ax = self._ax + self.fig = self._ax.get_figure() + else: + self.fig, self.ax = plt.subplots(**subplot_kw) + return self.fig, self.ax + + def _run_label(self) -> str: + """One-line summary of the run settings, from the output folder's parameters.""" + if self.params is None: + return "" + bits = [] + for obj, attr, name in ( + ("time_opts", "dt", "dt"), + ("time_opts", "split_algo", "algo"), + ("grid", "num_elements", "Nel"), + ("derham_opts", "degree", "p"), + ): + holder = getattr(self.params, obj, None) + value = getattr(holder, attr, None) if holder is not None else None + if value is not None: + bits.append(f"{name}={value}") + return ", ".join(bits) + + def draw(self): + raise NotImplementedError + + def _finish(self): + run = self._run_label() + if run and self.fig is not None and self._ax is None: + self.fig.suptitle(run, fontsize="small") + if self.tight and self.fig is not None and self._ax is None: + self.fig.tight_layout() + return self + + def plot(self): + """Draw into the axes and return self.""" + with plt.rc_context(STRUPHY_STYLE): + self.draw() + self._finish() + return self + + def show(self): + self.plot() + plt.show() + return self + + def save(self, path, **kwargs): + self.plot() + kwargs.setdefault("bbox_inches", "tight") + self.fig.savefig(path, **kwargs) + return self + + +class TimeSeriesPlot(StruphyPlot): + """Scalar quantities against time, optionally log-scaled with a growth-rate fit. + + Parameters + ---------- + data : StruphyArray or sequence of StruphyArray + One or more signals sharing a ``t`` dimension. + logy : bool + Log-scale the ordinate. + fit : bool + Overlay an exponential fit and report the rate in the legend. + fit_window : tuple, optional + ``(t0, t1)`` bounds for the fit. + fit_of_sqrt : bool + Fit the growth rate of the amplitude rather than of the plotted quantity. + """ + + def __init__(self, data, *, logy=True, fit=False, fit_window=None, fit_of_sqrt=False, **kwargs): + series = [data] if isinstance(data, StruphyArray) else list(data) + super().__init__(series[0], **kwargs) + self.series = series + self.logy = logy + self.fit = fit + self.fit_window = fit_window or (None, None) + self.fit_of_sqrt = fit_of_sqrt + self.fit_result = None + + def draw(self): + fig, ax = self._make_axes() + + for s in self.series: + ax.plot(s.coord("t"), xp.asarray(s), label=s.label or None) + + if self.fit: + target = self.series[0] + gamma, b, window = growth_rate( + target, + t0=self.fit_window[0], + t1=self.fit_window[1], + of_sqrt=self.fit_of_sqrt, + ) + self.fit_result = (gamma, b, window) + if gamma is not None: + t_fit = xp.asarray(target.coord("t"))[window] + scale = 2.0 if self.fit_of_sqrt else 1.0 + ax.plot( + t_fit, + xp.exp(scale * (gamma * t_fit + b)), + "--", + color="black", + label=rf"fit: $\gamma$ = {gamma:.4e}", + ) + ax.axvspan(t_fit[0], t_fit[-1], alpha=0.12, color="grey") + + if self.logy: + ax.set_yscale("log") + + ax.set_xlabel(self.data.axis_label("t")) + ax.set_ylabel(self.data.value_label) + ax.set_title(self.title) + if any(s.label for s in self.series) or self.fit: + ax.legend() + + +class Slice2DPlot(StruphyPlot): + """A 2D quantity as a pcolormesh, with the colorbar and orientation handled. + + Parameters + ---------- + data : StruphyArray + Two-dimensional, or higher with the extra dimensions already selected. + grids : tuple, optional + ``(xgrid, ygrid, xlabel, ylabel)`` from :func:`physical_grids` or + :func:`logical_grids`. Defaults to the logical grids of ``data``. + equal_aspect : bool + Force an equal aspect ratio, appropriate for physical coordinates. + """ + + def __init__(self, data, *, grids=None, vmin=None, vmax=None, equal_aspect=False, **kwargs): + super().__init__(data, **kwargs) + self.grids = grids if grids is not None else logical_grids(data) + self.vmin = vmin + self.vmax = vmax + self.equal_aspect = equal_aspect + + def draw(self): + fig, ax = self._make_axes() + xgrid, ygrid, xlabel, ylabel = self.grids + + values = match_to_grid(self.data, xgrid) + pcm = ax.pcolormesh(xgrid, ygrid, values, shading="auto", vmin=self.vmin, vmax=self.vmax) + fig.colorbar(pcm, ax=ax, label=self.data.value_label) + + if self.equal_aspect: + ax.set_aspect("equal", adjustable="box") + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.set_title(self.title) + ax.grid(False) + self.mesh = pcm + + +class PanelGridPlot(StruphyPlot): + """A grid of 2D snapshots at times spread evenly over the run. + + Replaces the hand-rolled ``nrows``/``ncols``/``time_indices`` loop. + + Parameters + ---------- + data : StruphyArray + Must have a ``t`` dimension and two further dimensions. + nrows, ncols : int + Panel layout. ``nrows * ncols`` snapshots are shown. + shared_clim : bool + Use one colour range across all panels, so panels are comparable. + """ + + tight = False + + def __init__(self, data, *, nrows=3, ncols=4, grids=None, shared_clim=False, equal_aspect=False, **kwargs): + super().__init__(data, **kwargs) + self.nrows = nrows + self.ncols = ncols + self.grids = grids + self.shared_clim = shared_clim + self.equal_aspect = equal_aspect + + def draw(self): + n = self.nrows * self.ncols + nt = self.data.shape[self.data.axis("t")] + indices = [int(i / max(n - 1, 1) * (nt - 1)) for i in range(n)] + + t = self.data.coord("t") + snapshots = [self.data.isel(t=i) for i in indices] + grids = self.grids if self.grids is not None else logical_grids(snapshots[0]) + xgrid, ygrid, xlabel, ylabel = grids + + vmin = vmax = None + if self.shared_clim: + vmin = float(min(xp.nanmin(xp.asarray(s)) for s in snapshots)) + vmax = float(max(xp.nanmax(xp.asarray(s)) for s in snapshots)) + + fig, axs = plt.subplots( + nrows=self.nrows, + ncols=self.ncols, + figsize=(3.5 * self.ncols, 2.8 * self.nrows), + sharex=True, + sharey=True, + squeeze=False, + layout="constrained", + ) + self.fig, self.ax = fig, axs + + for panel, (idx, snap) in enumerate(zip(indices, snapshots)): + ax = axs[panel // self.ncols][panel % self.ncols] + pcm = ax.pcolormesh( + xgrid, + ygrid, + match_to_grid(snap, xgrid), + shading="auto", + vmin=vmin, + vmax=vmax, + ) + ax.set_title(f"t = {float(t[idx]):.2e}") + ax.grid(False) + if self.equal_aspect: + ax.set_aspect("equal", adjustable="box") + if not self.shared_clim: + fig.colorbar(pcm, ax=ax) + + for ax in axs[-1]: + ax.set_xlabel(xlabel) + for row in axs: + row[0].set_ylabel(ylabel) + + if self.shared_clim: + fig.colorbar(pcm, ax=list(axs.ravel()), label=self.data.value_label) + + fig.suptitle(" — ".join(filter(None, (self.title, self._run_label())))) + + +class SliderPlot(StruphyPlot): + """A 2D quantity with a time slider, and a second slider for the free axis in 3D. + + The returned object keeps a reference to its sliders; discarding it stops the + widgets from responding. + + Parameters + ---------- + data : StruphyArray + Dimensions ``(t, a, b)`` or ``(t, a, b, c)``; the fourth is swept by the + second slider. + slice_dim : str, optional + Which dimension the second slider steps through. Defaults to the last. + """ + + tight = False + + def __init__(self, data, *, grids=None, slice_dim=None, vmin=None, vmax=None, equal_aspect=True, **kwargs): + super().__init__(data, **kwargs) + self.grids = grids + self.vmin = vmin + self.vmax = vmax + self.equal_aspect = equal_aspect + spatial = [d for d in data.dims if d != "t"] + self.slice_dim = slice_dim if slice_dim is not None else (spatial[-1] if len(spatial) > 2 else None) + self.sliders = [] + + def _frame(self, t_index, slice_index): + frame = self.data.isel(t=t_index) + if self.slice_dim is not None: + frame = frame.isel(**{self.slice_dim: slice_index}) + return frame + + def draw(self): + nt = self.data.shape[self.data.axis("t")] + t = self.data.coord("t") + + n_slice = self.data.shape[self.data.axis(self.slice_dim)] if self.slice_dim else 0 + slice_index = n_slice // 2 if n_slice else 0 + + first = self._frame(0, slice_index) + grids = self.grids if self.grids is not None else logical_grids(first) + xgrid, ygrid, xlabel, ylabel = grids + + fig, ax = self._make_axes() + fig.subplots_adjust(bottom=0.24 if self.slice_dim else 0.18) + + pcm = ax.pcolormesh( + xgrid, + ygrid, + match_to_grid(first, xgrid), + shading="auto", + vmin=self.vmin, + vmax=self.vmax, + ) + cbar = fig.colorbar(pcm, ax=ax, label=self.data.value_label) + if self.equal_aspect: + ax.set_aspect("equal", adjustable="box") + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.set_title(f"{self.title} at t = {float(t[0]):.4e}") + ax.grid(False) + + s_time = Slider(fig.add_axes([0.20, 0.08, 0.60, 0.03]), "time", 0, nt - 1, valinit=0, valstep=1) + self.sliders = [s_time] + s_slice = None + if self.slice_dim: + s_slice = Slider( + fig.add_axes([0.20, 0.03, 0.60, 0.03]), + f"{self.slice_dim} index", + 0, + n_slice - 1, + valinit=slice_index, + valstep=1, + ) + self.sliders.append(s_slice) + + def update(_): + ti = int(s_time.val) + si = int(s_slice.val) if s_slice is not None else 0 + frame = match_to_grid(self._frame(ti, si), xgrid) + + pcm.set_array(frame.ravel()) + if self.vmin is None and self.vmax is None: + pcm.set_clim(float(xp.nanmin(frame)), float(xp.nanmax(frame))) + cbar.update_normal(pcm) + ax.set_title(f"{self.title} at t = {float(t[ti]):.4e}") + fig.canvas.draw_idle() + + for s in self.sliders: + s.on_changed(update) + + self.mesh = pcm + + +class AnimationPlot(StruphyPlot): + """Sweep a 2D quantity over time, as a matplotlib animation or a frame sequence. + + Parameters + ---------- + data : StruphyArray + Dimensions ``(t, a, b)``. + step : int + Keep every ``step``-th time index. + shared_clim : bool + Hold the colour range fixed across frames, so brightness changes are physical. + """ + + tight = False + + def __init__(self, data, *, grids=None, step=1, vmin=None, vmax=None, shared_clim=True, equal_aspect=False, **kwargs): + super().__init__(data, **kwargs) + self.grids = grids + self.step = step + self.vmin = vmin + self.vmax = vmax + self.shared_clim = shared_clim + self.equal_aspect = equal_aspect + + @property + def frames(self): + """Time indices that will be drawn.""" + return range(0, self.data.shape[self.data.axis("t")], self.step) + + def _setup(self): + first = self.data.isel(t=0) + grids = self.grids if self.grids is not None else logical_grids(first) + xgrid, ygrid, xlabel, ylabel = grids + + vmin, vmax = self.vmin, self.vmax + if self.shared_clim and vmin is None and vmax is None: + values = xp.asarray(self.data) + vmin, vmax = float(xp.nanmin(values)), float(xp.nanmax(values)) + + fig, ax = self._make_axes() + pcm = ax.pcolormesh(xgrid, ygrid, match_to_grid(first, xgrid), shading="auto", vmin=vmin, vmax=vmax) + fig.colorbar(pcm, ax=ax, label=self.data.value_label) + if self.equal_aspect: + ax.set_aspect("equal", adjustable="box") + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.grid(False) + return fig, ax, pcm, xgrid + + def _update(self, ax, pcm, xgrid, index): + t = self.data.coord("t") + pcm.set_array(match_to_grid(self.data.isel(t=index), xgrid).ravel()) + ax.set_title(f"{self.title} at t = {float(t[index]):.4e}") + + def draw(self): + fig, ax, pcm, xgrid = self._setup() + self._update(ax, pcm, xgrid, 0) + self.mesh = pcm + + def animate(self, *, interval=100): + """Return a :class:`matplotlib.animation.FuncAnimation` over the frames.""" + from matplotlib.animation import FuncAnimation + + with plt.rc_context(STRUPHY_STYLE): + fig, ax, pcm, xgrid = self._setup() + anim = FuncAnimation( + fig, + lambda i: self._update(ax, pcm, xgrid, i), + frames=list(self.frames), + interval=interval, + blit=False, + ) + self.fig = fig + return anim + + def save_frames(self, directory, *, prefix="frame", dpi=110): + """Write one PNG per frame into ``directory``, creating it if needed. + + Returns the list of paths written. + """ + import os + + os.makedirs(directory, exist_ok=True) + paths = [] + + with plt.rc_context(STRUPHY_STYLE): + fig, ax, pcm, xgrid = self._setup() + for n, index in enumerate(self.frames): + self._update(ax, pcm, xgrid, index) + path = os.path.join(directory, f"{prefix}_{n:04d}.png") + fig.savefig(path, dpi=dpi, bbox_inches="tight") + paths.append(path) + plt.close(fig) + + return paths + + +class MarkerTrajectoryPlot(StruphyPlot): + """Marker positions in 3D over time, coloured by weight, with a time slider. + + Parameters + ---------- + orbits : StruphyArray + Dimensions ``(t, marker, attribute)``; columns 0-2 are position, 6 is weight. + max_markers : int + Cap on the number of markers drawn. + show_paths : bool, optional + Trail each marker's history. Defaults to on for small marker counts. + """ + + tight = False + + def __init__(self, orbits, *, max_markers=200, show_paths=None, **kwargs): + kwargs.setdefault("title", "Marker trajectories") + super().__init__(orbits, **kwargs) + self.max_markers = max_markers + self.show_paths = show_paths if show_paths is not None else max_markers <= 200 + self.sliders = [] + + def draw(self): + orbs = xp.asarray(self.data) + n = min(orbs.shape[1], self.max_markers) + cols = getattr(self.data, "columns", None) or orbit_columns(orbs.shape[-1]) + + x, y, z = (orbs[:, :n, i] for i in range(cols["position"].start, cols["position"].stop)) + w = orbs[:, :n, cols["weight"]] if "weight" in cols else None + nt = x.shape[0] + + fig = plt.figure(figsize=(8, 7)) + ax = fig.add_subplot(111, projection="3d") + self.fig, self.ax = fig, ax + fig.subplots_adjust(bottom=0.18) + + colouring = {"c": w[0], "cmap": "viridis"} if w is not None else {} + scatter = ax.scatter(x[0], y[0], z[0], s=8, **colouring) + lines = [ax.plot(x[:1, j], y[:1, j], z[:1, j], lw=0.8, alpha=0.5)[0] for j in range(n)] if self.show_paths else [] + + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_zlabel("Z") + ax.set_title(f"{self.title} | step 0/{nt - 1}") + if w is not None: + fig.colorbar(scatter, ax=ax, label="marker weight") + + slider = Slider(fig.add_axes([0.18, 0.06, 0.65, 0.03]), "time", 0, nt - 1, valinit=0, valstep=1) + self.sliders = [slider] + + def update(_): + it = int(slider.val) + scatter._offsets3d = (x[it], y[it], z[it]) + if w is not None: + scatter.set_array(w[it]) + for j, line in enumerate(lines): + line.set_data(x[: it + 1, j], y[: it + 1, j]) + line.set_3d_properties(z[: it + 1, j]) + ax.set_title(f"{self.title} | step {it}/{nt - 1}") + fig.canvas.draw_idle() + + slider.on_changed(update) + + +def plot_equilibrium_profile(path_out, *, ax=None): + """Radial profiles of the equilibrium written to ``geometry.vts``.""" + import os + + import pyvista as pv + + equil = pv.read(os.path.join(path_out, "geometry.vts")) + dims = equil.dimensions + grid = xp.reshape(equil.points, dims + (3,)) + r = xp.sqrt(grid[:, :, :, 0] ** 2 + grid[:, :, :, 1] ** 2) + p0 = xp.reshape(equil.point_data["p0"], dims) + + with plt.rc_context(STRUPHY_STYLE): + if ax is None: + fig, ax = plt.subplots() + ax.plot(r[0, 0, :], p0[0, 0, :], label=r"$p_0$") + + if "n0" in equil.point_data: + n0 = xp.reshape(equil.point_data["n0"], dims) + ax.plot(r[0, 0, :], n0[0, 0, :], label=r"$n_0$") + ax.plot(r[0, 0, :], p0[0, 0, :] / n0[0, 0, :], label=r"$T_0$") + + ax.set_xlabel(r"$R$") + ax.set_title("Radial equilibrium profiles") + ax.legend() + return ax diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py new file mode 100644 index 000000000..4e9ef4bfb --- /dev/null +++ b/src/struphy/post_processing/arrays.py @@ -0,0 +1,337 @@ +"""Labeled arrays for post-processed Struphy output data.""" + +from dataclasses import dataclass, field, replace + +import cunumpy as xp + +#: LaTeX display labels for the dimension names used across Struphy output. +DIM_LABELS = { + "t": r"$t$", + "e1": r"$\eta_1$", + "e2": r"$\eta_2$", + "e3": r"$\eta_3$", + "v1": r"$v_1$", + "v2": r"$v_2$", + "v3": r"$v_3$", + "x": r"$x$", + "y": r"$y$", + "z": r"$z$", + "R": r"$R$", + "Z": r"$Z$", + "comp": "component", + "marker": "marker", +} + +#: Display labels for the binned quantities written by the post-processor. +BINNED_LABELS = { + "f_binned": "$f$", + "delta_f_binned": r"$\delta f$", + "n_sph": "$n$", +} + +#: Physical unit attribute on ``Units`` that each dimension is measured in. +DIM_UNITS = { + "t": "t", + "e1": None, + "e2": None, + "e3": None, + "v1": "v", + "v2": "v", + "v3": "v", + "x": "x", + "y": "x", + "z": "x", + "R": "x", + "Z": "x", +} + + +@dataclass +class StruphyArray: + """Array of simulation output together with its dimension names, coordinates and unit. + + Passing an instance to ``xp.asarray`` or to a matplotlib call yields ``values``, + so it can be used anywhere a plain array is expected. + + Parameters + ---------- + values : xp.ndarray + The data. Its rank must match the length of ``dims``. + dims : tuple of str + Name of each axis, e.g. ``("t", "e1", "v1")``. + coords : dict + Maps a dimension name to its 1D coordinate array. Dimensions may be absent, + in which case they are indexed by position only. + unit : str + Physical unit of ``values``, for axis labels. Empty means arbitrary units. + label : str + Display name of the quantity, e.g. ``r"$f$"``. + """ + + values: xp.ndarray + dims: tuple[str, ...] + coords: dict[str, xp.ndarray] = field(default_factory=dict) + unit: str = "" + label: str = "" + + def __post_init__(self): + self.values = xp.asarray(self.values) + self.dims = tuple(self.dims) + + if self.values.ndim != len(self.dims): + raise ValueError(f"values has rank {self.values.ndim} but {len(self.dims)} dims were given: {self.dims}") + + self.coords = {k: xp.asarray(v) for k, v in self.coords.items()} + for name, c in self.coords.items(): + if name not in self.dims: + raise ValueError(f"coord {name!r} is not one of the dims {self.dims}") + if c.shape != (self.values.shape[self.axis(name)],): + raise ValueError( + f"coord {name!r} has shape {c.shape}, expected ({self.values.shape[self.axis(name)]},)" + ) + + def __array__(self, dtype=None): + return xp.asarray(self.values, dtype=dtype) + + def __len__(self): + return len(self.values) + + def __getitem__(self, key): + """Positional indexing, returning a plain array. + + Kept so that code written against the unlabeled arrays still works; use + :meth:`isel` or :meth:`at` to index by dimension name and keep the labels. + """ + return self.values[key] + + @property + def shape(self): + return self.values.shape + + @property + def ndim(self): + return self.values.ndim + + def axis(self, dim: str) -> int: + """Position of ``dim`` in ``dims``.""" + if dim not in self.dims: + raise KeyError(f"no dim {dim!r} in {self.dims}") + return self.dims.index(dim) + + def coord(self, dim: str) -> xp.ndarray: + """Coordinate array of ``dim``, falling back to an integer index range.""" + if dim in self.coords: + return self.coords[dim] + return xp.arange(self.shape[self.axis(dim)]) + + def axis_label(self, dim: str) -> str: + """Axis label for ``dim``, including its unit where one is known.""" + base = DIM_LABELS.get(dim, dim) + unit = self.coord_units.get(dim, "") + return f"{base} [{unit}]" if unit else base + + @property + def value_label(self) -> str: + """Axis or colorbar label for the values themselves.""" + base = self.label or "" + unit = self.unit or "a.u." + return f"{base} [{unit}]" if base else f"[{unit}]" + + @property + def coord_units(self) -> dict[str, str]: + """Unit string per dimension, populated by the loader where units are known.""" + return getattr(self, "_coord_units", {}) + + def with_coord_units(self, **units: str) -> "StruphyArray": + """Attach unit strings to dimensions, for axis labels.""" + out = replace(self) + out._coord_units = {**self.coord_units, **units} + return out + + def isel(self, **sel: int) -> "StruphyArray": + """Select by integer index along named dimensions. + + A dimension indexed with an ``int`` is dropped; one indexed with a ``slice`` + is kept. + """ + idx = [slice(None)] * self.ndim + for dim, i in sel.items(): + idx[self.axis(dim)] = i + + dropped = {dim for dim, i in sel.items() if not isinstance(i, slice)} + new_dims = tuple(d for d in self.dims if d not in dropped) + new_coords = { + d: (self.coords[d][sel[d]] if d in sel else self.coords[d]) for d in self.coords if d not in dropped + } + + out = StruphyArray( + self.values[tuple(idx)], + new_dims, + new_coords, + self.unit, + self.label, + ) + out._coord_units = dict(self.coord_units) + return out + + def at(self, **sel: float) -> "StruphyArray": + """Select the nearest coordinate value along named dimensions. + + Replaces the ``xp.abs(t_grid - t).argmin()`` idiom. + """ + return self.isel(**{dim: int(xp.abs(self.coord(dim) - v).argmin()) for dim, v in sel.items()}) + + def transpose_to(self, *dims: str) -> "StruphyArray": + """Reorder axes to the given dimension order.""" + if set(dims) != set(self.dims): + raise ValueError(f"cannot transpose dims {self.dims} to {dims}") + + out = StruphyArray( + xp.transpose(self.values, [self.axis(d) for d in dims]), + dims, + self.coords, + self.unit, + self.label, + ) + out._coord_units = dict(self.coord_units) + return out + + def __repr__(self): + dims = ", ".join(f"{d}: {n}" for d, n in zip(self.dims, self.shape)) + name = self.label or "StruphyArray" + return f"<{name} ({dims}) [{self.unit or 'a.u.'}]>" + + +def orbit_columns(n_columns: int) -> dict: + """Meaning of each marker-orbit column for a given saved width. + + The post-processor saves a different set of marker columns depending on the + velocity dimension of the species, so the weight is not always at the same index. + Positions occupy the first three columns and the marker id the last in every case. + + Parameters + ---------- + n_columns : int + Size of the last axis of the orbit array. + + Returns + ------- + dict + Maps ``"position"``, ``"velocity"``, ``"weight"`` and ``"id"`` to an index or + slice. ``weight`` is absent when the species does not save one. + """ + cols = {"position": slice(0, 3), "id": n_columns - 1} + if n_columns == 8: + cols["velocity"] = slice(3, 6) + cols["weight"] = 6 + elif n_columns == 5: + cols["velocity"] = 3 + else: + cols["velocity"] = slice(3, n_columns - 1) + return cols + + +def wrap_orbits(values, t_grid) -> StruphyArray: + """Label a marker-orbit array with its ``(t, marker, attribute)`` dimensions.""" + values = xp.asarray(values) + out = StruphyArray( + values, + dims=("t", "marker", "attribute"), + coords={"t": t_grid}, + label="marker orbits", + ) + out.columns = orbit_columns(values.shape[-1]) + return out + + +def wrap_field_data(data: dict, grids_log=None, *, label: str = "") -> StruphyArray: + """Stack a time-keyed dict of evaluated field components into one labeled array. + + The post-processor stores evaluated fields as ``{time: [component, ...]}`` with each + component on the 3D evaluation grid. This turns that into a single array with + dimensions ``(t, comp, e1, e2, e3)``, dropping ``comp`` for scalar fields. + + Parameters + ---------- + data : dict + Maps time value to a list of component arrays, or to a single array. + grids_log : list, optional + The three 1D logical grids, attached as coordinates when given. + """ + times = sorted(data.keys()) + if not times: + return None + + first = data[times[0]] + scalar = not isinstance(first, (list, tuple)) or len(first) == 1 + + if scalar: + stacked = xp.stack([xp.asarray(data[t] if not isinstance(data[t], (list, tuple)) else data[t][0]) for t in times]) + dims = ("t", "e1", "e2", "e3") + else: + stacked = xp.stack([xp.stack([xp.asarray(c) for c in data[t]]) for t in times]) + dims = ("t", "comp", "e1", "e2", "e3") + + coords = {"t": xp.asarray(times)} + if grids_log is not None: + coords.update({f"e{i + 1}": xp.asarray(g) for i, g in enumerate(grids_log)}) + + # a field evaluated on a subset of directions will not match the full grid + coords = {k: v for k, v in coords.items() if k in dims and len(v) == stacked.shape[dims.index(k)]} + + return StruphyArray(stacked, dims=dims, coords=coords, label=label) + + +def wrap_binned_slice(holder, slice_name: str, t_grid, coord_units: dict = None): + """Replace the binned arrays on ``holder`` with labeled ones. + + A binned slice folder is named after the dimensions it bins, e.g. ``e1_v1_density``, + and holds one ``grid_`` array per dimension alongside the binned quantities + (``f_binned``, ``delta_f_binned``, ...). This pairs them up, so the binned data + carries its coordinates and no caller has to match ``grid_e1`` to axis 0 by hand. + + Parameters + ---------- + holder : Slice + Container whose attributes were just populated from disk. Modified in place. + slice_name : str + Folder name, used to recover the dimension order. + t_grid : array + Time coordinate shared by every binned quantity. + """ + grids = {k[len("grid_") :]: getattr(holder, k) for k in list(vars(holder)) if k.startswith("grid_")} + if not grids: + return holder + + # dimension order follows the slice name, e.g. "e1_v1_density" -> ("e1", "v1") + dims = [part for part in slice_name.split("_") if part in grids] + if len(dims) != len(grids): + return holder + + coords = {d: grids[d] for d in dims} + coords["t"] = t_grid + units = coord_units or {} + + for name in list(vars(holder)): + if name.startswith("grid_"): + continue + values = getattr(holder, name) + if not hasattr(values, "shape"): + continue + + expected = (len(t_grid), *(len(coords[d]) for d in dims)) + if tuple(values.shape) != expected: + continue + + setattr( + holder, + name, + StruphyArray( + values, + dims=("t", *dims), + coords=coords, + label=BINNED_LABELS.get(name, name.replace("_", " ")), + ).with_coord_units(**units), + ) + + return holder diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 3c1662b8f..eaffb151b 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -26,6 +26,12 @@ from struphy.models.species import ParticleSpecies from struphy.models.variables import PICVariable, SPHVariable from struphy.pic.base import Particles +from struphy.post_processing.arrays import ( + StruphyArray, + wrap_binned_slice, + wrap_field_data, + wrap_orbits, +) from struphy.post_processing.orbits import orbits_tools from struphy.topology.grids import TensorProductGrid from struphy.utils.progress import tqdm @@ -39,7 +45,29 @@ PUSH_KINDS = {"H1": "0", "Hcurl": "1", "Hdiv": "2", "L2": "3", "H1vec": "v"} -class SplineValues: +class Container: + """Mapping access over attributes set by the loader, so contents are discoverable.""" + + def keys(self): + return tuple(k for k in self.__dict__ if not k.startswith("_")) + + def __getitem__(self, key): + try: + return self.__dict__[key] + except KeyError: + raise KeyError(f"{key!r} not found, available: {self.keys()}") from None + + def __contains__(self, key): + return key in self.keys() + + def __iter__(self): + return iter(self.keys()) + + def __len__(self): + return len(self.keys()) + + +class SplineValues(Container): def __str__(self): out = "" for name, species in inspect.getmembers(self): @@ -49,7 +77,7 @@ def __str__(self): return out -class Orbits: +class Orbits(Container): def __str__(self): out = "" for species, orbits in self.__dict__.items(): @@ -61,7 +89,7 @@ def __str__(self): return out -class DistributionFunction: +class DistributionFunction(Container): def __str__(self): out = "" for name, species in inspect.getmembers(self): @@ -71,7 +99,7 @@ def __str__(self): return out -class DensitySPH: +class DensitySPH(Container): def __str__(self): out = "" for name, species in inspect.getmembers(self): @@ -81,7 +109,7 @@ def __str__(self): return out -class SpecHolder: +class SpecHolder(Container): def __str__(self): out = "" for name, val in self.__dict__.items(): @@ -89,13 +117,39 @@ def __str__(self): return out -class Slice: +class Slice(Container): pass +class Scalars(Container): + """Time series recorded at every step, read straight from the raw HDF5 output. + + Unlike the other containers this needs no prior call to :meth:`PostProcessor.process`. + """ + + def __str__(self): + out = "" + for name in self.keys(): + out += f" {name}\n" + return out + + class DataDict: - def __init__(self, data: dict): + def __init__(self, data: dict, grids_log=None, name: str = ""): self.data = data + self.grids_log = grids_log + self.name = name + self._array = None + + @property + def array(self) -> StruphyArray: + """The field as one labeled array with dims ``(t, comp, e1, e2, e3)``. + + Built on first access; ``data`` remains the raw time-keyed dict. + """ + if self._array is None: + self._array = wrap_field_data(self.data, self.grids_log, label=self.name) + return self._array def __str__(self): out = f"{type(self.data) = }\n" @@ -285,13 +339,22 @@ def __init__( self.rank = 0 self.range_ranks = range(int(self.comm_size)) - # create or remove output paths + # the directory is only cleared in process(), so that constructing a + # PostProcessor to inspect a run does not destroy its post-processed data if self.rank == 0: - try: - os.mkdir(self.path_pproc) - except: + os.makedirs(self.path_pproc, exist_ok=True) + self.comm.Barrier() + + @property + def is_processed(self) -> bool: + """Whether ``path_pproc`` already holds post-processed output.""" + return os.path.exists(self.path_pproc) and bool(os.listdir(self.path_pproc)) + + def _reset_pproc_dir(self): + if self.rank == 0: + if os.path.exists(self.path_pproc): shutil.rmtree(self.path_pproc) - os.mkdir(self.path_pproc) + os.mkdir(self.path_pproc) self.comm.Barrier() def process( @@ -302,6 +365,7 @@ def process( guiding_center: bool = False, classify: bool = False, create_vtk: bool = True, + force: bool = True, ): """Run post-processing for fields and particle data in ``self.path_out``. @@ -321,7 +385,20 @@ def process( If True, run orbit classification (passing, trapped, lost) after computing orbits. create_vtk : bool If True, create VTK files for visualisation. + force : bool + Reprocess even when output already exists. Set False to reuse a previous + run's results, so a plotting script can be re-run cheaply. + + Returns + ------- + bool + Whether post-processing actually ran. """ + if not force and self.is_processed: + logger.warning(f"\nReusing existing post-processing in {self.path_pproc}") + return False + + self._reset_pproc_dir() logger.warning(f"\nPost-processing path {self.path_out}") # check for fields and kinetic data in hdf5 file that need post processing @@ -370,6 +447,8 @@ def process( classify=classify, ) + return True + def process_fields( self, step: int = 1, @@ -1379,6 +1458,7 @@ def __init__(self, sim: "Simulation" = None, path_out: str = None): else: path_out = sim.env.path_out + self.path_out = path_out self.path_pproc = os.path.join(path_out, "post_processing") assert os.path.exists(self.path_pproc), f"Path {self.path_pproc} does not exist, run 'pproc' first?" @@ -1387,10 +1467,63 @@ def __init__(self, sim: "Simulation" = None, path_out: str = None): self._f = DistributionFunction() self._spline_values = SplineValues() self._n_sph = DensitySPH() + self._scalars = Scalars() + self._params = None + self._units = None self.grids_log: list[xp.ndarray] = None self.grids_phy: list[xp.ndarray] = None self.t_grid: xp.ndarray = None + @property + def params(self) -> ParamsIn: + """Input parameters of the run, read from the output folder on first access. + + Removes the need for a plotting script to import the simulation's ``params_*.py``. + """ + if self._params is None: + self._params = ParamsIn(self.path_out) + return self._params + + @property + def domain(self) -> Domain: + """Domain of the run, for mapping logical to physical coordinates.""" + return self.params.domain + + @property + def units(self): + """Fully derived :class:`~struphy.physics.physics.Units` of the run. + + Replaces the ``Units(base_units)`` / ``derive_units(...)`` sequence that every + plotting script would otherwise repeat. + """ + if self._units is None: + from struphy.physics.physics import Units + + model = self.params.model + units = Units(model.base_units) + bulk = model.bulk_species + units.derive_units( + velocity_scale=model.velocity_scale, + A_bulk=None if bulk is None else bulk.mass_number, + Z_bulk=None if bulk is None else bulk.charge_number, + ) + self._units = units + return self._units + + @property + def scalars(self) -> Scalars: + """Scalar time series recorded every step, keyed by name. + + Each entry is a :class:`~struphy.post_processing.arrays.StruphyArray` over ``t``, + with the time coordinate already converted to seconds. + + Returns + ------- + Scalars + Container supporting ``.keys()``, ``["name"]`` and attribute access. + """ + return self._scalars + @property def orbits(self) -> Orbits: """Particle orbit data by species. @@ -1443,6 +1576,39 @@ def n_sph(self) -> DensitySPH: """ return self._n_sph + def load_scalars(self, *, physical_time: bool = True): + """Read the ``scalar`` group of the raw HDF5 output into :attr:`scalars`. + + Post-processing is not required for these, so this may be called on its own. + + Parameters + ---------- + physical_time : bool + Scale the time coordinate to seconds using the run's units. Set False to + keep Struphy time units. + """ + path_data = os.path.join(self.path_out, "data", "data_proc0.hdf5") + if not os.path.exists(path_data): + logger.warning(f"No raw data at {path_data}, skipping scalars.") + return self._scalars + + unit_t = self.units.t if physical_time else 1.0 + t_unit_label = "s" if physical_time else "a.u." + + with h5py.File(path_data, "r") as f: + t = xp.asarray(f["time"]["value"][()]) * unit_t + for name in f["scalar"].keys(): + arr = StruphyArray( + xp.asarray(f["scalar"][name][()]), + dims=("t",), + coords={"t": t}, + label=name.replace("_", " "), + ).with_coord_units(t=t_unit_label) + setattr(self._scalars, name, arr) + + logger.info(f"Loaded scalars: {self._scalars.keys()}") + return self._scalars + def load(self): """Load all post-processed data from disk into memory. @@ -1464,6 +1630,8 @@ def load(self): # load time grid self.t_grid = xp.load(os.path.join(self.path_pproc, "t_grid.npy")) + self.load_scalars() + # data paths path_fields = os.path.join(self.path_pproc, "fields_data") path_kinetic = os.path.join(self.path_pproc, "kinetic_data") @@ -1491,7 +1659,7 @@ def load(self): var = file.split(".")[0] with open(os.path.join(path_spec, file), "rb") as f: # try: - data_dict = DataDict(pickle.load(f)) + data_dict = DataDict(pickle.load(f), self.grids_log, var) setattr(spec_holder, var, data_dict) # self.arrays[spec][var] = pickle.load(f) @@ -1510,6 +1678,7 @@ def load(self): files = next(sub_wlk)[2] Nt = len(files) // 2 n = 0 + arr = None for file in files: # logger.info(f"{file = }") if ".npy" in file: @@ -1517,9 +1686,10 @@ def load(self): tmp = xp.load(os.path.join(path_dat, file)) if n == 0: arr = xp.zeros((Nt, *tmp.shape), dtype=float) - setattr(self.orbits, spec, arr) arr[step] = tmp n += 1 + if arr is not None: + setattr(self.orbits, spec, wrap_orbits(arr, self.t_grid[:Nt])) elif "distribution_function" in folder: spec_holder = SpecHolder() @@ -1537,6 +1707,7 @@ def load(self): tmp = xp.load(os.path.join(path_dat, sli, file)) logger.info(f"{name = }") setattr(s, name, tmp) + wrap_binned_slice(s, sli, self.t_grid) elif "n_sph" in folder: spec_holder = SpecHolder() @@ -1554,6 +1725,7 @@ def load(self): tmp = xp.load(os.path.join(path_dat, sli, file)) # logger.info(f"{name = }") setattr(s, name, tmp) + wrap_binned_slice(s, sli, self.t_grid) else: logger.info(f"{folder =}") From 0389afb5288d9ecb51942024328db51fe16e507e Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 5 Sep 2026 16:53:41 +0300 Subject: [PATCH 004/193] Added some slider and time series plotters --- .../cyclone/pproc_cyclone.py | 577 ++--------------- .../itg_cylindre/pproc_drift_kinetic.py | 571 ++--------------- .../diocotron_instability/pproc_diocotron.py | 582 +++--------------- src/struphy/diagnostics/plotting.py | 81 ++- src/struphy/diagnostics/tests/__init__.py | 0 .../diagnostics/tests/test_plotting.py | 290 +++++++++ .../post_processing/tests/test_arrays.py | 230 +++++++ .../tests/test_plotting_data.py | 138 +++++ 8 files changed, 904 insertions(+), 1565 deletions(-) create mode 100644 src/struphy/diagnostics/tests/__init__.py create mode 100644 src/struphy/diagnostics/tests/test_plotting.py create mode 100644 src/struphy/post_processing/tests/test_arrays.py create mode 100644 src/struphy/post_processing/tests/test_plotting_data.py diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 25c6004c0..4c8d290ba 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,542 +1,79 @@ -import importlib.util import os import sys -from mpl_toolkits.mplot3d import Axes3D # noqa: F401 -import glob - -import h5py -import pyvista as pv -import cunumpy as xp -from matplotlib import pyplot as plt -from matplotlib.widgets import Slider from struphy import PlottingData, PostProcessor - - -# ============================================================ -# User options -# ============================================================ -FIT_T0 = 0.0 -FIT_T1 = None # None -> last available time -FIT_QUANTITY = "phi_integral" # or "en_phi" if this scalar exists in data_proc0.hdf5 +from struphy.diagnostics.plotting import ( + MarkerTrajectoryPlot, + SliderPlot, + TimeSeriesPlot, + field_slice_grids, + physical_grids, + plot_equilibrium_profile, +) + +# quantity whose exponential growth rate is fitted +FIT_QUANTITY = "phi_integral" +FIT_WINDOW = (0.0, None) SHOW_EQUIL_PROFILE = False -SHOW_DENSITY_SLIDER = True -SHOW_FIELD_SLIDER = True +# binned densities to sweep, as (bin name, quantity, physical plane) DENSITY_PLOTS = [ - { - "bin": "e1_e2_density", - "quantity": "delta_f_binned", - "physical": True, - "axes": "RZ", - "vmin": None, - "vmax": None, - "title": "delta_f (R,Z)", - }, + ("e1_e2_density", "delta_f_binned", "RZ"), ] +# fields to sweep, as (species, field, component, physical plane) FIELD_PLOTS = [ - { - "species": "em_fields", - "field": "phi_phy", - "component": 0, - "axes": "RZT", - "fixed_index": 0, - "vmin": None, - "vmax": None, - "title": "Electric potential phi", - }, - { - "species": "diagnostics", - "field": "rho_phy", - "component": 0, - "axes": "RZT", - "fixed_index": 0, - "vmin": None, - "vmax": None, - "title": "Right-hand side of Poisson equation", - }, - { - "species": "diagnostics", - "field": "rho_phy", - "component": 0, - "axes": "XYZ", - "fixed_index": 0, - "vmin": None, - "vmax": None, - "title": "Right-hand side of Poisson equation", - }, + ("em_fields", "phi_phy", 0, "RZ"), + ("diagnostics", "rho_phy", 0, "RZ"), + ("diagnostics", "rho_phy", 0, "XY"), ] -SAVE_DENSITY_IMAGES = False -SAVE_DENSITY_QUANTITY = "delta_f_binned" -SAVE_DENSITY_EVERY = 1 -SAVE_DENSITY_VMIN = None -SAVE_DENSITY_VMAX = None - - -# ============================================================ -# Small utilities -# ============================================================ -def load_params(sim_path): - spec = importlib.util.spec_from_file_location("params", os.path.join(sim_path, "parameters.py")) - params = importlib.util.module_from_spec(spec) - spec.loader.exec_module(params) - return params - - -def ensure_post_processing(params, sim_path): - if not os.path.isdir(os.path.join(sim_path, "post_processing")): - pp = PostProcessor(sim=params.sim) - pp.process(physical=True) - - -def load_scalar(data_path, quantity): - with h5py.File(os.path.join(data_path, "data_proc0.hdf5"), "r") as f: - time = xp.asarray(f["time"]["value"][()]) - if quantity in f["scalar"]: - y = xp.asarray(f["scalar"][quantity][()]) - elif quantity == "en_phi" and "phi_integral" in f["scalar"]: - y = xp.asarray(f["scalar"]["phi_integral"][()]) - else: - available = list(f["scalar"].keys()) - raise KeyError(f"Scalar {quantity!r} not found. Available scalars: {available}") - return time, y - - -def fit_window(time, y, t0, t1): - """Return safe indices for a log-fit of sqrt(y).""" - if t1 is None: - t1 = float(time[-1]) - - lo, hi = sorted((float(t0), float(t1))) - mask = (time >= lo) & (time <= hi) & xp.isfinite(y) & (y > 0.0) - - # If the user window is unusable, use all positive finite samples. - if xp.count_nonzero(mask) < 2: - mask = xp.isfinite(y) & (y > 0.0) - - # If there are still too few points, disable fit cleanly. - if xp.count_nonzero(mask) < 2: - return None - - idx = xp.nonzero(mask)[0] - return int(idx[0]), int(idx[-1]) + 1 - - -def plot_energy_fit(time, en_phi, t0=FIT_T0, t1=FIT_T1): - window = fit_window(time, en_phi, t0, t1) - - fig, ax = plt.subplots() - ax.plot(time, en_phi, label=FIT_QUANTITY) - ax.set_xlabel("time") - ax.set_ylabel(FIT_QUANTITY) - ax.set_title(f"Evolution of {FIT_QUANTITY}") - - if window is not None: - i0, i1 = window - fit_time = time[i0:i1] - fit_signal = xp.log(xp.sqrt(en_phi[i0:i1])) - gamma, b = xp.polyfit(fit_time, fit_signal, 1) - en_fit = xp.exp(2.0 * (gamma * fit_time + b)) - ax.plot(fit_time, en_fit, "--", label=f"fit: gamma={gamma:.4e}") - ax.axvspan(fit_time[0], fit_time[-1], alpha=0.12) - print(f"{FIT_QUANTITY} fit window: [{fit_time[0]:.6e}, {fit_time[-1]:.6e}]") - print(f"growth rate from log(sqrt({FIT_QUANTITY})): gamma = {gamma:.8e}") - else: - print(f"No valid positive data found for exponential fit of {FIT_QUANTITY}.") - - ax.legend() - fig.tight_layout() - plt.show() - - -def plot_equilibrium_profile(sim_path): - equil_data = pv.read(os.path.join(sim_path, "geometry.vts")) - dims = equil_data.dimensions - grid = xp.reshape(equil_data.points, dims + (3,)) - r = xp.sqrt(grid[:, :, :, 0] ** 2 + grid[:, :, :, 1] ** 2) - p0 = xp.reshape(equil_data.point_data["p0"], dims) - - fig, ax = plt.subplots() - ax.set_title("Radial equilibrium profiles") - ax.set_xlabel("R") - ax.plot(r[0, 0, :], p0[0, 0, :], label="p0") - - if "n0" in equil_data.point_data: - n0 = xp.reshape(equil_data.point_data["n0"], dims) - ax.plot(r[0, 0, :], n0[0, 0, :], label="n0") - ax.plot(r[0, 0, :], p0[0, 0, :] / n0[0, 0, :], label="T0") - - ax.legend() - fig.tight_layout() - plt.show() - - -def match_field_to_grid(field, xgrid): - """Return field with orientation compatible with xgrid when possible.""" - if field.shape == xgrid.shape: - return field - if field.T.shape == xgrid.shape: - return field.T - raise ValueError(f"Cannot match field shape {field.shape} with grid shape {xgrid.shape}.") - - -def get_binned_data(pdata, bin_name, quantity): - return xp.asarray(getattr(getattr(pdata.f.kinetic_ions, bin_name), quantity)) - - -def get_binned_grids( - params, - pdata, - bin_name, - in_physical=True, - plot_axes="RZ", - fixed_eta=(0.5, 0.0, 0.0), -): - bin_data = getattr(pdata.f.kinetic_ions, bin_name) - - bin_axes = [int(part[1]) for part in bin_name.split("_") if part.startswith("e")] - if len(bin_axes) != 2: - raise ValueError(f"Cannot infer two binned axes from bin_name={bin_name!r}") - - g0 = getattr(bin_data, f"grid_e{bin_axes[0]}") - g1 = getattr(bin_data, f"grid_e{bin_axes[1]}") - - if not in_physical: - xgrid, ygrid = xp.meshgrid(g0, g1, indexing="ij") - return xgrid, ygrid, f"eta{bin_axes[0]}", f"eta{bin_axes[1]}" - - etas = [] - for ax in (1, 2, 3): - if ax == bin_axes[0]: - etas.append(g0) - elif ax == bin_axes[1]: - etas.append(g1) - else: - etas.append(fixed_eta[ax - 1]) - - x, y, z = params.domain(*etas, squeeze_out=True) - - if plot_axes == "RZ": - return xp.sqrt(x**2 + y**2), z, "R", "Z" - elif plot_axes == "XY": - return x, y, "X", "Y" - elif plot_axes == "XZ": - return x, z, "X", "Z" - elif plot_axes == "YZ": - return y, z, "Y", "Z" - else: - raise ValueError(f"Unknown plot_axes={plot_axes!r}") - - -def make_slider_plot(time_grid, xgrid, ygrid, data, *, title, xlabel, ylabel, vmin=None, vmax=None): - data0 = match_field_to_grid(xp.asarray(data[0]), xgrid) - - fig, ax = plt.subplots() - fig.subplots_adjust(bottom=0.20) - pcm = ax.pcolormesh(xgrid, ygrid, data0, shading="auto", vmin=vmin, vmax=vmax) - cbar = fig.colorbar(pcm, ax=ax) - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(f"{title} at t = {time_grid[0]:.4e}") - - slider_ax = fig.add_axes([0.20, 0.07, 0.60, 0.03]) - slider = Slider( - slider_ax, - "time index", - 0, - len(time_grid) - 1, - valinit=0, - valstep=1, - ) - - def update(_): - idx = int(slider.val) - field = match_field_to_grid(xp.asarray(data[idx]), xgrid) - pcm.set_array(field.ravel()) - if vmin is None and vmax is None: - pcm.set_clim(float(xp.nanmin(field)), float(xp.nanmax(field))) - cbar.update_normal(pcm) - ax.set_title(f"{title} at t = {time_grid[idx]:.4e}") - fig.canvas.draw_idle() - - slider.on_changed(update) - plt.show() - - -def plot_binned_quantity_slider(params, pdata, *, bin_name, quantity, in_physical=True, axes="RZ", vmin=None, vmax=None, title="density binned"): - data = get_binned_data(pdata, bin_name, quantity) - xgrid, ygrid, xlabel, ylabel = get_binned_grids(params, pdata, bin_name, in_physical=in_physical, plot_axes=axes) - make_slider_plot( - pdata.t_grid, - xgrid, - ygrid, - data, - title=title, - xlabel=xlabel, - ylabel=ylabel, - vmin=vmin, - vmax=vmax, - ) - -def get_field_3d(pdata, species, field, component=0): - field_data = getattr(getattr(pdata.spline_values, species), field) - times = list(field_data.data.keys()) - values = [field_data.data[t][component] for t in times] - return xp.array(times), values +def main(path_out): + PostProcessor(path_out=path_out).process(physical=True, force=False) - -def get_slice_from_field(pdata, arr3d, axes="RZT", fixed_index=0): - X = pdata.grids_phy[0] - Y = pdata.grids_phy[1] - Z = pdata.grids_phy[2] - R = xp.sqrt(X**2 + Y**2) - - if axes == "RZT": - data = arr3d[:, :, fixed_index] - return R[:, :, fixed_index], Z[:, :, fixed_index], data, "R", "Z" - - elif axes == "XYZ": - data = arr3d[:, fixed_index, :] - return X[:, fixed_index, :], Y[:, fixed_index, :], data, "X", "Y" - - elif axes == "RTP": - data = arr3d[:, fixed_index, :] - return R[:, fixed_index, :], Z[:, fixed_index, :], data, "R", "Z" - - else: - raise ValueError(f"Unknown field slice axes={axes!r}") - - -def plot_field_slider( - pdata, - species, - field, - component=0, - axes="RZT", - vmin=None, - vmax=None, - title=None, -): - times, values = get_field_3d(pdata, species, field, component=component) - - nt = len(values) - shape = values[0].shape - - if axes == "RZT": - nslice = shape[2] - slice_label = "toroidal index" - elif axes == "XYZ": - nslice = shape[1] - slice_label = "poloidal/radial slice index" - elif axes == "RTP": - nslice = shape[1] - slice_label = "eta2 index" - else: - raise ValueError(f"Unknown field slice axes={axes!r}") - - fig, ax = plt.subplots() - plt.subplots_adjust(bottom=0.22) - - time_idx = 0 - slice_idx = min(nslice - 1, nslice // 2) - - xg, yg, data, xlabel, ylabel = get_slice_from_field( - pdata, - values[time_idx], - axes=axes, - fixed_index=slice_idx, - ) - - pcm = ax.pcolormesh(xg, yg, data, shading="auto", vmin=vmin, vmax=vmax) - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(title or f"{species}.{field}") - - cbar = fig.colorbar(pcm, ax=ax) - - ax_time = plt.axes([0.15, 0.10, 0.70, 0.03]) - ax_slice = plt.axes([0.15, 0.05, 0.70, 0.03]) - - s_time = Slider(ax_time, "time", 0, nt - 1, valinit=time_idx, valstep=1) - s_slice = Slider(ax_slice, slice_label, 0, nslice - 1, valinit=slice_idx, valstep=1) - - def update(_): - ti = int(s_time.val) - si = int(s_slice.val) - - xg, yg, data, xlabel, ylabel = get_slice_from_field( - pdata, - values[ti], - axes=axes, - fixed_index=si, - ) - - pcm.set_array(data.ravel()) - pcm.set_clim( - vmin if vmin is not None else xp.nanmin(data), - vmax if vmax is not None else xp.nanmax(data), - ) - ax.set_title(f"{title or field} | t = {times[ti]:.4e}, slice = {si}") - fig.canvas.draw_idle() - - s_time.on_changed(update) - s_slice.on_changed(update) - - plt.show() - - - - -def load_marker_data(pdata, species="kinetic_ions", max_markers=200): - orbs = getattr(pdata.orbits, species) - nb_markers = min(orbs.shape[1], max_markers) - return orbs[:, :nb_markers, 0], orbs[:, :nb_markers, 1], orbs[:, :nb_markers, 2], orbs[:, :nb_markers, 6] - - -def plot_marker_trajectories_slider( - pdata, - species="kinetic_ions", - max_markers=200, - show_paths=None, - title="Marker trajectories", -): - if show_paths is None: - show_paths = max_markers <= 200 - - x, y, z, weights = load_marker_data( - pdata=pdata, - species=species, - max_markers=max_markers, - ) - - nt, nmarkers = x.shape - print(f"loaded markers: {x.shape}") - print(f"plotted trajectories: {nmarkers}") - print("x min/max:", xp.nanmin(x), xp.nanmax(x)) - print("y min/max:", xp.nanmin(y), xp.nanmax(y)) - print("z min/max:", xp.nanmin(z), xp.nanmax(z)) - - fig = plt.figure(figsize=(8, 7)) - ax = fig.add_subplot(111, projection="3d") - plt.subplots_adjust(bottom=0.18) - - it0 = 0 - sc = ax.scatter( - x[it0], - y[it0], - z[it0], - c=weights[it0], - s=8, - cmap="viridis", - ) - - lines = [] - if show_paths: - for j in range(nmarkers): - line, = ax.plot( - x[: it0 + 1, j], - y[: it0 + 1, j], - z[: it0 + 1, j], - lw=0.8, - alpha=0.5, - ) - lines.append(line) - - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.set_title(f"{title} | step {it0}/{nt - 1}") - - fig.colorbar(sc, ax=ax, label="marker weights") - - ax_slider = plt.axes([0.18, 0.06, 0.65, 0.03]) - slider = Slider(ax_slider, "time index", 0, nt - 1, valinit=it0, valstep=1) - - def update(_): - it = int(slider.val) - - sc._offsets3d = (x[it], y[it], z[it]) - sc.set_array(weights[it]) - - if show_paths: - for j, line in enumerate(lines): - line.set_data(x[: it + 1, j], y[: it + 1, j]) - line.set_3d_properties(z[: it + 1, j]) - - ax.set_title(f"{title} | step {it}/{nt - 1}") - fig.canvas.draw_idle() - - slider.on_changed(update) - plt.show() - -# ============================================================ -# Main -# ============================================================ -def main(): - ensure_post_processing(params, sim_path) - - pdata = PlottingData(sim=params.sim) + pdata = PlottingData(path_out=path_out) pdata.load() - params.domain.show() - - data_path = os.path.join(sim_path, "data") - - time, en_phi = load_scalar(data_path, FIT_QUANTITY) - plot_energy_fit(time, en_phi) + # growth rate of the electrostatic potential + TimeSeriesPlot( + pdata.scalars[FIT_QUANTITY], + fit=True, + fit_window=FIT_WINDOW, + fit_of_sqrt=True, + params=pdata.params, + title=f"Evolution of {FIT_QUANTITY}", + ).show() if SHOW_EQUIL_PROFILE: - plot_equilibrium_profile(sim_path) - - if SHOW_DENSITY_SLIDER: - for cfg in DENSITY_PLOTS: - plot_binned_quantity_slider( - params, - pdata, - bin_name=cfg["bin"], - quantity=cfg["quantity"], - in_physical=cfg.get("physical", True), - axes=cfg.get("axes", "RZ"), - vmin=cfg.get("vmin"), - vmax=cfg.get("vmax"), - title=cfg.get("title"), - ) - - if SHOW_FIELD_SLIDER: - for cfg in FIELD_PLOTS: - plot_field_slider( - pdata, - species=cfg["species"], - field=cfg["field"], - component=cfg.get("component", 0), - axes=cfg.get("axes", "RZT"), - vmin=cfg.get("vmin"), - vmax=cfg.get("vmax"), - title=cfg.get("title"), - ) - - plot_marker_trajectories_slider( - pdata=pdata, - species="kinetic_ions", - max_markers=1000, - show_paths=True, - ) - + plot_equilibrium_profile(path_out) + + for bin_name, quantity, plane in DENSITY_PLOTS: + data = pdata.f.kinetic_ions[bin_name][quantity] + SliderPlot( + data, + grids=physical_grids(data.isel(t=0), pdata.domain, axes=plane), + params=pdata.params, + title=f"{quantity} ({plane})", + ).show() + + for species, field, component, plane in FIELD_PLOTS: + data = pdata.spline_values[species][field].array.isel(comp=component) + SliderPlot( + data, + # the cut plane moves with the slider, so the grids follow it + grids=lambda index, plane=plane: field_slice_grids( + pdata.grids_phy, fixed_dim="e3", index=index, plane=plane + ), + slice_dim="e3", + params=pdata.params, + title=f"{species}.{field} ({plane})", + ).show() + + MarkerTrajectoryPlot(pdata.orbits.kinetic_ions, max_markers=1000).show() -if len(sys.argv) > 1 and __name__ == "__main__": - sim_name = sys.argv[1] - sim_path = os.path.join(os.getcwd(), sim_name) - params = load_params(sim_path) -else: - sim_name = "sim_1" - sim_path = os.path.join(os.getcwd(), sim_name) - import params_cyclone as params if __name__ == "__main__": - main() + sim_name = sys.argv[1] if len(sys.argv) > 1 else "sim_1" + main(os.path.join(os.getcwd(), sim_name)) diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 4e94f5d7c..1af41fef9 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,536 +1,79 @@ -import importlib.util import os import sys -from mpl_toolkits.mplot3d import Axes3D # noqa: F401 -import glob - -import h5py -import pyvista as pv -import cunumpy as xp -from matplotlib import pyplot as plt -from matplotlib.widgets import Slider from struphy import PlottingData, PostProcessor - - -# ============================================================ -# User options -# ============================================================ -FIT_T0 = 0.0 -FIT_T1 = None # None -> last available time -FIT_QUANTITY = "phi_integral" # or "en_phi" if this scalar exists in data_proc0.hdf5 +from struphy.diagnostics.plotting import ( + MarkerTrajectoryPlot, + SliderPlot, + TimeSeriesPlot, + field_slice_grids, + physical_grids, + plot_equilibrium_profile, +) + +# quantity whose exponential growth rate is fitted +FIT_QUANTITY = "phi_integral" +FIT_WINDOW = (0.0, None) SHOW_EQUIL_PROFILE = False -SHOW_DENSITY_SLIDER = True -SHOW_FIELD_SLIDER = True +# binned densities to sweep, as (bin name, quantity, physical plane) DENSITY_PLOTS = [ - { - "bin": "e1_e2_density", - "quantity": "f_binned", - "physical": True, - "axes": "XY", - "vmin": None, - "vmax": None, - "title": "delta_f (R,Z)", - }, - { - "bin": "e1_e2_density", - "quantity": "delta_f_binned", - "physical": True, - "axes": "XY", - "vmin": None, - "vmax": None, - "title": "delta_f (X,Y)", - }, + ("e1_e2_density", "f_binned", "XY"), + ("e1_e2_density", "delta_f_binned", "XY"), ] +# fields to sweep, as (species, field, component, physical plane) FIELD_PLOTS = [ - { - "species": "em_fields", - "field": "phi_phy", - "component": 0, - "axes": "XYZ", - "fixed_index": 0, - "vmin": None, - "vmax": None, - "title": "Electric potential phi", - }, - { - "species": "diagnostics", - "field": "rho_phy", - "component": 0, - "axes": "XYZ", - "fixed_index": 0, - "vmin": None, - "vmax": None, - "title": "Right-hand side of Poisson equation", - }, + ("em_fields", "phi_phy", 0, "XY"), + ("diagnostics", "rho_phy", 0, "XY"), ] +def main(path_out): + PostProcessor(path_out=path_out).process(physical=True, force=False) -# ============================================================ -# Small utilities -# ============================================================ -def load_params(sim_path): - spec = importlib.util.spec_from_file_location("params", os.path.join(sim_path, "parameters.py")) - params = importlib.util.module_from_spec(spec) - spec.loader.exec_module(params) - return params - - -def ensure_post_processing(params, sim_path): - if not os.path.isdir(os.path.join(sim_path, "post_processing")): - pp = PostProcessor(sim=params.sim) - pp.process(physical=True) - - -def load_scalar(data_path, quantity): - with h5py.File(os.path.join(data_path, "data_proc0.hdf5"), "r") as f: - time = xp.asarray(f["time"]["value"][()]) - if quantity in f["scalar"]: - y = xp.asarray(f["scalar"][quantity][()]) - elif quantity == "en_phi" and "phi_integral" in f["scalar"]: - y = xp.asarray(f["scalar"]["phi_integral"][()]) - else: - available = list(f["scalar"].keys()) - raise KeyError(f"Scalar {quantity!r} not found. Available scalars: {available}") - return time, y - - -def fit_window(time, y, t0, t1): - """Return safe indices for a log-fit of sqrt(y).""" - if t1 is None: - t1 = float(time[-1]) - - lo, hi = sorted((float(t0), float(t1))) - mask = (time >= lo) & (time <= hi) & xp.isfinite(y) & (y > 0.0) - - # If the user window is unusable, use all positive finite samples. - if xp.count_nonzero(mask) < 2: - mask = xp.isfinite(y) & (y > 0.0) - - # If there are still too few points, disable fit cleanly. - if xp.count_nonzero(mask) < 2: - return None - - idx = xp.nonzero(mask)[0] - return int(idx[0]), int(idx[-1]) + 1 - - -def plot_energy_fit(time, en_phi, t0=FIT_T0, t1=FIT_T1): - window = fit_window(time, en_phi, t0, t1) - - fig, ax = plt.subplots() - ax.plot(time, en_phi, label=FIT_QUANTITY) - ax.set_xlabel("time") - ax.set_ylabel(FIT_QUANTITY) - ax.set_title(f"Evolution of {FIT_QUANTITY}") - - if window is not None: - i0, i1 = window - fit_time = time[i0:i1] - fit_signal = xp.log(xp.sqrt(en_phi[i0:i1])) - gamma, b = xp.polyfit(fit_time, fit_signal, 1) - en_fit = xp.exp(2.0 * (gamma * fit_time + b)) - ax.plot(fit_time, en_fit, "--", label=f"fit: gamma={gamma:.4e}") - ax.axvspan(fit_time[0], fit_time[-1], alpha=0.12) - print(f"{FIT_QUANTITY} fit window: [{fit_time[0]:.6e}, {fit_time[-1]:.6e}]") - print(f"growth rate from log(sqrt({FIT_QUANTITY})): gamma = {gamma:.8e}") - else: - print(f"No valid positive data found for exponential fit of {FIT_QUANTITY}.") - - ax.legend() - fig.tight_layout() - plt.show() - - -def plot_equilibrium_profile(sim_path): - equil_data = pv.read(os.path.join(sim_path, "geometry.vts")) - dims = equil_data.dimensions - grid = xp.reshape(equil_data.points, dims + (3,)) - r = xp.sqrt(grid[:, :, :, 0] ** 2 + grid[:, :, :, 1] ** 2) - p0 = xp.reshape(equil_data.point_data["p0"], dims) - - fig, ax = plt.subplots() - ax.set_title("Radial equilibrium profiles") - ax.set_xlabel("R") - ax.plot(r[0, 0, :], p0[0, 0, :], label="p0") - - if "n0" in equil_data.point_data: - n0 = xp.reshape(equil_data.point_data["n0"], dims) - ax.plot(r[0, 0, :], n0[0, 0, :], label="n0") - ax.plot(r[0, 0, :], p0[0, 0, :] / n0[0, 0, :], label="T0") - - ax.legend() - fig.tight_layout() - plt.show() - - -def match_field_to_grid(field, xgrid): - """Return field with orientation compatible with xgrid when possible.""" - if field.shape == xgrid.shape: - return field - if field.T.shape == xgrid.shape: - return field.T - raise ValueError(f"Cannot match field shape {field.shape} with grid shape {xgrid.shape}.") - - -def get_binned_data(pdata, bin_name, quantity): - return xp.asarray(getattr(getattr(pdata.f.kinetic_ions, bin_name), quantity)) - - -def get_binned_grids( - params, - pdata, - bin_name, - in_physical=True, - plot_axes="RZ", - fixed_eta=(0.5, 0.0, 0.0), -): - bin_data = getattr(pdata.f.kinetic_ions, bin_name) - - bin_axes = [int(part[1]) for part in bin_name.split("_") if part.startswith("e")] - if len(bin_axes) != 2: - raise ValueError(f"Cannot infer two binned axes from bin_name={bin_name!r}") - - g0 = getattr(bin_data, f"grid_e{bin_axes[0]}") - g1 = getattr(bin_data, f"grid_e{bin_axes[1]}") - - if not in_physical: - xgrid, ygrid = xp.meshgrid(g0, g1, indexing="ij") - return xgrid, ygrid, f"eta{bin_axes[0]}", f"eta{bin_axes[1]}" - - etas = [] - for ax in (1, 2, 3): - if ax == bin_axes[0]: - etas.append(g0) - elif ax == bin_axes[1]: - etas.append(g1) - else: - etas.append(fixed_eta[ax - 1]) - - x, y, z = params.domain(*etas, squeeze_out=True) - - if plot_axes == "RZ": - return xp.sqrt(x**2 + y**2), z, "R", "Z" - elif plot_axes == "XY": - return x, y, "X", "Y" - elif plot_axes == "XZ": - return x, z, "X", "Z" - elif plot_axes == "YZ": - return y, z, "Y", "Z" - else: - raise ValueError(f"Unknown plot_axes={plot_axes!r}") - - -def make_slider_plot(time_grid, xgrid, ygrid, data, *, title, xlabel, ylabel, vmin=None, vmax=None): - data0 = match_field_to_grid(xp.asarray(data[0]), xgrid) - - fig, ax = plt.subplots() - fig.subplots_adjust(bottom=0.20) - pcm = ax.pcolormesh(xgrid, ygrid, data0, shading="auto", vmin=vmin, vmax=vmax) - cbar = fig.colorbar(pcm, ax=ax) - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(f"{title} at t = {time_grid[0]:.4e}") - - slider_ax = fig.add_axes([0.20, 0.07, 0.60, 0.03]) - slider = Slider( - slider_ax, - "time index", - 0, - len(time_grid) - 1, - valinit=0, - valstep=1, - ) - - def update(_): - idx = int(slider.val) - field = match_field_to_grid(xp.asarray(data[idx]), xgrid) - pcm.set_array(field.ravel()) - if vmin is None and vmax is None: - pcm.set_clim(float(xp.nanmin(field)), float(xp.nanmax(field))) - cbar.update_normal(pcm) - ax.set_title(f"{title} at t = {time_grid[idx]:.4e}") - fig.canvas.draw_idle() - - slider.on_changed(update) - plt.show() - - -def plot_binned_quantity_slider(params, pdata, *, bin_name, quantity, in_physical=True, axes="RZ", vmin=None, vmax=None, title="density binned"): - data = get_binned_data(pdata, bin_name, quantity) - xgrid, ygrid, xlabel, ylabel = get_binned_grids(params, pdata, bin_name, in_physical=in_physical, plot_axes=axes) - make_slider_plot( - pdata.t_grid, - xgrid, - ygrid, - data, - title=title, - xlabel=xlabel, - ylabel=ylabel, - vmin=vmin, - vmax=vmax, - ) - - -def get_field_3d(pdata, species, field, component=0): - field_data = getattr(getattr(pdata.spline_values, species), field) - times = list(field_data.data.keys()) - values = [field_data.data[t][component] for t in times] - return xp.array(times), values - - -def get_slice_from_field(pdata, arr3d, axes="RZT", fixed_index=0): - X = pdata.grids_phy[0] - Y = pdata.grids_phy[1] - Z = pdata.grids_phy[2] - R = xp.sqrt(X**2 + Y**2) - - if axes == "RZT": - data = arr3d[:, :, fixed_index] - return R[:, :, fixed_index], Z[:, :, fixed_index], data, "R", "Z" - - elif axes == "XYZ": - data = arr3d[:, :, fixed_index] - return X[:, :, fixed_index], Y[:, :, fixed_index], data, "X", "Y" - - elif axes == "RTP": - data = arr3d[:, fixed_index, :] - return R[:, fixed_index, :], Z[:, fixed_index, :], data, "R", "Z" - - else: - raise ValueError(f"Unknown field slice axes={axes!r}") - - -def plot_field_slider( - pdata, - species, - field, - component=0, - axes="RZT", - vmin=None, - vmax=None, - title=None, -): - times, values = get_field_3d(pdata, species, field, component=component) - - nt = len(values) - shape = values[0].shape - - if axes == "RZT": - nslice = shape[2] - slice_label = "toroidal index" - elif axes == "XYZ": - nslice = shape[2] - slice_label = "poloidal/radial slice index" - elif axes == "RTP": - nslice = shape[1] - slice_label = "eta2 index" - else: - raise ValueError(f"Unknown field slice axes={axes!r}") - - fig, ax = plt.subplots() - plt.subplots_adjust(bottom=0.22) - - time_idx = 0 - slice_idx = min(nslice - 1, nslice // 2) - - xg, yg, data, xlabel, ylabel = get_slice_from_field( - pdata, - values[time_idx], - axes=axes, - fixed_index=slice_idx, - ) - - pcm = ax.pcolormesh(xg, yg, data, shading="auto", vmin=vmin, vmax=vmax) - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(title or f"{species}.{field}") - - cbar = fig.colorbar(pcm, ax=ax) - - ax_time = plt.axes([0.15, 0.10, 0.70, 0.03]) - ax_slice = plt.axes([0.15, 0.05, 0.70, 0.03]) - - s_time = Slider(ax_time, "time", 0, nt - 1, valinit=time_idx, valstep=1) - s_slice = Slider(ax_slice, slice_label, 0, nslice - 1, valinit=slice_idx, valstep=1) - - def update(_): - ti = int(s_time.val) - si = int(s_slice.val) - - xg, yg, data, xlabel, ylabel = get_slice_from_field( - pdata, - values[ti], - axes=axes, - fixed_index=si, - ) - - pcm.set_array(data.ravel()) - pcm.set_clim( - vmin if vmin is not None else xp.nanmin(data), - vmax if vmax is not None else xp.nanmax(data), - ) - ax.set_title(f"{title or field} | t = {times[ti]:.4e}, slice = {si}") - fig.canvas.draw_idle() - - s_time.on_changed(update) - s_slice.on_changed(update) - - plt.show() - - - -def load_marker_data(pdata, species="kinetic_ions", max_markers=200): - orbs = getattr(pdata.orbits, species) - nb_markers = min(orbs.shape[1], max_markers) - return orbs[:, :nb_markers, 0], orbs[:, :nb_markers, 1], orbs[:, :nb_markers, 2], orbs[:, :nb_markers, 6] - - -def plot_marker_trajectories_slider( - pdata, - species="kinetic_ions", - max_markers=200, - show_paths=None, - title="Marker trajectories", -): - if show_paths is None: - show_paths = max_markers <= 200 - - x, y, z, weights = load_marker_data( - pdata=pdata, - species=species, - max_markers=max_markers, - ) - - nt, nmarkers = x.shape - print(f"loaded markers: {x.shape}") - print(f"plotted trajectories: {nmarkers}") - print("x min/max:", xp.nanmin(x), xp.nanmax(x)) - print("y min/max:", xp.nanmin(y), xp.nanmax(y)) - print("z min/max:", xp.nanmin(z), xp.nanmax(z)) - - fig = plt.figure(figsize=(8, 7)) - ax = fig.add_subplot(111, projection="3d") - plt.subplots_adjust(bottom=0.18) - - it0 = 0 - sc = ax.scatter( - x[it0], - y[it0], - z[it0], - c=weights[it0], - s=8, - cmap="viridis", - ) - - lines = [] - if show_paths: - for j in range(nmarkers): - line, = ax.plot( - x[: it0 + 1, j], - y[: it0 + 1, j], - z[: it0 + 1, j], - lw=0.8, - alpha=0.5, - ) - lines.append(line) - - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.set_title(f"{title} | step {it0}/{nt - 1}") - - fig.colorbar(sc, ax=ax, label="marker weights") - - ax_slider = plt.axes([0.18, 0.06, 0.65, 0.03]) - slider = Slider(ax_slider, "time index", 0, nt - 1, valinit=it0, valstep=1) - - def update(_): - it = int(slider.val) - - sc._offsets3d = (x[it], y[it], z[it]) - sc.set_array(weights[it]) - - if show_paths: - for j, line in enumerate(lines): - line.set_data(x[: it + 1, j], y[: it + 1, j]) - line.set_3d_properties(z[: it + 1, j]) - - ax.set_title(f"{title} | step {it}/{nt - 1}") - fig.canvas.draw_idle() - - slider.on_changed(update) - plt.show() - -# ============================================================ -# Main -# ============================================================ -def main(): - ensure_post_processing(params, sim_path) - - pdata = PlottingData(sim=params.sim) + pdata = PlottingData(path_out=path_out) pdata.load() - params.domain.show() - - data_path = os.path.join(sim_path, "data") - - time, en_phi = load_scalar(data_path, FIT_QUANTITY) - plot_energy_fit(time, en_phi) + # growth rate of the electrostatic potential + TimeSeriesPlot( + pdata.scalars[FIT_QUANTITY], + fit=True, + fit_window=FIT_WINDOW, + fit_of_sqrt=True, + params=pdata.params, + title=f"Evolution of {FIT_QUANTITY}", + ).show() if SHOW_EQUIL_PROFILE: - plot_equilibrium_profile(sim_path) - - if SHOW_DENSITY_SLIDER: - for cfg in DENSITY_PLOTS: - plot_binned_quantity_slider( - params, - pdata, - bin_name=cfg["bin"], - quantity=cfg["quantity"], - in_physical=cfg.get("physical", True), - axes=cfg.get("axes", "RZ"), - vmin=cfg.get("vmin"), - vmax=cfg.get("vmax"), - title=cfg.get("title"), - ) - - if SHOW_FIELD_SLIDER: - for cfg in FIELD_PLOTS: - plot_field_slider( - pdata, - species=cfg["species"], - field=cfg["field"], - component=cfg.get("component", 0), - axes=cfg.get("axes", "RZT"), - vmin=cfg.get("vmin"), - vmax=cfg.get("vmax"), - title=cfg.get("title"), - ) - - plot_marker_trajectories_slider( - pdata=pdata, - species="kinetic_ions", - max_markers=1000, - show_paths=True, - ) - - -if len(sys.argv) > 1 and __name__ == "__main__": - sim_name = sys.argv[1] - sim_path = os.path.join(os.getcwd(), sim_name) - params = load_params(sim_path) -else: - sim_name = "sim_1" - sim_path = os.path.join(os.getcwd(), sim_name) - import params_drift_kinetic as params + plot_equilibrium_profile(path_out) + + for bin_name, quantity, plane in DENSITY_PLOTS: + data = pdata.f.kinetic_ions[bin_name][quantity] + SliderPlot( + data, + grids=physical_grids(data.isel(t=0), pdata.domain, axes=plane), + params=pdata.params, + title=f"{quantity} ({plane})", + ).show() + + for species, field, component, plane in FIELD_PLOTS: + data = pdata.spline_values[species][field].array.isel(comp=component) + SliderPlot( + data, + # the cut plane moves with the slider, so the grids follow it + grids=lambda index, plane=plane: field_slice_grids( + pdata.grids_phy, fixed_dim="e3", index=index, plane=plane + ), + slice_dim="e3", + params=pdata.params, + title=f"{species}.{field} ({plane})", + ).show() + + MarkerTrajectoryPlot(pdata.orbits.kinetic_ions, max_markers=1000).show() if __name__ == "__main__": - main() + sim_name = sys.argv[1] if len(sys.argv) > 1 else "sim_1" + main(os.path.join(os.getcwd(), sim_name)) diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 94100a03f..fe7a34e28 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -1,534 +1,100 @@ -import importlib.util -from struphy import PlottingData, PostProcessor +"""Post-process and plot the diocotron instability. + +Run as ``python pproc_diocotron.py [sim_1 sim_2 ...]`` to compare several runs; with +more than one folder only the growth-rate comparison is shown. +""" import os import sys -import cunumpy as xp -import scipy.optimize as sc -from matplotlib import pyplot as plt -from matplotlib.widgets import Slider -import h5py -import pyvista as pv - -from struphy import logging, set_logging_level -set_logging_level(logging.INFO) - -# User options +from struphy import PlottingData, PostProcessor +from struphy.diagnostics.plotting import ( + MarkerTrajectoryPlot, + SliderPlot, + TimeSeriesPlot, + field_slice_grids, + physical_grids, + plot_equilibrium_profile, +) + +FIT_QUANTITY = "en_phi" +FIT_WINDOW = (0.0, 42.0) SHOW_EQUIL_PROFILE = True -SHOW_DENSITY_SLIDER = True -SHOW_FIELD_SLIDER = True +# binned densities to sweep, as (bin name, quantity, physical plane) DENSITY_PLOTS = [ - { - "bin": "e1_e2_density", - "quantity": "f_binned", - "physical": True, - "axes": "XY", - "vmin": None, - "vmax": None, - "title": "f (R,Z)", - }, - { - "bin": "e1_e2_density", - "quantity": "delta_f_binned", - "physical": True, - "axes": "XY", - "vmin": None, - "vmax": None, - "title": "delta_f (X,Y)", - }, + ("e1_e2_density", "f_binned", "XY"), + ("e1_e2_density", "delta_f_binned", "XY"), ] +# fields to sweep, as (species, field, component, physical plane) FIELD_PLOTS = [ - { - "species": "em_fields", - "field": "phi_phy", - "component": 0, - "axes": "XYZ", - "fixed_index": 0, - "vmin": None, - "vmax": None, - "title": "Electric potential phi", - }, + ("em_fields", "phi_phy", 0, "XY"), ] -# ============================================================ -# Small utilities -# ============================================================ -def load_params(sim_path): - spec = importlib.util.spec_from_file_location("params", os.path.join(sim_path, "parameters.py")) - params = importlib.util.module_from_spec(spec) - spec.loader.exec_module(params) - return params - - -def ensure_post_processing(params, sim_path): - if not os.path.isdir(os.path.join(sim_path, "post_processing")): - pp = PostProcessor(sim=params.sim) - pp.process(physical=True) - - -def plot_equilibrium_profile(sim_path): - equil_data = pv.read(os.path.join(sim_path, "geometry.vts")) - dims = equil_data.dimensions - grid = xp.reshape(equil_data.points, dims + (3,)) - r = xp.sqrt(grid[:, :, :, 0] ** 2 + grid[:, :, :, 1] ** 2) - p0 = xp.reshape(equil_data.point_data["p0"], dims) - - fig, ax = plt.subplots() - ax.set_title("Radial equilibrium profiles") - ax.set_xlabel("R") - ax.plot(r[0, 0, :], p0[0, 0, :], label="p0") - - if "n0" in equil_data.point_data: - n0 = xp.reshape(equil_data.point_data["n0"], dims) - ax.plot(r[0, 0, :], n0[0, 0, :], label="n0") - ax.plot(r[0, 0, :], p0[0, 0, :] / n0[0, 0, :], label="T0") - - ax.legend() - fig.tight_layout() - plt.show() - - -def match_field_to_grid(field, xgrid): - """Return field with orientation compatible with xgrid when possible.""" - if field.shape == xgrid.shape: - return field - if field.T.shape == xgrid.shape: - return field.T - raise ValueError(f"Cannot match field shape {field.shape} with grid shape {xgrid.shape}.") - - -def get_binned_data(pdata, bin_name, quantity): - return xp.asarray(getattr(getattr(pdata.f.kinetic_ions, bin_name), quantity)) - - -def get_binned_grids( - params, - pdata, - bin_name, - in_physical=True, - plot_axes="RZ", - fixed_eta=(0.5, 0.0, 0.0), -): - bin_data = getattr(pdata.f.kinetic_ions, bin_name) - - bin_axes = [int(part[1]) for part in bin_name.split("_") if part.startswith("e")] - if len(bin_axes) != 2: - raise ValueError(f"Cannot infer two binned axes from bin_name={bin_name!r}") - - g0 = getattr(bin_data, f"grid_e{bin_axes[0]}") - g1 = getattr(bin_data, f"grid_e{bin_axes[1]}") - - if not in_physical: - xgrid, ygrid = xp.meshgrid(g0, g1, indexing="ij") - return xgrid, ygrid, f"eta{bin_axes[0]}", f"eta{bin_axes[1]}" - - etas = [] - for ax in (1, 2, 3): - if ax == bin_axes[0]: - etas.append(g0) - elif ax == bin_axes[1]: - etas.append(g1) - else: - etas.append(fixed_eta[ax - 1]) - - x, y, z = params.domain(*etas, squeeze_out=True) - - if plot_axes == "RZ": - return xp.sqrt(x**2 + y**2), z, "R", "Z" - elif plot_axes == "XY": - return x, y, "X", "Y" - elif plot_axes == "XZ": - return x, z, "X", "Z" - elif plot_axes == "YZ": - return y, z, "Y", "Z" - else: - raise ValueError(f"Unknown plot_axes={plot_axes!r}") - - -def make_slider_plot(time_grid, xgrid, ygrid, data, *, title, xlabel, ylabel, vmin=None, vmax=None): - data0 = match_field_to_grid(xp.asarray(data[0]), xgrid) - - fig, ax = plt.subplots() - fig.subplots_adjust(bottom=0.20) - pcm = ax.pcolormesh(xgrid, ygrid, data0, shading="auto", vmin=vmin, vmax=vmax) - cbar = fig.colorbar(pcm, ax=ax) - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(f"{title} at t = {time_grid[0]:.4e}") - - slider_ax = fig.add_axes([0.20, 0.07, 0.60, 0.03]) - slider = Slider( - slider_ax, - "time index", - 0, - len(time_grid) - 1, - valinit=0, - valstep=1, - ) - - def update(_): - idx = int(slider.val) - field = match_field_to_grid(xp.asarray(data[idx]), xgrid) - pcm.set_array(field.ravel()) - if vmin is None and vmax is None: - pcm.set_clim(float(xp.nanmin(field)), float(xp.nanmax(field))) - cbar.update_normal(pcm) - ax.set_title(f"{title} at t = {time_grid[idx]:.4e}") - fig.canvas.draw_idle() - - slider.on_changed(update) - plt.show() - - -def plot_binned_quantity_slider(params, pdata, *, bin_name, quantity, in_physical=True, axes="RZ", vmin=None, vmax=None, title="density binned"): - data = get_binned_data(pdata, bin_name, quantity) - xgrid, ygrid, xlabel, ylabel = get_binned_grids(params, pdata, bin_name, in_physical=in_physical, plot_axes=axes) - make_slider_plot( - pdata.t_grid, - xgrid, - ygrid, - data, - title=title, - xlabel=xlabel, - ylabel=ylabel, - vmin=vmin, - vmax=vmax, - ) - - -def get_field_3d(pdata, species, field, component=0): - field_data = getattr(getattr(pdata.spline_values, species), field) - times = list(field_data.data.keys()) - values = [field_data.data[t][component] for t in times] - return xp.array(times), values - - -def get_slice_from_field(pdata, arr3d, axes="RZT", fixed_index=0): - X = pdata.grids_phy[0] - Y = pdata.grids_phy[1] - Z = pdata.grids_phy[2] - R = xp.sqrt(X**2 + Y**2) - - if axes == "RZT": - data = arr3d[:, :, fixed_index] - return R[:, :, fixed_index], Z[:, :, fixed_index], data, "R", "Z" - - elif axes == "XYZ": - data = arr3d[:, :, fixed_index] - return X[:, :, fixed_index], Y[:, :, fixed_index], data, "X", "Y" - - elif axes == "RTP": - data = arr3d[:, fixed_index, :] - return R[:, fixed_index, :], Z[:, fixed_index, :], data, "R", "Z" - - else: - raise ValueError(f"Unknown field slice axes={axes!r}") - - -def plot_field_slider( - pdata, - species, - field, - component=0, - axes="RZT", - vmin=None, - vmax=None, - title=None, -): - times, values = get_field_3d(pdata, species, field, component=component) - - nt = len(values) - shape = values[0].shape - if axes == "RZT": - nslice = shape[2] - slice_label = "toroidal index" - elif axes == "XYZ": - nslice = shape[2] - slice_label = "poloidal/radial slice index" - elif axes == "RTP": - nslice = shape[1] - slice_label = "eta2 index" - else: - raise ValueError(f"Unknown field slice axes={axes!r}") +def load(path_out): + PostProcessor(path_out=path_out).process(physical=True, force=False) + pdata = PlottingData(path_out=path_out) + pdata.load() + return pdata - fig, ax = plt.subplots() - plt.subplots_adjust(bottom=0.22) - time_idx = 0 - slice_idx = min(nslice - 1, nslice // 2) +def main(paths): + runs = {os.path.basename(p): load(p) for p in paths} - xg, yg, data, xlabel, ylabel = get_slice_from_field( - pdata, - values[time_idx], - axes=axes, - fixed_index=slice_idx, - ) + # growth rate of the electrostatic energy, one curve per run + series = [] + for name, pdata in runs.items(): + energy = pdata.scalars[FIT_QUANTITY] + energy.label = name if len(runs) > 1 else FIT_QUANTITY + series.append(energy) - pcm = ax.pcolormesh(xg, yg, data, shading="auto", vmin=vmin, vmax=vmax) - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(title or f"{species}.{field}") + plot = TimeSeriesPlot( + series, + fit=True, + fit_window=FIT_WINDOW, + params=next(iter(runs.values())).params, + title=f"Evolution of {FIT_QUANTITY}", + ).show() - cbar = fig.colorbar(pcm, ax=ax) + for name, (gamma, _, _) in zip(runs, plot.fit_results): + print(f"{name}: growth rate = {gamma}") - ax_time = plt.axes([0.15, 0.10, 0.70, 0.03]) - ax_slice = plt.axes([0.15, 0.05, 0.70, 0.03]) + if len(runs) > 1: + return - s_time = Slider(ax_time, "time", 0, nt - 1, valinit=time_idx, valstep=1) - s_slice = Slider(ax_slice, slice_label, 0, nslice - 1, valinit=slice_idx, valstep=1) - - def update(_): - ti = int(s_time.val) - si = int(s_slice.val) - - xg, yg, data, xlabel, ylabel = get_slice_from_field( - pdata, - values[ti], - axes=axes, - fixed_index=si, - ) - - pcm.set_array(data.ravel()) - pcm.set_clim( - vmin if vmin is not None else xp.nanmin(data), - vmax if vmax is not None else xp.nanmax(data), - ) - ax.set_title(f"{title or field} | t = {times[ti]:.4e}, slice = {si}") - fig.canvas.draw_idle() - - s_time.on_changed(update) - s_slice.on_changed(update) - - plt.show() - - - -def load_marker_data(pdata, species="kinetic_ions", max_markers=200): - orbs = getattr(pdata.orbits, species) - nb_markers = min(orbs.shape[1], max_markers) - return orbs[:, :nb_markers, 0], orbs[:, :nb_markers, 1], orbs[:, :nb_markers, 2], orbs[:, :nb_markers, 6] - - -def plot_marker_trajectories_slider( - pdata, - species="kinetic_ions", - max_markers=200, - show_paths=None, - title="Marker trajectories", -): - if show_paths is None: - show_paths = max_markers <= 200 - - x, y, z, weights = load_marker_data( - pdata=pdata, - species=species, - max_markers=max_markers, - ) - - nt, nmarkers = x.shape - print(f"loaded markers: {x.shape}") - print(f"plotted trajectories: {nmarkers}") - print("x min/max:", xp.nanmin(x), xp.nanmax(x)) - print("y min/max:", xp.nanmin(y), xp.nanmax(y)) - print("z min/max:", xp.nanmin(z), xp.nanmax(z)) - - fig = plt.figure(figsize=(8, 7)) - ax = fig.add_subplot(111, projection="3d") - plt.subplots_adjust(bottom=0.18) - - it0 = 0 - sc = ax.scatter( - x[it0], - y[it0], - z[it0], - c=weights[it0], - s=8, - cmap="viridis", - ) - - lines = [] - if show_paths: - for j in range(nmarkers): - line, = ax.plot( - x[: it0 + 1, j], - y[: it0 + 1, j], - z[: it0 + 1, j], - lw=0.8, - alpha=0.5, - ) - lines.append(line) - - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.set_title(f"{title} | step {it0}/{nt - 1}") - - fig.colorbar(sc, ax=ax, label="marker weights") - - ax_slider = plt.axes([0.18, 0.06, 0.65, 0.03]) - slider = Slider(ax_slider, "time index", 0, nt - 1, valinit=it0, valstep=1) - - def update(_): - it = int(slider.val) - - sc._offsets3d = (x[it], y[it], z[it]) - sc.set_array(weights[it]) - - if show_paths: - for j, line in enumerate(lines): - line.set_data(x[: it + 1, j], y[: it + 1, j]) - line.set_3d_properties(z[: it + 1, j]) - - ax.set_title(f"{title} | step {it}/{nt - 1}") - fig.canvas.draw_idle() - - slider.on_changed(update) - plt.show() - - -# ------------------ -# Post process simulation data -# In order to compare different simulations, execute this file as `python pproc_diocotron.py sim_1 sim_2 ...` -# where `sim_1`, `sim_2`, etc. are the names of the simulation folders to be post-processed and plotted together. -# If only one argument, the 2D plots will be shown. If multiple arguments, only the growth rate plot will be shown. -# ------------------ -def main(): - en_phis = [] - times = [] - sls = [] - params_opts = [] - fitting = [] - for i, sim_name in enumerate(sim_names): - params = params_files[i] - sim_path = sim_paths[i] - if not os.path.isdir(os.path.join(sim_path, "post_processing")): - pp = PostProcessor(sim=params.sim) - pp.process(physical=True) - - pdata = PlottingData(sim=params.sim) - pdata.load() - - # ------------------ - # Determine electrical potentail growth rate - # ------------------ - - # get scalar data (post processing not needed for scalar data) - pa_data = os.path.join(sim_path, "data") - with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: - times.append(f["time"]["value"][()]) - en_phis.append(xp.power(f["scalar"]["en_phi"][()], 1.0)) - - # time interval to determine growth rate - ti, tf = 0.0, 42.0 - if tf>times[i][-1]: tf = times[i][-1] - if ti>tf: - ti = tf/2 - xi = xp.abs(pdata.t_grid - ti).argmin() # index of time 100 [a.lu.] (observed end of growth rate) - xf = xp.abs(pdata.t_grid - tf).argmin() + 1 # index of time 200 [a.lu.] (observed end of growth rate) - if xi==0: - xi=1 # avoid including t=0 in fit - - sls.append(tuple([slice(xi, xf)])) - - if len(times[i]) > 3: - fitting.append(True) - # determine growth rate - fitting_func = lambda x,m,b,c0: xp.exp(m*x+b)+c0 - jac_func = lambda x,m,b,c0: xp.array([x*xp.exp(m*x+b), xp.exp(m*x+b), xp.ones_like(x)]).transpose() - - params_opt, _ = sc.curve_fit(fitting_func, times[i][sls[i]], en_phis[i][sls[i]], p0=(1e-3, -5, en_phis[i][1]), jac=jac_func, maxfev=10000)#3.07e2 - params_opts.append(params_opt) - - logging.info(f"Fitted growth rate for {sim_name}: {params_opt[0]:.4e}") - else: - fitting.append(False) - - fig, ax = plt.subplots(1, figsize = (6, 4)) - for i in range(len(sim_names)): - ax.scatter(times[i][1:], en_phis[i][1:], marker='x', s=0.05, label=r"$\phi$")#_{"+sim_names[i][4:]+r"}$") - if fitting[i]: - ax.plot( - times[i][sls[i]], - fitting_func(times[i][sls[i]], *params_opts[i]), - label=f"{ti=}, {tf=}, fitted growth_rate={params_opts[i][0]:.4e}", - c="orange" - ) - ax.axvline(ti, color="gray", linestyle="--", alpha=0.5) - ax.axvline(tf, color="gray", linestyle="--", alpha=0.5) - - #ax.set_yscale('log') - ax.legend() - - ax.set_title(f"{params.time_opts.dt=}, {params.time_opts.split_algo=}, {params.grid.num_elements=}, {params.derham_opts.degree=}, {params.loading_params.ppc=}") - ax.set_xlabel("time") - ax.set_ylabel("Energy [a.u.]") - - plt.tight_layout() - plt.show() + path_out, pdata = paths[0], next(iter(runs.values())) if SHOW_EQUIL_PROFILE: - plot_equilibrium_profile(sim_path) - - if SHOW_DENSITY_SLIDER: - for cfg in DENSITY_PLOTS: - plot_binned_quantity_slider( - params, - pdata, - bin_name=cfg["bin"], - quantity=cfg["quantity"], - in_physical=cfg.get("physical", True), - axes=cfg.get("axes", "RZ"), - vmin=cfg.get("vmin"), - vmax=cfg.get("vmax"), - title=cfg.get("title"), - ) - - if SHOW_FIELD_SLIDER: - for cfg in FIELD_PLOTS: - plot_field_slider( - pdata, - species=cfg["species"], - field=cfg["field"], - component=cfg.get("component", 0), - axes=cfg.get("axes", "RZT"), - vmin=cfg.get("vmin"), - vmax=cfg.get("vmax"), - title=cfg.get("title"), - ) - - plot_marker_trajectories_slider( - pdata=pdata, - species="kinetic_ions", - max_markers=1000, - show_paths=True, - ) - - -if len(sys.argv)>1 and __name__ == "__main__": - sim_names = sys.argv[1:] - params_files = [] - sim_paths = [] - - for i, sim_name in enumerate(sim_names): - sim_path = os.path.join(os.getcwd(), sim_name) + plot_equilibrium_profile(path_out) + + for bin_name, quantity, plane in DENSITY_PLOTS: + data = pdata.f.kinetic_ions[bin_name][quantity] + SliderPlot( + data, + grids=physical_grids(data.isel(t=0), pdata.domain, axes=plane), + params=pdata.params, + title=f"{quantity} ({plane})", + ).show() + + for species, field, component, plane in FIELD_PLOTS: + data = pdata.spline_values[species][field].array.isel(comp=component) + SliderPlot( + data, + # the cut plane moves with the slider, so the grids follow it + grids=lambda index, plane=plane: field_slice_grids( + pdata.grids_phy, fixed_dim="e3", index=index, plane=plane + ), + slice_dim="e3", + params=pdata.params, + title=f"{species}.{field} ({plane})", + ).show() + + MarkerTrajectoryPlot(pdata.orbits.kinetic_ions, max_markers=1000).show() - spec = importlib.util.spec_from_file_location("params", os.path.join(sim_path, "parameters.py")) - params = importlib.util.module_from_spec(spec) - spec.loader.exec_module(params) - sim_paths.append(sim_path) - params_files.append(params) -else: - sim_names = ["sim_1"] - import params_diocotron as params - params_files = [params] - sim_paths = [os.path.join(os.getcwd(), sim_names[0])] if __name__ == "__main__": - main() \ No newline at end of file + sim_names = sys.argv[1:] or ["sim_1"] + main([os.path.join(os.getcwd(), name) for name in sim_names]) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 8e003ff14..f913ee90f 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -271,34 +271,39 @@ def __init__(self, data, *, logy=True, fit=False, fit_window=None, fit_of_sqrt=F self.fit = fit self.fit_window = fit_window or (None, None) self.fit_of_sqrt = fit_of_sqrt - self.fit_result = None + #: one ``(gamma, b, window)`` per series once drawn, for reporting the rates + self.fit_results = [] def draw(self): fig, ax = self._make_axes() + self.fit_results = [] for s in self.series: - ax.plot(s.coord("t"), xp.asarray(s), label=s.label or None) + line, = ax.plot(s.coord("t"), xp.asarray(s), label=s.label or None) + + if not self.fit: + continue - if self.fit: - target = self.series[0] gamma, b, window = growth_rate( - target, + s, t0=self.fit_window[0], t1=self.fit_window[1], of_sqrt=self.fit_of_sqrt, ) - self.fit_result = (gamma, b, window) - if gamma is not None: - t_fit = xp.asarray(target.coord("t"))[window] - scale = 2.0 if self.fit_of_sqrt else 1.0 - ax.plot( - t_fit, - xp.exp(scale * (gamma * t_fit + b)), - "--", - color="black", - label=rf"fit: $\gamma$ = {gamma:.4e}", - ) - ax.axvspan(t_fit[0], t_fit[-1], alpha=0.12, color="grey") + self.fit_results.append((gamma, b, window)) + if gamma is None: + continue + + t_fit = xp.asarray(s.coord("t"))[window] + scale = 2.0 if self.fit_of_sqrt else 1.0 + ax.plot( + t_fit, + xp.exp(scale * (gamma * t_fit + b)), + "--", + color=line.get_color(), + label=rf"fit: $\gamma$ = {gamma:.4e}", + ) + ax.axvspan(t_fit[0], t_fit[-1], alpha=0.12, color="grey") if self.logy: ax.set_yscale("log") @@ -440,6 +445,10 @@ class SliderPlot(StruphyPlot): second slider. slice_dim : str, optional Which dimension the second slider steps through. Defaults to the last. + grids : tuple or callable, optional + Either fixed ``(xgrid, ygrid, xlabel, ylabel)``, or a function of the slice + index returning them. Pass a callable when the physical grid depends on where + the cut is taken, so that it follows the slider instead of going stale. """ tight = False @@ -460,6 +469,13 @@ def _frame(self, t_index, slice_index): frame = frame.isel(**{self.slice_dim: slice_index}) return frame + def _grids_for(self, slice_index): + if callable(self.grids): + return self.grids(slice_index) + if self.grids is not None: + return self.grids + return logical_grids(self._frame(0, slice_index)) + def draw(self): nt = self.data.shape[self.data.axis("t")] t = self.data.coord("t") @@ -468,8 +484,7 @@ def draw(self): slice_index = n_slice // 2 if n_slice else 0 first = self._frame(0, slice_index) - grids = self.grids if self.grids is not None else logical_grids(first) - xgrid, ygrid, xlabel, ylabel = grids + xgrid, ygrid, xlabel, ylabel = self._grids_for(slice_index) fig, ax = self._make_axes() fig.subplots_adjust(bottom=0.24 if self.slice_dim else 0.18) @@ -504,15 +519,35 @@ def draw(self): ) self.sliders.append(s_slice) + state = {"mesh": pcm, "xgrid": xgrid, "slice": slice_index} + def update(_): ti = int(s_time.val) si = int(s_slice.val) if s_slice is not None else 0 - frame = match_to_grid(self._frame(ti, si), xgrid) - pcm.set_array(frame.ravel()) + # a grid that depends on the cut has to be redrawn, not just refilled + if callable(self.grids) and si != state["slice"]: + xg, yg, _, _ = self._grids_for(si) + state["mesh"].remove() + state["mesh"] = ax.pcolormesh( + xg, + yg, + match_to_grid(self._frame(ti, si), xg), + shading="auto", + vmin=self.vmin, + vmax=self.vmax, + ) + state["xgrid"] = xg + state["slice"] = si + cbar.update_normal(state["mesh"]) + + mesh, grid = state["mesh"], state["xgrid"] + frame = match_to_grid(self._frame(ti, si), grid) + + mesh.set_array(frame.ravel()) if self.vmin is None and self.vmax is None: - pcm.set_clim(float(xp.nanmin(frame)), float(xp.nanmax(frame))) - cbar.update_normal(pcm) + mesh.set_clim(float(xp.nanmin(frame)), float(xp.nanmax(frame))) + cbar.update_normal(mesh) ax.set_title(f"{self.title} at t = {float(t[ti]):.4e}") fig.canvas.draw_idle() diff --git a/src/struphy/diagnostics/tests/__init__.py b/src/struphy/diagnostics/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py new file mode 100644 index 000000000..2083ae03f --- /dev/null +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -0,0 +1,290 @@ +"""Unit tests for the standardized plotters. + +These render into the Agg backend, so they check the geometry and labeling that the +plotters derive from the data rather than the appearance of the result. +""" + +import matplotlib +import numpy as np +import pytest + +matplotlib.use("Agg") + +from matplotlib import pyplot as plt # noqa: E402 + +from struphy.diagnostics.plotting import ( # noqa: E402 + PLANES, + AnimationPlot, + MarkerTrajectoryPlot, + PanelGridPlot, + Slice2DPlot, + SliderPlot, + TimeSeriesPlot, + field_slice_grids, + growth_rate, + logical_grids, + match_to_grid, +) +from struphy.post_processing.arrays import StruphyArray, wrap_orbits # noqa: E402 + + +# FuncAnimation warns when it is collected without having been rendered, which is +# exactly what happens to the animations these tests build and discard. +pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") + + +@pytest.fixture(autouse=True) +def close_figures(): + yield + plt.close("all") + + +def phase_space(nt=12, n1=6, nv=8): + return StruphyArray( + np.random.default_rng(0).random((nt, n1, nv)), + dims=("t", "e1", "v1"), + coords={"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, n1), "v1": np.linspace(-3, 3, nv)}, + label="$f$", + ) + + +def meshgrids(n1=6, n2=7, n3=5): + return np.meshgrid( + np.linspace(1.0, 2.0, n1), + np.linspace(0.0, 2 * np.pi, n2), + np.linspace(-1.0, 1.0, n3), + indexing="ij", + ) + + +# ---------------------------------------------------------------- growth rate + + +def test_growth_rate_recovers_a_known_exponential(): + t = np.linspace(0, 10, 100) + y = StruphyArray(1e-6 * np.exp(0.3 * t), dims=("t",), coords={"t": t}) + + gamma, b, window = growth_rate(y) + assert gamma == pytest.approx(0.3) + assert np.exp(b) == pytest.approx(1e-6, rel=1e-6) + + +def test_growth_rate_of_sqrt_halves_the_exponent(): + """An energy grows at twice the rate of the amplitude it is quadratic in.""" + t = np.linspace(0, 10, 100) + y = StruphyArray(np.exp(0.3 * t), dims=("t",), coords={"t": t}) + assert growth_rate(y, of_sqrt=True)[0] == pytest.approx(0.15) + + +def test_growth_rate_honours_the_window(): + t = np.linspace(0, 10, 101) + y = StruphyArray(np.exp(0.3 * t), dims=("t",), coords={"t": t}) + _, _, window = growth_rate(y, t0=2.0, t1=4.0) + assert t[window][0] >= 2.0 and t[window][-1] <= 4.0 + + +def test_growth_rate_ignores_non_positive_samples(): + t = np.linspace(0, 10, 50) + values = np.exp(0.3 * t) + values[:5] = -1.0 + gamma, _, window = growth_rate(StruphyArray(values, dims=("t",), coords={"t": t})) + assert window.start >= 5 + assert gamma == pytest.approx(0.3) + + +def test_growth_rate_gives_up_cleanly_on_degenerate_input(): + t = np.linspace(0, 1, 4) + y = StruphyArray(-np.ones(4), dims=("t",), coords={"t": t}) + assert growth_rate(y) == (None, None, None) + + +# ---------------------------------------------------------------- grid helpers + + +def test_match_to_grid_transposes_when_that_is_what_fits(): + grid = np.zeros((3, 4)) + np.testing.assert_allclose(match_to_grid(np.zeros((4, 3)), grid).shape, (3, 4)) + np.testing.assert_allclose(match_to_grid(np.zeros((3, 4)), grid).shape, (3, 4)) + + +def test_match_to_grid_rejects_an_incompatible_shape(): + with pytest.raises(ValueError, match="cannot match"): + match_to_grid(np.zeros((5, 9)), np.zeros((3, 4))) + + +def test_logical_grids_uses_the_non_time_dims(): + """Phase-space slices are (e1, v1), not two ``e`` axes.""" + xgrid, ygrid, xlabel, ylabel = logical_grids(phase_space().isel(t=0)) + assert xgrid.shape == (6, 8) + assert xlabel == r"$\eta_1$" + assert ylabel == "$v_1$" + + +def test_logical_grids_needs_exactly_two_plotted_dims(): + three_d = StruphyArray(np.zeros((2, 3, 4, 5)), dims=("t", "e1", "e2", "e3")) + with pytest.raises(ValueError, match="two non-time dims"): + logical_grids(three_d) + + +@pytest.mark.parametrize("plane", sorted(PLANES)) +def test_field_slice_grids_returns_each_plane(plane): + xgrid, ygrid, xlabel, ylabel = field_slice_grids(meshgrids(), fixed_dim="e3", index=0, plane=plane) + assert xgrid.shape == ygrid.shape == (6, 7) + assert (xlabel, ylabel) == (PLANES[plane][2], PLANES[plane][3]) + + +@pytest.mark.parametrize( + "fixed_dim, expected", + [("e1", (7, 5)), ("e2", (6, 5)), ("e3", (6, 7))], +) +def test_field_slice_grids_slices_the_named_axis(fixed_dim, expected): + """Regression: the copy-pasted versions of this disagreed on which axis to cut. + + ``pproc_cyclone`` cut ``arr[:, index, :]`` for the same case where + ``pproc_drift_kinetic`` cut ``arr[:, :, index]``, so one of the two silently + plotted the wrong slice. + """ + xgrid, _, _, _ = field_slice_grids(meshgrids(), fixed_dim=fixed_dim, index=0, plane="XY") + assert xgrid.shape == expected + + +@pytest.mark.parametrize("fixed_dim", ["e1", "e2", "e3"]) +def test_field_slice_grids_agrees_with_isel(fixed_dim): + """The grid and the field must be cut on the same axis, by construction.""" + grids = meshgrids() + field = StruphyArray( + np.random.default_rng(1).random((2, 6, 7, 5)), + dims=("t", "e1", "e2", "e3"), + ) + sliced = field.isel(t=0, **{fixed_dim: 1}) + xgrid, _, _, _ = field_slice_grids(grids, fixed_dim=fixed_dim, index=1, plane="XY") + assert sliced.shape == xgrid.shape + + +def test_field_slice_grids_rejects_unknown_inputs(): + with pytest.raises(ValueError, match="unknown plane"): + field_slice_grids(meshgrids(), plane="QQ") + with pytest.raises(ValueError, match="fixed_dim"): + field_slice_grids(meshgrids(), fixed_dim="e9") + + +# ---------------------------------------------------------------- plotters + + +def test_time_series_labels_axes_from_the_data(): + t = np.linspace(0, 10, 40) + y = StruphyArray(np.exp(0.3 * t), dims=("t",), coords={"t": t}, label="energy").with_coord_units(t="s") + + plot = TimeSeriesPlot(y, fit=True, title="Energy").plot() + assert plot.ax.get_xlabel() == "$t$ [s]" + assert plot.ax.get_ylabel() == "energy [a.u.]" + assert plot.ax.get_yscale() == "log" + assert plot.fit_results[0][0] == pytest.approx(0.3) + + +def test_time_series_fits_every_series(): + """Comparing runs means each curve gets its own rate, not just the first.""" + t = np.linspace(0, 10, 60) + series = [ + StruphyArray(np.exp(rate * t), dims=("t",), coords={"t": t}, label=f"run {rate}") + for rate in (0.2, 0.4) + ] + plot = TimeSeriesPlot(series, fit=True).plot() + assert [f[0] for f in plot.fit_results] == pytest.approx([0.2, 0.4]) + + +def test_time_series_draws_every_series(): + t = np.linspace(0, 1, 10) + a = StruphyArray(np.ones(10), dims=("t",), coords={"t": t}, label="a") + b = StruphyArray(np.ones(10) * 2, dims=("t",), coords={"t": t}, label="b") + assert len(TimeSeriesPlot([a, b], fit=False).plot().ax.get_lines()) == 2 + + +def test_slice_2d_draws_into_a_supplied_axes(): + fig, ax = plt.subplots() + plot = Slice2DPlot(phase_space().isel(t=0), ax=ax).plot() + assert plot.ax is ax + assert plot.mesh is not None + + +def test_panel_grid_spreads_panels_over_the_run(): + plot = PanelGridPlot(phase_space(), nrows=2, ncols=3, shared_clim=True).plot() + axes = plot.ax.ravel() + assert len(axes) == 6 + # first and last panel are the first and last time step + assert axes[0].get_title().endswith("0.00e+00") + assert axes[-1].get_title().endswith("1.00e+00") + + +def test_panel_grid_shares_the_colour_range_when_asked(): + plot = PanelGridPlot(phase_space(), nrows=1, ncols=2, shared_clim=True).plot() + clims = {tuple(c.get_clim()) for ax in plot.ax.ravel() for c in ax.collections} + assert len(clims) == 1 + + +def test_slider_plot_adds_a_second_slider_for_a_free_axis(): + two_d = phase_space() + assert len(SliderPlot(two_d).plot().sliders) == 1 + + three_d = StruphyArray(np.zeros((4, 5, 6, 7)), dims=("t", "e1", "e2", "e3")) + plot = SliderPlot(three_d).plot() + assert plot.slice_dim == "e3" + assert len(plot.sliders) == 2 + + +def test_slider_grids_may_follow_the_cut(): + """A physical grid that depends on where the cut is taken must not go stale.""" + field = StruphyArray(np.zeros((3, 6, 7, 5)), dims=("t", "e1", "e2", "e3")) + asked = [] + + def grids(index): + asked.append(index) + return field_slice_grids(meshgrids(), fixed_dim="e3", index=index, plane="XY") + + plot = SliderPlot(field, grids=grids, slice_dim="e3").plot() + assert plot.slice_dim == "e3" + # built at the initial cut, and re-queried when the slider moves + assert asked == [2] + + plot.sliders[1].set_val(4) + assert asked[-1] == 4 + + +def test_slider_time_updates_the_title(): + field = StruphyArray(np.zeros((3, 6, 7, 5)), dims=("t", "e1", "e2", "e3")) + grids = field_slice_grids(meshgrids(), fixed_dim="e3", index=0, plane="XY") + plot = SliderPlot(field, grids=grids, slice_dim="e3", title="phi").plot() + + plot.sliders[0].set_val(2) + assert plot.ax.get_title() == "phi at t = 2.0000e+00" + + +def test_marker_trajectory_handles_a_species_without_weights(): + with_weight = wrap_orbits(np.random.default_rng(2).random((5, 20, 8)), np.arange(5.0)) + assert MarkerTrajectoryPlot(with_weight, max_markers=4).plot().fig is not None + + without_weight = wrap_orbits(np.random.default_rng(2).random((5, 20, 5)), np.arange(5.0)) + assert MarkerTrajectoryPlot(without_weight, max_markers=4).plot().fig is not None + + +def test_animation_writes_one_frame_per_step(tmp_path): + plot = AnimationPlot(phase_space(nt=10), step=3) + assert list(plot.frames) == [0, 3, 6, 9] + + paths = plot.save_frames(tmp_path) + assert len(paths) == 4 + assert all(p.exists() for p in map(__import__("pathlib").Path, paths)) + + +def test_animation_builds_a_matplotlib_animation(): + anim = AnimationPlot(phase_space(nt=6), step=2).animate() + assert len(list(anim.new_frame_seq())) == 3 + + +def test_save_writes_a_file(tmp_path): + out = tmp_path / "fig.png" + TimeSeriesPlot( + StruphyArray(np.arange(1.0, 5.0), dims=("t",), coords={"t": np.arange(4.0)}), + fit=False, + ).save(out) + assert out.exists() and out.stat().st_size > 0 diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py new file mode 100644 index 000000000..1ff35fef1 --- /dev/null +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -0,0 +1,230 @@ +"""Unit tests for the labeled arrays used to hand post-processed data to the plotters. + +None of these need a simulation: they build small arrays by hand and check that the +dimension bookkeeping, coordinate pairing and back-compatible indexing behave. +""" + +import numpy as np +import pytest + +from struphy.post_processing.arrays import ( + StruphyArray, + orbit_columns, + wrap_binned_slice, + wrap_field_data, + wrap_orbits, +) + + +class Holder: + """Stand-in for the ``Slice`` container the loader populates by setattr.""" + + +def make_array(): + return StruphyArray( + np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4), + dims=("t", "e1", "v1"), + coords={"t": np.array([0.0, 1.0]), "e1": np.linspace(0, 1, 3), "v1": np.linspace(-1, 1, 4)}, + label="$f$", + ) + + +# ---------------------------------------------------------------- StruphyArray + + +def test_rank_must_match_dims(): + with pytest.raises(ValueError, match="rank"): + StruphyArray(np.zeros((2, 3)), dims=("t",)) + + +def test_coord_must_match_axis_length(): + with pytest.raises(ValueError, match="shape"): + StruphyArray(np.zeros((2, 3)), dims=("t", "e1"), coords={"e1": np.zeros(7)}) + + +def test_coord_must_name_a_dim(): + with pytest.raises(ValueError, match="not one of the dims"): + StruphyArray(np.zeros(2), dims=("t",), coords={"e1": np.zeros(2)}) + + +def test_behaves_as_a_plain_array(): + """Existing code that indexes or reduces the raw arrays must keep working.""" + f = make_array() + assert np.asarray(f).shape == (2, 3, 4) + assert f[1].T.shape == (4, 3) + assert np.sum(f) == pytest.approx(np.sum(np.arange(24))) + assert len(f) == 2 + + +def test_isel_drops_int_axes_and_keeps_slices(): + f = make_array() + + dropped = f.isel(t=0) + assert dropped.dims == ("e1", "v1") + assert "t" not in dropped.coords + + kept = f.isel(t=slice(0, 1)) + assert kept.dims == ("t", "e1", "v1") + assert kept.shape == (1, 3, 4) + + +def test_isel_subsets_the_coordinate_of_a_sliced_dim(): + f = make_array() + sub = f.isel(v1=slice(1, 3)) + assert sub.shape[-1] == 2 + np.testing.assert_allclose(sub.coords["v1"], f.coords["v1"][1:3]) + + +def test_at_picks_the_nearest_coordinate(): + """Replaces the ``abs(t_grid - t).argmin()`` idiom, including ties away from a node.""" + f = make_array() + np.testing.assert_allclose(np.asarray(f.at(t=0.4)), np.asarray(f.isel(t=0))) + np.testing.assert_allclose(np.asarray(f.at(t=0.9)), np.asarray(f.isel(t=1))) + + +def test_transpose_to_reorders_values_and_dims(): + f = make_array().isel(t=0) + tr = f.transpose_to("v1", "e1") + assert tr.dims == ("v1", "e1") + np.testing.assert_allclose(np.asarray(tr), np.asarray(f).T) + + +def test_transpose_to_rejects_a_different_dim_set(): + with pytest.raises(ValueError, match="cannot transpose"): + make_array().transpose_to("t", "e1") + + +def test_coord_falls_back_to_an_index_range(): + f = StruphyArray(np.zeros((2, 3)), dims=("t", "e1")) + np.testing.assert_allclose(f.coord("e1"), np.arange(3)) + + +def test_axis_label_includes_units_when_known(): + f = make_array().with_coord_units(t="s") + assert f.axis_label("t") == "$t$ [s]" + assert f.axis_label("e1") == r"$\eta_1$" + assert f.value_label == "$f$ [a.u.]" + + +def test_coord_units_survive_selection(): + f = make_array().with_coord_units(t="s") + assert f.isel(e1=0).coord_units == {"t": "s"} + assert f.isel(t=0).transpose_to("v1", "e1").coord_units == {"t": "s"} + + +def test_unknown_axis_raises(): + with pytest.raises(KeyError): + make_array().axis("nope") + + +# ---------------------------------------------------------------- orbit columns + + +@pytest.mark.parametrize( + "n_columns, expect_weight", + [(8, 6), (5, None), (6, None)], +) +def test_orbit_columns_resolves_weight_from_width(n_columns, expect_weight): + """The saved marker columns depend on the species' velocity dimension. + + A 1V species saves no weight at all, so reading index 6 unconditionally would + silently return a velocity component instead. + """ + cols = orbit_columns(n_columns) + assert cols["position"] == slice(0, 3) + assert cols["id"] == n_columns - 1 + assert cols.get("weight") == expect_weight + + +def test_wrap_orbits_attaches_columns(): + orb = wrap_orbits(np.zeros((4, 10, 8)), np.arange(4.0)) + assert orb.dims == ("t", "marker", "attribute") + assert orb.columns["weight"] == 6 + + +# ---------------------------------------------------------------- binned slices + + +def test_wrap_binned_slice_pairs_grids_with_data(): + holder = Holder() + holder.grid_e1 = np.linspace(0, 1, 3) + holder.grid_v1 = np.linspace(-1, 1, 4) + holder.f_binned = np.zeros((2, 3, 4)) + holder.delta_f_binned = np.zeros((2, 3, 4)) + + wrap_binned_slice(holder, "e1_v1_density", np.array([0.0, 1.0])) + + assert holder.f_binned.dims == ("t", "e1", "v1") + np.testing.assert_allclose(holder.f_binned.coord("v1"), np.linspace(-1, 1, 4)) + assert holder.f_binned.label == "$f$" + assert holder.delta_f_binned.label == r"$\delta f$" + # the grids themselves stay raw + assert isinstance(holder.grid_e1, np.ndarray) + + +def test_wrap_binned_slice_takes_dim_order_from_the_name(): + """``v1_v2_density`` must map axis 0 to v1, not to whichever grid was set first.""" + holder = Holder() + holder.grid_v2 = np.linspace(0, 1, 5) + holder.grid_v1 = np.linspace(0, 1, 3) + holder.f_binned = np.zeros((2, 3, 5)) + + wrap_binned_slice(holder, "v1_v2_density", np.array([0.0, 1.0])) + assert holder.f_binned.dims == ("t", "v1", "v2") + + +def test_wrap_binned_slice_handles_one_dimensional_binning(): + holder = Holder() + holder.grid_e1 = np.linspace(0, 1, 6) + holder.f_binned = np.zeros((2, 6)) + + wrap_binned_slice(holder, "e1_current_1", np.array([0.0, 1.0])) + assert holder.f_binned.dims == ("t", "e1") + + +def test_wrap_binned_slice_leaves_mismatched_entries_alone(): + holder = Holder() + holder.grid_e1 = np.linspace(0, 1, 3) + holder.other = np.zeros((9, 9)) + + wrap_binned_slice(holder, "e1_density", np.array([0.0, 1.0])) + assert isinstance(holder.other, np.ndarray) + + +def test_wrap_binned_slice_without_grids_is_a_noop(): + holder = Holder() + holder.f_binned = np.zeros((2, 3)) + wrap_binned_slice(holder, "whatever", np.array([0.0, 1.0])) + assert isinstance(holder.f_binned, np.ndarray) + + +# ---------------------------------------------------------------- field data + + +def test_wrap_field_data_stacks_vector_components(): + grids = [np.linspace(0, 1, n) for n in (2, 3, 4)] + data = {0.0: [np.zeros((2, 3, 4)) for _ in range(3)], 1.0: [np.ones((2, 3, 4)) for _ in range(3)]} + + arr = wrap_field_data(data, grids, label="B") + assert arr.dims == ("t", "comp", "e1", "e2", "e3") + assert arr.shape == (2, 3, 2, 3, 4) + np.testing.assert_allclose(arr.coord("t"), [0.0, 1.0]) + np.testing.assert_allclose(np.asarray(arr.isel(t=1, comp=0)), 1.0) + + +def test_wrap_field_data_drops_comp_for_scalars(): + grids = [np.linspace(0, 1, n) for n in (2, 3, 4)] + arr = wrap_field_data({0.0: [np.zeros((2, 3, 4))]}, grids, label="phi") + assert arr.dims == ("t", "e1", "e2", "e3") + + +def test_wrap_field_data_sorts_times(): + grids = [np.linspace(0, 1, n) for n in (2, 2, 2)] + data = {1.0: [np.ones((2, 2, 2))], 0.0: [np.zeros((2, 2, 2))]} + arr = wrap_field_data(data, grids) + np.testing.assert_allclose(arr.coord("t"), [0.0, 1.0]) + np.testing.assert_allclose(np.asarray(arr.isel(t=0)), 0.0) + + +def test_wrap_field_data_of_empty_dict_is_none(): + assert wrap_field_data({}, None) is None diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py new file mode 100644 index 000000000..46b87d03c --- /dev/null +++ b/src/struphy/post_processing/tests/test_plotting_data.py @@ -0,0 +1,138 @@ +"""Integration test for loading post-processed data into labeled arrays. + +No simulation is run: a post-processing folder is written out by hand in the layout +:meth:`PostProcessor.process` produces, then loaded back. This covers the wiring that +turns files on disk into the objects the plotting scripts index into. +""" + +import os +import pickle + +import numpy as np +import pytest + +from struphy.post_processing.arrays import StruphyArray +from struphy.post_processing.post_processing_tools import PlottingData + +NT, N1, N2, N3 = 3, 4, 5, 6 +NV = 7 +N_MARKERS = 10 + + +def write_pproc_tree(root): + """Write a minimal post-processing folder, returning the output path.""" + pproc = os.path.join(root, "post_processing") + fields = os.path.join(pproc, "fields_data") + kinetic = os.path.join(pproc, "kinetic_data") + os.makedirs(fields) + os.makedirs(kinetic) + + t_grid = np.linspace(0.0, 1.0, NT) + np.save(os.path.join(pproc, "t_grid.npy"), t_grid) + + grids_log = [np.linspace(0, 1, n) for n in (N1, N2, N3)] + grids_phy = list(np.meshgrid(*grids_log, indexing="ij")) + for name, grids in (("grids_log", grids_log), ("grids_phy", grids_phy)): + with open(os.path.join(fields, f"{name}.bin"), "wb") as f: + pickle.dump(grids, f) + + # a vector field and a scalar field, keyed by time as the post-processor writes them + species_dir = os.path.join(fields, "em_fields") + os.makedirs(species_dir) + vector = {t: [np.full((N1, N2, N3), i + t) for i in range(3)] for t in t_grid} + scalar = {t: [np.full((N1, N2, N3), t)] for t in t_grid} + for name, data in (("e_field_log", vector), ("phi_phy", scalar)): + with open(os.path.join(species_dir, f"{name}.bin"), "wb") as f: + pickle.dump(data, f) + + # binned distribution function + slice_dir = os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density") + os.makedirs(slice_dir) + np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) + np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) + np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) + np.save(os.path.join(slice_dir, "delta_f_binned.npy"), np.zeros((NT, N1, NV))) + + # marker orbits: one .npy and one .txt per saved step + orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") + os.makedirs(orbit_dir) + for step in range(NT): + np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), float(step))) + open(os.path.join(orbit_dir, f"kinetic_ions_{step}.txt"), "w").close() + + return root + + +@pytest.fixture +def pdata(tmp_path): + out = write_pproc_tree(str(tmp_path)) + data = PlottingData(path_out=out) + data.load() + return data + + +def test_load_without_raw_data_skips_scalars(pdata): + """Scalars come from the raw HDF5, which a post-processing-only folder lacks.""" + assert pdata.scalars.keys() == () + + +def test_grids_are_loaded(pdata): + assert len(pdata.grids_log) == 3 + assert pdata.grids_phy[0].shape == (N1, N2, N3) + np.testing.assert_allclose(pdata.t_grid, np.linspace(0.0, 1.0, NT)) + + +def test_containers_are_discoverable(pdata): + """Contents can be listed instead of having to be known in advance.""" + assert "em_fields" in pdata.spline_values + assert set(pdata.spline_values["em_fields"].keys()) == {"e_field_log", "phi_phy"} + assert "e1_v1_density" in pdata.f["kinetic_ions"] + + +def test_field_becomes_one_labeled_array(pdata): + """The chain the migrated plotting scripts use.""" + field = pdata.spline_values["em_fields"]["e_field_log"].array + + assert field.dims == ("t", "comp", "e1", "e2", "e3") + assert field.shape == (NT, 3, N1, N2, N3) + np.testing.assert_allclose(field.coord("e2"), np.linspace(0, 1, N2)) + # component i at time t was filled with i + t + np.testing.assert_allclose(np.asarray(field.isel(t=0, comp=2)), 2.0) + + +def test_scalar_field_has_no_component_axis(pdata): + assert pdata.spline_values["em_fields"]["phi_phy"].array.dims == ("t", "e1", "e2", "e3") + + +def test_raw_field_dict_still_available(pdata): + """Existing scripts index ``.data[t][component]`` directly.""" + dd = pdata.spline_values["em_fields"]["e_field_log"] + assert dd.data[0.0][1].shape == (N1, N2, N3) + + +def test_binned_data_carries_its_grids(pdata): + f = pdata.f["kinetic_ions"]["e1_v1_density"]["f_binned"] + + assert isinstance(f, StruphyArray) + assert f.dims == ("t", "e1", "v1") + np.testing.assert_allclose(f.coord("v1"), np.linspace(-3, 3, NV)) + assert f[0].T.shape == (NV, N1) # back-compat indexing + + +def test_orbits_are_labeled_with_columns(pdata): + orbits = pdata.orbits["kinetic_ions"] + + assert orbits.dims == ("t", "marker", "attribute") + assert orbits.shape == (NT, N_MARKERS, 8) + assert orbits.columns["weight"] == 6 + # step n was filled with the value n + np.testing.assert_allclose(np.asarray(orbits.isel(t=2)), 2.0) + + +def test_field_slice_matches_the_physical_grid(pdata): + """A cut field and its grid must have the same shape, which is what the plotters assume.""" + from struphy.diagnostics.plotting import field_slice_grids + + cut = pdata.spline_values["em_fields"]["phi_phy"].array.isel(t=0, e3=1) + xgrid, _, _, _ = field_slice_grids(pdata.grids_phy, fixed_dim="e3", index=1, plane="XY") + assert cut.shape == xgrid.shape From a3f06c0f44240e202245909cf9f6d6691613153e Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 8 Sep 2026 14:33:46 +0300 Subject: [PATCH 005/193] Log a warning if scalar doesn't exist --- src/struphy/post_processing/post_processing_tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index eaffb151b..8fe5bc6e6 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1596,6 +1596,9 @@ def load_scalars(self, *, physical_time: bool = True): t_unit_label = "s" if physical_time else "a.u." with h5py.File(path_data, "r") as f: + if "scalar" not in f: + logger.warning(f"No scalar diagnostics saved in {path_data}, skipping scalars.") + return self._scalars t = xp.asarray(f["time"]["value"][()]) * unit_t for name in f["scalar"].keys(): arr = StruphyArray( From 423d89ec2539fcc5bb88ee573083a780a2fe24cd Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 13 Sep 2026 16:01:25 +0200 Subject: [PATCH 006/193] cleanup --- .claude/skills/setup-simulation/SKILL.md | 18 +- .../pull_request_template.md | 2 +- .github/actions/compile/action.yml | 2 +- .../install/feectools-submodule/action.yml | 20 +- .../install-struphy-editable/action.yml | 3 +- .../install/install-struphy/action.yml | 6 +- .../actions/install/macos-latest/action.yml | 3 +- .../install/struphy_in_container/action.yml | 38 +- .../actions/install/ubuntu-latest/action.yml | 2 +- .github/actions/struphy-version/action.yml | 2 +- .github/actions/submodule-diff/action.yml | 2 +- .github/issue_template.md | 2 - .github/pull_request_template.md | 2 +- .github/workflows/create-PR-parameters.yml | 40 +- .github/workflows/create-PR-tutorials.yml | 46 +- .github/workflows/docs-preview.yml | 4 +- .github/workflows/docs.yml | 2 +- .github/workflows/gh-release.yml | 14 +- .github/workflows/ghcr.yml | 7 +- .github/workflows/profiling-clusters.yml | 4 +- .github/workflows/pypi-release.yml | 42 +- .../workflows/reusable-profiling-clusters.yml | 6 +- .github/workflows/reusable-scheduled.yml | 2 +- .github/workflows/reusable-unit-testing.yml | 8 +- .github/workflows/scheduled-macos.yml | 6 +- .github/workflows/scheduled-ubuntu.yml | 4 +- .github/workflows/static_analysis.yml | 4 +- .github/workflows/submod-feectools.yml | 6 +- .github/workflows/test-PR-examples.yml | 10 +- .github/workflows/test-PR-generate-params.yml | 8 +- .github/workflows/test-PR-models-clones.yml | 4 +- .github/workflows/test-PR-models.yml | 6 +- .github/workflows/test-PR-pure-python.yml | 24 +- .github/workflows/test-PR-tutorials.yml | 12 +- .github/workflows/test-PR-unit.yml | 2 +- .github/workflows/test-clusters.yml | 4 +- .gitlab-ci.yml | 145 ++-- .pre-commit-config.yaml | 10 +- .readthedocs.yml | 10 +- CHANGELOG.md | 232 +++---- CONTRIBUTING.md | 22 +- README.md | 11 +- doc/_static/css/custom.css | 9 +- doc/_static/my_theme.css | 2 +- doc/markdown/vlasov-maxwell.md | 657 +++++++++++------- setup/modules.json | 12 +- 46 files changed, 785 insertions(+), 692 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index cf79abc83..cdcb405d5 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -7,7 +7,7 @@ description: Use when creating or editing a Struphy simulation parameter file (p Struphy simulations are configured as plain Python scripts (`params_.py`) that build a `Simulation` object from the Struphy API, then call `sim.run()`. There is no -YAML/JSON config — the params file *is* the config, so it can use real Python +YAML/JSON config — the params file _is_ the config, so it can use real Python (loops, conditionals, computed values) to derive parameters. ## Workflow @@ -116,15 +116,15 @@ if __name__ == "__main__": Key building blocks and where to look them up: -| Piece | Source | -|---|---| -| Domains (`Cuboid`, `HollowTorus`, `Tokamak`, ...) | `src/struphy/geometry/domains.py` | -| Grid | `src/struphy/topology/grids.py` (`TensorProductGrid`) | -| Fluid equilibria | `src/struphy/fields_background/equils.py` | -| Perturbations | `src/struphy/initial/perturbations.py` | -| Kinetic backgrounds | `src/struphy/kinetic_background/maxwellians.py` | +| Piece | Source | +| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| Domains (`Cuboid`, `HollowTorus`, `Tokamak`, ...) | `src/struphy/geometry/domains.py` | +| Grid | `src/struphy/topology/grids.py` (`TensorProductGrid`) | +| Fluid equilibria | `src/struphy/fields_background/equils.py` | +| Perturbations | `src/struphy/initial/perturbations.py` | +| Kinetic backgrounds | `src/struphy/kinetic_background/maxwellians.py` | | Options dataclasses (`Time`, `BaseUnits`, `DerhamOptions`, `EnvironmentOptions`, `FieldsBackground`) | `src/struphy/io/options.py` (full docstrings with every field) | -| Models and their propagators/species | `src/struphy/models/.py` | +| Models and their propagators/species | `src/struphy/models/.py` | ## Post-processing pattern diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 87f9ff3cc..a307ed6c2 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -12,4 +12,4 @@ None **Documentation changes:** -None \ No newline at end of file +None diff --git a/.github/actions/compile/action.yml b/.github/actions/compile/action.yml index 8b7e237b7..b5b968e35 100644 --- a/.github/actions/compile/action.yml +++ b/.github/actions/compile/action.yml @@ -1,6 +1,6 @@ name: "Compile kernels with pyccel" -description: +description: inputs: language: diff --git a/.github/actions/install/feectools-submodule/action.yml b/.github/actions/install/feectools-submodule/action.yml index ab1a0c9f5..5de8b49be 100644 --- a/.github/actions/install/feectools-submodule/action.yml +++ b/.github/actions/install/feectools-submodule/action.yml @@ -14,13 +14,13 @@ runs: - name: Install feectools from submodule shell: bash run: | - if [ -n "${{ inputs.env-name }}" ] ; then - echo "Using env: ${{ inputs.env-name }}" - source ${{ inputs.env-name }}/bin/activate - else - echo "No env specified, installing outside of any env" - fi - pip show feectools - psydac-accelerate --cleanup --yes - pip uninstall feectools -y - python3 -m pip install ./feectools/ + if [ -n "${{ inputs.env-name }}" ] ; then + echo "Using env: ${{ inputs.env-name }}" + source ${{ inputs.env-name }}/bin/activate + else + echo "No env specified, installing outside of any env" + fi + pip show feectools + psydac-accelerate --cleanup --yes + pip uninstall feectools -y + python3 -m pip install ./feectools/ diff --git a/.github/actions/install/install-struphy-editable/action.yml b/.github/actions/install/install-struphy-editable/action.yml index 50e088c17..8728e3d39 100644 --- a/.github/actions/install/install-struphy-editable/action.yml +++ b/.github/actions/install/install-struphy-editable/action.yml @@ -1,11 +1,10 @@ name: "Clone and install struphy" -description: +description: runs: using: composite steps: - - name: Install struphy shell: bash run: | diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml index 0f26d5cf0..35d469a8f 100644 --- a/.github/actions/install/install-struphy/action.yml +++ b/.github/actions/install/install-struphy/action.yml @@ -1,12 +1,12 @@ name: "Install struphy (in virtual environment)" -description: +description: inputs: - env-name: + env-name: default: "" optional-deps: - default: 'dev' + default: "dev" runs: using: composite diff --git a/.github/actions/install/macos-latest/action.yml b/.github/actions/install/macos-latest/action.yml index 0dc3f859c..480d47dc3 100644 --- a/.github/actions/install/macos-latest/action.yml +++ b/.github/actions/install/macos-latest/action.yml @@ -1,6 +1,6 @@ name: "Install MacOS prereqs" -description: +description: runs: using: composite @@ -32,4 +32,3 @@ runs: echo "FC=$(which gfortran)" >> $GITHUB_ENV # for gvec echo "CC=$(which gcc)" >> $GITHUB_ENV # for gvec echo "CXX=$(which g++)" >> $GITHUB_ENV # for gvec - diff --git a/.github/actions/install/struphy_in_container/action.yml b/.github/actions/install/struphy_in_container/action.yml index 5f4405ccb..1e4bef8b4 100644 --- a/.github/actions/install/struphy_in_container/action.yml +++ b/.github/actions/install/struphy_in_container/action.yml @@ -1,6 +1,6 @@ name: "Install Struphy in Container" -description: +description: runs: using: composite @@ -14,21 +14,21 @@ runs: - name: Install struphy shell: bash run: | - ls / -a - which python3 - cd /struphy_fortran_ - ls -a - git status - git fetch origin - echo ${GIT_BRANCH_NAME} - git checkout ${GIT_BRANCH_NAME} - git pull - git status - source env_fortran_/bin/activate - which python3 - pip install -U --upgrade-strategy eager -e ".[phys,mpi]" - pip install -e ".[doc]" - PYTHON=$(which python) - STRUPHY_PATH=$($PYTHON -c 'import importlib.util; import os; print(os.path.dirname(importlib.util.find_spec("struphy").origin))') - echo "Struphy is installed at: $STRUPHY_PATH" - echo "STRUPHY_PATH=${STRUPHY_PATH}" >> $GITHUB_ENV \ No newline at end of file + ls / -a + which python3 + cd /struphy_fortran_ + ls -a + git status + git fetch origin + echo ${GIT_BRANCH_NAME} + git checkout ${GIT_BRANCH_NAME} + git pull + git status + source env_fortran_/bin/activate + which python3 + pip install -U --upgrade-strategy eager -e ".[phys,mpi]" + pip install -e ".[doc]" + PYTHON=$(which python) + STRUPHY_PATH=$($PYTHON -c 'import importlib.util; import os; print(os.path.dirname(importlib.util.find_spec("struphy").origin))') + echo "Struphy is installed at: $STRUPHY_PATH" + echo "STRUPHY_PATH=${STRUPHY_PATH}" >> $GITHUB_ENV diff --git a/.github/actions/install/ubuntu-latest/action.yml b/.github/actions/install/ubuntu-latest/action.yml index e87f36cdd..81d0b597e 100644 --- a/.github/actions/install/ubuntu-latest/action.yml +++ b/.github/actions/install/ubuntu-latest/action.yml @@ -1,6 +1,6 @@ name: "Install ubuntu prereqs" -description: +description: runs: using: composite diff --git a/.github/actions/struphy-version/action.yml b/.github/actions/struphy-version/action.yml index 077c6ac44..016095cd3 100644 --- a/.github/actions/struphy-version/action.yml +++ b/.github/actions/struphy-version/action.yml @@ -13,4 +13,4 @@ runs: echo "Failed to parse version from pyproject.toml" >&2 exit 1 fi - echo "STRUPHY_VERSION=$STRUPHY_VERSION" >> "$GITHUB_ENV" \ No newline at end of file + echo "STRUPHY_VERSION=$STRUPHY_VERSION" >> "$GITHUB_ENV" diff --git a/.github/actions/submodule-diff/action.yml b/.github/actions/submodule-diff/action.yml index 77139a736..87dc03ff5 100644 --- a/.github/actions/submodule-diff/action.yml +++ b/.github/actions/submodule-diff/action.yml @@ -35,4 +35,4 @@ runs: echo "SUBMOD_CHANGED=true" >> $GITHUB_ENV echo "SUBMOD_NAME=${{ inputs.submod-name }}" >> $GITHUB_ENV exit 0 - fi \ No newline at end of file + fi diff --git a/.github/issue_template.md b/.github/issue_template.md index b2c4eb7bc..87d6f257b 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -9,5 +9,3 @@ **Proposed solution:** ... - - diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 87f9ff3cc..a307ed6c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,4 +12,4 @@ None **Documentation changes:** -None \ No newline at end of file +None diff --git a/.github/workflows/create-PR-parameters.yml b/.github/workflows/create-PR-parameters.yml index c42a24996..616db493e 100644 --- a/.github/workflows/create-PR-parameters.yml +++ b/.github/workflows/create-PR-parameters.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Read Struphy version from pyproject @@ -29,26 +29,26 @@ jobs: - name: Copy examples and push new branch run: | - cd struphy-parameter-files - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - cp -r ../examples/. ./ - git status - git add . - git commit -m "Changes to parameter files from struphy (on push to main)" - git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + cd struphy-parameter-files + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + cp -r ../examples/. ./ + git status + git add . + git commit -m "Changes to parameter files from struphy (on push to main)" + git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - name: Create version tag in struphy-parameter-files run: | - cd struphy-parameter-files - TAG_NAME="v${STRUPHY_VERSION}" - if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then - echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." - else - git tag -a "${TAG_NAME}" -m "Parameter files for Struphy v${STRUPHY_VERSION}" - git push origin "${TAG_NAME}" - fi + cd struphy-parameter-files + TAG_NAME="v${STRUPHY_VERSION}" + if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then + echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." + else + git tag -a "${TAG_NAME}" -m "Parameter files for Struphy v${STRUPHY_VERSION}" + git push origin "${TAG_NAME}" + fi - name: Create pull request run: | @@ -57,6 +57,6 @@ jobs: --title "New parameter files from struphy v${STRUPHY_VERSION}" \ --body "This is an auto-generated PR from a Struphy workflow (on push to main)." \ --head from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} \ - --base main + --base main env: - GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} diff --git a/.github/workflows/create-PR-tutorials.yml b/.github/workflows/create-PR-tutorials.yml index 826a19a7b..4de0178b1 100644 --- a/.github/workflows/create-PR-tutorials.yml +++ b/.github/workflows/create-PR-tutorials.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Read Struphy version from pyproject @@ -29,29 +29,29 @@ jobs: - name: Copy tutorials and push new branch run: | - cd struphy-tutorials - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - ls -a - rm ./tutorial* - ls -a - cp -r ../tutorials/. ./ - git status - git add . - git commit -m "Changes to tutorials from struphy (on push to main)" - git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + cd struphy-tutorials + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + ls -a + rm ./tutorial* + ls -a + cp -r ../tutorials/. ./ + git status + git add . + git commit -m "Changes to tutorials from struphy (on push to main)" + git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - name: Create version tag in struphy-tutorials run: | - cd struphy-tutorials - TAG_NAME="v${STRUPHY_VERSION}" - if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then - echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." - else - git tag -a "${TAG_NAME}" -m "Tutorials for Struphy v${STRUPHY_VERSION}" - git push origin "${TAG_NAME}" - fi + cd struphy-tutorials + TAG_NAME="v${STRUPHY_VERSION}" + if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then + echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." + else + git tag -a "${TAG_NAME}" -m "Tutorials for Struphy v${STRUPHY_VERSION}" + git push origin "${TAG_NAME}" + fi - name: Create pull request run: | @@ -60,6 +60,6 @@ jobs: --title "New tutorials from struphy v${STRUPHY_VERSION}" \ --body "This is an auto-generated PR from a Struphy workflow (on push to main)." \ --head from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} \ - --base main + --base main env: - GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index fd17f1678..293af805b 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to build/publish a preview for' + description: "PR number to build/publish a preview for" required: true concurrency: @@ -94,7 +94,7 @@ jobs: - name: Install Struphy (dev-doc) uses: ./.github/actions/install/install-struphy with: - optional-deps: 'dev,doc' + optional-deps: "dev,doc" - name: Compile Struphy uses: ./.github/actions/compile diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5c9f89c76..6b79ea907 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,7 +46,7 @@ jobs: - name: Install Struphy (dev-doc) uses: ./.github/actions/install/install-struphy with: - optional-deps: 'dev,doc' + optional-deps: "dev,doc" - name: Compile Struphy uses: ./.github/actions/compile diff --git a/.github/workflows/gh-release.yml b/.github/workflows/gh-release.yml index 27c946633..e3903f7c9 100644 --- a/.github/workflows/gh-release.yml +++ b/.github/workflows/gh-release.yml @@ -2,7 +2,7 @@ name: Release Struphy on Github on: push: - branches: + branches: - main jobs: @@ -21,14 +21,14 @@ jobs: uses: maybe-hello-world/pyproject-check-version@v4 id: versioncheck with: - pyproject-path: "./pyproject.toml" # default value - + pyproject-path: "./pyproject.toml" # default value + - name: Check output shell: bash run: | - echo "Output: ${{ steps.versioncheck.outputs.local_version_is_higher }}" # 'true' or 'false - echo "Local version: ${{ steps.versioncheck.outputs.local_version }}" # e.g., 0.1.1 - echo "Public version: ${{ steps.versioncheck.outputs.public_version }}" # e.g., 0.1.0 + echo "Output: ${{ steps.versioncheck.outputs.local_version_is_higher }}" # 'true' or 'false + echo "Local version: ${{ steps.versioncheck.outputs.local_version }}" # e.g., 0.1.1 + echo "Public version: ${{ steps.versioncheck.outputs.public_version }}" # e.g., 0.1.0 - name: Release uses: softprops/action-gh-release@v2 @@ -38,5 +38,3 @@ jobs: with: tag_name: v${{ steps.versioncheck.outputs.local_version }} body_path: ${{ github.workspace }}/CHANGELOG.md - - \ No newline at end of file diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index 17a0706bc..28fee8bdf 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -10,7 +10,7 @@ on: # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: REGISTRY: ghcr.io - IMAGE_NAME_1: ${{ github.repository }}/ubuntu-with-reqs + IMAGE_NAME_1: ${{ github.repository }}/ubuntu-with-reqs IMAGE_NAME_2: ${{ github.repository }}/ubuntu-with-struphy # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. @@ -52,7 +52,7 @@ jobs: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_1 }}:latest labels: ${{ steps.meta.outputs.labels }} - + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 @@ -98,7 +98,7 @@ jobs: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2 }}:latest labels: ${{ steps.meta.outputs.labels }} - + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 @@ -106,4 +106,3 @@ jobs: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2}} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true - diff --git a/.github/workflows/profiling-clusters.yml b/.github/workflows/profiling-clusters.yml index 18b3c6b31..95b5ae38d 100644 --- a/.github/workflows/profiling-clusters.yml +++ b/.github/workflows/profiling-clusters.yml @@ -14,7 +14,7 @@ jobs: # uses: ./.github/workflows/reusable-profiling-clusters.yml # with: # runner_tags: '["self-hosted", "protected", "raven"]' - + # Viper: # uses: ./.github/workflows/reusable-profiling-clusters.yml # with: @@ -23,7 +23,7 @@ jobs: # uses: ./.github/workflows/reusable-profiling-clusters.yml # with: # runner_tags: '["self-hosted", "tok"]' - + Pitagora: uses: ./.github/workflows/reusable-profiling-clusters.yml with: diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index 2afbaa837..c7acf5446 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -17,26 +17,26 @@ jobs: url: https://pypi.org/project/struphy/ permissions: - id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: Install build tools - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Build the package - run: python -m build - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - # with: - # repository-url: https://test.pypi.org/legacy/ \ No newline at end of file + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build the package + run: python -m build + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/reusable-profiling-clusters.yml b/.github/workflows/reusable-profiling-clusters.yml index 1459915d6..736d51bf4 100644 --- a/.github/workflows/reusable-profiling-clusters.yml +++ b/.github/workflows/reusable-profiling-clusters.yml @@ -42,7 +42,7 @@ jobs: curl -fsSL https://raw.githubusercontent.com/max-models/whereami/main/install.sh | bash export PATH="${HOME}/.local/bin:${PATH}" whereami - + - name: Create a virtual environment for Fortran shell: bash run: | @@ -62,7 +62,7 @@ jobs: fi source "$VENV_NAME/bin/activate" echo "Activated virtual environment" - + - name: Install dependencies shell: bash run: | @@ -80,7 +80,7 @@ jobs: source .venv-fortran/bin/activate # python -c "import struphy; print(struphy.__version__)" struphy --help - + - name: Verify MPI installation shell: bash run: | diff --git a/.github/workflows/reusable-scheduled.yml b/.github/workflows/reusable-scheduled.yml index fa12f281a..23ee9b99b 100644 --- a/.github/workflows/reusable-scheduled.yml +++ b/.github/workflows/reusable-scheduled.yml @@ -76,7 +76,7 @@ jobs: - name: Install struphy uses: ./.github/actions/install/install-struphy with: - optional-deps: 'mpi,phys' + optional-deps: "mpi,phys" env: FC: ${{ env.FC }} CC: ${{ env.CC }} diff --git a/.github/workflows/reusable-unit-testing.yml b/.github/workflows/reusable-unit-testing.yml index 3910378aa..4c2077b7e 100644 --- a/.github/workflows/reusable-unit-testing.yml +++ b/.github/workflows/reusable-unit-testing.yml @@ -43,7 +43,7 @@ jobs: - shard: shard-3 test_files: feec/tests/ - shard: shard-4 - test_files: pic/tests/ + test_files: pic/tests/ container: image: ghcr.io/struphy-hub/struphy/ubuntu-with-struphy:latest @@ -63,7 +63,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Set environment for serial run @@ -108,7 +108,7 @@ jobs: - name: Install Struphy in Container uses: ./.github/actions/install/struphy_in_container - + - name: Get submodule diff uses: ./.github/actions/submodule-diff with: @@ -143,4 +143,4 @@ jobs: $UNINSTALL_MPI git status || true echo "Running $PYTEST_CMD $TEST_FILES" - $PYTEST_CMD $TEST_FILES \ No newline at end of file + $PYTEST_CMD $TEST_FILES diff --git a/.github/workflows/scheduled-macos.yml b/.github/workflows/scheduled-macos.yml index 56aa6e462..42e2b49f7 100644 --- a/.github/workflows/scheduled-macos.yml +++ b/.github/workflows/scheduled-macos.yml @@ -3,8 +3,8 @@ name: Scheduled MacOS on: schedule: # run at 1 a.m. on Sundays and Wednesdays - - cron: "0 1 * * 0" - - cron: "0 1 * * 3" + - cron: "0 1 * * 0" + - cron: "0 1 * * 3" workflow_dispatch: concurrency: @@ -15,4 +15,4 @@ jobs: macos-install-test: uses: ./.github/workflows/reusable-scheduled.yml with: - os: macos-latest \ No newline at end of file + os: macos-latest diff --git a/.github/workflows/scheduled-ubuntu.yml b/.github/workflows/scheduled-ubuntu.yml index 97d497958..866a831f9 100644 --- a/.github/workflows/scheduled-ubuntu.yml +++ b/.github/workflows/scheduled-ubuntu.yml @@ -3,7 +3,7 @@ name: Scheduled Ubuntu on: schedule: # run at 1 a.m. on Sundays and Wednesdays - - cron: "0 1 * * 0" + - cron: "0 1 * * 0" - cron: "0 1 * * 3" workflow_dispatch: @@ -15,4 +15,4 @@ jobs: ubuntu-install-test: uses: ./.github/workflows/reusable-scheduled.yml with: - os: ubuntu-latest \ No newline at end of file + os: ubuntu-latest diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index e2f031bfc..49958455c 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -33,7 +33,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Set up Python ${{ matrix.python-version }} @@ -117,7 +117,7 @@ jobs: run: | pip install ruff==0.15.0 ruff check - + # - name: ruff format --check # run: | # ruff format --check diff --git a/.github/workflows/submod-feectools.yml b/.github/workflows/submod-feectools.yml index 33f8d9e40..a0d0a9bc9 100644 --- a/.github/workflows/submod-feectools.yml +++ b/.github/workflows/submod-feectools.yml @@ -21,15 +21,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Get submodule diff uses: ./.github/actions/submodule-diff with: start-dir: "." - + - name: Run workflow if submodule changed (all PR commits) if: env.SUBMOD_CHANGED == 'true' run: | - echo "${{ env.SUBMOD_NAME }} has changed, running tests..." \ No newline at end of file + echo "${{ env.SUBMOD_NAME }} has changed, running tests..." diff --git a/.github/workflows/test-PR-examples.yml b/.github/workflows/test-PR-examples.yml index 83cb3f8a5..c69a02594 100644 --- a/.github/workflows/test-PR-examples.yml +++ b/.github/workflows/test-PR-examples.yml @@ -34,7 +34,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Check .testmondata 1 @@ -58,7 +58,7 @@ jobs: run: | ls .testmon* || echo "No .testmondata" - - name: Install Struphy in Container + - name: Install Struphy in Container uses: ./.github/actions/install/struphy_in_container - name: Get submodule diff @@ -66,8 +66,8 @@ jobs: with: start-dir: /struphy_fortran_ - - name: Reinstall feectools from submodule - if: env.SUBMOD_CHANGED == 'true' + - name: Reinstall feectools from submodule + if: env.SUBMOD_CHANGED == 'true' uses: ./.github/actions/install/feectools-submodule with: env-name: /struphy_fortran_/env_fortran_ @@ -76,7 +76,7 @@ jobs: uses: ./.github/actions/compile with: env-name: /struphy_fortran_/env_fortran_ - + - name: Run examples tests (serial) env: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-examples diff --git a/.github/workflows/test-PR-generate-params.yml b/.github/workflows/test-PR-generate-params.yml index 7bc4b016b..4b2930662 100644 --- a/.github/workflows/test-PR-generate-params.yml +++ b/.github/workflows/test-PR-generate-params.yml @@ -29,14 +29,13 @@ jobs: username: spossann password: ${{ secrets.GHCR_TOKEN }} steps: - - name: Check for dockerenv file run: (ls /.dockerenv && echo Found dockerenv) || (echo No dockerenv) - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: fetch origin devel (for submodule-diff action) @@ -47,7 +46,7 @@ jobs: git fetch --no-tags origin devel:refs/remotes/origin/devel git rev-parse --verify origin/devel - - name: Build Struphy + - name: Build Struphy run: | ls python3 -m venv struphy_env @@ -81,6 +80,3 @@ jobs: source struphy_env/bin/activate struphy params Maxwell -y struphy params VlasovAmpereOneSpecies -y - - - diff --git a/.github/workflows/test-PR-models-clones.yml b/.github/workflows/test-PR-models-clones.yml index 7b729f217..6349c73a9 100644 --- a/.github/workflows/test-PR-models-clones.yml +++ b/.github/workflows/test-PR-models-clones.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Check .testmondata 1 @@ -88,4 +88,4 @@ jobs: source /struphy_fortran_/env_fortran_/bin/activate cd /struphy_fortran_/src/struphy mpirun -n 1 pytest -m single --testmon-forceselect -xs --with-mpi --model-name Maxwell - mpirun --oversubscribe -n 4 pytest -x --testmon --with-mpi --nclones 2 models/tests/verification/ \ No newline at end of file + mpirun --oversubscribe -n 4 pytest -x --testmon --with-mpi --nclones 2 models/tests/verification/ diff --git a/.github/workflows/test-PR-models.yml b/.github/workflows/test-PR-models.yml index 4a2e83442..60caab2f4 100644 --- a/.github/workflows/test-PR-models.yml +++ b/.github/workflows/test-PR-models.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Check .testmondata 1 @@ -94,7 +94,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-model run: | source /struphy_fortran_/env_fortran_/bin/activate - pytest -x -m models --testmon-forceselect /struphy_fortran_/src/struphy/models/tests/default_params/ + pytest -x -m models --testmon-forceselect /struphy_fortran_/src/struphy/models/tests/default_params/ - name: Verification tests shell: bash @@ -121,4 +121,4 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-model-mpi run: | source /struphy_fortran_/env_fortran_/bin/activate - mpirun --oversubscribe -n 2 pytest -x --testmon --with-mpi /struphy_fortran_/src/struphy/models/tests/verification/ \ No newline at end of file + mpirun --oversubscribe -n 2 pytest -x --testmon --with-mpi /struphy_fortran_/src/struphy/models/tests/verification/ diff --git a/.github/workflows/test-PR-pure-python.yml b/.github/workflows/test-PR-pure-python.yml index 85f52858f..d118b5655 100644 --- a/.github/workflows/test-PR-pure-python.yml +++ b/.github/workflows/test-PR-pure-python.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: fetch origin devel @@ -89,14 +89,14 @@ jobs: source env/bin/activate struphy compile -d struphy compile --status - + - name: MPI run check shell: bash run: | - source env/bin/activate - mpirun --version - mpirun --oversubscribe -n 1 python -c "from mpi4py import MPI; print('rank:', MPI.COMM_WORLD.rank); print('size:', MPI.COMM_WORLD.size)" - + source env/bin/activate + mpirun --version + mpirun --oversubscribe -n 1 python -c "from mpi4py import MPI; print('rank:', MPI.COMM_WORLD.rank); print('size:', MPI.COMM_WORLD.size)" + # - name: Pytest-MPI plugin check # shell: bash # run: | @@ -109,7 +109,7 @@ jobs: source env/bin/activate cd src/struphy mpirun --oversubscribe -n 1 pytest --collect-only -q - + - name: Manual LinearMHD test shell: bash env: @@ -142,7 +142,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python run: | source env/bin/activate - pytest -m single -xvs --model-name GuidingCenter $STRUPHY_PATH + pytest -m single -xvs --model-name GuidingCenter $STRUPHY_PATH - name: VlasovAmpere test shell: bash @@ -150,7 +150,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python run: | source env/bin/activate - pytest -m single -xvs --model-name VlasovAmpereOneSpecies $STRUPHY_PATH + pytest -m single -xvs --model-name VlasovAmpereOneSpecies $STRUPHY_PATH - name: ViscousEulerSPH test shell: bash @@ -158,7 +158,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python run: | source env/bin/activate - pytest -m single -xvs --model-name ViscousEulerSPH $STRUPHY_PATH + pytest -m single -xvs --model-name ViscousEulerSPH $STRUPHY_PATH - name: Vlasov test MPI shell: bash @@ -184,7 +184,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name VlasovAmpereOneSpecies $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name VlasovAmpereOneSpecies $STRUPHY_PATH - name: ViscousEulerSPH test MPI shell: bash @@ -192,4 +192,4 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name ViscousEulerSPH $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name ViscousEulerSPH $STRUPHY_PATH diff --git a/.github/workflows/test-PR-tutorials.yml b/.github/workflows/test-PR-tutorials.yml index 9acb3754f..0ddf38e17 100644 --- a/.github/workflows/test-PR-tutorials.yml +++ b/.github/workflows/test-PR-tutorials.yml @@ -34,10 +34,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - - name: Install Struphy in Container + - name: Install Struphy in Container uses: ./.github/actions/install/struphy_in_container - name: Get submodule diff @@ -45,8 +45,8 @@ jobs: with: start-dir: /struphy_fortran_ - - name: Reinstall feectools from submodule - if: env.SUBMOD_CHANGED == 'true' + - name: Reinstall feectools from submodule + if: env.SUBMOD_CHANGED == 'true' uses: ./.github/actions/install/feectools-submodule with: env-name: /struphy_fortran_/env_fortran_ @@ -55,7 +55,7 @@ jobs: uses: ./.github/actions/compile with: env-name: /struphy_fortran_/env_fortran_ - + - name: Run tutorials env: PYVISTA_OFF_SCREEN: "true" @@ -68,4 +68,4 @@ jobs: cd /struphy_fortran_/tutorials ls jupyter nbconvert --show-config - jupyter nbconvert --execute --embed-images --to html tutorial_*.ipynb \ No newline at end of file + jupyter nbconvert --execute --embed-images --to html tutorial_*.ipynb diff --git a/.github/workflows/test-PR-unit.yml b/.github/workflows/test-PR-unit.yml index 10cdf62c4..849a88954 100644 --- a/.github/workflows/test-PR-unit.yml +++ b/.github/workflows/test-PR-unit.yml @@ -20,4 +20,4 @@ jobs: os: ubuntu-latest n-procs: 1 secrets: - ghcr-token: ${{ secrets.GHCR_TOKEN }} \ No newline at end of file + ghcr-token: ${{ secrets.GHCR_TOKEN }} diff --git a/.github/workflows/test-clusters.yml b/.github/workflows/test-clusters.yml index 11b6990dd..fd7634287 100644 --- a/.github/workflows/test-clusters.yml +++ b/.github/workflows/test-clusters.yml @@ -13,7 +13,7 @@ jobs: uses: ./.github/workflows/reusable-clusters.yml with: runner_tags: '["self-hosted", "protected", "raven"]' - + Viper: uses: ./.github/workflows/reusable-clusters.yml with: @@ -22,7 +22,7 @@ jobs: # uses: ./.github/workflows/reusable-clusters.yml # with: # runner_tags: '["self-hosted", "protected", "tok"]' - + Pitagora: uses: ./.github/workflows/reusable-clusters.yml with: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6d60c7e1f..ae64c61b0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ -# Keyword reference for the .gitlab-ci.yml file: -# https://gitlab.mpcdf.mpg.de/help/ci/yaml/index.md +# Keyword reference for the .gitlab-ci.yml file: +# https://gitlab.mpcdf.mpg.de/help/ci/yaml/index.md # A "scheduled" pipeline can be tested by clicking "Run pipeline" on Gitlab and setting the TEST_SCHEDULED = true. # The doc can be built by clicking "Run pipeline" on Gitlab and setting the MAKE_PAGES = true. @@ -38,7 +38,7 @@ stages: - test - lint - pages - - release + - release # --- images --- @@ -58,8 +58,8 @@ stages: .variables_push: variables: - LANGUAGE: 'c' - OMP: '' + LANGUAGE: "c" + OMP: "" # --- rules --- @@ -161,12 +161,12 @@ stages: - module list - module avail # struphy modules - - module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 + - module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 # gvec modules and variables - - module load cmake netcdf-serial mkl hdf5-serial - - export FC=`which gfortran` - - export CC=`which gcc` - - export CXX=`which g++` + - module load cmake netcdf-serial mkl hdf5-serial + - export FC=`which gfortran` + - export CC=`which gcc` + - export CXX=`which g++` # verify - module list - echo "Fortran compiler is ${FC}" @@ -178,22 +178,22 @@ stages: - !reference [.scripts, inspect_directory] # basic - dnf install -y wget yum-utils make openssl-devel bzip2-devel libffi-devel zlib-devel - - dnf update -y + - dnf update -y # compilers and mpi - dnf install -y gcc - - dnf install -y gfortran - - dnf install -y blas-devel lapack-devel - - dnf install -y openmpi openmpi-devel + - dnf install -y gfortran + - dnf install -y blas-devel lapack-devel + - dnf install -y openmpi openmpi-devel # python - dnf install -y python3-devel - - dnf install -y python3-mpi4py-openmpi + - dnf install -y python3-mpi4py-openmpi # gvec - - dnf install -y g++ cmake netcdf netcdf-devel netcdf-fortran netcdf-fortran-devel pkgconf - - export FC=`which gfortran` - - export CC=`which gcc` + - dnf install -y g++ cmake netcdf netcdf-devel netcdf-fortran netcdf-fortran-devel pkgconf + - export FC=`which gfortran` + - export CC=`which gcc` - export CXX=`which g++` # additional - - dnf install -y git + - dnf install -y git - dnf install -y pandoc - dnf update -y - export OMPI_ALLOW_RUN_AS_ROOT=1 @@ -203,7 +203,7 @@ stages: - source env/bin/activate - . /etc/profile.d/modules.sh - module load mpi/openmpi-$(arch) - - module list + - module list .requirements_opensuse: before_script: @@ -211,24 +211,24 @@ stages: # basic - zypper refresh # compilers and mpi - - zypper install -y gcc-fortran gcc - - zypper install -y blas-devel lapack-devel + - zypper install -y gcc-fortran gcc + - zypper install -y blas-devel lapack-devel - zypper install -y openmpi openmpi-devel openmpi4-devel - - zypper install -y libgomp1 + - zypper install -y libgomp1 # python - zypper install -y python3 python3-devel - zypper install -y python3-pip python3-virtualenv python3-pkgconfig # gvec - - zypper install -y gcc-c++ cmake netcdf - - zypper addrepo -G https://download.opensuse.org/repositories/science/openSUSE_Tumbleweed/science.repo - - zypper install -y netcdf-fortran-devel - - export FC=`which gfortran` - - export CC=`which gcc` + - zypper install -y gcc-c++ cmake netcdf + - zypper addrepo -G https://download.opensuse.org/repositories/science/openSUSE_Tumbleweed/science.repo + - zypper install -y netcdf-fortran-devel + - export FC=`which gfortran` + - export CC=`which gcc` - export CXX=`which g++` # additional - - zypper install -y git - - zypper install -y pandoc - - zypper install -y vim + - zypper install -y git + - zypper install -y pandoc + - zypper install -y vim - zypper install -y make - export OMPI_ALLOW_RUN_AS_ROOT=1 - export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 @@ -243,21 +243,21 @@ stages: - !reference [.scripts, inspect_directory] # basic - yum install -y wget yum-utils make openssl-devel bzip2-devel libffi-devel zlib-devel - - yum update -y - - yum clean all + - yum update -y + - yum clean all # compilers and mpi - - yum install -y gcc - - yum install -y gfortran - - yum install -y openmpi openmpi-devel + - yum install -y gcc + - yum install -y gfortran + - yum install -y openmpi openmpi-devel - yum install -y libgomp - - yum install -y environment-modules + - yum install -y environment-modules # python - - wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz - - tar xzf Python-3.12.8.tgz - - cd Python-3.12.8 - - ./configure --with-system-ffi --with-computed-gotos --enable-loadable-sqlite-extensions - - make -j ${nproc} - - make altinstall + - wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz + - tar xzf Python-3.12.8.tgz + - cd Python-3.12.8 + - ./configure --with-system-ffi --with-computed-gotos --enable-loadable-sqlite-extensions + - make -j ${nproc} + - make altinstall - alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.12 1 - alternatives --set python3 /usr/local/bin/python3.12 - mv /usr/local/lib/libpython3.12.a libpython3.12.a.bak @@ -265,7 +265,7 @@ stages: - cd .. - pwd - ls - # additional + # additional - yum install -y git - export OMPI_ALLOW_RUN_AS_ROOT=1 - export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 @@ -274,10 +274,10 @@ stages: - export PATH="/usr/lib64/openmpi/bin:$PATH" # gvec - yum install -y g++ cmake which flexiblas-devel - - yum install -y epel-release - - yum install -y netcdf-devel netcdf-fortran-devel - - export FC=`which gfortran` - - export CC=`which gcc` + - yum install -y epel-release + - yum install -y netcdf-devel netcdf-fortran-devel + - export FC=`which gfortran` + - export CC=`which gcc` - export CXX=`which g++` # virtual env - python3 -m pip list @@ -409,11 +409,11 @@ stages: - struphy compile --status - struphy test LinearMHD - struphy test toy - - struphy test models - - struphy test verification + - struphy test models + - struphy test verification model_tests_mpi: - struphy compile --status - - struphy test models + - struphy test models - struphy test models --mpi 2 - struphy test verification --mpi 1 - struphy test verification --mpi 4 @@ -421,7 +421,7 @@ stages: - struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 quickstart_tests: - struphy -h - - struphy params VlasovAmpereOneSpecies + - struphy params VlasovAmpereOneSpecies - ls -1a - mv params_VlasovAmpereOneSpecies.py test.py - python3 test.py @@ -461,7 +461,7 @@ stages: .artifacts_scheduled: artifacts: - name: 'python-env-installed-${LANGUAGE}-${OMP}' + name: "python-env-installed-${LANGUAGE}-${OMP}" paths: - env_${CI_PIPELINE_ID}_${LANGUAGE}_${OMP} expire_in: 1 day @@ -668,7 +668,7 @@ pages_tests: - cd doc ; make html - mv _build/html/ $CI_PROJECT_DIR/documentation/ artifacts: - expose_as: 'Documentation' + expose_as: "Documentation" paths: - documentation/ @@ -699,7 +699,7 @@ macos_nmpp: before_script: # - brew install cmake - brew link --overwrite cmake - - cmake --version + - cmake --version - make -v - printenv - system_profiler SPHardwareDataType @@ -716,7 +716,7 @@ macos_nmpp: - pwd - ls -a - echo $_JOB_PATH - - ls -a $_JOB_PATH + - ls -a $_JOB_PATH - rm -rf $_JOB_PATH cleanup_macos: @@ -742,7 +742,6 @@ cleanup_macos: ### SCHEDULED PIPELINE ### ########################## - install_scheduled: stage: install needs: [] @@ -1019,7 +1018,7 @@ lint_full_repo_report: - struphy lint all --output-format report allow_failure: true artifacts: - expose_as: 'Branch lint report' + expose_as: "Branch lint report" paths: - code_analysis_report.html expire_in: 1 month @@ -1068,7 +1067,7 @@ lint_branch_report: - struphy lint branch --output-format report allow_failure: true artifacts: - expose_as: 'Branch lint report' + expose_as: "Branch lint report" paths: - code_analysis_report.html expire_in: 1 month @@ -1094,7 +1093,7 @@ pages: - cd doc ; make html - mv _build/html/ $CI_PROJECT_DIR/public/ artifacts: - name: 'pages' + name: "pages" paths: - public/ @@ -1126,14 +1125,14 @@ vars: - set -- $var - echo ${16} - for (( i=1; i <= "$#"; i++ )); do if (( ${i} < 30 )); then echo ${i}; echo ${!i}; fi done - - for (( i=1; i <= "$#"; i++ )); do if [[ ${!i} == "version" ]]; then echo ${i}; echo ${!i}; index=$((${i} + 2)); fi done - - echo $index + - for (( i=1; i <= "$#"; i++ )); do if [[ ${!i} == "version" ]]; then echo ${i}; echo ${!i}; index=$((${i} + 2)); fi done + - echo $index - VERSION_STR=${!index} - echo $VERSION_STR - echo "VERSION=$(echo $VERSION_STR | sed 's/^.//' | sed 's/.$//')" - echo "VERSION=$(echo $VERSION_STR | sed 's/^.//' | sed 's/.$//')" >> vars.env artifacts: - name: 'vars' + name: "vars" reports: dotenv: vars.env expire_in: 1 day @@ -1143,20 +1142,20 @@ release_job: image: registry.gitlab.com/gitlab-org/release-cli:latest extends: - .rules_gitlab_release - needs: ['vars'] + needs: ["vars"] before_script: - !reference [.scripts, inspect_directory] script: - cat /etc/*-release - echo $VERSION - release: # See https://docs.gitlab.com/ee/ci/yaml/#release for available properties - tag_name: 'v$VERSION' # The version is incremented per pipeline. - name: 'v$VERSION' - ref: '$CI_COMMIT_SHA' # The tag is created from the pipeline SHA. - description: 'CHANGELOG.md' + release: # See https://docs.gitlab.com/ee/ci/yaml/#release for available properties + tag_name: "v$VERSION" # The version is incremented per pipeline. + name: "v$VERSION" + ref: "$CI_COMMIT_SHA" # The tag is created from the pipeline SHA. + description: "CHANGELOG.md" assets: links: - - name: 'Documentation' - url: 'https://struphy-hub.github.io/struphy/index.html' - - name: 'PyPI' - url: 'https://pypi.org/project/struphy/' + - name: "Documentation" + url: "https://struphy-hub.github.io/struphy/index.html" + - name: "PyPI" + url: "https://pypi.org/project/struphy/" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f6845ca0..ad2c97896 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 # Use the latest stable version + rev: v6.0.0 # Use the latest stable version hooks: - id: check-added-large-files # Prevent giant files from being committed. - args: ["--maxkb=1000"] - - id: check-merge-conflict # Check for files that contain merge conflict strings. + args: ["--maxkb=1000"] + - id: check-merge-conflict # Check for files that contain merge conflict strings. args: ["--assume-in-merge"] - - id: check-toml # Attempts to load all TOML files to verify syntax. - - id: check-yaml # Attempts to load all yaml files to verify syntax. + - id: check-toml # Attempts to load all TOML files to verify syntax. + - id: check-yaml # Attempts to load all yaml files to verify syntax. args: ["--unsafe"] - repo: https://github.com/kynan/nbstripout diff --git a/.readthedocs.yml b/.readthedocs.yml index 5d2977f12..8a1b24ffc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,3 @@ - - # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details @@ -34,13 +32,11 @@ build: # Build documentation in the "doc/" directory with Sphinx sphinx: - configuration: doc/conf.py + configuration: doc/conf.py # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: - install: - - requirements: doc/requirements.txt - - + install: + - requirements: doc/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 596cb7250..0d73a968d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,120 +1,106 @@ # Changelog - ## Struphy 3.2.0 - 2026-06-09 -* [PyPI](https://pypi.org/project/struphy/3.2.0) -* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.2.0) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.1.0...v3.2.0) - +- [PyPI](https://pypi.org/project/struphy/3.2.0) +- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.2.0) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.1.0...v3.2.0) ### Headlines -* New Quickstart, Userguide, Tutorials and Developer's guide in the documentation: https://github.com/struphy-hub/struphy/pull/252 -* Each `Propagator` now sits in his own .py file: https://github.com/struphy-hub/struphy/pull/236 -* New propagator `CurlCurlSolve()` for curl-curl problems: https://github.com/struphy-hub/struphy/pull/245 -* New classmethods to inspect the model docstring (in a jupyter notebook for example): https://github.com/struphy-hub/struphy/pull/229 +- New Quickstart, Userguide, Tutorials and Developer's guide in the documentation: https://github.com/struphy-hub/struphy/pull/252 +- Each `Propagator` now sits in his own .py file: https://github.com/struphy-hub/struphy/pull/236 +- New propagator `CurlCurlSolve()` for curl-curl problems: https://github.com/struphy-hub/struphy/pull/245 +- New classmethods to inspect the model docstring (in a jupyter notebook for example): https://github.com/struphy-hub/struphy/pull/229 ### API changes -* Change in the signature of `ParticleSpecies.set_markers()`, which is used in parameter files featuring particles. The new classes `SortingParameters` and `SavingParameters` replace the methods `set_sorting_boxes` and `set_save_data`. Instances of the new classes are passed to `set_markers()`: https://github.com/struphy-hub/struphy/pull/247 +- Change in the signature of `ParticleSpecies.set_markers()`, which is used in parameter files featuring particles. The new classes `SortingParameters` and `SavingParameters` replace the methods `set_sorting_boxes` and `set_save_data`. Instances of the new classes are passed to `set_markers()`: https://github.com/struphy-hub/struphy/pull/247 ### User news -* Clean-up logging levels for simulation output: https://github.com/struphy-hub/struphy/pull/247 -* New plotting functionality for kinetic backgrounds: https://github.com/struphy-hub/struphy/pull/239 -* Addition of a matrix-free averaging operator for distributed FEEC data: https://github.com/struphy-hub/struphy/pull/246 -* New default for `boxes_per_dim` is `tuple = (1, 1, 1)`: https://github.com/struphy-hub/struphy/pull/247 +- Clean-up logging levels for simulation output: https://github.com/struphy-hub/struphy/pull/247 +- New plotting functionality for kinetic backgrounds: https://github.com/struphy-hub/struphy/pull/239 +- Addition of a matrix-free averaging operator for distributed FEEC data: https://github.com/struphy-hub/struphy/pull/246 +- New default for `boxes_per_dim` is `tuple = (1, 1, 1)`: https://github.com/struphy-hub/struphy/pull/247 ### Bug fixes -* Adaptation to general equilibria and new tests of the gyrokinetic Poisson solve: https://github.com/struphy-hub/struphy/pull/238 - - - +- Adaptation to general equilibria and new tests of the gyrokinetic Poisson solve: https://github.com/struphy-hub/struphy/pull/238 ## Struphy 3.1.0 - 2026-04-24 -* [PyPI](https://pypi.org/project/struphy/3.1.0) -* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.1.0) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.4...v3.1.0) - +- [PyPI](https://pypi.org/project/struphy/3.1.0) +- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.1.0) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.4...v3.1.0) ### Headlines -* Refactoring of the `Derham` class: https://github.com/struphy-hub/struphy/pull/212 - - Renamed `Nel -> num_elements`, `p -> degree`, `nq_pr -> nquads_proj`, `polar_ck -> polar_splines` - - The type of spline basis functions is now set via the argument `bcs`, which is a tuple of length three. It holds the type for each direction: `None` for periodic, or a tuple of length two with either `free` or `dirichlet` to indicate the type of clamped splines. `bcs` replaces the two arguments `spl_kind` and `dirichlet_bc` - - The lifting is defined for each `FEECVariable` separately through the new attribute `lifting_function` -* Refactoring of `WeightedMassOperators.create_weighted_mass` method: https://github.com/struphy-hub/struphy/pull/227 - - The signature remains largely the same, except that list input for weights has been replaced by tuple input. Moreover, within the tuple, one can now pass a `SplineFunction` object, either from H1 or L2 space, which will be multiplied to the constant weights during `.assemble()`. **This allows for time dependent mass operators in nonlinear simulations (formerly treated with workarounds).** - - In the constructor of `create_weighted_mass`, we now evaluate all callables derived from `weights: tuple[str]` on the integration grid, multiply them together and then pass them as xp.arrays to `WeightedMassOperator` for building the object. This leads to much simpler code than in the previous version, where we built composed functions and passed them as weights. - - In `MassMatrixPreconditioner`, to build the Kronecker matrices from 1D matrices without domain decomposition, the values of the weights are now retrieved via an **MPI sub-communicator**, because the weights in M0, M1 etc. are now given as local xp.arrays, not as callables anymore. -* Use `logging` instead of print statements; use function `set_logging_level` from the API to set the logging level of all handlers. With pytest you can use `pytest --logging-level DEBUG src/struphy` to set the loggong level: https://github.com/struphy-hub/struphy/pull/199 and https://github.com/struphy-hub/struphy/pull/219 -* New user guide: https://github.com/struphy-hub/struphy/pull/200 -* Remove two submodules `struphy-parameter-files` and `struphy-tutorials` in favor of the new folders `examples/` or `tutorials/`: https://github.com/struphy-hub/struphy/pull/206 +- Refactoring of the `Derham` class: https://github.com/struphy-hub/struphy/pull/212 + - Renamed `Nel -> num_elements`, `p -> degree`, `nq_pr -> nquads_proj`, `polar_ck -> polar_splines` + - The type of spline basis functions is now set via the argument `bcs`, which is a tuple of length three. It holds the type for each direction: `None` for periodic, or a tuple of length two with either `free` or `dirichlet` to indicate the type of clamped splines. `bcs` replaces the two arguments `spl_kind` and `dirichlet_bc` + - The lifting is defined for each `FEECVariable` separately through the new attribute `lifting_function` +- Refactoring of `WeightedMassOperators.create_weighted_mass` method: https://github.com/struphy-hub/struphy/pull/227 + - The signature remains largely the same, except that list input for weights has been replaced by tuple input. Moreover, within the tuple, one can now pass a `SplineFunction` object, either from H1 or L2 space, which will be multiplied to the constant weights during `.assemble()`. **This allows for time dependent mass operators in nonlinear simulations (formerly treated with workarounds).** + - In the constructor of `create_weighted_mass`, we now evaluate all callables derived from `weights: tuple[str]` on the integration grid, multiply them together and then pass them as xp.arrays to `WeightedMassOperator` for building the object. This leads to much simpler code than in the previous version, where we built composed functions and passed them as weights. + - In `MassMatrixPreconditioner`, to build the Kronecker matrices from 1D matrices without domain decomposition, the values of the weights are now retrieved via an **MPI sub-communicator**, because the weights in M0, M1 etc. are now given as local xp.arrays, not as callables anymore. +- Use `logging` instead of print statements; use function `set_logging_level` from the API to set the logging level of all handlers. With pytest you can use `pytest --logging-level DEBUG src/struphy` to set the loggong level: https://github.com/struphy-hub/struphy/pull/199 and https://github.com/struphy-hub/struphy/pull/219 +- New user guide: https://github.com/struphy-hub/struphy/pull/200 +- Remove two submodules `struphy-parameter-files` and `struphy-tutorials` in favor of the new folders `examples/` or `tutorials/`: https://github.com/struphy-hub/struphy/pull/206 ### API changes -* `base_units` is removed from the `StruphyModel` constructor; all equation parameters that can be seen in the model docstring can be passed to the model constructor: https://github.com/struphy-hub/struphy/pull/222 - +- `base_units` is removed from the `StruphyModel` constructor; all equation parameters that can be seen in the model docstring can be passed to the model constructor: https://github.com/struphy-hub/struphy/pull/222 ### User news -* Add `to_dict` and `from_dict` methods to the `Simulation` class: https://github.com/struphy-hub/struphy/pull/186 -* Add `__repr__` and `__repr_no_defaults__` methods to most classes in the API: https://github.com/struphy-hub/struphy/pull/193 -* Using `pyvista`; ddd `show_3d` and `create_geometry_mesh` methods to `Domain` class: https://github.com/struphy-hub/struphy/pull/195 -* Add iterators to models and domains: https://github.com/struphy-hub/struphy/pull/196 -* Added export and from_file methods to `SimulationBase` class: https://github.com/struphy-hub/struphy/pull/197 -* Added name and description to the Simulation class: https://github.com/struphy-hub/struphy/pull/198 -* New model `ToyGyrokinetic` to simulate the diocotron instability: https://github.com/struphy-hub/struphy/pull/201 -* New classes for scalar quantities tracked during simulation (via new type `Scalar`): https://github.com/struphy-hub/struphy/pull/220 -* The components of a model docstring are now available as class methods: https://github.com/struphy-hub/struphy/pull/229 - +- Add `to_dict` and `from_dict` methods to the `Simulation` class: https://github.com/struphy-hub/struphy/pull/186 +- Add `__repr__` and `__repr_no_defaults__` methods to most classes in the API: https://github.com/struphy-hub/struphy/pull/193 +- Using `pyvista`; ddd `show_3d` and `create_geometry_mesh` methods to `Domain` class: https://github.com/struphy-hub/struphy/pull/195 +- Add iterators to models and domains: https://github.com/struphy-hub/struphy/pull/196 +- Added export and from_file methods to `SimulationBase` class: https://github.com/struphy-hub/struphy/pull/197 +- Added name and description to the Simulation class: https://github.com/struphy-hub/struphy/pull/198 +- New model `ToyGyrokinetic` to simulate the diocotron instability: https://github.com/struphy-hub/struphy/pull/201 +- New classes for scalar quantities tracked during simulation (via new type `Scalar`): https://github.com/struphy-hub/struphy/pull/220 +- The components of a model docstring are now available as class methods: https://github.com/struphy-hub/struphy/pull/229 ### Bug fixes -* Weights in initial Poisson solves of kinetic models without control variate fixed: https://github.com/struphy-hub/struphy/pull/192 - - - +- Weights in initial Poisson solves of kinetic models without control variate fixed: https://github.com/struphy-hub/struphy/pull/192 ## Struphy 3.0.4 - 2026-02-27 -* [PyPI](https://pypi.org/project/struphy/3.0.4) -* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.4) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.3...v3.0.4) +- [PyPI](https://pypi.org/project/struphy/3.0.4) +- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.4) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.3...v3.0.4) ### Bug fixes -* New lower bound on pyccel is set to 2.2.0, due to this pyccel bug fix: https://github.com/pyccel/pyccel/pull/2567 - - - +- New lower bound on pyccel is set to 2.2.0, due to this pyccel bug fix: https://github.com/pyccel/pyccel/pull/2567 ## Struphy 3.0.3 - 2026-02-23 -* [PyPI](https://pypi.org/project/struphy/3.0.3) -* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.3) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.2...v3.0.3) +- [PyPI](https://pypi.org/project/struphy/3.0.3) +- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.3) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.2...v3.0.3) ### Headlines 1. New class `Simulation` inherits the generic `SimulationBase`, both are in the new folder `struphy/simulations/`. The most important methods are: - * `Simulation.run()` - * `Simulation.pproc()` - * `Simulation.load_plotting_data()` - * `Simulation.spawn_sister()` (my new favorite!) - + - `Simulation.run()` + - `Simulation.pproc()` + - `Simulation.load_plotting_data()` + - `Simulation.spawn_sister()` (my new favorite!) + These are tested in the new tutorials: https://github.com/struphy-hub/struphy-tutorials/tree/use-species-properties The file `main.py` has been deleted. -The `Simulation` takes a model as input. Other API classes are passed as well (see tutorials). +The `Simulation` takes a model as input. Other API classes are passed as well (see tutorials). The model is viewed as everything related to the PDE, i.e. its variables, initial conditions etc. The simulation deals with the rest (geometry, derham, environment etc.) Some important changes to the logic: The model does not have access to `derham`, `mass_ops` etc. anymore, these can be called from `Propagator` when needed. Solves that need to happen before the time stepping (like initial Poisson solves) are moved to `model.allocate_helpers()`. @@ -123,55 +109,48 @@ Some important changes to the logic: The model does not have access to `derham`, 3. Several new classes have been introduced for post processing and plotting data, see `post_processing_tools.py`. The most important ones are `PostProcessor` and `PlottingData`. Dictionaries in the plotting data have been replaced by classes. Many classes now feature the `__repr__` dunder for customized printing. - ### API changes New classes exposed: `Simulation`, `PostProcessor` and `PlottingData`. - ### User news -* Add `set_zero_velocity` argument into `LoadingParameters`, enforcing velocities of all particles along specified axis to always be zero: https://github.com/struphy-hub/struphy/pull/176 -* New model `ViscousEulerSPH` replaces `EulerSPH`. The evaluation of the viscosity tensor has been implemented and tested for SPH methods. Unit tests for evaluation of the fluid velocity and its gradients (needed in the viscosity tensor) have been improved: https://github.com/struphy-hub/struphy/pull/160 - - +- Add `set_zero_velocity` argument into `LoadingParameters`, enforcing velocities of all particles along specified axis to always be zero: https://github.com/struphy-hub/struphy/pull/176 +- New model `ViscousEulerSPH` replaces `EulerSPH`. The evaluation of the viscosity tensor has been implemented and tested for SPH methods. Unit tests for evaluation of the fluid velocity and its gradients (needed in the viscosity tensor) have been improved: https://github.com/struphy-hub/struphy/pull/160 ## Struphy 3.0.2 - 2026-02-06 -* [PyPI](https://pypi.org/project/struphy/3.0.2) -* [Github pages](https://struphy-hub.github.io/struphy/index.html) -* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.2) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.1...v3.0.2) +- [PyPI](https://pypi.org/project/struphy/3.0.2) +- [Github pages](https://struphy-hub.github.io/struphy/index.html) +- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.2) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.1...v3.0.2) ### Headlines -* Added a public API. This allows imports like `from struphy import equils`: https://github.com/struphy-hub/struphy/pull/168 -* New default compile language is Fortran: https://github.com/struphy-hub/struphy/pull/158 -* Moved each model to its own file. Calling sub-processes must be avoided in the future because of incompatibility with MPI: https://github.com/struphy-hub/struphy/pull/152 +- Added a public API. This allows imports like `from struphy import equils`: https://github.com/struphy-hub/struphy/pull/168 +- New default compile language is Fortran: https://github.com/struphy-hub/struphy/pull/158 +- Moved each model to its own file. Calling sub-processes must be avoided in the future because of incompatibility with MPI: https://github.com/struphy-hub/struphy/pull/152 ### User news -* Added binning of higher order moments (current density, energy tensor) of f and delta f: https://github.com/struphy-hub/struphy/pull/162 +- Added binning of higher order moments (current density, energy tensor) of f and delta f: https://github.com/struphy-hub/struphy/pull/162 ### Developer news -* Use `pyccel 2.1`: https://github.com/struphy-hub/struphy/pull/153 -* Added three submodules: `struphy-parameter-files`, `struphy-tutorials` and`feectools`. The Struphy repo should be cloned with `git clone --recurse-submodules https://github.com/struphy-hub/struphy.git` to init and update the submodules. Also, run `git submodule update` regularly to get updates from the submodules. See https://github.com/struphy-hub/struphy/pull/154 -* Introduced class `options.LiteralOptions` for parsing literals. Moved `Units` to `physics.py`: https://github.com/struphy-hub/struphy/pull/167 - +- Use `pyccel 2.1`: https://github.com/struphy-hub/struphy/pull/153 +- Added three submodules: `struphy-parameter-files`, `struphy-tutorials` and`feectools`. The Struphy repo should be cloned with `git clone --recurse-submodules https://github.com/struphy-hub/struphy.git` to init and update the submodules. Also, run `git submodule update` regularly to get updates from the submodules. See https://github.com/struphy-hub/struphy/pull/154 +- Introduced class `options.LiteralOptions` for parsing literals. Moved `Units` to `physics.py`: https://github.com/struphy-hub/struphy/pull/167 ### Bug fixes -* Use `struphy.io.options.Units` in equils. This enables the use of GVEC, EQDSK and DESC in the new framework: https://github.com/struphy-hub/struphy/pull/158 - - +- Use `struphy.io.options.Units` in equils. This enables the use of GVEC, EQDSK and DESC in the new framework: https://github.com/struphy-hub/struphy/pull/158 ## Struphy 3.0.1 - 2025-12-11 -* [PyPI](https://pypi.org/project/struphy/3.0.1) -* [Github pages](https://struphy-hub.github.io/struphy/index.html) -* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.1) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.0...v3.0.1) +- [PyPI](https://pypi.org/project/struphy/3.0.1) +- [Github pages](https://struphy-hub.github.io/struphy/index.html) +- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.1) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.0...v3.0.1) ### Headlines @@ -185,76 +164,73 @@ None ### Developer news -* Removed legacy code (eigenvalue solver): https://github.com/struphy-hub/struphy/pull/129 -* Add context manager to h5py.File() calls: https://github.com/struphy-hub/struphy/pull/135 -* Fix undefined variables: https://github.com/struphy-hub/struphy/pull/141 +- Removed legacy code (eigenvalue solver): https://github.com/struphy-hub/struphy/pull/129 +- Add context manager to h5py.File() calls: https://github.com/struphy-hub/struphy/pull/135 +- Fix undefined variables: https://github.com/struphy-hub/struphy/pull/141 ### Bug fixes -* Fix setter in DESCequilibirum, update quickstart guide: https://github.com/struphy-hub/struphy/pull/132 -* Set defaults for given_in_basis: "0" for scalar and "v" for vector-valued: https://github.com/struphy-hub/struphy/pull/136 -* Fix the restart function: https://github.com/struphy-hub/struphy/pull/143 - +- Fix setter in DESCequilibirum, update quickstart guide: https://github.com/struphy-hub/struphy/pull/132 +- Set defaults for given_in_basis: "0" for scalar and "v" for vector-valued: https://github.com/struphy-hub/struphy/pull/136 +- Fix the restart function: https://github.com/struphy-hub/struphy/pull/143 ## Struphy 3.0.0 - 2025-11-13 -* [PyPI](https://pypi.org/project/struphy/3.0.0) -* [Github pages](https://struphy-hub.github.io/struphy/index.html) -* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.0) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v3.0.0) +- [PyPI](https://pypi.org/project/struphy/3.0.0) +- [Github pages](https://struphy-hub.github.io/struphy/index.html) +- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.0) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v3.0.0) ### Headlines Struphy 3 represents a major refactoring with breaking changes with respect to Struphy 2, in particular: -* The `.yml` parameter files cannot be used anymore. Simulation parameters have to be transferred to the new `.py` launch files that are generated from `struphy params MODEL`. See the [Struphy README](https://github.com/struphy-hub/struphy) for a quick introduction. -* The console command `struphy run ...` has been deprecated. The new way to launch simulations is by executing the `.py` launch file, for instance with `python params_MODEL.py`. -* Other deprecated console commands are `struphy pproc` and `struphy units`. Post-processing is now done through the API via `main.pproc()`. -* The Struphy repo has moved to [Github](https://github.com/struphy-hub/struphy). The [old Gitlab repo](https://gitlab.mpcdf.mpg.de/struphy/struphy) will persist but not be maintained any longer. Issues, discussion and PRs will solely take place on the new Github repo. +- The `.yml` parameter files cannot be used anymore. Simulation parameters have to be transferred to the new `.py` launch files that are generated from `struphy params MODEL`. See the [Struphy README](https://github.com/struphy-hub/struphy) for a quick introduction. +- The console command `struphy run ...` has been deprecated. The new way to launch simulations is by executing the `.py` launch file, for instance with `python params_MODEL.py`. +- Other deprecated console commands are `struphy pproc` and `struphy units`. Post-processing is now done through the API via `main.pproc()`. +- The Struphy repo has moved to [Github](https://github.com/struphy-hub/struphy). The [old Gitlab repo](https://gitlab.mpcdf.mpg.de/struphy/struphy) will persist but not be maintained any longer. Issues, discussion and PRs will solely take place on the new Github repo. ### User news -* Please consult the [Struphy README](https://github.com/struphy-hub/struphy) and links therein to get familiar with the new workflows. -* New tutorials can be found on [mybinder](https://mybinder.org/v2/gh/struphy-hub/struphy-tutorials/main). +- Please consult the [Struphy README](https://github.com/struphy-hub/struphy) and links therein to get familiar with the new workflows. +- New tutorials can be found on [mybinder](https://mybinder.org/v2/gh/struphy-hub/struphy-tutorials/main). ### Developer news Struphy has been refactored with the following principles in mind: -* get rid of console commands and increase the use of the Struphy API wherever possible -* become even more object-oriented -* use `Classes` instead of `dicts` wherever possible -* use `Literals` to show options for string arguments +- get rid of console commands and increase the use of the Struphy API wherever possible +- become even more object-oriented +- use `Classes` instead of `dicts` wherever possible +- use `Literals` to show options for string arguments In Struphy 3, models feature the following important objects: -* `ParticleSpecies`, `FieldSpecies`, `FluidSpecies` +- `ParticleSpecies`, `FieldSpecies`, `FluidSpecies` Each species is a collection of Variables: -* `PICVariable`, `FEECVariable`, `SPHVariable` +- `PICVariable`, `FEECVariable`, `SPHVariable` These variables are updated by `Propagators`. All options for a simluation can be set in the new `.py` launch file. ### Bug fixes -* Incorporate psydac updates: https://github.com/struphy-hub/struphy/pull/109 -* Auto install Psydac on first Struphy import: https://github.com/struphy-hub/struphy/pull/118 -* Remove MPI Barrier responsible for deadlock: https://github.com/struphy-hub/struphy/pull/121 - +- Incorporate psydac updates: https://github.com/struphy-hub/struphy/pull/109 +- Auto install Psydac on first Struphy import: https://github.com/struphy-hub/struphy/pull/118 +- Remove MPI Barrier responsible for deadlock: https://github.com/struphy-hub/struphy/pull/121 ## Struphy 2.6.0 - 2025-11-12 -* [PyPI](https://pypi.org/project/struphy/2.6.0) -* [Github pages](https://struphy-hub.github.io/struphy/index.html) -* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v2.6.0) -* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v2.6.0) +- [PyPI](https://pypi.org/project/struphy/2.6.0) +- [Github pages](https://struphy-hub.github.io/struphy/index.html) +- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v2.6.0) +- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v2.6.0) ### Headlines -* This is a test run for the relaease of Struphy 3.0 from the new Github repo - +- This is a test run for the relaease of Struphy 3.0 from the new Github repo ## Struphy 2.5.0 and prior releases -* See [Gitlab](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/releases) +- See [Gitlab](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/releases) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad8102e53..dd71e3ee5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,12 @@ # Repository -Struphy has two protected branches, **main** and **devel**. +Struphy has two protected branches, **main** and **devel**. Nobody can push directly to these branches. -The **main** branch holds the current release of the code. +The **main** branch holds the current release of the code. **devel** is the branch for developers. Feature branches must be checked out and merged into **devel**. - # Dependency Bounds On PRs Pull requests into **devel** are checked for stale dependency upper bounds in `pyproject.toml`. @@ -21,25 +20,22 @@ The policy is intentionally narrow: When the check fails, the CI summary prints local remediation commands. In short, run the checker locally, run `python utils/update_dependency_bounds.py` on that report, and commit the updated `pyproject.toml`. - # Releases Happen when pushed to **main**. - # Forking Please create a **public fork** to be able to merge your code into Struphy! You can create feature branches in your forked repo and create merge requests into the original Struphy repo. - # Contact -* [Mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) -* [MatrixChat developer's channel](https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de) -* [Issue tracker](https://github.com/struphy-hub/struphy/issues) -* [LinkedIn](https://www.linkedin.com/company/struphy/) -* [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) -* [max.lindqvist@ipp.mpg.de](mailto:max.lindqvist@ipp.mpg.de) -* [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) +- [Mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) +- [MatrixChat developer's channel](https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de) +- [Issue tracker](https://github.com/struphy-hub/struphy/issues) +- [LinkedIn](https://www.linkedin.com/company/struphy/) +- [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) +- [max.lindqvist@ipp.mpg.de](mailto:max.lindqvist@ipp.mpg.de) +- [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) diff --git a/README.md b/README.md index 6b97b9bcd..1224fa670 100755 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ - - ![STRUPHY Header](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/struphy_header_with_subs.png)

Release License badge Ubuntu latest MacOS latest isort and ruff PyPI PyPI Downloads +

# Welcome! @@ -74,7 +73,7 @@ The doc is on [Github pages](https://struphy-hub.github.io/struphy/index.html), Try out the Python API in a Jupyter notebook or any Python environment. For example, you can create a simulation object and show the domain and equilibrium magnetic field of the linear MHD model via -``` python +```python from struphy import ( Simulation, domains, @@ -132,11 +131,11 @@ There is also a [Docker image with just the prerequisites](https://hub.docker.co ## Publications - D. Bell, M.C. Pinto, S. Possanner, E. Sonnendrücker, -[**The linearized Vlasov–Maxwell system as a Hamiltonian system**](https://doi.org/10.1016/j.jcp.2026.114765), -Journal of Computational Physics, Volume 555, 114765 (2026). + [**The linearized Vlasov–Maxwell system as a Hamiltonian system**](https://doi.org/10.1016/j.jcp.2026.114765), + Journal of Computational Physics, Volume 555, 114765 (2026). - V. Carlier, M.C. Pinto, [**Variational discretizations of ideal magnetohydrodynamics in smooth regime using structure-preserving finite elements**](https://doi.org/10.1016/j.jcp.2024.113647), -Journal of Computational Physics, Volume 523, 113647 (2025). + Journal of Computational Physics, Volume 523, 113647 (2025). - Y. Li, M.C. Pinto, F. Holderied, S. Possanner, E. Sonnendrücker, [**Geometric Particle-In-Cell discretizations of a plasma hybrid model with kinetic ions and mass-less fluid electrons**](https://doi.org/10.1016/j.jcp.2023.112671), Journal of Computational Physics 498, 112671 (2023). diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 2cc31b589..06ab25c9c 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -1,12 +1,11 @@ .eqno { - float: right; + float: right; } .bd-main .bd-content .bd-article-container { - max-width: 100%; /* default is 60em */ - } + max-width: 100%; /* default is 60em */ +} .bd-page-width { -max-width: 95%; /* default is 88rem */ + max-width: 95%; /* default is 88rem */ } - \ No newline at end of file diff --git a/doc/_static/my_theme.css b/doc/_static/my_theme.css index 40affa3ef..ecb3e8fa0 100644 --- a/doc/_static/my_theme.css +++ b/doc/_static/my_theme.css @@ -1,3 +1,3 @@ /* .wy-nav-content { max-width: 1200px !important; -} */ \ No newline at end of file +} */ diff --git a/doc/markdown/vlasov-maxwell.md b/doc/markdown/vlasov-maxwell.md index 467567d1c..73e760440 100644 --- a/doc/markdown/vlasov-maxwell.md +++ b/doc/markdown/vlasov-maxwell.md @@ -1,12 +1,13 @@ (disc_example)= + # Example: Vlasov-Maxwell-Poisson discretization -The Vlasov-Maxwell equations for one species in a static background provide a good example +The Vlasov-Maxwell equations for one species in a static background provide a good example for PDE discretization in Struphy (see {class}`~struphy.models.kinetic.VlasovMaxwellOneSpecies`) for the full implementation). The model we are going to discretize reads as follows: $$ \begin{aligned} - &\partial_t f + \mathbf{v} \cdot \nabla f - \frac em (\mathbf{E} + \mathbf{v} \times \mathbf{B}) + &\partial_t f + \mathbf{v} \cdot \nabla f - \frac em (\mathbf{E} + \mathbf{v} \times \mathbf{B}) \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, \\[2mm] -\frac{1}{c^2} &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\mu_0 e \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, @@ -17,10 +18,13 @@ $$ (eq:model) Here, $f(t, \mathbf x, \mathbf v)$ denotes the kinetic distribution function, $\mathbf E(t, \mathbf x)$ and $\mathbf B(t, \mathbf x)$ are the electric and magnetic field, respectively, $e/m$ is the charge-to-mass ratio of the electrons, $c$ denotes the speed of light and $\mu_0$ stands for the magnetic constant. In order to determine an initial electric field that is consistent with Gauss' law, one has to solve Poisson's equation once at the beginning of the simulation: + $$ + \begin{equation} - -\epsilon_0\Delta \phi = \rho_\textrm{i0} - e \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v}\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. +-\epsilon*0\Delta \phi = \rho*\textrm{i0} - e \int\_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v}\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. \end{equation} + $$ Here, $\phi(\mathbf x)$ denotes the electrostatic potential, $\epsilon_0$ is the dielectric constant and $\rho_\textrm{i0}(\mathbf x)$ is a static ion background (the ion current is assumed zero). Aside from field and particle pushing propagators, the model features also field-particle coupling propagators, and has two particle-to-grid accumulations, one charge deposition in Poisson's equation (to be solved only once at the beginning of the simulation), and one current deposition in Ampère's law. @@ -40,96 +44,129 @@ Let us now go through these steps for the above model. Prior to implementation, we have to find suitable units for the model quantities, a process called {ref}`normalization`. For this, let us write the Vlasov-Maxwell system in terms of units (with a hat) and dimensionless quantities (with a prime): + $$ + \begin{aligned} - & \frac{\hat f}{\hat t}\,\partial_{t'} f' + \frac{\hat v \hat f}{\hat x}\,\mathbf{v}' \cdot \nabla' f' - \frac em \hat B \hat f\left(\frac{\hat E}{\hat v\hat B}\mathbf{E}' + \mathbf{v}' \times \mathbf{B}' \right) - \cdot \frac{\partial f'}{\partial \mathbf{v}'} = 0 \,, - \\[2mm] - -\frac{1}{c^2} \frac{\hat E}{\hat t}&\frac{\partial \mathbf{E}'}{\partial t'} + \frac{\hat B}{\hat x}\nabla' \times \mathbf{B}' = -\mu_0 e\, \hat v \hat n \int_{\mathbb{R}^3} \mathbf{v}' f' \, \text{d} \mathbf{v}' \,, - \\[3mm] - &\frac{\hat B}{\hat t}\frac{\partial \mathbf{B}'}{\partial t'} + \frac{\hat E}{\hat x}\nabla' \times \mathbf{E}' = 0 \,, - \\[3mm] - &-\epsilon_0\,\frac{\hat \phi}{\hat x^2}\Delta' \phi' = e \hat n\left(\rho_\textrm{i0}' - \int_{\mathbb{R}^3} f'(t=0) \, \text{d} \mathbf{v}' \right)\,,\qquad \hat E\mathbf E'(t=0) = -\frac{\hat \phi}{\hat x}\nabla' \phi'\,. +& \frac{\hat f}{\hat t}\,\partial*{t'} f' + \frac{\hat v \hat f}{\hat x}\,\mathbf{v}' \cdot \nabla' f' - \frac em \hat B \hat f\left(\frac{\hat E}{\hat v\hat B}\mathbf{E}' + \mathbf{v}' \times \mathbf{B}' \right) +\cdot \frac{\partial f'}{\partial \mathbf{v}'} = 0 \,, +\\[2mm] +-\frac{1}{c^2} \frac{\hat E}{\hat t}&\frac{\partial \mathbf{E}'}{\partial t'} + \frac{\hat B}{\hat x}\nabla' \times \mathbf{B}' = -\mu_0 e\, \hat v \hat n \int*{\mathbb{R}^3} \mathbf{v}' f' \, \text{d} \mathbf{v}' \,, +\\[3mm] +&\frac{\hat B}{\hat t}\frac{\partial \mathbf{B}'}{\partial t'} + \frac{\hat E}{\hat x}\nabla' \times \mathbf{E}' = 0 \,, +\\[3mm] +&-\epsilon*0\,\frac{\hat \phi}{\hat x^2}\Delta' \phi' = e \hat n\left(\rho*\textrm{i0}' - \int\_{\mathbb{R}^3} f'(t=0) \, \text{d} \mathbf{v}' \right)\,,\qquad \hat E\mathbf E'(t=0) = -\frac{\hat \phi}{\hat x}\nabla' \phi'\,. \end{aligned} -$$ (eq:norm) + +$$ +(eq:norm) In Struphy, the three basic units $\hat x$, $\hat B$ and $\hat n$ are defined by the user. Moreover, several other units are fixed, as described in {ref}`normalization`, namely: + $$ + \hat t, \, \hat p,\, \hat \rho,\,\hat \jmath \quad \textrm{are fixed}\,. + $$ -Therefore, in the present model, +Therefore, in the present model, + $$ + \hat v,\,\hat f,\,\hat E,\,\hat \phi + $$ must be defined in order to complete the normalization process. Let us introduce the unit of the electron cyclotron frequency and its product with the time unit, + $$ - \hat \Omega_\textrm{ce} := \frac em \hat B\qquad \varepsilon := \frac{1}{\hat \Omega_\textrm{ce} \hat t}\,. + +\hat \Omega*\textrm{ce} := \frac em \hat B\qquad \varepsilon := \frac{1}{\hat \Omega*\textrm{ce} \hat t}\,. + $$ In our model, it makes sense to set the unit of the $E \times B$-velocity to $\hat v$, + $$ + \frac{\hat E}{\hat B} = \hat v = \frac{\hat x}{\hat t}\,. + $$ This determines the unit $\hat E$ of the electric field and renders Faraday's law (7) scale invariant. It also sets the unit for the electric potential, + $$ - \hat \phi = \hat E \hat x = \hat v \hat B \hat x\,. + +\hat \phi = \hat E \hat x = \hat v \hat B \hat x\,. + $$ In the Poisson equation, this brings into play the unit of the electron plasma frequency and its ration to the unit of the electron cyclotron frequency, + $$ - \hat \Omega_\textrm{pe} := \sqrt{\frac{e^2 \hat n}{\epsilon_0 m}}\qquad \alpha := \frac{\hat \Omega_\textrm{pe}}{\hat \Omega_\textrm{ce}}\,. + +\hat \Omega*\textrm{pe} := \sqrt{\frac{e^2 \hat n}{\epsilon_0 m}}\qquad \alpha := \frac{\hat \Omega*\textrm{pe}}{\hat \Omega\_\textrm{ce}}\,. + $$ Let us summarize what we have thus far, omitting the primes in {eq}`eq:norm` for clarity: + $$ + \begin{aligned} - & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) - \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, - \\[2mm] - -\frac{\hat v^2}{c^2} &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\mu_0 e\, \hat v \hat n\hat x}{\hat B} \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, - \\[2mm] - &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, - \\[2mm] - &-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho_\textrm{i0} - \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. +& \partial*{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) +\cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, +\\[2mm] +-\frac{\hat v^2}{c^2} &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\mu_0 e\, \hat v \hat n\hat x}{\hat B} \int*{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, +\\[2mm] +&\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, +\\[2mm] +&-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho*\textrm{i0} - \int*{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. \end{aligned} + $$ In Ampere's law we have + $$ - \frac{\mu_0 e\, \hat v \hat n\hat x}{\hat B} = \frac{\epsilon_0\mu_0 e^2\, \hat v^2 m\hat n\hat x}{\epsilon_0 me\hat B \hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega_\textrm{pe}^2}{\hat \Omega_\textrm{ce}} \frac{\hat x}{\hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega_\textrm{pe}^2}{\hat \Omega_\textrm{ce}^2} \frac{1}{\varepsilon}\,. + +\frac{\mu*0 e\, \hat v \hat n\hat x}{\hat B} = \frac{\epsilon_0\mu_0 e^2\, \hat v^2 m\hat n\hat x}{\epsilon_0 me\hat B \hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega*\textrm{pe}^2}{\hat \Omega*\textrm{ce}} \frac{\hat x}{\hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega*\textrm{pe}^2}{\hat \Omega\_\textrm{ce}^2} \frac{1}{\varepsilon}\,. + $$ -Therefore, choosing the velocity unit as +Therefore, choosing the velocity unit as + $$ - \hat v = c\,, + +\hat v = c\,, + $$ leads to the final, Struphy-normalized equations + $$ + \begin{aligned} - & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) - \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, - \\[2mm] - - &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\alpha^2}{\varepsilon} \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, - \\[2mm] - &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, - \\[2mm] - &-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho_\textrm{i0} - \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. +& \partial*{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) +\cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, +\\[2mm] - &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\alpha^2}{\varepsilon} \int*{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, +\\[2mm] +&\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, +\\[2mm] +&-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho*\textrm{i0} - \int*{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. \end{aligned} + $$ (def_spaces)= @@ -143,20 +180,23 @@ The above rule applied to the current Vlasov-Maxwell model means that Ampère's Find $(f, \mathbf E, \mathbf B, \phi) \in C^\infty \times H(\textrm{curl}) \times H(\textrm{div}) \times H^1$ such that + $$ + \begin{aligned} - & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) - \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, - \\[2mm] - - &\int \mathbf F \cdot \frac{\partial \mathbf{E}}{\partial t} \,\textrm d \mathbf x + \int \nabla \times \mathbf{F} \cdot \mathbf B \,\textrm d \mathbf x = -\frac{\alpha^2}{\varepsilon} \int\int_{\mathbb{R}^3} \mathbf{v} \cdot \mathbf F f \,\, \text{d} \mathbf{v}\textrm d \mathbf x \,,\qquad \forall \ \mathbf F \in H(\textrm{curl})\,, - \\[3mm] - &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, - \\[2mm] - &\int \nabla \psi \cdot \nabla \phi\,\textrm d \mathbf x = \frac{\alpha^2}{\varepsilon}\left(\int \rho_\textrm{i0}\,\psi\,\textrm d \mathbf x - \int\int_{\mathbb{R}^3} f(t=0)\, \psi \, \text{d} \mathbf{v} \textrm d \mathbf x\right)\,, \qquad \forall \ \psi \in H^1\,, - \\[4mm] - &\mathbf E(t=0) = -\nabla \phi\,. +& \partial*{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) +\cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, +\\[2mm] - &\int \mathbf F \cdot \frac{\partial \mathbf{E}}{\partial t} \,\textrm d \mathbf x + \int \nabla \times \mathbf{F} \cdot \mathbf B \,\textrm d \mathbf x = -\frac{\alpha^2}{\varepsilon} \int\int*{\mathbb{R}^3} \mathbf{v} \cdot \mathbf F f \,\, \text{d} \mathbf{v}\textrm d \mathbf x \,,\qquad \forall \ \mathbf F \in H(\textrm{curl})\,, +\\[3mm] +&\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, +\\[2mm] +&\int \nabla \psi \cdot \nabla \phi\,\textrm d \mathbf x = \frac{\alpha^2}{\varepsilon}\left(\int \rho*\textrm{i0}\,\psi\,\textrm d \mathbf x - \int\int*{\mathbb{R}^3} f(t=0)\, \psi \, \text{d} \mathbf{v} \textrm d \mathbf x\right)\,, \qquad \forall \ \psi \in H^1\,, +\\[4mm] +&\mathbf E(t=0) = -\nabla \phi\,. \end{aligned} -$$ (eq:spaces) + +$$ +(eq:spaces) (pullback)= ## Pull-back to the logical domain @@ -189,43 +229,52 @@ The connection of differential $p$-forms to the {ref}`Struphy de Rham spaces `. For this one needs to invert the Schur complement $S = A - BC$ of the 2x2 block matrix, which in our case reads + $$ - S = \mathbb M^1 + \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1 \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top \qquad \in \mathbb R^{N_1 \times N_1}\,. + +S = \mathbb M^1 + \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1 \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top \qquad \in \mathbb R^{N_1 \times N_1}\,. + $$ This matrix size $N_1 \times N_1$ is independent of the particle number $N$ and an inversion is thus feasible. Indeed, using the Schur complement amounts to inserting one equation into the other, thereby eliminating one variable from the solution step. Moreover, the term + $$ - M^{\mu, \nu}_{ijk, mno} := \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1_{(\mu,ijk)} \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top_{(\nu,mno)}\,, + +M^{\mu, \nu}_{ijk, mno} := \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1_{(\mu,ijk)} \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top\_{(\nu,mno)}\,, + $$ -is a classic accumulation term into a matrix $\mathbb M = (M^{\mu, \nu}_{ijk, mno}) \in \mathbb R^{N_1 \times N_1}$ of the same size as the mass matrix $\mathbb M^1$. In Struphy, such a term can be conveniently handled with {class}`Accumulator `. \ No newline at end of file +is a classic accumulation term into a matrix $\mathbb M = (M^{\mu, \nu}_{ijk, mno}) \in \mathbb R^{N_1 \times N_1}$ of the same size as the mass matrix $\mathbb M^1$. In Struphy, such a term can be conveniently handled with {class}`Accumulator `. +$$ diff --git a/setup/modules.json b/setup/modules.json index 5a5c893e0..b10843fe0 100644 --- a/setup/modules.json +++ b/setup/modules.json @@ -126,18 +126,10 @@ "match": { "machine_name": "TOK" }, "profiles": { "default": { - "modules_load": [ - "gcc/14", - "openmpi/5.0", - "python-waterboa/2025.06" - ] + "modules_load": ["gcc/14", "openmpi/5.0", "python-waterboa/2025.06"] }, "gcc": { - "modules_load": [ - "gcc/14", - "openmpi/5.0", - "python-waterboa/2025.06" - ] + "modules_load": ["gcc/14", "openmpi/5.0", "python-waterboa/2025.06"] }, "intel": { "modules_load": [ From 3929d3b5a48e8ef5f407e5b4469da31c60f67e2a Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 13 Sep 2026 16:15:00 +0200 Subject: [PATCH 007/193] Added a plotter for the scalars --- .gitignore | 1 + .../two_stream/pproc_two_stream.py | 3 + src/struphy/diagnostics/plotting.py | 420 +++++++++++++++--- .../diagnostics/tests/test_plotting.py | 184 ++++++++ src/struphy/post_processing/arrays.py | 112 +++++ .../post_processing/post_processing_tools.py | 39 ++ .../post_processing/tests/test_arrays.py | 93 ++++ .../tests/test_plotting_data.py | 57 +++ 8 files changed, 852 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 8e8873977..136d46db9 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ pyvenv.cfg *profile_output*.txt *kernels.txt struphy.log +struphy.log.* diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 9df602b90..b8951c815 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -12,6 +12,9 @@ def main(): pdata = PlottingData(sim=params.sim) pdata.load() + # every scalar at every time step: post_processing/scalars/{scalars.csv,*.png} + pdata.save_scalar_plots() + # electric field growth against the analytical rate (0.2845 in units of m/c) energy = pdata.scalars["electric_energy"] t = energy.coord("t") diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index f913ee90f..b53a44ced 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -5,11 +5,22 @@ needs no further arguments. """ +import logging +import os + import cunumpy as xp from matplotlib import pyplot as plt from matplotlib.widgets import Slider -from struphy.post_processing.arrays import StruphyArray, orbit_columns +from struphy.post_processing.arrays import ( + SCALARS_EXCLUDE, + StruphyArray, + orbit_columns, + save_scalars, + scalar_names, +) + +logger = logging.getLogger("struphy") #: rcParams applied by every plotter, so figures from different scripts match. STRUPHY_STYLE = { @@ -62,6 +73,49 @@ def growth_rate(y: StruphyArray, *, t0: float = None, t1: float = None, of_sqrt: return float(gamma), float(b), window +def drift(y: StruphyArray, *, ref: float = None) -> StruphyArray: + """Deviation ``y(t) - y_ref`` of a conserved quantity, with ``y_ref = y(0)`` by default.""" + values = xp.asarray(y) + reference = float(values[0]) if ref is None else float(ref) + return StruphyArray( + values - reference, + dims=y.dims, + coords=y.coords, + unit=y.unit, + label=f"{y.label} drift" if y.label else "drift", + ).with_coord_units(**y.coord_units) + + +def relative_error(y: StruphyArray, *, ref: float = None, skip_first: bool = True) -> StruphyArray: + """Relative deviation ``|y(t) - y_ref| / |y_ref|`` of a conserved quantity. + + The standard energy-conservation diagnostic: a run that conserves energy exactly + stays at zero, so on a log axis this shows the scheme's error over time. + + Parameters + ---------- + y : StruphyArray + Signal with a ``t`` dimension. + ref : float, optional + Reference value. Defaults to the first sample. + skip_first : bool + Drop ``t = 0``, where the error is identically zero and so cannot be + drawn on a log axis. + """ + values = xp.asarray(y) + reference = float(values[0]) if ref is None else float(ref) + if reference == 0.0: + raise ValueError("cannot take a relative error against a reference of zero") + + out = StruphyArray( + xp.abs(values - reference) / abs(reference), + dims=y.dims, + coords=y.coords, + label=rf"$|\Delta$ {y.label}$| / |${y.label}$(0)|$" if y.label else "relative error", + ).with_coord_units(**y.coord_units) + return out.isel(t=slice(1, None)) if skip_first else out + + def match_to_grid(values, xgrid): """Return ``values`` oriented to match ``xgrid``, transposing if that is what fits.""" values = xp.asarray(values) @@ -239,12 +293,104 @@ def show(self): plt.show() return self - def save(self, path, **kwargs): + def save(self, path, *, close: bool = False, **kwargs): + """Draw and write the figure to ``path``. + + Parameters + ---------- + close : bool + Close the figure afterwards. Pass this when saving many figures in a + loop, so that they do not all stay open. + """ self.plot() kwargs.setdefault("bbox_inches", "tight") self.fig.savefig(path, **kwargs) + if close: + self.close() return self + def close(self): + """Close the figure, unless it was supplied by the caller.""" + if self.fig is not None and self._ax is None: + plt.close(self.fig) + self.fig = None + self.ax = None + return self + + +class FrameSequence: + """Frame-by-frame output for the plotters that sweep a 2D quantity over time. + + A subclass says what a frame contains (:meth:`_frame_values`, :meth:`_frame_grids`, + :meth:`_frame_title`, :meth:`_clim`); this draws them into a single reused figure. + """ + + #: keep every ``step``-th time index + step = 1 + + @property + def frames(self): + """Time indices that will be drawn.""" + return range(0, self.data.shape[self.data.axis("t")], self.step) + + def _frame_grids(self): + return self.grids if self.grids is not None else logical_grids(self._frame_values(0)) + + def _frame_values(self, index): + return self.data.isel(t=index) + + def _frame_title(self, index): + return f"{self.title} at t = {float(self.data.coord('t')[index]):.4e}" + + def _clim(self): + return self.vmin, self.vmax + + def _setup(self): + xgrid, ygrid, xlabel, ylabel = self._frame_grids() + vmin, vmax = self._clim() + + fig, ax = self._make_axes() + pcm = ax.pcolormesh( + xgrid, + ygrid, + match_to_grid(self._frame_values(0), xgrid), + shading="auto", + vmin=vmin, + vmax=vmax, + ) + fig.colorbar(pcm, ax=ax, label=self.data.value_label) + if self.equal_aspect: + ax.set_aspect("equal", adjustable="box") + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.grid(False) + return fig, ax, pcm, xgrid + + def _update(self, ax, pcm, xgrid, index): + pcm.set_array(match_to_grid(self._frame_values(index), xgrid).ravel()) + ax.set_title(self._frame_title(index)) + + def save_frames(self, directory, *, prefix="frame", dpi=110): + """Write one PNG per frame into ``directory``, creating it if needed. + + Returns the list of paths written. + """ + os.makedirs(directory, exist_ok=True) + paths = [] + + with plt.rc_context(STRUPHY_STYLE): + fig, ax, pcm, xgrid = self._setup() + for n, index in enumerate(self.frames): + self._update(ax, pcm, xgrid, index) + path = os.path.join(directory, f"{prefix}_{n:04d}.png") + fig.savefig(path, dpi=dpi, bbox_inches="tight") + paths.append(path) + plt.close(fig) + self.fig = None + self.ax = None + + return paths + class TimeSeriesPlot(StruphyPlot): """Scalar quantities against time, optionally log-scaled with a growth-rate fit. @@ -315,6 +461,116 @@ def draw(self): ax.legend() +class ScalarsPlot(StruphyPlot): + """Every scalar recorded during a run on one axes, over an energy-error panel. + + The overview figure of a run: all tracked scalars against time, plus the + relative error of the conserved quantity underneath. + + Parameters + ---------- + scalars : Scalars or dict + Maps a name to a :class:`~struphy.post_processing.arrays.StruphyArray` over + ``t``, as :attr:`~struphy.post_processing.post_processing_tools.PlottingData.scalars` + provides. + names : sequence of str, optional + Plot these, in this order. Defaults to all of them. + exclude : sequence of str + Names to leave out when ``names`` is not given. + logy : bool + Log-scale the ordinate of the main axes. + relative_to : str, optional + Divide every series by this one, e.g. ``"en_tot"``. Use it when the scalars + do not share a unit, so that the common ordinate means something. + error_panel : str or None + Scalar whose conservation error is drawn in a panel below, if it was + recorded. ``None`` suppresses the panel. + + Attributes + ---------- + error : StruphyArray or None + The relative error that was drawn, so a script can report its final value. + """ + + tight = False + + def __init__( + self, + scalars, + *, + names=None, + exclude=SCALARS_EXCLUDE, + logy=False, + relative_to=None, + error_panel="en_tot", + **kwargs, + ): + self.names = scalar_names(scalars, names=names, exclude=exclude) + if not self.names: + raise ValueError(f"no scalars to plot, available: {tuple(scalars.keys())}") + + kwargs.setdefault("title", "Scalars") + super().__init__(scalars[self.names[0]], **kwargs) + + self.scalars = scalars + self.logy = logy + self.relative_to = relative_to + # a panel cannot be added to an axes the caller supplied + self.error_panel = error_panel if (error_panel in scalars and self._ax is None) else None + self.error = None + self.error_ax = None + + def _series(self, name) -> StruphyArray: + values = xp.asarray(self.scalars[name]) + if self.relative_to is None: + return values + return values / xp.asarray(self.scalars[self.relative_to]) + + def _ylabel(self) -> str: + if self.relative_to is not None: + return f"quantity / {self.relative_to}" + units = {self.scalars[n].unit for n in self.names} + return f"[{units.pop()}]" if len(units) == 1 else "[a.u.]" + + def draw(self): + if self.error_panel is None: + fig, ax = self._make_axes() + ax_err = None + else: + fig, (ax, ax_err) = plt.subplots( + 2, + 1, + sharex=True, + figsize=(8.0, 6.5), + height_ratios=(2, 1), + layout="constrained", + ) + self.fig, self.ax = fig, ax + self.error_ax = ax_err + + for name in self.names: + ax.plot(self.scalars[name].coord("t"), self._series(name), label=name) + + if self.logy: + ax.set_yscale("log") + ax.set_ylabel(self._ylabel()) + ax.set_title(self.title) + ax.legend(fontsize="small", ncols=max(1, len(self.names) // 6)) + + if ax_err is None: + ax.set_xlabel(self.data.axis_label("t")) + return + + self.error = relative_error(self.scalars[self.error_panel]) + error_values = xp.asarray(self.error) + ax_err.plot(self.error.coord("t"), error_values) + # an exactly conserved quantity has nothing to show on a log axis + if xp.any(error_values > 0.0): + ax_err.set_yscale("log") + ax_err.set_xlabel(self.data.axis_label("t")) + ax_err.set_ylabel(rf"$|\Delta$ {self.error_panel}$|$ / {self.error_panel}$(0)$", fontsize="small") + + class Slice2DPlot(StruphyPlot): """A 2D quantity as a pcolormesh, with the colorbar and orientation handled. @@ -432,7 +688,7 @@ def draw(self): fig.suptitle(" — ".join(filter(None, (self.title, self._run_label())))) -class SliderPlot(StruphyPlot): +class SliderPlot(FrameSequence, StruphyPlot): """A 2D quantity with a time slider, and a second slider for the free axis in 3D. The returned object keeps a reference to its sliders; discarding it stops the @@ -445,6 +701,11 @@ class SliderPlot(StruphyPlot): second slider. slice_dim : str, optional Which dimension the second slider steps through. Defaults to the last. + slice_index : int, optional + Where the second slider starts, and which cut :meth:`save_frames` writes. + Defaults to the middle of ``slice_dim``. + step : int + Keep every ``step``-th time index when writing frames. grids : tuple or callable, optional Either fixed ``(xgrid, ygrid, xlabel, ylabel)``, or a function of the slice index returning them. Pass a callable when the physical grid depends on where @@ -453,16 +714,38 @@ class SliderPlot(StruphyPlot): tight = False - def __init__(self, data, *, grids=None, slice_dim=None, vmin=None, vmax=None, equal_aspect=True, **kwargs): + def __init__( + self, + data, + *, + grids=None, + slice_dim=None, + slice_index=None, + step=1, + vmin=None, + vmax=None, + equal_aspect=True, + **kwargs, + ): super().__init__(data, **kwargs) self.grids = grids + self.step = step self.vmin = vmin self.vmax = vmax self.equal_aspect = equal_aspect spatial = [d for d in data.dims if d != "t"] self.slice_dim = slice_dim if slice_dim is not None else (spatial[-1] if len(spatial) > 2 else None) + n_slice = data.shape[data.axis(self.slice_dim)] if self.slice_dim else 0 + self.slice_index = n_slice // 2 if slice_index is None else slice_index self.sliders = [] + def _frame_values(self, index): + """The frame sequence holds the cut fixed and sweeps time, as the time slider does.""" + return self._frame(index, self.slice_index) + + def _frame_grids(self): + return self._grids_for(self.slice_index) + def _frame(self, t_index, slice_index): frame = self.data.isel(t=t_index) if self.slice_dim is not None: @@ -481,7 +764,7 @@ def draw(self): t = self.data.coord("t") n_slice = self.data.shape[self.data.axis(self.slice_dim)] if self.slice_dim else 0 - slice_index = n_slice // 2 if n_slice else 0 + slice_index = self.slice_index if n_slice else 0 first = self._frame(0, slice_index) xgrid, ygrid, xlabel, ylabel = self._grids_for(slice_index) @@ -524,6 +807,8 @@ def draw(self): def update(_): ti = int(s_time.val) si = int(s_slice.val) if s_slice is not None else 0 + # so that a cut found with the slider is the one save_frames writes + self.slice_index = si # a grid that depends on the cut has to be redrawn, not just refilled if callable(self.grids) and si != state["slice"]: @@ -557,7 +842,7 @@ def update(_): self.mesh = pcm -class AnimationPlot(StruphyPlot): +class AnimationPlot(FrameSequence, StruphyPlot): """Sweep a 2D quantity over time, as a matplotlib animation or a frame sequence. Parameters @@ -581,35 +866,11 @@ def __init__(self, data, *, grids=None, step=1, vmin=None, vmax=None, shared_cli self.shared_clim = shared_clim self.equal_aspect = equal_aspect - @property - def frames(self): - """Time indices that will be drawn.""" - return range(0, self.data.shape[self.data.axis("t")], self.step) - - def _setup(self): - first = self.data.isel(t=0) - grids = self.grids if self.grids is not None else logical_grids(first) - xgrid, ygrid, xlabel, ylabel = grids - - vmin, vmax = self.vmin, self.vmax - if self.shared_clim and vmin is None and vmax is None: + def _clim(self): + if self.shared_clim and self.vmin is None and self.vmax is None: values = xp.asarray(self.data) - vmin, vmax = float(xp.nanmin(values)), float(xp.nanmax(values)) - - fig, ax = self._make_axes() - pcm = ax.pcolormesh(xgrid, ygrid, match_to_grid(first, xgrid), shading="auto", vmin=vmin, vmax=vmax) - fig.colorbar(pcm, ax=ax, label=self.data.value_label) - if self.equal_aspect: - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.grid(False) - return fig, ax, pcm, xgrid - - def _update(self, ax, pcm, xgrid, index): - t = self.data.coord("t") - pcm.set_array(match_to_grid(self.data.isel(t=index), xgrid).ravel()) - ax.set_title(f"{self.title} at t = {float(t[index]):.4e}") + return float(xp.nanmin(values)), float(xp.nanmax(values)) + return self.vmin, self.vmax def draw(self): fig, ax, pcm, xgrid = self._setup() @@ -632,27 +893,6 @@ def animate(self, *, interval=100): self.fig = fig return anim - def save_frames(self, directory, *, prefix="frame", dpi=110): - """Write one PNG per frame into ``directory``, creating it if needed. - - Returns the list of paths written. - """ - import os - - os.makedirs(directory, exist_ok=True) - paths = [] - - with plt.rc_context(STRUPHY_STYLE): - fig, ax, pcm, xgrid = self._setup() - for n, index in enumerate(self.frames): - self._update(ax, pcm, xgrid, index) - path = os.path.join(directory, f"{prefix}_{n:04d}.png") - fig.savefig(path, dpi=dpi, bbox_inches="tight") - paths.append(path) - plt.close(fig) - - return paths - class MarkerTrajectoryPlot(StruphyPlot): """Marker positions in 3D over time, coloured by weight, with a time slider. @@ -718,10 +958,76 @@ def update(_): slider.on_changed(update) +def save_all_scalars( + scalars, + directory, + *, + names=None, + exclude=SCALARS_EXCLUDE, + logy=False, + params=None, + table: str = "csv", + file_format: str = "png", + dpi: int = 110, +) -> list[str]: + """Write the standard scalar output of a run: the table, an overview, one figure each. + + Everything a finished run should leave behind for its scalars, in one call. + + Parameters + ---------- + scalars : Scalars or dict + Maps a name to a :class:`~struphy.post_processing.arrays.StruphyArray` over ``t``. + directory : str + Created if it does not exist. + names, exclude + See :func:`~struphy.post_processing.arrays.scalar_names`. + logy : bool + Log-scale the ordinate of every figure. + params : ParamsIn, optional + Run settings, added to each figure as a suptitle. + table : str or None + Format of the per-time-step table, ``"csv"`` or ``"npz"``; ``None`` to skip it. + file_format : str + Image format of the figures. + + Returns + ------- + list of str + The paths written, the table first. + """ + selected = scalar_names(scalars, names=names, exclude=exclude) + if not selected: + logger.warning("No scalars to save.") + return [] + + os.makedirs(directory, exist_ok=True) + paths = [] + + if table is not None: + paths.append(save_scalars(scalars, os.path.join(directory, f"scalars.{table}"), names=selected, fmt=table)) + + overview = os.path.join(directory, f"scalars.{file_format}") + ScalarsPlot(scalars, names=selected, logy=logy, params=params).save(overview, dpi=dpi, close=True) + paths.append(overview) + + for name in selected: + path = os.path.join(directory, f"{name}.{file_format}") + TimeSeriesPlot( + scalars[name], + logy=logy, + fit=False, + title=name, + params=params, + ).save(path, dpi=dpi, close=True) + paths.append(path) + + logger.info(f"Wrote {len(paths)} scalar output files to {directory}") + return paths + + def plot_equilibrium_profile(path_out, *, ax=None): """Radial profiles of the equilibrium written to ``geometry.vts``.""" - import os - import pyvista as pv equil = pv.read(os.path.join(path_out, "geometry.vts")) diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 2083ae03f..414bb40d9 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -4,6 +4,8 @@ plotters derive from the data rather than the appearance of the result. """ +import os + import matplotlib import numpy as np import pytest @@ -17,13 +19,17 @@ AnimationPlot, MarkerTrajectoryPlot, PanelGridPlot, + ScalarsPlot, Slice2DPlot, SliderPlot, TimeSeriesPlot, + drift, field_slice_grids, growth_rate, logical_grids, match_to_grid, + relative_error, + save_all_scalars, ) from struphy.post_processing.arrays import StruphyArray, wrap_orbits # noqa: E402 @@ -288,3 +294,181 @@ def test_save_writes_a_file(tmp_path): fit=False, ).save(out) assert out.exists() and out.stat().st_size > 0 + + +# ---------------------------------------------------------------- conservation + + +def scalars(nt=6): + """The shape of ``PlottingData.scalars``, with a drifting total energy.""" + t = np.linspace(0.0, 1.0, nt) + return { + "en_tot": StruphyArray(2.0 + 0.02 * t, dims=("t",), coords={"t": t}, label="en tot"), + "en_e": StruphyArray(np.linspace(1.0, 1.5, nt), dims=("t",), coords={"t": t}, label="en e"), + "en_b": StruphyArray(np.linspace(1.0, 0.5, nt), dims=("t",), coords={"t": t}, label="en b"), + "time": StruphyArray(t, dims=("t",), coords={"t": t}), + } + + +def test_relative_error_is_measured_against_the_first_sample(): + t = np.linspace(0, 1, 5) + y = StruphyArray(np.array([2.0, 2.0, 2.2, 2.0, 1.8]), dims=("t",), coords={"t": t}, label="E") + + err = relative_error(y) + # t = 0 is dropped, where the error is identically zero and unplottable on a log axis + assert err.shape == (4,) + np.testing.assert_allclose(np.asarray(err), [0.0, 0.1, 0.0, 0.1], atol=1e-12) + np.testing.assert_allclose(err.coord("t"), t[1:]) + + +def test_relative_error_takes_an_explicit_reference(): + y = StruphyArray(np.array([2.0, 3.0]), dims=("t",), coords={"t": np.arange(2.0)}) + np.testing.assert_allclose(np.asarray(relative_error(y, ref=1.0, skip_first=False)), [1.0, 2.0]) + + +def test_relative_error_refuses_a_zero_reference(): + y = StruphyArray(np.zeros(3), dims=("t",), coords={"t": np.arange(3.0)}) + with pytest.raises(ValueError, match="reference of zero"): + relative_error(y) + + +def test_drift_is_the_signed_deviation(): + y = StruphyArray(np.array([2.0, 2.5, 1.0]), dims=("t",), coords={"t": np.arange(3.0)}, label="E") + d = drift(y) + np.testing.assert_allclose(np.asarray(d), [0.0, 0.5, -1.0]) + assert d.dims == ("t",) + + +# ---------------------------------------------------------------- scalars plot + + +def test_scalars_plot_draws_every_scalar_but_the_excluded(): + plot = ScalarsPlot(scalars()).plot() + labels = [line.get_label() for line in plot.ax.get_lines()] + assert labels == ["en_tot", "en_e", "en_b"] + + +def test_scalars_plot_adds_the_conservation_panel(): + plot = ScalarsPlot(scalars()).plot() + + assert plot.error_ax is not None + assert plot.error_ax.get_yscale() == "log" + # en_tot drifts by 1% of its initial value over the run + assert float(np.asarray(plot.error)[-1]) == pytest.approx(0.01) + + +def test_scalars_plot_without_a_conserved_quantity_has_no_panel(): + """Not every model tracks ``en_tot``; the overview must still work.""" + without = {k: v for k, v in scalars().items() if k != "en_tot"} + plot = ScalarsPlot(without).plot() + assert plot.error_ax is None and plot.error is None + assert plot.ax.get_xlabel() == "$t$" + + +def test_scalars_plot_can_normalize_the_mixed_units_away(): + plot = ScalarsPlot(scalars(), relative_to="en_tot").plot() + assert plot.ax.get_ylabel() == "quantity / en_tot" + # en_tot against itself is one everywhere + np.testing.assert_allclose(plot.ax.get_lines()[0].get_ydata(), 1.0) + + +def test_scalars_plot_labels_the_shared_unit(): + t = np.linspace(0, 1, 4) + joules = {n: StruphyArray(np.ones(4), dims=("t",), coords={"t": t}, unit="J") for n in ("en_e", "en_b")} + assert ScalarsPlot(joules, error_panel=None).plot().ax.get_ylabel() == "[J]" + + +def test_scalars_plot_honours_a_supplied_axes(): + fig, ax = plt.subplots() + plot = ScalarsPlot(scalars(), ax=ax).plot() + assert plot.ax is ax + assert plot.error_ax is None # a panel cannot be added to someone else's axes + + +def test_scalars_plot_needs_something_to_plot(): + with pytest.raises(ValueError, match="no scalars to plot"): + ScalarsPlot({"time": StruphyArray(np.zeros(3), dims=("t",))}) + + +# ---------------------------------------------------------------- saving + + +def test_save_all_scalars_writes_the_table_and_one_figure_each(tmp_path): + paths = save_all_scalars(scalars(), str(tmp_path)) + names = sorted(os.path.basename(p) for p in paths) + + assert names == ["en_b.png", "en_e.png", "en_tot.png", "scalars.csv", "scalars.png"] + assert all(os.path.getsize(p) > 0 for p in paths) + + +def test_save_all_scalars_closes_its_figures(tmp_path): + """Saving a run's worth of scalars must not leave every figure open.""" + save_all_scalars(scalars(), str(tmp_path)) + assert plt.get_fignums() == [] + + +def test_save_all_scalars_can_skip_the_table(tmp_path): + paths = save_all_scalars(scalars(), str(tmp_path), table=None) + assert not any(p.endswith(".csv") for p in paths) + + +def test_save_all_scalars_of_nothing_writes_nothing(tmp_path): + assert save_all_scalars({}, str(tmp_path)) == [] + + +def test_save_closes_the_figure_only_when_asked(tmp_path): + y = StruphyArray(np.arange(1.0, 5.0), dims=("t",), coords={"t": np.arange(4.0)}) + + plot = TimeSeriesPlot(y, fit=False).save(tmp_path / "a.png") + assert plot.fig is not None and plt.get_fignums() != [] + plt.close("all") + + plot = TimeSeriesPlot(y, fit=False).save(tmp_path / "b.png", close=True) + assert plot.fig is None and plt.get_fignums() == [] + + +def test_save_does_not_close_an_axes_it_was_given(tmp_path): + fig, ax = plt.subplots() + y = StruphyArray(np.arange(1.0, 5.0), dims=("t",), coords={"t": np.arange(4.0)}) + TimeSeriesPlot(y, fit=False, ax=ax).save(tmp_path / "c.png", close=True) + assert plt.get_fignums() == [fig.number] + + +def test_slider_plot_writes_frames_at_the_current_cut(tmp_path): + field = StruphyArray( + np.arange(3 * 6 * 7 * 5, dtype=float).reshape(3, 6, 7, 5), + dims=("t", "e1", "e2", "e3"), + coords={"t": np.linspace(0, 1, 3)}, + ) + plot = SliderPlot(field, slice_dim="e3", slice_index=1) + + paths = plot.save_frames(tmp_path, prefix="phi") + assert [os.path.basename(p) for p in paths] == ["phi_0000.png", "phi_0001.png", "phi_0002.png"] + assert plt.get_fignums() == [] + + +def test_slider_frames_follow_the_slider(tmp_path): + """The cut found interactively is the one that gets written out.""" + field = StruphyArray(np.zeros((2, 4, 4, 5)), dims=("t", "e1", "e2", "e3")) + plot = SliderPlot(field, slice_dim="e3").plot() + assert plot.slice_index == 2 + + plot.sliders[1].set_val(4) + assert plot.slice_index == 4 + + +def test_slider_plot_steps_through_time(tmp_path): + plot = SliderPlot(phase_space(nt=10), step=4) + assert list(plot.frames) == [0, 4, 8] + assert len(plot.save_frames(tmp_path)) == 3 + + +def test_scalars_plot_keeps_a_linear_error_axis_when_nothing_drifts(): + """A short run can conserve exactly, which a log axis cannot draw.""" + t = np.linspace(0, 1, 4) + exact = { + "en_tot": StruphyArray(np.full(4, 2.0), dims=("t",), coords={"t": t}), + "en_e": StruphyArray(np.ones(4), dims=("t",), coords={"t": t}), + } + plot = ScalarsPlot(exact).plot() + assert plot.error_ax.get_yscale() == "linear" diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 4e9ef4bfb..2ec0de179 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -1,9 +1,13 @@ """Labeled arrays for post-processed Struphy output data.""" +import logging +import os from dataclasses import dataclass, field, replace import cunumpy as xp +logger = logging.getLogger("struphy") + #: LaTeX display labels for the dimension names used across Struphy output. DIM_LABELS = { "t": r"$t$", @@ -46,6 +50,10 @@ } +#: Names in the ``scalar`` group that are not physics quantities. +SCALARS_EXCLUDE = ("time",) + + @dataclass class StruphyArray: """Array of simulation output together with its dimension names, coordinates and unit. @@ -202,6 +210,110 @@ def __repr__(self): return f"<{name} ({dims}) [{self.unit or 'a.u.'}]>" +def scalar_names(scalars, *, names=None, exclude=SCALARS_EXCLUDE) -> list[str]: + """Names of the scalar time series to work with, in a stable order. + + Parameters + ---------- + scalars : Scalars or dict + Anything with ``keys()`` and ``[]`` returning a :class:`StruphyArray` over ``t``. + names : sequence of str, optional + Restrict to these, in the given order. Unknown names raise. + exclude : sequence of str + Dropped when ``names`` is not given. + """ + if names is not None: + missing = [n for n in names if n not in scalars] + if missing: + raise KeyError(f"no scalars {missing}, available: {tuple(scalars.keys())}") + return list(names) + return [n for n in scalars.keys() if n not in exclude] + + +def scalars_table(scalars, *, names=None, exclude=SCALARS_EXCLUDE): + """Stack the scalar time series into one table, rows being time steps. + + This is the per-step record of the run in the form it is usually wanted in: + one time column and one column per scalar. + + Parameters + ---------- + scalars : Scalars or dict + Maps a name to a :class:`StruphyArray` over ``t``. + names, exclude + See :func:`scalar_names`. + + Returns + ------- + t, names, values : xp.ndarray, list of str, xp.ndarray + ``values`` has shape ``(len(t), len(names))``. Series whose length does not + match the time coordinate are dropped, since they cannot share the table. + """ + selected = scalar_names(scalars, names=names, exclude=exclude) + if not selected: + return xp.zeros(0), [], xp.zeros((0, 0)) + + t = xp.asarray(scalars[selected[0]].coord("t")) + + kept, columns = [], [] + for name in selected: + values = xp.asarray(scalars[name]) + if values.shape != t.shape: + logger.warning(f"Scalar {name!r} has shape {values.shape}, expected {t.shape}; excluded from the table.") + continue + kept.append(name) + columns.append(values) + + return t, kept, xp.stack(columns, axis=1) + + +def save_scalars(scalars, path: str, *, names=None, exclude=SCALARS_EXCLUDE, fmt: str = None) -> str: + """Write every scalar time series, at every time step, to one file. + + Parameters + ---------- + scalars : Scalars or dict + Maps a name to a :class:`StruphyArray` over ``t``. + path : str + Destination. The format is taken from its suffix unless ``fmt`` is given. + names, exclude + See :func:`scalar_names`. + fmt : str + ``"csv"`` (a header row of ``t`` and the scalar names) or ``"npz"`` (one + array per name plus ``t``). + + Returns + ------- + str + The path written. + """ + t, names_out, values = scalars_table(scalars, names=names, exclude=exclude) + + if fmt is None: + fmt = os.path.splitext(path)[1].lstrip(".").lower() or "csv" + if fmt not in ("csv", "npz"): + raise ValueError(f"unknown format {fmt!r}, expected 'csv' or 'npz'") + + # savez appends the suffix itself, so the returned path would otherwise be wrong + if fmt == "npz" and not path.endswith(".npz"): + path += ".npz" + + directory = os.path.dirname(path) + if directory: + os.makedirs(directory, exist_ok=True) + + if fmt == "npz": + xp.savez(path, t=t, **{n: values[:, i] for i, n in enumerate(names_out)}) + else: + with open(path, "w") as f: + f.write(",".join(["t", *names_out]) + "\n") + for row in range(len(t)): + f.write(",".join(f"{float(v):.17g}" for v in (t[row], *values[row])) + "\n") + + logger.info(f"Wrote {len(names_out)} scalars over {len(t)} time steps to {path}") + return path + + def orbit_columns(n_columns: int) -> dict: """Meaning of each marker-orbit column for a given saved width. diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 8fe5bc6e6..a1facbceb 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -28,6 +28,7 @@ from struphy.pic.base import Particles from struphy.post_processing.arrays import ( StruphyArray, + save_scalars, wrap_binned_slice, wrap_field_data, wrap_orbits, @@ -1612,6 +1613,44 @@ def load_scalars(self, *, physical_time: bool = True): logger.info(f"Loaded scalars: {self._scalars.keys()}") return self._scalars + def save_scalars(self, path: str = None, **kwargs) -> str: + """Write every scalar, at every time step, as one table. + + Parameters + ---------- + path : str, optional + Destination; the format follows its suffix. Defaults to + ``post_processing/scalars.csv`` in the output folder. + **kwargs + Passed to :func:`~struphy.post_processing.arrays.save_scalars`. + """ + if not self._scalars.keys(): + self.load_scalars() + if path is None: + path = os.path.join(self.path_pproc, "scalars.csv") + return save_scalars(self._scalars, path, **kwargs) + + def save_scalar_plots(self, directory: str = None, **kwargs) -> list[str]: + """Write the table, an overview figure and one figure per scalar. + + Parameters + ---------- + directory : str, optional + Defaults to ``post_processing/scalars`` in the output folder. + **kwargs + Passed to :func:`~struphy.diagnostics.plotting.save_all_scalars`. + """ + from struphy.diagnostics.plotting import save_all_scalars + + if not self._scalars.keys(): + self.load_scalars() + if directory is None: + directory = os.path.join(self.path_pproc, "scalars") + # not setdefault: reading the parameters must not be forced when they are given + if "params" not in kwargs: + kwargs["params"] = self.params + return save_all_scalars(self._scalars, directory, **kwargs) + def load(self): """Load all post-processed data from disk into memory. diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index 1ff35fef1..835b56c20 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -4,12 +4,17 @@ dimension bookkeeping, coordinate pairing and back-compatible indexing behave. """ +import os + import numpy as np import pytest from struphy.post_processing.arrays import ( StruphyArray, orbit_columns, + save_scalars, + scalar_names, + scalars_table, wrap_binned_slice, wrap_field_data, wrap_orbits, @@ -228,3 +233,91 @@ def test_wrap_field_data_sorts_times(): def test_wrap_field_data_of_empty_dict_is_none(): assert wrap_field_data({}, None) is None + + +# ---------------------------------------------------------------- scalar export + +NT_SCALARS = 5 + + +def make_scalars(**extra): + """The shape of ``PlottingData.scalars``: a name -> time series mapping.""" + t = np.linspace(0.0, 2.0, NT_SCALARS) + scalars = { + "en_tot": StruphyArray(np.full(NT_SCALARS, 3.0), dims=("t",), coords={"t": t}, label="en tot"), + "en_e": StruphyArray(np.linspace(1.0, 2.0, NT_SCALARS), dims=("t",), coords={"t": t}, label="en e"), + "time": StruphyArray(t, dims=("t",), coords={"t": t}), + } + scalars.update(extra) + return scalars + + +def test_scalar_names_drops_the_excluded_ones(): + assert scalar_names(make_scalars()) == ["en_tot", "en_e"] + + +def test_scalar_names_keeps_the_requested_order(): + assert scalar_names(make_scalars(), names=["en_e", "time"]) == ["en_e", "time"] + + +def test_scalar_names_rejects_an_unknown_name(): + with pytest.raises(KeyError, match="no scalars"): + scalar_names(make_scalars(), names=["en_nope"]) + + +def test_scalars_table_is_one_column_per_scalar(): + t, names, values = scalars_table(make_scalars()) + + assert names == ["en_tot", "en_e"] + assert values.shape == (NT_SCALARS, 2) + np.testing.assert_allclose(t, np.linspace(0.0, 2.0, NT_SCALARS)) + np.testing.assert_allclose(values[:, 0], 3.0) + np.testing.assert_allclose(values[:, 1], np.linspace(1.0, 2.0, NT_SCALARS)) + + +def test_scalars_table_drops_a_series_of_the_wrong_length(): + """A short series cannot share the time column, and must not corrupt the table.""" + odd = StruphyArray(np.zeros(2), dims=("t",), coords={"t": np.zeros(2)}) + t, names, values = scalars_table(make_scalars(odd_one=odd)) + + assert "odd_one" not in names + assert values.shape == (NT_SCALARS, 2) + + +def test_scalars_table_of_nothing_is_empty(): + t, names, values = scalars_table({}) + assert names == [] and len(t) == 0 + + +def test_save_scalars_writes_a_csv_with_a_header(tmp_path): + path = save_scalars(make_scalars(), str(tmp_path / "scalars.csv")) + lines = open(path).read().splitlines() + + assert lines[0] == "t,en_tot,en_e" + assert len(lines) == NT_SCALARS + 1 + # every row is one time step: t, then one value per scalar + first = [float(v) for v in lines[1].split(",")] + assert first == pytest.approx([0.0, 3.0, 1.0]) + + +def test_save_scalars_round_trips_through_npz(tmp_path): + path = save_scalars(make_scalars(), str(tmp_path / "scalars.npz")) + loaded = np.load(path) + + assert set(loaded.files) == {"t", "en_tot", "en_e"} + np.testing.assert_allclose(loaded["en_e"], np.linspace(1.0, 2.0, NT_SCALARS)) + + +def test_save_scalars_takes_the_format_from_the_suffix(tmp_path): + npz = save_scalars(make_scalars(), str(tmp_path / "table"), fmt="npz") + assert npz.endswith(".npz") and os.path.exists(npz) + + +def test_save_scalars_rejects_an_unknown_format(tmp_path): + with pytest.raises(ValueError, match="unknown format"): + save_scalars(make_scalars(), str(tmp_path / "scalars.xlsx")) + + +def test_save_scalars_creates_the_directory(tmp_path): + path = save_scalars(make_scalars(), str(tmp_path / "new" / "dir" / "scalars.csv")) + assert os.path.exists(path) diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py index 46b87d03c..020668193 100644 --- a/src/struphy/post_processing/tests/test_plotting_data.py +++ b/src/struphy/post_processing/tests/test_plotting_data.py @@ -71,11 +71,68 @@ def pdata(tmp_path): return data +def write_raw_scalars(root): + """Write the ``scalar`` group of a raw output file, as the simulation records it.""" + import h5py + + data_dir = os.path.join(root, "data") + os.makedirs(data_dir, exist_ok=True) + t = np.linspace(0.0, 1.0, NT) + + with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as f: + f.create_dataset("time/value", data=t) + f.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) + f.create_dataset("scalar/en_e", data=np.linspace(1.0, 1.5, NT)) + return t + + +@pytest.fixture +def pdata_scalars(tmp_path): + """A run whose raw scalars are readable without its parameter file.""" + out = write_pproc_tree(str(tmp_path)) + t = write_raw_scalars(out) + data = PlottingData(path_out=out) + # physical_time would need the run's units, and so its parameters + data.load_scalars(physical_time=False) + return data, t + + def test_load_without_raw_data_skips_scalars(pdata): """Scalars come from the raw HDF5, which a post-processing-only folder lacks.""" assert pdata.scalars.keys() == () +def test_scalars_are_labeled_time_series(pdata_scalars): + data, t = pdata_scalars + + assert set(data.scalars.keys()) == {"en_tot", "en_e"} + assert data.scalars["en_tot"].dims == ("t",) + np.testing.assert_allclose(data.scalars["en_tot"].coord("t"), t) + + +def test_save_scalars_defaults_into_the_post_processing_folder(pdata_scalars): + data, t = pdata_scalars + path = data.save_scalars() + + assert path == os.path.join(data.path_pproc, "scalars.csv") + lines = open(path).read().splitlines() + assert set(lines[0].split(",")) == {"t", "en_tot", "en_e"} + assert len(lines) == len(t) + 1 + + +def test_save_scalar_plots_writes_the_standard_set(pdata_scalars): + data, _ = pdata_scalars + paths = data.save_scalar_plots(params=None) + + assert sorted(os.path.basename(p) for p in paths) == [ + "en_e.png", + "en_tot.png", + "scalars.csv", + "scalars.png", + ] + assert all(p.startswith(os.path.join(data.path_pproc, "scalars")) for p in paths) + + def test_grids_are_loaded(pdata): assert len(pdata.grids_log) == 3 assert pdata.grids_phy[0].shape == (N1, N2, N3) From 90e005bb8e999630b01e87290781908f81585aa4 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 13 Sep 2026 16:38:47 +0200 Subject: [PATCH 008/193] Update docstring --- src/struphy/post_processing/post_processing_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index a1facbceb..58a7a043b 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -123,7 +123,7 @@ class Slice(Container): class Scalars(Container): - """Time series recorded at every step, read straight from the raw HDF5 output. + """Time series recorded every ``save_step``-th step, read straight from the raw HDF5 output. Unlike the other containers this needs no prior call to :meth:`PostProcessor.process`. """ @@ -1513,7 +1513,7 @@ def units(self): @property def scalars(self) -> Scalars: - """Scalar time series recorded every step, keyed by name. + """Scalar time series recorded every ``save_step``-th step, keyed by name. Each entry is a :class:`~struphy.post_processing.arrays.StruphyArray` over ``t``, with the time coordinate already converted to seconds. From cf4421e3d635c996cf803bb6e51b604ba94d1946 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 08:47:56 +0200 Subject: [PATCH 009/193] Restore .github to devel version --- .../pull_request_template.md | 2 +- .github/actions/compile/action.yml | 2 +- .../install/feectools-submodule/action.yml | 20 ++++---- .../install-struphy-editable/action.yml | 3 +- .../install/install-struphy/action.yml | 6 +-- .../actions/install/macos-latest/action.yml | 3 +- .../install/struphy_in_container/action.yml | 38 +++++++-------- .../actions/install/ubuntu-latest/action.yml | 2 +- .github/actions/struphy-version/action.yml | 2 +- .github/actions/submodule-diff/action.yml | 2 +- .github/issue_template.md | 2 + .github/pull_request_template.md | 2 +- .github/workflows/create-PR-parameters.yml | 40 ++++++++-------- .github/workflows/create-PR-tutorials.yml | 46 +++++++++---------- .github/workflows/docs-preview.yml | 4 +- .github/workflows/docs.yml | 2 +- .github/workflows/gh-release.yml | 14 +++--- .github/workflows/ghcr.yml | 7 +-- .github/workflows/profiling-clusters.yml | 4 +- .github/workflows/pypi-release.yml | 42 ++++++++--------- .../workflows/reusable-profiling-clusters.yml | 6 +-- .github/workflows/reusable-scheduled.yml | 2 +- .github/workflows/reusable-unit-testing.yml | 8 ++-- .github/workflows/scheduled-macos.yml | 6 +-- .github/workflows/scheduled-ubuntu.yml | 4 +- .github/workflows/static_analysis.yml | 4 +- .github/workflows/submod-feectools.yml | 6 +-- .github/workflows/test-PR-examples.yml | 10 ++-- .github/workflows/test-PR-generate-params.yml | 8 +++- .github/workflows/test-PR-models-clones.yml | 4 +- .github/workflows/test-PR-models.yml | 6 +-- .github/workflows/test-PR-pure-python.yml | 24 +++++----- .github/workflows/test-PR-tutorials.yml | 12 ++--- .github/workflows/test-PR-unit.yml | 2 +- .github/workflows/test-clusters.yml | 4 +- 35 files changed, 180 insertions(+), 169 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index a307ed6c2..87f9ff3cc 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -12,4 +12,4 @@ None **Documentation changes:** -None +None \ No newline at end of file diff --git a/.github/actions/compile/action.yml b/.github/actions/compile/action.yml index b5b968e35..8b7e237b7 100644 --- a/.github/actions/compile/action.yml +++ b/.github/actions/compile/action.yml @@ -1,6 +1,6 @@ name: "Compile kernels with pyccel" -description: +description: inputs: language: diff --git a/.github/actions/install/feectools-submodule/action.yml b/.github/actions/install/feectools-submodule/action.yml index 5de8b49be..ab1a0c9f5 100644 --- a/.github/actions/install/feectools-submodule/action.yml +++ b/.github/actions/install/feectools-submodule/action.yml @@ -14,13 +14,13 @@ runs: - name: Install feectools from submodule shell: bash run: | - if [ -n "${{ inputs.env-name }}" ] ; then - echo "Using env: ${{ inputs.env-name }}" - source ${{ inputs.env-name }}/bin/activate - else - echo "No env specified, installing outside of any env" - fi - pip show feectools - psydac-accelerate --cleanup --yes - pip uninstall feectools -y - python3 -m pip install ./feectools/ + if [ -n "${{ inputs.env-name }}" ] ; then + echo "Using env: ${{ inputs.env-name }}" + source ${{ inputs.env-name }}/bin/activate + else + echo "No env specified, installing outside of any env" + fi + pip show feectools + psydac-accelerate --cleanup --yes + pip uninstall feectools -y + python3 -m pip install ./feectools/ diff --git a/.github/actions/install/install-struphy-editable/action.yml b/.github/actions/install/install-struphy-editable/action.yml index 8728e3d39..50e088c17 100644 --- a/.github/actions/install/install-struphy-editable/action.yml +++ b/.github/actions/install/install-struphy-editable/action.yml @@ -1,10 +1,11 @@ name: "Clone and install struphy" -description: +description: runs: using: composite steps: + - name: Install struphy shell: bash run: | diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml index 35d469a8f..0f26d5cf0 100644 --- a/.github/actions/install/install-struphy/action.yml +++ b/.github/actions/install/install-struphy/action.yml @@ -1,12 +1,12 @@ name: "Install struphy (in virtual environment)" -description: +description: inputs: - env-name: + env-name: default: "" optional-deps: - default: "dev" + default: 'dev' runs: using: composite diff --git a/.github/actions/install/macos-latest/action.yml b/.github/actions/install/macos-latest/action.yml index 480d47dc3..0dc3f859c 100644 --- a/.github/actions/install/macos-latest/action.yml +++ b/.github/actions/install/macos-latest/action.yml @@ -1,6 +1,6 @@ name: "Install MacOS prereqs" -description: +description: runs: using: composite @@ -32,3 +32,4 @@ runs: echo "FC=$(which gfortran)" >> $GITHUB_ENV # for gvec echo "CC=$(which gcc)" >> $GITHUB_ENV # for gvec echo "CXX=$(which g++)" >> $GITHUB_ENV # for gvec + diff --git a/.github/actions/install/struphy_in_container/action.yml b/.github/actions/install/struphy_in_container/action.yml index 1e4bef8b4..5f4405ccb 100644 --- a/.github/actions/install/struphy_in_container/action.yml +++ b/.github/actions/install/struphy_in_container/action.yml @@ -1,6 +1,6 @@ name: "Install Struphy in Container" -description: +description: runs: using: composite @@ -14,21 +14,21 @@ runs: - name: Install struphy shell: bash run: | - ls / -a - which python3 - cd /struphy_fortran_ - ls -a - git status - git fetch origin - echo ${GIT_BRANCH_NAME} - git checkout ${GIT_BRANCH_NAME} - git pull - git status - source env_fortran_/bin/activate - which python3 - pip install -U --upgrade-strategy eager -e ".[phys,mpi]" - pip install -e ".[doc]" - PYTHON=$(which python) - STRUPHY_PATH=$($PYTHON -c 'import importlib.util; import os; print(os.path.dirname(importlib.util.find_spec("struphy").origin))') - echo "Struphy is installed at: $STRUPHY_PATH" - echo "STRUPHY_PATH=${STRUPHY_PATH}" >> $GITHUB_ENV + ls / -a + which python3 + cd /struphy_fortran_ + ls -a + git status + git fetch origin + echo ${GIT_BRANCH_NAME} + git checkout ${GIT_BRANCH_NAME} + git pull + git status + source env_fortran_/bin/activate + which python3 + pip install -U --upgrade-strategy eager -e ".[phys,mpi]" + pip install -e ".[doc]" + PYTHON=$(which python) + STRUPHY_PATH=$($PYTHON -c 'import importlib.util; import os; print(os.path.dirname(importlib.util.find_spec("struphy").origin))') + echo "Struphy is installed at: $STRUPHY_PATH" + echo "STRUPHY_PATH=${STRUPHY_PATH}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/actions/install/ubuntu-latest/action.yml b/.github/actions/install/ubuntu-latest/action.yml index 81d0b597e..e87f36cdd 100644 --- a/.github/actions/install/ubuntu-latest/action.yml +++ b/.github/actions/install/ubuntu-latest/action.yml @@ -1,6 +1,6 @@ name: "Install ubuntu prereqs" -description: +description: runs: using: composite diff --git a/.github/actions/struphy-version/action.yml b/.github/actions/struphy-version/action.yml index 016095cd3..077c6ac44 100644 --- a/.github/actions/struphy-version/action.yml +++ b/.github/actions/struphy-version/action.yml @@ -13,4 +13,4 @@ runs: echo "Failed to parse version from pyproject.toml" >&2 exit 1 fi - echo "STRUPHY_VERSION=$STRUPHY_VERSION" >> "$GITHUB_ENV" + echo "STRUPHY_VERSION=$STRUPHY_VERSION" >> "$GITHUB_ENV" \ No newline at end of file diff --git a/.github/actions/submodule-diff/action.yml b/.github/actions/submodule-diff/action.yml index 87dc03ff5..77139a736 100644 --- a/.github/actions/submodule-diff/action.yml +++ b/.github/actions/submodule-diff/action.yml @@ -35,4 +35,4 @@ runs: echo "SUBMOD_CHANGED=true" >> $GITHUB_ENV echo "SUBMOD_NAME=${{ inputs.submod-name }}" >> $GITHUB_ENV exit 0 - fi + fi \ No newline at end of file diff --git a/.github/issue_template.md b/.github/issue_template.md index 87d6f257b..b2c4eb7bc 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -9,3 +9,5 @@ **Proposed solution:** ... + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a307ed6c2..87f9ff3cc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,4 +12,4 @@ None **Documentation changes:** -None +None \ No newline at end of file diff --git a/.github/workflows/create-PR-parameters.yml b/.github/workflows/create-PR-parameters.yml index 616db493e..c42a24996 100644 --- a/.github/workflows/create-PR-parameters.yml +++ b/.github/workflows/create-PR-parameters.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Read Struphy version from pyproject @@ -29,26 +29,26 @@ jobs: - name: Copy examples and push new branch run: | - cd struphy-parameter-files - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - cp -r ../examples/. ./ - git status - git add . - git commit -m "Changes to parameter files from struphy (on push to main)" - git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + cd struphy-parameter-files + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + cp -r ../examples/. ./ + git status + git add . + git commit -m "Changes to parameter files from struphy (on push to main)" + git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - name: Create version tag in struphy-parameter-files run: | - cd struphy-parameter-files - TAG_NAME="v${STRUPHY_VERSION}" - if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then - echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." - else - git tag -a "${TAG_NAME}" -m "Parameter files for Struphy v${STRUPHY_VERSION}" - git push origin "${TAG_NAME}" - fi + cd struphy-parameter-files + TAG_NAME="v${STRUPHY_VERSION}" + if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then + echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." + else + git tag -a "${TAG_NAME}" -m "Parameter files for Struphy v${STRUPHY_VERSION}" + git push origin "${TAG_NAME}" + fi - name: Create pull request run: | @@ -57,6 +57,6 @@ jobs: --title "New parameter files from struphy v${STRUPHY_VERSION}" \ --body "This is an auto-generated PR from a Struphy workflow (on push to main)." \ --head from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} \ - --base main + --base main env: - GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} + GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/create-PR-tutorials.yml b/.github/workflows/create-PR-tutorials.yml index 4de0178b1..826a19a7b 100644 --- a/.github/workflows/create-PR-tutorials.yml +++ b/.github/workflows/create-PR-tutorials.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Read Struphy version from pyproject @@ -29,29 +29,29 @@ jobs: - name: Copy tutorials and push new branch run: | - cd struphy-tutorials - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - ls -a - rm ./tutorial* - ls -a - cp -r ../tutorials/. ./ - git status - git add . - git commit -m "Changes to tutorials from struphy (on push to main)" - git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + cd struphy-tutorials + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} + ls -a + rm ./tutorial* + ls -a + cp -r ../tutorials/. ./ + git status + git add . + git commit -m "Changes to tutorials from struphy (on push to main)" + git push origin from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} - name: Create version tag in struphy-tutorials run: | - cd struphy-tutorials - TAG_NAME="v${STRUPHY_VERSION}" - if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then - echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." - else - git tag -a "${TAG_NAME}" -m "Tutorials for Struphy v${STRUPHY_VERSION}" - git push origin "${TAG_NAME}" - fi + cd struphy-tutorials + TAG_NAME="v${STRUPHY_VERSION}" + if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" > /dev/null; then + echo "Tag ${TAG_NAME} already exists in origin. Skipping tag creation." + else + git tag -a "${TAG_NAME}" -m "Tutorials for Struphy v${STRUPHY_VERSION}" + git push origin "${TAG_NAME}" + fi - name: Create pull request run: | @@ -60,6 +60,6 @@ jobs: --title "New tutorials from struphy v${STRUPHY_VERSION}" \ --body "This is an auto-generated PR from a Struphy workflow (on push to main)." \ --head from-struphy-${{ github.ref_name }}-${{ github.event.number }}-${{ github.run_number }} \ - --base main + --base main env: - GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} + GH_TOKEN: ${{ secrets.STRUPHY_HUB_ACCESS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 293af805b..fd17f1678 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: pr_number: - description: "PR number to build/publish a preview for" + description: 'PR number to build/publish a preview for' required: true concurrency: @@ -94,7 +94,7 @@ jobs: - name: Install Struphy (dev-doc) uses: ./.github/actions/install/install-struphy with: - optional-deps: "dev,doc" + optional-deps: 'dev,doc' - name: Compile Struphy uses: ./.github/actions/compile diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6b79ea907..5c9f89c76 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,7 +46,7 @@ jobs: - name: Install Struphy (dev-doc) uses: ./.github/actions/install/install-struphy with: - optional-deps: "dev,doc" + optional-deps: 'dev,doc' - name: Compile Struphy uses: ./.github/actions/compile diff --git a/.github/workflows/gh-release.yml b/.github/workflows/gh-release.yml index e3903f7c9..27c946633 100644 --- a/.github/workflows/gh-release.yml +++ b/.github/workflows/gh-release.yml @@ -2,7 +2,7 @@ name: Release Struphy on Github on: push: - branches: + branches: - main jobs: @@ -21,14 +21,14 @@ jobs: uses: maybe-hello-world/pyproject-check-version@v4 id: versioncheck with: - pyproject-path: "./pyproject.toml" # default value - + pyproject-path: "./pyproject.toml" # default value + - name: Check output shell: bash run: | - echo "Output: ${{ steps.versioncheck.outputs.local_version_is_higher }}" # 'true' or 'false - echo "Local version: ${{ steps.versioncheck.outputs.local_version }}" # e.g., 0.1.1 - echo "Public version: ${{ steps.versioncheck.outputs.public_version }}" # e.g., 0.1.0 + echo "Output: ${{ steps.versioncheck.outputs.local_version_is_higher }}" # 'true' or 'false + echo "Local version: ${{ steps.versioncheck.outputs.local_version }}" # e.g., 0.1.1 + echo "Public version: ${{ steps.versioncheck.outputs.public_version }}" # e.g., 0.1.0 - name: Release uses: softprops/action-gh-release@v2 @@ -38,3 +38,5 @@ jobs: with: tag_name: v${{ steps.versioncheck.outputs.local_version }} body_path: ${{ github.workspace }}/CHANGELOG.md + + \ No newline at end of file diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index 28fee8bdf..17a0706bc 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -10,7 +10,7 @@ on: # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: REGISTRY: ghcr.io - IMAGE_NAME_1: ${{ github.repository }}/ubuntu-with-reqs + IMAGE_NAME_1: ${{ github.repository }}/ubuntu-with-reqs IMAGE_NAME_2: ${{ github.repository }}/ubuntu-with-struphy # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. @@ -52,7 +52,7 @@ jobs: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_1 }}:latest labels: ${{ steps.meta.outputs.labels }} - + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 @@ -98,7 +98,7 @@ jobs: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2 }}:latest labels: ${{ steps.meta.outputs.labels }} - + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 @@ -106,3 +106,4 @@ jobs: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2}} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true + diff --git a/.github/workflows/profiling-clusters.yml b/.github/workflows/profiling-clusters.yml index 95b5ae38d..18b3c6b31 100644 --- a/.github/workflows/profiling-clusters.yml +++ b/.github/workflows/profiling-clusters.yml @@ -14,7 +14,7 @@ jobs: # uses: ./.github/workflows/reusable-profiling-clusters.yml # with: # runner_tags: '["self-hosted", "protected", "raven"]' - + # Viper: # uses: ./.github/workflows/reusable-profiling-clusters.yml # with: @@ -23,7 +23,7 @@ jobs: # uses: ./.github/workflows/reusable-profiling-clusters.yml # with: # runner_tags: '["self-hosted", "tok"]' - + Pitagora: uses: ./.github/workflows/reusable-profiling-clusters.yml with: diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index c7acf5446..2afbaa837 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -17,26 +17,26 @@ jobs: url: https://pypi.org/project/struphy/ permissions: - id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: Install build tools - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Build the package - run: python -m build - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - # with: - # repository-url: https://test.pypi.org/legacy/ + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build the package + run: python -m build + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/.github/workflows/reusable-profiling-clusters.yml b/.github/workflows/reusable-profiling-clusters.yml index 736d51bf4..1459915d6 100644 --- a/.github/workflows/reusable-profiling-clusters.yml +++ b/.github/workflows/reusable-profiling-clusters.yml @@ -42,7 +42,7 @@ jobs: curl -fsSL https://raw.githubusercontent.com/max-models/whereami/main/install.sh | bash export PATH="${HOME}/.local/bin:${PATH}" whereami - + - name: Create a virtual environment for Fortran shell: bash run: | @@ -62,7 +62,7 @@ jobs: fi source "$VENV_NAME/bin/activate" echo "Activated virtual environment" - + - name: Install dependencies shell: bash run: | @@ -80,7 +80,7 @@ jobs: source .venv-fortran/bin/activate # python -c "import struphy; print(struphy.__version__)" struphy --help - + - name: Verify MPI installation shell: bash run: | diff --git a/.github/workflows/reusable-scheduled.yml b/.github/workflows/reusable-scheduled.yml index 23ee9b99b..fa12f281a 100644 --- a/.github/workflows/reusable-scheduled.yml +++ b/.github/workflows/reusable-scheduled.yml @@ -76,7 +76,7 @@ jobs: - name: Install struphy uses: ./.github/actions/install/install-struphy with: - optional-deps: "mpi,phys" + optional-deps: 'mpi,phys' env: FC: ${{ env.FC }} CC: ${{ env.CC }} diff --git a/.github/workflows/reusable-unit-testing.yml b/.github/workflows/reusable-unit-testing.yml index 4c2077b7e..3910378aa 100644 --- a/.github/workflows/reusable-unit-testing.yml +++ b/.github/workflows/reusable-unit-testing.yml @@ -43,7 +43,7 @@ jobs: - shard: shard-3 test_files: feec/tests/ - shard: shard-4 - test_files: pic/tests/ + test_files: pic/tests/ container: image: ghcr.io/struphy-hub/struphy/ubuntu-with-struphy:latest @@ -63,7 +63,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Set environment for serial run @@ -108,7 +108,7 @@ jobs: - name: Install Struphy in Container uses: ./.github/actions/install/struphy_in_container - + - name: Get submodule diff uses: ./.github/actions/submodule-diff with: @@ -143,4 +143,4 @@ jobs: $UNINSTALL_MPI git status || true echo "Running $PYTEST_CMD $TEST_FILES" - $PYTEST_CMD $TEST_FILES + $PYTEST_CMD $TEST_FILES \ No newline at end of file diff --git a/.github/workflows/scheduled-macos.yml b/.github/workflows/scheduled-macos.yml index 42e2b49f7..56aa6e462 100644 --- a/.github/workflows/scheduled-macos.yml +++ b/.github/workflows/scheduled-macos.yml @@ -3,8 +3,8 @@ name: Scheduled MacOS on: schedule: # run at 1 a.m. on Sundays and Wednesdays - - cron: "0 1 * * 0" - - cron: "0 1 * * 3" + - cron: "0 1 * * 0" + - cron: "0 1 * * 3" workflow_dispatch: concurrency: @@ -15,4 +15,4 @@ jobs: macos-install-test: uses: ./.github/workflows/reusable-scheduled.yml with: - os: macos-latest + os: macos-latest \ No newline at end of file diff --git a/.github/workflows/scheduled-ubuntu.yml b/.github/workflows/scheduled-ubuntu.yml index 866a831f9..97d497958 100644 --- a/.github/workflows/scheduled-ubuntu.yml +++ b/.github/workflows/scheduled-ubuntu.yml @@ -3,7 +3,7 @@ name: Scheduled Ubuntu on: schedule: # run at 1 a.m. on Sundays and Wednesdays - - cron: "0 1 * * 0" + - cron: "0 1 * * 0" - cron: "0 1 * * 3" workflow_dispatch: @@ -15,4 +15,4 @@ jobs: ubuntu-install-test: uses: ./.github/workflows/reusable-scheduled.yml with: - os: ubuntu-latest + os: ubuntu-latest \ No newline at end of file diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 49958455c..e2f031bfc 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -33,7 +33,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Set up Python ${{ matrix.python-version }} @@ -117,7 +117,7 @@ jobs: run: | pip install ruff==0.15.0 ruff check - + # - name: ruff format --check # run: | # ruff format --check diff --git a/.github/workflows/submod-feectools.yml b/.github/workflows/submod-feectools.yml index a0d0a9bc9..33f8d9e40 100644 --- a/.github/workflows/submod-feectools.yml +++ b/.github/workflows/submod-feectools.yml @@ -21,15 +21,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Get submodule diff uses: ./.github/actions/submodule-diff with: start-dir: "." - + - name: Run workflow if submodule changed (all PR commits) if: env.SUBMOD_CHANGED == 'true' run: | - echo "${{ env.SUBMOD_NAME }} has changed, running tests..." + echo "${{ env.SUBMOD_NAME }} has changed, running tests..." \ No newline at end of file diff --git a/.github/workflows/test-PR-examples.yml b/.github/workflows/test-PR-examples.yml index c69a02594..83cb3f8a5 100644 --- a/.github/workflows/test-PR-examples.yml +++ b/.github/workflows/test-PR-examples.yml @@ -34,7 +34,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Check .testmondata 1 @@ -58,7 +58,7 @@ jobs: run: | ls .testmon* || echo "No .testmondata" - - name: Install Struphy in Container + - name: Install Struphy in Container uses: ./.github/actions/install/struphy_in_container - name: Get submodule diff @@ -66,8 +66,8 @@ jobs: with: start-dir: /struphy_fortran_ - - name: Reinstall feectools from submodule - if: env.SUBMOD_CHANGED == 'true' + - name: Reinstall feectools from submodule + if: env.SUBMOD_CHANGED == 'true' uses: ./.github/actions/install/feectools-submodule with: env-name: /struphy_fortran_/env_fortran_ @@ -76,7 +76,7 @@ jobs: uses: ./.github/actions/compile with: env-name: /struphy_fortran_/env_fortran_ - + - name: Run examples tests (serial) env: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-examples diff --git a/.github/workflows/test-PR-generate-params.yml b/.github/workflows/test-PR-generate-params.yml index 4b2930662..7bc4b016b 100644 --- a/.github/workflows/test-PR-generate-params.yml +++ b/.github/workflows/test-PR-generate-params.yml @@ -29,13 +29,14 @@ jobs: username: spossann password: ${{ secrets.GHCR_TOKEN }} steps: + - name: Check for dockerenv file run: (ls /.dockerenv && echo Found dockerenv) || (echo No dockerenv) - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: fetch origin devel (for submodule-diff action) @@ -46,7 +47,7 @@ jobs: git fetch --no-tags origin devel:refs/remotes/origin/devel git rev-parse --verify origin/devel - - name: Build Struphy + - name: Build Struphy run: | ls python3 -m venv struphy_env @@ -80,3 +81,6 @@ jobs: source struphy_env/bin/activate struphy params Maxwell -y struphy params VlasovAmpereOneSpecies -y + + + diff --git a/.github/workflows/test-PR-models-clones.yml b/.github/workflows/test-PR-models-clones.yml index 6349c73a9..7b729f217 100644 --- a/.github/workflows/test-PR-models-clones.yml +++ b/.github/workflows/test-PR-models-clones.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Check .testmondata 1 @@ -88,4 +88,4 @@ jobs: source /struphy_fortran_/env_fortran_/bin/activate cd /struphy_fortran_/src/struphy mpirun -n 1 pytest -m single --testmon-forceselect -xs --with-mpi --model-name Maxwell - mpirun --oversubscribe -n 4 pytest -x --testmon --with-mpi --nclones 2 models/tests/verification/ + mpirun --oversubscribe -n 4 pytest -x --testmon --with-mpi --nclones 2 models/tests/verification/ \ No newline at end of file diff --git a/.github/workflows/test-PR-models.yml b/.github/workflows/test-PR-models.yml index 60caab2f4..4a2e83442 100644 --- a/.github/workflows/test-PR-models.yml +++ b/.github/workflows/test-PR-models.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: Check .testmondata 1 @@ -94,7 +94,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-model run: | source /struphy_fortran_/env_fortran_/bin/activate - pytest -x -m models --testmon-forceselect /struphy_fortran_/src/struphy/models/tests/default_params/ + pytest -x -m models --testmon-forceselect /struphy_fortran_/src/struphy/models/tests/default_params/ - name: Verification tests shell: bash @@ -121,4 +121,4 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-model-mpi run: | source /struphy_fortran_/env_fortran_/bin/activate - mpirun --oversubscribe -n 2 pytest -x --testmon --with-mpi /struphy_fortran_/src/struphy/models/tests/verification/ + mpirun --oversubscribe -n 2 pytest -x --testmon --with-mpi /struphy_fortran_/src/struphy/models/tests/verification/ \ No newline at end of file diff --git a/.github/workflows/test-PR-pure-python.yml b/.github/workflows/test-PR-pure-python.yml index d118b5655..85f52858f 100644 --- a/.github/workflows/test-PR-pure-python.yml +++ b/.github/workflows/test-PR-pure-python.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - name: fetch origin devel @@ -89,14 +89,14 @@ jobs: source env/bin/activate struphy compile -d struphy compile --status - + - name: MPI run check shell: bash run: | - source env/bin/activate - mpirun --version - mpirun --oversubscribe -n 1 python -c "from mpi4py import MPI; print('rank:', MPI.COMM_WORLD.rank); print('size:', MPI.COMM_WORLD.size)" - + source env/bin/activate + mpirun --version + mpirun --oversubscribe -n 1 python -c "from mpi4py import MPI; print('rank:', MPI.COMM_WORLD.rank); print('size:', MPI.COMM_WORLD.size)" + # - name: Pytest-MPI plugin check # shell: bash # run: | @@ -109,7 +109,7 @@ jobs: source env/bin/activate cd src/struphy mpirun --oversubscribe -n 1 pytest --collect-only -q - + - name: Manual LinearMHD test shell: bash env: @@ -142,7 +142,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python run: | source env/bin/activate - pytest -m single -xvs --model-name GuidingCenter $STRUPHY_PATH + pytest -m single -xvs --model-name GuidingCenter $STRUPHY_PATH - name: VlasovAmpere test shell: bash @@ -150,7 +150,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python run: | source env/bin/activate - pytest -m single -xvs --model-name VlasovAmpereOneSpecies $STRUPHY_PATH + pytest -m single -xvs --model-name VlasovAmpereOneSpecies $STRUPHY_PATH - name: ViscousEulerSPH test shell: bash @@ -158,7 +158,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python run: | source env/bin/activate - pytest -m single -xvs --model-name ViscousEulerSPH $STRUPHY_PATH + pytest -m single -xvs --model-name ViscousEulerSPH $STRUPHY_PATH - name: Vlasov test MPI shell: bash @@ -184,7 +184,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name VlasovAmpereOneSpecies $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name VlasovAmpereOneSpecies $STRUPHY_PATH - name: ViscousEulerSPH test MPI shell: bash @@ -192,4 +192,4 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name ViscousEulerSPH $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name ViscousEulerSPH $STRUPHY_PATH diff --git a/.github/workflows/test-PR-tutorials.yml b/.github/workflows/test-PR-tutorials.yml index 0ddf38e17..9acb3754f 100644 --- a/.github/workflows/test-PR-tutorials.yml +++ b/.github/workflows/test-PR-tutorials.yml @@ -34,10 +34,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - submodules: true # This is crucial! + submodules: true # This is crucial! fetch-depth: 5 - - name: Install Struphy in Container + - name: Install Struphy in Container uses: ./.github/actions/install/struphy_in_container - name: Get submodule diff @@ -45,8 +45,8 @@ jobs: with: start-dir: /struphy_fortran_ - - name: Reinstall feectools from submodule - if: env.SUBMOD_CHANGED == 'true' + - name: Reinstall feectools from submodule + if: env.SUBMOD_CHANGED == 'true' uses: ./.github/actions/install/feectools-submodule with: env-name: /struphy_fortran_/env_fortran_ @@ -55,7 +55,7 @@ jobs: uses: ./.github/actions/compile with: env-name: /struphy_fortran_/env_fortran_ - + - name: Run tutorials env: PYVISTA_OFF_SCREEN: "true" @@ -68,4 +68,4 @@ jobs: cd /struphy_fortran_/tutorials ls jupyter nbconvert --show-config - jupyter nbconvert --execute --embed-images --to html tutorial_*.ipynb + jupyter nbconvert --execute --embed-images --to html tutorial_*.ipynb \ No newline at end of file diff --git a/.github/workflows/test-PR-unit.yml b/.github/workflows/test-PR-unit.yml index 849a88954..10cdf62c4 100644 --- a/.github/workflows/test-PR-unit.yml +++ b/.github/workflows/test-PR-unit.yml @@ -20,4 +20,4 @@ jobs: os: ubuntu-latest n-procs: 1 secrets: - ghcr-token: ${{ secrets.GHCR_TOKEN }} + ghcr-token: ${{ secrets.GHCR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test-clusters.yml b/.github/workflows/test-clusters.yml index fd7634287..11b6990dd 100644 --- a/.github/workflows/test-clusters.yml +++ b/.github/workflows/test-clusters.yml @@ -13,7 +13,7 @@ jobs: uses: ./.github/workflows/reusable-clusters.yml with: runner_tags: '["self-hosted", "protected", "raven"]' - + Viper: uses: ./.github/workflows/reusable-clusters.yml with: @@ -22,7 +22,7 @@ jobs: # uses: ./.github/workflows/reusable-clusters.yml # with: # runner_tags: '["self-hosted", "protected", "tok"]' - + Pitagora: uses: ./.github/workflows/reusable-clusters.yml with: From 5ee4389152e67f7a794d2c28a48e5925a7b5df8d Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 08:51:51 +0200 Subject: [PATCH 010/193] Restored various other files (only aestetic changes --- .gitlab-ci.yml | 145 ++++---- .pre-commit-config.yaml | 10 +- .readthedocs.yml | 10 +- CHANGELOG.md | 231 ++++++------ CONTRIBUTING.md | 22 +- README.md | 11 +- doc/_static/css/custom.css | 9 +- doc/_static/my_theme.css | 2 +- doc/markdown/vlasov-maxwell.md | 657 +++++++++++++-------------------- 9 files changed, 492 insertions(+), 605 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ae64c61b0..6d60c7e1f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ -# Keyword reference for the .gitlab-ci.yml file: -# https://gitlab.mpcdf.mpg.de/help/ci/yaml/index.md +# Keyword reference for the .gitlab-ci.yml file: +# https://gitlab.mpcdf.mpg.de/help/ci/yaml/index.md # A "scheduled" pipeline can be tested by clicking "Run pipeline" on Gitlab and setting the TEST_SCHEDULED = true. # The doc can be built by clicking "Run pipeline" on Gitlab and setting the MAKE_PAGES = true. @@ -38,7 +38,7 @@ stages: - test - lint - pages - - release + - release # --- images --- @@ -58,8 +58,8 @@ stages: .variables_push: variables: - LANGUAGE: "c" - OMP: "" + LANGUAGE: 'c' + OMP: '' # --- rules --- @@ -161,12 +161,12 @@ stages: - module list - module avail # struphy modules - - module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 + - module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 # gvec modules and variables - - module load cmake netcdf-serial mkl hdf5-serial - - export FC=`which gfortran` - - export CC=`which gcc` - - export CXX=`which g++` + - module load cmake netcdf-serial mkl hdf5-serial + - export FC=`which gfortran` + - export CC=`which gcc` + - export CXX=`which g++` # verify - module list - echo "Fortran compiler is ${FC}" @@ -178,22 +178,22 @@ stages: - !reference [.scripts, inspect_directory] # basic - dnf install -y wget yum-utils make openssl-devel bzip2-devel libffi-devel zlib-devel - - dnf update -y + - dnf update -y # compilers and mpi - dnf install -y gcc - - dnf install -y gfortran - - dnf install -y blas-devel lapack-devel - - dnf install -y openmpi openmpi-devel + - dnf install -y gfortran + - dnf install -y blas-devel lapack-devel + - dnf install -y openmpi openmpi-devel # python - dnf install -y python3-devel - - dnf install -y python3-mpi4py-openmpi + - dnf install -y python3-mpi4py-openmpi # gvec - - dnf install -y g++ cmake netcdf netcdf-devel netcdf-fortran netcdf-fortran-devel pkgconf - - export FC=`which gfortran` - - export CC=`which gcc` + - dnf install -y g++ cmake netcdf netcdf-devel netcdf-fortran netcdf-fortran-devel pkgconf + - export FC=`which gfortran` + - export CC=`which gcc` - export CXX=`which g++` # additional - - dnf install -y git + - dnf install -y git - dnf install -y pandoc - dnf update -y - export OMPI_ALLOW_RUN_AS_ROOT=1 @@ -203,7 +203,7 @@ stages: - source env/bin/activate - . /etc/profile.d/modules.sh - module load mpi/openmpi-$(arch) - - module list + - module list .requirements_opensuse: before_script: @@ -211,24 +211,24 @@ stages: # basic - zypper refresh # compilers and mpi - - zypper install -y gcc-fortran gcc - - zypper install -y blas-devel lapack-devel + - zypper install -y gcc-fortran gcc + - zypper install -y blas-devel lapack-devel - zypper install -y openmpi openmpi-devel openmpi4-devel - - zypper install -y libgomp1 + - zypper install -y libgomp1 # python - zypper install -y python3 python3-devel - zypper install -y python3-pip python3-virtualenv python3-pkgconfig # gvec - - zypper install -y gcc-c++ cmake netcdf - - zypper addrepo -G https://download.opensuse.org/repositories/science/openSUSE_Tumbleweed/science.repo - - zypper install -y netcdf-fortran-devel - - export FC=`which gfortran` - - export CC=`which gcc` + - zypper install -y gcc-c++ cmake netcdf + - zypper addrepo -G https://download.opensuse.org/repositories/science/openSUSE_Tumbleweed/science.repo + - zypper install -y netcdf-fortran-devel + - export FC=`which gfortran` + - export CC=`which gcc` - export CXX=`which g++` # additional - - zypper install -y git - - zypper install -y pandoc - - zypper install -y vim + - zypper install -y git + - zypper install -y pandoc + - zypper install -y vim - zypper install -y make - export OMPI_ALLOW_RUN_AS_ROOT=1 - export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 @@ -243,21 +243,21 @@ stages: - !reference [.scripts, inspect_directory] # basic - yum install -y wget yum-utils make openssl-devel bzip2-devel libffi-devel zlib-devel - - yum update -y - - yum clean all + - yum update -y + - yum clean all # compilers and mpi - - yum install -y gcc - - yum install -y gfortran - - yum install -y openmpi openmpi-devel + - yum install -y gcc + - yum install -y gfortran + - yum install -y openmpi openmpi-devel - yum install -y libgomp - - yum install -y environment-modules + - yum install -y environment-modules # python - - wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz - - tar xzf Python-3.12.8.tgz - - cd Python-3.12.8 - - ./configure --with-system-ffi --with-computed-gotos --enable-loadable-sqlite-extensions - - make -j ${nproc} - - make altinstall + - wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz + - tar xzf Python-3.12.8.tgz + - cd Python-3.12.8 + - ./configure --with-system-ffi --with-computed-gotos --enable-loadable-sqlite-extensions + - make -j ${nproc} + - make altinstall - alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.12 1 - alternatives --set python3 /usr/local/bin/python3.12 - mv /usr/local/lib/libpython3.12.a libpython3.12.a.bak @@ -265,7 +265,7 @@ stages: - cd .. - pwd - ls - # additional + # additional - yum install -y git - export OMPI_ALLOW_RUN_AS_ROOT=1 - export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 @@ -274,10 +274,10 @@ stages: - export PATH="/usr/lib64/openmpi/bin:$PATH" # gvec - yum install -y g++ cmake which flexiblas-devel - - yum install -y epel-release - - yum install -y netcdf-devel netcdf-fortran-devel - - export FC=`which gfortran` - - export CC=`which gcc` + - yum install -y epel-release + - yum install -y netcdf-devel netcdf-fortran-devel + - export FC=`which gfortran` + - export CC=`which gcc` - export CXX=`which g++` # virtual env - python3 -m pip list @@ -409,11 +409,11 @@ stages: - struphy compile --status - struphy test LinearMHD - struphy test toy - - struphy test models - - struphy test verification + - struphy test models + - struphy test verification model_tests_mpi: - struphy compile --status - - struphy test models + - struphy test models - struphy test models --mpi 2 - struphy test verification --mpi 1 - struphy test verification --mpi 4 @@ -421,7 +421,7 @@ stages: - struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 quickstart_tests: - struphy -h - - struphy params VlasovAmpereOneSpecies + - struphy params VlasovAmpereOneSpecies - ls -1a - mv params_VlasovAmpereOneSpecies.py test.py - python3 test.py @@ -461,7 +461,7 @@ stages: .artifacts_scheduled: artifacts: - name: "python-env-installed-${LANGUAGE}-${OMP}" + name: 'python-env-installed-${LANGUAGE}-${OMP}' paths: - env_${CI_PIPELINE_ID}_${LANGUAGE}_${OMP} expire_in: 1 day @@ -668,7 +668,7 @@ pages_tests: - cd doc ; make html - mv _build/html/ $CI_PROJECT_DIR/documentation/ artifacts: - expose_as: "Documentation" + expose_as: 'Documentation' paths: - documentation/ @@ -699,7 +699,7 @@ macos_nmpp: before_script: # - brew install cmake - brew link --overwrite cmake - - cmake --version + - cmake --version - make -v - printenv - system_profiler SPHardwareDataType @@ -716,7 +716,7 @@ macos_nmpp: - pwd - ls -a - echo $_JOB_PATH - - ls -a $_JOB_PATH + - ls -a $_JOB_PATH - rm -rf $_JOB_PATH cleanup_macos: @@ -742,6 +742,7 @@ cleanup_macos: ### SCHEDULED PIPELINE ### ########################## + install_scheduled: stage: install needs: [] @@ -1018,7 +1019,7 @@ lint_full_repo_report: - struphy lint all --output-format report allow_failure: true artifacts: - expose_as: "Branch lint report" + expose_as: 'Branch lint report' paths: - code_analysis_report.html expire_in: 1 month @@ -1067,7 +1068,7 @@ lint_branch_report: - struphy lint branch --output-format report allow_failure: true artifacts: - expose_as: "Branch lint report" + expose_as: 'Branch lint report' paths: - code_analysis_report.html expire_in: 1 month @@ -1093,7 +1094,7 @@ pages: - cd doc ; make html - mv _build/html/ $CI_PROJECT_DIR/public/ artifacts: - name: "pages" + name: 'pages' paths: - public/ @@ -1125,14 +1126,14 @@ vars: - set -- $var - echo ${16} - for (( i=1; i <= "$#"; i++ )); do if (( ${i} < 30 )); then echo ${i}; echo ${!i}; fi done - - for (( i=1; i <= "$#"; i++ )); do if [[ ${!i} == "version" ]]; then echo ${i}; echo ${!i}; index=$((${i} + 2)); fi done - - echo $index + - for (( i=1; i <= "$#"; i++ )); do if [[ ${!i} == "version" ]]; then echo ${i}; echo ${!i}; index=$((${i} + 2)); fi done + - echo $index - VERSION_STR=${!index} - echo $VERSION_STR - echo "VERSION=$(echo $VERSION_STR | sed 's/^.//' | sed 's/.$//')" - echo "VERSION=$(echo $VERSION_STR | sed 's/^.//' | sed 's/.$//')" >> vars.env artifacts: - name: "vars" + name: 'vars' reports: dotenv: vars.env expire_in: 1 day @@ -1142,20 +1143,20 @@ release_job: image: registry.gitlab.com/gitlab-org/release-cli:latest extends: - .rules_gitlab_release - needs: ["vars"] + needs: ['vars'] before_script: - !reference [.scripts, inspect_directory] script: - cat /etc/*-release - echo $VERSION - release: # See https://docs.gitlab.com/ee/ci/yaml/#release for available properties - tag_name: "v$VERSION" # The version is incremented per pipeline. - name: "v$VERSION" - ref: "$CI_COMMIT_SHA" # The tag is created from the pipeline SHA. - description: "CHANGELOG.md" + release: # See https://docs.gitlab.com/ee/ci/yaml/#release for available properties + tag_name: 'v$VERSION' # The version is incremented per pipeline. + name: 'v$VERSION' + ref: '$CI_COMMIT_SHA' # The tag is created from the pipeline SHA. + description: 'CHANGELOG.md' assets: links: - - name: "Documentation" - url: "https://struphy-hub.github.io/struphy/index.html" - - name: "PyPI" - url: "https://pypi.org/project/struphy/" + - name: 'Documentation' + url: 'https://struphy-hub.github.io/struphy/index.html' + - name: 'PyPI' + url: 'https://pypi.org/project/struphy/' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad2c97896..6f6845ca0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 # Use the latest stable version + rev: v6.0.0 # Use the latest stable version hooks: - id: check-added-large-files # Prevent giant files from being committed. - args: ["--maxkb=1000"] - - id: check-merge-conflict # Check for files that contain merge conflict strings. + args: ["--maxkb=1000"] + - id: check-merge-conflict # Check for files that contain merge conflict strings. args: ["--assume-in-merge"] - - id: check-toml # Attempts to load all TOML files to verify syntax. - - id: check-yaml # Attempts to load all yaml files to verify syntax. + - id: check-toml # Attempts to load all TOML files to verify syntax. + - id: check-yaml # Attempts to load all yaml files to verify syntax. args: ["--unsafe"] - repo: https://github.com/kynan/nbstripout diff --git a/.readthedocs.yml b/.readthedocs.yml index fdc85632d..8905f66a3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,3 +1,5 @@ + + # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details @@ -36,11 +38,13 @@ build: # Build documentation in the "doc/" directory with Sphinx sphinx: - configuration: doc/conf.py + configuration: doc/conf.py # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: - install: - - requirements: doc/requirements.txt + install: + - requirements: doc/requirements.txt + + diff --git a/CHANGELOG.md b/CHANGELOG.md index d55c3b504..34b536f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,105 +74,118 @@ ## Struphy 3.2.0 - 2026-06-09 -- [PyPI](https://pypi.org/project/struphy/3.2.0) -- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.2.0) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.1.0...v3.2.0) +* [PyPI](https://pypi.org/project/struphy/3.2.0) +* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.2.0) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.1.0...v3.2.0) + ### Headlines -- New Quickstart, Userguide, Tutorials and Developer's guide in the documentation: https://github.com/struphy-hub/struphy/pull/252 -- Each `Propagator` now sits in his own .py file: https://github.com/struphy-hub/struphy/pull/236 -- New propagator `CurlCurlSolve()` for curl-curl problems: https://github.com/struphy-hub/struphy/pull/245 -- New classmethods to inspect the model docstring (in a jupyter notebook for example): https://github.com/struphy-hub/struphy/pull/229 +* New Quickstart, Userguide, Tutorials and Developer's guide in the documentation: https://github.com/struphy-hub/struphy/pull/252 +* Each `Propagator` now sits in his own .py file: https://github.com/struphy-hub/struphy/pull/236 +* New propagator `CurlCurlSolve()` for curl-curl problems: https://github.com/struphy-hub/struphy/pull/245 +* New classmethods to inspect the model docstring (in a jupyter notebook for example): https://github.com/struphy-hub/struphy/pull/229 ### API changes -- Change in the signature of `ParticleSpecies.set_markers()`, which is used in parameter files featuring particles. The new classes `SortingParameters` and `SavingParameters` replace the methods `set_sorting_boxes` and `set_save_data`. Instances of the new classes are passed to `set_markers()`: https://github.com/struphy-hub/struphy/pull/247 +* Change in the signature of `ParticleSpecies.set_markers()`, which is used in parameter files featuring particles. The new classes `SortingParameters` and `SavingParameters` replace the methods `set_sorting_boxes` and `set_save_data`. Instances of the new classes are passed to `set_markers()`: https://github.com/struphy-hub/struphy/pull/247 ### User news -- Clean-up logging levels for simulation output: https://github.com/struphy-hub/struphy/pull/247 -- New plotting functionality for kinetic backgrounds: https://github.com/struphy-hub/struphy/pull/239 -- Addition of a matrix-free averaging operator for distributed FEEC data: https://github.com/struphy-hub/struphy/pull/246 -- New default for `boxes_per_dim` is `tuple = (1, 1, 1)`: https://github.com/struphy-hub/struphy/pull/247 +* Clean-up logging levels for simulation output: https://github.com/struphy-hub/struphy/pull/247 +* New plotting functionality for kinetic backgrounds: https://github.com/struphy-hub/struphy/pull/239 +* Addition of a matrix-free averaging operator for distributed FEEC data: https://github.com/struphy-hub/struphy/pull/246 +* New default for `boxes_per_dim` is `tuple = (1, 1, 1)`: https://github.com/struphy-hub/struphy/pull/247 ### Bug fixes -- Adaptation to general equilibria and new tests of the gyrokinetic Poisson solve: https://github.com/struphy-hub/struphy/pull/238 +* Adaptation to general equilibria and new tests of the gyrokinetic Poisson solve: https://github.com/struphy-hub/struphy/pull/238 + + + ## Struphy 3.1.0 - 2026-04-24 -- [PyPI](https://pypi.org/project/struphy/3.1.0) -- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.1.0) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.4...v3.1.0) +* [PyPI](https://pypi.org/project/struphy/3.1.0) +* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.1.0) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.4...v3.1.0) + ### Headlines -- Refactoring of the `Derham` class: https://github.com/struphy-hub/struphy/pull/212 - - Renamed `Nel -> num_elements`, `p -> degree`, `nq_pr -> nquads_proj`, `polar_ck -> polar_splines` - - The type of spline basis functions is now set via the argument `bcs`, which is a tuple of length three. It holds the type for each direction: `None` for periodic, or a tuple of length two with either `free` or `dirichlet` to indicate the type of clamped splines. `bcs` replaces the two arguments `spl_kind` and `dirichlet_bc` - - The lifting is defined for each `FEECVariable` separately through the new attribute `lifting_function` -- Refactoring of `WeightedMassOperators.create_weighted_mass` method: https://github.com/struphy-hub/struphy/pull/227 - - The signature remains largely the same, except that list input for weights has been replaced by tuple input. Moreover, within the tuple, one can now pass a `SplineFunction` object, either from H1 or L2 space, which will be multiplied to the constant weights during `.assemble()`. **This allows for time dependent mass operators in nonlinear simulations (formerly treated with workarounds).** - - In the constructor of `create_weighted_mass`, we now evaluate all callables derived from `weights: tuple[str]` on the integration grid, multiply them together and then pass them as xp.arrays to `WeightedMassOperator` for building the object. This leads to much simpler code than in the previous version, where we built composed functions and passed them as weights. - - In `MassMatrixPreconditioner`, to build the Kronecker matrices from 1D matrices without domain decomposition, the values of the weights are now retrieved via an **MPI sub-communicator**, because the weights in M0, M1 etc. are now given as local xp.arrays, not as callables anymore. -- Use `logging` instead of print statements; use function `set_logging_level` from the API to set the logging level of all handlers. With pytest you can use `pytest --logging-level DEBUG src/struphy` to set the loggong level: https://github.com/struphy-hub/struphy/pull/199 and https://github.com/struphy-hub/struphy/pull/219 -- New user guide: https://github.com/struphy-hub/struphy/pull/200 -- Remove two submodules `struphy-parameter-files` and `struphy-tutorials` in favor of the new folders `examples/` or `tutorials/`: https://github.com/struphy-hub/struphy/pull/206 +* Refactoring of the `Derham` class: https://github.com/struphy-hub/struphy/pull/212 + - Renamed `Nel -> num_elements`, `p -> degree`, `nq_pr -> nquads_proj`, `polar_ck -> polar_splines` + - The type of spline basis functions is now set via the argument `bcs`, which is a tuple of length three. It holds the type for each direction: `None` for periodic, or a tuple of length two with either `free` or `dirichlet` to indicate the type of clamped splines. `bcs` replaces the two arguments `spl_kind` and `dirichlet_bc` + - The lifting is defined for each `FEECVariable` separately through the new attribute `lifting_function` +* Refactoring of `WeightedMassOperators.create_weighted_mass` method: https://github.com/struphy-hub/struphy/pull/227 + - The signature remains largely the same, except that list input for weights has been replaced by tuple input. Moreover, within the tuple, one can now pass a `SplineFunction` object, either from H1 or L2 space, which will be multiplied to the constant weights during `.assemble()`. **This allows for time dependent mass operators in nonlinear simulations (formerly treated with workarounds).** + - In the constructor of `create_weighted_mass`, we now evaluate all callables derived from `weights: tuple[str]` on the integration grid, multiply them together and then pass them as xp.arrays to `WeightedMassOperator` for building the object. This leads to much simpler code than in the previous version, where we built composed functions and passed them as weights. + - In `MassMatrixPreconditioner`, to build the Kronecker matrices from 1D matrices without domain decomposition, the values of the weights are now retrieved via an **MPI sub-communicator**, because the weights in M0, M1 etc. are now given as local xp.arrays, not as callables anymore. +* Use `logging` instead of print statements; use function `set_logging_level` from the API to set the logging level of all handlers. With pytest you can use `pytest --logging-level DEBUG src/struphy` to set the loggong level: https://github.com/struphy-hub/struphy/pull/199 and https://github.com/struphy-hub/struphy/pull/219 +* New user guide: https://github.com/struphy-hub/struphy/pull/200 +* Remove two submodules `struphy-parameter-files` and `struphy-tutorials` in favor of the new folders `examples/` or `tutorials/`: https://github.com/struphy-hub/struphy/pull/206 ### API changes -- `base_units` is removed from the `StruphyModel` constructor; all equation parameters that can be seen in the model docstring can be passed to the model constructor: https://github.com/struphy-hub/struphy/pull/222 +* `base_units` is removed from the `StruphyModel` constructor; all equation parameters that can be seen in the model docstring can be passed to the model constructor: https://github.com/struphy-hub/struphy/pull/222 + ### User news -- Add `to_dict` and `from_dict` methods to the `Simulation` class: https://github.com/struphy-hub/struphy/pull/186 -- Add `__repr__` and `__repr_no_defaults__` methods to most classes in the API: https://github.com/struphy-hub/struphy/pull/193 -- Using `pyvista`; ddd `show_3d` and `create_geometry_mesh` methods to `Domain` class: https://github.com/struphy-hub/struphy/pull/195 -- Add iterators to models and domains: https://github.com/struphy-hub/struphy/pull/196 -- Added export and from_file methods to `SimulationBase` class: https://github.com/struphy-hub/struphy/pull/197 -- Added name and description to the Simulation class: https://github.com/struphy-hub/struphy/pull/198 -- New model `ToyGyrokinetic` to simulate the diocotron instability: https://github.com/struphy-hub/struphy/pull/201 -- New classes for scalar quantities tracked during simulation (via new type `Scalar`): https://github.com/struphy-hub/struphy/pull/220 -- The components of a model docstring are now available as class methods: https://github.com/struphy-hub/struphy/pull/229 +* Add `to_dict` and `from_dict` methods to the `Simulation` class: https://github.com/struphy-hub/struphy/pull/186 +* Add `__repr__` and `__repr_no_defaults__` methods to most classes in the API: https://github.com/struphy-hub/struphy/pull/193 +* Using `pyvista`; ddd `show_3d` and `create_geometry_mesh` methods to `Domain` class: https://github.com/struphy-hub/struphy/pull/195 +* Add iterators to models and domains: https://github.com/struphy-hub/struphy/pull/196 +* Added export and from_file methods to `SimulationBase` class: https://github.com/struphy-hub/struphy/pull/197 +* Added name and description to the Simulation class: https://github.com/struphy-hub/struphy/pull/198 +* New model `ToyGyrokinetic` to simulate the diocotron instability: https://github.com/struphy-hub/struphy/pull/201 +* New classes for scalar quantities tracked during simulation (via new type `Scalar`): https://github.com/struphy-hub/struphy/pull/220 +* The components of a model docstring are now available as class methods: https://github.com/struphy-hub/struphy/pull/229 + ### Bug fixes -- Weights in initial Poisson solves of kinetic models without control variate fixed: https://github.com/struphy-hub/struphy/pull/192 +* Weights in initial Poisson solves of kinetic models without control variate fixed: https://github.com/struphy-hub/struphy/pull/192 + + + ## Struphy 3.0.4 - 2026-02-27 -- [PyPI](https://pypi.org/project/struphy/3.0.4) -- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.4) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.3...v3.0.4) +* [PyPI](https://pypi.org/project/struphy/3.0.4) +* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.4) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.3...v3.0.4) ### Bug fixes -- New lower bound on pyccel is set to 2.2.0, due to this pyccel bug fix: https://github.com/pyccel/pyccel/pull/2567 +* New lower bound on pyccel is set to 2.2.0, due to this pyccel bug fix: https://github.com/pyccel/pyccel/pull/2567 + + + ## Struphy 3.0.3 - 2026-02-23 -- [PyPI](https://pypi.org/project/struphy/3.0.3) -- [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) -- [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.3) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.2...v3.0.3) +* [PyPI](https://pypi.org/project/struphy/3.0.3) +* [GitHub Pages](https://struphy-hub.github.io/struphy/index.html) +* [GitHub release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.3) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.2...v3.0.3) ### Headlines 1. New class `Simulation` inherits the generic `SimulationBase`, both are in the new folder `struphy/simulations/`. The most important methods are: - - `Simulation.run()` - - `Simulation.pproc()` - - `Simulation.load_plotting_data()` - - `Simulation.spawn_sister()` (my new favorite!) - + * `Simulation.run()` + * `Simulation.pproc()` + * `Simulation.load_plotting_data()` + * `Simulation.spawn_sister()` (my new favorite!) + These are tested in the new tutorials: https://github.com/struphy-hub/struphy-tutorials/tree/use-species-properties The file `main.py` has been deleted. -The `Simulation` takes a model as input. Other API classes are passed as well (see tutorials). +The `Simulation` takes a model as input. Other API classes are passed as well (see tutorials). The model is viewed as everything related to the PDE, i.e. its variables, initial conditions etc. The simulation deals with the rest (geometry, derham, environment etc.) Some important changes to the logic: The model does not have access to `derham`, `mass_ops` etc. anymore, these can be called from `Propagator` when needed. Solves that need to happen before the time stepping (like initial Poisson solves) are moved to `model.allocate_helpers()`. @@ -181,48 +194,55 @@ Some important changes to the logic: The model does not have access to `derham`, 3. Several new classes have been introduced for post processing and plotting data, see `post_processing_tools.py`. The most important ones are `PostProcessor` and `PlottingData`. Dictionaries in the plotting data have been replaced by classes. Many classes now feature the `__repr__` dunder for customized printing. + ### API changes New classes exposed: `Simulation`, `PostProcessor` and `PlottingData`. + ### User news -- Add `set_zero_velocity` argument into `LoadingParameters`, enforcing velocities of all particles along specified axis to always be zero: https://github.com/struphy-hub/struphy/pull/176 -- New model `ViscousEulerSPH` replaces `EulerSPH`. The evaluation of the viscosity tensor has been implemented and tested for SPH methods. Unit tests for evaluation of the fluid velocity and its gradients (needed in the viscosity tensor) have been improved: https://github.com/struphy-hub/struphy/pull/160 +* Add `set_zero_velocity` argument into `LoadingParameters`, enforcing velocities of all particles along specified axis to always be zero: https://github.com/struphy-hub/struphy/pull/176 +* New model `ViscousEulerSPH` replaces `EulerSPH`. The evaluation of the viscosity tensor has been implemented and tested for SPH methods. Unit tests for evaluation of the fluid velocity and its gradients (needed in the viscosity tensor) have been improved: https://github.com/struphy-hub/struphy/pull/160 + + ## Struphy 3.0.2 - 2026-02-06 -- [PyPI](https://pypi.org/project/struphy/3.0.2) -- [Github pages](https://struphy-hub.github.io/struphy/index.html) -- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.2) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.1...v3.0.2) +* [PyPI](https://pypi.org/project/struphy/3.0.2) +* [Github pages](https://struphy-hub.github.io/struphy/index.html) +* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.2) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.1...v3.0.2) ### Headlines -- Added a public API. This allows imports like `from struphy import equils`: https://github.com/struphy-hub/struphy/pull/168 -- New default compile language is Fortran: https://github.com/struphy-hub/struphy/pull/158 -- Moved each model to its own file. Calling sub-processes must be avoided in the future because of incompatibility with MPI: https://github.com/struphy-hub/struphy/pull/152 +* Added a public API. This allows imports like `from struphy import equils`: https://github.com/struphy-hub/struphy/pull/168 +* New default compile language is Fortran: https://github.com/struphy-hub/struphy/pull/158 +* Moved each model to its own file. Calling sub-processes must be avoided in the future because of incompatibility with MPI: https://github.com/struphy-hub/struphy/pull/152 ### User news -- Added binning of higher order moments (current density, energy tensor) of f and delta f: https://github.com/struphy-hub/struphy/pull/162 +* Added binning of higher order moments (current density, energy tensor) of f and delta f: https://github.com/struphy-hub/struphy/pull/162 ### Developer news -- Use `pyccel 2.1`: https://github.com/struphy-hub/struphy/pull/153 -- Added three submodules: `struphy-parameter-files`, `struphy-tutorials` and`feectools`. The Struphy repo should be cloned with `git clone --recurse-submodules https://github.com/struphy-hub/struphy.git` to init and update the submodules. Also, run `git submodule update` regularly to get updates from the submodules. See https://github.com/struphy-hub/struphy/pull/154 -- Introduced class `options.LiteralOptions` for parsing literals. Moved `Units` to `physics.py`: https://github.com/struphy-hub/struphy/pull/167 +* Use `pyccel 2.1`: https://github.com/struphy-hub/struphy/pull/153 +* Added three submodules: `struphy-parameter-files`, `struphy-tutorials` and`feectools`. The Struphy repo should be cloned with `git clone --recurse-submodules https://github.com/struphy-hub/struphy.git` to init and update the submodules. Also, run `git submodule update` regularly to get updates from the submodules. See https://github.com/struphy-hub/struphy/pull/154 +* Introduced class `options.LiteralOptions` for parsing literals. Moved `Units` to `physics.py`: https://github.com/struphy-hub/struphy/pull/167 + ### Bug fixes -- Use `struphy.io.options.Units` in equils. This enables the use of GVEC, EQDSK and DESC in the new framework: https://github.com/struphy-hub/struphy/pull/158 +* Use `struphy.io.options.Units` in equils. This enables the use of GVEC, EQDSK and DESC in the new framework: https://github.com/struphy-hub/struphy/pull/158 + + ## Struphy 3.0.1 - 2025-12-11 -- [PyPI](https://pypi.org/project/struphy/3.0.1) -- [Github pages](https://struphy-hub.github.io/struphy/index.html) -- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.1) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.0...v3.0.1) +* [PyPI](https://pypi.org/project/struphy/3.0.1) +* [Github pages](https://struphy-hub.github.io/struphy/index.html) +* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.1) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v3.0.0...v3.0.1) ### Headlines @@ -236,73 +256,76 @@ None ### Developer news -- Removed legacy code (eigenvalue solver): https://github.com/struphy-hub/struphy/pull/129 -- Add context manager to h5py.File() calls: https://github.com/struphy-hub/struphy/pull/135 -- Fix undefined variables: https://github.com/struphy-hub/struphy/pull/141 +* Removed legacy code (eigenvalue solver): https://github.com/struphy-hub/struphy/pull/129 +* Add context manager to h5py.File() calls: https://github.com/struphy-hub/struphy/pull/135 +* Fix undefined variables: https://github.com/struphy-hub/struphy/pull/141 ### Bug fixes -- Fix setter in DESCequilibirum, update quickstart guide: https://github.com/struphy-hub/struphy/pull/132 -- Set defaults for given_in_basis: "0" for scalar and "v" for vector-valued: https://github.com/struphy-hub/struphy/pull/136 -- Fix the restart function: https://github.com/struphy-hub/struphy/pull/143 +* Fix setter in DESCequilibirum, update quickstart guide: https://github.com/struphy-hub/struphy/pull/132 +* Set defaults for given_in_basis: "0" for scalar and "v" for vector-valued: https://github.com/struphy-hub/struphy/pull/136 +* Fix the restart function: https://github.com/struphy-hub/struphy/pull/143 + ## Struphy 3.0.0 - 2025-11-13 -- [PyPI](https://pypi.org/project/struphy/3.0.0) -- [Github pages](https://struphy-hub.github.io/struphy/index.html) -- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.0) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v3.0.0) +* [PyPI](https://pypi.org/project/struphy/3.0.0) +* [Github pages](https://struphy-hub.github.io/struphy/index.html) +* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v3.0.0) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v3.0.0) ### Headlines Struphy 3 represents a major refactoring with breaking changes with respect to Struphy 2, in particular: -- The `.yml` parameter files cannot be used anymore. Simulation parameters have to be transferred to the new `.py` launch files that are generated from `struphy params MODEL`. See the [Struphy README](https://github.com/struphy-hub/struphy) for a quick introduction. -- The console command `struphy run ...` has been deprecated. The new way to launch simulations is by executing the `.py` launch file, for instance with `python params_MODEL.py`. -- Other deprecated console commands are `struphy pproc` and `struphy units`. Post-processing is now done through the API via `main.pproc()`. -- The Struphy repo has moved to [Github](https://github.com/struphy-hub/struphy). The [old Gitlab repo](https://gitlab.mpcdf.mpg.de/struphy/struphy) will persist but not be maintained any longer. Issues, discussion and PRs will solely take place on the new Github repo. +* The `.yml` parameter files cannot be used anymore. Simulation parameters have to be transferred to the new `.py` launch files that are generated from `struphy params MODEL`. See the [Struphy README](https://github.com/struphy-hub/struphy) for a quick introduction. +* The console command `struphy run ...` has been deprecated. The new way to launch simulations is by executing the `.py` launch file, for instance with `python params_MODEL.py`. +* Other deprecated console commands are `struphy pproc` and `struphy units`. Post-processing is now done through the API via `main.pproc()`. +* The Struphy repo has moved to [Github](https://github.com/struphy-hub/struphy). The [old Gitlab repo](https://gitlab.mpcdf.mpg.de/struphy/struphy) will persist but not be maintained any longer. Issues, discussion and PRs will solely take place on the new Github repo. ### User news -- Please consult the [Struphy README](https://github.com/struphy-hub/struphy) and links therein to get familiar with the new workflows. -- New tutorials can be found on [mybinder](https://mybinder.org/v2/gh/struphy-hub/struphy-tutorials/main). +* Please consult the [Struphy README](https://github.com/struphy-hub/struphy) and links therein to get familiar with the new workflows. +* New tutorials can be found on [mybinder](https://mybinder.org/v2/gh/struphy-hub/struphy-tutorials/main). ### Developer news Struphy has been refactored with the following principles in mind: -- get rid of console commands and increase the use of the Struphy API wherever possible -- become even more object-oriented -- use `Classes` instead of `dicts` wherever possible -- use `Literals` to show options for string arguments +* get rid of console commands and increase the use of the Struphy API wherever possible +* become even more object-oriented +* use `Classes` instead of `dicts` wherever possible +* use `Literals` to show options for string arguments In Struphy 3, models feature the following important objects: -- `ParticleSpecies`, `FieldSpecies`, `FluidSpecies` +* `ParticleSpecies`, `FieldSpecies`, `FluidSpecies` Each species is a collection of Variables: -- `PICVariable`, `FEECVariable`, `SPHVariable` +* `PICVariable`, `FEECVariable`, `SPHVariable` These variables are updated by `Propagators`. All options for a simluation can be set in the new `.py` launch file. ### Bug fixes -- Incorporate psydac updates: https://github.com/struphy-hub/struphy/pull/109 -- Auto install Psydac on first Struphy import: https://github.com/struphy-hub/struphy/pull/118 -- Remove MPI Barrier responsible for deadlock: https://github.com/struphy-hub/struphy/pull/121 +* Incorporate psydac updates: https://github.com/struphy-hub/struphy/pull/109 +* Auto install Psydac on first Struphy import: https://github.com/struphy-hub/struphy/pull/118 +* Remove MPI Barrier responsible for deadlock: https://github.com/struphy-hub/struphy/pull/121 + ## Struphy 2.6.0 - 2025-11-12 -- [PyPI](https://pypi.org/project/struphy/2.6.0) -- [Github pages](https://struphy-hub.github.io/struphy/index.html) -- [Github release](https://github.com/struphy-hub/struphy/releases/tag/v2.6.0) -- [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v2.6.0) +* [PyPI](https://pypi.org/project/struphy/2.6.0) +* [Github pages](https://struphy-hub.github.io/struphy/index.html) +* [Github release](https://github.com/struphy-hub/struphy/releases/tag/v2.6.0) +* [Diff to previous release](https://github.com/struphy-hub/struphy/compare/v2.5.0...v2.6.0) ### Headlines -- This is a test run for the relaease of Struphy 3.0 from the new Github repo +* This is a test run for the relaease of Struphy 3.0 from the new Github repo + ## Struphy 2.5.0 and prior releases -- See [Gitlab](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/releases) +* See [Gitlab](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/releases) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd71e3ee5..ad8102e53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,13 @@ # Repository -Struphy has two protected branches, **main** and **devel**. +Struphy has two protected branches, **main** and **devel**. Nobody can push directly to these branches. -The **main** branch holds the current release of the code. +The **main** branch holds the current release of the code. **devel** is the branch for developers. Feature branches must be checked out and merged into **devel**. + # Dependency Bounds On PRs Pull requests into **devel** are checked for stale dependency upper bounds in `pyproject.toml`. @@ -20,22 +21,25 @@ The policy is intentionally narrow: When the check fails, the CI summary prints local remediation commands. In short, run the checker locally, run `python utils/update_dependency_bounds.py` on that report, and commit the updated `pyproject.toml`. + # Releases Happen when pushed to **main**. + # Forking Please create a **public fork** to be able to merge your code into Struphy! You can create feature branches in your forked repo and create merge requests into the original Struphy repo. + # Contact -- [Mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) -- [MatrixChat developer's channel](https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de) -- [Issue tracker](https://github.com/struphy-hub/struphy/issues) -- [LinkedIn](https://www.linkedin.com/company/struphy/) -- [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) -- [max.lindqvist@ipp.mpg.de](mailto:max.lindqvist@ipp.mpg.de) -- [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) +* [Mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) +* [MatrixChat developer's channel](https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de) +* [Issue tracker](https://github.com/struphy-hub/struphy/issues) +* [LinkedIn](https://www.linkedin.com/company/struphy/) +* [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) +* [max.lindqvist@ipp.mpg.de](mailto:max.lindqvist@ipp.mpg.de) +* [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) diff --git a/README.md b/README.md index 1224fa670..6b97b9bcd 100755 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ + + ![STRUPHY Header](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/struphy_header_with_subs.png)

Release License badge Ubuntu latest MacOS latest isort and ruff PyPI PyPI Downloads -

# Welcome! @@ -73,7 +74,7 @@ The doc is on [Github pages](https://struphy-hub.github.io/struphy/index.html), Try out the Python API in a Jupyter notebook or any Python environment. For example, you can create a simulation object and show the domain and equilibrium magnetic field of the linear MHD model via -```python +``` python from struphy import ( Simulation, domains, @@ -131,11 +132,11 @@ There is also a [Docker image with just the prerequisites](https://hub.docker.co ## Publications - D. Bell, M.C. Pinto, S. Possanner, E. Sonnendrücker, - [**The linearized Vlasov–Maxwell system as a Hamiltonian system**](https://doi.org/10.1016/j.jcp.2026.114765), - Journal of Computational Physics, Volume 555, 114765 (2026). +[**The linearized Vlasov–Maxwell system as a Hamiltonian system**](https://doi.org/10.1016/j.jcp.2026.114765), +Journal of Computational Physics, Volume 555, 114765 (2026). - V. Carlier, M.C. Pinto, [**Variational discretizations of ideal magnetohydrodynamics in smooth regime using structure-preserving finite elements**](https://doi.org/10.1016/j.jcp.2024.113647), - Journal of Computational Physics, Volume 523, 113647 (2025). +Journal of Computational Physics, Volume 523, 113647 (2025). - Y. Li, M.C. Pinto, F. Holderied, S. Possanner, E. Sonnendrücker, [**Geometric Particle-In-Cell discretizations of a plasma hybrid model with kinetic ions and mass-less fluid electrons**](https://doi.org/10.1016/j.jcp.2023.112671), Journal of Computational Physics 498, 112671 (2023). diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 06ab25c9c..2cc31b589 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -1,11 +1,12 @@ .eqno { - float: right; + float: right; } .bd-main .bd-content .bd-article-container { - max-width: 100%; /* default is 60em */ -} + max-width: 100%; /* default is 60em */ + } .bd-page-width { - max-width: 95%; /* default is 88rem */ +max-width: 95%; /* default is 88rem */ } + \ No newline at end of file diff --git a/doc/_static/my_theme.css b/doc/_static/my_theme.css index ecb3e8fa0..40affa3ef 100644 --- a/doc/_static/my_theme.css +++ b/doc/_static/my_theme.css @@ -1,3 +1,3 @@ /* .wy-nav-content { max-width: 1200px !important; -} */ +} */ \ No newline at end of file diff --git a/doc/markdown/vlasov-maxwell.md b/doc/markdown/vlasov-maxwell.md index 73e760440..467567d1c 100644 --- a/doc/markdown/vlasov-maxwell.md +++ b/doc/markdown/vlasov-maxwell.md @@ -1,13 +1,12 @@ (disc_example)= - # Example: Vlasov-Maxwell-Poisson discretization -The Vlasov-Maxwell equations for one species in a static background provide a good example +The Vlasov-Maxwell equations for one species in a static background provide a good example for PDE discretization in Struphy (see {class}`~struphy.models.kinetic.VlasovMaxwellOneSpecies`) for the full implementation). The model we are going to discretize reads as follows: $$ \begin{aligned} - &\partial_t f + \mathbf{v} \cdot \nabla f - \frac em (\mathbf{E} + \mathbf{v} \times \mathbf{B}) + &\partial_t f + \mathbf{v} \cdot \nabla f - \frac em (\mathbf{E} + \mathbf{v} \times \mathbf{B}) \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, \\[2mm] -\frac{1}{c^2} &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\mu_0 e \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, @@ -18,13 +17,10 @@ $$ (eq:model) Here, $f(t, \mathbf x, \mathbf v)$ denotes the kinetic distribution function, $\mathbf E(t, \mathbf x)$ and $\mathbf B(t, \mathbf x)$ are the electric and magnetic field, respectively, $e/m$ is the charge-to-mass ratio of the electrons, $c$ denotes the speed of light and $\mu_0$ stands for the magnetic constant. In order to determine an initial electric field that is consistent with Gauss' law, one has to solve Poisson's equation once at the beginning of the simulation: - $$ - \begin{equation} --\epsilon*0\Delta \phi = \rho*\textrm{i0} - e \int\_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v}\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. + -\epsilon_0\Delta \phi = \rho_\textrm{i0} - e \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v}\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. \end{equation} - $$ Here, $\phi(\mathbf x)$ denotes the electrostatic potential, $\epsilon_0$ is the dielectric constant and $\rho_\textrm{i0}(\mathbf x)$ is a static ion background (the ion current is assumed zero). Aside from field and particle pushing propagators, the model features also field-particle coupling propagators, and has two particle-to-grid accumulations, one charge deposition in Poisson's equation (to be solved only once at the beginning of the simulation), and one current deposition in Ampère's law. @@ -44,129 +40,96 @@ Let us now go through these steps for the above model. Prior to implementation, we have to find suitable units for the model quantities, a process called {ref}`normalization`. For this, let us write the Vlasov-Maxwell system in terms of units (with a hat) and dimensionless quantities (with a prime): - $$ - \begin{aligned} -& \frac{\hat f}{\hat t}\,\partial*{t'} f' + \frac{\hat v \hat f}{\hat x}\,\mathbf{v}' \cdot \nabla' f' - \frac em \hat B \hat f\left(\frac{\hat E}{\hat v\hat B}\mathbf{E}' + \mathbf{v}' \times \mathbf{B}' \right) -\cdot \frac{\partial f'}{\partial \mathbf{v}'} = 0 \,, -\\[2mm] --\frac{1}{c^2} \frac{\hat E}{\hat t}&\frac{\partial \mathbf{E}'}{\partial t'} + \frac{\hat B}{\hat x}\nabla' \times \mathbf{B}' = -\mu_0 e\, \hat v \hat n \int*{\mathbb{R}^3} \mathbf{v}' f' \, \text{d} \mathbf{v}' \,, -\\[3mm] -&\frac{\hat B}{\hat t}\frac{\partial \mathbf{B}'}{\partial t'} + \frac{\hat E}{\hat x}\nabla' \times \mathbf{E}' = 0 \,, -\\[3mm] -&-\epsilon*0\,\frac{\hat \phi}{\hat x^2}\Delta' \phi' = e \hat n\left(\rho*\textrm{i0}' - \int\_{\mathbb{R}^3} f'(t=0) \, \text{d} \mathbf{v}' \right)\,,\qquad \hat E\mathbf E'(t=0) = -\frac{\hat \phi}{\hat x}\nabla' \phi'\,. + & \frac{\hat f}{\hat t}\,\partial_{t'} f' + \frac{\hat v \hat f}{\hat x}\,\mathbf{v}' \cdot \nabla' f' - \frac em \hat B \hat f\left(\frac{\hat E}{\hat v\hat B}\mathbf{E}' + \mathbf{v}' \times \mathbf{B}' \right) + \cdot \frac{\partial f'}{\partial \mathbf{v}'} = 0 \,, + \\[2mm] + -\frac{1}{c^2} \frac{\hat E}{\hat t}&\frac{\partial \mathbf{E}'}{\partial t'} + \frac{\hat B}{\hat x}\nabla' \times \mathbf{B}' = -\mu_0 e\, \hat v \hat n \int_{\mathbb{R}^3} \mathbf{v}' f' \, \text{d} \mathbf{v}' \,, + \\[3mm] + &\frac{\hat B}{\hat t}\frac{\partial \mathbf{B}'}{\partial t'} + \frac{\hat E}{\hat x}\nabla' \times \mathbf{E}' = 0 \,, + \\[3mm] + &-\epsilon_0\,\frac{\hat \phi}{\hat x^2}\Delta' \phi' = e \hat n\left(\rho_\textrm{i0}' - \int_{\mathbb{R}^3} f'(t=0) \, \text{d} \mathbf{v}' \right)\,,\qquad \hat E\mathbf E'(t=0) = -\frac{\hat \phi}{\hat x}\nabla' \phi'\,. \end{aligned} - -$$ -(eq:norm) +$$ (eq:norm) In Struphy, the three basic units $\hat x$, $\hat B$ and $\hat n$ are defined by the user. Moreover, several other units are fixed, as described in {ref}`normalization`, namely: - $$ - \hat t, \, \hat p,\, \hat \rho,\,\hat \jmath \quad \textrm{are fixed}\,. - $$ -Therefore, in the present model, - +Therefore, in the present model, $$ - \hat v,\,\hat f,\,\hat E,\,\hat \phi - $$ must be defined in order to complete the normalization process. Let us introduce the unit of the electron cyclotron frequency and its product with the time unit, - $$ - -\hat \Omega*\textrm{ce} := \frac em \hat B\qquad \varepsilon := \frac{1}{\hat \Omega*\textrm{ce} \hat t}\,. - + \hat \Omega_\textrm{ce} := \frac em \hat B\qquad \varepsilon := \frac{1}{\hat \Omega_\textrm{ce} \hat t}\,. $$ In our model, it makes sense to set the unit of the $E \times B$-velocity to $\hat v$, - $$ - \frac{\hat E}{\hat B} = \hat v = \frac{\hat x}{\hat t}\,. - $$ This determines the unit $\hat E$ of the electric field and renders Faraday's law (7) scale invariant. It also sets the unit for the electric potential, - $$ - -\hat \phi = \hat E \hat x = \hat v \hat B \hat x\,. - + \hat \phi = \hat E \hat x = \hat v \hat B \hat x\,. $$ In the Poisson equation, this brings into play the unit of the electron plasma frequency and its ration to the unit of the electron cyclotron frequency, - $$ - -\hat \Omega*\textrm{pe} := \sqrt{\frac{e^2 \hat n}{\epsilon_0 m}}\qquad \alpha := \frac{\hat \Omega*\textrm{pe}}{\hat \Omega\_\textrm{ce}}\,. - + \hat \Omega_\textrm{pe} := \sqrt{\frac{e^2 \hat n}{\epsilon_0 m}}\qquad \alpha := \frac{\hat \Omega_\textrm{pe}}{\hat \Omega_\textrm{ce}}\,. $$ Let us summarize what we have thus far, omitting the primes in {eq}`eq:norm` for clarity: - $$ - \begin{aligned} -& \partial*{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) -\cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, -\\[2mm] --\frac{\hat v^2}{c^2} &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\mu_0 e\, \hat v \hat n\hat x}{\hat B} \int*{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, -\\[2mm] -&\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, -\\[2mm] -&-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho*\textrm{i0} - \int*{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. + & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) + \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, + \\[2mm] + -\frac{\hat v^2}{c^2} &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\mu_0 e\, \hat v \hat n\hat x}{\hat B} \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, + \\[2mm] + &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, + \\[2mm] + &-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho_\textrm{i0} - \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. \end{aligned} - $$ In Ampere's law we have - $$ - -\frac{\mu*0 e\, \hat v \hat n\hat x}{\hat B} = \frac{\epsilon_0\mu_0 e^2\, \hat v^2 m\hat n\hat x}{\epsilon_0 me\hat B \hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega*\textrm{pe}^2}{\hat \Omega*\textrm{ce}} \frac{\hat x}{\hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega*\textrm{pe}^2}{\hat \Omega\_\textrm{ce}^2} \frac{1}{\varepsilon}\,. - + \frac{\mu_0 e\, \hat v \hat n\hat x}{\hat B} = \frac{\epsilon_0\mu_0 e^2\, \hat v^2 m\hat n\hat x}{\epsilon_0 me\hat B \hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega_\textrm{pe}^2}{\hat \Omega_\textrm{ce}} \frac{\hat x}{\hat v} = \frac{\hat v^2}{c^2} \frac{\hat \Omega_\textrm{pe}^2}{\hat \Omega_\textrm{ce}^2} \frac{1}{\varepsilon}\,. $$ -Therefore, choosing the velocity unit as - +Therefore, choosing the velocity unit as $$ - -\hat v = c\,, - + \hat v = c\,, $$ leads to the final, Struphy-normalized equations - $$ - \begin{aligned} -& \partial*{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) -\cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, -\\[2mm] - &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\alpha^2}{\varepsilon} \int*{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, -\\[2mm] -&\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, -\\[2mm] -&-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho*\textrm{i0} - \int*{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. + & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) + \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, + \\[2mm] + - &\frac{\partial \mathbf{E}}{\partial t} + \nabla \times \mathbf{B} = -\frac{\alpha^2}{\varepsilon} \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d} \mathbf{v} \,, + \\[2mm] + &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, + \\[2mm] + &-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho_\textrm{i0} - \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. \end{aligned} - $$ (def_spaces)= @@ -180,23 +143,20 @@ The above rule applied to the current Vlasov-Maxwell model means that Ampère's Find $(f, \mathbf E, \mathbf B, \phi) \in C^\infty \times H(\textrm{curl}) \times H(\textrm{div}) \times H^1$ such that - $$ - \begin{aligned} -& \partial*{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) -\cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, -\\[2mm] - &\int \mathbf F \cdot \frac{\partial \mathbf{E}}{\partial t} \,\textrm d \mathbf x + \int \nabla \times \mathbf{F} \cdot \mathbf B \,\textrm d \mathbf x = -\frac{\alpha^2}{\varepsilon} \int\int*{\mathbb{R}^3} \mathbf{v} \cdot \mathbf F f \,\, \text{d} \mathbf{v}\textrm d \mathbf x \,,\qquad \forall \ \mathbf F \in H(\textrm{curl})\,, -\\[3mm] -&\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, -\\[2mm] -&\int \nabla \psi \cdot \nabla \phi\,\textrm d \mathbf x = \frac{\alpha^2}{\varepsilon}\left(\int \rho*\textrm{i0}\,\psi\,\textrm d \mathbf x - \int\int*{\mathbb{R}^3} f(t=0)\, \psi \, \text{d} \mathbf{v} \textrm d \mathbf x\right)\,, \qquad \forall \ \psi \in H^1\,, -\\[4mm] -&\mathbf E(t=0) = -\nabla \phi\,. + & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) + \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, + \\[2mm] + - &\int \mathbf F \cdot \frac{\partial \mathbf{E}}{\partial t} \,\textrm d \mathbf x + \int \nabla \times \mathbf{F} \cdot \mathbf B \,\textrm d \mathbf x = -\frac{\alpha^2}{\varepsilon} \int\int_{\mathbb{R}^3} \mathbf{v} \cdot \mathbf F f \,\, \text{d} \mathbf{v}\textrm d \mathbf x \,,\qquad \forall \ \mathbf F \in H(\textrm{curl})\,, + \\[3mm] + &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, + \\[2mm] + &\int \nabla \psi \cdot \nabla \phi\,\textrm d \mathbf x = \frac{\alpha^2}{\varepsilon}\left(\int \rho_\textrm{i0}\,\psi\,\textrm d \mathbf x - \int\int_{\mathbb{R}^3} f(t=0)\, \psi \, \text{d} \mathbf{v} \textrm d \mathbf x\right)\,, \qquad \forall \ \psi \in H^1\,, + \\[4mm] + &\mathbf E(t=0) = -\nabla \phi\,. \end{aligned} - -$$ -(eq:spaces) +$$ (eq:spaces) (pullback)= ## Pull-back to the logical domain @@ -229,52 +189,43 @@ The connection of differential $p$-forms to the {ref}`Struphy de Rham spaces `. For this one needs to invert the Schur complement $S = A - BC$ of the 2x2 block matrix, which in our case reads - $$ - -S = \mathbb M^1 + \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1 \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top \qquad \in \mathbb R^{N_1 \times N_1}\,. - + S = \mathbb M^1 + \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1 \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top \qquad \in \mathbb R^{N_1 \times N_1}\,. $$ This matrix size $N_1 \times N_1$ is independent of the particle number $N$ and an inversion is thus feasible. Indeed, using the Schur complement amounts to inserting one equation into the other, thereby eliminating one variable from the solution step. Moreover, the term - $$ - -M^{\mu, \nu}_{ijk, mno} := \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1_{(\mu,ijk)} \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top\_{(\nu,mno)}\,, - + M^{\mu, \nu}_{ijk, mno} := \frac{\Delta t^2 }{4} \frac{\alpha^2}{\varepsilon^2} \mathbb L^1_{(\mu,ijk)} \bar{DF}^{-1} \bar{\mathbf w} \bar{DF}^{-\top} (\mathbb L^1)^\top_{(\nu,mno)}\,, $$ -is a classic accumulation term into a matrix $\mathbb M = (M^{\mu, \nu}_{ijk, mno}) \in \mathbb R^{N_1 \times N_1}$ of the same size as the mass matrix $\mathbb M^1$. In Struphy, such a term can be conveniently handled with {class}`Accumulator `. -$$ +is a classic accumulation term into a matrix $\mathbb M = (M^{\mu, \nu}_{ijk, mno}) \in \mathbb R^{N_1 \times N_1}$ of the same size as the mass matrix $\mathbb M^1$. In Struphy, such a term can be conveniently handled with {class}`Accumulator `. \ No newline at end of file From 943d9aaa78d590dd7987e62101bafb496ad3b86c Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 17:25:51 +0200 Subject: [PATCH 011/193] formatting --- src/struphy/diagnostics/plotting.py | 10 +++++++--- src/struphy/diagnostics/tests/test_plotting.py | 6 +----- src/struphy/post_processing/arrays.py | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index b53a44ced..fbff31da5 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -425,7 +425,7 @@ def draw(self): self.fit_results = [] for s in self.series: - line, = ax.plot(s.coord("t"), xp.asarray(s), label=s.label or None) + (line,) = ax.plot(s.coord("t"), xp.asarray(s), label=s.label or None) if not self.fit: continue @@ -857,7 +857,9 @@ class AnimationPlot(FrameSequence, StruphyPlot): tight = False - def __init__(self, data, *, grids=None, step=1, vmin=None, vmax=None, shared_clim=True, equal_aspect=False, **kwargs): + def __init__( + self, data, *, grids=None, step=1, vmin=None, vmax=None, shared_clim=True, equal_aspect=False, **kwargs + ): super().__init__(data, **kwargs) self.grids = grids self.step = step @@ -932,7 +934,9 @@ def draw(self): colouring = {"c": w[0], "cmap": "viridis"} if w is not None else {} scatter = ax.scatter(x[0], y[0], z[0], s=8, **colouring) - lines = [ax.plot(x[:1, j], y[:1, j], z[:1, j], lw=0.8, alpha=0.5)[0] for j in range(n)] if self.show_paths else [] + lines = ( + [ax.plot(x[:1, j], y[:1, j], z[:1, j], lw=0.8, alpha=0.5)[0] for j in range(n)] if self.show_paths else [] + ) ax.set_xlabel("X") ax.set_ylabel("Y") diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 414bb40d9..289489055 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -33,7 +33,6 @@ ) from struphy.post_processing.arrays import StruphyArray, wrap_orbits # noqa: E402 - # FuncAnimation warns when it is collected without having been rendered, which is # exactly what happens to the animations these tests build and discard. pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") @@ -191,10 +190,7 @@ def test_time_series_labels_axes_from_the_data(): def test_time_series_fits_every_series(): """Comparing runs means each curve gets its own rate, not just the first.""" t = np.linspace(0, 10, 60) - series = [ - StruphyArray(np.exp(rate * t), dims=("t",), coords={"t": t}, label=f"run {rate}") - for rate in (0.2, 0.4) - ] + series = [StruphyArray(np.exp(rate * t), dims=("t",), coords={"t": t}, label=f"run {rate}") for rate in (0.2, 0.4)] plot = TimeSeriesPlot(series, fit=True).plot() assert [f[0] for f in plot.fit_results] == pytest.approx([0.2, 0.4]) diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 2ec0de179..55fbc2c83 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -378,7 +378,9 @@ def wrap_field_data(data: dict, grids_log=None, *, label: str = "") -> StruphyAr scalar = not isinstance(first, (list, tuple)) or len(first) == 1 if scalar: - stacked = xp.stack([xp.asarray(data[t] if not isinstance(data[t], (list, tuple)) else data[t][0]) for t in times]) + stacked = xp.stack( + [xp.asarray(data[t] if not isinstance(data[t], (list, tuple)) else data[t][0]) for t in times] + ) dims = ("t", "e1", "e2", "e3") else: stacked = xp.stack([xp.stack([xp.asarray(c) for c in data[t]]) for t in times]) From 9b30e75dbe03126ad3499f686b6fbdda2b097898 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 09:20:06 +0200 Subject: [PATCH 012/193] Added post_process() helper and load=False to sim.pproc() --- doc/sections/tutorials.rst | 7 +++ src/struphy/__init__.py | 3 +- src/struphy/api/post_processing/__init__.py | 53 ++++++++++++++++++++- src/struphy/simulation/sim.py | 32 +++++++++---- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/doc/sections/tutorials.rst b/doc/sections/tutorials.rst index 3484f1fcf..0f2db1cd2 100644 --- a/doc/sections/tutorials.rst +++ b/doc/sections/tutorials.rst @@ -9,6 +9,13 @@ They can be run with Jupyter notebooks or Jupyter lab. It is recommended to use the same Python environment as for Struphy, e.g., by installing the Jupyter packages in the same environment. +.. toctree:: + :maxdepth: 1 + :caption: Post-processing: + + ../_collections/tutorials/tutorial_post_processing + + .. toctree:: :maxdepth: 1 :caption: Pure FEEC models: diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index 40aea9cdd..c4173109f 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -167,7 +167,7 @@ def setup_logging(logging_level: int = logging.WARNING): WeightsParameters, ) from struphy.api.perturbations import perturbations -from struphy.api.post_processing import PlottingData, PostProcessor +from struphy.api.post_processing import PlottingData, PostProcessor, post_process from struphy.api.simulation import Simulation __all__ = [ @@ -193,5 +193,6 @@ def setup_logging(logging_level: int = logging.WARNING): "ButcherTableau", "PostProcessor", "PlottingData", + "post_process", "Simulation", ] diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index 2b392318e..572de88e4 100644 --- a/src/struphy/api/post_processing/__init__.py +++ b/src/struphy/api/post_processing/__init__.py @@ -1,3 +1,54 @@ from struphy.post_processing.post_processing_tools import PlottingData, PostProcessor -__all__ = ["PostProcessor", "PlottingData"] + +def post_process( + sim=None, + path_out: str = None, + *, + step: int = 1, + celldivide=1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = True, + force: bool = True, +) -> PlottingData: + """Post-process a completed run and return its loaded plotting data. + + This is the convenient serial entry point for the common process-then-load + workflow. Use :class:`PostProcessor` and :class:`PlottingData` separately when + processing under MPI or when the processed files should not be loaded into + memory immediately. + + Parameters are the same as :meth:`PostProcessor.process`; identify the run with + either ``sim`` or ``path_out``. + """ + if sim is not None: + return sim.pproc( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + force=force, + load=True, + ) + + processor = PostProcessor(sim=sim, path_out=path_out) + processor.process( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + force=force, + ) + + data = PlottingData(sim=sim, path_out=path_out) + data.load() + return data + + +__all__ = ["PostProcessor", "PlottingData", "post_process"] diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 1c1366d39..e1e8e2b0f 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -881,11 +881,17 @@ def pproc( classify: bool = False, create_vtk: bool = True, parallel_pproc: bool = False, - ): + force: bool = True, + load: bool = False, + ) -> PlottingData | None: """Run post-processing on saved simulation data. - Uses `PostProcessor` to generate plots, process guiding-center or - physical field views, and optionally produce VTK outputs. + Uses `PostProcessor` to process guiding-center or physical field views + and optionally produce VTK outputs. With ``load=True``, load the results + on rank 0 and return them as `PlottingData`; non-root ranks return ``None``. + + Loading is opt-in because processed field and particle arrays can be + large. ``force=False`` reuses an existing post-processing directory. """ # setup post processor and plotting @@ -899,6 +905,7 @@ def pproc( guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, + force=force, ) else: if self.rank == 0: @@ -911,17 +918,25 @@ def pproc( guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, + force=force, ) - def load_plotting_data(self): + if load and self.rank == 0: + return self.load_plotting_data() + return None + + def load_plotting_data(self) -> PlottingData | None: """Load plotting datasets produced by post-processing. - Creates a `PlottingData` instance on rank 0 (if needed), loads the - data and exposes convenient attributes such as `orbits`, `f`, and - grid information for downstream plotting or analysis. + On rank 0, creates a `PlottingData` instance if needed, loads the data, + exposes convenient attributes such as `orbits`, `f`, and grid information + for downstream plotting or analysis, and returns the instance. Non-root + ranks return ``None``. """ - if not hasattr(self, "_plotting_data") and self.rank == 0: + if self.rank != 0: + return None + if not hasattr(self, "_plotting_data"): self._plotting_data = PlottingData(sim=self) self.plotting_data.load() @@ -933,6 +948,7 @@ def load_plotting_data(self): self.grids_log = self.plotting_data.grids_log self.grids_phy = self.plotting_data.grids_phy self.t_grid = self.plotting_data.t_grid + return self.plotting_data # --------------------- # Code specific methods From 2ccbfced157633caa17e3fdcab4ac94b775bc98b Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 17:36:35 +0200 Subject: [PATCH 013/193] Added tutorial --- src/struphy/diagnostics/plotting.py | 54 +++ .../diagnostics/tests/test_plotting.py | 47 +++ .../post_processing/post_processing_tools.py | 18 + src/struphy/post_processing/tests/test_api.py | 68 ++++ .../tests/test_plotting_data.py | 8 + .../simulation/tests/test_pproc_api.py | 76 ++++ tutorials/tutorial_post_processing.ipynb | 361 ++++++++++++++++++ 7 files changed, 632 insertions(+) create mode 100644 src/struphy/post_processing/tests/test_api.py create mode 100644 src/struphy/simulation/tests/test_pproc_api.py create mode 100644 tutorials/tutorial_post_processing.ipynb diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index fbff31da5..f63d29dc4 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -962,6 +962,60 @@ def update(_): slider.on_changed(update) +class PlottingAccessor: + """High-level plotting methods bound to a loaded ``PlottingData`` instance. + + The accessor supplies run metadata automatically and resolves scalar and orbit + names from their containers. Each method draws immediately and returns the + underlying :class:`StruphyPlot`, preserving access to its axes, fit results, + sliders and saving methods. + """ + + def __init__(self, plotting_data): + self.data = plotting_data + + def _draw(self, plot_type, data, **kwargs): + kwargs.setdefault("params", self.data.params) + return plot_type(data, **kwargs).plot() + + def _scalar_series(self, data): + if isinstance(data, str): + return self.data.scalars[data] + if isinstance(data, StruphyArray): + return data + return [self.data.scalars[item] if isinstance(item, str) else item for item in data] + + def scalars(self, **kwargs) -> ScalarsPlot: + """Draw the overview of the run's scalar diagnostics.""" + return self._draw(ScalarsPlot, self.data.scalars, **kwargs) + + def time_series(self, data, **kwargs) -> TimeSeriesPlot: + """Draw one or more time series, accepting scalar names or arrays.""" + return self._draw(TimeSeriesPlot, self._scalar_series(data), **kwargs) + + def slice(self, data: StruphyArray, **kwargs) -> Slice2DPlot: + """Draw one two-dimensional array or selected snapshot.""" + return self._draw(Slice2DPlot, data, **kwargs) + + def panels(self, data: StruphyArray, **kwargs) -> PanelGridPlot: + """Draw evenly spaced snapshots of a time-dependent 2D array.""" + return self._draw(PanelGridPlot, data, **kwargs) + + def slider(self, data: StruphyArray, **kwargs) -> SliderPlot: + """Draw a 2D array with time and optional cut-plane sliders.""" + return self._draw(SliderPlot, data, **kwargs) + + def animation(self, data: StruphyArray, **kwargs) -> AnimationPlot: + """Draw the initial view of a time-dependent 2D animation.""" + return self._draw(AnimationPlot, data, **kwargs) + + def orbits(self, data, **kwargs) -> MarkerTrajectoryPlot: + """Draw marker trajectories, accepting either a species name or an array.""" + if isinstance(data, str): + data = self.data.orbits[data] + return self._draw(MarkerTrajectoryPlot, data, **kwargs) + + def save_all_scalars( scalars, directory, diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 289489055..56cb5b44a 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -5,6 +5,7 @@ """ import os +from types import SimpleNamespace import matplotlib import numpy as np @@ -19,6 +20,7 @@ AnimationPlot, MarkerTrajectoryPlot, PanelGridPlot, + PlottingAccessor, ScalarsPlot, Slice2DPlot, SliderPlot, @@ -269,6 +271,51 @@ def test_marker_trajectory_handles_a_species_without_weights(): assert MarkerTrajectoryPlot(without_weight, max_markers=4).plot().fig is not None +# ---------------------------------------------------------------- PlottingData accessor + + +@pytest.fixture +def plot_accessor(): + data = SimpleNamespace( + params=object(), + scalars=scalars(), + orbits={"ions": wrap_orbits(np.random.default_rng(2).random((5, 20, 8)), np.arange(5.0))}, + ) + return PlottingAccessor(data) + + +def test_plot_accessor_draws_scalars_with_run_context(plot_accessor): + plot = plot_accessor.scalars(error_panel="en_tot") + + assert isinstance(plot, ScalarsPlot) + assert plot.fig is not None + assert plot.params is plot_accessor.data.params + + +def test_plot_accessor_resolves_scalar_names(plot_accessor): + plot = plot_accessor.time_series(["en_e", "en_b"], logy=False) + + assert isinstance(plot, TimeSeriesPlot) + assert [series.label for series in plot.series] == ["en e", "en b"] + assert len(plot.ax.get_lines()) == 2 + + +def test_plot_accessor_draws_2d_views(plot_accessor): + data = phase_space() + + assert isinstance(plot_accessor.slice(data.isel(t=0)), Slice2DPlot) + assert isinstance(plot_accessor.panels(data, nrows=1, ncols=2), PanelGridPlot) + assert isinstance(plot_accessor.slider(data), SliderPlot) + assert isinstance(plot_accessor.animation(data), AnimationPlot) + + +def test_plot_accessor_resolves_orbit_species(plot_accessor): + plot = plot_accessor.orbits("ions", max_markers=4) + + assert isinstance(plot, MarkerTrajectoryPlot) + assert plot.data is plot_accessor.data.orbits["ions"] + + def test_animation_writes_one_frame_per_step(tmp_path): plot = AnimationPlot(phase_space(nt=10), step=3) assert list(plot.frames) == [0, 3, 6, 9] diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 58a7a043b..3a4d37890 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1577,6 +1577,24 @@ def n_sph(self) -> DensitySPH: """ return self._n_sph + @property + def plot(self): + """Plotting methods bound to this run's data and metadata. + + Examples + -------- + >>> pdata.plot.scalars() + >>> pdata.plot.time_series("electric_energy", fit=True) + >>> pdata.plot.slider(pdata.f.kinetic_ions["e1_v1_density"]["f_binned"]) + """ + if not hasattr(self, "_plot_accessor"): + # Keep matplotlib and the plotting implementation out of the data-loading + # import path until a plot is actually requested. + from struphy.diagnostics.plotting import PlottingAccessor + + self._plot_accessor = PlottingAccessor(self) + return self._plot_accessor + def load_scalars(self, *, physical_time: bool = True): """Read the ``scalar`` group of the raw HDF5 output into :attr:`scalars`. diff --git a/src/struphy/post_processing/tests/test_api.py b/src/struphy/post_processing/tests/test_api.py new file mode 100644 index 000000000..c608b7e01 --- /dev/null +++ b/src/struphy/post_processing/tests/test_api.py @@ -0,0 +1,68 @@ +"""Tests for the public post-processing convenience API.""" + +from struphy.api import post_processing + + +def test_post_process_processes_then_loads(monkeypatch): + calls = [] + expected = object() + + class FakeSimulation: + def pproc(self, **kwargs): + calls.append(kwargs) + return expected + + sim = FakeSimulation() + + data = post_processing.post_process( + sim=sim, + step=2, + celldivide=(2, 3, 4), + physical=True, + guiding_center=True, + classify=True, + create_vtk=False, + force=False, + ) + + assert data is expected + assert calls == [ + { + "step": 2, + "celldivide": (2, 3, 4), + "physical": True, + "guiding_center": True, + "classify": True, + "create_vtk": False, + "force": False, + "load": True, + } + ] + + +def test_post_process_accepts_an_output_path(monkeypatch): + seen = [] + + class FakePostProcessor: + def __init__(self, **kwargs): + seen.append(kwargs) + + def process(self, **kwargs): + pass + + class FakePlottingData: + def __init__(self, **kwargs): + seen.append(kwargs) + + def load(self): + pass + + monkeypatch.setattr(post_processing, "PostProcessor", FakePostProcessor) + monkeypatch.setattr(post_processing, "PlottingData", FakePlottingData) + + post_processing.post_process(path_out="sim_1") + + assert seen == [ + {"sim": None, "path_out": "sim_1"}, + {"sim": None, "path_out": "sim_1"}, + ] diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py index 020668193..4da11c107 100644 --- a/src/struphy/post_processing/tests/test_plotting_data.py +++ b/src/struphy/post_processing/tests/test_plotting_data.py @@ -146,6 +146,14 @@ def test_containers_are_discoverable(pdata): assert "e1_v1_density" in pdata.f["kinetic_ions"] +def test_plot_accessor_is_created_lazily_and_cached(pdata): + from struphy.diagnostics.plotting import PlottingAccessor + + assert not hasattr(pdata, "_plot_accessor") + assert isinstance(pdata.plot, PlottingAccessor) + assert pdata.plot is pdata.plot + + def test_field_becomes_one_labeled_array(pdata): """The chain the migrated plotting scripts use.""" field = pdata.spline_values["em_fields"]["e_field_log"].array diff --git a/src/struphy/simulation/tests/test_pproc_api.py b/src/struphy/simulation/tests/test_pproc_api.py new file mode 100644 index 000000000..ab77a083d --- /dev/null +++ b/src/struphy/simulation/tests/test_pproc_api.py @@ -0,0 +1,76 @@ +"""Unit tests for the Simulation post-processing convenience behavior.""" + +import importlib + +from struphy import Simulation + + +def test_pproc_can_load_and_return_plotting_data(monkeypatch): + calls = [] + expected = object() + + class FakePostProcessor: + def __init__(self, **kwargs): + calls.append(("construct", kwargs)) + + def process(self, **kwargs): + calls.append(("process", kwargs)) + + class FakeSimulation: + rank = 0 + + @property + def post_processor(self): + return self._post_processor + + def load_plotting_data(self): + calls.append(("load", {})) + return expected + + simulation_module = importlib.import_module("struphy.simulation.sim") + monkeypatch.setattr(simulation_module, "PostProcessor", FakePostProcessor) + + sim = FakeSimulation() + data = Simulation.pproc(sim, physical=True, create_vtk=False, force=False, load=True) + + assert data is expected + assert calls == [ + ("construct", {"sim": sim, "parallel_pproc": False}), + ( + "process", + { + "step": 1, + "celldivide": 1, + "physical": True, + "guiding_center": False, + "classify": False, + "create_vtk": False, + "force": False, + }, + ), + ("load", {}), + ] + + +def test_pproc_does_not_load_by_default(monkeypatch): + class FakePostProcessor: + def __init__(self, **kwargs): + pass + + def process(self, **kwargs): + pass + + class FakeSimulation: + rank = 0 + + @property + def post_processor(self): + return self._post_processor + + def load_plotting_data(self): + raise AssertionError("plotting data should not be loaded") + + simulation_module = importlib.import_module("struphy.simulation.sim") + monkeypatch.setattr(simulation_module, "PostProcessor", FakePostProcessor) + + assert Simulation.pproc(FakeSimulation()) is None diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb new file mode 100644 index 000000000..f08bd11ff --- /dev/null +++ b/tutorials/tutorial_post_processing.ipynb @@ -0,0 +1,361 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "postprocessing-intro", + "metadata": {}, + "source": [ + "# Post-processing and standard plots\n", + "\n", + "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, turn its raw output into labeled arrays with `sim.pproc(load=True)`, and make the plots most commonly used to inspect a simulation.\n", + "\n", + "For a production run you can skip the simulation setup and construct both objects with `path_out=\"path/to/sim\"` instead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "from struphy import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " DerhamOptions,\n", + " EnvironmentOptions,\n", + " LoadingParameters,\n", + " SavingParameters,\n", + " Simulation,\n", + " SortingParameters,\n", + " Time,\n", + " WeightsParameters,\n", + " domains,\n", + " grids,\n", + " maxwellians,\n", + " perturbations,\n", + ")\n", + "from struphy.models import VlasovAmpereOneSpecies" + ] + }, + { + "cell_type": "markdown", + "id": "demo-heading", + "metadata": {}, + "source": [ + "## Create a compact demonstration run\n", + "\n", + "Post-processing operates on a completed run. The small setup below saves an electric field, a few marker trajectories, scalar diagnostics, and a binned $(\\eta_1,v_1)$ distribution. These are the main output types handled by the plotting interface." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "configure-run", + "metadata": {}, + "outputs": [], + "source": [ + "model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", + "model.em_fields.e_field.save_data = True\n", + "model.em_fields.phi.save_data = True\n", + "model.kinetic_ions.var.save_data = True\n", + "\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + "model.propagators.coupling_va.options = model.propagators.coupling_va.Options()\n", + "model.initial_poisson.options = model.initial_poisson.Options(stab_mat=\"M0\")\n", + "\n", + "binplot = BinningPlot(\n", + " slice=\"e1_v1\",\n", + " n_bins=(32, 32),\n", + " ranges=((0.0, 1.0), (-5.0, 5.0)),\n", + ")\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=LoadingParameters(ppc=32, seed=1234),\n", + " weights_params=WeightsParameters(control_variate=True),\n", + " boundary_params=BoundaryParameters(),\n", + " sorting_params=SortingParameters(boxes_per_dim=(4, 1, 1), do_sort=True),\n", + " saving_params=SavingParameters(n_markers=12, binning_plots=(binplot,)),\n", + ")\n", + "\n", + "background = maxwellians.Maxwellian3D(n=(1.0, None))\n", + "model.kinetic_ions.var.add_background(background)\n", + "density_mode = perturbations.ModesCos(ls=(1,), amps=(1e-3,))\n", + "model.kinetic_ions.var.add_initial_condition(maxwellians.Maxwellian3D(n=(1.0, density_mode)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "run-demo", + "metadata": {}, + "outputs": [], + "source": [ + "demo_tmp = tempfile.TemporaryDirectory(prefix=\"struphy_postprocessing_\")\n", + "demo_root = demo_tmp.name\n", + "\n", + "env = EnvironmentOptions(\n", + " out_folders=demo_root,\n", + " sim_folder=\"vlasov_ampere_demo\",\n", + " save_restart=False,\n", + ")\n", + "sim = Simulation(\n", + " model=model,\n", + " env=env,\n", + " time_opts=Time(dt=0.1, Tend=0.4),\n", + " domain=domains.Cuboid(r1=2 * 3.141592653589793),\n", + " grid=grids.TensorProductGrid(num_elements=(8, 1, 1)),\n", + " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", + ")\n", + "sim.run()\n", + "print(f\"Raw output: {sim.env.path_out}\")" + ] + }, + { + "cell_type": "markdown", + "id": "process-heading", + "metadata": {}, + "source": [ + "## Process and load the output\n", + "\n", + "`sim.pproc(load=True)` evaluates saved FEEC fields, organizes particle diagnostics, and returns the loaded plotting data. `physical=True` additionally creates physical field components; `create_vtk=False` keeps this notebook quick. With `force=False`, an existing post-processing directory is reused.\n", + "\n", + "The result exposes the output as `StruphyArray` objects. Each array carries named dimensions, coordinates, units, and a display label. The lower-level `PostProcessor` and `PlottingData` classes remain available when processing and loading should be performed separately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "process-load", + "metadata": {}, + "outputs": [], + "source": [ + "pdata = sim.pproc(\n", + " physical=True,\n", + " create_vtk=False,\n", + " force=False,\n", + " load=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "discover-text", + "metadata": {}, + "source": [ + "The containers are discoverable, so a plotting script does not need to guess what a run saved. Dictionary-style and attribute-style access are both supported." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "inspect-data", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"scalars:\", pdata.scalars.keys())\n", + "print(\"field species:\", pdata.spline_values.keys())\n", + "print(\"kinetic species:\", pdata.f.keys())\n", + "print(\"particle orbits:\", pdata.orbits.keys())\n", + "\n", + "phase_space = pdata.f.kinetic_ions[\"e1_v1_density\"][\"f_binned\"]\n", + "print(phase_space)\n", + "print(\"dimensions:\", phase_space.dims)\n", + "print(\"time coordinate:\", phase_space.coord(\"t\"))" + ] + }, + { + "cell_type": "markdown", + "id": "scalar-heading", + "metadata": {}, + "source": [ + "## Scalar overview and time series\n", + "\n", + "`pdata.plot.scalars()` gives a quick overview of every recorded scalar. If `total_energy` is available, it is also used for the conservation-error panel. Individual time series can be shown on linear or logarithmic axes, and an exponential fit can be restricted to a chosen time interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "scalar-overview", + "metadata": {}, + "outputs": [], + "source": [ + "pdata.plot.scalars(\n", + " error_panel=\"total_energy\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "energy-series", + "metadata": {}, + "outputs": [], + "source": [ + "electric_energy = pdata.scalars[\"electric_energy\"]\n", + "energy_plot = pdata.plot.time_series(\n", + " \"electric_energy\",\n", + " logy=True,\n", + " fit=True,\n", + " fit_window=(0.0, 0.4 * pdata.units.t),\n", + " fit_of_sqrt=True, # report the field-amplitude rate of this quadratic energy\n", + " title=\"Electric-field energy\",\n", + ")\n", + "print(\"fit result (gamma, intercept, index window):\", energy_plot.fit_results[0])" + ] + }, + { + "cell_type": "markdown", + "id": "data2d-heading", + "metadata": {}, + "source": [ + "## Two-dimensional data\n", + "\n", + "Named selection keeps plots readable. Use `isel` for an integer index and `at` for the point nearest a coordinate value. A single phase-space snapshot can then be passed directly to `pdata.plot.slice()`; coordinates and labels come from the array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "final-slice", + "metadata": {}, + "outputs": [], + "source": [ + "final_distribution = phase_space.isel(t=-1)\n", + "pdata.plot.slice(\n", + " final_distribution,\n", + " equal_aspect=False,\n", + " title=\"Final phase-space distribution\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "panel-text", + "metadata": {}, + "source": [ + "For a compact view of the evolution, `pdata.plot.panels()` chooses evenly spaced snapshots. `shared_clim=True` makes their colors directly comparable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "panels", + "metadata": {}, + "outputs": [], + "source": [ + "pdata.plot.panels(\n", + " phase_space,\n", + " nrows=1,\n", + " ncols=5,\n", + " shared_clim=True,\n", + " equal_aspect=False,\n", + " title=\"Phase-space evolution\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "interactive-heading", + "metadata": {}, + "source": [ + "## Interactive plots\n", + "\n", + "`pdata.plot.slider()` adds a time slider to any `(t, a, b)` array. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the returned object alive so its widget callbacks remain connected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "slider", + "metadata": {}, + "outputs": [], + "source": [ + "phase_slider = pdata.plot.slider(\n", + " phase_space,\n", + " equal_aspect=False,\n", + " title=\"Phase-space distribution\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "orbits-text", + "metadata": {}, + "source": [ + "Saved marker orbits use a three-dimensional trajectory plot with a time slider. `max_markers` limits rendering cost for large production runs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "orbits-plot", + "metadata": {}, + "outputs": [], + "source": [ + "orbit_plot = pdata.plot.orbits(\n", + " \"kinetic_ions\",\n", + " max_markers=12,\n", + " show_paths=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "save-heading", + "metadata": {}, + "source": [ + "## Save standard output\n", + "\n", + "The same plot objects support `.save(path)`. For a complete scalar report, `save_scalar_plots()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/scalars/`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "save-output", + "metadata": {}, + "outputs": [], + "source": [ + "written = pdata.save_scalar_plots()\n", + "print(\"Wrote:\")\n", + "for path in written:\n", + " print(\" \", os.path.relpath(path, pdata.path_out))" + ] + }, + { + "cell_type": "markdown", + "id": "reuse-heading", + "metadata": {}, + "source": [ + "## Apply the workflow to another run\n", + "\n", + "For an already completed simulation, the complete loading pattern is:\n", + "\n", + "```python\n", + "path_out = \"/path/to/sim_1\"\n", + "from struphy import post_process\n", + "pdata = post_process(path_out=path_out, physical=True, force=False)\n", + "```\n", + "\n", + "Use `pdata.scalars`, `pdata.spline_values`, `pdata.f`, `pdata.orbits`, and `pdata.n_sph` to discover and plot the data available in that run." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.12.3.final.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 92ceb23d4404392bb4a34f2990fd5aa2a383a5bb Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 17:44:59 +0200 Subject: [PATCH 014/193] Simplify postprocessing --- .../cyclone/pproc_cyclone.py | 51 +- .../itg_cylindre/pproc_drift_kinetic.py | 51 +- .../diocotron_instability/pproc_diocotron.py | 63 +- .../two_stream/pproc_two_stream.py | 35 +- pyproject.toml | 1 + src/struphy/__init__.py | 4 +- src/struphy/api/post_processing/__init__.py | 62 +- src/struphy/diagnostics/plotting.py | 1463 +++++------------ .../diagnostics/tests/test_plotting.py | 560 ++----- src/struphy/post_processing/arrays.py | 525 ++---- .../post_processing/post_processing_tools.py | 123 +- src/struphy/post_processing/run_output.py | 266 +++ .../post_processing/tests/test_arrays.py | 359 +--- .../tests/test_plotting_data.py | 203 --- 14 files changed, 1167 insertions(+), 2599 deletions(-) create mode 100644 src/struphy/post_processing/run_output.py delete mode 100644 src/struphy/post_processing/tests/test_plotting_data.py diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 4c8d290ba..0fddce5df 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,13 +1,13 @@ import os import sys -from struphy import PlottingData, PostProcessor +from struphy import PostProcessor, RunOutput from struphy.diagnostics.plotting import ( - MarkerTrajectoryPlot, - SliderPlot, - TimeSeriesPlot, - field_slice_grids, - physical_grids, + GrowthFit, + InteractiveSliceViewer, + View, + plot_marker_trajectories, + plot_timeseries, plot_equilibrium_profile, ) @@ -33,16 +33,13 @@ def main(path_out): PostProcessor(path_out=path_out).process(physical=True, force=False) - pdata = PlottingData(path_out=path_out) - pdata.load() + run = RunOutput.open(path_out) # growth rate of the electrostatic potential - TimeSeriesPlot( - pdata.scalars[FIT_QUANTITY], - fit=True, - fit_window=FIT_WINDOW, - fit_of_sqrt=True, - params=pdata.params, + plot_timeseries( + run.scalars[FIT_QUANTITY], + fit=GrowthFit(FIT_WINDOW, amplitude_from_quadratic=True), + run_label=run.label, title=f"Evolution of {FIT_QUANTITY}", ).show() @@ -50,28 +47,16 @@ def main(path_out): plot_equilibrium_profile(path_out) for bin_name, quantity, plane in DENSITY_PLOTS: - data = pdata.f.kinetic_ions[bin_name][quantity] - SliderPlot( - data, - grids=physical_grids(data.isel(t=0), pdata.domain, axes=plane), - params=pdata.params, - title=f"{quantity} ({plane})", - ).show() + data = run.distributions[f"kinetic_ions/{bin_name}/{quantity}"] + InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), + run_label=run.label).show() for species, field, component, plane in FIELD_PLOTS: - data = pdata.spline_values[species][field].array.isel(comp=component) - SliderPlot( - data, - # the cut plane moves with the slider, so the grids follow it - grids=lambda index, plane=plane: field_slice_grids( - pdata.grids_phy, fixed_dim="e3", index=index, plane=plane - ), - slice_dim="e3", - params=pdata.params, - title=f"{species}.{field} ({plane})", - ).show() + data = run.fields[f"{species}/{field}"].isel(component=component) + InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), + run_label=run.label).show() - MarkerTrajectoryPlot(pdata.orbits.kinetic_ions, max_markers=1000).show() + plot_marker_trajectories(run.orbits["kinetic_ions"], max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 1af41fef9..585a98e09 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,13 +1,13 @@ import os import sys -from struphy import PlottingData, PostProcessor +from struphy import PostProcessor, RunOutput from struphy.diagnostics.plotting import ( - MarkerTrajectoryPlot, - SliderPlot, - TimeSeriesPlot, - field_slice_grids, - physical_grids, + GrowthFit, + InteractiveSliceViewer, + View, + plot_marker_trajectories, + plot_timeseries, plot_equilibrium_profile, ) @@ -33,16 +33,13 @@ def main(path_out): PostProcessor(path_out=path_out).process(physical=True, force=False) - pdata = PlottingData(path_out=path_out) - pdata.load() + run = RunOutput.open(path_out) # growth rate of the electrostatic potential - TimeSeriesPlot( - pdata.scalars[FIT_QUANTITY], - fit=True, - fit_window=FIT_WINDOW, - fit_of_sqrt=True, - params=pdata.params, + plot_timeseries( + run.scalars[FIT_QUANTITY], + fit=GrowthFit(FIT_WINDOW, amplitude_from_quadratic=True), + run_label=run.label, title=f"Evolution of {FIT_QUANTITY}", ).show() @@ -50,28 +47,16 @@ def main(path_out): plot_equilibrium_profile(path_out) for bin_name, quantity, plane in DENSITY_PLOTS: - data = pdata.f.kinetic_ions[bin_name][quantity] - SliderPlot( - data, - grids=physical_grids(data.isel(t=0), pdata.domain, axes=plane), - params=pdata.params, - title=f"{quantity} ({plane})", - ).show() + data = run.distributions[f"kinetic_ions/{bin_name}/{quantity}"] + InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), + run_label=run.label).show() for species, field, component, plane in FIELD_PLOTS: - data = pdata.spline_values[species][field].array.isel(comp=component) - SliderPlot( - data, - # the cut plane moves with the slider, so the grids follow it - grids=lambda index, plane=plane: field_slice_grids( - pdata.grids_phy, fixed_dim="e3", index=index, plane=plane - ), - slice_dim="e3", - params=pdata.params, - title=f"{species}.{field} ({plane})", - ).show() + data = run.fields[f"{species}/{field}"].isel(component=component) + InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), + run_label=run.label).show() - MarkerTrajectoryPlot(pdata.orbits.kinetic_ions, max_markers=1000).show() + plot_marker_trajectories(run.orbits["kinetic_ions"], max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index fe7a34e28..8128a4e6e 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -7,13 +7,13 @@ import os import sys -from struphy import PlottingData, PostProcessor +from struphy import PostProcessor, RunOutput from struphy.diagnostics.plotting import ( - MarkerTrajectoryPlot, - SliderPlot, - TimeSeriesPlot, - field_slice_grids, - physical_grids, + GrowthFit, + InteractiveSliceViewer, + View, + plot_marker_trajectories, + plot_timeseries, plot_equilibrium_profile, ) @@ -36,9 +36,7 @@ def load(path_out): PostProcessor(path_out=path_out).process(physical=True, force=False) - pdata = PlottingData(path_out=path_out) - pdata.load() - return pdata + return RunOutput.open(path_out) def main(paths): @@ -46,53 +44,40 @@ def main(paths): # growth rate of the electrostatic energy, one curve per run series = [] - for name, pdata in runs.items(): - energy = pdata.scalars[FIT_QUANTITY] - energy.label = name if len(runs) > 1 else FIT_QUANTITY + for name, run in runs.items(): + energy = run.scalars[FIT_QUANTITY].copy() + energy.attrs["label"] = name if len(runs) > 1 else FIT_QUANTITY series.append(energy) - plot = TimeSeriesPlot( + plot = plot_timeseries( series, - fit=True, - fit_window=FIT_WINDOW, - params=next(iter(runs.values())).params, + fit=GrowthFit(FIT_WINDOW), + run_label=next(iter(runs.values())).label, title=f"Evolution of {FIT_QUANTITY}", ).show() - for name, (gamma, _, _) in zip(runs, plot.fit_results): - print(f"{name}: growth rate = {gamma}") + for name, result in zip(runs, plot.fit_results): + print(f"{name}: growth rate = {None if result is None else result.rate}") if len(runs) > 1: return - path_out, pdata = paths[0], next(iter(runs.values())) + path_out, run = paths[0], next(iter(runs.values())) if SHOW_EQUIL_PROFILE: plot_equilibrium_profile(path_out) for bin_name, quantity, plane in DENSITY_PLOTS: - data = pdata.f.kinetic_ions[bin_name][quantity] - SliderPlot( - data, - grids=physical_grids(data.isel(t=0), pdata.domain, axes=plane), - params=pdata.params, - title=f"{quantity} ({plane})", - ).show() + data = run.distributions[f"kinetic_ions/{bin_name}/{quantity}"] + InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), + run_label=run.label).show() for species, field, component, plane in FIELD_PLOTS: - data = pdata.spline_values[species][field].array.isel(comp=component) - SliderPlot( - data, - # the cut plane moves with the slider, so the grids follow it - grids=lambda index, plane=plane: field_slice_grids( - pdata.grids_phy, fixed_dim="e3", index=index, plane=plane - ), - slice_dim="e3", - params=pdata.params, - title=f"{species}.{field} ({plane})", - ).show() - - MarkerTrajectoryPlot(pdata.orbits.kinetic_ions, max_markers=1000).show() + data = run.fields[f"{species}/{field}"].isel(component=component) + InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), + run_label=run.label).show() + + plot_marker_trajectories(run.orbits["kinetic_ions"], max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index b8951c815..988e17b17 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,43 +1,36 @@ -import cunumpy as xp import params_two_stream as params -from struphy import PlottingData, PostProcessor -from struphy.diagnostics.plotting import PanelGridPlot, SliderPlot, TimeSeriesPlot -from struphy.post_processing.arrays import StruphyArray +from struphy import PostProcessor, RunOutput +from struphy.diagnostics.plotting import InteractiveSliceViewer, View, plot_panels, plot_timeseries def main(): PostProcessor(sim=params.sim).process(force=False) - pdata = PlottingData(sim=params.sim) - pdata.load() + run = RunOutput.open(sim=params.sim) # every scalar at every time step: post_processing/scalars/{scalars.csv,*.png} - pdata.save_scalar_plots() + run.save_scalar_plots() # electric field growth against the analytical rate (0.2845 in units of m/c) - energy = pdata.scalars["electric_energy"] - t = energy.coord("t") - analytical = StruphyArray( - 10 ** (0.2845 / pdata.units.t * t - 5.3), - dims=("t",), - coords={"t": t}, - label="analytical", - ).with_coord_units(t="s") - - TimeSeriesPlot( + energy = run.scalars["electric_energy"] + analytical = energy.copy(data=10 ** (0.2845 / run.units.t * energy.t - 5.3)) + analytical.attrs["label"] = "analytical" + + plot_timeseries( [energy, analytical], - params=pdata.params, + run_label=run.label, title="Electric energy", ).show() # phase space evolution - f = pdata.f.kinetic_ions["e1_v1_density"]["f_binned"] + f = run.distributions["kinetic_ions/e1_v1_density/f_binned"] + view = View(x="e1", y="v1") - PanelGridPlot(f, nrows=3, ncols=4, shared_clim=True, params=pdata.params).show() + plot_panels(f, view=view, nrows=3, ncols=4, shared_clim=True, run_label=run.label).show() # interactive alternative to dumping a frame sequence - SliderPlot(f, equal_aspect=False, params=pdata.params).show() + InteractiveSliceViewer(f, view=view, run_label=run.label).show() if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 3b0b039ff..c230ad71e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "feectools >= 0.1.10, <=0.1.10", "scipy<=1.18.0", "h5py<=3.16.0", + "xarray<=2026.2.0", "matplotlib<=3.11.0", "pyyaml<=6.0.3", "vtk<=9.7.0", diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index c4173109f..def4f68b4 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -167,7 +167,8 @@ def setup_logging(logging_level: int = logging.WARNING): WeightsParameters, ) from struphy.api.perturbations import perturbations -from struphy.api.post_processing import PlottingData, PostProcessor, post_process +from struphy.api.post_processing import PlottingData, PostProcessor, RunOutput +from struphy.api.post_processing import post_process from struphy.api.simulation import Simulation __all__ = [ @@ -192,6 +193,7 @@ def setup_logging(logging_level: int = logging.WARNING): "FieldsBackground", "ButcherTableau", "PostProcessor", + "RunOutput", "PlottingData", "post_process", "Simulation", diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index 572de88e4..1ec68460d 100644 --- a/src/struphy/api/post_processing/__init__.py +++ b/src/struphy/api/post_processing/__init__.py @@ -1,4 +1,7 @@ -from struphy.post_processing.post_processing_tools import PlottingData, PostProcessor +from struphy.post_processing.post_processing_tools import PostProcessor +from struphy.post_processing.run_output import RunOutput + +PlottingData = RunOutput def post_process( @@ -11,44 +14,21 @@ def post_process( guiding_center: bool = False, classify: bool = False, create_vtk: bool = True, - force: bool = True, -) -> PlottingData: - """Post-process a completed run and return its loaded plotting data. - - This is the convenient serial entry point for the common process-then-load - workflow. Use :class:`PostProcessor` and :class:`PlottingData` separately when - processing under MPI or when the processed files should not be loaded into - memory immediately. - - Parameters are the same as :meth:`PostProcessor.process`; identify the run with - either ``sim`` or ``path_out``. - """ + force: bool = False, +) -> RunOutput: + """Process a completed run and return its lazy :class:`RunOutput`.""" if sim is not None: - return sim.pproc( - step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, - classify=classify, - create_vtk=create_vtk, - force=force, - load=True, - ) - - processor = PostProcessor(sim=sim, path_out=path_out) - processor.process( - step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, - classify=classify, - create_vtk=create_vtk, - force=force, - ) - - data = PlottingData(sim=sim, path_out=path_out) - data.load() - return data - - -__all__ = ["PostProcessor", "PlottingData", "post_process"] + sim.pproc(step=step, celldivide=celldivide, physical=physical, + guiding_center=guiding_center, classify=classify, + create_vtk=create_vtk, force=force, load=True) + return RunOutput(sim=sim) + if path_out is None: + raise ValueError("path_out or sim is required") + processor = PostProcessor(path_out=path_out) + processor.process(step=step, celldivide=celldivide, physical=physical, + guiding_center=guiding_center, classify=classify, + create_vtk=create_vtk, force=force) + return RunOutput(path_out=path_out) + + +__all__ = ["PostProcessor", "RunOutput", "PlottingData", "post_process"] diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index f63d29dc4..1aafb02dc 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -1,1110 +1,483 @@ -"""Standardized plots for post-processed Struphy output. +"""Small, composable plotting functions for labeled Struphy output.""" -Every plotter accepts a :class:`~struphy.post_processing.arrays.StruphyArray` and -derives its axis labels, coordinates and units from it, so a correct labeled figure -needs no further arguments. -""" +from __future__ import annotations import logging -import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal -import cunumpy as xp -from matplotlib import pyplot as plt +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr from matplotlib.widgets import Slider from struphy.post_processing.arrays import ( SCALARS_EXCLUDE, - StruphyArray, + axis_label, orbit_columns, save_scalars, scalar_names, + validate_array, + value_label, ) logger = logging.getLogger("struphy") -#: rcParams applied by every plotter, so figures from different scripts match. STRUPHY_STYLE = { - "figure.figsize": (8.0, 5.0), - "figure.dpi": 110, - "axes.grid": True, - "grid.alpha": 0.3, - "axes.titlesize": "medium", - "legend.frameon": False, + "figure.figsize": (8.0, 5.0), "figure.dpi": 110, "axes.grid": True, + "grid.alpha": 0.3, "axes.titlesize": "medium", "legend.frameon": False, "image.cmap": "viridis", } - -def growth_rate(y: StruphyArray, *, t0: float = None, t1: float = None, of_sqrt: bool = False): - """Fit an exponential ``exp(gamma*t + b)`` over a time window. - - Parameters - ---------- - y : StruphyArray - Signal with a ``t`` dimension. Non-positive and non-finite samples are excluded. - t0, t1 : float, optional - Window bounds. Default to the full range. - of_sqrt : bool - Fit the growth rate of ``sqrt(y)`` rather than of ``y``. Use this for a - quadratic quantity such as an energy whose amplitude growth rate is wanted. - - Returns - ------- - gamma, b, window : float, float, slice - ``None`` in place of all three if fewer than two usable samples remain. - """ - t = xp.asarray(y.coord("t")) - vals = xp.asarray(y) - - lo = float(t[0]) if t0 is None else float(t0) - hi = float(t[-1]) if t1 is None else float(t1) - lo, hi = sorted((lo, hi)) - - mask = (t >= lo) & (t <= hi) & xp.isfinite(vals) & (vals > 0.0) - if xp.count_nonzero(mask) < 2: - mask = xp.isfinite(vals) & (vals > 0.0) - if xp.count_nonzero(mask) < 2: - return None, None, None - - idx = xp.nonzero(mask)[0] - window = slice(int(idx[0]), int(idx[-1]) + 1) - - signal = xp.log(xp.sqrt(vals[window])) if of_sqrt else xp.log(vals[window]) - gamma, b = xp.polyfit(t[window], signal, 1) - return float(gamma), float(b), window - - -def drift(y: StruphyArray, *, ref: float = None) -> StruphyArray: - """Deviation ``y(t) - y_ref`` of a conserved quantity, with ``y_ref = y(0)`` by default.""" - values = xp.asarray(y) - reference = float(values[0]) if ref is None else float(ref) - return StruphyArray( - values - reference, - dims=y.dims, - coords=y.coords, - unit=y.unit, - label=f"{y.label} drift" if y.label else "drift", - ).with_coord_units(**y.coord_units) - - -def relative_error(y: StruphyArray, *, ref: float = None, skip_first: bool = True) -> StruphyArray: - """Relative deviation ``|y(t) - y_ref| / |y_ref|`` of a conserved quantity. - - The standard energy-conservation diagnostic: a run that conserves energy exactly - stays at zero, so on a log axis this shows the scheme's error over time. - - Parameters - ---------- - y : StruphyArray - Signal with a ``t`` dimension. - ref : float, optional - Reference value. Defaults to the first sample. - skip_first : bool - Drop ``t = 0``, where the error is identically zero and so cannot be - drawn on a log axis. - """ - values = xp.asarray(y) - reference = float(values[0]) if ref is None else float(ref) - if reference == 0.0: - raise ValueError("cannot take a relative error against a reference of zero") - - out = StruphyArray( - xp.abs(values - reference) / abs(reference), - dims=y.dims, - coords=y.coords, - label=rf"$|\Delta$ {y.label}$| / |${y.label}$(0)|$" if y.label else "relative error", - ).with_coord_units(**y.coord_units) - return out.isel(t=slice(1, None)) if skip_first else out - - -def match_to_grid(values, xgrid): - """Return ``values`` oriented to match ``xgrid``, transposing if that is what fits.""" - values = xp.asarray(values) - if values.shape == xgrid.shape: - return values - if values.T.shape == xgrid.shape: - return values.T - raise ValueError(f"cannot match data shape {values.shape} to grid shape {xgrid.shape}") - - -def physical_grids(data: StruphyArray, domain, *, axes: str = "XY", fixed_eta=(0.5, 0.0, 0.0)): - """Map the two logical dimensions of ``data`` through ``domain`` to physical coordinates. - - Parameters - ---------- - data : StruphyArray - Must have exactly two ``e`` dimensions. - domain : Domain - Struphy domain, called as ``domain(eta1, eta2, eta3, squeeze_out=True)``. - axes : str - Which physical plane to return: ``"XY"``, ``"RZ"``, ``"XZ"`` or ``"YZ"``. - fixed_eta : tuple - Logical position along the dimension that is not binned. - - Returns - ------- - xgrid, ygrid, xlabel, ylabel - """ - logical = [d for d in data.dims if d.startswith("e") and d[1:].isdigit()] - if len(logical) != 2: - raise ValueError(f"expected two logical dims, got {logical} from dims {data.dims}") - - nums = [int(d[1]) for d in logical] - etas = [data.coord(logical[nums.index(ax)]) if ax in nums else fixed_eta[ax - 1] for ax in (1, 2, 3)] - - if axes not in PLANES: - raise ValueError(f"unknown axes {axes!r}, expected one of {sorted(PLANES)}") - - x, y, z = domain(*etas, squeeze_out=True) - fx, fy, xlabel, ylabel = PLANES[axes] - return fx(x, y, z), fy(x, y, z), xlabel, ylabel - - -def logical_grids(data: StruphyArray): - """Meshgrid of the two plotted dimensions of ``data``, for plotting without a domain map. - - The plotted dimensions are whatever remains after ``t``, so this covers phase-space - slices such as ``(e1, v1)`` as well as purely spatial ones. - """ - plotted = [d for d in data.dims if d != "t"] - if len(plotted) != 2: - raise ValueError(f"expected two non-time dims, got {plotted} from dims {data.dims}") - g0, g1 = (data.coord(d) for d in plotted) - xgrid, ygrid = xp.meshgrid(g0, g1, indexing="ij") - return xgrid, ygrid, data.axis_label(plotted[0]), data.axis_label(plotted[1]) - - -#: Physical coordinate planes, as a function of the (X, Y, Z) meshgrids. PLANES = { - "XY": (lambda x, y, z: x, lambda x, y, z: y, "X", "Y"), - "XZ": (lambda x, y, z: x, lambda x, y, z: z, "X", "Z"), - "YZ": (lambda x, y, z: y, lambda x, y, z: z, "Y", "Z"), - "RZ": (lambda x, y, z: xp.sqrt(x**2 + y**2), lambda x, y, z: z, "R", "Z"), + "XY": ("X", "Y", "X", "Y"), + "XZ": ("X", "Z", "X", "Z"), + "YZ": ("Y", "Z", "Y", "Z"), + "RZ": ("R", "Z", "R", "Z"), } -def field_slice_grids(grids_phy, *, fixed_dim: str = "e3", index: int = 0, plane: str = "XY"): - """Physical grids for a 2D cut through the 3D evaluation grid. +@dataclass(frozen=True) +class GrowthFit: + """Configuration for an exponential growth-rate fit.""" - Companion to ``StruphyArray.isel(**{fixed_dim: index})``: pass the same ``fixed_dim`` - and ``index`` here to get grids matching the sliced field. + window: tuple[float | None, float | None] = (None, None) + amplitude_from_quadratic: bool = False - Parameters - ---------- - grids_phy : list - The three 3D physical coordinate arrays from :attr:`PlottingData.grids_phy`. - fixed_dim : str - Logical dimension held constant, ``"e1"``, ``"e2"`` or ``"e3"``. - index : int - Index along ``fixed_dim``. - plane : str - Which physical plane to return, one of :data:`PLANES`. - Returns - ------- - xgrid, ygrid, xlabel, ylabel - """ - if plane not in PLANES: - raise ValueError(f"unknown plane {plane!r}, expected one of {sorted(PLANES)}") - - axis = {"e1": 0, "e2": 1, "e3": 2} - if fixed_dim not in axis: - raise ValueError(f"fixed_dim must be one of {sorted(axis)}, got {fixed_dim!r}") - - cut = [slice(None)] * 3 - cut[axis[fixed_dim]] = index - x, y, z = (xp.asarray(g)[tuple(cut)] for g in grids_phy) - - fx, fy, xlabel, ylabel = PLANES[plane] - return fx(x, y, z), fy(x, y, z), xlabel, ylabel - - -class StruphyPlot: - """Base for the plotters: owns style, figure creation, titling and output. - - Parameters - ---------- - data : StruphyArray - The quantity to draw. - ax : matplotlib Axes, optional - Draw into an existing axes instead of creating a figure. - title : str, optional - Defaults to the quantity's label. - params : ParamsIn, optional - When given, run settings are appended to the figure as a suptitle. - """ - - #: Slider-bearing subclasses position their axes manually. - tight = True - - def __init__(self, data: StruphyArray, *, ax=None, title: str = None, params=None, **kwargs): - self.data = data - self.title = title if title is not None else (data.label or "") - self.params = params - self.options = kwargs - self._ax = ax - self.fig = None - self.ax = None - - def _make_axes(self, **subplot_kw): - if self._ax is not None: - self.ax = self._ax - self.fig = self._ax.get_figure() - else: - self.fig, self.ax = plt.subplots(**subplot_kw) - return self.fig, self.ax - - def _run_label(self) -> str: - """One-line summary of the run settings, from the output folder's parameters.""" - if self.params is None: - return "" - bits = [] - for obj, attr, name in ( - ("time_opts", "dt", "dt"), - ("time_opts", "split_algo", "algo"), - ("grid", "num_elements", "Nel"), - ("derham_opts", "degree", "p"), - ): - holder = getattr(self.params, obj, None) - value = getattr(holder, attr, None) if holder is not None else None - if value is not None: - bits.append(f"{name}={value}") - return ", ".join(bits) +@dataclass(frozen=True) +class FitResult: + rate: float + intercept: float + time: np.ndarray + fitted: np.ndarray - def draw(self): - raise NotImplementedError - - def _finish(self): - run = self._run_label() - if run and self.fig is not None and self._ax is None: - self.fig.suptitle(run, fontsize="small") - if self.tight and self.fig is not None and self._ax is None: - self.fig.tight_layout() - return self - def plot(self): - """Draw into the axes and return self.""" - with plt.rc_context(STRUPHY_STYLE): - self.draw() - self._finish() - return self +@dataclass(frozen=True) +class View: + """A reusable selection and rendering recipe for an N-dimensional product.""" + + x: str | None = None + y: str | None = None + sweep: str = "t" + select: dict[str, float] = field(default_factory=dict) + isel: dict[str, int] = field(default_factory=dict) + coordinates: Literal["logical", "physical"] = "logical" + plane: Literal["XY", "XZ", "YZ", "RZ"] = "XY" - def show(self): - self.plot() - plt.show() - return self - def save(self, path, *, close: bool = False, **kwargs): - """Draw and write the figure to ``path``. +@dataclass +class PlotResult: + """Already-rendered Matplotlib objects; saving never redraws them.""" - Parameters - ---------- - close : bool - Close the figure afterwards. Pass this when saving many figures in a - loop, so that they do not all stay open. - """ - self.plot() + fig: object + ax: object + artists: list = field(default_factory=list) + fit_results: list[FitResult | None] = field(default_factory=list) + + def save(self, path, *, close=False, **kwargs): kwargs.setdefault("bbox_inches", "tight") self.fig.savefig(path, **kwargs) if close: - self.close() - return self - - def close(self): - """Close the figure, unless it was supplied by the caller.""" - if self.fig is not None and self._ax is None: plt.close(self.fig) - self.fig = None - self.ax = None - return self - + return str(path) -class FrameSequence: - """Frame-by-frame output for the plotters that sweep a 2D quantity over time. - - A subclass says what a frame contains (:meth:`_frame_values`, :meth:`_frame_grids`, - :meth:`_frame_title`, :meth:`_clim`); this draws them into a single reused figure. - """ - - #: keep every ``step``-th time index - step = 1 - - @property - def frames(self): - """Time indices that will be drawn.""" - return range(0, self.data.shape[self.data.axis("t")], self.step) - - def _frame_grids(self): - return self.grids if self.grids is not None else logical_grids(self._frame_values(0)) - - def _frame_values(self, index): - return self.data.isel(t=index) - - def _frame_title(self, index): - return f"{self.title} at t = {float(self.data.coord('t')[index]):.4e}" - - def _clim(self): - return self.vmin, self.vmax + def show(self): + plt.show() + return self - def _setup(self): - xgrid, ygrid, xlabel, ylabel = self._frame_grids() - vmin, vmax = self._clim() - fig, ax = self._make_axes() - pcm = ax.pcolormesh( - xgrid, - ygrid, - match_to_grid(self._frame_values(0), xgrid), - shading="auto", - vmin=vmin, - vmax=vmax, - ) - fig.colorbar(pcm, ax=ax, label=self.data.value_label) - if self.equal_aspect: - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.grid(False) - return fig, ax, pcm, xgrid - - def _update(self, ax, pcm, xgrid, index): - pcm.set_array(match_to_grid(self._frame_values(index), xgrid).ravel()) - ax.set_title(self._frame_title(index)) +def _label(data): + return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" + + +def _finish(fig, *, run_label="", tight=True): + if run_label: + fig.suptitle(run_label, fontsize="small") + if tight: + fig.tight_layout() + + +def _select(data: xr.DataArray, view: View, *, keep_sweep=True): + validate_array(data) + overlap = set(view.select) & set(view.isel) + if overlap: + raise ValueError(f"dimensions cannot appear in both select and isel: {sorted(overlap)}") + selected = data + if view.select: + selected = selected.sel(view.select, method="nearest") + if view.isel: + selected = selected.isel(view.isel) + if not keep_sweep and view.sweep in selected.dims: + selected = selected.isel({view.sweep: 0}) + return selected + + +def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: + """Fit ``exp(rate*t + intercept)`` using only finite, positive samples.""" + validate_array(data, required_dims=("t",)) + if data.dims != ("t",): + raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") + fit = fit or GrowthFit() + time, values = np.asarray(data.t), np.asarray(data) + lo = time[0] if fit.window[0] is None else fit.window[0] + hi = time[-1] if fit.window[1] is None else fit.window[1] + lo, hi = sorted((lo, hi)) + valid = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) + if np.count_nonzero(valid) < 2: + return None + selected_time = time[valid] + signal = np.log(np.sqrt(values[valid])) if fit.amplitude_from_quadratic else np.log(values[valid]) + rate, intercept = np.polyfit(selected_time, signal, 1) + scale = 2.0 if fit.amplitude_from_quadratic else 1.0 + fitted = np.exp(scale * (rate * selected_time + intercept)) + return FitResult(float(rate), float(intercept), selected_time, fitted) + + +def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: + """Signed deviation from an explicit reference or the first time sample.""" + validate_array(data, required_dims=("t",)) + reference = data.isel(t=0) if ref is None else ref + out = data - reference + out.attrs = dict(data.attrs) + out.attrs["label"] = f"{_label(data)} drift".strip() + return out + + +def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: + """Absolute relative deviation from an explicit reference or first sample.""" + validate_array(data, required_dims=("t",)) + reference = data.isel(t=0) if ref is None else ref + if np.any(np.asarray(reference) == 0): + raise ValueError("cannot take a relative error against a reference of zero") + out = abs(data - reference) / abs(reference) + out.attrs = {"label": f"relative error of {_label(data)}".strip(), "units": ""} + return out.isel(t=slice(1, None)) if skip_first else out - def save_frames(self, directory, *, prefix="frame", dpi=110): - """Write one PNG per frame into ``directory``, creating it if needed. - Returns the list of paths written. - """ - os.makedirs(directory, exist_ok=True) - paths = [] +def logical_grids(data: xr.DataArray, *, x=None, y=None): + """Return 2-D logical coordinate grids and their labels.""" + if x is None or y is None: + if data.ndim != 2: + raise ValueError(f"x and y are required unless data is two-dimensional; got {data.dims}") + x, y = data.dims + if set(data.dims) != {x, y}: + raise ValueError(f"selected data must contain exactly {x!r} and {y!r}; got {data.dims}") + xgrid, ygrid = np.meshgrid(np.asarray(data.coords[x]), np.asarray(data.coords[y]), indexing="ij") + return xgrid, ygrid, axis_label(data, x), axis_label(data, y) - with plt.rc_context(STRUPHY_STYLE): - fig, ax, pcm, xgrid = self._setup() - for n, index in enumerate(self.frames): - self._update(ax, pcm, xgrid, index) - path = os.path.join(directory, f"{prefix}_{n:04d}.png") - fig.savefig(path, dpi=dpi, bbox_inches="tight") - paths.append(path) - plt.close(fig) - self.fig = None - self.ax = None - - return paths - - -class TimeSeriesPlot(StruphyPlot): - """Scalar quantities against time, optionally log-scaled with a growth-rate fit. - - Parameters - ---------- - data : StruphyArray or sequence of StruphyArray - One or more signals sharing a ``t`` dimension. - logy : bool - Log-scale the ordinate. - fit : bool - Overlay an exponential fit and report the rate in the legend. - fit_window : tuple, optional - ``(t0, t1)`` bounds for the fit. - fit_of_sqrt : bool - Fit the growth rate of the amplitude rather than of the plotted quantity. - """ - - def __init__(self, data, *, logy=True, fit=False, fit_window=None, fit_of_sqrt=False, **kwargs): - series = [data] if isinstance(data, StruphyArray) else list(data) - super().__init__(series[0], **kwargs) - self.series = series - self.logy = logy - self.fit = fit - self.fit_window = fit_window or (None, None) - self.fit_of_sqrt = fit_of_sqrt - #: one ``(gamma, b, window)`` per series once drawn, for reporting the rates - self.fit_results = [] - def draw(self): - fig, ax = self._make_axes() - - self.fit_results = [] - for s in self.series: - (line,) = ax.plot(s.coord("t"), xp.asarray(s), label=s.label or None) - - if not self.fit: - continue - - gamma, b, window = growth_rate( - s, - t0=self.fit_window[0], - t1=self.fit_window[1], - of_sqrt=self.fit_of_sqrt, - ) - self.fit_results.append((gamma, b, window)) - if gamma is None: - continue - - t_fit = xp.asarray(s.coord("t"))[window] - scale = 2.0 if self.fit_of_sqrt else 1.0 - ax.plot( - t_fit, - xp.exp(scale * (gamma * t_fit + b)), - "--", - color=line.get_color(), - label=rf"fit: $\gamma$ = {gamma:.4e}", - ) - ax.axvspan(t_fit[0], t_fit[-1], alpha=0.12, color="grey") - - if self.logy: +def physical_grids(data: xr.DataArray, *, plane="XY"): + """Return physical auxiliary coordinates already attached to a selected field.""" + if plane not in PLANES: + raise ValueError(f"unknown plane {plane!r}; expected one of {tuple(PLANES)}") + xname, yname, xlabel, ylabel = PLANES[plane] + missing = [name for name in ("X", "Y", "Z") if name not in data.coords] + if missing: + raise ValueError(f"physical coordinates are not attached to {data.name!r}: missing {missing}") + xcoord = np.sqrt(data.X**2 + data.Y**2) if xname == "R" else data.coords[xname] + ycoord = data.coords[yname] + if xcoord.ndim != 2 or ycoord.ndim != 2: + raise ValueError("select all but two spatial dimensions before requesting a physical grid") + return np.asarray(xcoord), np.asarray(ycoord), xlabel, ylabel + + +def _slice_data(data, view): + selected = _select(data, view) + if view.sweep in selected.dims: + raise ValueError(f"select one {view.sweep!r} value before drawing a static slice") + if view.x is None or view.y is None: + if selected.ndim != 2: + raise ValueError(f"view.x and view.y are required for remaining dims {selected.dims}") + x, y = selected.dims + else: + x, y = view.x, view.y + if set(selected.dims) != {x, y}: + raise ValueError(f"selection leaves dimensions {selected.dims}; expected only {x!r}, {y!r}") + selected = selected.transpose(x, y) + grids = physical_grids(selected, plane=view.plane) if view.coordinates == "physical" else logical_grids(selected, x=x, y=y) + return selected, grids + + +def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=""): + """Plot one or more aligned time series.""" + series = [data] if isinstance(data, xr.DataArray) else list(data) + if not series: + raise ValueError("at least one time series is required") + for item in series: + validate_array(item, required_dims=("t",)) + if item.dims != ("t",): + raise ValueError(f"time series must have dims ('t',), got {item.dims}") + if len(series) > 1: + series = list(xr.align(*series, join="exact")) + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + artists, fits = [], [] + for item in series: + line, = ax.plot(item.t, item, label=_label(item) or None) + artists.append(line) + result = growth_rate(item, fit) if fit is not None else None + fits.append(result) + if result is not None: + fitted, = ax.plot(result.time, result.fitted, "--", color=line.get_color(), + label=rf"fit: $\gamma$ = {result.rate:.4e}") + ax.axvspan(result.time[0], result.time[-1], alpha=0.12, color="grey") + artists.append(fitted) + if logy: ax.set_yscale("log") - - ax.set_xlabel(self.data.axis_label("t")) - ax.set_ylabel(self.data.value_label) - ax.set_title(self.title) - if any(s.label for s in self.series) or self.fit: + ax.set_xlabel(axis_label(series[0], "t")) + ax.set_ylabel(value_label(series[0])) + ax.set_title(title if title is not None else _label(series[0])) + if any(_label(item) for item in series) or fit is not None: ax.legend() + _finish(fig, run_label=run_label, tight=ax is not None) + return PlotResult(fig, ax, artists, fits) -class ScalarsPlot(StruphyPlot): - """Every scalar recorded during a run on one axes, over an energy-error panel. - - The overview figure of a run: all tracked scalars against time, plus the - relative error of the conserved quantity underneath. - - Parameters - ---------- - scalars : Scalars or dict - Maps a name to a :class:`~struphy.post_processing.arrays.StruphyArray` over - ``t``, as :attr:`~struphy.post_processing.post_processing_tools.PlottingData.scalars` - provides. - names : sequence of str, optional - Plot these, in this order. Defaults to all of them. - exclude : sequence of str - Names to leave out when ``names`` is not given. - logy : bool - Log-scale the ordinate of the main axes. - relative_to : str, optional - Divide every series by this one, e.g. ``"en_tot"``. Use it when the scalars - do not share a unit, so that the common ordinate means something. - error_panel : str or None - Scalar whose conservation error is drawn in a panel below, if it was - recorded. ``None`` suppresses the panel. - - Attributes - ---------- - error : StruphyArray or None - The relative error that was drawn, so a script can report its final value. - """ - - tight = False - - def __init__( - self, - scalars, - *, - names=None, - exclude=SCALARS_EXCLUDE, - logy=False, - relative_to=None, - error_panel="en_tot", - **kwargs, - ): - self.names = scalar_names(scalars, names=names, exclude=exclude) - if not self.names: - raise ValueError(f"no scalars to plot, available: {tuple(scalars.keys())}") - - kwargs.setdefault("title", "Scalars") - super().__init__(scalars[self.names[0]], **kwargs) - - self.scalars = scalars - self.logy = logy - self.relative_to = relative_to - # a panel cannot be added to an axes the caller supplied - self.error_panel = error_panel if (error_panel in scalars and self._ax is None) else None - self.error = None - self.error_ax = None - - def _series(self, name) -> StruphyArray: - values = xp.asarray(self.scalars[name]) - if self.relative_to is None: - return values - return values / xp.asarray(self.scalars[self.relative_to]) - - def _ylabel(self) -> str: - if self.relative_to is not None: - return f"quantity / {self.relative_to}" - units = {self.scalars[n].unit for n in self.names} - return f"[{units.pop()}]" if len(units) == 1 else "[a.u.]" - - def draw(self): - if self.error_panel is None: - fig, ax = self._make_axes() - ax_err = None - else: - fig, (ax, ax_err) = plt.subplots( - 2, - 1, - sharex=True, - figsize=(8.0, 6.5), - height_ratios=(2, 1), - layout="constrained", - ) - self.fig, self.ax = fig, ax - self.error_ax = ax_err - - for name in self.names: - ax.plot(self.scalars[name].coord("t"), self._series(name), label=name) - - if self.logy: - ax.set_yscale("log") - ax.set_ylabel(self._ylabel()) - ax.set_title(self.title) - ax.legend(fontsize="small", ncols=max(1, len(self.names) // 6)) - - if ax_err is None: - ax.set_xlabel(self.data.axis_label("t")) - return - - self.error = relative_error(self.scalars[self.error_panel]) - error_values = xp.asarray(self.error) - ax_err.plot(self.error.coord("t"), error_values) - # an exactly conserved quantity has nothing to show on a log axis - if xp.any(error_values > 0.0): - ax_err.set_yscale("log") - ax_err.set_xlabel(self.data.axis_label("t")) - ax_err.set_ylabel(rf"$|\Delta$ {self.error_panel}$|$ / {self.error_panel}$(0)$", fontsize="small") - - -class Slice2DPlot(StruphyPlot): - """A 2D quantity as a pcolormesh, with the colorbar and orientation handled. - - Parameters - ---------- - data : StruphyArray - Two-dimensional, or higher with the extra dimensions already selected. - grids : tuple, optional - ``(xgrid, ygrid, xlabel, ylabel)`` from :func:`physical_grids` or - :func:`logical_grids`. Defaults to the logical grids of ``data``. - equal_aspect : bool - Force an equal aspect ratio, appropriate for physical coordinates. - """ - - def __init__(self, data, *, grids=None, vmin=None, vmax=None, equal_aspect=False, **kwargs): - super().__init__(data, **kwargs) - self.grids = grids if grids is not None else logical_grids(data) - self.vmin = vmin - self.vmax = vmax - self.equal_aspect = equal_aspect - - def draw(self): - fig, ax = self._make_axes() - xgrid, ygrid, xlabel, ylabel = self.grids - - values = match_to_grid(self.data, xgrid) - pcm = ax.pcolormesh(xgrid, ygrid, values, shading="auto", vmin=self.vmin, vmax=self.vmax) - fig.colorbar(pcm, ax=ax, label=self.data.value_label) - - if self.equal_aspect: +def plot_slice(data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, + equal_aspect=None, title=None, run_label=""): + """Render one selected two-dimensional slice.""" + view = view or View() + selected, (xgrid, ygrid, xlabel, ylabel) = _slice_data(data, view) + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + mesh = ax.pcolormesh(xgrid, ygrid, np.asarray(selected), shading="auto", vmin=vmin, vmax=vmax) + fig.colorbar(mesh, ax=ax, label=value_label(data)) + if equal_aspect if equal_aspect is not None else view.coordinates == "physical": ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(self.title) + ax.set(xlabel=xlabel, ylabel=ylabel, title=title if title is not None else _label(data)) ax.grid(False) - self.mesh = pcm - - -class PanelGridPlot(StruphyPlot): - """A grid of 2D snapshots at times spread evenly over the run. - - Replaces the hand-rolled ``nrows``/``ncols``/``time_indices`` loop. - - Parameters - ---------- - data : StruphyArray - Must have a ``t`` dimension and two further dimensions. - nrows, ncols : int - Panel layout. ``nrows * ncols`` snapshots are shown. - shared_clim : bool - Use one colour range across all panels, so panels are comparable. - """ - - tight = False + _finish(fig, run_label=run_label) + return PlotResult(fig, ax, [mesh]) + + +def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim=True, + title=None, run_label=""): + """Plot snapshots spread across a sweep coordinate.""" + view = view or View() + selected = _select(data, view) + validate_array(selected, required_dims=(view.sweep,)) + count = nrows * ncols + indices = np.linspace(0, selected.sizes[view.sweep] - 1, count).astype(int) + snapshots = [selected.isel({view.sweep: int(index)}) for index in indices] + limits = (None, None) + if shared_clim: + limits = (min(float(item.min()) for item in snapshots), max(float(item.max()) for item in snapshots)) + with plt.rc_context(STRUPHY_STYLE): + fig, axes = plt.subplots(nrows, ncols, figsize=(3.5*ncols, 2.8*nrows), sharex=True, + sharey=True, squeeze=False, layout="constrained") + meshes = [] + for ax, index, snapshot in zip(axes.ravel(), indices, snapshots): + local_view = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) + values, (xg, yg, xlabel, ylabel) = _slice_data(snapshot, local_view) + mesh = ax.pcolormesh(xg, yg, values, shading="auto", vmin=limits[0], vmax=limits[1]) + meshes.append(mesh) + ax.set_title(f"{view.sweep} = {float(selected[view.sweep][index]):.3e}") + ax.grid(False) + if not shared_clim: + fig.colorbar(mesh, ax=ax) + for ax in axes[-1]: ax.set_xlabel(xlabel) + for row in axes: row[0].set_ylabel(ylabel) + if shared_clim: fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) + heading = title if title is not None else _label(data) + fig.suptitle(" — ".join(filter(None, (heading, run_label)))) + return PlotResult(fig, axes, meshes) + + +class InteractiveSliceViewer: + """Stateful viewer using one recipe for the sweep and all remaining dimensions.""" + + def __init__(self, data: xr.DataArray, *, view=None, vmin=None, vmax=None, run_label=""): + self.data = validate_array(data) + self.view = view or View() + self.vmin, self.vmax, self.run_label = vmin, vmax, run_label + self.result = None + self.sliders = {} - def __init__(self, data, *, nrows=3, ncols=4, grids=None, shared_clim=False, equal_aspect=False, **kwargs): - super().__init__(data, **kwargs) - self.nrows = nrows - self.ncols = ncols - self.grids = grids - self.shared_clim = shared_clim - self.equal_aspect = equal_aspect + def show(self): + return self.draw().show() def draw(self): - n = self.nrows * self.ncols - nt = self.data.shape[self.data.axis("t")] - indices = [int(i / max(n - 1, 1) * (nt - 1)) for i in range(n)] - - t = self.data.coord("t") - snapshots = [self.data.isel(t=i) for i in indices] - grids = self.grids if self.grids is not None else logical_grids(snapshots[0]) - xgrid, ygrid, xlabel, ylabel = grids - - vmin = vmax = None - if self.shared_clim: - vmin = float(min(xp.nanmin(xp.asarray(s)) for s in snapshots)) - vmax = float(max(xp.nanmax(xp.asarray(s)) for s in snapshots)) - - fig, axs = plt.subplots( - nrows=self.nrows, - ncols=self.ncols, - figsize=(3.5 * self.ncols, 2.8 * self.nrows), - sharex=True, - sharey=True, - squeeze=False, - layout="constrained", - ) - self.fig, self.ax = fig, axs - - for panel, (idx, snap) in enumerate(zip(indices, snapshots)): - ax = axs[panel // self.ncols][panel % self.ncols] - pcm = ax.pcolormesh( - xgrid, - ygrid, - match_to_grid(snap, xgrid), - shading="auto", - vmin=vmin, - vmax=vmax, - ) - ax.set_title(f"t = {float(t[idx]):.2e}") + base = _select(self.data, self.view) + x, y = self.view.x, self.view.y + if x is None or y is None: + candidates = [dim for dim in base.dims if dim != self.view.sweep] + if len(candidates) < 2: + raise ValueError("viewer needs two display dimensions") + x, y = candidates[:2] + controls = [dim for dim in base.dims if dim not in {x, y}] + indices = {dim: 0 for dim in controls} + + def frame(): + return base.isel(indices), View(x=x, y=y, coordinates=self.view.coordinates, plane=self.view.plane) + + selected, frame_view = frame() + selected, (xg, yg, xlabel, ylabel) = _slice_data(selected, frame_view) + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() + fig.subplots_adjust(bottom=0.13 + 0.05*len(controls)) + mesh = ax.pcolormesh(xg, yg, selected, shading="auto", vmin=self.vmin, vmax=self.vmax) + colorbar = fig.colorbar(mesh, ax=ax, label=value_label(self.data)) + ax.set(xlabel=xlabel, ylabel=ylabel) ax.grid(False) - if self.equal_aspect: - ax.set_aspect("equal", adjustable="box") - if not self.shared_clim: - fig.colorbar(pcm, ax=ax) - - for ax in axs[-1]: - ax.set_xlabel(xlabel) - for row in axs: - row[0].set_ylabel(ylabel) - - if self.shared_clim: - fig.colorbar(pcm, ax=list(axs.ravel()), label=self.data.value_label) - - fig.suptitle(" — ".join(filter(None, (self.title, self._run_label())))) - - -class SliderPlot(FrameSequence, StruphyPlot): - """A 2D quantity with a time slider, and a second slider for the free axis in 3D. - - The returned object keeps a reference to its sliders; discarding it stops the - widgets from responding. - - Parameters - ---------- - data : StruphyArray - Dimensions ``(t, a, b)`` or ``(t, a, b, c)``; the fourth is swept by the - second slider. - slice_dim : str, optional - Which dimension the second slider steps through. Defaults to the last. - slice_index : int, optional - Where the second slider starts, and which cut :meth:`save_frames` writes. - Defaults to the middle of ``slice_dim``. - step : int - Keep every ``step``-th time index when writing frames. - grids : tuple or callable, optional - Either fixed ``(xgrid, ygrid, xlabel, ylabel)``, or a function of the slice - index returning them. Pass a callable when the physical grid depends on where - the cut is taken, so that it follows the slider instead of going stale. - """ - - tight = False - - def __init__( - self, - data, - *, - grids=None, - slice_dim=None, - slice_index=None, - step=1, - vmin=None, - vmax=None, - equal_aspect=True, - **kwargs, - ): - super().__init__(data, **kwargs) - self.grids = grids - self.step = step - self.vmin = vmin - self.vmax = vmax - self.equal_aspect = equal_aspect - spatial = [d for d in data.dims if d != "t"] - self.slice_dim = slice_dim if slice_dim is not None else (spatial[-1] if len(spatial) > 2 else None) - n_slice = data.shape[data.axis(self.slice_dim)] if self.slice_dim else 0 - self.slice_index = n_slice // 2 if slice_index is None else slice_index - self.sliders = [] - - def _frame_values(self, index): - """The frame sequence holds the cut fixed and sweeps time, as the time slider does.""" - return self._frame(index, self.slice_index) - - def _frame_grids(self): - return self._grids_for(self.slice_index) - - def _frame(self, t_index, slice_index): - frame = self.data.isel(t=t_index) - if self.slice_dim is not None: - frame = frame.isel(**{self.slice_dim: slice_index}) - return frame - - def _grids_for(self, slice_index): - if callable(self.grids): - return self.grids(slice_index) - if self.grids is not None: - return self.grids - return logical_grids(self._frame(0, slice_index)) - - def draw(self): - nt = self.data.shape[self.data.axis("t")] - t = self.data.coord("t") - - n_slice = self.data.shape[self.data.axis(self.slice_dim)] if self.slice_dim else 0 - slice_index = self.slice_index if n_slice else 0 - - first = self._frame(0, slice_index) - xgrid, ygrid, xlabel, ylabel = self._grids_for(slice_index) - - fig, ax = self._make_axes() - fig.subplots_adjust(bottom=0.24 if self.slice_dim else 0.18) - - pcm = ax.pcolormesh( - xgrid, - ygrid, - match_to_grid(first, xgrid), - shading="auto", - vmin=self.vmin, - vmax=self.vmax, - ) - cbar = fig.colorbar(pcm, ax=ax, label=self.data.value_label) - if self.equal_aspect: - ax.set_aspect("equal", adjustable="box") - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) - ax.set_title(f"{self.title} at t = {float(t[0]):.4e}") - ax.grid(False) + if self.view.coordinates == "physical": ax.set_aspect("equal", adjustable="box") + state = {"mesh": mesh} - s_time = Slider(fig.add_axes([0.20, 0.08, 0.60, 0.03]), "time", 0, nt - 1, valinit=0, valstep=1) - self.sliders = [s_time] - s_slice = None - if self.slice_dim: - s_slice = Slider( - fig.add_axes([0.20, 0.03, 0.60, 0.03]), - f"{self.slice_dim} index", - 0, - n_slice - 1, - valinit=slice_index, - valstep=1, - ) - self.sliders.append(s_slice) - - state = {"mesh": pcm, "xgrid": xgrid, "slice": slice_index} - - def update(_): - ti = int(s_time.val) - si = int(s_slice.val) if s_slice is not None else 0 - # so that a cut found with the slider is the one save_frames writes - self.slice_index = si - - # a grid that depends on the cut has to be redrawn, not just refilled - if callable(self.grids) and si != state["slice"]: - xg, yg, _, _ = self._grids_for(si) + def update(_=None): + for dim, slider in self.sliders.items(): indices[dim] = int(slider.val) + item, item_view = frame() + item, grids = _slice_data(item, item_view) state["mesh"].remove() - state["mesh"] = ax.pcolormesh( - xg, - yg, - match_to_grid(self._frame(ti, si), xg), - shading="auto", - vmin=self.vmin, - vmax=self.vmax, - ) - state["xgrid"] = xg - state["slice"] = si - cbar.update_normal(state["mesh"]) - - mesh, grid = state["mesh"], state["xgrid"] - frame = match_to_grid(self._frame(ti, si), grid) - - mesh.set_array(frame.ravel()) - if self.vmin is None and self.vmax is None: - mesh.set_clim(float(xp.nanmin(frame)), float(xp.nanmax(frame))) - cbar.update_normal(mesh) - ax.set_title(f"{self.title} at t = {float(t[ti]):.4e}") - fig.canvas.draw_idle() - - for s in self.sliders: - s.on_changed(update) - - self.mesh = pcm - - -class AnimationPlot(FrameSequence, StruphyPlot): - """Sweep a 2D quantity over time, as a matplotlib animation or a frame sequence. - - Parameters - ---------- - data : StruphyArray - Dimensions ``(t, a, b)``. - step : int - Keep every ``step``-th time index. - shared_clim : bool - Hold the colour range fixed across frames, so brightness changes are physical. - """ - - tight = False - - def __init__( - self, data, *, grids=None, step=1, vmin=None, vmax=None, shared_clim=True, equal_aspect=False, **kwargs - ): - super().__init__(data, **kwargs) - self.grids = grids - self.step = step - self.vmin = vmin - self.vmax = vmax - self.shared_clim = shared_clim - self.equal_aspect = equal_aspect - - def _clim(self): - if self.shared_clim and self.vmin is None and self.vmax is None: - values = xp.asarray(self.data) - return float(xp.nanmin(values)), float(xp.nanmax(values)) - return self.vmin, self.vmax - - def draw(self): - fig, ax, pcm, xgrid = self._setup() - self._update(ax, pcm, xgrid, 0) - self.mesh = pcm + state["mesh"] = ax.pcolormesh(grids[0], grids[1], item, shading="auto", + vmin=self.vmin, vmax=self.vmax) + if self.vmin is None and self.vmax is None: + state["mesh"].set_clim(float(item.min()), float(item.max())) + colorbar.update_normal(state["mesh"]) + values = ", ".join(f"{dim}={float(base[dim][index]):.3e}" for dim, index in indices.items()) + ax.set_title(" at ".join(filter(None, (_label(self.data), values)))) + fig.canvas.draw_idle() + + for row, dim in enumerate(controls): + slider_ax = fig.add_axes([0.20, 0.05 + 0.05*row, 0.60, 0.025]) + slider = Slider(slider_ax, dim, 0, base.sizes[dim]-1, valstep=1) + slider.on_changed(update) + self.sliders[dim] = slider + update() + _finish(fig, run_label=self.run_label, tight=False) + self.result = PlotResult(fig, ax, [state["mesh"]]) + return self.result + + +def animate_slices(data: xr.DataArray, *, view=None, interval=100, step=1, vmin=None, vmax=None): + """Create an animation using the same :class:`View` as static slices.""" + from matplotlib.animation import FuncAnimation + view = view or View() + selected = _select(data, view) + frames = range(0, selected.sizes[view.sweep], step) + first = selected.isel({view.sweep: 0}) + local = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) + values, grids = _slice_data(first, local) + fig, ax = plt.subplots() + mesh = ax.pcolormesh(grids[0], grids[1], values, shading="auto", vmin=vmin, vmax=vmax) + fig.colorbar(mesh, ax=ax, label=value_label(data)) + ax.set(xlabel=grids[2], ylabel=grids[3]) + + def update(index): + item = selected.isel({view.sweep: index}) + item, item_grids = _slice_data(item, local) + mesh.set_array(np.asarray(item).ravel()) + ax.set_title(f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}") + return mesh, + return FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) + + +def save_frames(data: xr.DataArray, directory, *, view=None, step=1, prefix="frame", dpi=110): + """Write a sweep as PNG frames without retaining figures.""" + view = view or View() + selected = _select(data, view) + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + paths = [] + for frame, index in enumerate(range(0, selected.sizes[view.sweep], step)): + item = selected.isel({view.sweep: index}) + local = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) + result = plot_slice(item, view=local, + title=f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}") + path = directory / f"{prefix}_{frame:04d}.png" + result.save(path, dpi=dpi, close=True) + paths.append(str(path)) + return paths - def animate(self, *, interval=100): - """Return a :class:`matplotlib.animation.FuncAnimation` over the frames.""" - from matplotlib.animation import FuncAnimation - with plt.rc_context(STRUPHY_STYLE): - fig, ax, pcm, xgrid = self._setup() - anim = FuncAnimation( - fig, - lambda i: self._update(ax, pcm, xgrid, i), - frames=list(self.frames), - interval=interval, - blit=False, - ) - self.fig = fig - return anim - - -class MarkerTrajectoryPlot(StruphyPlot): - """Marker positions in 3D over time, coloured by weight, with a time slider. - - Parameters - ---------- - orbits : StruphyArray - Dimensions ``(t, marker, attribute)``; columns 0-2 are position, 6 is weight. - max_markers : int - Cap on the number of markers drawn. - show_paths : bool, optional - Trail each marker's history. Defaults to on for small marker counts. - """ - - tight = False - - def __init__(self, orbits, *, max_markers=200, show_paths=None, **kwargs): - kwargs.setdefault("title", "Marker trajectories") - super().__init__(orbits, **kwargs) - self.max_markers = max_markers - self.show_paths = show_paths if show_paths is not None else max_markers <= 200 - self.sliders = [] - - def draw(self): - orbs = xp.asarray(self.data) - n = min(orbs.shape[1], self.max_markers) - cols = getattr(self.data, "columns", None) or orbit_columns(orbs.shape[-1]) - - x, y, z = (orbs[:, :n, i] for i in range(cols["position"].start, cols["position"].stop)) - w = orbs[:, :n, cols["weight"]] if "weight" in cols else None - nt = x.shape[0] - - fig = plt.figure(figsize=(8, 7)) - ax = fig.add_subplot(111, projection="3d") - self.fig, self.ax = fig, ax - fig.subplots_adjust(bottom=0.18) - - colouring = {"c": w[0], "cmap": "viridis"} if w is not None else {} - scatter = ax.scatter(x[0], y[0], z[0], s=8, **colouring) - lines = ( - [ax.plot(x[:1, j], y[:1, j], z[:1, j], lw=0.8, alpha=0.5)[0] for j in range(n)] if self.show_paths else [] - ) - - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.set_title(f"{self.title} | step 0/{nt - 1}") - if w is not None: - fig.colorbar(scatter, ax=ax, label="marker weight") - - slider = Slider(fig.add_axes([0.18, 0.06, 0.65, 0.03]), "time", 0, nt - 1, valinit=0, valstep=1) - self.sliders = [slider] - - def update(_): - it = int(slider.val) - scatter._offsets3d = (x[it], y[it], z[it]) - if w is not None: - scatter.set_array(w[it]) - for j, line in enumerate(lines): - line.set_data(x[: it + 1, j], y[: it + 1, j]) - line.set_3d_properties(z[: it + 1, j]) - ax.set_title(f"{self.title} | step {it}/{nt - 1}") - fig.canvas.draw_idle() - - slider.on_changed(update) - - -class PlottingAccessor: - """High-level plotting methods bound to a loaded ``PlottingData`` instance. - - The accessor supplies run metadata automatically and resolves scalar and orbit - names from their containers. Each method draws immediately and returns the - underlying :class:`StruphyPlot`, preserving access to its axes, fit results, - sliders and saving methods. - """ - - def __init__(self, plotting_data): - self.data = plotting_data - - def _draw(self, plot_type, data, **kwargs): - kwargs.setdefault("params", self.data.params) - return plot_type(data, **kwargs).plot() - - def _scalar_series(self, data): - if isinstance(data, str): - return self.data.scalars[data] - if isinstance(data, StruphyArray): - return data - return [self.data.scalars[item] if isinstance(item, str) else item for item in data] - - def scalars(self, **kwargs) -> ScalarsPlot: - """Draw the overview of the run's scalar diagnostics.""" - return self._draw(ScalarsPlot, self.data.scalars, **kwargs) - - def time_series(self, data, **kwargs) -> TimeSeriesPlot: - """Draw one or more time series, accepting scalar names or arrays.""" - return self._draw(TimeSeriesPlot, self._scalar_series(data), **kwargs) - - def slice(self, data: StruphyArray, **kwargs) -> Slice2DPlot: - """Draw one two-dimensional array or selected snapshot.""" - return self._draw(Slice2DPlot, data, **kwargs) - - def panels(self, data: StruphyArray, **kwargs) -> PanelGridPlot: - """Draw evenly spaced snapshots of a time-dependent 2D array.""" - return self._draw(PanelGridPlot, data, **kwargs) - - def slider(self, data: StruphyArray, **kwargs) -> SliderPlot: - """Draw a 2D array with time and optional cut-plane sliders.""" - return self._draw(SliderPlot, data, **kwargs) - - def animation(self, data: StruphyArray, **kwargs) -> AnimationPlot: - """Draw the initial view of a time-dependent 2D animation.""" - return self._draw(AnimationPlot, data, **kwargs) - - def orbits(self, data, **kwargs) -> MarkerTrajectoryPlot: - """Draw marker trajectories, accepting either a species name or an array.""" - if isinstance(data, str): - data = self.data.orbits[data] - return self._draw(MarkerTrajectoryPlot, data, **kwargs) - - -def save_all_scalars( - scalars, - directory, - *, - names=None, - exclude=SCALARS_EXCLUDE, - logy=False, - params=None, - table: str = "csv", - file_format: str = "png", - dpi: int = 110, -) -> list[str]: - """Write the standard scalar output of a run: the table, an overview, one figure each. - - Everything a finished run should leave behind for its scalars, in one call. - - Parameters - ---------- - scalars : Scalars or dict - Maps a name to a :class:`~struphy.post_processing.arrays.StruphyArray` over ``t``. - directory : str - Created if it does not exist. - names, exclude - See :func:`~struphy.post_processing.arrays.scalar_names`. - logy : bool - Log-scale the ordinate of every figure. - params : ParamsIn, optional - Run settings, added to each figure as a suptitle. - table : str or None - Format of the per-time-step table, ``"csv"`` or ``"npz"``; ``None`` to skip it. - file_format : str - Image format of the figures. - - Returns - ------- - list of str - The paths written, the table first. - """ +def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, + error_panel="en_tot", logy=False, run_label=""): + """Plot a scalar overview and optional conservation-error panel.""" selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: - logger.warning("No scalars to save.") - return [] - - os.makedirs(directory, exist_ok=True) + if not selected: raise ValueError("no scalars to plot") + has_error = error_panel is not None and error_panel in scalars + fig, axes = plt.subplots(2 if has_error else 1, 1, sharex=has_error, + figsize=(8, 6.5) if has_error else None, + height_ratios=(2, 1) if has_error else None, + layout="constrained") + ax = axes[0] if has_error else axes + for name in selected: + values = scalars[name] / scalars[relative_to] if relative_to else scalars[name] + ax.plot(values.t, values, label=name) + if logy: ax.set_yscale("log") + units = {scalars[name].attrs.get("units", "") for name in selected} + ylabel = f"quantity / {relative_to}" if relative_to else (f"[{units.pop()}]" if len(units) == 1 else "[a.u.]") + ax.set(ylabel=ylabel, title="Scalars") + ax.legend(fontsize="small") + artists, error = list(ax.lines), None + if has_error: + error = relative_error(scalars[error_panel]) + axes[1].plot(error.t, error) + if np.any(np.asarray(error) > 0): axes[1].set_yscale("log") + axes[1].set(xlabel=axis_label(error, "t"), ylabel=f"relative error of {error_panel}") + artists.extend(axes[1].lines) + else: + ax.set_xlabel(axis_label(scalars[selected[0]], "t")) + if run_label: fig.suptitle(run_label, fontsize="small") + return PlotResult(fig, axes, artists), error + + +def save_all_scalars(scalars, directory, *, names=None, exclude=SCALARS_EXCLUDE, logy=False, + run_label="", table="csv", file_format="png", dpi=110): + """Write a table, scalar overview and one figure per scalar.""" + selected = scalar_names(scalars, names=names, exclude=exclude) + if not selected: return [] + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) paths = [] - - if table is not None: - paths.append(save_scalars(scalars, os.path.join(directory, f"scalars.{table}"), names=selected, fmt=table)) - - overview = os.path.join(directory, f"scalars.{file_format}") - ScalarsPlot(scalars, names=selected, logy=logy, params=params).save(overview, dpi=dpi, close=True) - paths.append(overview) - + if table: + paths.append(save_scalars(scalars, str(directory / f"scalars.{table}"), names=selected, fmt=table)) + overview, _ = plot_scalars(scalars, names=selected, logy=logy, run_label=run_label) + path = directory / f"scalars.{file_format}" + overview.save(path, dpi=dpi, close=True) + paths.append(str(path)) for name in selected: - path = os.path.join(directory, f"{name}.{file_format}") - TimeSeriesPlot( - scalars[name], - logy=logy, - fit=False, - title=name, - params=params, - ).save(path, dpi=dpi, close=True) - paths.append(path) - - logger.info(f"Wrote {len(paths)} scalar output files to {directory}") + result = plot_timeseries(scalars[name], logy=logy, title=name, run_label=run_label) + path = directory / f"{name}.{file_format}" + result.save(path, dpi=dpi, close=True) + paths.append(str(path)) return paths -def plot_equilibrium_profile(path_out, *, ax=None): - """Radial profiles of the equilibrium written to ``geometry.vts``.""" - import pyvista as pv - - equil = pv.read(os.path.join(path_out, "geometry.vts")) - dims = equil.dimensions - grid = xp.reshape(equil.points, dims + (3,)) - r = xp.sqrt(grid[:, :, :, 0] ** 2 + grid[:, :, :, 1] ** 2) - p0 = xp.reshape(equil.point_data["p0"], dims) +def plot_marker_trajectories(orbits: xr.DataArray, *, ax=None, max_markers=200, show_paths=None): + """Plot a static 3-D trajectory overview; interactive marker UI is intentionally separate.""" + validate_array(orbits, required_dims=("t", "marker", "attribute")) + columns = orbits.attrs.get("columns", orbit_columns(orbits.sizes["attribute"])) + count = min(orbits.sizes["marker"], max_markers) + values = np.asarray(orbits.isel(marker=slice(0, count))) + fig = plt.figure() if ax is None else ax.figure + ax = fig.add_subplot(111, projection="3d") if ax is None else ax + positions = values[..., columns["position"]] + show_paths = count <= 200 if show_paths is None else show_paths + artists = [] + if show_paths: + for marker in range(count): + artists.extend(ax.plot(*positions[:, marker].T, lw=.8, alpha=.5)) + artists.append(ax.scatter(*positions[-1].T, s=8)) + ax.set(xlabel="X", ylabel="Y", zlabel="Z", title="Marker trajectories") + return PlotResult(fig, ax, artists) - with plt.rc_context(STRUPHY_STYLE): - if ax is None: - fig, ax = plt.subplots() - ax.plot(r[0, 0, :], p0[0, 0, :], label=r"$p_0$") - if "n0" in equil.point_data: - n0 = xp.reshape(equil.point_data["n0"], dims) - ax.plot(r[0, 0, :], n0[0, 0, :], label=r"$n_0$") - ax.plot(r[0, 0, :], p0[0, 0, :] / n0[0, 0, :], label=r"$T_0$") +def plot_equilibrium_profile(path_out, *, ax=None): + """Plot radial equilibrium profiles from ``geometry.vts``.""" + import pyvista as pv - ax.set_xlabel(r"$R$") - ax.set_title("Radial equilibrium profiles") - ax.legend() - return ax + equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) + shape = equilibrium.dimensions + grid = np.reshape(equilibrium.points, shape + (3,)) + radius = np.sqrt(grid[..., 0]**2 + grid[..., 1]**2) + pressure = np.reshape(equilibrium.point_data["p0"], shape) + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + ax.plot(radius[0, 0], pressure[0, 0], label=r"$p_0$") + if "n0" in equilibrium.point_data: + density = np.reshape(equilibrium.point_data["n0"], shape) + ax.plot(radius[0, 0], density[0, 0], label=r"$n_0$") + ax.plot(radius[0, 0], pressure[0, 0]/density[0, 0], label=r"$T_0$") + ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") + ax.legend() + return PlotResult(fig, ax, list(ax.lines)) diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 56cb5b44a..1729b4228 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -1,42 +1,32 @@ -"""Unit tests for the standardized plotters. - -These render into the Agg backend, so they check the geometry and labeling that the -plotters derive from the data rather than the appearance of the result. -""" - -import os -from types import SimpleNamespace +"""Tests for functional plotting and the shared view recipe.""" import matplotlib import numpy as np import pytest +import xarray as xr matplotlib.use("Agg") - from matplotlib import pyplot as plt # noqa: E402 from struphy.diagnostics.plotting import ( # noqa: E402 - PLANES, - AnimationPlot, - MarkerTrajectoryPlot, - PanelGridPlot, - PlottingAccessor, - ScalarsPlot, - Slice2DPlot, - SliderPlot, - TimeSeriesPlot, + GrowthFit, + InteractiveSliceViewer, + View, + animate_slices, drift, - field_slice_grids, growth_rate, logical_grids, - match_to_grid, + physical_grids, + plot_panels, + plot_scalars, + plot_slice, + plot_timeseries, relative_error, save_all_scalars, + save_frames, ) -from struphy.post_processing.arrays import StruphyArray, wrap_orbits # noqa: E402 +from struphy.post_processing.arrays import data_array # noqa: E402 -# FuncAnimation warns when it is collected without having been rendered, which is -# exactly what happens to the animations these tests build and discard. pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") @@ -46,472 +36,118 @@ def close_figures(): plt.close("all") -def phase_space(nt=12, n1=6, nv=8): - return StruphyArray( - np.random.default_rng(0).random((nt, n1, nv)), - dims=("t", "e1", "v1"), - coords={"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, n1), "v1": np.linspace(-3, 3, nv)}, - label="$f$", - ) - - -def meshgrids(n1=6, n2=7, n3=5): - return np.meshgrid( - np.linspace(1.0, 2.0, n1), - np.linspace(0.0, 2 * np.pi, n2), - np.linspace(-1.0, 1.0, n3), - indexing="ij", - ) - - -# ---------------------------------------------------------------- growth rate - - -def test_growth_rate_recovers_a_known_exponential(): - t = np.linspace(0, 10, 100) - y = StruphyArray(1e-6 * np.exp(0.3 * t), dims=("t",), coords={"t": t}) - - gamma, b, window = growth_rate(y) - assert gamma == pytest.approx(0.3) - assert np.exp(b) == pytest.approx(1e-6, rel=1e-6) - - -def test_growth_rate_of_sqrt_halves_the_exponent(): - """An energy grows at twice the rate of the amplitude it is quadratic in.""" - t = np.linspace(0, 10, 100) - y = StruphyArray(np.exp(0.3 * t), dims=("t",), coords={"t": t}) - assert growth_rate(y, of_sqrt=True)[0] == pytest.approx(0.15) - - -def test_growth_rate_honours_the_window(): - t = np.linspace(0, 10, 101) - y = StruphyArray(np.exp(0.3 * t), dims=("t",), coords={"t": t}) - _, _, window = growth_rate(y, t0=2.0, t1=4.0) - assert t[window][0] >= 2.0 and t[window][-1] <= 4.0 - - -def test_growth_rate_ignores_non_positive_samples(): - t = np.linspace(0, 10, 50) - values = np.exp(0.3 * t) - values[:5] = -1.0 - gamma, _, window = growth_rate(StruphyArray(values, dims=("t",), coords={"t": t})) - assert window.start >= 5 - assert gamma == pytest.approx(0.3) - - -def test_growth_rate_gives_up_cleanly_on_degenerate_input(): - t = np.linspace(0, 1, 4) - y = StruphyArray(-np.ones(4), dims=("t",), coords={"t": t}) - assert growth_rate(y) == (None, None, None) - - -# ---------------------------------------------------------------- grid helpers - - -def test_match_to_grid_transposes_when_that_is_what_fits(): - grid = np.zeros((3, 4)) - np.testing.assert_allclose(match_to_grid(np.zeros((4, 3)), grid).shape, (3, 4)) - np.testing.assert_allclose(match_to_grid(np.zeros((3, 4)), grid).shape, (3, 4)) - - -def test_match_to_grid_rejects_an_incompatible_shape(): - with pytest.raises(ValueError, match="cannot match"): - match_to_grid(np.zeros((5, 9)), np.zeros((3, 4))) - - -def test_logical_grids_uses_the_non_time_dims(): - """Phase-space slices are (e1, v1), not two ``e`` axes.""" - xgrid, ygrid, xlabel, ylabel = logical_grids(phase_space().isel(t=0)) - assert xgrid.shape == (6, 8) - assert xlabel == r"$\eta_1$" - assert ylabel == "$v_1$" - - -def test_logical_grids_needs_exactly_two_plotted_dims(): - three_d = StruphyArray(np.zeros((2, 3, 4, 5)), dims=("t", "e1", "e2", "e3")) - with pytest.raises(ValueError, match="two non-time dims"): - logical_grids(three_d) - - -@pytest.mark.parametrize("plane", sorted(PLANES)) -def test_field_slice_grids_returns_each_plane(plane): - xgrid, ygrid, xlabel, ylabel = field_slice_grids(meshgrids(), fixed_dim="e3", index=0, plane=plane) - assert xgrid.shape == ygrid.shape == (6, 7) - assert (xlabel, ylabel) == (PLANES[plane][2], PLANES[plane][3]) - - -@pytest.mark.parametrize( - "fixed_dim, expected", - [("e1", (7, 5)), ("e2", (6, 5)), ("e3", (6, 7))], -) -def test_field_slice_grids_slices_the_named_axis(fixed_dim, expected): - """Regression: the copy-pasted versions of this disagreed on which axis to cut. - - ``pproc_cyclone`` cut ``arr[:, index, :]`` for the same case where - ``pproc_drift_kinetic`` cut ``arr[:, :, index]``, so one of the two silently - plotted the wrong slice. - """ - xgrid, _, _, _ = field_slice_grids(meshgrids(), fixed_dim=fixed_dim, index=0, plane="XY") - assert xgrid.shape == expected - - -@pytest.mark.parametrize("fixed_dim", ["e1", "e2", "e3"]) -def test_field_slice_grids_agrees_with_isel(fixed_dim): - """The grid and the field must be cut on the same axis, by construction.""" - grids = meshgrids() - field = StruphyArray( - np.random.default_rng(1).random((2, 6, 7, 5)), - dims=("t", "e1", "e2", "e3"), - ) - sliced = field.isel(t=0, **{fixed_dim: 1}) - xgrid, _, _, _ = field_slice_grids(grids, fixed_dim=fixed_dim, index=1, plane="XY") - assert sliced.shape == xgrid.shape - - -def test_field_slice_grids_rejects_unknown_inputs(): - with pytest.raises(ValueError, match="unknown plane"): - field_slice_grids(meshgrids(), plane="QQ") - with pytest.raises(ValueError, match="fixed_dim"): - field_slice_grids(meshgrids(), fixed_dim="e9") - - -# ---------------------------------------------------------------- plotters - - -def test_time_series_labels_axes_from_the_data(): - t = np.linspace(0, 10, 40) - y = StruphyArray(np.exp(0.3 * t), dims=("t",), coords={"t": t}, label="energy").with_coord_units(t="s") - - plot = TimeSeriesPlot(y, fit=True, title="Energy").plot() - assert plot.ax.get_xlabel() == "$t$ [s]" - assert plot.ax.get_ylabel() == "energy [a.u.]" - assert plot.ax.get_yscale() == "log" - assert plot.fit_results[0][0] == pytest.approx(0.3) - - -def test_time_series_fits_every_series(): - """Comparing runs means each curve gets its own rate, not just the first.""" - t = np.linspace(0, 10, 60) - series = [StruphyArray(np.exp(rate * t), dims=("t",), coords={"t": t}, label=f"run {rate}") for rate in (0.2, 0.4)] - plot = TimeSeriesPlot(series, fit=True).plot() - assert [f[0] for f in plot.fit_results] == pytest.approx([0.2, 0.4]) - - -def test_time_series_draws_every_series(): - t = np.linspace(0, 1, 10) - a = StruphyArray(np.ones(10), dims=("t",), coords={"t": t}, label="a") - b = StruphyArray(np.ones(10) * 2, dims=("t",), coords={"t": t}, label="b") - assert len(TimeSeriesPlot([a, b], fit=False).plot().ax.get_lines()) == 2 - - -def test_slice_2d_draws_into_a_supplied_axes(): - fig, ax = plt.subplots() - plot = Slice2DPlot(phase_space().isel(t=0), ax=ax).plot() - assert plot.ax is ax - assert plot.mesh is not None - - -def test_panel_grid_spreads_panels_over_the_run(): - plot = PanelGridPlot(phase_space(), nrows=2, ncols=3, shared_clim=True).plot() - axes = plot.ax.ravel() - assert len(axes) == 6 - # first and last panel are the first and last time step - assert axes[0].get_title().endswith("0.00e+00") - assert axes[-1].get_title().endswith("1.00e+00") - - -def test_panel_grid_shares_the_colour_range_when_asked(): - plot = PanelGridPlot(phase_space(), nrows=1, ncols=2, shared_clim=True).plot() - clims = {tuple(c.get_clim()) for ax in plot.ax.ravel() for c in ax.collections} - assert len(clims) == 1 - - -def test_slider_plot_adds_a_second_slider_for_a_free_axis(): - two_d = phase_space() - assert len(SliderPlot(two_d).plot().sliders) == 1 - - three_d = StruphyArray(np.zeros((4, 5, 6, 7)), dims=("t", "e1", "e2", "e3")) - plot = SliderPlot(three_d).plot() - assert plot.slice_dim == "e3" - assert len(plot.sliders) == 2 - - -def test_slider_grids_may_follow_the_cut(): - """A physical grid that depends on where the cut is taken must not go stale.""" - field = StruphyArray(np.zeros((3, 6, 7, 5)), dims=("t", "e1", "e2", "e3")) - asked = [] - - def grids(index): - asked.append(index) - return field_slice_grids(meshgrids(), fixed_dim="e3", index=index, plane="XY") +def phase_space(nt=6): + return data_array(np.arange(nt*4*5).reshape(nt, 4, 5), ("t", "e1", "v1"), + {"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, 4), + "v1": np.linspace(-2, 2, 5)}, name="f", label="$f$", coord_units={"t": "s"}) - plot = SliderPlot(field, grids=grids, slice_dim="e3").plot() - assert plot.slice_dim == "e3" - # built at the initial cut, and re-queried when the slider moves - assert asked == [2] - plot.sliders[1].set_val(4) - assert asked[-1] == 4 +def physical_field(): + coords = {"t": [0, 1], "e1": range(3), "e2": range(4), "e3": range(5)} + grids = np.meshgrid(coords["e1"], coords["e2"], coords["e3"], indexing="ij") + coords.update({name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), grids)}) + return data_array(np.ones((2, 3, 4, 5)), ("t", "e1", "e2", "e3"), coords, name="phi") -def test_slider_time_updates_the_title(): - field = StruphyArray(np.zeros((3, 6, 7, 5)), dims=("t", "e1", "e2", "e3")) - grids = field_slice_grids(meshgrids(), fixed_dim="e3", index=0, plane="XY") - plot = SliderPlot(field, grids=grids, slice_dim="e3", title="phi").plot() +def scalar_dataset(): + t = np.linspace(0, 1, 6) + return xr.Dataset({"en_tot": ("t", 2 + .02*t), "en_e": ("t", 1 + .1*t)}, coords={"t": t}) - plot.sliders[0].set_val(2) - assert plot.ax.get_title() == "phi at t = 2.0000e+00" +def test_growth_rate_uses_only_valid_samples_inside_window(): + data = data_array([1, 0, 4, np.nan, 16], ("t",), {"t": range(5)}) + result = growth_rate(data, GrowthFit((0, 4))) + assert result is not None and np.isfinite(result.rate) + np.testing.assert_array_equal(result.time, [0, 2, 4]) -def test_marker_trajectory_handles_a_species_without_weights(): - with_weight = wrap_orbits(np.random.default_rng(2).random((5, 20, 8)), np.arange(5.0)) - assert MarkerTrajectoryPlot(with_weight, max_markers=4).plot().fig is not None - without_weight = wrap_orbits(np.random.default_rng(2).random((5, 20, 5)), np.arange(5.0)) - assert MarkerTrajectoryPlot(without_weight, max_markers=4).plot().fig is not None +def test_growth_rate_does_not_fall_back_outside_requested_window(): + data = data_array(np.exp(np.arange(5)), ("t",), {"t": range(5)}) + assert growth_rate(data, GrowthFit((1.1, 1.2))) is None -# ---------------------------------------------------------------- PlottingData accessor +def test_growth_rate_of_quadratic_reports_amplitude_rate(): + t = np.linspace(0, 4, 20) + result = growth_rate(data_array(np.exp(.6*t), ("t",), {"t": t}), + GrowthFit(amplitude_from_quadratic=True)) + assert result.rate == pytest.approx(.3) -@pytest.fixture -def plot_accessor(): - data = SimpleNamespace( - params=object(), - scalars=scalars(), - orbits={"ions": wrap_orbits(np.random.default_rng(2).random((5, 20, 8)), np.arange(5.0))}, - ) - return PlottingAccessor(data) +def test_diagnostics_preserve_time_coordinates(): + data = data_array([2, 2.2, 1.8], ("t",), {"t": [0, 1, 2]}, label="E") + np.testing.assert_allclose(drift(data), [0, .2, -.2]) + np.testing.assert_allclose(relative_error(data), [.1, .1]) + np.testing.assert_array_equal(relative_error(data).t, [1, 2]) -def test_plot_accessor_draws_scalars_with_run_context(plot_accessor): - plot = plot_accessor.scalars(error_panel="en_tot") +def test_logical_and_physical_grids_follow_selected_dimensions(): + logical = phase_space().isel(t=0) + assert logical_grids(logical)[0].shape == (4, 5) + physical = physical_field().isel(t=0, e3=2) + assert physical_grids(physical, plane="XY")[0].shape == (3, 4) + assert physical_grids(physical, plane="RZ")[0].shape == (3, 4) - assert isinstance(plot, ScalarsPlot) - assert plot.fig is not None - assert plot.params is plot_accessor.data.params +def test_plot_timeseries_renders_once_and_save_does_not_redraw(tmp_path): + data = data_array(np.exp(np.arange(4)), ("t",), {"t": range(4)}, label="energy", + coord_units={"t": "s"}) + result = plot_timeseries(data, fit=GrowthFit(), run_label="dt=.1") + lines = len(result.ax.lines) + result.save(tmp_path / "energy.png") + assert len(result.ax.lines) == lines + assert len(plt.get_fignums()) == 1 + assert result.fig._suptitle.get_text() == "dt=.1" -def test_plot_accessor_resolves_scalar_names(plot_accessor): - plot = plot_accessor.time_series(["en_e", "en_b"], logy=False) - - assert isinstance(plot, TimeSeriesPlot) - assert [series.label for series in plot.series] == ["en e", "en b"] - assert len(plot.ax.get_lines()) == 2 - - -def test_plot_accessor_draws_2d_views(plot_accessor): - data = phase_space() - - assert isinstance(plot_accessor.slice(data.isel(t=0)), Slice2DPlot) - assert isinstance(plot_accessor.panels(data, nrows=1, ncols=2), PanelGridPlot) - assert isinstance(plot_accessor.slider(data), SliderPlot) - assert isinstance(plot_accessor.animation(data), AnimationPlot) - - -def test_plot_accessor_resolves_orbit_species(plot_accessor): - plot = plot_accessor.orbits("ions", max_markers=4) - - assert isinstance(plot, MarkerTrajectoryPlot) - assert plot.data is plot_accessor.data.orbits["ions"] - - -def test_animation_writes_one_frame_per_step(tmp_path): - plot = AnimationPlot(phase_space(nt=10), step=3) - assert list(plot.frames) == [0, 3, 6, 9] - - paths = plot.save_frames(tmp_path) - assert len(paths) == 4 - assert all(p.exists() for p in map(__import__("pathlib").Path, paths)) - - -def test_animation_builds_a_matplotlib_animation(): - anim = AnimationPlot(phase_space(nt=6), step=2).animate() - assert len(list(anim.new_frame_seq())) == 3 - - -def test_save_writes_a_file(tmp_path): - out = tmp_path / "fig.png" - TimeSeriesPlot( - StruphyArray(np.arange(1.0, 5.0), dims=("t",), coords={"t": np.arange(4.0)}), - fit=False, - ).save(out) - assert out.exists() and out.stat().st_size > 0 - - -# ---------------------------------------------------------------- conservation - - -def scalars(nt=6): - """The shape of ``PlottingData.scalars``, with a drifting total energy.""" - t = np.linspace(0.0, 1.0, nt) - return { - "en_tot": StruphyArray(2.0 + 0.02 * t, dims=("t",), coords={"t": t}, label="en tot"), - "en_e": StruphyArray(np.linspace(1.0, 1.5, nt), dims=("t",), coords={"t": t}, label="en e"), - "en_b": StruphyArray(np.linspace(1.0, 0.5, nt), dims=("t",), coords={"t": t}, label="en b"), - "time": StruphyArray(t, dims=("t",), coords={"t": t}), - } - - -def test_relative_error_is_measured_against_the_first_sample(): - t = np.linspace(0, 1, 5) - y = StruphyArray(np.array([2.0, 2.0, 2.2, 2.0, 1.8]), dims=("t",), coords={"t": t}, label="E") - - err = relative_error(y) - # t = 0 is dropped, where the error is identically zero and unplottable on a log axis - assert err.shape == (4,) - np.testing.assert_allclose(np.asarray(err), [0.0, 0.1, 0.0, 0.1], atol=1e-12) - np.testing.assert_allclose(err.coord("t"), t[1:]) - - -def test_relative_error_takes_an_explicit_reference(): - y = StruphyArray(np.array([2.0, 3.0]), dims=("t",), coords={"t": np.arange(2.0)}) - np.testing.assert_allclose(np.asarray(relative_error(y, ref=1.0, skip_first=False)), [1.0, 2.0]) - - -def test_relative_error_refuses_a_zero_reference(): - y = StruphyArray(np.zeros(3), dims=("t",), coords={"t": np.arange(3.0)}) - with pytest.raises(ValueError, match="reference of zero"): - relative_error(y) - - -def test_drift_is_the_signed_deviation(): - y = StruphyArray(np.array([2.0, 2.5, 1.0]), dims=("t",), coords={"t": np.arange(3.0)}, label="E") - d = drift(y) - np.testing.assert_allclose(np.asarray(d), [0.0, 0.5, -1.0]) - assert d.dims == ("t",) - - -# ---------------------------------------------------------------- scalars plot - - -def test_scalars_plot_draws_every_scalar_but_the_excluded(): - plot = ScalarsPlot(scalars()).plot() - labels = [line.get_label() for line in plot.ax.get_lines()] - assert labels == ["en_tot", "en_e", "en_b"] - - -def test_scalars_plot_adds_the_conservation_panel(): - plot = ScalarsPlot(scalars()).plot() - - assert plot.error_ax is not None - assert plot.error_ax.get_yscale() == "log" - # en_tot drifts by 1% of its initial value over the run - assert float(np.asarray(plot.error)[-1]) == pytest.approx(0.01) - - -def test_scalars_plot_without_a_conserved_quantity_has_no_panel(): - """Not every model tracks ``en_tot``; the overview must still work.""" - without = {k: v for k, v in scalars().items() if k != "en_tot"} - plot = ScalarsPlot(without).plot() - assert plot.error_ax is None and plot.error is None - assert plot.ax.get_xlabel() == "$t$" - - -def test_scalars_plot_can_normalize_the_mixed_units_away(): - plot = ScalarsPlot(scalars(), relative_to="en_tot").plot() - assert plot.ax.get_ylabel() == "quantity / en_tot" - # en_tot against itself is one everywhere - np.testing.assert_allclose(plot.ax.get_lines()[0].get_ydata(), 1.0) - - -def test_scalars_plot_labels_the_shared_unit(): - t = np.linspace(0, 1, 4) - joules = {n: StruphyArray(np.ones(4), dims=("t",), coords={"t": t}, unit="J") for n in ("en_e", "en_b")} - assert ScalarsPlot(joules, error_panel=None).plot().ax.get_ylabel() == "[J]" - - -def test_scalars_plot_honours_a_supplied_axes(): - fig, ax = plt.subplots() - plot = ScalarsPlot(scalars(), ax=ax).plot() - assert plot.ax is ax - assert plot.error_ax is None # a panel cannot be added to someone else's axes - - -def test_scalars_plot_needs_something_to_plot(): - with pytest.raises(ValueError, match="no scalars to plot"): - ScalarsPlot({"time": StruphyArray(np.zeros(3), dims=("t",))}) - - -# ---------------------------------------------------------------- saving - - -def test_save_all_scalars_writes_the_table_and_one_figure_each(tmp_path): - paths = save_all_scalars(scalars(), str(tmp_path)) - names = sorted(os.path.basename(p) for p in paths) - - assert names == ["en_b.png", "en_e.png", "en_tot.png", "scalars.csv", "scalars.png"] - assert all(os.path.getsize(p) > 0 for p in paths) - - -def test_save_all_scalars_closes_its_figures(tmp_path): - """Saving a run's worth of scalars must not leave every figure open.""" - save_all_scalars(scalars(), str(tmp_path)) - assert plt.get_fignums() == [] - - -def test_save_all_scalars_can_skip_the_table(tmp_path): - paths = save_all_scalars(scalars(), str(tmp_path), table=None) - assert not any(p.endswith(".csv") for p in paths) - - -def test_save_all_scalars_of_nothing_writes_nothing(tmp_path): - assert save_all_scalars({}, str(tmp_path)) == [] - - -def test_save_closes_the_figure_only_when_asked(tmp_path): - y = StruphyArray(np.arange(1.0, 5.0), dims=("t",), coords={"t": np.arange(4.0)}) - - plot = TimeSeriesPlot(y, fit=False).save(tmp_path / "a.png") - assert plot.fig is not None and plt.get_fignums() != [] - plt.close("all") - plot = TimeSeriesPlot(y, fit=False).save(tmp_path / "b.png", close=True) - assert plot.fig is None and plt.get_fignums() == [] +def test_plot_slice_accepts_named_value_and_index_selection(): + result = plot_slice(phase_space(), view=View(x="e1", y="v1", select={"t": .52})) + assert result.ax.get_xlabel() == r"$\eta_1$" + assert len(result.artists) == 1 -def test_save_does_not_close_an_axes_it_was_given(tmp_path): - fig, ax = plt.subplots() - y = StruphyArray(np.arange(1.0, 5.0), dims=("t",), coords={"t": np.arange(4.0)}) - TimeSeriesPlot(y, fit=False, ax=ax).save(tmp_path / "c.png", close=True) - assert plt.get_fignums() == [fig.number] +def test_plot_slice_physical_coordinates_are_intrinsic(): + result = plot_slice(physical_field(), view=View(x="e1", y="e2", isel={"t": 0, "e3": 2}, + coordinates="physical", plane="XY")) + assert result.ax.get_xlabel() == "X" + assert result.ax.get_aspect() == 1.0 -def test_slider_plot_writes_frames_at_the_current_cut(tmp_path): - field = StruphyArray( - np.arange(3 * 6 * 7 * 5, dtype=float).reshape(3, 6, 7, 5), - dims=("t", "e1", "e2", "e3"), - coords={"t": np.linspace(0, 1, 3)}, - ) - plot = SliderPlot(field, slice_dim="e3", slice_index=1) +def test_plot_slice_rejects_underspecified_selection(): + with pytest.raises(ValueError, match="selection leaves"): + plot_slice(physical_field(), view=View(x="e1", y="e2", isel={"t": 0})) - paths = plot.save_frames(tmp_path, prefix="phi") - assert [os.path.basename(p) for p in paths] == ["phi_0000.png", "phi_0001.png", "phi_0002.png"] - assert plt.get_fignums() == [] +def test_panels_use_one_recipe_and_keep_full_title(): + result = plot_panels(phase_space(), view=View(x="e1", y="v1"), nrows=1, ncols=2, + title="Distribution", run_label="dt=.1") + assert result.fig._suptitle.get_text() == "Distribution — dt=.1" + assert len(result.artists) == 2 -def test_slider_frames_follow_the_slider(tmp_path): - """The cut found interactively is the one that gets written out.""" - field = StruphyArray(np.zeros((2, 4, 4, 5)), dims=("t", "e1", "e2", "e3")) - plot = SliderPlot(field, slice_dim="e3").plot() - assert plot.slice_index == 2 - plot.sliders[1].set_val(4) - assert plot.slice_index == 4 +def test_viewer_builds_controls_for_every_non_display_dimension(): + viewer = InteractiveSliceViewer(physical_field(), view=View(x="e1", y="e2", coordinates="physical")) + result = viewer.draw() + assert set(viewer.sliders) == {"t", "e3"} + viewer.sliders["e3"].set_val(3) + assert result.fig is not None -def test_slider_plot_steps_through_time(tmp_path): - plot = SliderPlot(phase_space(nt=10), step=4) - assert list(plot.frames) == [0, 4, 8] - assert len(plot.save_frames(tmp_path)) == 3 +def test_animation_and_frames_share_the_view(tmp_path): + data = phase_space(nt=7) + view = View(x="e1", y="v1") + animation = animate_slices(data, view=view, step=3) + assert len(list(animation.new_frame_seq())) == 3 + paths = save_frames(data, tmp_path, view=view, step=3) + assert len(paths) == 3 + assert all(__import__("pathlib").Path(path).exists() for path in paths) -def test_scalars_plot_keeps_a_linear_error_axis_when_nothing_drifts(): - """A short run can conserve exactly, which a log axis cannot draw.""" - t = np.linspace(0, 1, 4) - exact = { - "en_tot": StruphyArray(np.full(4, 2.0), dims=("t",), coords={"t": t}), - "en_e": StruphyArray(np.ones(4), dims=("t",), coords={"t": t}), - } - plot = ScalarsPlot(exact).plot() - assert plot.error_ax.get_yscale() == "linear" +def test_scalar_overview_and_export(tmp_path): + result, error = plot_scalars(scalar_dataset(), run_label="run") + assert error is not None + assert result.fig._suptitle.get_text() == "run" + paths = save_all_scalars(scalar_dataset(), tmp_path) + assert sorted(__import__("os").path.basename(path) for path in paths) == [ + "en_e.png", "en_tot.png", "scalars.csv", "scalars.png" + ] + assert plt.get_fignums() == [result.fig.number] diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 55fbc2c83..be4518fe5 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -1,451 +1,170 @@ -"""Labeled arrays for post-processed Struphy output data.""" +"""Labeled post-processing arrays built on :mod:`xarray`.""" + +from __future__ import annotations import logging import os -from dataclasses import dataclass, field, replace +from collections.abc import Mapping, Sequence -import cunumpy as xp +import numpy as np +import xarray as xr logger = logging.getLogger("struphy") -#: LaTeX display labels for the dimension names used across Struphy output. DIM_LABELS = { - "t": r"$t$", - "e1": r"$\eta_1$", - "e2": r"$\eta_2$", - "e3": r"$\eta_3$", - "v1": r"$v_1$", - "v2": r"$v_2$", - "v3": r"$v_3$", - "x": r"$x$", - "y": r"$y$", - "z": r"$z$", - "R": r"$R$", - "Z": r"$Z$", - "comp": "component", - "marker": "marker", -} - -#: Display labels for the binned quantities written by the post-processor. -BINNED_LABELS = { - "f_binned": "$f$", - "delta_f_binned": r"$\delta f$", - "n_sph": "$n$", -} - -#: Physical unit attribute on ``Units`` that each dimension is measured in. -DIM_UNITS = { - "t": "t", - "e1": None, - "e2": None, - "e3": None, - "v1": "v", - "v2": "v", - "v3": "v", - "x": "x", - "y": "x", - "z": "x", - "R": "x", - "Z": "x", + "t": r"$t$", "e1": r"$\eta_1$", "e2": r"$\eta_2$", "e3": r"$\eta_3$", + "v1": r"$v_1$", "v2": r"$v_2$", "v3": r"$v_3$", + "x": r"$x$", "y": r"$y$", "z": r"$z$", "R": r"$R$", "Z": r"$Z$", + "component": "component", "marker": "marker", "attribute": "attribute", } - - -#: Names in the ``scalar`` group that are not physics quantities. +BINNED_LABELS = {"f_binned": "$f$", "delta_f_binned": r"$\delta f$", "n_sph": "$n$"} SCALARS_EXCLUDE = ("time",) -@dataclass -class StruphyArray: - """Array of simulation output together with its dimension names, coordinates and unit. - - Passing an instance to ``xp.asarray`` or to a matplotlib call yields ``values``, - so it can be used anywhere a plain array is expected. - - Parameters - ---------- - values : xp.ndarray - The data. Its rank must match the length of ``dims``. - dims : tuple of str - Name of each axis, e.g. ``("t", "e1", "v1")``. - coords : dict - Maps a dimension name to its 1D coordinate array. Dimensions may be absent, - in which case they are indexed by position only. - unit : str - Physical unit of ``values``, for axis labels. Empty means arbitrary units. - label : str - Display name of the quantity, e.g. ``r"$f$"``. - """ - - values: xp.ndarray - dims: tuple[str, ...] - coords: dict[str, xp.ndarray] = field(default_factory=dict) - unit: str = "" - label: str = "" - - def __post_init__(self): - self.values = xp.asarray(self.values) - self.dims = tuple(self.dims) - - if self.values.ndim != len(self.dims): - raise ValueError(f"values has rank {self.values.ndim} but {len(self.dims)} dims were given: {self.dims}") - - self.coords = {k: xp.asarray(v) for k, v in self.coords.items()} - for name, c in self.coords.items(): - if name not in self.dims: - raise ValueError(f"coord {name!r} is not one of the dims {self.dims}") - if c.shape != (self.values.shape[self.axis(name)],): - raise ValueError( - f"coord {name!r} has shape {c.shape}, expected ({self.values.shape[self.axis(name)]},)" - ) - - def __array__(self, dtype=None): - return xp.asarray(self.values, dtype=dtype) - - def __len__(self): - return len(self.values) - - def __getitem__(self, key): - """Positional indexing, returning a plain array. - - Kept so that code written against the unlabeled arrays still works; use - :meth:`isel` or :meth:`at` to index by dimension name and keep the labels. - """ - return self.values[key] - - @property - def shape(self): - return self.values.shape - - @property - def ndim(self): - return self.values.ndim - - def axis(self, dim: str) -> int: - """Position of ``dim`` in ``dims``.""" - if dim not in self.dims: - raise KeyError(f"no dim {dim!r} in {self.dims}") - return self.dims.index(dim) - - def coord(self, dim: str) -> xp.ndarray: - """Coordinate array of ``dim``, falling back to an integer index range.""" - if dim in self.coords: - return self.coords[dim] - return xp.arange(self.shape[self.axis(dim)]) - - def axis_label(self, dim: str) -> str: - """Axis label for ``dim``, including its unit where one is known.""" - base = DIM_LABELS.get(dim, dim) - unit = self.coord_units.get(dim, "") - return f"{base} [{unit}]" if unit else base - - @property - def value_label(self) -> str: - """Axis or colorbar label for the values themselves.""" - base = self.label or "" - unit = self.unit or "a.u." - return f"{base} [{unit}]" if base else f"[{unit}]" - - @property - def coord_units(self) -> dict[str, str]: - """Unit string per dimension, populated by the loader where units are known.""" - return getattr(self, "_coord_units", {}) - - def with_coord_units(self, **units: str) -> "StruphyArray": - """Attach unit strings to dimensions, for axis labels.""" - out = replace(self) - out._coord_units = {**self.coord_units, **units} - return out - - def isel(self, **sel: int) -> "StruphyArray": - """Select by integer index along named dimensions. - - A dimension indexed with an ``int`` is dropped; one indexed with a ``slice`` - is kept. - """ - idx = [slice(None)] * self.ndim - for dim, i in sel.items(): - idx[self.axis(dim)] = i - - dropped = {dim for dim, i in sel.items() if not isinstance(i, slice)} - new_dims = tuple(d for d in self.dims if d not in dropped) - new_coords = { - d: (self.coords[d][sel[d]] if d in sel else self.coords[d]) for d in self.coords if d not in dropped - } - - out = StruphyArray( - self.values[tuple(idx)], - new_dims, - new_coords, - self.unit, - self.label, - ) - out._coord_units = dict(self.coord_units) - return out - - def at(self, **sel: float) -> "StruphyArray": - """Select the nearest coordinate value along named dimensions. - - Replaces the ``xp.abs(t_grid - t).argmin()`` idiom. - """ - return self.isel(**{dim: int(xp.abs(self.coord(dim) - v).argmin()) for dim, v in sel.items()}) +def data_array(values, dims: Sequence[str], coords: Mapping | None = None, *, name: str | None = None, + label: str = "", unit: str = "", coord_units: Mapping[str, str] | None = None, + attrs: Mapping | None = None) -> xr.DataArray: + """Construct a consistently annotated :class:`xarray.DataArray`.""" + metadata = dict(attrs or {}) + metadata.update(label=label, units=unit) + out = xr.DataArray(values, dims=tuple(dims), coords=coords, name=name, attrs=metadata) + for dim, value in (coord_units or {}).items(): + if dim in out.coords: + out.coords[dim].attrs["units"] = value + return validate_array(out) + + +def validate_array(data: xr.DataArray, *, required_dims: Sequence[str] = ()) -> xr.DataArray: + """Validate the inexpensive invariants diagnostics rely on.""" + if not isinstance(data, xr.DataArray): + raise TypeError(f"expected xarray.DataArray, got {type(data).__name__}") + missing = tuple(dim for dim in required_dims if dim not in data.dims) + if missing: + raise ValueError(f"missing dimensions {missing}; available dimensions are {data.dims}") + for dim in data.dims: + if dim not in data.coords or data.coords[dim].ndim != 1: + continue + coord = np.asarray(data.coords[dim]) + if len(coord) > 1 and np.issubdtype(coord.dtype, np.number): + delta = np.diff(coord) + if not (np.all(delta > 0) or np.all(delta < 0)): + raise ValueError(f"coordinate {dim!r} must be strictly monotonic") + return data - def transpose_to(self, *dims: str) -> "StruphyArray": - """Reorder axes to the given dimension order.""" - if set(dims) != set(self.dims): - raise ValueError(f"cannot transpose dims {self.dims} to {dims}") - out = StruphyArray( - xp.transpose(self.values, [self.axis(d) for d in dims]), - dims, - self.coords, - self.unit, - self.label, - ) - out._coord_units = dict(self.coord_units) - return out +def axis_label(data: xr.DataArray, dim: str) -> str: + """Human-readable coordinate label, including a unit when available.""" + if dim not in data.dims: + raise KeyError(f"dimension {dim!r} not found in {data.dims}") + coord = data.coords.get(dim) + label = ("" if coord is None else coord.attrs.get("long_name", "")) or DIM_LABELS.get(dim, dim) + unit = "" if coord is None else coord.attrs.get("units", "") + return f"{label} [{unit}]" if unit else label - def __repr__(self): - dims = ", ".join(f"{d}: {n}" for d, n in zip(self.dims, self.shape)) - name = self.label or "StruphyArray" - return f"<{name} ({dims}) [{self.unit or 'a.u.'}]>" +def value_label(data: xr.DataArray) -> str: + """Human-readable value label, including the value unit.""" + label = data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" + unit = data.attrs.get("units", "") or "a.u." + return f"{label} [{unit}]" if label else f"[{unit}]" -def scalar_names(scalars, *, names=None, exclude=SCALARS_EXCLUDE) -> list[str]: - """Names of the scalar time series to work with, in a stable order. - Parameters - ---------- - scalars : Scalars or dict - Anything with ``keys()`` and ``[]`` returning a :class:`StruphyArray` over ``t``. - names : sequence of str, optional - Restrict to these, in the given order. Unknown names raise. - exclude : sequence of str - Dropped when ``names`` is not given. - """ +def scalar_names(scalars: xr.Dataset | Mapping, *, names=None, exclude=SCALARS_EXCLUDE) -> list[str]: + available = tuple(scalars.data_vars if isinstance(scalars, xr.Dataset) else scalars.keys()) if names is not None: - missing = [n for n in names if n not in scalars] + missing = [name for name in names if name not in available] if missing: - raise KeyError(f"no scalars {missing}, available: {tuple(scalars.keys())}") + raise KeyError(f"no scalars {missing}, available: {available}") return list(names) - return [n for n in scalars.keys() if n not in exclude] + return [name for name in available if name not in exclude] -def scalars_table(scalars, *, names=None, exclude=SCALARS_EXCLUDE): - """Stack the scalar time series into one table, rows being time steps. - - This is the per-step record of the run in the form it is usually wanted in: - one time column and one column per scalar. - - Parameters - ---------- - scalars : Scalars or dict - Maps a name to a :class:`StruphyArray` over ``t``. - names, exclude - See :func:`scalar_names`. - - Returns - ------- - t, names, values : xp.ndarray, list of str, xp.ndarray - ``values`` has shape ``(len(t), len(names))``. Series whose length does not - match the time coordinate are dropped, since they cannot share the table. - """ +def scalars_table(scalars: xr.Dataset | Mapping, *, names=None, exclude=SCALARS_EXCLUDE): + """Return ``(time, names, values)`` for scalar export.""" selected = scalar_names(scalars, names=names, exclude=exclude) if not selected: - return xp.zeros(0), [], xp.zeros((0, 0)) - - t = xp.asarray(scalars[selected[0]].coord("t")) - - kept, columns = [], [] - for name in selected: - values = xp.asarray(scalars[name]) - if values.shape != t.shape: - logger.warning(f"Scalar {name!r} has shape {values.shape}, expected {t.shape}; excluded from the table.") - continue - kept.append(name) - columns.append(values) - - return t, kept, xp.stack(columns, axis=1) - - -def save_scalars(scalars, path: str, *, names=None, exclude=SCALARS_EXCLUDE, fmt: str = None) -> str: - """Write every scalar time series, at every time step, to one file. - - Parameters - ---------- - scalars : Scalars or dict - Maps a name to a :class:`StruphyArray` over ``t``. - path : str - Destination. The format is taken from its suffix unless ``fmt`` is given. - names, exclude - See :func:`scalar_names`. - fmt : str - ``"csv"`` (a header row of ``t`` and the scalar names) or ``"npz"`` (one - array per name plus ``t``). - - Returns - ------- - str - The path written. - """ - t, names_out, values = scalars_table(scalars, names=names, exclude=exclude) - - if fmt is None: - fmt = os.path.splitext(path)[1].lstrip(".").lower() or "csv" - if fmt not in ("csv", "npz"): + return np.zeros(0), [], np.zeros((0, 0)) + arrays = [scalars[name] for name in selected] + for array in arrays: + validate_array(array, required_dims=("t",)) + if array.dims != ("t",): + raise ValueError(f"scalar {array.name!r} must have only the 't' dimension, got {array.dims}") + aligned = xr.align(*arrays, join="exact") + return np.asarray(aligned[0].coords["t"]), selected, np.column_stack([np.asarray(a) for a in aligned]) + + +def save_scalars(scalars: xr.Dataset | Mapping, path: str, *, names=None, exclude=SCALARS_EXCLUDE, fmt=None) -> str: + """Write selected scalar time series to CSV or NPZ.""" + time, selected, values = scalars_table(scalars, names=names, exclude=exclude) + fmt = (fmt or os.path.splitext(path)[1].lstrip(".") or "csv").lower() + if fmt not in {"csv", "npz"}: raise ValueError(f"unknown format {fmt!r}, expected 'csv' or 'npz'") - - # savez appends the suffix itself, so the returned path would otherwise be wrong if fmt == "npz" and not path.endswith(".npz"): path += ".npz" - - directory = os.path.dirname(path) - if directory: - os.makedirs(directory, exist_ok=True) - + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) if fmt == "npz": - xp.savez(path, t=t, **{n: values[:, i] for i, n in enumerate(names_out)}) + np.savez(path, t=time, **{name: values[:, i] for i, name in enumerate(selected)}) else: - with open(path, "w") as f: - f.write(",".join(["t", *names_out]) + "\n") - for row in range(len(t)): - f.write(",".join(f"{float(v):.17g}" for v in (t[row], *values[row])) + "\n") - - logger.info(f"Wrote {len(names_out)} scalars over {len(t)} time steps to {path}") + np.savetxt(path, np.column_stack((time, values)), delimiter=",", + header=",".join(("t", *selected)), comments="") + logger.info("Wrote %d scalars over %d time steps to %s", len(selected), len(time), path) return path def orbit_columns(n_columns: int) -> dict: - """Meaning of each marker-orbit column for a given saved width. - - The post-processor saves a different set of marker columns depending on the - velocity dimension of the species, so the weight is not always at the same index. - Positions occupy the first three columns and the marker id the last in every case. - - Parameters - ---------- - n_columns : int - Size of the last axis of the orbit array. - - Returns - ------- - dict - Maps ``"position"``, ``"velocity"``, ``"weight"`` and ``"id"`` to an index or - slice. ``weight`` is absent when the species does not save one. - """ - cols = {"position": slice(0, 3), "id": n_columns - 1} + columns = {"position": slice(0, 3), "id": n_columns - 1} if n_columns == 8: - cols["velocity"] = slice(3, 6) - cols["weight"] = 6 + columns.update(velocity=slice(3, 6), weight=6) elif n_columns == 5: - cols["velocity"] = 3 + columns["velocity"] = 3 else: - cols["velocity"] = slice(3, n_columns - 1) - return cols - + columns["velocity"] = slice(3, n_columns - 1) + return columns -def wrap_orbits(values, t_grid) -> StruphyArray: - """Label a marker-orbit array with its ``(t, marker, attribute)`` dimensions.""" - values = xp.asarray(values) - out = StruphyArray( - values, - dims=("t", "marker", "attribute"), - coords={"t": t_grid}, - label="marker orbits", - ) - out.columns = orbit_columns(values.shape[-1]) - return out +def wrap_orbits(values, time, *, time_unit="") -> xr.DataArray: + """Label marker orbits with time, marker and attribute dimensions.""" + values = np.asarray(values) + return data_array(values, ("t", "marker", "attribute"), + {"t": time, "marker": np.arange(values.shape[1]), + "attribute": np.arange(values.shape[2])}, + name="orbits", label="marker orbits", coord_units={"t": time_unit}, + attrs={"columns": orbit_columns(values.shape[-1])}) -def wrap_field_data(data: dict, grids_log=None, *, label: str = "") -> StruphyArray: - """Stack a time-keyed dict of evaluated field components into one labeled array. - The post-processor stores evaluated fields as ``{time: [component, ...]}`` with each - component on the 3D evaluation grid. This turns that into a single array with - dimensions ``(t, comp, e1, e2, e3)``, dropping ``comp`` for scalar fields. - - Parameters - ---------- - data : dict - Maps time value to a list of component arrays, or to a single array. - grids_log : list, optional - The three 1D logical grids, attached as coordinates when given. - """ - times = sorted(data.keys()) +def wrap_field_data(values_by_time: Mapping, grids_log=None, *, grids_phy=None, name: str = "", + time_scale: float = 1.0, time_unit: str = "") -> xr.DataArray | None: + """Stack one field product and attach logical and physical coordinates.""" + times = sorted(values_by_time) if not times: return None - - first = data[times[0]] + first = values_by_time[times[0]] scalar = not isinstance(first, (list, tuple)) or len(first) == 1 - if scalar: - stacked = xp.stack( - [xp.asarray(data[t] if not isinstance(data[t], (list, tuple)) else data[t][0]) for t in times] - ) + values = np.stack([np.asarray(values_by_time[t] if not isinstance(values_by_time[t], (list, tuple)) + else values_by_time[t][0]) for t in times]) dims = ("t", "e1", "e2", "e3") else: - stacked = xp.stack([xp.stack([xp.asarray(c) for c in data[t]]) for t in times]) - dims = ("t", "comp", "e1", "e2", "e3") - - coords = {"t": xp.asarray(times)} + values = np.stack([np.stack([np.asarray(c) for c in values_by_time[t]]) for t in times]) + dims = ("t", "component", "e1", "e2", "e3") + coords: dict = {"t": np.asarray(times) * time_scale} + if "component" in dims: + coords["component"] = np.arange(values.shape[1]) if grids_log is not None: - coords.update({f"e{i + 1}": xp.asarray(g) for i, g in enumerate(grids_log)}) - - # a field evaluated on a subset of directions will not match the full grid - coords = {k: v for k, v in coords.items() if k in dims and len(v) == stacked.shape[dims.index(k)]} - - return StruphyArray(stacked, dims=dims, coords=coords, label=label) - - -def wrap_binned_slice(holder, slice_name: str, t_grid, coord_units: dict = None): - """Replace the binned arrays on ``holder`` with labeled ones. - - A binned slice folder is named after the dimensions it bins, e.g. ``e1_v1_density``, - and holds one ``grid_`` array per dimension alongside the binned quantities - (``f_binned``, ``delta_f_binned``, ...). This pairs them up, so the binned data - carries its coordinates and no caller has to match ``grid_e1`` to axis 0 by hand. - - Parameters - ---------- - holder : Slice - Container whose attributes were just populated from disk. Modified in place. - slice_name : str - Folder name, used to recover the dimension order. - t_grid : array - Time coordinate shared by every binned quantity. - """ - grids = {k[len("grid_") :]: getattr(holder, k) for k in list(vars(holder)) if k.startswith("grid_")} - if not grids: - return holder - - # dimension order follows the slice name, e.g. "e1_v1_density" -> ("e1", "v1") - dims = [part for part in slice_name.split("_") if part in grids] - if len(dims) != len(grids): - return holder - - coords = {d: grids[d] for d in dims} - coords["t"] = t_grid - units = coord_units or {} - - for name in list(vars(holder)): - if name.startswith("grid_"): - continue - values = getattr(holder, name) - if not hasattr(values, "shape"): - continue - - expected = (len(t_grid), *(len(coords[d]) for d in dims)) - if tuple(values.shape) != expected: - continue - - setattr( - holder, - name, - StruphyArray( - values, - dims=("t", *dims), - coords=coords, - label=BINNED_LABELS.get(name, name.replace("_", " ")), - ).with_coord_units(**units), - ) - - return holder + for i, grid in enumerate(grids_log, 1): + dim = f"e{i}" + if len(grid) == values.shape[dims.index(dim)]: + coords[dim] = np.asarray(grid) + spatial_shape = tuple(values.shape[dims.index(dim)] for dim in ("e1", "e2", "e3")) + if grids_phy is not None and all(np.asarray(grid).shape == spatial_shape for grid in grids_phy): + for coordinate, grid in zip(("X", "Y", "Z"), grids_phy): + coords[coordinate] = (("e1", "e2", "e3"), np.asarray(grid)) + return data_array(values, dims, coords, name=name or None, label=name, coord_units={"t": time_unit}) + + +def wrap_binned_data(values, dims: Sequence[str], coords: Mapping, *, name: str, + time_unit: str = "") -> xr.DataArray: + """Label a memory-mapped binned distribution or density product.""" + return data_array(values, ("t", *dims), coords, name=name, + label=BINNED_LABELS.get(name, name.replace("_", " ")), + coord_units={"t": time_unit}) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 3a4d37890..25d526f4a 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1,4 +1,5 @@ import inspect +import hashlib import json import logging import os @@ -27,9 +28,9 @@ from struphy.models.variables import PICVariable, SPHVariable from struphy.pic.base import Particles from struphy.post_processing.arrays import ( - StruphyArray, + data_array, save_scalars, - wrap_binned_slice, + wrap_binned_data, wrap_field_data, wrap_orbits, ) @@ -143,7 +144,7 @@ def __init__(self, data: dict, grids_log=None, name: str = ""): self._array = None @property - def array(self) -> StruphyArray: + def array(self): """The field as one labeled array with dims ``(t, comp, e1, e2, e3)``. Built on first access; ``data`` remains the raw time-keyed dict. @@ -348,8 +349,57 @@ def __init__( @property def is_processed(self) -> bool: - """Whether ``path_pproc`` already holds post-processed output.""" - return os.path.exists(self.path_pproc) and bool(os.listdir(self.path_pproc)) + """Whether a complete manifest matches the current raw run.""" + path = os.path.join(self.path_pproc, "manifest.json") + if not os.path.exists(path): + return False + try: + with open(path) as stream: + manifest = json.load(stream) + return (manifest.get("schema_version") == 1 + and manifest.get("status") == "complete" + and manifest.get("source_fingerprint") == self._source_fingerprint()) + except (OSError, ValueError): + return False + + def _source_fingerprint(self): + """Fingerprint inputs that determine post-processing products.""" + digest = hashlib.sha256() + for name in ("config.json", "parameters.py", "meta.yml", "data/data_proc0.hdf5"): + path = os.path.join(self.path_out, name) + if not os.path.exists(path): + continue + stat = os.stat(path) + digest.update(name.encode()) + digest.update(f"{stat.st_size}:{stat.st_mtime_ns}".encode()) + if name != "data/data_proc0.hdf5": + with open(path, "rb") as stream: + digest.update(stream.read()) + return digest.hexdigest() + + def _write_manifest(self, status, *, options=None, error=None): + if self.rank != 0: + return + manifest = { + "schema_version": 1, + "status": status, + "source_fingerprint": self._source_fingerprint(), + "options": options or {}, + } + if error is not None: + manifest["error"] = str(error) + if status == "complete": + manifest["products"] = sorted( + os.path.relpath(os.path.join(root, name), self.path_pproc) + for root, _, files in os.walk(self.path_pproc) + for name in files if name != "manifest.json" + ) + path = os.path.join(self.path_pproc, "manifest.json") + temporary = path + ".tmp" + with open(temporary, "w") as stream: + json.dump(manifest, stream, indent=2, sort_keys=True) + stream.write("\n") + os.replace(temporary, path) def _reset_pproc_dir(self): if self.rank == 0: @@ -366,7 +416,7 @@ def process( guiding_center: bool = False, classify: bool = False, create_vtk: bool = True, - force: bool = True, + force: bool = False, ): """Run post-processing for fields and particle data in ``self.path_out``. @@ -400,6 +450,9 @@ def process( return False self._reset_pproc_dir() + options = {"step": step, "celldivide": celldivide, "physical": physical, + "guiding_center": guiding_center, "classify": classify, "create_vtk": create_vtk} + self._write_manifest("processing", options=options) logger.warning(f"\nPost-processing path {self.path_out}") # check for fields and kinetic data in hdf5 file that need post processing @@ -434,19 +487,14 @@ def process( self.exist_particles = None # feec variables - self.process_fields( - step=step, - celldivide=celldivide, - physical=physical, - create_vtk=create_vtk, - ) + try: + self.process_fields(step=step, celldivide=celldivide, physical=physical, create_vtk=create_vtk) + self.process_particles(step=step, guiding_center=guiding_center, classify=classify) + except Exception as error: + self._write_manifest("failed", options=options, error=error) + raise - # particle variables - self.process_particles( - step=step, - guiding_center=guiding_center, - classify=classify, - ) + self._write_manifest("complete", options=options) return True @@ -1408,7 +1456,7 @@ def _post_process_n_sph( xp.save(os.path.join(path_view, "n_sph.npy"), data) -class PlottingData: +class LegacyPlottingData: """Container for loading and accessing post-processed Struphy simulation data. This class provides convenient access to field data (spline values), particle orbits, @@ -1620,12 +1668,14 @@ def load_scalars(self, *, physical_time: bool = True): return self._scalars t = xp.asarray(f["time"]["value"][()]) * unit_t for name in f["scalar"].keys(): - arr = StruphyArray( + arr = data_array( xp.asarray(f["scalar"][name][()]), dims=("t",), coords={"t": t}, + name=name, label=name.replace("_", " "), - ).with_coord_units(t=t_unit_label) + coord_units={"t": t_unit_label}, + ) setattr(self._scalars, name, arr) logger.info(f"Loaded scalars: {self._scalars.keys()}") @@ -1767,7 +1817,17 @@ def load(self): tmp = xp.load(os.path.join(path_dat, sli, file)) logger.info(f"{name = }") setattr(s, name, tmp) - wrap_binned_slice(s, sli, self.t_grid) + grids = {key.removeprefix("grid_"): getattr(s, key) for key in s.keys() + if key.startswith("grid_")} + dims = tuple(part for part in sli.split("_") if part in grids) + for name in tuple(s.keys()): + values = getattr(s, name) + if name.startswith("grid_") or not hasattr(values, "shape"): + continue + expected = (len(self.t_grid), *(len(grids[dim]) for dim in dims)) + if values.shape == expected: + setattr(s, name, wrap_binned_data(values, dims, + {"t": self.t_grid, **{dim: grids[dim] for dim in dims}}, name=name)) elif "n_sph" in folder: spec_holder = SpecHolder() @@ -1785,7 +1845,17 @@ def load(self): tmp = xp.load(os.path.join(path_dat, sli, file)) # logger.info(f"{name = }") setattr(s, name, tmp) - wrap_binned_slice(s, sli, self.t_grid) + grids = {key.removeprefix("grid_"): getattr(s, key) for key in s.keys() + if key.startswith("grid_")} + dims = tuple(part for part in sli.split("_") if part in grids) + for name in tuple(s.keys()): + values = getattr(s, name) + if name.startswith("grid_") or not hasattr(values, "shape"): + continue + expected = (len(self.t_grid), *(len(grids[dim]) for dim in dims)) + if values.shape == expected: + setattr(s, name, wrap_binned_data(values, dims, + {"t": self.t_grid, **{dim: grids[dim] for dim in dims}}, name=name)) else: logger.info(f"{folder =}") @@ -1810,3 +1880,10 @@ def load(self): logger.warning(self.f) logger.warning("self.n_sph:") logger.warning(self.n_sph) + + +# The old eager attribute tree remains in this module only to make old pickles and +# out-of-tree imports fail gently. New code receives the lazy, xarray-backed API. +from struphy.post_processing.run_output import RunOutput # noqa: E402 + +PlottingData = RunOutput diff --git a/src/struphy/post_processing/run_output.py b/src/struphy/post_processing/run_output.py new file mode 100644 index 000000000..cfa1bf8f1 --- /dev/null +++ b/src/struphy/post_processing/run_output.py @@ -0,0 +1,266 @@ +"""Discover and lazily load the products of one Struphy run.""" + +from __future__ import annotations + +import logging +import pickle +from collections.abc import Callable, Iterator, Mapping +from pathlib import Path + +import h5py +import numpy as np +import xarray as xr + +from struphy.post_processing.arrays import data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits + +logger = logging.getLogger("struphy") + + +class ProductMapping(Mapping[str, xr.DataArray]): + """A discoverable mapping whose products are loaded on first access.""" + + def __init__(self, loaders: Mapping[str, Callable[[], xr.DataArray]]): + self._loaders = dict(loaders) + self._cache: dict[str, xr.DataArray] = {} + + def __getitem__(self, key: str) -> xr.DataArray: + if key not in self._loaders: + raise KeyError(f"{key!r} not found; available products: {tuple(self)}") + if key not in self._cache: + self._cache[key] = self._loaders[key]() + return self._cache[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._loaders) + + def __len__(self) -> int: + return len(self._loaders) + + def clear_cache(self): + """Drop loaded arrays while keeping product discovery information.""" + self._cache.clear() + + +class PlotAccessor: + """Convenient plotting entry points bound to a run.""" + + def __init__(self, run: "RunOutput"): + self._run = run + + def timeseries(self, data, **kwargs): + from struphy.diagnostics.plotting import plot_timeseries + return plot_timeseries(data, run_label=self._run.label, **kwargs) + + def scalar(self, name: str, **kwargs): + return self.timeseries(self._run.scalars[name], **kwargs) + + def slice(self, data, **kwargs): + from struphy.diagnostics.plotting import plot_slice + return plot_slice(data, run_label=self._run.label, **kwargs) + + def viewer(self, data, **kwargs): + from struphy.diagnostics.plotting import InteractiveSliceViewer + return InteractiveSliceViewer(data, run_label=self._run.label, **kwargs) + + +class RunOutput: + """The self-describing, lazily loaded output of a completed simulation. + + Use :meth:`open` rather than constructing this class directly. Product names are + discovered immediately, while their arrays are loaded only when indexed. + + Parameters + ---------- + time_units: + ``"physical"`` converts every time coordinate to seconds. ``"normalized"`` + consistently leaves every product in Struphy time units. + """ + + def __init__(self, path_out=None, *, sim=None, time_units: str = "physical"): + if sim is not None: + path_out = sim.env.path_out + if path_out is None: + raise ValueError("path_out or sim is required") + if time_units not in {"physical", "normalized"}: + raise ValueError("time_units must be 'physical' or 'normalized'") + self.path_out = Path(path_out).resolve() + self.path_pproc = self.path_out / "post_processing" + if not self.path_pproc.is_dir(): + raise FileNotFoundError(f"{self.path_pproc} does not exist; run post-processing first") + self.time_units = time_units + self._params = self._units = self._time = self._grids_log = self._grids_phy = self._scalars = None + self.fields = ProductMapping(self._discover_fields()) + self.distributions = ProductMapping(self._discover_binned("distribution_function")) + self.densities = ProductMapping(self._discover_binned("n_sph")) + self.orbits = ProductMapping(self._discover_orbits()) + self.plot = PlotAccessor(self) + + @classmethod + def open(cls, path_out=None, *, sim=None, time_units="physical") -> "RunOutput": + if sim is not None: + path_out = sim.env.path_out + if path_out is None: + raise ValueError("path_out or sim is required") + return cls(path_out, time_units=time_units) + + @property + def params(self): + if self._params is None: + from struphy.post_processing.post_processing_tools import ParamsIn + self._params = ParamsIn(str(self.path_out)) + return self._params + + @property + def domain(self): + return self.params.domain + + @property + def units(self): + if self._units is None: + from struphy.physics.physics import Units + model = self.params.model + units = Units(model.base_units) + bulk = model.bulk_species + units.derive_units(velocity_scale=model.velocity_scale, + A_bulk=None if bulk is None else bulk.mass_number, + Z_bulk=None if bulk is None else bulk.charge_number) + self._units = units + return self._units + + @property + def time_scale(self) -> float: + return float(self.units.t) if self.time_units == "physical" else 1.0 + + @property + def time_unit(self) -> str: + return "s" if self.time_units == "physical" else "" + + @property + def time(self): + if self._time is None: + path = self.path_pproc / "t_grid.npy" + self._time = np.load(path, mmap_mode="r") * self.time_scale + return self._time + + @property + def grids_log(self): + if self._grids_log is None: + with (self.path_pproc / "fields_data" / "grids_log.bin").open("rb") as stream: + self._grids_log = pickle.load(stream) + return self._grids_log + + @property + def grids_phy(self): + if self._grids_phy is None: + with (self.path_pproc / "fields_data" / "grids_phy.bin").open("rb") as stream: + self._grids_phy = pickle.load(stream) + return self._grids_phy + + @property + def scalars(self) -> xr.Dataset: + if self._scalars is None: + path = self.path_out / "data" / "data_proc0.hdf5" + if not path.exists(): + self._scalars = xr.Dataset() + return self._scalars + with h5py.File(path) as file: + if "scalar" not in file: + self._scalars = xr.Dataset() + return self._scalars + time = np.asarray(file["time/value"]) * self.time_scale + variables = {} + for name, dataset in file["scalar"].items(): + variables[name] = data_array(np.asarray(dataset), ("t",), {"t": time}, name=name, + label=name.replace("_", " "), coord_units={"t": self.time_unit}) + self._scalars = xr.Dataset(variables) + return self._scalars + + @property + def label(self) -> str: + try: + values = [] + for holder, attr, name in ((self.params.time_opts, "dt", "dt"), + (self.params.time_opts, "split_algo", "algo"), + (self.params.grid, "num_elements", "Nel"), + (self.params.derham_opts, "degree", "p")): + value = getattr(holder, attr, None) if holder is not None else None + if value is not None: + values.append(f"{name}={value}") + return ", ".join(values) + except FileNotFoundError: + return self.path_out.name + + def save_scalars(self, path=None, **kwargs) -> str: + path = Path(path) if path else self.path_pproc / "scalars.csv" + return save_scalars(self.scalars, str(path), **kwargs) + + def save_scalar_plots(self, directory=None, **kwargs): + from struphy.diagnostics.plotting import save_all_scalars + directory = Path(directory) if directory else self.path_pproc / "scalars" + return save_all_scalars(self.scalars, directory, run_label=self.label, **kwargs) + + def _discover_fields(self): + loaders = {} + root = self.path_pproc / "fields_data" + for path in sorted(root.glob("*/*.bin")) if root.exists() else (): + key = f"{path.parent.name}/{path.stem}" + loaders[key] = lambda path=path, key=key: self._load_field(path, key) + return loaders + + def _load_field(self, path: Path, key: str): + with path.open("rb") as stream: + raw = pickle.load(stream) + try: + physical = self.grids_phy + except FileNotFoundError: + physical = None + return wrap_field_data(raw, self.grids_log, grids_phy=physical, name=key.split("/")[-1], + time_scale=self.time_scale, time_unit=self.time_unit) + + def _discover_binned(self, category: str): + loaders = {} + root = self.path_pproc / "kinetic_data" + pattern = f"*/{category}/*/*.npy" + for path in sorted(root.glob(pattern)) if root.exists() else (): + if path.stem.startswith("grid_"): + continue + species, slice_name = path.parents[2].name, path.parent.name + key = f"{species}/{slice_name}/{path.stem}" + loaders[key] = lambda path=path, slice_name=slice_name: self._load_binned(path, slice_name) + return loaders + + def _load_binned(self, path: Path, slice_name: str): + grid_paths = sorted(path.parent.glob("grid_*.npy")) + grids = {p.stem.removeprefix("grid_"): np.load(p, mmap_mode="r") for p in grid_paths} + dims = tuple(part for part in slice_name.split("_") if part in grids) + values = np.load(path, mmap_mode="r") + expected = (len(self.time), *(len(grids[dim]) for dim in dims)) + if values.shape != expected: + raise ValueError(f"{path} has shape {values.shape}; expected {expected} from its coordinates") + coords = {"t": self.time, **{dim: grids[dim] for dim in dims}} + logical_dims = tuple(dim for dim in dims if dim in {"e1", "e2", "e3"}) + if len(logical_dims) == 2: + mesh = np.meshgrid(*(np.asarray(grids[dim]) for dim in logical_dims), indexing="ij") + arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} + arguments.update(dict(zip(logical_dims, mesh))) + try: + physical = self.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) + for coordinate, grid in zip(("X", "Y", "Z"), physical): + coords[coordinate] = (logical_dims, np.asarray(grid)) + except (FileNotFoundError, TypeError, ValueError): + logger.debug("Could not attach physical coordinates to %s", path, exc_info=True) + return wrap_binned_data(values, dims, coords, name=path.stem, time_unit=self.time_unit) + + def _discover_orbits(self): + loaders = {} + root = self.path_pproc / "kinetic_data" + for directory in sorted(root.glob("*/orbits")) if root.exists() else (): + loaders[directory.parent.name] = lambda directory=directory: self._load_orbits(directory) + return loaders + + def _load_orbits(self, directory: Path): + paths = sorted(directory.glob("*.npy"), key=lambda p: int(p.stem.rsplit("_", 1)[-1])) + if not paths: + raise FileNotFoundError(f"no orbit arrays in {directory}") + values = np.stack([np.load(path, mmap_mode="r") for path in paths]) + return wrap_orbits(values, self.time[:len(paths)], time_unit=self.time_unit) diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index 835b56c20..6c9d7ef6f 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -1,323 +1,92 @@ -"""Unit tests for the labeled arrays used to hand post-processed data to the plotters. - -None of these need a simulation: they build small arrays by hand and check that the -dimension bookkeeping, coordinate pairing and back-compatible indexing behave. -""" - -import os +"""Contracts for the xarray post-processing representation.""" import numpy as np import pytest +import xarray as xr from struphy.post_processing.arrays import ( - StruphyArray, - orbit_columns, + axis_label, + data_array, save_scalars, - scalar_names, scalars_table, - wrap_binned_slice, + validate_array, + value_label, + wrap_binned_data, wrap_field_data, wrap_orbits, ) -class Holder: - """Stand-in for the ``Slice`` container the loader populates by setattr.""" - - -def make_array(): - return StruphyArray( - np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4), - dims=("t", "e1", "v1"), - coords={"t": np.array([0.0, 1.0]), "e1": np.linspace(0, 1, 3), "v1": np.linspace(-1, 1, 4)}, - label="$f$", - ) - - -# ---------------------------------------------------------------- StruphyArray - - -def test_rank_must_match_dims(): - with pytest.raises(ValueError, match="rank"): - StruphyArray(np.zeros((2, 3)), dims=("t",)) - - -def test_coord_must_match_axis_length(): - with pytest.raises(ValueError, match="shape"): - StruphyArray(np.zeros((2, 3)), dims=("t", "e1"), coords={"e1": np.zeros(7)}) - - -def test_coord_must_name_a_dim(): - with pytest.raises(ValueError, match="not one of the dims"): - StruphyArray(np.zeros(2), dims=("t",), coords={"e1": np.zeros(2)}) - - -def test_behaves_as_a_plain_array(): - """Existing code that indexes or reduces the raw arrays must keep working.""" - f = make_array() - assert np.asarray(f).shape == (2, 3, 4) - assert f[1].T.shape == (4, 3) - assert np.sum(f) == pytest.approx(np.sum(np.arange(24))) - assert len(f) == 2 - - -def test_isel_drops_int_axes_and_keeps_slices(): - f = make_array() - - dropped = f.isel(t=0) - assert dropped.dims == ("e1", "v1") - assert "t" not in dropped.coords - - kept = f.isel(t=slice(0, 1)) - assert kept.dims == ("t", "e1", "v1") - assert kept.shape == (1, 3, 4) - - -def test_isel_subsets_the_coordinate_of_a_sliced_dim(): - f = make_array() - sub = f.isel(v1=slice(1, 3)) - assert sub.shape[-1] == 2 - np.testing.assert_allclose(sub.coords["v1"], f.coords["v1"][1:3]) - - -def test_at_picks_the_nearest_coordinate(): - """Replaces the ``abs(t_grid - t).argmin()`` idiom, including ties away from a node.""" - f = make_array() - np.testing.assert_allclose(np.asarray(f.at(t=0.4)), np.asarray(f.isel(t=0))) - np.testing.assert_allclose(np.asarray(f.at(t=0.9)), np.asarray(f.isel(t=1))) - - -def test_transpose_to_reorders_values_and_dims(): - f = make_array().isel(t=0) - tr = f.transpose_to("v1", "e1") - assert tr.dims == ("v1", "e1") - np.testing.assert_allclose(np.asarray(tr), np.asarray(f).T) - - -def test_transpose_to_rejects_a_different_dim_set(): - with pytest.raises(ValueError, match="cannot transpose"): - make_array().transpose_to("t", "e1") - - -def test_coord_falls_back_to_an_index_range(): - f = StruphyArray(np.zeros((2, 3)), dims=("t", "e1")) - np.testing.assert_allclose(f.coord("e1"), np.arange(3)) - - -def test_axis_label_includes_units_when_known(): - f = make_array().with_coord_units(t="s") - assert f.axis_label("t") == "$t$ [s]" - assert f.axis_label("e1") == r"$\eta_1$" - assert f.value_label == "$f$ [a.u.]" - - -def test_coord_units_survive_selection(): - f = make_array().with_coord_units(t="s") - assert f.isel(e1=0).coord_units == {"t": "s"} - assert f.isel(t=0).transpose_to("v1", "e1").coord_units == {"t": "s"} - - -def test_unknown_axis_raises(): - with pytest.raises(KeyError): - make_array().axis("nope") - - -# ---------------------------------------------------------------- orbit columns - - -@pytest.mark.parametrize( - "n_columns, expect_weight", - [(8, 6), (5, None), (6, None)], -) -def test_orbit_columns_resolves_weight_from_width(n_columns, expect_weight): - """The saved marker columns depend on the species' velocity dimension. - - A 1V species saves no weight at all, so reading index 6 unconditionally would - silently return a velocity component instead. - """ - cols = orbit_columns(n_columns) - assert cols["position"] == slice(0, 3) - assert cols["id"] == n_columns - 1 - assert cols.get("weight") == expect_weight - - -def test_wrap_orbits_attaches_columns(): - orb = wrap_orbits(np.zeros((4, 10, 8)), np.arange(4.0)) - assert orb.dims == ("t", "marker", "attribute") - assert orb.columns["weight"] == 6 - - -# ---------------------------------------------------------------- binned slices - - -def test_wrap_binned_slice_pairs_grids_with_data(): - holder = Holder() - holder.grid_e1 = np.linspace(0, 1, 3) - holder.grid_v1 = np.linspace(-1, 1, 4) - holder.f_binned = np.zeros((2, 3, 4)) - holder.delta_f_binned = np.zeros((2, 3, 4)) - - wrap_binned_slice(holder, "e1_v1_density", np.array([0.0, 1.0])) - - assert holder.f_binned.dims == ("t", "e1", "v1") - np.testing.assert_allclose(holder.f_binned.coord("v1"), np.linspace(-1, 1, 4)) - assert holder.f_binned.label == "$f$" - assert holder.delta_f_binned.label == r"$\delta f$" - # the grids themselves stay raw - assert isinstance(holder.grid_e1, np.ndarray) - - -def test_wrap_binned_slice_takes_dim_order_from_the_name(): - """``v1_v2_density`` must map axis 0 to v1, not to whichever grid was set first.""" - holder = Holder() - holder.grid_v2 = np.linspace(0, 1, 5) - holder.grid_v1 = np.linspace(0, 1, 3) - holder.f_binned = np.zeros((2, 3, 5)) - - wrap_binned_slice(holder, "v1_v2_density", np.array([0.0, 1.0])) - assert holder.f_binned.dims == ("t", "v1", "v2") - - -def test_wrap_binned_slice_handles_one_dimensional_binning(): - holder = Holder() - holder.grid_e1 = np.linspace(0, 1, 6) - holder.f_binned = np.zeros((2, 6)) - - wrap_binned_slice(holder, "e1_current_1", np.array([0.0, 1.0])) - assert holder.f_binned.dims == ("t", "e1") - - -def test_wrap_binned_slice_leaves_mismatched_entries_alone(): - holder = Holder() - holder.grid_e1 = np.linspace(0, 1, 3) - holder.other = np.zeros((9, 9)) - - wrap_binned_slice(holder, "e1_density", np.array([0.0, 1.0])) - assert isinstance(holder.other, np.ndarray) - - -def test_wrap_binned_slice_without_grids_is_a_noop(): - holder = Holder() - holder.f_binned = np.zeros((2, 3)) - wrap_binned_slice(holder, "whatever", np.array([0.0, 1.0])) - assert isinstance(holder.f_binned, np.ndarray) - - -# ---------------------------------------------------------------- field data - - -def test_wrap_field_data_stacks_vector_components(): - grids = [np.linspace(0, 1, n) for n in (2, 3, 4)] - data = {0.0: [np.zeros((2, 3, 4)) for _ in range(3)], 1.0: [np.ones((2, 3, 4)) for _ in range(3)]} - - arr = wrap_field_data(data, grids, label="B") - assert arr.dims == ("t", "comp", "e1", "e2", "e3") - assert arr.shape == (2, 3, 2, 3, 4) - np.testing.assert_allclose(arr.coord("t"), [0.0, 1.0]) - np.testing.assert_allclose(np.asarray(arr.isel(t=1, comp=0)), 1.0) - - -def test_wrap_field_data_drops_comp_for_scalars(): - grids = [np.linspace(0, 1, n) for n in (2, 3, 4)] - arr = wrap_field_data({0.0: [np.zeros((2, 3, 4))]}, grids, label="phi") - assert arr.dims == ("t", "e1", "e2", "e3") - - -def test_wrap_field_data_sorts_times(): - grids = [np.linspace(0, 1, n) for n in (2, 2, 2)] - data = {1.0: [np.ones((2, 2, 2))], 0.0: [np.zeros((2, 2, 2))]} - arr = wrap_field_data(data, grids) - np.testing.assert_allclose(arr.coord("t"), [0.0, 1.0]) - np.testing.assert_allclose(np.asarray(arr.isel(t=0)), 0.0) - - -def test_wrap_field_data_of_empty_dict_is_none(): - assert wrap_field_data({}, None) is None - - -# ---------------------------------------------------------------- scalar export - -NT_SCALARS = 5 - - -def make_scalars(**extra): - """The shape of ``PlottingData.scalars``: a name -> time series mapping.""" - t = np.linspace(0.0, 2.0, NT_SCALARS) - scalars = { - "en_tot": StruphyArray(np.full(NT_SCALARS, 3.0), dims=("t",), coords={"t": t}, label="en tot"), - "en_e": StruphyArray(np.linspace(1.0, 2.0, NT_SCALARS), dims=("t",), coords={"t": t}, label="en e"), - "time": StruphyArray(t, dims=("t",), coords={"t": t}), - } - scalars.update(extra) - return scalars - - -def test_scalar_names_drops_the_excluded_ones(): - assert scalar_names(make_scalars()) == ["en_tot", "en_e"] - - -def test_scalar_names_keeps_the_requested_order(): - assert scalar_names(make_scalars(), names=["en_e", "time"]) == ["en_e", "time"] - - -def test_scalar_names_rejects_an_unknown_name(): - with pytest.raises(KeyError, match="no scalars"): - scalar_names(make_scalars(), names=["en_nope"]) - - -def test_scalars_table_is_one_column_per_scalar(): - t, names, values = scalars_table(make_scalars()) - - assert names == ["en_tot", "en_e"] - assert values.shape == (NT_SCALARS, 2) - np.testing.assert_allclose(t, np.linspace(0.0, 2.0, NT_SCALARS)) - np.testing.assert_allclose(values[:, 0], 3.0) - np.testing.assert_allclose(values[:, 1], np.linspace(1.0, 2.0, NT_SCALARS)) - +def test_data_array_carries_names_coordinates_and_units(): + data = data_array(np.ones((3, 4)), ("t", "e1"), {"t": [0, 1, 2], "e1": np.arange(4)}, + name="density", label="$n$", unit="m^-3", coord_units={"t": "s"}) + assert isinstance(data, xr.DataArray) + assert data.sel(t=1).dims == ("e1",) + assert axis_label(data, "t") == "$t$ [s]" + assert value_label(data) == "$n$ [m^-3]" -def test_scalars_table_drops_a_series_of_the_wrong_length(): - """A short series cannot share the time column, and must not corrupt the table.""" - odd = StruphyArray(np.zeros(2), dims=("t",), coords={"t": np.zeros(2)}) - t, names, values = scalars_table(make_scalars(odd_one=odd)) - assert "odd_one" not in names - assert values.shape == (NT_SCALARS, 2) +def test_validation_rejects_missing_dims_and_nonmonotonic_coordinates(): + data = xr.DataArray(np.ones(3), dims="x", coords={"x": [0, 2, 1]}) + with pytest.raises(ValueError, match="monotonic"): + validate_array(data) + with pytest.raises(ValueError, match="missing"): + validate_array(xr.DataArray(np.ones(3), dims="x"), required_dims=("t",)) -def test_scalars_table_of_nothing_is_empty(): - t, names, values = scalars_table({}) - assert names == [] and len(t) == 0 +def test_xarray_arithmetic_preserves_dimension_alignment_and_metadata(): + left = data_array([1, 2], ("t",), {"t": [0, 1]}, label="left", unit="J") + right = data_array([3, 4], ("t",), {"t": [0, 1]}, label="right", unit="J") + result = left + right + assert result.dims == ("t",) + np.testing.assert_array_equal(result, [4, 6]) -def test_save_scalars_writes_a_csv_with_a_header(tmp_path): - path = save_scalars(make_scalars(), str(tmp_path / "scalars.csv")) - lines = open(path).read().splitlines() +def test_field_wrapper_attaches_curvilinear_physical_coordinates(): + logical = [np.linspace(0, 1, n) for n in (2, 3, 4)] + physical = np.meshgrid(*logical, indexing="ij") + raw = {0.0: [np.zeros((2, 3, 4))], 1.0: [np.ones((2, 3, 4))]} + field = wrap_field_data(raw, logical, grids_phy=physical, name="phi", time_scale=2, time_unit="s") + assert field.dims == ("t", "e1", "e2", "e3") + assert field.X.dims == ("e1", "e2", "e3") + np.testing.assert_array_equal(field.t, [0, 2]) + assert field.t.attrs["units"] == "s" - assert lines[0] == "t,en_tot,en_e" - assert len(lines) == NT_SCALARS + 1 - # every row is one time step: t, then one value per scalar - first = [float(v) for v in lines[1].split(",")] - assert first == pytest.approx([0.0, 3.0, 1.0]) +def test_vector_field_has_named_component_dimension(): + component = np.zeros((2, 2, 2)) + field = wrap_field_data({0.0: [component, component, component]}, name="E") + assert field.dims == ("t", "component", "e1", "e2", "e3") + assert field.isel(component=1).dims == ("t", "e1", "e2", "e3") -def test_save_scalars_round_trips_through_npz(tmp_path): - path = save_scalars(make_scalars(), str(tmp_path / "scalars.npz")) - loaded = np.load(path) - assert set(loaded.files) == {"t", "en_tot", "en_e"} - np.testing.assert_allclose(loaded["en_e"], np.linspace(1.0, 2.0, NT_SCALARS)) +def test_binned_wrapper_keeps_memory_mappable_values(): + values = np.ones((2, 3, 4)) + data = wrap_binned_data(values, ("e1", "v1"), {"t": [0, 1], "e1": range(3), "v1": range(4)}, + name="f_binned") + assert data.dims == ("t", "e1", "v1") + assert data.attrs["label"] == "$f$" -def test_save_scalars_takes_the_format_from_the_suffix(tmp_path): - npz = save_scalars(make_scalars(), str(tmp_path / "table"), fmt="npz") - assert npz.endswith(".npz") and os.path.exists(npz) +def test_orbits_store_column_semantics_as_metadata(): + data = wrap_orbits(np.zeros((2, 5, 8)), [0, 1]) + assert data.attrs["columns"]["weight"] == 6 + assert data.sel(marker=2).dims == ("t", "attribute") -def test_save_scalars_rejects_an_unknown_format(tmp_path): - with pytest.raises(ValueError, match="unknown format"): - save_scalars(make_scalars(), str(tmp_path / "scalars.xlsx")) +def test_scalar_alignment_is_exact(): + good = xr.Dataset({"a": ("t", [1, 2]), "b": ("t", [3, 4])}, coords={"t": [0, 1]}) + time, names, values = scalars_table(good) + assert names == ["a", "b"] + np.testing.assert_array_equal(values, [[1, 3], [2, 4]]) + bad = {"a": good.a, "b": xr.DataArray([3, 4], dims="t", coords={"t": [1, 2]})} + with pytest.raises(ValueError, match="align"): + scalars_table(bad) -def test_save_scalars_creates_the_directory(tmp_path): - path = save_scalars(make_scalars(), str(tmp_path / "new" / "dir" / "scalars.csv")) - assert os.path.exists(path) +@pytest.mark.parametrize("suffix", ["csv", "npz"]) +def test_save_scalars(tmp_path, suffix): + scalars = xr.Dataset({"a": ("t", [1, 2]), "b": ("t", [3, 4])}, coords={"t": [0, 1]}) + path = save_scalars(scalars, str(tmp_path / f"scalars.{suffix}")) + assert (tmp_path / f"scalars.{suffix}").stat().st_size > 0 + assert path.endswith(suffix) diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py deleted file mode 100644 index 4da11c107..000000000 --- a/src/struphy/post_processing/tests/test_plotting_data.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Integration test for loading post-processed data into labeled arrays. - -No simulation is run: a post-processing folder is written out by hand in the layout -:meth:`PostProcessor.process` produces, then loaded back. This covers the wiring that -turns files on disk into the objects the plotting scripts index into. -""" - -import os -import pickle - -import numpy as np -import pytest - -from struphy.post_processing.arrays import StruphyArray -from struphy.post_processing.post_processing_tools import PlottingData - -NT, N1, N2, N3 = 3, 4, 5, 6 -NV = 7 -N_MARKERS = 10 - - -def write_pproc_tree(root): - """Write a minimal post-processing folder, returning the output path.""" - pproc = os.path.join(root, "post_processing") - fields = os.path.join(pproc, "fields_data") - kinetic = os.path.join(pproc, "kinetic_data") - os.makedirs(fields) - os.makedirs(kinetic) - - t_grid = np.linspace(0.0, 1.0, NT) - np.save(os.path.join(pproc, "t_grid.npy"), t_grid) - - grids_log = [np.linspace(0, 1, n) for n in (N1, N2, N3)] - grids_phy = list(np.meshgrid(*grids_log, indexing="ij")) - for name, grids in (("grids_log", grids_log), ("grids_phy", grids_phy)): - with open(os.path.join(fields, f"{name}.bin"), "wb") as f: - pickle.dump(grids, f) - - # a vector field and a scalar field, keyed by time as the post-processor writes them - species_dir = os.path.join(fields, "em_fields") - os.makedirs(species_dir) - vector = {t: [np.full((N1, N2, N3), i + t) for i in range(3)] for t in t_grid} - scalar = {t: [np.full((N1, N2, N3), t)] for t in t_grid} - for name, data in (("e_field_log", vector), ("phi_phy", scalar)): - with open(os.path.join(species_dir, f"{name}.bin"), "wb") as f: - pickle.dump(data, f) - - # binned distribution function - slice_dir = os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density") - os.makedirs(slice_dir) - np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) - np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) - np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) - np.save(os.path.join(slice_dir, "delta_f_binned.npy"), np.zeros((NT, N1, NV))) - - # marker orbits: one .npy and one .txt per saved step - orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") - os.makedirs(orbit_dir) - for step in range(NT): - np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), float(step))) - open(os.path.join(orbit_dir, f"kinetic_ions_{step}.txt"), "w").close() - - return root - - -@pytest.fixture -def pdata(tmp_path): - out = write_pproc_tree(str(tmp_path)) - data = PlottingData(path_out=out) - data.load() - return data - - -def write_raw_scalars(root): - """Write the ``scalar`` group of a raw output file, as the simulation records it.""" - import h5py - - data_dir = os.path.join(root, "data") - os.makedirs(data_dir, exist_ok=True) - t = np.linspace(0.0, 1.0, NT) - - with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as f: - f.create_dataset("time/value", data=t) - f.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) - f.create_dataset("scalar/en_e", data=np.linspace(1.0, 1.5, NT)) - return t - - -@pytest.fixture -def pdata_scalars(tmp_path): - """A run whose raw scalars are readable without its parameter file.""" - out = write_pproc_tree(str(tmp_path)) - t = write_raw_scalars(out) - data = PlottingData(path_out=out) - # physical_time would need the run's units, and so its parameters - data.load_scalars(physical_time=False) - return data, t - - -def test_load_without_raw_data_skips_scalars(pdata): - """Scalars come from the raw HDF5, which a post-processing-only folder lacks.""" - assert pdata.scalars.keys() == () - - -def test_scalars_are_labeled_time_series(pdata_scalars): - data, t = pdata_scalars - - assert set(data.scalars.keys()) == {"en_tot", "en_e"} - assert data.scalars["en_tot"].dims == ("t",) - np.testing.assert_allclose(data.scalars["en_tot"].coord("t"), t) - - -def test_save_scalars_defaults_into_the_post_processing_folder(pdata_scalars): - data, t = pdata_scalars - path = data.save_scalars() - - assert path == os.path.join(data.path_pproc, "scalars.csv") - lines = open(path).read().splitlines() - assert set(lines[0].split(",")) == {"t", "en_tot", "en_e"} - assert len(lines) == len(t) + 1 - - -def test_save_scalar_plots_writes_the_standard_set(pdata_scalars): - data, _ = pdata_scalars - paths = data.save_scalar_plots(params=None) - - assert sorted(os.path.basename(p) for p in paths) == [ - "en_e.png", - "en_tot.png", - "scalars.csv", - "scalars.png", - ] - assert all(p.startswith(os.path.join(data.path_pproc, "scalars")) for p in paths) - - -def test_grids_are_loaded(pdata): - assert len(pdata.grids_log) == 3 - assert pdata.grids_phy[0].shape == (N1, N2, N3) - np.testing.assert_allclose(pdata.t_grid, np.linspace(0.0, 1.0, NT)) - - -def test_containers_are_discoverable(pdata): - """Contents can be listed instead of having to be known in advance.""" - assert "em_fields" in pdata.spline_values - assert set(pdata.spline_values["em_fields"].keys()) == {"e_field_log", "phi_phy"} - assert "e1_v1_density" in pdata.f["kinetic_ions"] - - -def test_plot_accessor_is_created_lazily_and_cached(pdata): - from struphy.diagnostics.plotting import PlottingAccessor - - assert not hasattr(pdata, "_plot_accessor") - assert isinstance(pdata.plot, PlottingAccessor) - assert pdata.plot is pdata.plot - - -def test_field_becomes_one_labeled_array(pdata): - """The chain the migrated plotting scripts use.""" - field = pdata.spline_values["em_fields"]["e_field_log"].array - - assert field.dims == ("t", "comp", "e1", "e2", "e3") - assert field.shape == (NT, 3, N1, N2, N3) - np.testing.assert_allclose(field.coord("e2"), np.linspace(0, 1, N2)) - # component i at time t was filled with i + t - np.testing.assert_allclose(np.asarray(field.isel(t=0, comp=2)), 2.0) - - -def test_scalar_field_has_no_component_axis(pdata): - assert pdata.spline_values["em_fields"]["phi_phy"].array.dims == ("t", "e1", "e2", "e3") - - -def test_raw_field_dict_still_available(pdata): - """Existing scripts index ``.data[t][component]`` directly.""" - dd = pdata.spline_values["em_fields"]["e_field_log"] - assert dd.data[0.0][1].shape == (N1, N2, N3) - - -def test_binned_data_carries_its_grids(pdata): - f = pdata.f["kinetic_ions"]["e1_v1_density"]["f_binned"] - - assert isinstance(f, StruphyArray) - assert f.dims == ("t", "e1", "v1") - np.testing.assert_allclose(f.coord("v1"), np.linspace(-3, 3, NV)) - assert f[0].T.shape == (NV, N1) # back-compat indexing - - -def test_orbits_are_labeled_with_columns(pdata): - orbits = pdata.orbits["kinetic_ions"] - - assert orbits.dims == ("t", "marker", "attribute") - assert orbits.shape == (NT, N_MARKERS, 8) - assert orbits.columns["weight"] == 6 - # step n was filled with the value n - np.testing.assert_allclose(np.asarray(orbits.isel(t=2)), 2.0) - - -def test_field_slice_matches_the_physical_grid(pdata): - """A cut field and its grid must have the same shape, which is what the plotters assume.""" - from struphy.diagnostics.plotting import field_slice_grids - - cut = pdata.spline_values["em_fields"]["phi_phy"].array.isel(t=0, e3=1) - xgrid, _, _, _ = field_slice_grids(pdata.grids_phy, fixed_dim="e3", index=1, plane="XY") - assert cut.shape == xgrid.shape From 48fe9a9286dad86e7ee04ce63d01924bb4ac5318 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 17:50:45 +0200 Subject: [PATCH 015/193] Finish redesign of plotting --- src/struphy/api/post_processing/__init__.py | 13 +-- src/struphy/diagnostics/plotting.py | 3 +- src/struphy/post_processing/run_output.py | 65 ++++++++++++++ .../tests/test_plotting_data.py | 88 +++++++++++++++++++ src/struphy/simulation/sim.py | 22 +++-- 5 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 src/struphy/post_processing/tests/test_plotting_data.py diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index 1ec68460d..e5abe010f 100644 --- a/src/struphy/api/post_processing/__init__.py +++ b/src/struphy/api/post_processing/__init__.py @@ -18,17 +18,18 @@ def post_process( ) -> RunOutput: """Process a completed run and return its lazy :class:`RunOutput`.""" if sim is not None: - sim.pproc(step=step, celldivide=celldivide, physical=physical, - guiding_center=guiding_center, classify=classify, - create_vtk=create_vtk, force=force, load=True) - return RunOutput(sim=sim) + return sim.pproc(step=step, celldivide=celldivide, physical=physical, + guiding_center=guiding_center, classify=classify, + create_vtk=create_vtk, force=force, load=True) if path_out is None: raise ValueError("path_out or sim is required") - processor = PostProcessor(path_out=path_out) + processor = PostProcessor(sim=None, path_out=path_out) processor.process(step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, force=force) - return RunOutput(path_out=path_out) + data = PlottingData(sim=None, path_out=path_out) + data.load() + return data __all__ = ["PostProcessor", "RunOutput", "PlottingData", "post_process"] diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 1aafb02dc..4f2b483eb 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -244,7 +244,8 @@ def plot_slice(data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, fig, ax = plt.subplots() if ax is None else (ax.figure, ax) mesh = ax.pcolormesh(xgrid, ygrid, np.asarray(selected), shading="auto", vmin=vmin, vmax=vmax) fig.colorbar(mesh, ax=ax, label=value_label(data)) - if equal_aspect if equal_aspect is not None else view.coordinates == "physical": + use_equal_aspect = view.coordinates == "physical" if equal_aspect is None else equal_aspect + if use_equal_aspect: ax.set_aspect("equal", adjustable="box") ax.set(xlabel=xlabel, ylabel=ylabel, title=title if title is not None else _label(data)) ax.grid(False) diff --git a/src/struphy/post_processing/run_output.py b/src/struphy/post_processing/run_output.py index cfa1bf8f1..81faa23e8 100644 --- a/src/struphy/post_processing/run_output.py +++ b/src/struphy/post_processing/run_output.py @@ -41,6 +41,51 @@ def clear_cache(self): self._cache.clear() +class _ProductValue: + """Attribute view used only by legacy examples during the transition.""" + + def __init__(self, mapping, key): + self._mapping, self._key = mapping, key + + @property + def array(self): + return self._mapping[self._key] + + def __getattr__(self, name): + data = self.array + if name.startswith("grid_"): + return data.coords[name.removeprefix("grid_")].values + if name in data.attrs: + return data.attrs[name] + return getattr(data, name) + + def __getitem__(self, key): + return self.array[key] + + def __len__(self): + return self.array.sizes[self.array.dims[0]] + + def __array__(self, dtype=None): + return np.asarray(self.array, dtype=dtype) + + +class _ProductNamespace: + def __init__(self, mapping, prefix=""): + self._mapping, self._prefix = mapping, prefix + + def __getattr__(self, name): + key = f"{self._prefix}/{name}" if self._prefix else name + if key in self._mapping: + return _ProductValue(self._mapping, key) + prefix = key + "/" + if any(product.startswith(prefix) for product in self._mapping): + return _ProductNamespace(self._mapping, key) + raise AttributeError(f"{name!r}; available products: {tuple(self._mapping)}") + + def __getitem__(self, key): + return getattr(self, key) + + class PlotAccessor: """Convenient plotting entry points bound to a run.""" @@ -103,6 +148,26 @@ def open(cls, path_out=None, *, sim=None, time_units="physical") -> "RunOutput": raise ValueError("path_out or sim is required") return cls(path_out, time_units=time_units) + def load(self): + """Materialize no arrays; retained as an explicit migration no-op.""" + return self + + @property + def t_grid(self): + return self.time + + @property + def f(self): + return _ProductNamespace(self.distributions) + + @property + def spline_values(self): + return _ProductNamespace(self.fields) + + @property + def n_sph(self): + return _ProductNamespace(self.densities) + @property def params(self): if self._params is None: diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py new file mode 100644 index 000000000..0c1923e35 --- /dev/null +++ b/src/struphy/post_processing/tests/test_plotting_data.py @@ -0,0 +1,88 @@ +"""Integration tests for lazy RunOutput discovery.""" + +import os +import pickle + +import h5py +import numpy as np +import pytest + +from struphy.post_processing.run_output import RunOutput + +NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 + + +def write_tree(root): + pproc = os.path.join(root, "post_processing") + fields = os.path.join(pproc, "fields_data") + kinetic = os.path.join(pproc, "kinetic_data") + os.makedirs(os.path.join(fields, "em_fields")) + os.makedirs(os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density")) + os.makedirs(os.path.join(kinetic, "kinetic_ions", "orbits")) + t = np.linspace(0, 1, NT) + np.save(os.path.join(pproc, "t_grid.npy"), t) + logical = [np.linspace(0, 1, n) for n in (N1, N2, N3)] + physical = np.meshgrid(*logical, indexing="ij") + for name, value in (("grids_log", logical), ("grids_phy", physical)): + with open(os.path.join(fields, f"{name}.bin"), "wb") as stream: + pickle.dump(value, stream) + values = {time: [np.full((N1, N2, N3), i + time) for i in range(3)] for time in t} + with open(os.path.join(fields, "em_fields", "E.bin"), "wb") as stream: + pickle.dump(values, stream) + slice_dir = os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density") + np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) + np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) + np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) + orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") + for step in range(NT): + np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), step)) + data_dir = os.path.join(root, "data") + os.makedirs(data_dir) + with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as file: + file.create_dataset("time/value", data=t) + file.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) + return root + + +@pytest.fixture +def run(tmp_path): + return RunOutput.open(write_tree(str(tmp_path)), time_units="normalized") + + +def test_products_are_discovered_without_loading_arrays(run): + assert tuple(run.fields) == ("em_fields/E",) + assert tuple(run.distributions) == ("kinetic_ions/e1_v1_density/f_binned",) + assert tuple(run.orbits) == ("kinetic_ions",) + assert run.fields._cache == {} + + +def test_field_has_named_and_curvilinear_coordinates(run): + field = run.fields["em_fields/E"] + assert field.dims == ("t", "component", "e1", "e2", "e3") + assert field.X.dims == ("e1", "e2", "e3") + np.testing.assert_allclose(field.isel(t=0, component=2), 2) + assert run.fields._cache["em_fields/E"] is field + + +def test_binned_products_have_coordinates(run): + data = run.distributions["kinetic_ions/e1_v1_density/f_binned"] + assert data.dims == ("t", "e1", "v1") + np.testing.assert_allclose(data.v1, np.linspace(-3, 3, NV)) + + +def test_orbit_product_keeps_column_semantics(run): + data = run.orbits["kinetic_ions"] + assert data.dims == ("t", "marker", "attribute") + assert data.attrs["columns"]["weight"] == 6 + + +def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): + assert set(run.scalars.data_vars) == {"en_tot"} + np.testing.assert_allclose(run.scalars.en_tot.t, run.time) + + +def test_saving_scalars_and_bound_plot_accessor(run, tmp_path): + path = run.save_scalars(tmp_path / "scalars.csv") + assert os.path.exists(path) + result = run.plot.timeseries(run.scalars.en_tot, logy=False) + assert result.ax.get_xlabel() == "$t$" diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index e1e8e2b0f..7416ad096 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -25,7 +25,7 @@ BaseUnits, DerhamOptions, EnvironmentOptions, - PlottingData, + RunOutput, PostProcessor, ProfilingOptions, Time, @@ -883,7 +883,7 @@ def pproc( parallel_pproc: bool = False, force: bool = True, load: bool = False, - ) -> PlottingData | None: + ) -> RunOutput | None: """Run post-processing on saved simulation data. Uses `PostProcessor` to process guiding-center or physical field views @@ -925,29 +925,27 @@ def pproc( return self.load_plotting_data() return None - def load_plotting_data(self) -> PlottingData | None: + def load_plotting_data(self) -> RunOutput | None: """Load plotting datasets produced by post-processing. - On rank 0, creates a `PlottingData` instance if needed, loads the data, - exposes convenient attributes such as `orbits`, `f`, and grid information - for downstream plotting or analysis, and returns the instance. Non-root - ranks return ``None``. + Creates a lazy :class:`RunOutput` instance on rank 0 and exposes its + product mappings for downstream analysis. Non-root ranks return ``None``. """ if self.rank != 0: return None if not hasattr(self, "_plotting_data"): - self._plotting_data = PlottingData(sim=self) + self._plotting_data = RunOutput(sim=self) self.plotting_data.load() # expose attributes self.orbits = self.plotting_data.orbits - self.f = self.plotting_data.f - self.spline_values = self.plotting_data.spline_values - self.n_sph = self.plotting_data.n_sph + self.f = self.plotting_data.distributions + self.spline_values = self.plotting_data.fields + self.n_sph = self.plotting_data.densities self.grids_log = self.plotting_data.grids_log self.grids_phy = self.plotting_data.grids_phy - self.t_grid = self.plotting_data.t_grid + self.t_grid = self.plotting_data.time return self.plotting_data # --------------------- From baab9a6af282626bb8568adf049c6659e46f2a39 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 14 Sep 2026 18:02:32 +0200 Subject: [PATCH 016/193] changed the API to hierarchical namespaces --- .../cyclone/pproc_cyclone.py | 6 +- .../itg_cylindre/pproc_drift_kinetic.py | 6 +- .../diocotron_instability/pproc_diocotron.py | 6 +- .../two_stream/pproc_two_stream.py | 2 +- src/struphy/post_processing/run_output.py | 66 ++++++++++++++++--- .../tests/test_plotting_data.py | 8 +-- 6 files changed, 70 insertions(+), 24 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 0fddce5df..3582783fb 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -47,16 +47,16 @@ def main(path_out): plot_equilibrium_profile(path_out) for bin_name, quantity, plane in DENSITY_PLOTS: - data = run.distributions[f"kinetic_ions/{bin_name}/{quantity}"] + data = getattr(getattr(run.distributions.kinetic_ions, bin_name), quantity) InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), run_label=run.label).show() for species, field, component, plane in FIELD_PLOTS: - data = run.fields[f"{species}/{field}"].isel(component=component) + data = getattr(getattr(run.fields, species), field).isel(component=component) InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), run_label=run.label).show() - plot_marker_trajectories(run.orbits["kinetic_ions"], max_markers=1000).show() + plot_marker_trajectories(run.orbits.kinetic_ions, max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 585a98e09..6eddbe2e2 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -47,16 +47,16 @@ def main(path_out): plot_equilibrium_profile(path_out) for bin_name, quantity, plane in DENSITY_PLOTS: - data = run.distributions[f"kinetic_ions/{bin_name}/{quantity}"] + data = getattr(getattr(run.distributions.kinetic_ions, bin_name), quantity) InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), run_label=run.label).show() for species, field, component, plane in FIELD_PLOTS: - data = run.fields[f"{species}/{field}"].isel(component=component) + data = getattr(getattr(run.fields, species), field).isel(component=component) InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), run_label=run.label).show() - plot_marker_trajectories(run.orbits["kinetic_ions"], max_markers=1000).show() + plot_marker_trajectories(run.orbits.kinetic_ions, max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 8128a4e6e..9477d38b6 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -68,16 +68,16 @@ def main(paths): plot_equilibrium_profile(path_out) for bin_name, quantity, plane in DENSITY_PLOTS: - data = run.distributions[f"kinetic_ions/{bin_name}/{quantity}"] + data = getattr(getattr(run.distributions.kinetic_ions, bin_name), quantity) InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), run_label=run.label).show() for species, field, component, plane in FIELD_PLOTS: - data = run.fields[f"{species}/{field}"].isel(component=component) + data = getattr(getattr(run.fields, species), field).isel(component=component) InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), run_label=run.label).show() - plot_marker_trajectories(run.orbits["kinetic_ions"], max_markers=1000).show() + plot_marker_trajectories(run.orbits.kinetic_ions, max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 988e17b17..8fe1faec1 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -24,7 +24,7 @@ def main(): ).show() # phase space evolution - f = run.distributions["kinetic_ions/e1_v1_density/f_binned"] + f = run.distributions.kinetic_ions.e1_v1_density.f_binned view = View(x="e1", y="v1") plot_panels(f, view=view, nrows=3, ncols=4, shared_clim=True, run_label=run.label).show() diff --git a/src/struphy/post_processing/run_output.py b/src/struphy/post_processing/run_output.py index 81faa23e8..dcc18628e 100644 --- a/src/struphy/post_processing/run_output.py +++ b/src/struphy/post_processing/run_output.py @@ -69,22 +69,64 @@ def __array__(self, dtype=None): return np.asarray(self.array, dtype=dtype) -class _ProductNamespace: +class ProductNamespace: + """Hierarchical, discoverable attribute view over product names. + + A product named ``species/slice/value`` is exposed as + ``namespace.species.slice.value``. Product names are simulation-dependent, so + ``__dir__`` is populated from the on-disk catalog for interactive completion. + Use :attr:`catalog` when generic iteration over arbitrary products is needed. + """ + def __init__(self, mapping, prefix=""): self._mapping, self._prefix = mapping, prefix def __getattr__(self, name): key = f"{self._prefix}/{name}" if self._prefix else name if key in self._mapping: - return _ProductValue(self._mapping, key) + return self._mapping[key] prefix = key + "/" if any(product.startswith(prefix) for product in self._mapping): - return _ProductNamespace(self._mapping, key) + return type(self)(self._mapping, key) raise AttributeError(f"{name!r}; available products: {tuple(self._mapping)}") def __getitem__(self, key): + if "/" in key: + return self._mapping[key] return getattr(self, key) + def __iter__(self): + prefix = f"{self._prefix}/" if self._prefix else "" + children = {key[len(prefix):].split("/", 1)[0] for key in self._mapping if key.startswith(prefix)} + return iter(sorted(children)) + + def __len__(self): + return sum(1 for _ in self) + + def __dir__(self): + return sorted(set(super().__dir__()) | set(self)) + + @property + def catalog(self): + """Flat lazy catalog for algorithms that do not know product names.""" + return self._mapping + + +class FieldProducts(ProductNamespace): + """Fields grouped as ``run.fields..``.""" + + +class DistributionProducts(ProductNamespace): + """Binned products grouped as ``run.distributions...``.""" + + +class DensityProducts(ProductNamespace): + """SPH products grouped by species, slice and quantity.""" + + +class OrbitProducts(ProductNamespace): + """Marker trajectories grouped by species.""" + class PlotAccessor: """Convenient plotting entry points bound to a run.""" @@ -134,10 +176,14 @@ def __init__(self, path_out=None, *, sim=None, time_units: str = "physical"): raise FileNotFoundError(f"{self.path_pproc} does not exist; run post-processing first") self.time_units = time_units self._params = self._units = self._time = self._grids_log = self._grids_phy = self._scalars = None - self.fields = ProductMapping(self._discover_fields()) - self.distributions = ProductMapping(self._discover_binned("distribution_function")) - self.densities = ProductMapping(self._discover_binned("n_sph")) - self.orbits = ProductMapping(self._discover_orbits()) + self.field_catalog = ProductMapping(self._discover_fields()) + self.distribution_catalog = ProductMapping(self._discover_binned("distribution_function")) + self.density_catalog = ProductMapping(self._discover_binned("n_sph")) + self.orbit_catalog = ProductMapping(self._discover_orbits()) + self.fields: FieldProducts = FieldProducts(self.field_catalog) + self.distributions: DistributionProducts = DistributionProducts(self.distribution_catalog) + self.densities: DensityProducts = DensityProducts(self.density_catalog) + self.orbits: OrbitProducts = OrbitProducts(self.orbit_catalog) self.plot = PlotAccessor(self) @classmethod @@ -158,15 +204,15 @@ def t_grid(self): @property def f(self): - return _ProductNamespace(self.distributions) + return ProductNamespace(self.distribution_catalog) @property def spline_values(self): - return _ProductNamespace(self.fields) + return ProductNamespace(self.field_catalog) @property def n_sph(self): - return _ProductNamespace(self.densities) + return ProductNamespace(self.density_catalog) @property def params(self): diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py index 0c1923e35..eecbb1806 100644 --- a/src/struphy/post_processing/tests/test_plotting_data.py +++ b/src/struphy/post_processing/tests/test_plotting_data.py @@ -50,10 +50,10 @@ def run(tmp_path): def test_products_are_discovered_without_loading_arrays(run): - assert tuple(run.fields) == ("em_fields/E",) - assert tuple(run.distributions) == ("kinetic_ions/e1_v1_density/f_binned",) + assert tuple(run.fields) == ("em_fields",) + assert tuple(run.distributions) == ("kinetic_ions",) assert tuple(run.orbits) == ("kinetic_ions",) - assert run.fields._cache == {} + assert run.field_catalog._cache == {} def test_field_has_named_and_curvilinear_coordinates(run): @@ -61,7 +61,7 @@ def test_field_has_named_and_curvilinear_coordinates(run): assert field.dims == ("t", "component", "e1", "e2", "e3") assert field.X.dims == ("e1", "e2", "e3") np.testing.assert_allclose(field.isel(t=0, component=2), 2) - assert run.fields._cache["em_fields/E"] is field + assert run.field_catalog._cache["em_fields/E"] is field def test_binned_products_have_coordinates(run): From c0a3e15eeb7d29df4b90b9c92e1723a96d1c0f84 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Tue, 15 Sep 2026 08:28:19 +0200 Subject: [PATCH 017/193] Update postprocessing tutorial for RunOutput API --- tutorials/tutorial_post_processing.ipynb | 156 +++++++++++++---------- 1 file changed, 90 insertions(+), 66 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index f08bd11ff..147cc1478 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -2,12 +2,12 @@ "cells": [ { "cell_type": "markdown", - "id": "postprocessing-intro", + "id": "0", "metadata": {}, "source": [ "# Post-processing and standard plots\n", "\n", - "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, turn its raw output into labeled arrays with `sim.pproc(load=True)`, and make the plots most commonly used to inspect a simulation.\n", + "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, load its output as an autocomplete-friendly `RunOutput`, and make the plots most commonly used to inspect a simulation.\n", "\n", "For a production run you can skip the simulation setup and construct both objects with `path_out=\"path/to/sim\"` instead." ] @@ -15,7 +15,7 @@ { "cell_type": "code", "execution_count": null, - "id": "imports", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -38,12 +38,22 @@ " maxwellians,\n", " perturbations,\n", ")\n", + "from struphy.diagnostics.plotting import (\n", + " GrowthFit,\n", + " InteractiveSliceViewer,\n", + " View,\n", + " plot_marker_trajectories,\n", + " plot_panels,\n", + " plot_scalars,\n", + " plot_slice,\n", + " plot_timeseries,\n", + ")\n", "from struphy.models import VlasovAmpereOneSpecies" ] }, { "cell_type": "markdown", - "id": "demo-heading", + "id": "2", "metadata": {}, "source": [ "## Create a compact demonstration run\n", @@ -54,7 +64,7 @@ { "cell_type": "code", "execution_count": null, - "id": "configure-run", + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -89,7 +99,7 @@ { "cell_type": "code", "execution_count": null, - "id": "run-demo", + "id": "4", "metadata": {}, "outputs": [], "source": [ @@ -115,24 +125,24 @@ }, { "cell_type": "markdown", - "id": "process-heading", + "id": "5", "metadata": {}, "source": [ "## Process and load the output\n", "\n", - "`sim.pproc(load=True)` evaluates saved FEEC fields, organizes particle diagnostics, and returns the loaded plotting data. `physical=True` additionally creates physical field components; `create_vtk=False` keeps this notebook quick. With `force=False`, an existing post-processing directory is reused.\n", + "`sim.pproc(load=True)` evaluates saved FEEC fields, organizes particle diagnostics, and returns a lazy `RunOutput`. `physical=True` additionally creates physical field components; `create_vtk=False` keeps this notebook quick. With `force=False`, a complete post-processing result is reused.\n", "\n", - "The result exposes the output as `StruphyArray` objects. Each array carries named dimensions, coordinates, units, and a display label. The lower-level `PostProcessor` and `PlottingData` classes remain available when processing and loading should be performed separately." + "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Arrays are loaded only when accessed. `RunOutput.open(path_out)` can load an already processed run without running post-processing again." ] }, { "cell_type": "code", "execution_count": null, - "id": "process-load", + "id": "6", "metadata": {}, "outputs": [], "source": [ - "pdata = sim.pproc(\n", + "run = sim.pproc(\n", " physical=True,\n", " create_vtk=False,\n", " force=False,\n", @@ -142,193 +152,205 @@ }, { "cell_type": "markdown", - "id": "discover-text", + "id": "7", "metadata": {}, "source": [ - "The containers are discoverable, so a plotting script does not need to guess what a run saved. Dictionary-style and attribute-style access are both supported." + "Products are arranged into clear namespaces. VS Code and interactive shells can complete the available names after a run is opened: fields are grouped by field species, while distribution and density products are grouped by species and saved slice. Flat catalogs remain available for code that needs to iterate over arbitrary products." ] }, { "cell_type": "code", "execution_count": null, - "id": "inspect-data", + "id": "8", "metadata": {}, "outputs": [], "source": [ - "print(\"scalars:\", pdata.scalars.keys())\n", - "print(\"field species:\", pdata.spline_values.keys())\n", - "print(\"kinetic species:\", pdata.f.keys())\n", - "print(\"particle orbits:\", pdata.orbits.keys())\n", + "print(\"scalars:\", tuple(run.scalars.data_vars))\n", + "print(\"field species:\", tuple(run.fields))\n", + "print(\"distribution species:\", tuple(run.distributions))\n", + "print(\"particle species:\", tuple(run.orbits))\n", + "print(\"all field products:\", tuple(run.field_catalog))\n", "\n", - "phase_space = pdata.f.kinetic_ions[\"e1_v1_density\"][\"f_binned\"]\n", + "phase_space = run.distributions.kinetic_ions.e1_v1_density.f_binned\n", "print(phase_space)\n", "print(\"dimensions:\", phase_space.dims)\n", - "print(\"time coordinate:\", phase_space.coord(\"t\"))" + "print(\"time coordinate:\", phase_space.t)" ] }, { "cell_type": "markdown", - "id": "scalar-heading", + "id": "9", "metadata": {}, "source": [ "## Scalar overview and time series\n", "\n", - "`pdata.plot.scalars()` gives a quick overview of every recorded scalar. If `total_energy` is available, it is also used for the conservation-error panel. Individual time series can be shown on linear or logarithmic axes, and an exponential fit can be restricted to a chosen time interval." + "`plot_scalars()` gives a quick overview of every recorded scalar. If `total_energy` is available, it can also be used for the conservation-error panel. Individual time series can be shown on linear or logarithmic axes, and `GrowthFit` restricts an exponential fit to a chosen time interval. Plot functions return an already-rendered `PlotResult`; calling `.save()` never draws a second figure." ] }, { "cell_type": "code", "execution_count": null, - "id": "scalar-overview", + "id": "10", "metadata": {}, "outputs": [], "source": [ - "pdata.plot.scalars(\n", + "scalar_plot, energy_error = plot_scalars(\n", + " run.scalars,\n", " error_panel=\"total_energy\",\n", - ")" + " run_label=run.label,\n", + ")\n", + "scalar_plot.fig" ] }, { "cell_type": "code", "execution_count": null, - "id": "energy-series", + "id": "11", "metadata": {}, "outputs": [], "source": [ - "electric_energy = pdata.scalars[\"electric_energy\"]\n", - "energy_plot = pdata.plot.time_series(\n", - " \"electric_energy\",\n", + "electric_energy = run.scalars.electric_energy\n", + "energy_plot = plot_timeseries(\n", + " electric_energy,\n", " logy=True,\n", - " fit=True,\n", - " fit_window=(0.0, 0.4 * pdata.units.t),\n", - " fit_of_sqrt=True, # report the field-amplitude rate of this quadratic energy\n", + " fit=GrowthFit(\n", + " window=(0.0, 0.4 * run.units.t),\n", + " amplitude_from_quadratic=True,\n", + " ),\n", " title=\"Electric-field energy\",\n", + " run_label=run.label,\n", ")\n", - "print(\"fit result (gamma, intercept, index window):\", energy_plot.fit_results[0])" + "print(\"growth rate:\", energy_plot.fit_results[0].rate)" ] }, { "cell_type": "markdown", - "id": "data2d-heading", + "id": "12", "metadata": {}, "source": [ "## Two-dimensional data\n", "\n", - "Named selection keeps plots readable. Use `isel` for an integer index and `at` for the point nearest a coordinate value. A single phase-space snapshot can then be passed directly to `pdata.plot.slice()`; coordinates and labels come from the array." + "Named xarray selection keeps plots readable. Use `.isel()` for an integer index and `.sel(..., method=\"nearest\")` for the point nearest a coordinate value. A `View` records the display axes, coordinate system, and reusable selections." ] }, { "cell_type": "code", "execution_count": null, - "id": "final-slice", + "id": "13", "metadata": {}, "outputs": [], "source": [ "final_distribution = phase_space.isel(t=-1)\n", - "pdata.plot.slice(\n", + "plot_slice(\n", " final_distribution,\n", + " view=View(x=\"e1\", y=\"v1\"),\n", " equal_aspect=False,\n", " title=\"Final phase-space distribution\",\n", - ")" + " run_label=run.label,\n", + ").fig" ] }, { "cell_type": "markdown", - "id": "panel-text", + "id": "14", "metadata": {}, "source": [ - "For a compact view of the evolution, `pdata.plot.panels()` chooses evenly spaced snapshots. `shared_clim=True` makes their colors directly comparable." + "For a compact view of the evolution, `plot_panels()` chooses evenly spaced snapshots. The same `View` can later drive an interactive viewer or animation. `shared_clim=True` makes panel colors directly comparable." ] }, { "cell_type": "code", "execution_count": null, - "id": "panels", + "id": "15", "metadata": {}, "outputs": [], "source": [ - "pdata.plot.panels(\n", + "phase_view = View(x=\"e1\", y=\"v1\")\n", + "plot_panels(\n", " phase_space,\n", + " view=phase_view,\n", " nrows=1,\n", " ncols=5,\n", " shared_clim=True,\n", - " equal_aspect=False,\n", " title=\"Phase-space evolution\",\n", - ")" + " run_label=run.label,\n", + ").fig" ] }, { "cell_type": "markdown", - "id": "interactive-heading", + "id": "16", "metadata": {}, "source": [ "## Interactive plots\n", "\n", - "`pdata.plot.slider()` adds a time slider to any `(t, a, b)` array. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the returned object alive so its widget callbacks remain connected." + "`InteractiveSliceViewer` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected." ] }, { "cell_type": "code", "execution_count": null, - "id": "slider", + "id": "17", "metadata": {}, "outputs": [], "source": [ - "phase_slider = pdata.plot.slider(\n", + "phase_viewer = InteractiveSliceViewer(\n", " phase_space,\n", - " equal_aspect=False,\n", - " title=\"Phase-space distribution\",\n", - ")" + " view=phase_view,\n", + " run_label=run.label,\n", + ")\n", + "phase_viewer.draw().fig" ] }, { "cell_type": "markdown", - "id": "orbits-text", + "id": "18", "metadata": {}, "source": [ - "Saved marker orbits use a three-dimensional trajectory plot with a time slider. `max_markers` limits rendering cost for large production runs." + "Saved marker orbits are grouped by species. `plot_marker_trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." ] }, { "cell_type": "code", "execution_count": null, - "id": "orbits-plot", + "id": "19", "metadata": {}, "outputs": [], "source": [ - "orbit_plot = pdata.plot.orbits(\n", - " \"kinetic_ions\",\n", + "orbit_plot = plot_marker_trajectories(\n", + " run.orbits.kinetic_ions,\n", " max_markers=12,\n", " show_paths=True,\n", - ")" + ")\n", + "orbit_plot.fig" ] }, { "cell_type": "markdown", - "id": "save-heading", + "id": "20", "metadata": {}, "source": [ "## Save standard output\n", "\n", - "The same plot objects support `.save(path)`. For a complete scalar report, `save_scalar_plots()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/scalars/`." + "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `run.save_scalar_plots()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/scalars/`." ] }, { "cell_type": "code", "execution_count": null, - "id": "save-output", + "id": "21", "metadata": {}, "outputs": [], "source": [ - "written = pdata.save_scalar_plots()\n", + "written = run.save_scalar_plots()\n", "print(\"Wrote:\")\n", "for path in written:\n", - " print(\" \", os.path.relpath(path, pdata.path_out))" + " print(\" \", os.path.relpath(path, run.path_out))" ] }, { "cell_type": "markdown", - "id": "reuse-heading", + "id": "22", "metadata": {}, "source": [ "## Apply the workflow to another run\n", @@ -337,11 +359,13 @@ "\n", "```python\n", "path_out = \"/path/to/sim_1\"\n", - "from struphy import post_process\n", - "pdata = post_process(path_out=path_out, physical=True, force=False)\n", + "from struphy import RunOutput, post_process\n", + "run = post_process(path_out=path_out, physical=True, force=False)\n", + "# If it is already processed:\n", + "run = RunOutput.open(path_out)\n", "```\n", "\n", - "Use `pdata.scalars`, `pdata.spline_values`, `pdata.f`, `pdata.orbits`, and `pdata.n_sph` to discover and plot the data available in that run." + "Use `run.scalars`, `run.fields`, `run.distributions`, `run.orbits`, and `run.densities`. Attribute access is the normal interactive API; the corresponding `*_catalog` mappings are intended for generic loops and tooling." ] } ], From a9f67df20a53066e46a9193dff274f9a8343a985 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 15 Sep 2026 23:02:17 +0200 Subject: [PATCH 018/193] New post processing API --- .claude/skills/setup-simulation/SKILL.md | 18 +- doc/sections/quickstart.rst | 24 +- doc/sections/userguide.rst | 168 ++-- .../cyclone/pproc_cyclone.py | 6 +- .../itg_cylindre/pproc_drift_kinetic.py | 6 +- .../diocotron_instability/pproc_diocotron.py | 9 +- .../two_stream/pproc_two_stream.py | 7 +- .../cube_strong_scaling/params_poisson.py | 22 +- src/struphy/__init__.py | 9 +- src/struphy/api/post_processing/__init__.py | 36 +- src/struphy/models/tests/utils_testing.py | 8 +- .../post_processing/post_processing_tools.py | 785 ++---------------- .../post_processing/{run_output.py => run.py} | 286 ++++--- src/struphy/post_processing/tests/test_api.py | 68 -- .../tests/test_plotting_data.py | 88 -- .../post_processing/tests/test_pproc.py | 98 +-- src/struphy/post_processing/tests/test_run.py | 223 +++++ src/struphy/simulation/base.py | 10 +- src/struphy/simulation/sim.py | 150 ++-- src/struphy/simulation/tests/test_output.py | 66 ++ .../simulation/tests/test_pproc_api.py | 76 -- tutorials/tutorial_post_processing.ipynb | 32 +- 22 files changed, 742 insertions(+), 1453 deletions(-) rename src/struphy/post_processing/{run_output.py => run.py} (55%) delete mode 100644 src/struphy/post_processing/tests/test_api.py delete mode 100644 src/struphy/post_processing/tests/test_plotting_data.py create mode 100644 src/struphy/post_processing/tests/test_run.py create mode 100644 src/struphy/simulation/tests/test_output.py delete mode 100644 src/struphy/simulation/tests/test_pproc_api.py diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index cdcb405d5..26a2a5aca 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -130,19 +130,19 @@ Key building blocks and where to look them up: ```python import params_ as params # importing does NOT re-run the sim (guarded above) -from struphy import PostProcessor, PlottingData -pp = PostProcessor(sim=params.sim) -pp.process() +run = params.sim.output # or, from anywhere: struphy.open_run() +run.process(physical=True) # optional; products are otherwise processed with defaults on first access -pdata = PlottingData(sim=params.sim) -pdata.load() -# pdata.f...f_binned / grid_e1 / grid_v1, etc. +run.scalars. # xarray time series, no post-processing needed +run.fields.._log # dims (t, [component,] e1, e2, e3) +run.distributions...f_binned # dims (t, ) +run.orbits. # dims (t, marker, attribute) +run.sim.model.units # the Simulation, restored without allocating ``` -For scalar diagnostics (energies, ...) saved every step, read directly from -`/data/data_proc0.hdf5` (`scalar/`, `time/value`) — no -post-processing needed, as shown in `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py`. +Plotting helpers for these arrays live in `src/struphy/diagnostics/plotting.py`; see +`examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. ## Common pitfalls diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index f9ae81e26..5eb9648b5 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -78,14 +78,14 @@ For periodic boundary conditions we will stabilize via ``options``. .. code-block:: python - sim.run(one_time_step=True) + run = sim.run(one_time_step=True) -7. Post-process and load plotting data. +7. Get the output. Fields are post-processed when first accessed and come as labeled + :class:`xarray.DataArray` objects. .. code-block:: python - sim.pproc() - sim.load_plotting_data() + phi = run.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) 8. Compare to the exact solution, and save the figure. @@ -93,9 +93,8 @@ For periodic boundary conditions we will stabilize via ``options``. import matplotlib.pyplot as plt - x = sim.grids_phy[0][:, 0, 0] - t_last = max(sim.spline_values.em_fields.phi_log.data) - phi_num = sim.spline_values.em_fields.phi_log.data[t_last][0][:, 0, 0] + x = phi.X.values + phi_num = phi.values phi_exact = np.cos(k * x) err_max = np.max(np.abs(phi_num - phi_exact)) @@ -148,14 +147,11 @@ Full copy-paste script: grid = grids.TensorProductGrid(num_elements=(64, 1, 1)) sim = Simulation(model=model, domain=domain, grid=grid) - sim.run(one_time_step=True) + run = sim.run(one_time_step=True) - sim.pproc() - sim.load_plotting_data() - - x = sim.grids_phy[0][:, 0, 0] - t_last = max(sim.spline_values.em_fields.phi_log.data) - phi_num = sim.spline_values.em_fields.phi_log.data[t_last][0][:, 0, 0] + phi = run.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) + x = phi.X.values + phi_num = phi.values phi_exact = np.cos(k * x) import matplotlib.pyplot as plt diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index cf6ce9519..9672c9c74 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -499,119 +499,98 @@ After initial conditions are set, launch the run: 11. Post-processing and visualization ------------------------------------- -After ``sim.run()`` finishes, two methods give access to simulation results: - -1. ``sim.pproc()`` — reads raw HDF5 data written during the run, evaluates - spline fields on a grid, and writes processed arrays to disk into a - ``post_processing/`` sub-folder inside the output directory. -2. ``sim.load_plotting_data()`` — reads those processed files back into - memory and attaches the data as attributes on ``sim``. - -The typical post-processing workflow is: +The output of a simulation is a :class:`~struphy.Run`. ``sim.run()`` returns it, +and it stays available as ``sim.output``: .. code-block:: python - sim.run() - sim.pproc() - sim.load_plotting_data() + run = sim.run() + run.scalars.total_energy # scalar time series, straight from the raw output + run.fields.em_fields.e_field_log # evaluated FEEC field (post-processed on first access) + run.sim # the Simulation that produced the output -``sim.pproc()`` -^^^^^^^^^^^^^^^ +Every product is an :class:`xarray.DataArray` with named dimensions +(``t``, ``component``, ``e1``, ``e2``, ``e3``, ``v1``, ...), coordinates and units. +Arrays are read from disk only when accessed. -``pproc`` accepts several keyword arguments that control what is evaluated -and how: +In a separate process, for example a plotting script on a laptop after a cluster +run, open the output folder instead. Nothing is allocated and no MPI is needed; +``run.sim`` is restored from the ``parameters.py`` (or ``config.json``) stored in the +folder: .. code-block:: python - sim.pproc( - step=1, # evaluate every N-th saved time step - celldivide=1, # sub-divide each grid cell for smoother output - physical=False, # also evaluate fields in physical coordinates - guiding_center=False, # compute guiding-center coordinates for markers - classify=False, # classify particles by trapping/passing etc. - create_vtk=True, # write VTK files for 3D visualization - ) + import struphy + + run = struphy.open_run("./runs/vm1s_scan_A/sim_1") + run.sim.domain, run.sim.model.units -All arguments are optional and default to the values shown above. -Use ``step > 1`` to skip snapshots and speed up post-processing on large runs. -Use ``physical=True`` to get field components in physical Cartesian coordinates -in addition to the default logical-coordinate evaluation. +Choosing post-processing options: ``run.process()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -``sim.load_plotting_data()`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Scalars need no post-processing. Fields, binned distribution functions, SPH +densities and orbits are evaluated from the raw HDF5 data and written to a +``post_processing/`` sub-folder of the output directory. Without an explicit call +this happens with default options the first time such a product is accessed. To +choose the options, call ``process`` first: -After calling ``pproc``, ``load_plotting_data`` populates the following -attributes on the ``sim`` object: +.. code-block:: python + + run.process( + step=1, # evaluate every N-th saved time step + celldivide=1, # sub-divide each grid cell for smoother output + physical=False, # also evaluate fields in physical coordinates (*_phy) + guiding_center=False, # compute guiding-center coordinates for markers + classify=False, # classify particles by trapping/passing etc. + create_vtk=False, # write VTK files for 3D visualization + parallel=False, # evaluate fields on all MPI ranks + force=False, # reprocess even if matching products exist + ) -1. ``sim.t_grid`` — 1D array of saved simulation times. -2. ``sim.grids_log`` — list of 3D arrays with logical-coordinate grid points, - one per direction. -3. ``sim.grids_phy`` — list of 3D arrays with physical-coordinate grid points, - one per direction. -4. ``sim.spline_values`` — evaluated FEEC field data, organized by species - and variable name. -5. ``sim.orbits`` — particle-orbit arrays, shape ``(time, particles, attributes)``. -6. ``sim.f`` — binned distribution-function snapshots, organized by species and - phase-space slice. -7. ``sim.n_sph`` — SPH-reconstructed density fields (for SPH-type runs). +All arguments are optional and default to the values shown above. Products that +were already made from the same raw output with the same options are reused, so a +plotting script can be re-run cheaply. Under MPI, call ``process`` on every rank: +serial processing runs on rank 0 while the other ranks wait, and +``parallel=True`` uses the allocated simulation on all ranks. Plotting field data ^^^^^^^^^^^^^^^^^^^^ -FEEC field data is stored under ``sim.spline_values`` indexed by species and -variable name. The inner container for each variable is a dict-like object -mapping a simulation time (float key) to the evaluated array: +Fields are grouped by species and named ``_log`` (logical +components) or ``_phy`` (physical components, with +``physical=True``): .. code-block:: python import matplotlib.pyplot as plt - # Access the electric field log for the em_fields species - e_log = sim.spline_values.em_fields.e_field_log - - # Plot the first component along the first direction at the last saved time - t_last = max(e_log.data) - e1_snapshot = e_log.data[t_last][0][:, 0, 0] # component 0, slice along eta1 - x = sim.grids_phy[0][:, 0, 0] # physical x-coordinates + e_field = run.fields.em_fields.e_field_log # dims (t, component, e1, e2, e3) + snapshot = e_field.isel(t=-1, component=0, e2=0, e3=0) plt.figure() - plt.plot(x, e1_snapshot) + plt.plot(snapshot.X, snapshot) # physical x-coordinate along eta1 plt.xlabel("x") plt.ylabel("E_1") - plt.title(f"Electric field at t = {t_last:.3f}") + plt.title(f"Electric field at t = {float(snapshot.t):.3e}") plt.show() -For a field saved with ``save_data = True``, the variable name in -``spline_values`` follows the pattern ``_log``. - Plotting distribution function slices ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Binned particle data is stored under ``sim.f`` after ``load_plotting_data()``: +Binned particle data is grouped by species and the slice defined in +``BinningPlot(slice=...)``. ``f_binned`` is the full distribution function, +``delta_f_binned`` the perturbation with respect to the background: .. code-block:: python - import matplotlib.pyplot as plt + from struphy.diagnostics.plotting import View, plot_slice - # Retrieve the phase-space slice defined in BinningPlot(slice='e1_v1', ...) - slice_data = sim.f.kinetic_ions.e1_v1 - - # f_binned contains the full-f distribution for each saved time step - # delta_f_binned is the perturbation w.r.t. the background - t_last = max(slice_data.f_binned) - f2d = slice_data.f_binned[t_last] - - plt.figure() - plt.imshow(f2d.T, origin="lower", aspect="auto") - plt.xlabel("eta1 bin") - plt.ylabel("v1 bin") - plt.colorbar(label="f") - plt.title(f"Phase-space distribution at t = {t_last:.3f}") - plt.show() + f = run.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) + plot_slice(f.isel(t=-1), view=View(x="e1", y="v1")).show() Plotting particle orbits @@ -619,52 +598,33 @@ Plotting particle orbits If ``n_markers > 0`` was set in :class:`~struphy.particles.parameters.SavingParameters`, individual marker -trajectories are available under ``sim.orbits``: +trajectories are available under ``run.orbits``: .. code-block:: python import matplotlib.pyplot as plt - # Shape: (n_timesteps, n_saved_markers, n_attributes) - # Column layout: [id, eta1, eta2, eta3, v1, v2, v3, weight] - orb = sim.orbits.kinetic_ions + orbits = run.orbits.kinetic_ions # dims (t, marker, attribute) + marker = orbits.isel(marker=0) plt.figure() - plt.plot(orb[:, 0, 1], orb[:, 0, 3]) # eta1 vs eta3 for marker 0 - plt.xlabel("eta1") - plt.ylabel("eta3") + plt.plot(marker.isel(attribute=0), marker.isel(attribute=2)) # position x vs z + plt.xlabel("x") + plt.ylabel("z") plt.title("Marker orbit (particle 0)") plt.show() -Loading data without a ``sim`` object -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Post-processed data can also be loaded directly from an output folder path, -without needing to reconstruct the ``Simulation`` object: - -.. code-block:: python - - from struphy.post_processing.post_processing_tools import PlottingData - - pdata = PlottingData(path_out="./runs/vm1s_scan_A/sim_1") - pdata.load() - - # All the same attributes are available directly on pdata: - x = pdata.grids_phy[0][:, 0, 0] - e_log = pdata.spline_values.em_fields.e_field_log - - VTK output for ParaView and PyVista ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you call ``sim.pproc(create_vtk=True)``, Struphy writes structured-grid VTK +If you call ``run.process(create_vtk=True)``, Struphy writes structured-grid VTK files (``.vts``) inside the post-processing folder, grouped by species. Typical locations are: 1. ``/post_processing/fields_data//vtk/*.vts`` 2. ``/post_processing/fields_data//vtk_phy/*.vts`` - (if ``physical=True`` was requested in ``pproc``) + (if ``physical=True`` was requested) You can discover all generated VTK files with: @@ -708,7 +668,7 @@ Open in PyVista (Python workflow): pl.show() This VTK path is usually the fastest way to inspect full 3D structure in large -runs, while ``sim.load_plotting_data()`` is often more convenient for custom +runs, while ``Run`` is often more convenient for custom Matplotlib analysis scripts. @@ -922,7 +882,7 @@ Gantt charts and flame graphs. Note that ``profiling_data.h5`` is a plain ``scope-profiler`` output file, so it is post-processed with ``scope-profiler`` itself rather than with -``sim.pproc()`` — the two are independent post-processing paths. +``run.process()`` — the two are independent post-processing paths. Post-processing with the ``scope-profiler`` CLI diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 3582783fb..13c666091 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,7 +1,7 @@ import os import sys -from struphy import PostProcessor, RunOutput +from struphy import open_run from struphy.diagnostics.plotting import ( GrowthFit, InteractiveSliceViewer, @@ -31,9 +31,7 @@ def main(path_out): - PostProcessor(path_out=path_out).process(physical=True, force=False) - - run = RunOutput.open(path_out) + run = open_run(path_out).process(physical=True) # growth rate of the electrostatic potential plot_timeseries( diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 6eddbe2e2..99dc430ee 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,7 +1,7 @@ import os import sys -from struphy import PostProcessor, RunOutput +from struphy import open_run from struphy.diagnostics.plotting import ( GrowthFit, InteractiveSliceViewer, @@ -31,9 +31,7 @@ def main(path_out): - PostProcessor(path_out=path_out).process(physical=True, force=False) - - run = RunOutput.open(path_out) + run = open_run(path_out).process(physical=True) # growth rate of the electrostatic potential plot_timeseries( diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 9477d38b6..dff3ed9b6 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -7,7 +7,7 @@ import os import sys -from struphy import PostProcessor, RunOutput +from struphy import open_run from struphy.diagnostics.plotting import ( GrowthFit, InteractiveSliceViewer, @@ -34,13 +34,8 @@ ] -def load(path_out): - PostProcessor(path_out=path_out).process(physical=True, force=False) - return RunOutput.open(path_out) - - def main(paths): - runs = {os.path.basename(p): load(p) for p in paths} + runs = {os.path.basename(p): open_run(p).process(physical=True) for p in paths} # growth rate of the electrostatic energy, one curve per run series = [] diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 8fe1faec1..fba90fca4 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,20 +1,17 @@ import params_two_stream as params -from struphy import PostProcessor, RunOutput from struphy.diagnostics.plotting import InteractiveSliceViewer, View, plot_panels, plot_timeseries def main(): - PostProcessor(sim=params.sim).process(force=False) - - run = RunOutput.open(sim=params.sim) + run = params.sim.output # every scalar at every time step: post_processing/scalars/{scalars.csv,*.png} run.save_scalar_plots() # electric field growth against the analytical rate (0.2845 in units of m/c) energy = run.scalars["electric_energy"] - analytical = energy.copy(data=10 ** (0.2845 / run.units.t * energy.t - 5.3)) + analytical = energy.copy(data=10 ** (0.2845 / run.sim.model.units.t * energy.t - 5.3)) analytical.attrs["label"] = "analytical" plot_timeseries( diff --git a/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py b/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py index 94e947380..22626741c 100644 --- a/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py +++ b/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py @@ -129,8 +129,8 @@ def rhs_fun(x, y, z): if __name__ == "__main__": - sim.run(profiling_activated=True, one_time_step=True) - sim.pproc(parallel_pproc=True) + run = sim.run(profiling_activated=True, one_time_step=True) + run.process(create_vtk=True, parallel=True) def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): from matplotlib import pyplot as plt @@ -203,20 +203,14 @@ def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): return fig if sim.comm.rank == 0: - sim.load_plotting_data() - - Tstart = sim.t_grid[0] - rhs_data = sim.spline_values.em_fields.source_log + rhs_data = run.fields.em_fields.source_log print(rhs_data) - rhs = rhs_data.data[Tstart][0] - - Tend = sim.t_grid[-1] - phi_data = sim.spline_values.em_fields.phi_log + rhs = rhs_data.isel(t=0).values + + phi_data = run.fields.em_fields.phi_log print(phi_data) - phi = phi_data.data[Tend][0] - x = sim.grids_phy[0] - y = sim.grids_phy[1] - z = sim.grids_phy[2] + phi = phi_data.isel(t=-1).values + x, y, z = run.grids_phy slice_pt_x = x.shape[0] // 2 slice_pt_y = y.shape[1] // 2 diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index def4f68b4..9ea4c320c 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -167,8 +167,7 @@ def setup_logging(logging_level: int = logging.WARNING): WeightsParameters, ) from struphy.api.perturbations import perturbations -from struphy.api.post_processing import PlottingData, PostProcessor, RunOutput -from struphy.api.post_processing import post_process +from struphy.api.post_processing import Run, open_run from struphy.api.simulation import Simulation __all__ = [ @@ -192,9 +191,7 @@ def setup_logging(logging_level: int = logging.WARNING): "DerhamOptions", "FieldsBackground", "ButcherTableau", - "PostProcessor", - "RunOutput", - "PlottingData", - "post_process", + "Run", + "open_run", "Simulation", ] diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index e5abe010f..7cbab63b2 100644 --- a/src/struphy/api/post_processing/__init__.py +++ b/src/struphy/api/post_processing/__init__.py @@ -1,35 +1,3 @@ -from struphy.post_processing.post_processing_tools import PostProcessor -from struphy.post_processing.run_output import RunOutput +from struphy.post_processing.run import Run, open_run -PlottingData = RunOutput - - -def post_process( - sim=None, - path_out: str = None, - *, - step: int = 1, - celldivide=1, - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - create_vtk: bool = True, - force: bool = False, -) -> RunOutput: - """Process a completed run and return its lazy :class:`RunOutput`.""" - if sim is not None: - return sim.pproc(step=step, celldivide=celldivide, physical=physical, - guiding_center=guiding_center, classify=classify, - create_vtk=create_vtk, force=force, load=True) - if path_out is None: - raise ValueError("path_out or sim is required") - processor = PostProcessor(sim=None, path_out=path_out) - processor.process(step=step, celldivide=celldivide, physical=physical, - guiding_center=guiding_center, classify=classify, - create_vtk=create_vtk, force=force) - data = PlottingData(sim=None, path_out=path_out) - data.load() - return data - - -__all__ = ["PostProcessor", "RunOutput", "PlottingData", "post_process"] +__all__ = ["Run", "open_run"] diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 4e999aaa6..5650aa951 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -124,13 +124,15 @@ def call_test(model: StruphyModel, test_profiling: bool = False): time_opts.Tend += time_opts.dt sim.show_parameters() - sim.run(profiling_activated=test_profiling) + run = sim.run(profiling_activated=test_profiling) if comm is not None: comm.Barrier() + run.process(create_vtk=True) if rank == 0: - sim.pproc() - sim.load_plotting_data() + # discover (but do not load) every product + for catalog in (run.field_catalog, run.distribution_catalog, run.density_catalog, run.orbit_catalog): + tuple(catalog) shutil.rmtree(test_folder) if comm is not None: comm.Barrier() diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 25d526f4a..93d692129 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1,4 +1,3 @@ -import inspect import hashlib import json import logging @@ -17,25 +16,11 @@ from pyevtk.hl import gridToVTK from struphy.feec.psydac_derham import Derham, SplineFunction -from struphy.fields_background.base import FluidEquilibrium -from struphy.geometry.base import Domain -from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, Time -from struphy.io.setup import import_parameters_py -from struphy.kinetic_background import maxwellians from struphy.kinetic_background.base import KineticBackground -from struphy.models.base import StruphyModel from struphy.models.species import ParticleSpecies from struphy.models.variables import PICVariable, SPHVariable from struphy.pic.base import Particles -from struphy.post_processing.arrays import ( - data_array, - save_scalars, - wrap_binned_data, - wrap_field_data, - wrap_orbits, -) from struphy.post_processing.orbits import orbits_tools -from struphy.topology.grids import TensorProductGrid from struphy.utils.progress import tqdm if TYPE_CHECKING: @@ -47,221 +32,67 @@ PUSH_KINDS = {"H1": "0", "Hcurl": "1", "Hdiv": "2", "L2": "3", "H1vec": "v"} -class Container: - """Mapping access over attributes set by the loader, so contents are discoverable.""" +MANIFEST_SCHEMA_VERSION = 1 - def keys(self): - return tuple(k for k in self.__dict__ if not k.startswith("_")) - def __getitem__(self, key): - try: - return self.__dict__[key] - except KeyError: - raise KeyError(f"{key!r} not found, available: {self.keys()}") from None - - def __contains__(self, key): - return key in self.keys() - - def __iter__(self): - return iter(self.keys()) - - def __len__(self): - return len(self.keys()) - - -class SplineValues(Container): - def __str__(self): - out = "" - for name, species in inspect.getmembers(self): - if isinstance(species, SpecHolder): - out += f" {name}\n" - out += f"{species}" - return out - - -class Orbits(Container): - def __str__(self): - out = "" - for species, orbits in self.__dict__.items(): - shp = orbits.shape - out += f" {species}, shape = {shp}\n" - out += f" Number of time points: {shp[0]}\n" - out += f" Number of particles: {shp[1]}\n" - out += f" Number of attributes: {shp[2]}\n" - return out - - -class DistributionFunction(Container): - def __str__(self): - out = "" - for name, species in inspect.getmembers(self): - if isinstance(species, SpecHolder): - out += f" {name}\n" - out += f"{species}" - return out - - -class DensitySPH(Container): - def __str__(self): - out = "" - for name, species in inspect.getmembers(self): - if isinstance(species, SpecHolder): - out += f" {name}\n" - out += f"{species}" - return out - - -class SpecHolder(Container): - def __str__(self): - out = "" - for name, val in self.__dict__.items(): - out += f" {name}\n" - return out - - -class Slice(Container): - pass - - -class Scalars(Container): - """Time series recorded every ``save_step``-th step, read straight from the raw HDF5 output. - - Unlike the other containers this needs no prior call to :meth:`PostProcessor.process`. - """ - - def __str__(self): - out = "" - for name in self.keys(): - out += f" {name}\n" - return out - - -class DataDict: - def __init__(self, data: dict, grids_log=None, name: str = ""): - self.data = data - self.grids_log = grids_log - self.name = name - self._array = None +def source_fingerprint(path_out: str) -> str: + """Fingerprint the raw run files that determine post-processing products.""" + digest = hashlib.sha256() + for name in ("config.json", "parameters.py", "meta.yml", "data/data_proc0.hdf5"): + path = os.path.join(path_out, name) + if not os.path.exists(path): + continue + stat = os.stat(path) + digest.update(name.encode()) + digest.update(f"{stat.st_size}:{stat.st_mtime_ns}".encode()) + if name != "data/data_proc0.hdf5": + with open(path, "rb") as stream: + digest.update(stream.read()) + return digest.hexdigest() - @property - def array(self): - """The field as one labeled array with dims ``(t, comp, e1, e2, e3)``. - Built on first access; ``data`` remains the raw time-keyed dict. - """ - if self._array is None: - self._array = wrap_field_data(self.data, self.grids_log, label=self.name) - return self._array - - def __str__(self): - out = f"{type(self.data) = }\n" - out += f"{len(self.data) = }\n" - for key, d in self.data.items(): - if isinstance(d, list): - shp = [comp.shape for comp in d] - else: - shp = d.shape - out += f"{key = }".ljust(25) - out += f"shape = {shp}\n" - return out +def normalize_options(**options) -> dict: + """JSON-comparable processing options, as stored in the manifest.""" + celldivide = options.get("celldivide") + if celldivide is not None: + options["celldivide"] = [int(celldivide)] * 3 if isinstance(celldivide, int) else [int(c) for c in celldivide] + return options -class ParamsIn: - """Holds the input parameters of a Struphy simulation as attributes. +def is_processed(path_out: str, options: dict | None = None) -> bool: + """Whether ``path_out`` holds complete post-processing of its current raw output. - Parameters - ---------- - path : str - Absolute path of simulation output folder. + With ``options``, the stored processing options must match as well, so a request for + different products (e.g. ``physical=True``) is never answered with stale ones. """ - - def __init__( - self, - path: str, - ): - logger.info(f"\nReading in parameters from {path} ... ") - - params_path = os.path.join(path, "parameters.py") - json_path = os.path.join(path, "config.json") - - if os.path.exists(params_path): - params_in = import_parameters_py(params_path) - env = params_in.env - time_opts = params_in.time_opts - domain = params_in.domain - equil = params_in.equil - grid = params_in.grid - derham_opts = params_in.derham_opts - model = params_in.model - sim = params_in.sim - - elif os.path.exists(json_path): - with open(json_path, "r") as f: - dct = json.load(f) - env = EnvironmentOptions.from_dict(dct["env"]) - time_opts = Time.from_dict(dct["time_opts"]) - domain: Domain = Domain.from_dict(dct["domain"]) - equil = FluidEquilibrium.from_dict(dct.get("equil")) - - grid_dct = dct.get("grid") - if grid_dct is not None: - grid_dct = dict(grid_dct) - if "num_elements" in grid_dct and grid_dct["num_elements"] is not None: - grid_dct["num_elements"] = tuple(grid_dct["num_elements"]) - if "mpi_dims_mask" in grid_dct and grid_dct["mpi_dims_mask"] is not None: - grid_dct["mpi_dims_mask"] = tuple(grid_dct["mpi_dims_mask"]) - grid = TensorProductGrid.from_dict(grid_dct) - else: - grid = None - - derham_dct = dct.get("derham_opts") - if derham_dct is not None: - derham_dct = dict(derham_dct) - if "degree" in derham_dct and derham_dct["degree"] is not None: - derham_dct["degree"] = tuple(derham_dct["degree"]) - if "bcs" in derham_dct and derham_dct["bcs"] is not None: - derham_dct["bcs"] = tuple(None if bc is None else tuple(bc) for bc in derham_dct["bcs"]) - if "nquads" in derham_dct and derham_dct["nquads"] is not None: - derham_dct["nquads"] = tuple(derham_dct["nquads"]) - if "nquads_proj" in derham_dct and derham_dct["nquads_proj"] is not None: - derham_dct["nquads_proj"] = tuple(derham_dct["nquads_proj"]) - derham_opts = DerhamOptions.from_dict(derham_dct) - else: - derham_opts = None - - model: StruphyModel = StruphyModel.from_dict(dct["model"]) - sim = None - - else: - raise FileNotFoundError(f"Neither of the paths {params_path} or {json_path} exists.") - - logger.info("... Done.") - - self.env = env - self.time_opts = time_opts - self.domain = domain - self.equil = equil - self.grid = grid - self.derham_opts = derham_opts - self.model = model - self.sim = sim + path = os.path.join(path_out, "post_processing", "manifest.json") + try: + with open(path) as stream: + manifest = json.load(stream) + except (OSError, ValueError): + return False + return ( + manifest.get("schema_version") == MANIFEST_SCHEMA_VERSION + and manifest.get("status") == "complete" + and manifest.get("source_fingerprint") == source_fingerprint(path_out) + and (options is None or manifest.get("options") == normalize_options(**options)) + ) class PostProcessor: - """Post-process results from a finished Struphy simulation. + """Post-process the raw output of a finished Struphy simulation. - This class collects and processes output data produced by a completed Struphy run. It can be - constructed either from a finished :class:`Simulation` object or from a path to an output - directory produced by a previous run. + Users do not call this directly; use :meth:`struphy.Run.process`, which also decides + on which MPI ranks processing runs. Parameters ---------- - sim : Simulation, optional - Simulation object of a finished run. If provided, its metadata and output paths are used. - path_out : str, optional - Path to the Struphy output folder. Required if ``sim`` is not given. + sim : Simulation + Simulation of the run, either the one that ran or one restored with + :meth:`Simulation.from_output`. Its ``env.path_out`` locates the output. parallel_pproc : bool, optional - Whether to run post-processing in parallel using MPI. Default is False (serial post-processing). + Whether to run post-processing in parallel using MPI. This requires an allocated + ``sim`` and a call on every rank. Default is False (serial post-processing). Attributes ---------- @@ -279,63 +110,30 @@ class PostProcessor: Number of MPI ranks used to produce the output. """ - def __init__( - self, - sim: "Simulation" = None, - path_out: str = None, - parallel_pproc: bool = False, - ): - - # import simulation parameters from sim object or from path_out - if sim is None: - assert path_out is not None, ( - "If no sim object is provided, a path_out must be given to retrieve the parameters of the run to post-process." - ) - params_in = ParamsIn(path=path_out) - grid = params_in.grid - derham_opts = params_in.derham_opts - domain = params_in.domain - model = params_in.model - imported_sim = params_in.sim - else: - path_out = sim.env.path_out - grid = sim.grid - derham_opts = sim.derham_opts - domain = sim.domain - model = sim.model - imported_sim = sim - - # create post-processing folder - self.path_out = path_out - self.path_pproc = os.path.join(path_out, "post_processing") - - # parallel post-processing (default: False) + def __init__(self, sim: "Simulation", parallel_pproc: bool = False): + self.path_out = sim.env.path_out + self.path_pproc = os.path.join(self.path_out, "post_processing") self.parallel_pproc = parallel_pproc # struphy objects needed for post-processing - self.domain = domain - self.model = model + self.domain = sim.domain + self.model = sim.model if self.parallel_pproc: - assert imported_sim is not None, "Parallel post-processing only supported when the sim object is provided." - self.derham = imported_sim.derham + assert sim.derham is not None, "Parallel post-processing needs an allocated simulation." + self.derham = sim.derham self.comm = self.derham.comm self.comm_size = self.comm.Get_size() self.rank = self.comm.Get_rank() self.range_ranks = range(self.rank, self.rank + 1) else: - if grid is None or derham_opts is None: + if sim.grid is None or sim.derham_opts is None: self.derham = None else: - self.derham = Derham( - grid, - derham_opts, - comm=None, - domain=domain, - ) + self.derham = Derham(sim.grid, sim.derham_opts, comm=None, domain=sim.domain) self.comm = MockComm() # get number of MPI ranks used in the simulation from meta.yml - with open(os.path.join(path_out, "meta.yml"), "r") as f: + with open(os.path.join(self.path_out, "meta.yml"), "r") as f: meta = yaml.load(f, Loader=yaml.FullLoader) self.comm_size = meta["MPI processes"] self.rank = 0 @@ -347,43 +145,13 @@ def __init__( os.makedirs(self.path_pproc, exist_ok=True) self.comm.Barrier() - @property - def is_processed(self) -> bool: - """Whether a complete manifest matches the current raw run.""" - path = os.path.join(self.path_pproc, "manifest.json") - if not os.path.exists(path): - return False - try: - with open(path) as stream: - manifest = json.load(stream) - return (manifest.get("schema_version") == 1 - and manifest.get("status") == "complete" - and manifest.get("source_fingerprint") == self._source_fingerprint()) - except (OSError, ValueError): - return False - - def _source_fingerprint(self): - """Fingerprint inputs that determine post-processing products.""" - digest = hashlib.sha256() - for name in ("config.json", "parameters.py", "meta.yml", "data/data_proc0.hdf5"): - path = os.path.join(self.path_out, name) - if not os.path.exists(path): - continue - stat = os.stat(path) - digest.update(name.encode()) - digest.update(f"{stat.st_size}:{stat.st_mtime_ns}".encode()) - if name != "data/data_proc0.hdf5": - with open(path, "rb") as stream: - digest.update(stream.read()) - return digest.hexdigest() - def _write_manifest(self, status, *, options=None, error=None): if self.rank != 0: return manifest = { - "schema_version": 1, + "schema_version": MANIFEST_SCHEMA_VERSION, "status": status, - "source_fingerprint": self._source_fingerprint(), + "source_fingerprint": source_fingerprint(self.path_out), "options": options or {}, } if error is not None: @@ -445,13 +213,13 @@ def process( bool Whether post-processing actually ran. """ - if not force and self.is_processed: + options = normalize_options(step=step, celldivide=celldivide, physical=physical, + guiding_center=guiding_center, classify=classify, create_vtk=create_vtk) + if not force and is_processed(self.path_out, options): logger.warning(f"\nReusing existing post-processing in {self.path_pproc}") return False self._reset_pproc_dir() - options = {"step": step, "celldivide": celldivide, "physical": physical, - "guiding_center": guiding_center, "classify": classify, "create_vtk": create_vtk} self._write_manifest("processing", options=options) logger.warning(f"\nPost-processing path {self.path_out}") @@ -1454,436 +1222,3 @@ def _post_process_n_sph( if self.rank == 0: # save sph density xp.save(os.path.join(path_view, "n_sph.npy"), data) - - -class LegacyPlottingData: - """Container for loading and accessing post-processed Struphy simulation data. - - This class provides convenient access to field data (spline values), particle orbits, - distribution functions, and SPH density fields that were generated by - :class:`PostProcessor`. Data is organized hierarchically by species and variable/view - and is exposed via read-only properties. - - Parameters - ---------- - sim : Simulation, optional - Simulation object of a completed run. If provided, its output path is used. - path_out : str, optional - Path to the Struphy output folder. Required if ``sim`` is not given. - - Raises - ------ - AssertionError - If neither ``sim`` nor ``path_out`` is provided, or if the post-processing - directory does not exist (call :meth:`PostProcessor.process` first). - - Attributes - ---------- - path_pproc : str - Path to the post-processing directory. - t_grid : xp.ndarray or None - Time grid (loaded after calling :meth:`load`). - grids_log : list of xp.ndarray or None - Logical coordinate grids (loaded after calling :meth:`load`). - grids_phy : list of xp.ndarray or None - Physical coordinate grids (loaded after calling :meth:`load`). - - Examples - -------- - >>> pdata = PlottingData(path_out=\"/path/to/sim/output\") - >>> pdata.load() - >>> # Access particle orbits for species 'electrons' - >>> orbits_e = pdata.orbits.electrons # shape: (time, particles, attributes) - >>> # Access field values - >>> E_log = pdata.spline_values.electrons.E_log # logical components - """ - - def __init__(self, sim: "Simulation" = None, path_out: str = None): - - if sim is None: - assert path_out is not None, ( - "If no sim object is provided, a path_out must be given to retrieve the parameters of the run to post-process." - ) - else: - path_out = sim.env.path_out - - self.path_out = path_out - self.path_pproc = os.path.join(path_out, "post_processing") - assert os.path.exists(self.path_pproc), f"Path {self.path_pproc} does not exist, run 'pproc' first?" - - # dictionaries to hold data - self._orbits = Orbits() - self._f = DistributionFunction() - self._spline_values = SplineValues() - self._n_sph = DensitySPH() - self._scalars = Scalars() - self._params = None - self._units = None - self.grids_log: list[xp.ndarray] = None - self.grids_phy: list[xp.ndarray] = None - self.t_grid: xp.ndarray = None - - @property - def params(self) -> ParamsIn: - """Input parameters of the run, read from the output folder on first access. - - Removes the need for a plotting script to import the simulation's ``params_*.py``. - """ - if self._params is None: - self._params = ParamsIn(self.path_out) - return self._params - - @property - def domain(self) -> Domain: - """Domain of the run, for mapping logical to physical coordinates.""" - return self.params.domain - - @property - def units(self): - """Fully derived :class:`~struphy.physics.physics.Units` of the run. - - Replaces the ``Units(base_units)`` / ``derive_units(...)`` sequence that every - plotting script would otherwise repeat. - """ - if self._units is None: - from struphy.physics.physics import Units - - model = self.params.model - units = Units(model.base_units) - bulk = model.bulk_species - units.derive_units( - velocity_scale=model.velocity_scale, - A_bulk=None if bulk is None else bulk.mass_number, - Z_bulk=None if bulk is None else bulk.charge_number, - ) - self._units = units - return self._units - - @property - def scalars(self) -> Scalars: - """Scalar time series recorded every ``save_step``-th step, keyed by name. - - Each entry is a :class:`~struphy.post_processing.arrays.StruphyArray` over ``t``, - with the time coordinate already converted to seconds. - - Returns - ------- - Scalars - Container supporting ``.keys()``, ``["name"]`` and attribute access. - """ - return self._scalars - - @property - def orbits(self) -> Orbits: - """Particle orbit data by species. - - Returns - ------- - Orbits - Container where attributes are species names. Each species attribute holds - a 3D array indexed by (t, p, a): t = time step, p = particle index, - a = attribute index (id, position_xyz, velocities, weight, etc.). - """ - return self._orbits - - @property - def f(self) -> DistributionFunction: - """Distribution function data by species. - - Returns - ------- - DistributionFunction - Container where attributes are species names. Each species holds a dict-like - object mapping slice names (e.g., 'e1_v1', 'e2_v2') to slice containers, - which store arrays like 'f_binned', 'delta_f_binned' for plotting. - """ - return self._f - - @property - def spline_values(self) -> SplineValues: - """Field (spline) values by species. - - Returns - ------- - SplineValues - Container where attributes are species names. Each species holds a dict-like - object mapping variable names (e.g., 'E_log', 'B_phy') to ``DataDict`` - objects containing evaluated field arrays on the grid. - """ - return self._spline_values - - @property - def n_sph(self) -> DensitySPH: - """SPH density fields by species. - - Returns - ------- - DensitySPH - Container where attributes are species names. Each species holds a dict-like - object mapping view names (e.g., 'view_0', 'view_1') to slice containers, - which store arrays like 'n_sph' and associated grids for plotting. - """ - return self._n_sph - - @property - def plot(self): - """Plotting methods bound to this run's data and metadata. - - Examples - -------- - >>> pdata.plot.scalars() - >>> pdata.plot.time_series("electric_energy", fit=True) - >>> pdata.plot.slider(pdata.f.kinetic_ions["e1_v1_density"]["f_binned"]) - """ - if not hasattr(self, "_plot_accessor"): - # Keep matplotlib and the plotting implementation out of the data-loading - # import path until a plot is actually requested. - from struphy.diagnostics.plotting import PlottingAccessor - - self._plot_accessor = PlottingAccessor(self) - return self._plot_accessor - - def load_scalars(self, *, physical_time: bool = True): - """Read the ``scalar`` group of the raw HDF5 output into :attr:`scalars`. - - Post-processing is not required for these, so this may be called on its own. - - Parameters - ---------- - physical_time : bool - Scale the time coordinate to seconds using the run's units. Set False to - keep Struphy time units. - """ - path_data = os.path.join(self.path_out, "data", "data_proc0.hdf5") - if not os.path.exists(path_data): - logger.warning(f"No raw data at {path_data}, skipping scalars.") - return self._scalars - - unit_t = self.units.t if physical_time else 1.0 - t_unit_label = "s" if physical_time else "a.u." - - with h5py.File(path_data, "r") as f: - if "scalar" not in f: - logger.warning(f"No scalar diagnostics saved in {path_data}, skipping scalars.") - return self._scalars - t = xp.asarray(f["time"]["value"][()]) * unit_t - for name in f["scalar"].keys(): - arr = data_array( - xp.asarray(f["scalar"][name][()]), - dims=("t",), - coords={"t": t}, - name=name, - label=name.replace("_", " "), - coord_units={"t": t_unit_label}, - ) - setattr(self._scalars, name, arr) - - logger.info(f"Loaded scalars: {self._scalars.keys()}") - return self._scalars - - def save_scalars(self, path: str = None, **kwargs) -> str: - """Write every scalar, at every time step, as one table. - - Parameters - ---------- - path : str, optional - Destination; the format follows its suffix. Defaults to - ``post_processing/scalars.csv`` in the output folder. - **kwargs - Passed to :func:`~struphy.post_processing.arrays.save_scalars`. - """ - if not self._scalars.keys(): - self.load_scalars() - if path is None: - path = os.path.join(self.path_pproc, "scalars.csv") - return save_scalars(self._scalars, path, **kwargs) - - def save_scalar_plots(self, directory: str = None, **kwargs) -> list[str]: - """Write the table, an overview figure and one figure per scalar. - - Parameters - ---------- - directory : str, optional - Defaults to ``post_processing/scalars`` in the output folder. - **kwargs - Passed to :func:`~struphy.diagnostics.plotting.save_all_scalars`. - """ - from struphy.diagnostics.plotting import save_all_scalars - - if not self._scalars.keys(): - self.load_scalars() - if directory is None: - directory = os.path.join(self.path_pproc, "scalars") - # not setdefault: reading the parameters must not be forced when they are given - if "params" not in kwargs: - kwargs["params"] = self.params - return save_all_scalars(self._scalars, directory, **kwargs) - - def load(self): - """Load all post-processed data from disk into memory. - - Reads binary pickle files (``.bin``) and NumPy archives (``.npy``) from the - post-processing directory. Populates ``self.t_grid``, ``self.grids_log``, - ``self.grids_phy``, and all species-dependent data properties (orbits, f, - spline_values, n_sph). - - Raises - ------ - FileNotFoundError - If expected post-processing files are missing. - NotImplementedError - If an unexpected data folder structure is encountered. - """ - logger.warning("\nLoading post-processed plotting data:") - logger.warning(f"Data path: {self.path_pproc}") - - # load time grid - self.t_grid = xp.load(os.path.join(self.path_pproc, "t_grid.npy")) - - self.load_scalars() - - # data paths - path_fields = os.path.join(self.path_pproc, "fields_data") - path_kinetic = os.path.join(self.path_pproc, "kinetic_data") - - # load point data - if os.path.exists(path_fields): - # grids - with open(os.path.join(path_fields, "grids_log.bin"), "rb") as f: - self.grids_log = pickle.load(f) - with open(os.path.join(path_fields, "grids_phy.bin"), "rb") as f: - self.grids_phy = pickle.load(f) - - # species folders - species = next(os.walk(path_fields))[1] - for spec in species: - spec_holder = SpecHolder() - setattr(self.spline_values, spec, spec_holder) - # self.arrays[spec] = {} - path_spec = os.path.join(path_fields, spec) - wlk = os.walk(path_spec) - files = next(wlk)[2] - logger.info(f"\nFiles in {path_spec}: {files}") - for file in files: - if ".bin" in file: - var = file.split(".")[0] - with open(os.path.join(path_spec, file), "rb") as f: - # try: - data_dict = DataDict(pickle.load(f), self.grids_log, var) - setattr(spec_holder, var, data_dict) - # self.arrays[spec][var] = pickle.load(f) - - if os.path.exists(path_kinetic): - # species folders - species = next(os.walk(path_kinetic))[1] - for spec in species: - path_spec = os.path.join(path_kinetic, spec) - wlk = os.walk(path_spec) - sub_folders = next(wlk)[1] - for folder in sub_folders: - path_dat = os.path.join(path_spec, folder) - sub_wlk = os.walk(path_dat) - - if "orbits" in folder: - files = next(sub_wlk)[2] - Nt = len(files) // 2 - n = 0 - arr = None - for file in files: - # logger.info(f"{file = }") - if ".npy" in file: - step = int(file.split(".")[0].split("_")[-1]) - tmp = xp.load(os.path.join(path_dat, file)) - if n == 0: - arr = xp.zeros((Nt, *tmp.shape), dtype=float) - arr[step] = tmp - n += 1 - if arr is not None: - setattr(self.orbits, spec, wrap_orbits(arr, self.t_grid[:Nt])) - - elif "distribution_function" in folder: - spec_holder = SpecHolder() - setattr(self.f, spec, spec_holder) - slices = next(sub_wlk)[1] - # logger.info(f"{slices = }") - for sli in slices: - s = Slice() - setattr(spec_holder, sli, s) - # logger.info(f"{sli = }") - files = next(sub_wlk)[2] - # logger.info(f"{files = }") - for file in files: - name = file.split(".")[0] - tmp = xp.load(os.path.join(path_dat, sli, file)) - logger.info(f"{name = }") - setattr(s, name, tmp) - grids = {key.removeprefix("grid_"): getattr(s, key) for key in s.keys() - if key.startswith("grid_")} - dims = tuple(part for part in sli.split("_") if part in grids) - for name in tuple(s.keys()): - values = getattr(s, name) - if name.startswith("grid_") or not hasattr(values, "shape"): - continue - expected = (len(self.t_grid), *(len(grids[dim]) for dim in dims)) - if values.shape == expected: - setattr(s, name, wrap_binned_data(values, dims, - {"t": self.t_grid, **{dim: grids[dim] for dim in dims}}, name=name)) - - elif "n_sph" in folder: - spec_holder = SpecHolder() - setattr(self.n_sph, spec, spec_holder) - slices = next(sub_wlk)[1] - # logger.info(f"{slices = }") - for sli in slices: - s = Slice() - setattr(spec_holder, sli, s) - # logger.info(f"{sli = }") - files = next(sub_wlk)[2] - # logger.info(f"{files = }") - for file in files: - name = file.split(".")[0] - tmp = xp.load(os.path.join(path_dat, sli, file)) - # logger.info(f"{name = }") - setattr(s, name, tmp) - grids = {key.removeprefix("grid_"): getattr(s, key) for key in s.keys() - if key.startswith("grid_")} - dims = tuple(part for part in sli.split("_") if part in grids) - for name in tuple(s.keys()): - values = getattr(s, name) - if name.startswith("grid_") or not hasattr(values, "shape"): - continue - expected = (len(self.t_grid), *(len(grids[dim]) for dim in dims)) - if values.shape == expected: - setattr(s, name, wrap_binned_data(values, dims, - {"t": self.t_grid, **{dim: grids[dim] for dim in dims}}, name=name)) - - else: - logger.info(f"{folder =}") - raise NotImplementedError - - logger.warning("\nThe following data has been loaded:") - logger.warning("\ngrids:") - logger.warning(f"{self.t_grid.shape =}") - if self.grids_log is not None: - logger.warning(f"{self.grids_log[0].shape =}") - logger.warning(f"{self.grids_log[1].shape =}") - logger.warning(f"{self.grids_log[2].shape =}") - if self.grids_phy is not None: - logger.warning(f"{self.grids_phy[0].shape =}") - logger.warning(f"{self.grids_phy[1].shape =}") - logger.warning(f"{self.grids_phy[2].shape =}") - logger.warning("\nself.spline_values:") - logger.warning(self.spline_values) - logger.warning("self.orbits:") - logger.warning(self.orbits) - logger.warning("self.f:") - logger.warning(self.f) - logger.warning("self.n_sph:") - logger.warning(self.n_sph) - - -# The old eager attribute tree remains in this module only to make old pickles and -# out-of-tree imports fail gently. New code receives the lazy, xarray-backed API. -from struphy.post_processing.run_output import RunOutput # noqa: E402 - -PlottingData = RunOutput diff --git a/src/struphy/post_processing/run_output.py b/src/struphy/post_processing/run.py similarity index 55% rename from src/struphy/post_processing/run_output.py rename to src/struphy/post_processing/run.py index dcc18628e..d94757b9f 100644 --- a/src/struphy/post_processing/run_output.py +++ b/src/struphy/post_processing/run.py @@ -41,34 +41,6 @@ def clear_cache(self): self._cache.clear() -class _ProductValue: - """Attribute view used only by legacy examples during the transition.""" - - def __init__(self, mapping, key): - self._mapping, self._key = mapping, key - - @property - def array(self): - return self._mapping[self._key] - - def __getattr__(self, name): - data = self.array - if name.startswith("grid_"): - return data.coords[name.removeprefix("grid_")].values - if name in data.attrs: - return data.attrs[name] - return getattr(data, name) - - def __getitem__(self, key): - return self.array[key] - - def __len__(self): - return self.array.sizes[self.array.dims[0]] - - def __array__(self, dtype=None): - return np.asarray(self.array, dtype=dtype) - - class ProductNamespace: """Hierarchical, discoverable attribute view over product names. @@ -131,7 +103,7 @@ class OrbitProducts(ProductNamespace): class PlotAccessor: """Convenient plotting entry points bound to a run.""" - def __init__(self, run: "RunOutput"): + def __init__(self, run: "Run"): self._run = run def timeseries(self, data, **kwargs): @@ -150,97 +122,182 @@ def viewer(self, data, **kwargs): return InteractiveSliceViewer(data, run_label=self._run.label, **kwargs) -class RunOutput: - """The self-describing, lazily loaded output of a completed simulation. +class Run: + """The output of one Struphy simulation, loaded lazily from its output folder. + + Obtain it from :attr:`Simulation.output` (or the return value of :meth:`Simulation.run`) + or, in a separate process, from :func:`open_run`. Nothing is read at construction. - Use :meth:`open` rather than constructing this class directly. Product names are - discovered immediately, while their arrays are loaded only when indexed. + * :attr:`scalars` are read directly from the raw HDF5 output. + * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` need + post-processed data. When there is none, the first access processes the run with + default options; call :meth:`process` beforehand to choose options. + * :attr:`sim` is the :class:`~struphy.Simulation` that produced the output: the live + object for ``sim.output``, otherwise restored from disk without allocating anything. Parameters ---------- + path_out: + The simulation output folder, ``sim.env.path_out``. + sim: + The simulation that wrote ``path_out``, if it is at hand. time_units: ``"physical"`` converts every time coordinate to seconds. ``"normalized"`` consistently leaves every product in Struphy time units. """ - def __init__(self, path_out=None, *, sim=None, time_units: str = "physical"): - if sim is not None: - path_out = sim.env.path_out - if path_out is None: - raise ValueError("path_out or sim is required") + def __init__(self, path_out, *, sim=None, time_units: str = "physical"): if time_units not in {"physical", "normalized"}: raise ValueError("time_units must be 'physical' or 'normalized'") self.path_out = Path(path_out).resolve() - self.path_pproc = self.path_out / "post_processing" - if not self.path_pproc.is_dir(): - raise FileNotFoundError(f"{self.path_pproc} does not exist; run post-processing first") self.time_units = time_units - self._params = self._units = self._time = self._grids_log = self._grids_phy = self._scalars = None - self.field_catalog = ProductMapping(self._discover_fields()) - self.distribution_catalog = ProductMapping(self._discover_binned("distribution_function")) - self.density_catalog = ProductMapping(self._discover_binned("n_sph")) - self.orbit_catalog = ProductMapping(self._discover_orbits()) - self.fields: FieldProducts = FieldProducts(self.field_catalog) - self.distributions: DistributionProducts = DistributionProducts(self.distribution_catalog) - self.densities: DensityProducts = DensityProducts(self.density_catalog) - self.orbits: OrbitProducts = OrbitProducts(self.orbit_catalog) - self.plot = PlotAccessor(self) - - @classmethod - def open(cls, path_out=None, *, sim=None, time_units="physical") -> "RunOutput": - if sim is not None: - path_out = sim.env.path_out - if path_out is None: - raise ValueError("path_out or sim is required") - return cls(path_out, time_units=time_units) - - def load(self): - """Materialize no arrays; retained as an explicit migration no-op.""" + self._sim = sim + self._reset() + + def __repr__(self): + return f"{type(self).__name__}({str(self.path_out)!r}, processed={self.is_processed})" + + def _reset(self): + self._time = self._grids_log = self._grids_phy = self._scalars = self._products = None + + @property + def path_pproc(self) -> Path: + return self.path_out / "post_processing" + + @property + def sim(self): + """The simulation that produced this output; restored from disk when not given.""" + if self._sim is None: + from struphy.simulation.sim import Simulation + + self._sim = Simulation.from_output(self.path_out) + return self._sim + + @property + def is_processed(self) -> bool: + """Whether complete post-processing of the current raw output exists.""" + from struphy.post_processing.post_processing_tools import is_processed + + return is_processed(str(self.path_out)) + + def process( + self, + *, + step: int = 1, + celldivide: int | tuple[int, int, int] = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = False, + parallel: bool = False, + force: bool = False, + ) -> "Run": + """Post-process the raw output; reuses existing products made with the same options. + + Call this on every MPI rank. Serial processing (the default) runs on rank 0 while + the other ranks wait; ``parallel=True`` needs the allocated simulation that ran. + + Parameters + ---------- + step: + Interval of saved time steps to post-process (1 = every step, 2 = every second step, ...). + celldivide: + Evaluation points per cell of FEEC fields, per logical direction or for all three. + physical: + Also compute push-forwarded Cartesian components of fields (``*_phy`` products). + guiding_center: + Compute guiding-center coordinates for particle orbits (Particles6D only). + classify: + Classify orbits (passing, trapped, lost); requires ``guiding_center``. + create_vtk: + Also write VTK files of the fields. + parallel: + Evaluate fields on all MPI ranks of the simulation's communicator. + force: + Reprocess even when matching products exist. + + Returns + ------- + Run + This run, so that ``run = open_run(path).process(physical=True)`` reads naturally. + """ + from struphy.post_processing.post_processing_tools import PostProcessor + + options = dict(step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, + classify=classify, create_vtk=create_vtk, force=force) + sim = self.sim + if parallel: + PostProcessor(sim, parallel_pproc=True).process(**options) + else: + if sim.rank == 0: + PostProcessor(sim).process(**options) + sim.Barrier() + self._reset() return self + def _ensure_processed(self): + if self.is_processed: + return + if self.sim.comm_size > 1: + raise RuntimeError(f"{self.path_out} has no post-processed data; call run.process() on all ranks first") + logger.warning("\nNo post-processed data in %s, processing with default options " + "(call run.process(...) to choose them)", self.path_out) + self.process() + + def _product_mappings(self) -> dict[str, ProductMapping]: + if self._products is None: + self._ensure_processed() + self._products = { + "fields": ProductMapping(self._discover_fields()), + "distributions": ProductMapping(self._discover_binned("distribution_function")), + "densities": ProductMapping(self._discover_binned("n_sph")), + "orbits": ProductMapping(self._discover_orbits()), + } + return self._products + @property - def t_grid(self): - return self.time + def fields(self) -> FieldProducts: + """FEEC fields as ``run.fields..``.""" + return FieldProducts(self.field_catalog) @property - def f(self): - return ProductNamespace(self.distribution_catalog) + def distributions(self) -> DistributionProducts: + """Binned distribution functions as ``run.distributions...``.""" + return DistributionProducts(self.distribution_catalog) @property - def spline_values(self): - return ProductNamespace(self.field_catalog) + def densities(self) -> DensityProducts: + """SPH densities as ``run.densities...``.""" + return DensityProducts(self.density_catalog) @property - def n_sph(self): - return ProductNamespace(self.density_catalog) + def orbits(self) -> OrbitProducts: + """Marker trajectories as ``run.orbits.``.""" + return OrbitProducts(self.orbit_catalog) @property - def params(self): - if self._params is None: - from struphy.post_processing.post_processing_tools import ParamsIn - self._params = ParamsIn(str(self.path_out)) - return self._params + def field_catalog(self) -> ProductMapping: + return self._product_mappings()["fields"] @property - def domain(self): - return self.params.domain + def distribution_catalog(self) -> ProductMapping: + return self._product_mappings()["distributions"] @property - def units(self): - if self._units is None: - from struphy.physics.physics import Units - model = self.params.model - units = Units(model.base_units) - bulk = model.bulk_species - units.derive_units(velocity_scale=model.velocity_scale, - A_bulk=None if bulk is None else bulk.mass_number, - Z_bulk=None if bulk is None else bulk.charge_number) - self._units = units - return self._units + def density_catalog(self) -> ProductMapping: + return self._product_mappings()["densities"] + + @property + def orbit_catalog(self) -> ProductMapping: + return self._product_mappings()["orbits"] + + @property + def plot(self) -> PlotAccessor: + return PlotAccessor(self) @property def time_scale(self) -> float: - return float(self.units.t) if self.time_units == "physical" else 1.0 + return float(self.sim.model.units.t) if self.time_units == "physical" else 1.0 @property def time_unit(self) -> str: @@ -248,14 +305,16 @@ def time_unit(self) -> str: @property def time(self): + """Time grid of the post-processed products.""" if self._time is None: - path = self.path_pproc / "t_grid.npy" - self._time = np.load(path, mmap_mode="r") * self.time_scale + self._ensure_processed() + self._time = np.load(self.path_pproc / "t_grid.npy", mmap_mode="r") * self.time_scale return self._time @property def grids_log(self): if self._grids_log is None: + self._ensure_processed() with (self.path_pproc / "fields_data" / "grids_log.bin").open("rb") as stream: self._grids_log = pickle.load(stream) return self._grids_log @@ -263,12 +322,14 @@ def grids_log(self): @property def grids_phy(self): if self._grids_phy is None: + self._ensure_processed() with (self.path_pproc / "fields_data" / "grids_phy.bin").open("rb") as stream: self._grids_phy = pickle.load(stream) return self._grids_phy @property def scalars(self) -> xr.Dataset: + """Scalar time series, read from the raw output; needs no post-processing.""" if self._scalars is None: path = self.path_out / "data" / "data_proc0.hdf5" if not path.exists(): @@ -288,18 +349,17 @@ def scalars(self) -> xr.Dataset: @property def label(self) -> str: - try: - values = [] - for holder, attr, name in ((self.params.time_opts, "dt", "dt"), - (self.params.time_opts, "split_algo", "algo"), - (self.params.grid, "num_elements", "Nel"), - (self.params.derham_opts, "degree", "p")): - value = getattr(holder, attr, None) if holder is not None else None - if value is not None: - values.append(f"{name}={value}") - return ", ".join(values) - except FileNotFoundError: - return self.path_out.name + """Short description of the numerical parameters, for figure titles.""" + sim = self.sim + values = [] + for holder, attr, name in ((sim.time_opts, "dt", "dt"), + (sim.time_opts, "split_algo", "algo"), + (sim.grid, "num_elements", "Nel"), + (sim.derham_opts, "degree", "p")): + value = getattr(holder, attr, None) if holder is not None else None + if value is not None: + values.append(f"{name}={value}") + return ", ".join(values) or self.path_out.name def save_scalars(self, path=None, **kwargs) -> str: path = Path(path) if path else self.path_pproc / "scalars.csv" @@ -355,7 +415,7 @@ def _load_binned(self, path: Path, slice_name: str): arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} arguments.update(dict(zip(logical_dims, mesh))) try: - physical = self.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) + physical = self.sim.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) for coordinate, grid in zip(("X", "Y", "Z"), physical): coords[coordinate] = (logical_dims, np.asarray(grid)) except (FileNotFoundError, TypeError, ValueError): @@ -375,3 +435,21 @@ def _load_orbits(self, directory: Path): raise FileNotFoundError(f"no orbit arrays in {directory}") values = np.stack([np.load(path, mmap_mode="r") for path in paths]) return wrap_orbits(values, self.time[:len(paths)], time_unit=self.time_unit) + + +def open_run(path_out, *, time_units: str = "physical") -> Run: + """Open the output folder of a finished simulation. + + Nothing is allocated and no MPI is needed; products are read on first access. + + Parameters + ---------- + path_out: + The simulation output folder (``sim.env.path_out`` of the run). + time_units: + ``"physical"`` (seconds) or ``"normalized"`` time coordinates. + """ + path = Path(path_out) + if not (path / "data").is_dir(): + raise FileNotFoundError(f"{path.resolve()} is not a Struphy output folder (it has no data/ directory)") + return Run(path, time_units=time_units) diff --git a/src/struphy/post_processing/tests/test_api.py b/src/struphy/post_processing/tests/test_api.py deleted file mode 100644 index c608b7e01..000000000 --- a/src/struphy/post_processing/tests/test_api.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Tests for the public post-processing convenience API.""" - -from struphy.api import post_processing - - -def test_post_process_processes_then_loads(monkeypatch): - calls = [] - expected = object() - - class FakeSimulation: - def pproc(self, **kwargs): - calls.append(kwargs) - return expected - - sim = FakeSimulation() - - data = post_processing.post_process( - sim=sim, - step=2, - celldivide=(2, 3, 4), - physical=True, - guiding_center=True, - classify=True, - create_vtk=False, - force=False, - ) - - assert data is expected - assert calls == [ - { - "step": 2, - "celldivide": (2, 3, 4), - "physical": True, - "guiding_center": True, - "classify": True, - "create_vtk": False, - "force": False, - "load": True, - } - ] - - -def test_post_process_accepts_an_output_path(monkeypatch): - seen = [] - - class FakePostProcessor: - def __init__(self, **kwargs): - seen.append(kwargs) - - def process(self, **kwargs): - pass - - class FakePlottingData: - def __init__(self, **kwargs): - seen.append(kwargs) - - def load(self): - pass - - monkeypatch.setattr(post_processing, "PostProcessor", FakePostProcessor) - monkeypatch.setattr(post_processing, "PlottingData", FakePlottingData) - - post_processing.post_process(path_out="sim_1") - - assert seen == [ - {"sim": None, "path_out": "sim_1"}, - {"sim": None, "path_out": "sim_1"}, - ] diff --git a/src/struphy/post_processing/tests/test_plotting_data.py b/src/struphy/post_processing/tests/test_plotting_data.py deleted file mode 100644 index eecbb1806..000000000 --- a/src/struphy/post_processing/tests/test_plotting_data.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Integration tests for lazy RunOutput discovery.""" - -import os -import pickle - -import h5py -import numpy as np -import pytest - -from struphy.post_processing.run_output import RunOutput - -NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 - - -def write_tree(root): - pproc = os.path.join(root, "post_processing") - fields = os.path.join(pproc, "fields_data") - kinetic = os.path.join(pproc, "kinetic_data") - os.makedirs(os.path.join(fields, "em_fields")) - os.makedirs(os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density")) - os.makedirs(os.path.join(kinetic, "kinetic_ions", "orbits")) - t = np.linspace(0, 1, NT) - np.save(os.path.join(pproc, "t_grid.npy"), t) - logical = [np.linspace(0, 1, n) for n in (N1, N2, N3)] - physical = np.meshgrid(*logical, indexing="ij") - for name, value in (("grids_log", logical), ("grids_phy", physical)): - with open(os.path.join(fields, f"{name}.bin"), "wb") as stream: - pickle.dump(value, stream) - values = {time: [np.full((N1, N2, N3), i + time) for i in range(3)] for time in t} - with open(os.path.join(fields, "em_fields", "E.bin"), "wb") as stream: - pickle.dump(values, stream) - slice_dir = os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density") - np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) - np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) - np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) - orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") - for step in range(NT): - np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), step)) - data_dir = os.path.join(root, "data") - os.makedirs(data_dir) - with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as file: - file.create_dataset("time/value", data=t) - file.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) - return root - - -@pytest.fixture -def run(tmp_path): - return RunOutput.open(write_tree(str(tmp_path)), time_units="normalized") - - -def test_products_are_discovered_without_loading_arrays(run): - assert tuple(run.fields) == ("em_fields",) - assert tuple(run.distributions) == ("kinetic_ions",) - assert tuple(run.orbits) == ("kinetic_ions",) - assert run.field_catalog._cache == {} - - -def test_field_has_named_and_curvilinear_coordinates(run): - field = run.fields["em_fields/E"] - assert field.dims == ("t", "component", "e1", "e2", "e3") - assert field.X.dims == ("e1", "e2", "e3") - np.testing.assert_allclose(field.isel(t=0, component=2), 2) - assert run.field_catalog._cache["em_fields/E"] is field - - -def test_binned_products_have_coordinates(run): - data = run.distributions["kinetic_ions/e1_v1_density/f_binned"] - assert data.dims == ("t", "e1", "v1") - np.testing.assert_allclose(data.v1, np.linspace(-3, 3, NV)) - - -def test_orbit_product_keeps_column_semantics(run): - data = run.orbits["kinetic_ions"] - assert data.dims == ("t", "marker", "attribute") - assert data.attrs["columns"]["weight"] == 6 - - -def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): - assert set(run.scalars.data_vars) == {"en_tot"} - np.testing.assert_allclose(run.scalars.en_tot.t, run.time) - - -def test_saving_scalars_and_bound_plot_accessor(run, tmp_path): - path = run.save_scalars(tmp_path / "scalars.csv") - assert os.path.exists(path) - result = run.plot.timeseries(run.scalars.en_tot, logy=False) - assert result.ax.get_xlabel() == "$t$" diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index 5bd8b64d2..8fc8bb83f 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -6,7 +6,7 @@ from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt -from struphy import Simulation, set_logging_level +from struphy import Run, Simulation, set_logging_level from struphy.io.setup import import_parameters_py set_logging_level(logging.WARNING) @@ -20,90 +20,52 @@ @pytest.mark.mpi(min_size=2) def test_pproc_mpi(show_plot=False): - def do_plotting(sim: Simulation, from_parallel=False): - sim.load_plotting_data() - - t_grid = sim.t_grid - eta1 = sim.grids_log[0] - e_field = sim.spline_values.em_fields.e_field_log - phi = sim.spline_values.em_fields.phi_log - - f = sim.f.kinetic_ions.e1_v1_density - print(f.__dict__.keys()) - bins_e1 = f.grid_e1 - bins_v1 = f.grid_v1 - f_binned = f.f_binned - df_binned = f.delta_f_binned - print(f"{f_binned.shape=}") - - if from_parallel: - extra = " (from parallel pproc)" - else: - extra = "" - - n = 0 # time index + def do_plotting(run: Run, from_parallel=False): + e_field = run.fields.em_fields.e_field_log.isel(t=0, component=0, e2=0, e3=0) + phi = run.fields.em_fields.phi_log.isel(t=0, e2=0, e3=0) + f = run.distributions.kinetic_ions.e1_v1_density + f_binned = f.f_binned.isel(t=0) + df_binned = f.delta_f_binned.isel(t=0) if show_plot: + extra = " (from parallel pproc)" if from_parallel else "" plt.figure(figsize=(12, 12)) - plt.subplot(2, 2, 1) - plt.plot(eta1, e_field.data[t_grid[n]][0][:, 0, 0], label="Ex") - plt.title(f"Ex at t={t_grid[n]} on rank 0{extra}") - plt.xlabel("$\\eta1$") - plt.ylabel("Ex") - plt.legend() - - plt.subplot(2, 2, 2) - plt.plot(eta1, phi.data[t_grid[n]][0][:, 0, 0], label="phi") - plt.title(f"phi at t={t_grid[n]} on rank 0{extra}") - plt.xlabel("$\\eta1$") - plt.ylabel("phi") - plt.legend() - - plt.subplot(2, 2, 3) - plt.pcolor(bins_e1, bins_v1, f_binned[n].T, shading="auto") - plt.title(f"full f at t={t_grid[n]} on rank 0{extra}") - plt.xlabel("$\\eta1$") - plt.ylabel("$v_x$") - - plt.subplot(2, 2, 4) - plt.pcolor(bins_e1, bins_v1, df_binned[n].T, shading="auto") - plt.title(f"delta f at t={t_grid[n]} on rank 0{extra}") - plt.xlabel("$\\eta1$") - plt.ylabel("$v_x$") - - return ( - e_field.data[t_grid[n]][0][:, 0, 0], - phi.data[t_grid[n]][0][:, 0, 0], - f_binned[n].T, - df_binned[n].T, - ) + for index, (data, title) in enumerate(((e_field, "Ex"), (phi, "phi")), 1): + plt.subplot(2, 2, index) + data.plot(label=title) + plt.title(f"{title} at t={float(data.t)} on rank 0{extra}") + plt.legend() + for index, (data, title) in enumerate(((f_binned, "full f"), (df_binned, "delta f")), 3): + plt.subplot(2, 2, index) + data.plot(x="e1", y="v1") + plt.title(f"{title} at t={float(data.t)} on rank 0{extra}") + + return tuple(np.asarray(data) for data in (e_field, phi, f_binned, df_binned)) test_mod = import_parameters_py(str(PARAMS_PATH), name="weak_Landau_damping") sim: Simulation = test_mod.test_weak_Landau(do_plot=False, exit_before_run=True) - sim.run(one_time_step=True) + run = sim.run(one_time_step=True) - if MPI.COMM_WORLD.Get_rank() == 0: - # serial pproc - sim.pproc() - r1, r2, r3, r4 = do_plotting(sim) - MPI.COMM_WORLD.Barrier() + # serial pproc + run.process(create_vtk=True) + if sim.rank == 0: + serial = do_plotting(run) # parallel pproc - sim.pproc(parallel_pproc=True) + run.process(create_vtk=True, parallel=True, force=True) # plot and compare results from serial and parallel pproc - if MPI.COMM_WORLD.Get_rank() == 0: - r1_mpi, r2_mpi, r3_mpi, r4_mpi = do_plotting(sim, from_parallel=True) + if sim.rank == 0: + parallel = do_plotting(run, from_parallel=True) if show_plot: plt.show() - assert np.allclose(r1, r1_mpi) - assert np.allclose(r2, r2_mpi) - assert np.allclose(r3, r3_mpi) - assert np.allclose(r4, r4_mpi) + for expected, actual in zip(serial, parallel): + assert np.allclose(expected, actual) print("All checks passed for parallel pproc vs serial pproc.") + MPI.COMM_WORLD.Barrier() if __name__ == "__main__": diff --git a/src/struphy/post_processing/tests/test_run.py b/src/struphy/post_processing/tests/test_run.py new file mode 100644 index 000000000..cc2fe7b52 --- /dev/null +++ b/src/struphy/post_processing/tests/test_run.py @@ -0,0 +1,223 @@ +"""Tests for the lazy Run output API.""" + +import json +import os +import pickle + +import h5py +import numpy as np +import pytest + +from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint +from struphy.post_processing.run import Run, open_run + +NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 + + +def write_tree(root): + pproc = os.path.join(root, "post_processing") + fields = os.path.join(pproc, "fields_data") + kinetic = os.path.join(pproc, "kinetic_data") + os.makedirs(os.path.join(fields, "em_fields")) + os.makedirs(os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density")) + os.makedirs(os.path.join(kinetic, "kinetic_ions", "orbits")) + t = np.linspace(0, 1, NT) + np.save(os.path.join(pproc, "t_grid.npy"), t) + logical = [np.linspace(0, 1, n) for n in (N1, N2, N3)] + physical = np.meshgrid(*logical, indexing="ij") + for name, value in (("grids_log", logical), ("grids_phy", physical)): + with open(os.path.join(fields, f"{name}.bin"), "wb") as stream: + pickle.dump(value, stream) + values = {time: [np.full((N1, N2, N3), i + time) for i in range(3)] for time in t} + with open(os.path.join(fields, "em_fields", "E.bin"), "wb") as stream: + pickle.dump(values, stream) + slice_dir = os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density") + np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) + np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) + np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) + orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") + for step in range(NT): + np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), step)) + data_dir = os.path.join(root, "data") + os.makedirs(data_dir) + with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as file: + file.create_dataset("time/value", data=t) + file.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) + write_manifest(root) + return root + + +def write_manifest(root, **options): + manifest = { + "schema_version": 1, + "status": "complete", + "source_fingerprint": source_fingerprint(root), + "options": normalize_options(**options), + } + with open(os.path.join(root, "post_processing", "manifest.json"), "w") as stream: + json.dump(manifest, stream) + + +class FakeSim: + """Just enough of a Simulation for Run: no configuration, a single rank.""" + + time_opts = grid = derham_opts = None + rank, comm_size = 0, 1 + + def __init__(self): + self.processed = [] + + def Barrier(self): + pass + + +@pytest.fixture +def run(tmp_path): + return Run(write_tree(str(tmp_path)), sim=FakeSim(), time_units="normalized") + + +def test_products_are_discovered_without_loading_arrays(run): + assert tuple(run.fields) == ("em_fields",) + assert tuple(run.distributions) == ("kinetic_ions",) + assert tuple(run.orbits) == ("kinetic_ions",) + assert run.field_catalog._cache == {} + assert run.is_processed + + +def test_field_has_named_and_curvilinear_coordinates(run): + field = run.fields["em_fields/E"] + assert field.dims == ("t", "component", "e1", "e2", "e3") + assert field.X.dims == ("e1", "e2", "e3") + np.testing.assert_allclose(field.isel(t=0, component=2), 2) + assert run.field_catalog._cache["em_fields/E"] is field + + +def test_binned_products_have_coordinates(run): + data = run.distributions["kinetic_ions/e1_v1_density/f_binned"] + assert data.dims == ("t", "e1", "v1") + np.testing.assert_allclose(data.v1, np.linspace(-3, 3, NV)) + + +def test_orbit_product_keeps_column_semantics(run): + data = run.orbits["kinetic_ions"] + assert data.dims == ("t", "marker", "attribute") + assert data.attrs["columns"]["weight"] == 6 + + +def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): + assert set(run.scalars.data_vars) == {"en_tot"} + np.testing.assert_allclose(run.scalars.en_tot.t, run.time) + + +def test_saving_scalars_and_bound_plot_accessor(run, tmp_path): + path = run.save_scalars(tmp_path / "scalars.csv") + assert os.path.exists(path) + result = run.plot.timeseries(run.scalars.en_tot, logy=False) + assert result.ax.get_xlabel() == "$t$" + + +def test_open_run_needs_an_output_folder(tmp_path): + with pytest.raises(FileNotFoundError, match="not a Struphy output folder"): + open_run(tmp_path) + run = open_run(write_tree(str(tmp_path))) + assert run.path_out == tmp_path.resolve() + + +def test_sim_is_restored_from_disk_only_on_access(tmp_path, monkeypatch): + from struphy.simulation.sim import Simulation + + restored = FakeSim() + calls = [] + monkeypatch.setattr(Simulation, "from_output", classmethod(lambda cls, path: calls.append(path) or restored)) + run = open_run(write_tree(str(tmp_path))) + assert calls == [] + assert run.sim is restored and run.sim is restored + assert calls == [tmp_path.resolve()] + + +def test_products_trigger_default_processing_when_missing(tmp_path, monkeypatch): + root = write_tree(str(tmp_path)) + os.remove(os.path.join(root, "post_processing", "manifest.json")) + run = Run(root, sim=FakeSim(), time_units="normalized") + calls = [] + + def fake_process(self, **options): + calls.append(options) + write_manifest(root) + self._reset() + return self + + monkeypatch.setattr(Run, "process", fake_process) + assert set(run.scalars.data_vars) == {"en_tot"} + assert calls == [], "scalars come from the raw output" + assert tuple(run.fields) == ("em_fields",) + assert calls == [{}] + + +def test_products_refuse_implicit_processing_on_many_ranks(tmp_path): + root = write_tree(str(tmp_path)) + os.remove(os.path.join(root, "post_processing", "manifest.json")) + sim = FakeSim() + sim.comm_size = 2 + with pytest.raises(RuntimeError, match="on all ranks"): + Run(root, sim=sim).fields + + +def test_processing_options_are_part_of_the_manifest(tmp_path): + root = write_tree(str(tmp_path)) + write_manifest(root, step=1, celldivide=2, physical=False) + assert is_processed(root) + assert is_processed(root, dict(step=1, celldivide=(2, 2, 2), physical=False)) + assert not is_processed(root, dict(step=1, celldivide=2, physical=True)) + + +def test_manifest_is_stale_when_raw_output_changes(tmp_path): + root = write_tree(str(tmp_path)) + with open(os.path.join(root, "meta.yml"), "w") as stream: + stream.write("MPI processes: 1\n") + assert not is_processed(root) + + +@pytest.mark.parametrize("rank", [0, 1]) +def test_serial_process_runs_on_rank_zero_only(tmp_path, monkeypatch, rank): + from struphy.post_processing import post_processing_tools + + calls = [] + + class FakePostProcessor: + def __init__(self, sim, parallel_pproc=False): + calls.append(("construct", parallel_pproc)) + + def process(self, **options): + calls.append(("process", options)) + + monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) + sim = FakeSim() + sim.rank = rank + run = Run(write_tree(str(tmp_path)), sim=sim) + assert run.process(physical=True) is run + expected = [ + ("construct", False), + ("process", dict(step=1, celldivide=1, physical=True, guiding_center=False, classify=False, + create_vtk=False, force=False)), + ] + assert calls == (expected if rank == 0 else []) + + +def test_parallel_process_runs_on_every_rank(tmp_path, monkeypatch): + from struphy.post_processing import post_processing_tools + + calls = [] + + class FakePostProcessor: + def __init__(self, sim, parallel_pproc=False): + calls.append(parallel_pproc) + + def process(self, **options): + pass + + monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) + sim = FakeSim() + sim.rank = 3 + Run(write_tree(str(tmp_path)), sim=sim).process(parallel=True) + assert calls == [True] diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index 82bfcd18d..c22b3a38c 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -32,14 +32,10 @@ def run(self): """Run the simulation.""" pass + @property @abstractmethod - def pproc(self): - """Post-process the simulation results.""" - pass - - @abstractmethod - def load_plotting_data(self): - """Load post-processed data for visualization.""" + def output(self): + """The output of the simulation.""" pass @abstractmethod diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 7416ad096..9e210e4c9 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,12 +1,14 @@ # third party imports +import dataclasses import glob +import hashlib import json import logging import os import shutil import sysconfig import time -from collections.abc import Sequence +from pathlib import Path import cunumpy as xp import h5py @@ -25,8 +27,6 @@ BaseUnits, DerhamOptions, EnvironmentOptions, - RunOutput, - PostProcessor, ProfilingOptions, Time, domains, @@ -53,6 +53,7 @@ ) from struphy.geometry.base import Domain from struphy.io.output_handling import DataContainer +from struphy.io.setup import import_parameters_py from struphy.models import Maxwell from struphy.models.base import StruphyModel from struphy.models.species import ( @@ -65,6 +66,7 @@ from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.physics.physics import Units from struphy.pic.base import Particles +from struphy.post_processing.run import Run from struphy.propagators.base import Propagator from struphy.simulation.base import SimulationBase from struphy.utils.clone_config import CloneConfig @@ -171,8 +173,8 @@ def __init__( self.Barrier() self.start_time = time.time() - self._save_config() self.clone_config = self._create_clone_config() + self._output = None self.Barrier() # ---------------- @@ -599,7 +601,7 @@ def initialize_data_storage(self): self.data.add_data({key_time: val}) self.data.add_data({key_time_restart: val}) - def run(self, one_time_step: bool = False, profiling_activated: bool | None = None): + def run(self, one_time_step: bool = False, profiling_activated: bool | None = None) -> Run: """Main entry point to execute the simulation time loop. Responsibilities include allocation (when not restarting), @@ -615,6 +617,11 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No profiling_activated : bool | None If True, activate profiling with scope-profiler for this run. If None, profiling is disabled. + + Returns + ------- + Run + The output of this run, see :attr:`output`. """ if profiling_activated is None: profiling_activated = False @@ -626,6 +633,10 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No logger.info(f"Description: {self.description}") self._remove_existing_output_files() + self._setup_folders() + self._save_config() + self.Barrier() + self._output = None with ProfileManager.session( options=self.profiling_opts, @@ -872,81 +883,18 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No if self.clone_config is not None: self.clone_config.free() - def pproc( - self, - step: int = 1, - celldivide: int | Sequence[int] = 1, - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - create_vtk: bool = True, - parallel_pproc: bool = False, - force: bool = True, - load: bool = False, - ) -> RunOutput | None: - """Run post-processing on saved simulation data. - - Uses `PostProcessor` to process guiding-center or physical field views - and optionally produce VTK outputs. With ``load=True``, load the results - on rank 0 and return them as `PlottingData`; non-root ranks return ``None``. - - Loading is opt-in because processed field and particle arrays can be - large. ``force=False`` reuses an existing post-processing directory. - """ - - # setup post processor and plotting - if parallel_pproc: - self._post_processor = PostProcessor(sim=self, parallel_pproc=True) - - self.post_processor.process( - step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, - classify=classify, - create_vtk=create_vtk, - force=force, - ) - else: - if self.rank == 0: - self._post_processor = PostProcessor(sim=self, parallel_pproc=False) - - self.post_processor.process( - step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, - classify=classify, - create_vtk=create_vtk, - force=force, - ) - - if load and self.rank == 0: - return self.load_plotting_data() - return None + return self.output - def load_plotting_data(self) -> RunOutput | None: - """Load plotting datasets produced by post-processing. + @property + def output(self) -> Run: + """The output of this simulation in ``env.path_out``, see :class:`~struphy.Run`. - Creates a lazy :class:`RunOutput` instance on rank 0 and exposes its - product mappings for downstream analysis. Non-root ranks return ``None``. + Scalars are available as soon as data is written; fields and particle products are + post-processed on first access, or explicitly with ``sim.output.process(...)``. """ - - if self.rank != 0: - return None - if not hasattr(self, "_plotting_data"): - self._plotting_data = RunOutput(sim=self) - self.plotting_data.load() - - # expose attributes - self.orbits = self.plotting_data.orbits - self.f = self.plotting_data.distributions - self.spline_values = self.plotting_data.fields - self.n_sph = self.plotting_data.densities - self.grids_log = self.plotting_data.grids_log - self.grids_phy = self.plotting_data.grids_phy - self.t_grid = self.plotting_data.time - return self.plotting_data + if self._output is None or self._output.path_out != Path(self.env.path_out).resolve(): + self._output = Run(self.env.path_out, sim=self) + return self._output # --------------------- # Code specific methods @@ -1702,6 +1650,31 @@ def convert_lists_to_tuples(obj): dct = convert_lists_to_tuples(dct) return cls.from_dict(dct) + @classmethod + def from_output(cls, path_out: str) -> "Simulation": + """Restore the simulation that wrote the output folder ``path_out``. + + The configuration is read from the ``parameters.py`` copied there by :meth:`run`, or + from ``config.json`` when the simulation was not created from a parameter file. + Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. + """ + path_out = os.path.abspath(path_out) + params_path = os.path.join(path_out, "parameters.py") + config_path = os.path.join(path_out, "config.json") + if os.path.exists(params_path): + module_name = "struphy_run_" + hashlib.sha1(path_out.encode()).hexdigest()[:12] + sim = getattr(import_parameters_py(params_path, name=module_name), "sim", None) + if not isinstance(sim, Simulation): + raise ValueError(f"{params_path} does not define a Simulation named 'sim'") + elif os.path.exists(config_path): + sim = cls.from_file(config_path) + else: + raise FileNotFoundError(f"Neither {params_path} nor {config_path} exists; is {path_out} a Struphy output folder?") + sim.env = dataclasses.replace( + sim.env, out_folders=os.path.dirname(path_out), sim_folder=os.path.basename(path_out) + ) + return sim + def generate_script( self, include_main_guard: bool = False, @@ -1871,9 +1844,6 @@ def env(self, value: EnvironmentOptions): assert isinstance(value, EnvironmentOptions) self._env = value - # create output folders - self._setup_folders() - @property def profiling_filepath(self) -> str: """Path to the profiling file, if profiling is enabled.""" @@ -2030,33 +2000,23 @@ def model_name(self) -> str: @property def derham(self): - """3d Derham sequence, see :ref:`derham`.""" - return self._derham + """3d Derham sequence, see :ref:`derham`; None before :meth:`allocate`.""" + return getattr(self, "_derham", None) @property def mass_ops(self): """WeighteMassOperators object, see :ref:`mass_ops`.""" - return self._mass_ops + return getattr(self, "_mass_ops", None) @property def basis_ops(self): """Basis projection operators.""" - return self._basis_ops + return getattr(self, "_basis_ops", None) @property def projected_equil(self): """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" - return self._projected_equil - - @property - def post_processor(self): - """PostProcessor object for post-processing finished Struphy runs.""" - return self._post_processor - - @property - def plotting_data(self): - """PlottingData object for loading and storing data generated during post-processing.""" - return self._plotting_data + return getattr(self, "_projected_equil", None) @property def clone_config(self): diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py new file mode 100644 index 000000000..613ba7f2a --- /dev/null +++ b/src/struphy/simulation/tests/test_output.py @@ -0,0 +1,66 @@ +"""Tests for the link between a Simulation and its output.""" + +import os + +import pytest + +from struphy import EnvironmentOptions, Run, Simulation, open_run +from struphy.models import Maxwell + + +def make_sim(tmp_path, **kwargs): + env = EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_1") + return Simulation(model=Maxwell(), env=env, **kwargs) + + +def test_constructing_a_simulation_writes_nothing(tmp_path): + sim = make_sim(tmp_path) + assert not os.path.exists(sim.env.path_out) + assert sim.derham is None + + +def test_output_is_the_run_of_the_current_output_folder(tmp_path): + sim = make_sim(tmp_path) + run = sim.output + assert isinstance(run, Run) + assert run.sim is sim + assert sim.output is run + + sim.env = EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_2") + assert sim.output is not run + assert sim.output.path_out.name == "sim_2" + + +def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): + sim = make_sim(tmp_path) + os.makedirs(os.path.join(sim.env.path_out, "data")) + sim._save_config() + + moved = tmp_path / "moved" + os.rename(sim.env.path_out, moved) + restored = open_run(moved).sim + + assert restored.to_dict()["model"] == sim.to_dict()["model"] + assert restored.domain == sim.domain + assert restored.env.path_out == str(moved) + assert restored.derham is None + assert sorted(os.listdir(tmp_path)) == ["moved"] + + +def test_from_output_prefers_the_parameter_file(tmp_path): + path_out = tmp_path / "sim_1" + os.makedirs(path_out / "data") + (path_out / "parameters.py").write_text( + "from struphy import EnvironmentOptions, Simulation, Time\n" + "from struphy.models import Maxwell\n" + "sim = Simulation(model=Maxwell(), env=EnvironmentOptions(sim_folder='elsewhere'), time_opts=Time(dt=0.123))\n" + ) + restored = Simulation.from_output(path_out) + assert restored.time_opts.dt == 0.123 + assert restored.env.path_out == str(path_out) + assert not os.path.exists(os.path.join(os.getcwd(), "elsewhere")) + + +def test_from_output_requires_a_configuration(tmp_path): + with pytest.raises(FileNotFoundError, match="parameters.py"): + Simulation.from_output(tmp_path) diff --git a/src/struphy/simulation/tests/test_pproc_api.py b/src/struphy/simulation/tests/test_pproc_api.py deleted file mode 100644 index ab77a083d..000000000 --- a/src/struphy/simulation/tests/test_pproc_api.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Unit tests for the Simulation post-processing convenience behavior.""" - -import importlib - -from struphy import Simulation - - -def test_pproc_can_load_and_return_plotting_data(monkeypatch): - calls = [] - expected = object() - - class FakePostProcessor: - def __init__(self, **kwargs): - calls.append(("construct", kwargs)) - - def process(self, **kwargs): - calls.append(("process", kwargs)) - - class FakeSimulation: - rank = 0 - - @property - def post_processor(self): - return self._post_processor - - def load_plotting_data(self): - calls.append(("load", {})) - return expected - - simulation_module = importlib.import_module("struphy.simulation.sim") - monkeypatch.setattr(simulation_module, "PostProcessor", FakePostProcessor) - - sim = FakeSimulation() - data = Simulation.pproc(sim, physical=True, create_vtk=False, force=False, load=True) - - assert data is expected - assert calls == [ - ("construct", {"sim": sim, "parallel_pproc": False}), - ( - "process", - { - "step": 1, - "celldivide": 1, - "physical": True, - "guiding_center": False, - "classify": False, - "create_vtk": False, - "force": False, - }, - ), - ("load", {}), - ] - - -def test_pproc_does_not_load_by_default(monkeypatch): - class FakePostProcessor: - def __init__(self, **kwargs): - pass - - def process(self, **kwargs): - pass - - class FakeSimulation: - rank = 0 - - @property - def post_processor(self): - return self._post_processor - - def load_plotting_data(self): - raise AssertionError("plotting data should not be loaded") - - simulation_module = importlib.import_module("struphy.simulation.sim") - monkeypatch.setattr(simulation_module, "PostProcessor", FakePostProcessor) - - assert Simulation.pproc(FakeSimulation()) is None diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 147cc1478..2d5c2d367 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -7,9 +7,9 @@ "source": [ "# Post-processing and standard plots\n", "\n", - "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, load its output as an autocomplete-friendly `RunOutput`, and make the plots most commonly used to inspect a simulation.\n", + "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, get its output as an autocomplete-friendly `Run`, and make the plots most commonly used to inspect a simulation.\n", "\n", - "For a production run you can skip the simulation setup and construct both objects with `path_out=\"path/to/sim\"` instead." + "For a production run you can skip the simulation setup and open its output folder with `struphy.open_run(\"path/to/sim\")` instead." ] }, { @@ -119,7 +119,7 @@ " grid=grids.TensorProductGrid(num_elements=(8, 1, 1)),\n", " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", ")\n", - "sim.run()\n", + "run = sim.run()\n", "print(f\"Raw output: {sim.env.path_out}\")" ] }, @@ -130,9 +130,11 @@ "source": [ "## Process and load the output\n", "\n", - "`sim.pproc(load=True)` evaluates saved FEEC fields, organizes particle diagnostics, and returns a lazy `RunOutput`. `physical=True` additionally creates physical field components; `create_vtk=False` keeps this notebook quick. With `force=False`, a complete post-processing result is reused.\n", + "`sim.run()` returns the run's output as a `Run`, which is also available later as `sim.output`. Scalars are read directly from the raw output; fields and particle products need post-processing, which runs with default options the first time they are accessed.\n", "\n", - "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Arrays are loaded only when accessed. `RunOutput.open(path_out)` can load an already processed run without running post-processing again." + "To choose options, call `run.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", + "\n", + "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Arrays are loaded only when accessed. The simulation that produced them is `run.sim`." ] }, { @@ -142,12 +144,7 @@ "metadata": {}, "outputs": [], "source": [ - "run = sim.pproc(\n", - " physical=True,\n", - " create_vtk=False,\n", - " force=False,\n", - " load=True,\n", - ")" + "run.process(physical=True)" ] }, { @@ -214,7 +211,7 @@ " electric_energy,\n", " logy=True,\n", " fit=GrowthFit(\n", - " window=(0.0, 0.4 * run.units.t),\n", + " window=(0.0, 0.4 * run.sim.model.units.t),\n", " amplitude_from_quadratic=True,\n", " ),\n", " title=\"Electric-field energy\",\n", @@ -355,14 +352,13 @@ "source": [ "## Apply the workflow to another run\n", "\n", - "For an already completed simulation, the complete loading pattern is:\n", + "For an already completed simulation, possibly in a separate process without MPI, open its output folder:\n", "\n", "```python\n", - "path_out = \"/path/to/sim_1\"\n", - "from struphy import RunOutput, post_process\n", - "run = post_process(path_out=path_out, physical=True, force=False)\n", - "# If it is already processed:\n", - "run = RunOutput.open(path_out)\n", + "import struphy\n", + "\n", + "run = struphy.open_run(\"/path/to/sim_1\").process(physical=True)\n", + "run.sim.domain, run.sim.model.units # the simulation, restored from disk without allocating\n", "```\n", "\n", "Use `run.scalars`, `run.fields`, `run.distributions`, `run.orbits`, and `run.densities`. Attribute access is the normal interactive API; the corresponding `*_catalog` mappings are intended for generic loops and tooling." From 89145e2e738278e98b7aedff03fda232ebbb860c Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 00:08:49 +0200 Subject: [PATCH 019/193] Updated the API --- .claude/skills/setup-simulation/SKILL.md | 5 +- doc/sections/userguide.rst | 40 ++- .../cyclone/pproc_cyclone.py | 48 +-- .../itg_cylindre/pproc_drift_kinetic.py | 48 +-- .../diocotron_instability/pproc_diocotron.py | 59 +--- .../bump_on/pproc_bump_on.py | 100 +------ .../pproc_strong_Landau_damping.py | 88 +----- .../two_stream/pproc_two_stream.py | 23 +- .../pproc_weak_Landau_damping.py | 129 ++------ .../pproc_weibel_instability.py | 277 +++++------------- src/struphy/diagnostics/diagn_tools.py | 79 ++--- src/struphy/diagnostics/plotting.py | 70 +++-- .../diagnostics/tests/test_diagn_tools.py | 47 +++ .../diagnostics/tests/test_plotting.py | 3 +- src/struphy/models/base.py | 28 +- ...est_verif_IncompressibleNavierStokesSPH.py | 38 +-- .../verification/test_verif_LinearMHD.py | 27 +- .../tests/verification/test_verif_Maxwell.py | 58 ++-- .../tests/verification/test_verif_Poisson.py | 22 +- .../test_verif_ViscousEulerSPH.py | 82 +++--- .../post_processing/post_processing_tools.py | 27 +- src/struphy/post_processing/run.py | 140 +++++---- src/struphy/post_processing/run_accessors.py | 234 +++++++++++++++ src/struphy/post_processing/tests/test_run.py | 14 +- .../tests/test_run_accessors.py | 115 ++++++++ src/struphy/simulation/sim.py | 3 + src/struphy/simulation/tests/test_output.py | 11 +- tutorials/dev_tutorial_feec_bcs.ipynb | 12 +- tutorials/tutorial_beltrami_sph.ipynb | 37 +-- tutorials/tutorial_dam_break_sph.ipynb | 12 +- tutorials/tutorial_gas_expansion_sph.ipynb | 30 +- tutorials/tutorial_hagen_poiseuille_sph.ipynb | 9 +- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 27 +- tutorials/tutorial_maxwell.ipynb | 38 +-- tutorials/tutorial_particle_tracing.ipynb | 104 ++----- tutorials/tutorial_poisson.ipynb | 53 ++-- tutorials/tutorial_post_processing.ipynb | 85 ++---- .../tutorial_pressureless_sph_shock.ipynb | 13 +- .../tutorial_velocity_diffusion_sph.ipynb | 17 +- tutorials/tutorial_viscous_euler_sph.ipynb | 25 +- 40 files changed, 1087 insertions(+), 1190 deletions(-) create mode 100644 src/struphy/diagnostics/tests/test_diagn_tools.py create mode 100644 src/struphy/post_processing/run_accessors.py create mode 100644 src/struphy/post_processing/tests/test_run_accessors.py diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 26a2a5aca..23cac9756 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -141,7 +141,10 @@ run.orbits. # dims (t, marker, attrib run.sim.model.units # the Simulation, restored without allocating ``` -Plotting helpers for these arrays live in `src/struphy/diagnostics/plotting.py`; see +Plots and analysis need no imports: `run.plot.scalars()`, `run.plot.timeseries("", fit=(t0, t1))`, +`run.plot.panels("//f_binned", x="e1", y="v1")`, `run.plot.viewer(...)`, +`run.plot.orbits("")`, `run.save_report()`, `run.analysis.growth_rate(...)`, +`run.analysis.dispersion(...)`. Names are looked up with `run[""]`. See `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. ## Common pitfalls diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 9672c9c74..4f9595d22 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -516,8 +516,10 @@ Arrays are read from disk only when accessed. In a separate process, for example a plotting script on a laptop after a cluster run, open the output folder instead. Nothing is allocated and no MPI is needed; -``run.sim`` is restored from the ``parameters.py`` (or ``config.json``) stored in the -folder: +``run.sim`` is restored from the ``parameters.py`` stored in the folder. A simulation +that was not created from a parameter file is restored from ``config.json``, which holds +the options and the model arguments (and thus the units), but not configuration applied +to the model afterwards, such as backgrounds or perturbations: .. code-block:: python @@ -556,6 +558,36 @@ serial processing runs on rank 0 while the other ranks wait, and ``parallel=True`` uses the allocated simulation on all ranks. +Standard plots and analysis: ``run.plot`` and ``run.analysis`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The common diagnostics are methods of the run, so no further imports are needed. They +accept a product name, ``run[""]``, or any array (sliced, derived, or from another +run), and figures are titled with the run's numerical parameters: + +.. code-block:: python + + run.plot.scalars() # overview + energy conservation error + run.plot.timeseries("en_phi", fit=(0.0, 40.0)) # exponential fit in a time window + run.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", isel={"t": -1}) + run.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4) + run.plot.viewer("em_fields/phi_phy", x="e1", y="e2", coords="physical").show() + run.plot.orbits("kinetic_ions") + run.save_report() # table + figures in post_processing/report/ + + run.analysis.growth_rate("en_phi", window=(0.0, 40.0)).rate + run.analysis.dispersion("em_fields/e_field_log", slice_at=(0, 0, None), fit_branches=1) + +Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of +several runs are labeled by run: + +.. code-block:: python + + run_a.plot.timeseries(run_a["en_phi"], run_b["en_phi"], fit=(0.0, 40.0)) + +The sections below access the arrays directly for custom Matplotlib plots. + + Plotting field data ^^^^^^^^^^^^^^^^^^^^ @@ -587,10 +619,8 @@ Binned particle data is grouped by species and the slice defined in .. code-block:: python - from struphy.diagnostics.plotting import View, plot_slice - f = run.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) - plot_slice(f.isel(t=-1), view=View(x="e1", y="v1")).show() + run.plot.slice(f.isel(t=-1), x="e1", y="v1").show() Plotting particle orbits diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 13c666091..b77d8eff2 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -2,14 +2,6 @@ import sys from struphy import open_run -from struphy.diagnostics.plotting import ( - GrowthFit, - InteractiveSliceViewer, - View, - plot_marker_trajectories, - plot_timeseries, - plot_equilibrium_profile, -) # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" @@ -17,16 +9,12 @@ SHOW_EQUIL_PROFILE = False -# binned densities to sweep, as (bin name, quantity, physical plane) -DENSITY_PLOTS = [ - ("e1_e2_density", "delta_f_binned", "RZ"), -] - -# fields to sweep, as (species, field, component, physical plane) -FIELD_PLOTS = [ - ("em_fields", "phi_phy", 0, "RZ"), - ("diagnostics", "rho_phy", 0, "RZ"), - ("diagnostics", "rho_phy", 0, "XY"), +# products to sweep interactively, as (name, displayed component or None, physical plane) +SWEEPS = [ + ("kinetic_ions/e1_e2_density/delta_f_binned", None, "RZ"), + ("em_fields/phi_phy", None, "RZ"), + ("diagnostics/rho_phy", None, "RZ"), + ("diagnostics/rho_phy", None, "XY"), ] @@ -34,27 +22,21 @@ def main(path_out): run = open_run(path_out).process(physical=True) # growth rate of the electrostatic potential - plot_timeseries( - run.scalars[FIT_QUANTITY], - fit=GrowthFit(FIT_WINDOW, amplitude_from_quadratic=True), - run_label=run.label, + run.plot.timeseries( + FIT_QUANTITY, + fit=FIT_WINDOW, + fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", ).show() if SHOW_EQUIL_PROFILE: - plot_equilibrium_profile(path_out) - - for bin_name, quantity, plane in DENSITY_PLOTS: - data = getattr(getattr(run.distributions.kinetic_ions, bin_name), quantity) - InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), - run_label=run.label).show() + run.plot.equilibrium() - for species, field, component, plane in FIELD_PLOTS: - data = getattr(getattr(run.fields, species), field).isel(component=component) - InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), - run_label=run.label).show() + for name, component, plane in SWEEPS: + isel = None if component is None else {"component": component} + run.plot.viewer(name, x="e1", y="e2", isel=isel, coords="physical", plane=plane).show() - plot_marker_trajectories(run.orbits.kinetic_ions, max_markers=1000).show() + run.plot.orbits("kinetic_ions", max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 99dc430ee..edcbf20ce 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -2,14 +2,6 @@ import sys from struphy import open_run -from struphy.diagnostics.plotting import ( - GrowthFit, - InteractiveSliceViewer, - View, - plot_marker_trajectories, - plot_timeseries, - plot_equilibrium_profile, -) # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" @@ -17,16 +9,12 @@ SHOW_EQUIL_PROFILE = False -# binned densities to sweep, as (bin name, quantity, physical plane) -DENSITY_PLOTS = [ - ("e1_e2_density", "f_binned", "XY"), - ("e1_e2_density", "delta_f_binned", "XY"), -] - -# fields to sweep, as (species, field, component, physical plane) -FIELD_PLOTS = [ - ("em_fields", "phi_phy", 0, "XY"), - ("diagnostics", "rho_phy", 0, "XY"), +# products to sweep interactively, as (name, displayed component or None, physical plane) +SWEEPS = [ + ("kinetic_ions/e1_e2_density/f_binned", None, "XY"), + ("kinetic_ions/e1_e2_density/delta_f_binned", None, "XY"), + ("em_fields/phi_phy", None, "XY"), + ("diagnostics/rho_phy", None, "XY"), ] @@ -34,27 +22,21 @@ def main(path_out): run = open_run(path_out).process(physical=True) # growth rate of the electrostatic potential - plot_timeseries( - run.scalars[FIT_QUANTITY], - fit=GrowthFit(FIT_WINDOW, amplitude_from_quadratic=True), - run_label=run.label, + run.plot.timeseries( + FIT_QUANTITY, + fit=FIT_WINDOW, + fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", ).show() if SHOW_EQUIL_PROFILE: - plot_equilibrium_profile(path_out) - - for bin_name, quantity, plane in DENSITY_PLOTS: - data = getattr(getattr(run.distributions.kinetic_ions, bin_name), quantity) - InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), - run_label=run.label).show() + run.plot.equilibrium() - for species, field, component, plane in FIELD_PLOTS: - data = getattr(getattr(run.fields, species), field).isel(component=component) - InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), - run_label=run.label).show() + for name, component, plane in SWEEPS: + isel = None if component is None else {"component": component} + run.plot.viewer(name, x="e1", y="e2", isel=isel, coords="physical", plane=plane).show() - plot_marker_trajectories(run.orbits.kinetic_ions, max_markers=1000).show() + run.plot.orbits("kinetic_ions", max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index dff3ed9b6..ec85fa21a 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -8,71 +8,44 @@ import sys from struphy import open_run -from struphy.diagnostics.plotting import ( - GrowthFit, - InteractiveSliceViewer, - View, - plot_marker_trajectories, - plot_timeseries, - plot_equilibrium_profile, -) FIT_QUANTITY = "en_phi" FIT_WINDOW = (0.0, 42.0) SHOW_EQUIL_PROFILE = True -# binned densities to sweep, as (bin name, quantity, physical plane) -DENSITY_PLOTS = [ - ("e1_e2_density", "f_binned", "XY"), - ("e1_e2_density", "delta_f_binned", "XY"), -] - -# fields to sweep, as (species, field, component, physical plane) -FIELD_PLOTS = [ - ("em_fields", "phi_phy", 0, "XY"), +# products to sweep interactively in the physical XY plane +SWEEPS = [ + "kinetic_ions/e1_e2_density/f_binned", + "kinetic_ions/e1_e2_density/delta_f_binned", + "em_fields/phi_phy", ] def main(paths): - runs = {os.path.basename(p): open_run(p).process(physical=True) for p in paths} + runs = [open_run(path).process(physical=True) for path in paths] + run = runs[0] # growth rate of the electrostatic energy, one curve per run - series = [] - for name, run in runs.items(): - energy = run.scalars[FIT_QUANTITY].copy() - energy.attrs["label"] = name if len(runs) > 1 else FIT_QUANTITY - series.append(energy) - - plot = plot_timeseries( - series, - fit=GrowthFit(FIT_WINDOW), - run_label=next(iter(runs.values())).label, + plot = run.plot.timeseries( + *(each[FIT_QUANTITY] for each in runs), + fit=FIT_WINDOW, title=f"Evolution of {FIT_QUANTITY}", ).show() - for name, result in zip(runs, plot.fit_results): - print(f"{name}: growth rate = {None if result is None else result.rate}") + for each, result in zip(runs, plot.fit_results): + print(f"{each.path_out.name}: growth rate = {None if result is None else result.rate}") if len(runs) > 1: return - path_out, run = paths[0], next(iter(runs.values())) - if SHOW_EQUIL_PROFILE: - plot_equilibrium_profile(path_out) - - for bin_name, quantity, plane in DENSITY_PLOTS: - data = getattr(getattr(run.distributions.kinetic_ions, bin_name), quantity) - InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), - run_label=run.label).show() + run.plot.equilibrium() - for species, field, component, plane in FIELD_PLOTS: - data = getattr(getattr(run.fields, species), field).isel(component=component) - InteractiveSliceViewer(data, view=View(x="e1", y="e2", coordinates="physical", plane=plane), - run_label=run.label).show() + for name in SWEEPS: + run.plot.viewer(name, x="e1", y="e2", coords="physical", plane="XY").show() - plot_marker_trajectories(run.orbits.kinetic_ions, max_markers=1000).show() + run.plot.orbits("kinetic_ions", max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index acedfa76d..71f763d67 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -1,102 +1,22 @@ import params_bump_on as params - -import os -import h5py -from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt -from struphy import PostProcessor, PlottingData -from struphy.physics.physics import Units def main(): - # post process raw data - path = os.path.join(os.getcwd(), "sim_data") - pp = PostProcessor(sim=params.sim) - pp.process() - - # get sim data - pdata = PlottingData(sim=params.sim) - pdata.load() - - ### Initial velocity distribution ### - v1_bins = pdata.f.kinetic_ions.v1_density.grid_v1 - f_v1 = pdata.f.kinetic_ions.v1_density.f_binned + run = params.sim.output - fig, ax = plt.subplots(1 ,figsize = (14,10)) - - ax.plot(v1_bins, f_v1[0]) - ax.set_xlabel("Velocity v") - ax.set_ylabel("Distribution f(v)") - ax.set_title("Initial velocity distribution") - plt.tight_layout() + # initial velocity distribution + initial = run["kinetic_ions/v1_density/f_binned"].isel(t=0) + ax = initial.plot()[0].axes + ax.set(xlabel="velocity $v$", ylabel="distribution $f(v)$", title="Initial velocity distribution") plt.show() - ### Electric field progression ### - # get parameters - dt = params.time_opts.dt - algo = params.time_opts.split_algo - num_elements = params.grid.num_elements - degree = params.derham_opts.degree - - env = params.env - ppc = params.loading_params.ppc - - #get units - units = Units(params.base_units) - model = params.model - model.units = units - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species.charge_number - model.units.derive_units( - velocity_scale = model.velocity_scale, - A_bulk = A_bulk, - Z_bulk = Z_bulk - ) - unit_t = model.units.t + # electric field energy + run.plot.timeseries("electric_energy", title="Electric energy").show() - # get scalar data (post processing not needed for scalar data) - if MPI.COMM_WORLD.Get_rank() == 0: - pa_data = os.path.join(env.path_out, "data") - with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: - time = f["time"]["value"][()]*unit_t - E = f["scalar"]["electric_energy"][()] + # full f in the e1-v1 plane + run.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() - # plot - plt.figure(figsize=(18, 12)) - plt.plot(time, E, label="numerical") - plt.legend() - plt.title(f"{dt=}, {algo=}, {num_elements=}, {degree=}, {ppc=}") - plt.yscale("log") - plt.xlabel("time [s]") - plt.ylabel("electric energy $E^2/2$ [a.u.]") - plt.show() - - ### Binning distribution progression ### - e1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_e1 - v1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_v1 - nrows = 3 - ncols = 4 - ntime = len(pdata.f.kinetic_ions.e1_v1_density.f_binned) - time_indices = [int( i/(nrows*ncols-1) * (ntime - 1) ) for i in range(nrows*ncols)] - - fig, axs = plt.subplots(nrows = nrows, ncols = ncols, figsize = (14,10), sharex=True, sharey=True) - for i in range(nrows): - for j in range(ncols): - ax_maxwellian = axs[i][j] - time_idx = time_indices[j + i*ncols] - - #maxwellian distribution plot - color_mapped = pdata.f.kinetic_ions.e1_v1_density.f_binned[time_idx].T - pcm = ax_maxwellian.pcolor(e1_bins,v1_bins, color_mapped) - - ax_maxwellian.set_xlabel(r"$\eta_1$") - ax_maxwellian.set_ylabel(r"$v_x$") - ax_maxwellian.set_title(fr"full-$f$ at t = {pdata.t_grid[time_idx]*unit_t:4.2e} s") - fig.colorbar(pcm, ax = ax_maxwellian) - - plt.tight_layout() - plt.show() - if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index f8b337c1b..72edc147c 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,91 +1,15 @@ import params_strong_Landau_damping as params -import os -import h5py -from feectools.ddm.mpi import mpi as MPI -from matplotlib import pyplot as plt -from struphy.physics.physics import Units -from struphy import PostProcessor, PlottingData - def main(): - ### Electric field progression ### - # get parameters - dt = params.time_opts.dt - algo = params.time_opts.split_algo - num_elements = params.grid.num_elements - degree = params.derham_opts.degree - - env = params.env - ppc = params.loading_params.ppc - - # get units - units = Units(params.base_units) - model = params.model - model.units = units - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species._charge_number - model.units.derive_units( - velocity_scale=model.velocity_scale, - A_bulk=A_bulk, - Z_bulk=Z_bulk - ) - unit_t = model.units.t - - # get scalar data (post processing not needed for scalar data) - if MPI.COMM_WORLD.Get_rank() == 0: - pa_data = os.path.join(env.path_out, "data") - with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: - time = f["time"]["value"][()] - E = f["scalar"]["electric_energy"][()] - - # plot - plt.figure(figsize=(18, 12)) - plt.plot(time, E, label="numerical") - plt.yscale("log") - plt.legend() - plt.title(f"{dt=}, {algo=}, {num_elements=}, {degree=}, {ppc=}") - plt.xlabel("time [s]") - plt.ylabel("electric energy $E^2/2$ [a.u.]") - - plt.show() - - ### Binning distribution progression ### - # post process raw data - path = os.path.join(os.getcwd(), "sim_data") - pp = PostProcessor(sim=params.sim) - pp.process() - - # get sim data - pdata = PlottingData(sim=params.sim) - pdata.load() - - # plot in e1-v1 - e1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_e1 - v1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_v1 - - nrows = 3 - ncols = 4 - ntime = len(pdata.f.kinetic_ions.e1_v1_density.f_binned) - time_indices = [int( i/(nrows*ncols-1) * (ntime - 1) ) for i in range(nrows*ncols)] + run = params.sim.output - fig, axs = plt.subplots(nrows = nrows, ncols = ncols, figsize = (14,10), sharex=True, sharey=True) - for i in range(nrows): - for j in range(ncols): - ax_maxwellian = axs[i][j] - time_idx = time_indices[j + i*ncols] + # electric field energy + run.plot.timeseries("electric_energy", title="Electric energy").show() - #maxwellian distribution plot - color_mapped = pdata.f.kinetic_ions.e1_v1_density.f_binned[time_idx].T - pcm = ax_maxwellian.pcolor(e1_bins,v1_bins, color_mapped) + # full f in the e1-v1 plane + run.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() - ax_maxwellian.set_xlabel(r"$\eta_1$") - ax_maxwellian.set_ylabel(r"$v_x$") - ax_maxwellian.set_title(fr"full-$f$ at t = {pdata.t_grid[time_idx]*unit_t:4.2e} s") - fig.colorbar(pcm, ax = ax_maxwellian) - plt.tight_layout() - plt.show() - if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index fba90fca4..b359e52f8 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,33 +1,24 @@ import params_two_stream as params -from struphy.diagnostics.plotting import InteractiveSliceViewer, View, plot_panels, plot_timeseries - def main(): run = params.sim.output - # every scalar at every time step: post_processing/scalars/{scalars.csv,*.png} - run.save_scalar_plots() + # table and figures of every scalar: post_processing/report/ + run.save_report() # electric field growth against the analytical rate (0.2845 in units of m/c) - energy = run.scalars["electric_energy"] + energy = run.scalars.electric_energy analytical = energy.copy(data=10 ** (0.2845 / run.sim.model.units.t * energy.t - 5.3)) analytical.attrs["label"] = "analytical" - - plot_timeseries( - [energy, analytical], - run_label=run.label, - title="Electric energy", - ).show() + run.plot.timeseries(energy, analytical, title="Electric energy").show() # phase space evolution - f = run.distributions.kinetic_ions.e1_v1_density.f_binned - view = View(x="e1", y="v1") - - plot_panels(f, view=view, nrows=3, ncols=4, shared_clim=True, run_label=run.label).show() + f = "kinetic_ions/e1_v1_density/f_binned" + run.plot.panels(f, x="e1", y="v1", nrows=3, ncols=4).show() # interactive alternative to dumping a frame sequence - InteractiveSliceViewer(f, view=view, run_label=run.label).show() + run.plot.viewer(f, x="e1", y="v1").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 6edf05e01..382b55963 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -1,118 +1,31 @@ -import params_weak_Landau_damping as params - -import os import cunumpy as xp -import h5py -from feectools.ddm.mpi import mpi as MPI -from matplotlib import pyplot as plt -from struphy.physics.physics import Units -from struphy import PostProcessor, PlottingData - - -def main(): - ### Electric field progression ### - # get parameters - dt = params.time_opts.dt - algo = params.time_opts.split_algo - num_elements = params.grid.num_elements - degree = params.derham_opts.degree - - env = params.env - ppc = params.loading_params.ppc - - # get units - units = Units(params.base_units) - model = params.model - model.units = units - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species.charge_number - model.units.derive_units( - velocity_scale=model.velocity_scale, - A_bulk=A_bulk, - Z_bulk=Z_bulk, - ) - unit_t = model.units.t - - def E_exact(t): - eps = params.perturbation.amps[0] - r = 0.3677 - omega_r = 1.4156 - omega_i = -0.1533 - phi = 0.5362 - return (4*eps*r*xp.exp(omega_i * t) * xp.cos(omega_r * t - phi))**2 * xp.pi - - # get scalar data (post processing not needed for scalar data) - if MPI.COMM_WORLD.Get_rank() == 0: - pa_data = os.path.join(env.path_out, "data") - with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: - time = f["time"]["value"][()]*unit_t - E = f["scalar"]["electric_energy"][()] - logE = xp.log10(E) - - # find where time derivative of E is zero - dEdt = (xp.roll(logE, -1) - xp.roll(logE, 1))[1:-1] / (2.0 * dt) - zeros = dEdt * xp.roll(dEdt, -1) < 0.0 - maxima_inds = xp.logical_and(zeros, dEdt > 0.0) - maxima = logE[1:-1][maxima_inds] - t_maxima = time[1:-1][maxima_inds] - - # plot - plt.figure(figsize=(18, 12)) - plt.plot(time, E, label="numerical") - plt.plot(time, E_exact(time/unit_t), linestyle = "--", color = "black", label = "analytical") - plt.yscale('log') - plt.legend() - plt.title(f"{dt=}, {algo=}, {num_elements=}, {degree=}, {ppc=}") - plt.xlabel("time [s]") - plt.ylabel("electric energy $E^2/2$ [a.u.]") - - plt.show() - - ### Binning distribution progression ### - # post process raw data - path = os.path.join(os.getcwd(), "sim_data") - pp = PostProcessor(sim=params.sim) - pp.process() - - # get sim data - pdata = PlottingData(sim=params.sim) - pdata.load() - - # plot in e1-v1 - e1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_e1 - v1_bins = pdata.f.kinetic_ions.e1_v1_density.grid_v1 +import params_weak_Landau_damping as params - nrows = 4 - ntime = len(pdata.f.kinetic_ions.e1_v1_density.f_binned) - time_indices = [int( i/(nrows-1) * (ntime - 1) ) for i in range(nrows)] - fig, axs = plt.subplots(nrows = nrows, ncols = 2, figsize = (14,10), sharex=True, sharey=True) - for index in range(nrows): - ax_maxwellian, ax_perturbation = axs[index][0], axs[index][1] - time_index = time_indices[index] - ax_title = f"t = {pdata.t_grid[time_index]} ms" +def E_exact(t): + """Analytical electric energy of weak Landau damping, t in normalized units.""" + eps = params.perturbation.amps[0] + r = 0.3677 + omega_r = 1.4156 + omega_i = -0.1533 + phi = 0.5362 + return (4 * eps * r * xp.exp(omega_i * t) * xp.cos(omega_r * t - phi)) ** 2 * xp.pi - #maxwellian distribution plot - color_mapped = pdata.f.kinetic_ions.e1_v1_density.f_binned[time_index].T - pcm = ax_maxwellian.pcolor(e1_bins,v1_bins, color_mapped) +def main(): + run = params.sim.output - ax_maxwellian.set_xlabel(r"$\eta_1$") - ax_maxwellian.set_ylabel(r"$v_x$") - ax_maxwellian.set_title(fr"full-$f$ at t = {pdata.t_grid[time_index]*unit_t:4.2e} s") - fig.colorbar(pcm, ax = ax_maxwellian) + # electric field energy against the analytical damping + energy = run.scalars.electric_energy.copy() + energy.attrs["label"] = "numerical" + analytical = energy.copy(data=E_exact(energy.t.values / run.sim.model.units.t)) + analytical.attrs["label"] = "analytical" + run.plot.timeseries(energy, analytical, title="Electric energy").show() - #perturbation plot - color_mapped = pdata.f.kinetic_ions.e1_v1_density.delta_f_binned[time_index].T - pcm = ax_perturbation.pcolor(e1_bins, v1_bins, color_mapped) + # full f and delta f in the e1-v1 plane at four times + for quantity, title in (("f_binned", "full-$f$"), ("delta_f_binned", r"$\delta f$")): + run.plot.panels(f"kinetic_ions/e1_v1_density/{quantity}", x="e1", y="v1", nrows=1, ncols=4, title=title).show() - ax_perturbation.set_xlabel(r"$\eta_1$") - ax_perturbation.set_ylabel(r"$v_x$") - ax_perturbation.set_title(fr"$\delta f$ at t = {pdata.t_grid[time_index]*unit_t:4.2e} s") - fig.colorbar(pcm, ax = ax_perturbation) - plt.tight_layout() - plt.show() - if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 209104479..d9b322f04 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -1,278 +1,153 @@ -import os - import cunumpy as xp -import h5py import params_weibel_instability as params from matplotlib import pyplot as plt -from feectools.ddm.mpi import mpi as MPI -from struphy import PlottingData, PostProcessor -from struphy.physics.physics import Units - def main(): - # post process raw data - sim_name = params.env.sim_folder - sim_path = os.path.join(os.getcwd(), sim_name) - # save_path = os.path.join(os.getcwd(), "result", "noPerb", "controlVariate"+sim_name[-1]) - - # pp = PostProcessor(path_out=sim_path) - pp = PostProcessor(sim=params.sim) - pp.process() - - # get sim data - pdata = PlottingData(sim=params.sim) - pdata.load() - - # get parameters - dt = params.time_opts.dt + run = params.sim.output.with_time_units("normalized") + time = run.time Tend = params.time_opts.Tend algo = params.time_opts.split_algo - num_elements = params.grid.num_elements - degree = params.derham_opts.degree - - env = params.env - ppc = params.loading_params.ppc # 32 grid points - - #get units - units = Units(params.base_units) - model = params.model - model.units = units - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species.charge_number - model.units.derive_units( - velocity_scale = model.velocity_scale, - A_bulk = A_bulk, - Z_bulk = Z_bulk - ) - unit_t = model.units.t - + ppc = params.loading_params.ppc control_variate = params.weights_params.control_variate - split_algo = params.time_opts.split_algo # ------------------ # Gauss law violation # ------------------ - if model.measure_gauss_law: - pa_data = os.path.join(env.path_out, "data") - with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: - time = f["time"]["value"][()] - gauss_error = f["scalar"]["gauss_error"][()] - - fig, ax = plt.subplots(1, figsize = (10,6)) - ax.plot(time, gauss_error) + if params.model.measure_gauss_law: + gauss_error = run.scalars.gauss_error + fig, ax = plt.subplots(1, figsize=(10, 6)) + ax.plot(gauss_error.t, gauss_error) ax.set_xlim(0, Tend) ax.set_yscale("log") - ax.set_xlabel("time") ax.set_ylabel("gauss error") ax.set_title("Gauss law violation as function of time") - ax.grid() plt.tight_layout() - # plt.savefig(os.path.join(save_path,"gauss_law")) plt.show() # ------------------ - # progression of EM-field energy - # along different direction + # progression of EM-field energy along different directions # ------------------ + e_field = run.fields.em_fields.e_field_log + b_field = run.fields.em_fields.b_field_log + spatial = ("e1", "e2", "e3") + unit_volume = xp.prod([1 / (e_field.sizes[dim] - 1) for dim in spatial]) - # energy in EM-field along different directions - phy_grid = pdata.grids_phy[0].shape - - Nt = pdata.t_grid - unit_volume = xp.prod([1/(phy_grid[i] - 1) for i in range(len(phy_grid))]) - - def field_energy(field) -> float: - """ - Calculate totoal energy of field in space - """ - - energy_square = xp.sum(field ** 2) - - return energy_square * unit_volume / 2 - - extract_field_energy_axes = lambda field: [ - xp.array([ - field_energy(getattr(pdata.spline_values.em_fields, field).data[t][i]) for t in Nt - ]) for i in range(3) - ] + def field_energy(field): + """Energy of each component over space, as array of shape (component, t).""" + return (field**2).sum(spatial).transpose("component", "t").values * unit_volume / 2 - electric_energy = extract_field_energy_axes("e_field_log") - magnetic_energy = extract_field_energy_axes("b_field_log") + electric_energy = field_energy(e_field) + magnetic_energy = field_energy(b_field) - # plot - fig, ax = plt.subplots(nrows = 1, ncols = 1, figsize = (10,6), sharex = True) - - # plot of energy in EM-fields - ax.plot(pdata.t_grid, electric_energy[0], label = r"|$E_1|^2$/2", color = "blue") - ax.plot(pdata.t_grid, electric_energy[1], label = r"|$E_2|^2$/2", color = "green") - ax.plot(pdata.t_grid, magnetic_energy[2], label = r"|$B_3|^2$/2", color = "red") + fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 6), sharex=True) + ax.plot(time, electric_energy[0], label=r"|$E_1|^2$/2", color="blue") + ax.plot(time, electric_energy[1], label=r"|$E_2|^2$/2", color="green") + ax.plot(time, magnetic_energy[2], label=r"|$B_3|^2$/2", color="red") # determine magnetic field growth rate - exp_func = lambda x, m, b: 10**(m*x + b) + exp_func = lambda x, m, b: 10 ** (m * x + b) - ti = pdata.t_grid[-1]//5 - if ti == 0.0: - tf = pdata.t_grid[-1] - else: - tf = 2*ti + ti = time[-1] // 5 + tf = time[-1] if ti == 0.0 else 2 * ti print(f"{ti = }, {tf = }") - xi = xp.abs(pdata.t_grid - ti).argmin() + 1 # index of time 100 [a.lu.] (observed end of growth rate) - xf = xp.abs(pdata.t_grid - tf).argmin() + 1 # index of time 200 [a.lu.] (observed end of growth rate) + xi = xp.abs(time - ti).argmin() + 1 + xf = xp.abs(time - tf).argmin() + 1 - fitting = xp.polyfit(pdata.t_grid[xi:xf], xp.log10(magnetic_energy[2][xi:xf]), deg = 1) + fitting = xp.polyfit(time[xi:xf], xp.log10(magnetic_energy[2][xi:xf]), deg=1) ax.plot( - pdata.t_grid, - exp_func(pdata.t_grid, *fitting), - label="fitted growth rate\n" + fr"$10^{{{fitting[0]:.5f}x {fitting[1]:.0f}}}$", - color="cyan" + time, + exp_func(time, *fitting), + label="fitted growth rate\n" + rf"$10^{{{fitting[0]:.5f}x {fitting[1]:.0f}}}$", + color="cyan", ) - ax.plot( - pdata.t_grid, - exp_func(pdata.t_grid, 0.02784, fitting[1]), - label="analytical growth rate\n" + fr"$10^{{0.02784x {fitting[1]:.0f}}}$", + time, + exp_func(time, 0.02784, fitting[1]), + label="analytical growth rate\n" + rf"$10^{{0.02784x {fitting[1]:.0f}}}$", color="cyan", ls="--", - alpha=0.5 + alpha=0.5, ) - ax.set_title(f"Field energy: {split_algo=}, {ppc=}, {control_variate=}") ax.set_title("Energy in EM field") ax.set_ylabel("Energy [a.u.]") ax.set_xlabel("time") - ax.set_ylim(1e-14,1e0) - ax.set_xlim(0,Tend) - ax.legend(ncol = 3) - + ax.set_ylim(1e-14, 1e0) + ax.set_xlim(0, Tend) + ax.legend(ncol=3) ax.set_yscale("log") ax.minorticks_on() fig.suptitle(f"VlasovMaxwellOneSpecies simulation:\n {control_variate=}, {ppc=}, {algo=}") plt.tight_layout() - # plt.savefig(os.path.join(save_path,"E")) plt.show() # ------------------ - # Binning distribution evolution + # Binning distribution evolution # ------------------ - - nrows = 5 - ncols = 4 - ntime = len(pdata.f.kinetic_ions.e1_v1_density.f_binned) - time_indices = [int( i/(nrows*ncols-1) * (ntime - 1) ) for i in range(nrows*ncols)] - - def plot_phaseSpace(bin, bin_name): - bins = bin_name.split("_")[:-1] - grid_1, grid_2, *_ = ["grid_" + s for s in bins] - bins_1 = getattr(getattr(pdata.f.kinetic_ions, bin_name), grid_1) - bins_2 = getattr(getattr(pdata.f.kinetic_ions, bin_name), grid_2) - - fig, axs = plt.subplots(nrows = nrows, ncols = ncols, figsize = (14,10), sharex=True, sharey=True) - for i in range(nrows): - for j in range(ncols): - ax_maxwellian = axs[i][j] - time_idx = time_indices[j + i*ncols] - - #maxwellian distribution plot - color_mapped = getattr( - getattr(pdata.f.kinetic_ions, bin_name), bin - )[time_idx].T - pcm = ax_maxwellian.pcolor(bins_1, bins_2, color_mapped) - - ax_maxwellian.set_xlabel(bins[0]) - ax_maxwellian.set_ylabel(bins[1]) - ax_maxwellian.set_title(f"{bin} at t = {pdata.t_grid[time_idx]:4.2e}") - fig.colorbar(pcm, ax = ax_maxwellian) - - plt.tight_layout() - # plt.savefig(os.path.join(save_path, f"{bin_name}_{bin}_phaseSpace")) - plt.show() - plt.close() - - plot_phaseSpace("f_binned", bin_name="e1_v1_density") - plot_phaseSpace("delta_f_binned", bin_name="e1_v1_density") - plot_phaseSpace("f_binned",bin_name="v1_v2_density") - plot_phaseSpace("delta_f_binned",bin_name="v1_v2_density") + distributions = run.distributions.kinetic_ions + for bin_name, x, y in (("e1_v1_density", "e1", "v1"), ("v1_v2_density", "v1", "v2")): + for quantity in ("f_binned", "delta_f_binned"): + run.plot.panels(f"kinetic_ions/{bin_name}/{quantity}", x=x, y=y, nrows=5, ncols=4).show() # ------------------ - # Plot EM-field of each time step + # EM field at selected times # ------------------ + def plot_EM_state(time_step: float, n_dim=3): + electric_field = e_field.sel(t=time_step, method="nearest").isel(e2=0, e3=0) + magnetic_field = b_field.sel(t=time_step, method="nearest").isel(e2=0, e3=0) - def plot_EM_state(time_step: float, n_dim = 3): - nearest_key = pdata.t_grid[xp.abs(pdata.t_grid - time_step).argmin()] - electric_field = pdata.spline_values.em_fields.e_field_log.data[nearest_key] - magnetic_field = pdata.spline_values.em_fields.b_field_log.data[nearest_key] - - fig, axs = plt.subplots(nrows = 2, ncols = 3, figsize = (8,6), sharex = True, sharey = True) - + fig, axs = plt.subplots(nrows=2, ncols=3, figsize=(8, 6), sharex=True, sharey=True) for i in range(n_dim): - axs[0,i].plot(pdata.grids_log[0], electric_field[i][:,0,0]) - axs[0,i].set_title(fr"$E_{i+1}$") - - axs[1,i].plot(pdata.grids_log[0], magnetic_field[i][:,0,0]) - axs[1,i].set_title(fr"$B_{i+1}$") - - axs[0,0].set_ylabel(r"Electric field value") - axs[1,0].set_ylabel(r"Magnetic field value") - axs[1,0].set_xlabel(r"$\eta_1$") - axs[1,1].set_xlabel(r"$\eta_1$") - axs[1,2].set_xlabel(r"$\eta_1$") - - axs[0,0].set_ylim(-5e-3, 5e-3) - axs[1,0].set_ylim(-5e-3, 5e-3) + axs[0, i].plot(electric_field.e1, electric_field.isel(component=i)) + axs[0, i].set_title(rf"$E_{i + 1}$") + axs[1, i].plot(magnetic_field.e1, magnetic_field.isel(component=i)) + axs[1, i].set_title(rf"$B_{i + 1}$") - fig.suptitle(f"EM-field at time step: {nearest_key:.2f}, {ppc=},{control_variate=}") + axs[0, 0].set_ylabel(r"Electric field value") + axs[1, 0].set_ylabel(r"Magnetic field value") + for i in range(n_dim): + axs[1, i].set_xlabel(r"$\eta_1$") + axs[0, 0].set_ylim(-5e-3, 5e-3) + axs[1, 0].set_ylim(-5e-3, 5e-3) - # plt.savefig(os.path.join(save_path, "EM_state", f"{nearest_key:.2f}".replace(".", "_") + ".png")) + fig.suptitle(f"EM-field at time step: {float(electric_field.t):.2f}, {ppc=},{control_variate=}") plt.show() plt.close() - # os.makedirs(os.path.join(save_path, "EM_state"), exist_ok=True) - for t in xp.linspace(0, pdata.t_grid[-1], 2): + for t in xp.linspace(0, time[-1], 2): plot_EM_state(t) # ------------------ # Current density evolution # ------------------ - - # current_density_path = os.path.join(save_path, "current_density") - # os.makedirs(current_density_path,exist_ok=True) - - def current_1D(time: float): - time_step = abs(pdata.t_grid - time).argmin() - fig, ax = plt.subplots(nrows = 3, ncols = 3, figsize = (9,9),sharey = True, sharex = True) - + def current_1D(time_step: float): + fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True) for i in range(3): for j in range(3): + current = getattr(distributions, f"e{i + 1}_current_{j + 1}").f_binned + current = current.sel(t=time_step, method="nearest") + ax[i, j].axhline(color="red", alpha=0.5) + ax[i, j].plot(current[f"e{i + 1}"], current) + ax[i, 0].set_ylim(-0.01, 0.01) - e_bins = getattr(pdata.f.kinetic_ions, f"e{i+1}_current_{j+1}").f_binned[time_step] - es = xp.linspace(0,1,e_bins.shape[0]) - - ax[i,j].axhline(color = "red", alpha = 0.5) - ax[i,j].plot(es, e_bins) - - ax[i,0].set_ylim(-0.01,0.01) - - for i in range(3): ax[i,0].set_ylabel(fr"$j_{i+1}$") - for j in range(3): ax[2,j].set_xlabel(fr"$\eta_{ {j+1} }$") - - fig.suptitle(f"Current density at time {time:.2f}") + for i in range(3): + ax[i, 0].set_ylabel(rf"$j_{i + 1}$") + for j in range(3): + ax[2, j].set_xlabel(rf"$\eta_{ {j + 1} }$") + fig.suptitle(f"Current density at time {time_step:.2f}") plt.tight_layout() - # plt.savefig(os.path.join( - # current_density_path, - # f"{time:.2f}".replace(".", "_") + ".png" - # )) plt.show() - # plt.close() - for t in xp.linspace(0, pdata.t_grid[-1], 2): + for t in xp.linspace(0, time[-1], 2): current_1D(t) - + + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 6b2f90c5d..12d157623 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -7,6 +7,7 @@ import cunumpy as xp import matplotlib.colors as colors import matplotlib.pyplot as plt +import xarray as xr from scipy.fft import fftfreq, fftn from scipy.signal import argrelextrema @@ -17,12 +18,10 @@ def power_spectrum_2d( - values: dict, - name: str, - grids: tuple, - grids_mapped: tuple = None, + field: xr.DataArray, component: int = 0, slice_at: tuple = (None, 0, 0), + physical: bool = False, do_plot: bool = False, disp_name: str = None, disp_params: dict = {}, @@ -39,27 +38,23 @@ def power_spectrum_2d( Parameters ---------- - values : dict - Dictionary holding values of a B-spline FemField on the grid as 3d xp.arrays: - values[n] contains the values at time step n, where n = 0:Nt-1:step with 0 str: + """The run description shared by all arrays (``attrs["run"]``), or ``default``. + + Arrays loaded from a :class:`~struphy.Run` carry it; arrays from different runs share none. + """ + runs = {item.attrs.get("run") for item in _items(data)} + if len(runs - {None, ""}) > 1: + return "" + runs.discard(None) + runs.discard("") + return runs.pop() if runs else default + + def _finish(fig, *, run_label="", tight=True): if run_label: fig.suptitle(run_label, fontsize="small") @@ -200,9 +222,9 @@ def _slice_data(data, view): return selected, grids -def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=""): - """Plot one or more aligned time series.""" - series = [data] if isinstance(data, xr.DataArray) else list(data) +def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=None): + """Plot one or more aligned time series; series of different runs are labeled by run.""" + series = _items(data) if not series: raise ValueError("at least one time series is required") for item in series: @@ -211,11 +233,17 @@ def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, t raise ValueError(f"time series must have dims ('t',), got {item.dims}") if len(series) > 1: series = list(xr.align(*series, join="exact")) + label_of = _label + if len({item.attrs.get("run_name") for item in series}) > 1: + def label_of(item): + return " ".join(filter(None, (_label(item), f"({item.attrs['run_name']})" if item.attrs.get("run_name") else ""))) + run_label = shared_run_label(series) if run_label is None else run_label + own_figure = ax is None with plt.rc_context(STRUPHY_STYLE): fig, ax = plt.subplots() if ax is None else (ax.figure, ax) artists, fits = [], [] for item in series: - line, = ax.plot(item.t, item, label=_label(item) or None) + line, = ax.plot(item.t, item, label=label_of(item) or None) artists.append(line) result = growth_rate(item, fit) if fit is not None else None fits.append(result) @@ -229,17 +257,19 @@ def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, t ax.set_xlabel(axis_label(series[0], "t")) ax.set_ylabel(value_label(series[0])) ax.set_title(title if title is not None else _label(series[0])) - if any(_label(item) for item in series) or fit is not None: + if any(label_of(item) for item in series) or fit is not None: ax.legend() - _finish(fig, run_label=run_label, tight=ax is not None) + _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) return PlotResult(fig, ax, artists, fits) def plot_slice(data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, - equal_aspect=None, title=None, run_label=""): + equal_aspect=None, title=None, run_label=None): """Render one selected two-dimensional slice.""" view = view or View() + run_label = shared_run_label(data) if run_label is None else run_label selected, (xgrid, ygrid, xlabel, ylabel) = _slice_data(data, view) + own_figure = ax is None with plt.rc_context(STRUPHY_STYLE): fig, ax = plt.subplots() if ax is None else (ax.figure, ax) mesh = ax.pcolormesh(xgrid, ygrid, np.asarray(selected), shading="auto", vmin=vmin, vmax=vmax) @@ -249,14 +279,15 @@ def plot_slice(data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, ax.set_aspect("equal", adjustable="box") ax.set(xlabel=xlabel, ylabel=ylabel, title=title if title is not None else _label(data)) ax.grid(False) - _finish(fig, run_label=run_label) + _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) return PlotResult(fig, ax, [mesh]) def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim=True, - title=None, run_label=""): + title=None, run_label=None): """Plot snapshots spread across a sweep coordinate.""" view = view or View() + run_label = shared_run_label(data) if run_label is None else run_label selected = _select(data, view) validate_array(selected, required_dims=(view.sweep,)) count = nrows * ncols @@ -289,10 +320,11 @@ def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim= class InteractiveSliceViewer: """Stateful viewer using one recipe for the sweep and all remaining dimensions.""" - def __init__(self, data: xr.DataArray, *, view=None, vmin=None, vmax=None, run_label=""): + def __init__(self, data: xr.DataArray, *, view=None, vmin=None, vmax=None, run_label=None): self.data = validate_array(data) self.view = view or View() - self.vmin, self.vmax, self.run_label = vmin, vmax, run_label + self.vmin, self.vmax = vmin, vmax + self.run_label = shared_run_label(data) if run_label is None else run_label self.result = None self.sliders = {} @@ -392,10 +424,14 @@ def save_frames(data: xr.DataArray, directory, *, view=None, step=1, prefix="fra def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, - error_panel="en_tot", logy=False, run_label=""): - """Plot a scalar overview and optional conservation-error panel.""" + error_panel="en_tot", logy=False, run_label=None): + """Plot a scalar overview and optional conservation-error panel. + + The relative error of ``error_panel`` is returned in ``result.data["relative_error"]``. + """ selected = scalar_names(scalars, names=names, exclude=exclude) if not selected: raise ValueError("no scalars to plot") + run_label = shared_run_label([scalars[name] for name in selected]) if run_label is None else run_label has_error = error_panel is not None and error_panel in scalars fig, axes = plt.subplots(2 if has_error else 1, 1, sharex=has_error, figsize=(8, 6.5) if has_error else None, @@ -420,11 +456,11 @@ def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=No else: ax.set_xlabel(axis_label(scalars[selected[0]], "t")) if run_label: fig.suptitle(run_label, fontsize="small") - return PlotResult(fig, axes, artists), error + return PlotResult(fig, axes, artists, data={"relative_error": error}) def save_all_scalars(scalars, directory, *, names=None, exclude=SCALARS_EXCLUDE, logy=False, - run_label="", table="csv", file_format="png", dpi=110): + run_label=None, table="csv", file_format="png", dpi=110): """Write a table, scalar overview and one figure per scalar.""" selected = scalar_names(scalars, names=names, exclude=exclude) if not selected: return [] @@ -433,7 +469,7 @@ def save_all_scalars(scalars, directory, *, names=None, exclude=SCALARS_EXCLUDE, paths = [] if table: paths.append(save_scalars(scalars, str(directory / f"scalars.{table}"), names=selected, fmt=table)) - overview, _ = plot_scalars(scalars, names=selected, logy=logy, run_label=run_label) + overview = plot_scalars(scalars, names=selected, logy=logy, run_label=run_label) path = directory / f"scalars.{file_format}" overview.save(path, dpi=dpi, close=True) paths.append(str(path)) diff --git a/src/struphy/diagnostics/tests/test_diagn_tools.py b/src/struphy/diagnostics/tests/test_diagn_tools.py new file mode 100644 index 000000000..2a55f437e --- /dev/null +++ b/src/struphy/diagnostics/tests/test_diagn_tools.py @@ -0,0 +1,47 @@ +"""Tests for the dispersion analysis on labeled field data.""" + +import numpy as np +import pytest + +from struphy.diagnostics.diagn_tools import power_spectrum_2d +from struphy.post_processing.arrays import data_array + +LENGTH, SPEED = 20.0, 1.0 + + +def standing_waves(dt=0.05, tend=2 * LENGTH, nx=128): + """Standing waves of all resolved wavenumbers with phase speed SPEED along eta3, as a field of a Run. + + The time window holds whole periods of every wave, so the spectrum has no leakage. + """ + t = np.arange(0.0, tend, dt) + eta = np.linspace(0.0, 1.0, nx, endpoint=False) + z = eta * LENGTH + rng = np.random.default_rng(0) + values = np.zeros((t.size, eta.size)) + for n in range(1, nx // 2): + k = 2 * np.pi * n / LENGTH + values += np.cos(k * z[None, :] + rng.uniform(0, 2 * np.pi)) * np.cos(k * SPEED * t[:, None]) + field = np.zeros((t.size, 2, 2, 1, nx)) + field[:, 1, :, 0, :] = values[:, None, :] + mesh = np.meshgrid(np.zeros(2), np.zeros(1), z, indexing="ij") + coords = {"t": t, "component": [0, 1], "e1": [0.0, 0.5], "e2": [0.0], "e3": eta} + coords.update({name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), mesh)}) + return data_array(field, ("t", "component", "e1", "e2", "e3"), coords, name="e_field_log") + + +@pytest.mark.parametrize("physical", [True, False]) +def test_fitted_phase_speed(physical): + field = standing_waves() + omega, kvec, dispersion, coeffs = power_spectrum_2d( + field, component=1, slice_at=(0, 0, None), physical=physical, fit_branches=1, noise_level=0.5 + ) + assert dispersion.shape == (omega.size, kvec.size) + # on the logical grid, wavenumbers are scaled by the domain length + expected = SPEED if physical else SPEED / LENGTH + assert coeffs[0][0] == pytest.approx(expected, rel=0.02) + + +def test_needs_exactly_one_fft_direction(): + with pytest.raises(AssertionError, match="slice_at"): + power_spectrum_2d(standing_waves(tend=1.0), slice_at=(None, None, 0)) diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 1729b4228..4bea38ac4 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -143,7 +143,8 @@ def test_animation_and_frames_share_the_view(tmp_path): def test_scalar_overview_and_export(tmp_path): - result, error = plot_scalars(scalar_dataset(), run_label="run") + result = plot_scalars(scalar_dataset(), run_label="run") + error = result.data["relative_error"] assert error is not None assert result.fig._suptitle.get_text() == "run" paths = save_all_scalars(scalar_dataset(), tmp_path) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 9bfd99437..643efca1b 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -927,15 +927,31 @@ def generate_default_parameter_file( return path def to_dict(self) -> dict: - """Serialize the model configuration to a dictionary.""" - dct = {"model": self.__class__.__name__} - return dct + """Serialize the model class and the arguments passed to its ``__init__``. + + Configuration applied after construction (markers, backgrounds, perturbations, + propagator options) is not part of this dictionary. + """ + params = {} + for key, value in self.params.items(): + if isinstance(value, BaseUnits): + value = {"BaseUnits": value.to_dict()} + elif not isinstance(value, (bool, int, float, str, tuple, list, type(None))): + raise TypeError(f"cannot serialize argument {key}={value!r} of {self.__class__.__name__}") + params[key] = value + return {"model": self.__class__.__name__, "params": params} @classmethod def from_dict(cls, dct) -> "StruphyModel": - """Deserialize a model configuration from a dictionary.""" - model_name = dct["model"] - return cls.from_name(model_name) + """Deserialize a model from :meth:`to_dict`.""" + from struphy.models.utils import get_model_by_name + + params = {} + for key, value in dct.get("params", {}).items(): + if isinstance(value, dict) and set(value) == {"BaseUnits"}: + value = BaseUnits.from_dict(value["BaseUnits"]) + params[key] = value + return get_model_by_name(dct["model"])(**params) @classmethod def from_name(cls, name: str) -> "StruphyModel": diff --git a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py index 735ba087a..be85c42f3 100644 --- a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py +++ b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py @@ -99,14 +99,12 @@ def test_chorin_projection_periodic_1d(nx: int, do_plot: bool = False): derham_opts=derham_opts, ) - sim.run() + run = sim.run() + run.process() if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - sim.load_plotting_data() - - e1_grid = np.asarray(sim.f.fluid.e1_current_1.grid_e1).flatten() - j1_binned = np.asarray(sim.f.fluid.e1_current_1.f_binned) # (Nt+1, n_bins) + e1_grid = run.distributions.fluid.e1_current_1.f_binned.e1.values.flatten() + j1_binned = run.distributions.fluid.e1_current_1.f_binned.values # (Nt+1, n_bins) amp_initial = 0.5 * (np.max(j1_binned[0]) - np.min(j1_binned[0])) amp_final = 0.5 * (np.max(j1_binned[-1]) - np.min(j1_binned[-1])) @@ -207,14 +205,12 @@ def test_chorin_projection_reflect_1d(nx: int, do_plot: bool = False): derham_opts=derham_opts, ) - sim.run() + run = sim.run() + run.process() if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - sim.load_plotting_data() - - e1_grid = np.asarray(sim.f.fluid.e1_current_1.grid_e1).flatten() - j1_binned = np.asarray(sim.f.fluid.e1_current_1.f_binned) # (Nt+1, n_bins) + e1_grid = run.distributions.fluid.e1_current_1.f_binned.e1.values.flatten() + j1_binned = run.distributions.fluid.e1_current_1.f_binned.values # (Nt+1, n_bins) amp_initial = np.max(np.abs(j1_binned[0])) amp_final = np.max(np.abs(j1_binned[-1])) @@ -320,15 +316,13 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): derham_opts=derham_opts, ) - sim.run() + run = sim.run() + run.process() if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - sim.load_plotting_data() - - e2_grid = np.asarray(sim.f.fluid.e2_current_1.grid_e2).flatten() - j1_binned = np.asarray(sim.f.fluid.e2_current_1.f_binned) # (Nt+1, n_bins) - j2_binned = np.asarray(sim.f.fluid.e2_current_2.f_binned) # (Nt+1, n_bins) + e2_grid = run.distributions.fluid.e2_current_1.f_binned.e2.values.flatten() + j1_binned = run.distributions.fluid.e2_current_1.f_binned.values # (Nt+1, n_bins) + j2_binned = run.distributions.fluid.e2_current_2.f_binned.values # (Nt+1, n_bins) # Analytische Profile U = 0.5 @@ -363,9 +357,9 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): plt.tight_layout() plt.show() - e2_grid = np.asarray(sim.f.fluid.e2_current_1.grid_e2).flatten() - j1_binned = np.asarray(sim.f.fluid.e2_current_1.f_binned) # (Nt+1, n_bins) - j2_binned = np.asarray(sim.f.fluid.e2_current_2.f_binned) # (Nt+1, n_bins) + e2_grid = run.distributions.fluid.e2_current_1.f_binned.e2.values.flatten() + j1_binned = run.distributions.fluid.e2_current_1.f_binned.values # (Nt+1, n_bins) + j2_binned = run.distributions.fluid.e2_current_2.f_binned.values # (Nt+1, n_bins) # --- DEBUG: Check marker velocities --- markers = model.fluid.density.particles.markers diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 72f63b343..854c305c4 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -18,7 +18,6 @@ perturbations, set_logging_level, ) -from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import LinearMHD set_logging_level() @@ -75,29 +74,23 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): ) # run - sim.run() + run = sim.run().with_time_units("normalized") # post processing - if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() + run.process() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - sim.load_plotting_data() # first fft - u_of_t = sim.spline_values.mhd.velocity_log.data - Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} - _1, _2, _3, coeffs = power_spectrum_2d( - u_of_t, - "velocity_log", - grids=sim.grids_log, - grids_mapped=sim.grids_phy, + _1, _2, _3, coeffs = run.analysis.dispersion( + "mhd/velocity_log", + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -116,13 +109,9 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 # second fft - p_of_t = sim.spline_values.mhd.pressure_log.data - - _1, _2, _3, coeffs = power_spectrum_2d( - p_of_t, - "pressure_log", - grids=sim.grids_log, - grids_mapped=sim.grids_phy, + _1, _2, _3, coeffs = run.analysis.dispersion( + "mhd/pressure_log", + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 414528986..8f62fa1bc 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -19,7 +19,6 @@ grids, perturbations, ) -from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import Maxwell logger = logging.getLogger("struphy") @@ -65,23 +64,17 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): ) # run - sim.run() + run = sim.run().with_time_units("normalized") # post processing - if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() + run.process() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - sim.load_plotting_data() - # fft - E_of_t = sim.spline_values.em_fields.e_field_log.data - _1, _2, _3, coeffs = power_spectrum_2d( - E_of_t, - "e_field_log", - grids=sim.grids_log, - grids_mapped=sim.grids_phy, + _1, _2, _3, coeffs = run.analysis.dispersion( + "em_fields/e_field_log", + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -150,11 +143,10 @@ def test_coaxial(do_plot: bool = False): ) # run - sim.run() + run = sim.run().with_time_units("normalized") # post processing - if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc(physical=True) + run.process(physical=True) # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: @@ -164,16 +156,14 @@ def test_coaxial(do_plot: bool = False): num_elements = grid.num_elements modes = m - # load data - sim.load_plotting_data() - - t_grid = sim.t_grid - grids_phy = sim.grids_phy - e_field_phy = sim.spline_values.em_fields.e_field_phy.data - b_field_phy = sim.spline_values.em_fields.b_field_phy.data + # load data at the final time in the plane eta3 = 0 + e_field_phy = run.fields.em_fields.e_field_phy.isel(t=-1, e3=0) + b_field_phy = run.fields.em_fields.b_field_phy.isel(t=-1, e3=0) + t_end = float(e_field_phy.t) - X = grids_phy[0][:, :, 0] - Y = grids_phy[1][:, :, 0] + X = e_field_phy.X.values + Y = e_field_phy.Y.values + Z = e_field_phy.Z.values # define analytic solution def B_z(X, Y, Z, m, t): @@ -208,13 +198,13 @@ def to_E_theta(X, Y, E_x, E_y): # plot if do_plot: - vmin = E_theta(X, Y, grids_phy[0], modes, 0).min() - vmax = E_theta(X, Y, grids_phy[0], modes, 0).max() + vmin = E_theta(X, Y, Z, modes, 0).min() + vmax = E_theta(X, Y, Z, modes, 0).max() fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) plot_exac = ax1.contourf( X, Y, - E_theta(X, Y, grids_phy[0], modes, t_grid[-1]), + E_theta(X, Y, Z, modes, t_end), cmap="plasma", levels=100, vmin=vmin, @@ -223,7 +213,7 @@ def to_E_theta(X, Y, E_x, E_y): ax2.contourf( X, Y, - to_E_theta(X, Y, e_field_phy[t_grid[-1]][0][:, :, 0], e_field_phy[t_grid[-1]][1][:, :, 0]), + to_E_theta(X, Y, e_field_phy.isel(component=0).values, e_field_phy.isel(component=1).values), cmap="plasma", levels=100, vmin=vmin, @@ -236,12 +226,12 @@ def to_E_theta(X, Y, E_x, E_y): plt.show() # assert - Ex_tend = e_field_phy[t_grid[-1]][0][:, :, 0] - Ey_tend = e_field_phy[t_grid[-1]][1][:, :, 0] - Er_exact = E_r(X, Y, grids_phy[0], modes, t_grid[-1]) - Etheta_exact = E_theta(X, Y, grids_phy[0], modes, t_grid[-1]) - Bz_tend = b_field_phy[t_grid[-1]][2][:, :, 0] - Bz_exact = B_z(X, Y, grids_phy[0], modes, t_grid[-1]) + Ex_tend = e_field_phy.isel(component=0).values + Ey_tend = e_field_phy.isel(component=1).values + Er_exact = E_r(X, Y, Z, modes, t_end) + Etheta_exact = E_theta(X, Y, Z, modes, t_end) + Bz_tend = b_field_phy.isel(component=2).values + Bz_exact = B_z(X, Y, Z, modes, t_end) error_Er = xp.max(xp.abs((to_E_r(X, Y, Ex_tend, Ey_tend) - Er_exact))) error_Etheta = xp.max(xp.abs((to_E_theta(X, Y, Ex_tend, Ey_tend) - Etheta_exact))) diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 1285648eb..ce2a5e337 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -76,22 +76,16 @@ def test_poisson_1d(do_plot=False): ) # run - sim.run() + run = sim.run().with_time_units("normalized") # post processing - if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() + run.process() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - sim.load_plotting_data() - - phi = sim.spline_values.em_fields.phi_log.data - source = sim.spline_values.em_fields.source_log.data - x = sim.grids_phy[0][:, 0, 0] - y = sim.grids_phy[1][0, :, 0] - z = sim.grids_phy[2][0, 0, :] - time = sim.t_grid + phi = run.fields.em_fields.phi_log.isel(e2=0, e3=0) + source = run.fields.em_fields.source_log.isel(e2=0, e3=0) + x = phi.X.values interval = 2 c = 0 @@ -99,8 +93,8 @@ def test_poisson_1d(do_plot=False): fig = plt.figure(figsize=(12, 40)) err = 0.0 - for i, t in enumerate(phi): - phi_h = phi[t][0][:, 0, 0] + for i, t in enumerate(phi.t.values): + phi_h = phi.isel(t=i).values phi_e = phi_exact(x, 0, 0, t) new_err = xp.abs(xp.max(phi_h - phi_e)) / (amp / (l * 2 * xp.pi / Lx) ** 2) if new_err > err: @@ -115,7 +109,7 @@ def test_poisson_1d(do_plot=False): plt.legend() plt.subplot(5, 2, 2 * c + 2) - plt.plot(x, source[t][0][:, 0, 0], label="rhs") + plt.plot(x, source.isel(t=i).values, label="rhs") plt.plot(x, rhs_exact(x, 0, 0, t), "r--", label="exact") plt.title(f"source at {t =}") plt.ylim(-amp, amp) diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index 2d04113bc..abcf54e5c 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -109,17 +109,14 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): ) # run - sim.run() + run = sim.run() + run.process() - # post processing + # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - - # diagnostics - sim.load_plotting_data() - - ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph - n_sph = sim.n_sph.euler_fluid.view_0.n_sph + density = run.densities.euler_fluid.view_0.n_sph + ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + n_sph = density.values if do_plot: ppb = 8 @@ -243,20 +240,17 @@ def test_damped_sound_wave(nx: int, plot_pts: int, do_plot: bool = False): ) # run - sim.run() + run = sim.run() + run.process() - # post processing + # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - - # diagnostics - sim.load_plotting_data() - - e1_binned = sim.f.euler_fluid.e1_density.grid_e1 - n_binned = sim.f.euler_fluid.e1_density.delta_f_binned - j1_binned = sim.f.euler_fluid.e1_current_1.f_binned - ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph - n_sph = sim.n_sph.euler_fluid.view_0.n_sph + e1_binned = run.distributions.euler_fluid.e1_density.f_binned.e1.values + n_binned = run.distributions.euler_fluid.e1_density.delta_f_binned.values + j1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.values + density = run.densities.euler_fluid.view_0.n_sph + ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + n_sph = density.values print(f"{e1_binned.shape = }") print(f"{n_binned.shape = }") @@ -452,20 +446,17 @@ def test_velocity_diffusion(nx: int, plot_pts: int, do_plot: bool = False): ) # run - sim.run() + run = sim.run() + run.process() - # post processing + # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - - # diagnostics - sim.load_plotting_data() - - ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph - n_sph = sim.n_sph.euler_fluid.view_0.n_sph - e1_binned = sim.f.euler_fluid.e1_density.grid_e1 - n_binned = sim.f.euler_fluid.e1_density.f_binned - j1_binned = sim.f.euler_fluid.e1_current_1.f_binned + density = run.densities.euler_fluid.view_0.n_sph + ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + n_sph = density.values + e1_binned = run.distributions.euler_fluid.e1_density.f_binned.e1.values + n_binned = run.distributions.euler_fluid.e1_density.f_binned.values + j1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.values print(f"{e1_binned.shape = }") print(f"{n_binned.shape = }") print(f"{j1_binned.shape = }") @@ -667,14 +658,12 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ derham_opts=None, ) - sim.run() + run = sim.run() + run.process() if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - sim.load_plotting_data() - - e2_grid = sim.f.euler_fluid.e2_current_1.grid_e2 # logical y in [0, 1] - j1_binned = sim.f.euler_fluid.e2_current_1.f_binned # shape (Nt+1, n_bins) + e2_grid = run.distributions.euler_fluid.e2_current_1.f_binned.e2.values # logical y in [0, 1] + j1_binned = run.distributions.euler_fluid.e2_current_1.f_binned.values # shape (Nt+1, n_bins) import numpy as np @@ -752,7 +741,7 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ from matplotlib.colors import LinearSegmentedColormap from tqdm import tqdm as _tqdm - orbits = np.asarray(sim.orbits.euler_fluid) # (Nt_orb, n_markers, n_attrs) + orbits = np.asarray(run.orbits.euler_fluid) # (Nt_orb, n_markers, n_attrs) # attrs for vdim=2: [x, y, z, v1, v2, w, diag, id] Nt_orb = orbits.shape[0] @@ -929,27 +918,26 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo derham_opts=None, ) - sim.run() + run = sim.run() + run.process() if MPI.COMM_WORLD.Get_rank() == 0: - sim.pproc() - sim.load_plotting_data() - import numpy as np dt = time_opts.dt Nt = int(time_opts.Tend / dt) times = np.linspace(0.0, time_opts.Tend, Nt + 1) - ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph - n_sph = sim.n_sph.euler_fluid.view_0.n_sph # (Nt+1, pts_e1, pts_e2, 1) + density = run.densities.euler_fluid.view_0.n_sph + ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + n_sph = density.values # (Nt+1, pts_e1, pts_e2, 1) X = np.asarray(ee1)[:, :, 0] * r1 # physical x, shape (pts_e1, pts_e2) Y = np.asarray(ee2)[:, :, 0] * r2 # physical y, shape (pts_e1, pts_e2) n_arr = np.asarray(n_sph) # (Nt+1, pts_e1, pts_e2, 1) # orbits needed for both do_plot scatter overlay and create_png - orbits = np.asarray(sim.orbits.euler_fluid) # (Nt_orb, n_markers, n_attrs) + orbits = np.asarray(run.orbits.euler_fluid) # (Nt_orb, n_markers, n_attrs) Nt_orb = orbits.shape[0] t_orbit = np.linspace(0.0, time_opts.Tend, Nt_orb) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 93d692129..afa89d85c 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1073,6 +1073,12 @@ def _post_process_f( assert isinstance(var, PICVariable | SPHVariable) f_bckgr: KineticBackground = var.backgrounds break + if f_bckgr is None: + raise ValueError( + f"The background of {species} is needed to post-process its delta-f distribution " + "function, but it is not configured. A simulation restored from config.json only " + "knows the model arguments; run from a parameter file to keep the background." + ) # load all grids of the variables of f grid_tot = [] @@ -1170,24 +1176,11 @@ def _post_process_n_sph( os.mkdir(path_view) self.comm.Barrier() - # build meshgrid and save - eta1 = file_0["kinetic/" + species + "/n_sph/" + view].attrs["eta1"] - eta2 = file_0["kinetic/" + species + "/n_sph/" + view].attrs["eta2"] - eta3 = file_0["kinetic/" + species + "/n_sph/" + view].attrs["eta3"] - - ee1, ee2, ee3 = xp.meshgrid( - eta1, - eta2, - eta3, - indexing="ij", - ) - + # save the 1d evaluation points, one file per logical direction + attrs = file_0["kinetic/" + species + "/n_sph/" + view].attrs if self.rank == 0: - grid_path = os.path.join( - path_view, - "grid_n_sph.npy", - ) - xp.save(grid_path, (ee1, ee2, ee3)) + for direction in ("1", "2", "3"): + xp.save(os.path.join(path_view, f"grid_e{direction}.npy"), attrs["eta" + direction][:]) # compute sph density for view in tqdm(views): diff --git a/src/struphy/post_processing/run.py b/src/struphy/post_processing/run.py index d94757b9f..2001bb71f 100644 --- a/src/struphy/post_processing/run.py +++ b/src/struphy/post_processing/run.py @@ -12,6 +12,7 @@ import xarray as xr from struphy.post_processing.arrays import data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits +from struphy.post_processing.run_accessors import RunAnalysis, RunPlots logger = logging.getLogger("struphy") @@ -100,28 +101,6 @@ class OrbitProducts(ProductNamespace): """Marker trajectories grouped by species.""" -class PlotAccessor: - """Convenient plotting entry points bound to a run.""" - - def __init__(self, run: "Run"): - self._run = run - - def timeseries(self, data, **kwargs): - from struphy.diagnostics.plotting import plot_timeseries - return plot_timeseries(data, run_label=self._run.label, **kwargs) - - def scalar(self, name: str, **kwargs): - return self.timeseries(self._run.scalars[name], **kwargs) - - def slice(self, data, **kwargs): - from struphy.diagnostics.plotting import plot_slice - return plot_slice(data, run_label=self._run.label, **kwargs) - - def viewer(self, data, **kwargs): - from struphy.diagnostics.plotting import InteractiveSliceViewer - return InteractiveSliceViewer(data, run_label=self._run.label, **kwargs) - - class Run: """The output of one Struphy simulation, loaded lazily from its output folder. @@ -134,6 +113,9 @@ class Run: default options; call :meth:`process` beforehand to choose options. * :attr:`sim` is the :class:`~struphy.Simulation` that produced the output: the live object for ``sim.output``, otherwise restored from disk without allocating anything. + * :attr:`plot` and :attr:`analysis` draw and evaluate standard diagnostics, e.g. + ``run.plot.timeseries("en_phi", fit=(0, 40))``; ``run["en_phi"]`` looks up any product. + * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. Parameters ---------- @@ -157,8 +139,29 @@ def __init__(self, path_out, *, sim=None, time_units: str = "physical"): def __repr__(self): return f"{type(self).__name__}({str(self.path_out)!r}, processed={self.is_processed})" + def with_time_units(self, time_units: str) -> "Run": + """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" + return type(self)(self.path_out, sim=self._sim, time_units=time_units) + def _reset(self): - self._time = self._grids_log = self._grids_phy = self._scalars = self._products = None + self._time = self._grids_log = self._grids_phy = self._scalars = self._products = self._label = None + + def __getitem__(self, name: str) -> xr.DataArray: + """Any product by name: a scalar (``"en_tot"``), a field (``"em_fields/phi_log"``), a binned + distribution or SPH density (``"kinetic_ions/e1_v1_density/f_binned"``) or orbits (``"kinetic_ions"``). + """ + if name in self.scalars.data_vars: + return self.scalars[name] + for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): + if name in catalog: + return catalog[name] + available = (*self.scalars.data_vars, *self.field_catalog, *self.distribution_catalog, + *self.density_catalog, *self.orbit_catalog) + raise KeyError(f"{name!r} not found; available products: {available}") + + def _stamp(self, array: xr.DataArray) -> xr.DataArray: + array.attrs.update(run=self.label, run_name=self.path_out.name) + return array @property def path_pproc(self) -> Path: @@ -247,11 +250,15 @@ def _ensure_processed(self): def _product_mappings(self) -> dict[str, ProductMapping]: if self._products is None: self._ensure_processed() + discovered = { + "fields": self._discover_fields(), + "distributions": self._discover_binned("distribution_function"), + "densities": self._discover_binned("n_sph"), + "orbits": self._discover_orbits(), + } self._products = { - "fields": ProductMapping(self._discover_fields()), - "distributions": ProductMapping(self._discover_binned("distribution_function")), - "densities": ProductMapping(self._discover_binned("n_sph")), - "orbits": ProductMapping(self._discover_orbits()), + kind: ProductMapping({key: (lambda load=load: self._stamp(load())) for key, load in loaders.items()}) + for kind, loaders in discovered.items() } return self._products @@ -292,8 +299,14 @@ def orbit_catalog(self) -> ProductMapping: return self._product_mappings()["orbits"] @property - def plot(self) -> PlotAccessor: - return PlotAccessor(self) + def plot(self) -> RunPlots: + """Standard plots, e.g. ``run.plot.scalars()`` or ``run.plot.panels(name, x="e1", y="v1")``.""" + return RunPlots(self) + + @property + def analysis(self) -> RunAnalysis: + """Quantitative diagnostics, e.g. ``run.analysis.growth_rate("en_phi", window=(0, 40))``.""" + return RunAnalysis(self) @property def time_scale(self) -> float: @@ -342,32 +355,45 @@ def scalars(self) -> xr.Dataset: time = np.asarray(file["time/value"]) * self.time_scale variables = {} for name, dataset in file["scalar"].items(): - variables[name] = data_array(np.asarray(dataset), ("t",), {"t": time}, name=name, - label=name.replace("_", " "), coord_units={"t": self.time_unit}) + variables[name] = self._stamp(data_array(np.asarray(dataset), ("t",), {"t": time}, name=name, + label=name.replace("_", " "), + coord_units={"t": self.time_unit})) self._scalars = xr.Dataset(variables) return self._scalars @property def label(self) -> str: """Short description of the numerical parameters, for figure titles.""" - sim = self.sim - values = [] - for holder, attr, name in ((sim.time_opts, "dt", "dt"), - (sim.time_opts, "split_algo", "algo"), - (sim.grid, "num_elements", "Nel"), - (sim.derham_opts, "degree", "p")): - value = getattr(holder, attr, None) if holder is not None else None - if value is not None: - values.append(f"{name}={value}") - return ", ".join(values) or self.path_out.name + if self._label is None: + try: + sim = self.sim + except FileNotFoundError: # an output folder without its configuration + self._label = self.path_out.name + return self._label + values = [] + for holder, attr, name in ((sim.time_opts, "dt", "dt"), + (sim.time_opts, "split_algo", "algo"), + (sim.grid, "num_elements", "Nel"), + (sim.derham_opts, "degree", "p")): + value = getattr(holder, attr, None) if holder is not None else None + if value is not None: + values.append(f"{name}={value}") + self._label = ", ".join(values) or self.path_out.name + return self._label def save_scalars(self, path=None, **kwargs) -> str: + """Write the scalar time series as CSV (or NPZ); ``post_processing/scalars.csv`` by default.""" path = Path(path) if path else self.path_pproc / "scalars.csv" return save_scalars(self.scalars, str(path), **kwargs) - def save_scalar_plots(self, directory=None, **kwargs): + def save_report(self, directory=None, **kwargs) -> list[str]: + """Write the standard report: a scalar table, the scalar overview and one figure per scalar. + + Files go to ``post_processing/report/`` by default; returns their paths. + """ from struphy.diagnostics.plotting import save_all_scalars - directory = Path(directory) if directory else self.path_pproc / "scalars" + + directory = Path(directory) if directory else self.path_pproc / "report" return save_all_scalars(self.scalars, directory, run_label=self.label, **kwargs) def _discover_fields(self): @@ -403,23 +429,28 @@ def _discover_binned(self, category: str): def _load_binned(self, path: Path, slice_name: str): grid_paths = sorted(path.parent.glob("grid_*.npy")) grids = {p.stem.removeprefix("grid_"): np.load(p, mmap_mode="r") for p in grid_paths} - dims = tuple(part for part in slice_name.split("_") if part in grids) + # binned slices are named after their dimensions (e1_v1_density); SPH views (view_0) are not + dims = tuple(part for part in slice_name.split("_") if part in grids) or tuple(sorted(grids)) values = np.load(path, mmap_mode="r") expected = (len(self.time), *(len(grids[dim]) for dim in dims)) if values.shape != expected: raise ValueError(f"{path} has shape {values.shape}; expected {expected} from its coordinates") coords = {"t": self.time, **{dim: grids[dim] for dim in dims}} logical_dims = tuple(dim for dim in dims if dim in {"e1", "e2", "e3"}) - if len(logical_dims) == 2: - mesh = np.meshgrid(*(np.asarray(grids[dim]) for dim in logical_dims), indexing="ij") - arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} - arguments.update(dict(zip(logical_dims, mesh))) - try: + try: + if len(logical_dims) == 2: + mesh = np.meshgrid(*(np.asarray(grids[dim]) for dim in logical_dims), indexing="ij") + arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} + arguments.update(dict(zip(logical_dims, mesh))) physical = self.sim.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) - for coordinate, grid in zip(("X", "Y", "Z"), physical): - coords[coordinate] = (logical_dims, np.asarray(grid)) - except (FileNotFoundError, TypeError, ValueError): - logger.debug("Could not attach physical coordinates to %s", path, exc_info=True) + elif len(logical_dims) == 3: + physical = self.sim.domain(*(np.asarray(grids[dim]) for dim in ("e1", "e2", "e3"))) + else: + physical = () + for coordinate, grid in zip(("X", "Y", "Z"), physical): + coords[coordinate] = (logical_dims, np.asarray(grid)) + except (FileNotFoundError, TypeError, ValueError): + logger.debug("Could not attach physical coordinates to %s", path, exc_info=True) return wrap_binned_data(values, dims, coords, name=path.stem, time_unit=self.time_unit) def _discover_orbits(self): @@ -433,7 +464,8 @@ def _load_orbits(self, directory: Path): paths = sorted(directory.glob("*.npy"), key=lambda p: int(p.stem.rsplit("_", 1)[-1])) if not paths: raise FileNotFoundError(f"no orbit arrays in {directory}") - values = np.stack([np.load(path, mmap_mode="r") for path in paths]) + # one small file per saved step: read them instead of keeping thousands of memory maps open + values = np.stack([np.load(path) for path in paths]) return wrap_orbits(values, self.time[:len(paths)], time_unit=self.time_unit) diff --git a/src/struphy/post_processing/run_accessors.py b/src/struphy/post_processing/run_accessors.py new file mode 100644 index 000000000..62aab575a --- /dev/null +++ b/src/struphy/post_processing/run_accessors.py @@ -0,0 +1,234 @@ +"""``run.plot`` and ``run.analysis``: plotting and analysis without extra imports. + +Every method accepts a product name (``"en_phi"``, ``"em_fields/phi_log"``, +``"kinetic_ions/e1_v1_density/f_binned"``; see :meth:`Run.__getitem__`) or any labeled +array, including arrays derived from or belonging to another run. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import xarray as xr + +if TYPE_CHECKING: + from struphy.post_processing.run import Run + +Coordinates = Literal["logical", "physical"] +Plane = Literal["XY", "XZ", "YZ", "RZ"] + + +class RunPlots: + """Standard plots of a run, as ``run.plot.(...)``. + + Plots return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with + ``.show()`` and ``.save(path)``. Figures are titled with the run's numerical parameters; + time series of different runs are labeled by run. + """ + + def __init__(self, run: "Run"): + self._run = run + + def _array(self, data) -> xr.DataArray: + return self._run[data] if isinstance(data, str) else data + + def _run_label(self, arrays) -> str: + from struphy.diagnostics.plotting import shared_run_label + + runs = {array.attrs.get("run") for array in arrays} - {None, ""} + return shared_run_label(arrays) if runs else self._run.label + + @staticmethod + def _view(x, y, sweep, coords, plane, select, isel): + from struphy.diagnostics.plotting import View + + return View(x=x, y=y, sweep=sweep, select=dict(select or {}), isel=dict(isel or {}), + coordinates=coords, plane=plane) + + def scalars(self, names=None, *, conservation: str | None = "auto", relative_to: str | None = None, + logy: bool = False): + """Overview of the scalar time series with a conservation-error panel. + + Parameters + ---------- + names: + Scalars to show; all by default. + conservation: + Scalar whose relative error is shown below, e.g. ``"en_tot"``; ``"auto"`` picks the + total energy if it was saved, ``None`` shows no panel. The error is returned in + ``result.data["relative_error"]``. + relative_to: + Show every scalar divided by this one. + logy: + Logarithmic value axis. + """ + from struphy.diagnostics.plotting import plot_scalars + + scalars = self._run.scalars + if conservation == "auto": + conservation = next((name for name in ("en_tot", "total_energy") if name in scalars), None) + return plot_scalars(scalars, names=names, relative_to=relative_to, error_panel=conservation, logy=logy, + run_label=self._run.label) + + def timeseries(self, *data, logy: bool = True, fit: tuple[float | None, float | None] | bool | None = None, + fit_amplitude: bool = False, title: str | None = None, ax=None): + """One or more time series, optionally with an exponential growth-rate fit. + + Parameters + ---------- + *data: + Names or arrays with the single dimension ``t``, e.g. ``"en_phi"``. + logy: + Logarithmic value axis. + fit: + Time window ``(t0, t1)`` of an exponential fit per series (``None`` for an open + end), or ``True`` for the whole series. Rates are in ``result.fit_results``. + fit_amplitude: + The series is quadratic in an amplitude (e.g. an energy); fit the amplitude's rate. + title: + Axes title; the first series' label by default. + ax: + Draw into these axes instead of a new figure. + """ + from struphy.diagnostics.plotting import GrowthFit, plot_timeseries + + if not data: + raise TypeError("timeseries() needs at least one name or array") + series = [self._array(item) for item in data] + growth = None + if fit is not None and fit is not False: + window = (None, None) if fit is True else tuple(fit) + growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) + return plot_timeseries(series, ax=ax, logy=logy, fit=growth, title=title, run_label=self._run_label(series)) + + def slice(self, data, *, x: str | None = None, y: str | None = None, coords: Coordinates = "logical", + plane: Plane = "XY", select: dict | None = None, isel: dict | None = None, vmin=None, vmax=None, + equal_aspect: bool | None = None, title: str | None = None, ax=None): + """A two-dimensional color plot of one slice. + + Parameters + ---------- + data: + Name or array; select all but two dimensions, here or with ``select``/``isel``. + x, y: + Displayed dimensions, e.g. ``x="e1", y="v1"``; inferred for two-dimensional data. + coords: + ``"physical"`` draws on the mapped coordinates of ``plane`` instead of logical ones. + select, isel: + Selections by nearest coordinate value or by index, e.g. ``isel={"t": -1}``. + """ + from struphy.diagnostics.plotting import plot_slice + + array = self._array(data) + return plot_slice(array, view=self._view(x, y, "t", coords, plane, select, isel), ax=ax, vmin=vmin, + vmax=vmax, equal_aspect=equal_aspect, title=title, run_label=self._run_label([array])) + + def panels(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, + isel: dict | None = None, nrows: int = 3, ncols: int = 4, shared_clim: bool = True, + title: str | None = None): + """Snapshots evenly spread along ``sweep`` (time by default), one panel each.""" + from struphy.diagnostics.plotting import plot_panels + + array = self._array(data) + return plot_panels(array, view=self._view(x, y, sweep, coords, plane, select, isel), nrows=nrows, + ncols=ncols, shared_clim=shared_clim, title=title, run_label=self._run_label([array])) + + def viewer(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, + isel: dict | None = None, vmin=None, vmax=None): + """An interactive slice viewer with one slider per non-displayed dimension. + + Call ``.show()`` on the result; keep it alive so that the sliders stay connected. + """ + from struphy.diagnostics.plotting import InteractiveSliceViewer + + array = self._array(data) + return InteractiveSliceViewer(array, view=self._view(x, y, sweep, coords, plane, select, isel), vmin=vmin, + vmax=vmax, run_label=self._run_label([array])) + + def animation(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, + isel: dict | None = None, interval: int = 100, step: int = 1, vmin=None, vmax=None): + """A Matplotlib animation along ``sweep``.""" + from struphy.diagnostics.plotting import animate_slices + + return animate_slices(self._array(data), view=self._view(x, y, sweep, coords, plane, select, isel), + interval=interval, step=step, vmin=vmin, vmax=vmax) + + def frames(self, data, directory, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, + isel: dict | None = None, step: int = 1, prefix: str = "frame", dpi: int = 110) -> list[str]: + """Write the slices along ``sweep`` as numbered PNG files; returns their paths.""" + from struphy.diagnostics.plotting import save_frames + + return save_frames(self._array(data), directory, view=self._view(x, y, sweep, coords, plane, select, isel), + step=step, prefix=prefix, dpi=dpi) + + def orbits(self, species: str | None = None, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): + """Three-dimensional trajectories of the saved markers of ``species``.""" + from struphy.diagnostics.plotting import plot_marker_trajectories + + available = tuple(self._run.orbits) + if species is None: + if len(available) != 1: + raise ValueError(f"choose a species from {available}") + species = available[0] + return plot_marker_trajectories(self._run.orbits[species], ax=ax, max_markers=max_markers, + show_paths=show_paths) + + def equilibrium(self, ax=None): + """Radial equilibrium profiles, from the geometry written at the start of the run.""" + from struphy.diagnostics.plotting import plot_equilibrium_profile + + return plot_equilibrium_profile(self._run.path_out, ax=ax) + + +class RunAnalysis: + """Quantitative diagnostics of a run, as ``run.analysis.(...)``.""" + + def __init__(self, run: "Run"): + self._run = run + + def _array(self, data) -> xr.DataArray: + return self._run[data] if isinstance(data, str) else data + + def growth_rate(self, data, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): + """Fit ``exp(rate * t + intercept)`` to a time series within ``window``. + + With ``amplitude=True`` the series is quadratic in an amplitude (e.g. an energy) and the + amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, + ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. + """ + from struphy.diagnostics.plotting import GrowthFit, growth_rate + + return growth_rate(self._array(data), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def drift(self, data, *, ref=None) -> xr.DataArray: + """Signed deviation of a time series from ``ref`` or from its first sample.""" + from struphy.diagnostics.plotting import drift + + return drift(self._array(data), ref=ref) + + def relative_error(self, data, *, ref=None, skip_first: bool = True) -> xr.DataArray: + """Absolute relative deviation of a time series from ``ref`` or from its first sample.""" + from struphy.diagnostics.plotting import relative_error + + return relative_error(self._array(data), ref=ref, skip_first=skip_first) + + def dispersion(self, field, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, + **kwargs): + """Space-time power spectrum of a field and fitted dispersion branches. + + The spectrum is computed in normalized time. See + :func:`struphy.diagnostics.diagn_tools.power_spectrum_2d` for ``slice_at``, the fit options + and ``do_plot``. Returns ``(omega, kvec, spectrum, coeffs)``. + """ + from struphy.diagnostics.diagn_tools import power_spectrum_2d + + if isinstance(field, str): + run = self._run if self._run.time_units == "normalized" else self._run.with_time_units("normalized") + field = run[field] + elif field.t.attrs.get("units") == "s": + raise ValueError("pass the field by name, or take it from run.with_time_units('normalized')") + return power_spectrum_2d(field, component=component, slice_at=slice_at, physical=physical, **kwargs) diff --git a/src/struphy/post_processing/tests/test_run.py b/src/struphy/post_processing/tests/test_run.py index cc2fe7b52..fb94ff037 100644 --- a/src/struphy/post_processing/tests/test_run.py +++ b/src/struphy/post_processing/tests/test_run.py @@ -35,6 +35,11 @@ def write_tree(root): np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) + view_dir = os.path.join(kinetic, "kinetic_ions", "n_sph", "view_0") + os.makedirs(view_dir) + for direction, n in zip("123", (N1, N2, 1)): + np.save(os.path.join(view_dir, f"grid_e{direction}.npy"), np.linspace(0, 1, n)) + np.save(os.path.join(view_dir, "n_sph.npy"), np.ones((NT, N1, N2, 1))) orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") for step in range(NT): np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), step)) @@ -61,7 +66,7 @@ def write_manifest(root, **options): class FakeSim: """Just enough of a Simulation for Run: no configuration, a single rank.""" - time_opts = grid = derham_opts = None + time_opts = grid = derham_opts = domain = None rank, comm_size = 0, 1 def __init__(self): @@ -98,6 +103,13 @@ def test_binned_products_have_coordinates(run): np.testing.assert_allclose(data.v1, np.linspace(-3, 3, NV)) +def test_sph_density_views_take_dimensions_from_their_grids(run): + data = run.densities.kinetic_ions.view_0.n_sph + assert data.dims == ("t", "e1", "e2", "e3") + assert data.shape == (NT, N1, N2, 1) + np.testing.assert_allclose(data.e2, np.linspace(0, 1, N2)) + + def test_orbit_product_keeps_column_semantics(run): data = run.orbits["kinetic_ions"] assert data.dims == ("t", "marker", "attribute") diff --git a/src/struphy/post_processing/tests/test_run_accessors.py b/src/struphy/post_processing/tests/test_run_accessors.py new file mode 100644 index 000000000..29b8f387c --- /dev/null +++ b/src/struphy/post_processing/tests/test_run_accessors.py @@ -0,0 +1,115 @@ +"""Tests for run.plot, run.analysis and product lookup by name.""" + +import os + +import h5py +import matplotlib + +matplotlib.use("Agg") + +import numpy as np # noqa: E402 +import pytest # noqa: E402 +from matplotlib import pyplot as plt # noqa: E402 + +from struphy.post_processing.run import Run # noqa: E402 +from struphy.post_processing.tests.test_run import NT, FakeSim, write_manifest, write_tree # noqa: E402 + +RATE = 2.0 + + +def make_run(root, name="sim_1"): + path = os.path.join(root, name) + os.makedirs(path) + write_tree(path) + with h5py.File(os.path.join(path, "data", "data_proc0.hdf5"), "a") as file: + time = np.asarray(file["time/value"]) + file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) + write_manifest(path) + return Run(path, sim=FakeSim(), time_units="normalized") + + +@pytest.fixture +def run(tmp_path): + return make_run(str(tmp_path)) + + +@pytest.fixture(autouse=True) +def close_figures(): + yield + plt.close("all") + + +def test_products_are_found_by_name(run): + assert run["en_tot"].dims == ("t",) + assert run["em_fields/E"].dims[:2] == ("t", "component") + assert run["kinetic_ions/e1_v1_density/f_binned"].dims == ("t", "e1", "v1") + assert run["kinetic_ions/view_0/n_sph"].dims == ("t", "e1", "e2", "e3") + assert run["kinetic_ions"].dims == ("t", "marker", "attribute") + with pytest.raises(KeyError, match="available products"): + run["t"] + + +def test_every_array_carries_its_run(run): + for array in (run.scalars.en_tot, run.fields.em_fields.E, run["kinetic_ions"]): + assert array.attrs["run"] == run.label + assert array.attrs["run_name"] == "sim_1" + assert run.scalars.en_tot.isel(t=slice(1, None)).attrs["run_name"] == "sim_1" + + +def test_timeseries_by_name_with_growth_fit(run): + result = run.plot.timeseries("en_phi", fit=True) + assert result.fit_results[0].rate == pytest.approx(RATE) + assert result.fig._suptitle.get_text() == run.label + + +def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): + first, second = make_run(str(tmp_path), "sim_1"), make_run(str(tmp_path), "sim_2") + result = first.plot.timeseries(first.scalars.en_phi, second.scalars.en_phi) + labels = [text.get_text() for text in result.ax.get_legend().get_texts()] + assert labels == ["en phi (sim_1)", "en phi (sim_2)"] + + +def test_timeseries_into_given_axes_keeps_the_figure_layout(run): + fig, ax = plt.subplots() + fig.suptitle("mine") + run.plot.timeseries("en_tot", ax=ax, logy=False) + assert fig._suptitle.get_text() == "mine" + + +def test_scalar_overview_picks_the_total_energy(run): + result = run.plot.scalars() + assert result.data["relative_error"].sizes["t"] == NT - 1 + assert run.plot.scalars(conservation=None).data["relative_error"] is None + + +def test_slices_panels_and_viewer_take_keyword_views(run): + name = "kinetic_ions/e1_v1_density/f_binned" + assert run.plot.slice(name, x="e1", y="v1", isel={"t": -1}).ax.get_xlabel() == r"$\eta_1$" + assert len(run.plot.panels(name, x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + viewer = run.plot.viewer("em_fields/E", x="e1", y="e2", isel={"component": 0}) + viewer.draw() + assert set(viewer.sliders) == {"t", "e3"} + + +def test_orbits_default_to_the_only_species(run): + assert run.plot.orbits().ax.name == "3d" + + +def test_report_is_written_below_post_processing(run): + paths = run.save_report() + assert all(path.startswith(str(run.path_pproc / "report")) for path in paths) + assert {os.path.basename(path) for path in paths} >= {"scalars.csv", "scalars.png", "en_phi.png"} + + +def test_analysis_by_name(run): + assert run.analysis.growth_rate("en_phi", window=(0.0, None)).rate == pytest.approx(RATE) + assert run.analysis.growth_rate("en_phi", amplitude=True).rate == pytest.approx(RATE / 2) + np.testing.assert_allclose(run.analysis.relative_error("en_tot"), 0.0) + np.testing.assert_allclose(run.analysis.drift("en_phi").isel(t=0), 0.0) + + +def test_dispersion_rejects_fields_in_seconds(run): + physical = run.with_time_units("physical") + physical._sim.model = type("Model", (), {"units": type("Units", (), {"t": 2.0})()})() + with pytest.raises(ValueError, match="normalized"): + physical.analysis.dispersion(physical.fields.em_fields.E) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 9e210e4c9..a8856aee1 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1656,6 +1656,9 @@ def from_output(cls, path_out: str) -> "Simulation": The configuration is read from the ``parameters.py`` copied there by :meth:`run`, or from ``config.json`` when the simulation was not created from a parameter file. + ``config.json`` holds the options objects and the arguments of the model (and thus its + units), but not configuration applied to the model after construction, such as + markers, backgrounds, perturbations and propagator options. Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. """ path_out = os.path.abspath(path_out) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 613ba7f2a..44fc780ea 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -4,8 +4,8 @@ import pytest -from struphy import EnvironmentOptions, Run, Simulation, open_run -from struphy.models import Maxwell +from struphy import BaseUnits, EnvironmentOptions, Run, Simulation, open_run +from struphy.models import Maxwell, VlasovAmpereOneSpecies def make_sim(tmp_path, **kwargs): @@ -32,7 +32,8 @@ def test_output_is_the_run_of_the_current_output_folder(tmp_path): def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): - sim = make_sim(tmp_path) + model = VlasovAmpereOneSpecies(base_units=BaseUnits(x=2.0, B=3.0, n=4.0), mass_number=4.0, with_B0=False) + sim = Simulation(model=model, env=EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_1")) os.makedirs(os.path.join(sim.env.path_out, "data")) sim._save_config() @@ -40,7 +41,9 @@ def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): os.rename(sim.env.path_out, moved) restored = open_run(moved).sim - assert restored.to_dict()["model"] == sim.to_dict()["model"] + assert restored.model.to_dict() == model.to_dict() + assert restored.model.params["mass_number"] == 4.0 + assert float(restored.model.units.t) == float(model.units.t) assert restored.domain == sim.domain assert restored.env.path_out == str(moved) assert restored.derham is None diff --git a/tutorials/dev_tutorial_feec_bcs.ipynb b/tutorials/dev_tutorial_feec_bcs.ipynb index 701000673..59ebe960c 100644 --- a/tutorials/dev_tutorial_feec_bcs.ipynb +++ b/tutorials/dev_tutorial_feec_bcs.ipynb @@ -344,7 +344,7 @@ "id": "24", "metadata": {}, "source": [ - "Post porcessing and loading the plotting data in `verbose` mode yields:" + "Post-processing the output of the run:" ] }, { @@ -354,8 +354,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.pproc()\n", - "sim.load_plotting_data()" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()" ] }, { @@ -373,7 +373,7 @@ "metadata": {}, "outputs": [], "source": [ - "e1h = sim.plotting_data.grids_log[0]\n", + "e1h = run.grids_log[0]\n", "print(e1h)" ] }, @@ -384,7 +384,7 @@ "metadata": {}, "outputs": [], "source": [ - "phi = sim.plotting_data.spline_values.em_fields.phi_log\n", + "phi = run.fields.em_fields.phi_log\n", "print(phi)" ] }, @@ -404,7 +404,7 @@ "outputs": [], "source": [ "plt.plot(e1, mfct_solution(e1), label=\"exact\")\n", - "plt.plot(e1h, phi.data[0.0][0][:, 0, 0], \"go\", label=\"numerical solution\")\n", + "plt.plot(e1h, phi.isel(t=0, e2=0, e3=0), \"go\", label=\"numerical solution\")\n", "plt.xlabel('e1')\n", "plt.legend()" ] diff --git a/tutorials/tutorial_beltrami_sph.ipynb b/tutorials/tutorial_beltrami_sph.ipynb index e66e02bf6..3dc68cf51 100644 --- a/tutorials/tutorial_beltrami_sph.ipynb +++ b/tutorials/tutorial_beltrami_sph.ipynb @@ -289,8 +289,7 @@ "Execute the simulation pipeline in order:\n", "\n", "1. `run()` to advance particles in time\n", - "2. `pproc()` to build post-processed outputs\n", - "3. `load_plotting_data()` to bring diagnostics into memory\n", + "2. `sim.output.process()` to build post-processed outputs, which are then loaded lazily from `run`\n", "\n", "Then plot marker trajectories to inspect how the Beltrami-driven flow evolves." ] @@ -312,17 +311,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20", - "metadata": {}, - "outputs": [], - "source": [ - "sim.load_plotting_data()" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()" ] }, { @@ -336,14 +326,14 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = sim.orbits.cold_fluid\n", + "orbits = run.orbits.cold_fluid.values\n", "\n", "coloring = np.select(\n", " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", - "Nt = sim.t_grid.size - 1\n", + "Nt = run.time.size - 1\n", "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", @@ -466,17 +456,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim_tess.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30", - "metadata": {}, - "outputs": [], - "source": [ - "sim_tess.load_plotting_data()" + "run_tess = sim_tess.output.with_time_units(\"normalized\")\n", + "run_tess.process()" ] }, { @@ -490,14 +471,14 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = sim_tess.orbits.cold_fluid\n", + "orbits = run_tess.orbits.cold_fluid.values\n", "\n", "coloring = np.select(\n", " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", - "Nt = sim_tess.t_grid.size - 1\n", + "Nt = run_tess.time.size - 1\n", "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", diff --git a/tutorials/tutorial_dam_break_sph.ipynb b/tutorials/tutorial_dam_break_sph.ipynb index 0cb09045b..f4602cc7e 100644 --- a/tutorials/tutorial_dam_break_sph.ipynb +++ b/tutorials/tutorial_dam_break_sph.ipynb @@ -264,7 +264,9 @@ "sim.run()\n", "print(\"Simulation complete.\")\n", "\n", - "sim.pproc()\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "\n", + "run.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -283,15 +285,15 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()\n", "\n", "# KDE density field: shape (Nt+1, pts_e1, pts_e2, 1)\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph\n", + "density = run.densities.euler_fluid.view_0.n_sph\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", + "n_sph = density.values\n", "\n", "# Marker orbits: shape (Nt_orb, n_markers, n_attrs)\n", "# attrs for vdim=2: [x, y, z, v1, v2, w, diag, id]\n", - "orbits = np.asarray(sim.orbits.euler_fluid)\n", + "orbits = np.asarray(run.orbits.euler_fluid.values)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_gas_expansion_sph.ipynb b/tutorials/tutorial_gas_expansion_sph.ipynb index 433a8df9c..2c8199454 100644 --- a/tutorials/tutorial_gas_expansion_sph.ipynb +++ b/tutorials/tutorial_gas_expansion_sph.ipynb @@ -345,7 +345,7 @@ "source": [ "### Step 9: Initialize, Run, and Load Results\n", "\n", - "Attach the Gaussian background and execute the standard workflow: `run()`, `pproc()`, `load_plotting_data()`.\n", + "Attach the Gaussian background and execute the standard workflow: `sim.run()`, then `run.process()` on the simulation output `run = sim.output`.\n", "\n", "Performance note: early time steps are typically slower because particles are highly concentrated; runtime usually improves as the cloud expands. Running from a console script (especially with MPI/GPU support) is faster than notebook execution." ] @@ -378,17 +378,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": {}, - "outputs": [], - "source": [ - "sim.load_plotting_data()" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()" ] }, { @@ -416,20 +407,21 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", + "density = run.densities.euler_fluid.view_0.n_sph\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", - "bc_x = sim.f.euler_fluid.e1_e2_density.grid_e1\n", - "bc_y = sim.f.euler_fluid.e1_e2_density.grid_e2\n", + "bc_x = run.distributions.euler_fluid.e1_e2_density.f_binned.e1.values\n", + "bc_y = run.distributions.euler_fluid.e1_e2_density.f_binned.e2.values\n", "\n", "# markers\n", - "orbits = sim.orbits.euler_fluid\n", + "orbits = run.orbits.euler_fluid.values\n", "positions = orbits[0, :, :3]\n", "weights = orbits[0, :, 6]\n", "\n", "# binning and sph eval\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph[0]\n", - "f_bin = sim.f.euler_fluid.e1_e2_density.f_binned[0]" + "n_sph = density.values[0]\n", + "f_bin = run.distributions.euler_fluid.e1_e2_density.f_binned.values[0]" ] }, { @@ -499,7 +491,7 @@ "outputs": [], "source": [ "dt = time_opts.dt\n", - "Nt = sim.t_grid.size - 1\n", + "Nt = run.time.size - 1\n", "\n", "positions = orbits[:, :, :3]\n", "\n", diff --git a/tutorials/tutorial_hagen_poiseuille_sph.ipynb b/tutorials/tutorial_hagen_poiseuille_sph.ipynb index ada024c24..104fe9e51 100644 --- a/tutorials/tutorial_hagen_poiseuille_sph.ipynb +++ b/tutorials/tutorial_hagen_poiseuille_sph.ipynb @@ -262,7 +262,9 @@ "sim.run()\n", "print(\"Simulation complete.\")\n", "\n", - "sim.pproc()\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "\n", + "run.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -281,10 +283,9 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()\n", "\n", - "e2_grid = sim.f.euler_fluid.e2_current_1.grid_e2 # logical y in [0, 1]\n", - "j1_binned = sim.f.euler_fluid.e2_current_1.f_binned # shape (Nt+1, n_bins)\n", + "e2_grid = run.distributions.euler_fluid.e2_current_1.f_binned.e2.values # logical y in [0, 1]\n", + "j1_binned = run.distributions.euler_fluid.e2_current_1.f_binned.values # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index 5ff5496cc..e087f0729 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -63,7 +63,6 @@ " grids,\n", " perturbations,\n", ")\n", - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", "from struphy.models import LinearMHD\n", "\n", "logger = logging.getLogger(\"struphy\")" @@ -221,7 +220,8 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing\n", - "sim.pproc()\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -244,13 +244,6 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", - "# Extract velocity and pressure time-series\n", - "u_of_t = sim.spline_values.mhd.velocity_log.data\n", - "p_of_t = sim.spline_values.mhd.pressure_log.data\n", - "\n", "# Dispersion relation parameters\n", "gamma = 5 / 3 # Adiabatic index\n", "disp_params = {\n", @@ -264,11 +257,9 @@ "\n", "# 1. Shear Alfvén wave analysis from velocity\n", "print(\"\\n=== Shear Alfvén Wave Analysis ===\")\n", - "_1, _2, _3, coeffs_alfven = power_spectrum_2d(\n", - " u_of_t,\n", - " \"velocity_log\",\n", - " grids=sim.grids_log,\n", - " grids_mapped=sim.grids_phy,\n", + "_1, _2, _3, coeffs_alfven = run.analysis.dispersion(\n", + " \"mhd/velocity_log\",\n", + " physical=True,\n", " component=0,\n", " slice_at=[0, 0, None],\n", " do_plot=True,\n", @@ -302,11 +293,9 @@ "source": [ "# 2. Magnetosonic waves analysis from pressure\n", "print(\"=== Slow and Fast Magnetosonic Wave Analysis ===\")\n", - "_1, _2, _3, coeffs_sonic = power_spectrum_2d(\n", - " p_of_t,\n", - " \"pressure_log\",\n", - " grids=sim.grids_log,\n", - " grids_mapped=sim.grids_phy,\n", + "_1, _2, _3, coeffs_sonic = run.analysis.dispersion(\n", + " \"mhd/pressure_log\",\n", + " physical=True,\n", " component=0,\n", " slice_at=[0, 0, None],\n", " do_plot=True,\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index 85af3dc2b..421daf525 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -111,7 +111,6 @@ " grids,\n", " perturbations,\n", ")\n", - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", "from struphy.models import Maxwell\n", "\n", "logger = logging.getLogger(\"struphy\")" @@ -239,7 +238,8 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing\n", - "sim.pproc()\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -262,19 +262,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", - "# Extract electric field time-series\n", - "E_of_t = sim.spline_values.em_fields.e_field_log.data\n", - "\n", "# Compute power spectrum and fit dispersion relation\n", "print(\"\\n=== Light Wave Dispersion Analysis ===\")\n", - "_1, _2, _3, coeffs = power_spectrum_2d(\n", - " E_of_t,\n", - " \"e_field_log\",\n", - " grids=sim.grids_log,\n", - " grids_mapped=sim.grids_phy,\n", + "_1, _2, _3, coeffs = run.analysis.dispersion(\n", + " \"em_fields/e_field_log\",\n", + " physical=True,\n", " component=0,\n", " slice_at=[0, 0, None],\n", " do_plot=True,\n", @@ -572,7 +564,8 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing (with physical=True to extract physical fields)\n", - "sim.pproc(physical=True)\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process(physical=True)\n", "print(\"Post-processing complete.\")" ] }, @@ -593,14 +586,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", "# Extract time and field data\n", - "t_grid = sim.t_grid\n", - "grids_phy = sim.grids_phy\n", - "e_field_phy = sim.spline_values.em_fields.e_field_phy.data\n", - "b_field_phy = sim.spline_values.em_fields.b_field_phy.data\n", + "t_grid = run.time\n", + "grids_phy = run.grids_phy\n", + "e_field_phy = run.fields.em_fields.e_field_phy # dims (t, component, e1, e2, e3)\n", + "b_field_phy = run.fields.em_fields.b_field_phy\n", "\n", "# Extract coordinate arrays in the first (r-θ) plane\n", "X = grids_phy[0][:, :, 0] # Radial coordinate (Cartesian x for plotting)\n", @@ -686,9 +676,9 @@ "t_end = t_grid[-1]\n", "\n", "# Numerical fields (Cartesian components)\n", - "Ex_num = e_field_phy[t_end][0][:, :, 0]\n", - "Ey_num = e_field_phy[t_end][1][:, :, 0]\n", - "Bz_num = b_field_phy[t_end][2][:, :, 0]\n", + "Ex_num = e_field_phy.isel(t=-1, component=0, e3=0).values\n", + "Ey_num = e_field_phy.isel(t=-1, component=1, e3=0).values\n", + "Bz_num = b_field_phy.isel(t=-1, component=2, e3=0).values\n", "\n", "# Analytical fields\n", "Er_analytic = E_r_analytic(X, Y, grids_phy[0], m, t_end)\n", diff --git a/tutorials/tutorial_particle_tracing.ipynb b/tutorials/tutorial_particle_tracing.ipynb index a793f94b2..b35342174 100644 --- a/tutorials/tutorial_particle_tracing.ipynb +++ b/tutorials/tutorial_particle_tracing.ipynb @@ -321,7 +321,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.pproc()" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()" ] }, { @@ -331,27 +332,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim_2.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": {}, - "outputs": [], - "source": [ - "sim.load_plotting_data()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25", - "metadata": {}, - "outputs": [], - "source": [ - "sim_2.load_plotting_data()" + "run_2 = sim_2.output.with_time_units(\"normalized\")\n", + "run_2.process()" ] }, { @@ -365,8 +347,8 @@ "\n", "fig = plt.figure(figsize=(10, 6))\n", "\n", - "orbits = sim.orbits.kinetic_ions\n", - "orbits_uni = sim_2.orbits.kinetic_ions\n", + "orbits = run.orbits.kinetic_ions.values\n", + "orbits_uni = run_2.orbits.kinetic_ions.values\n", "\n", "plt.subplot(1, 2, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -430,12 +412,12 @@ "model_3.kinetic_ions.var.add_background(background)\n", "\n", "sim_3.run()\n", - "sim_3.pproc()\n", - "sim_3.load_plotting_data()\n", + "run_3 = sim_3.output.with_time_units(\"normalized\")\n", + "run_3.process()\n", "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = sim_3.orbits.kinetic_ions\n", + "orbits_standard = run_3.orbits.kinetic_ions.values\n", "\n", "plt.subplot(1, 3, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -507,12 +489,12 @@ "model_3.kinetic_ions.var.add_background(background)\n", "\n", "sim_3.run()\n", - "sim_3.pproc()\n", - "sim_3.load_plotting_data()\n", + "run_3 = sim_3.output.with_time_units(\"normalized\")\n", + "run_3.process()\n", "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = sim_3.orbits.kinetic_ions\n", + "orbits_standard = run_3.orbits.kinetic_ions.values\n", "\n", "plt.subplot(1, 3, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -654,17 +636,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "40", - "metadata": {}, - "outputs": [], - "source": [ - "sim.load_plotting_data()" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()" ] }, { @@ -672,7 +645,7 @@ "id": "41", "metadata": {}, "source": [ - "Under `sim.orbits[]`, Struphy stores orbit data in a 3D NumPy array:\n", + "Under `run.orbits.`, Struphy stores orbit data as an `xarray.DataArray` with dims `(t, marker, attribute)`; `.values` gives the 3D NumPy array:\n", "\n", "- axis 0: time step,\n", "- axis 1: particle index,\n", @@ -688,7 +661,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = sim.orbits.kinetic_ions\n", + "orbits = run.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]\n", @@ -848,17 +821,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim_withB.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "54", - "metadata": {}, - "outputs": [], - "source": [ - "sim_withB.load_plotting_data()" + "run_withB = sim_withB.output.with_time_units(\"normalized\")\n", + "run_withB.process()" ] }, { @@ -868,7 +832,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = sim_withB.orbits.kinetic_ions\n", + "orbits = run_withB.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -1235,17 +1199,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim_asdex.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "75", - "metadata": {}, - "outputs": [], - "source": [ - "sim_asdex.load_plotting_data()" + "run_asdex = sim_asdex.output.with_time_units(\"normalized\")\n", + "run_asdex.process()" ] }, { @@ -1255,7 +1210,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = sim_asdex.orbits.kinetic_ions\n", + "orbits = run_asdex.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -1441,17 +1396,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim_gc.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "85", - "metadata": {}, - "outputs": [], - "source": [ - "sim_gc.load_plotting_data()" + "run_gc = sim_gc.output.with_time_units(\"normalized\")\n", + "run_gc.process()" ] }, { @@ -1461,7 +1407,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = sim_gc.orbits.kinetic_ions\n", + "orbits = run_gc.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" diff --git a/tutorials/tutorial_poisson.ipynb b/tutorials/tutorial_poisson.ipynb index 506447def..e55844251 100644 --- a/tutorials/tutorial_poisson.ipynb +++ b/tutorials/tutorial_poisson.ipynb @@ -136,8 +136,8 @@ "\n", "# For a stationary Poisson solve, one step is enough\n", "sim.run(one_time_step=True)\n", - "sim.pproc()\n", - "sim.load_plotting_data()" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()" ] }, { @@ -148,10 +148,9 @@ "outputs": [], "source": [ "# Extract 1D line data and compare to analytic solution\n", - "x = sim.grids_phy[0][:, 0, 0]\n", + "x = run.grids_phy[0][:, 0, 0]\n", "\n", - "t_last = max(sim.spline_values.em_fields.phi_log.data.keys())\n", - "phi_num = sim.spline_values.em_fields.phi_log.data[t_last][0][:, 0, 0]\n", + "phi_num = run.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0).values\n", "phi_ref = phi_exact(x, 0.0, 0.0)\n", "\n", "err = phi_num - phi_ref\n", @@ -282,8 +281,8 @@ " )\n", "\n", "sim2.run(one_time_step=True)\n", - "sim2.pproc()\n", - "sim2.load_plotting_data()" + "run2 = sim2.output.with_time_units(\"normalized\")\n", + "run2.process()" ] }, { @@ -294,11 +293,10 @@ "outputs": [], "source": [ "# 2D diagnostics and plots\n", - "t2_last = max(sim2.spline_values.em_fields.phi_log.data.keys())\n", - "X = sim2.grids_phy[0][:, :, 0]\n", - "Y = sim2.grids_phy[1][:, :, 0]\n", + "X = run2.grids_phy[0][:, :, 0]\n", + "Y = run2.grids_phy[1][:, :, 0]\n", "\n", - "phi2_num = sim2.spline_values.em_fields.phi_log.data[t2_last][0][:, :, 0]\n", + "phi2_num = run2.fields.em_fields.phi_log.isel(t=-1, e3=0).values\n", "phi2_ref = phi2_exact(X, Y, 0.0)\n", "err2 = phi2_num - phi2_ref\n", "err2_max = np.max(np.abs(err2))\n", @@ -418,8 +416,8 @@ " )\n", "\n", "sim3.run(one_time_step=True)\n", - "sim3.pproc()\n", - "sim3.load_plotting_data()" + "run3 = sim3.output.with_time_units(\"normalized\")\n", + "run3.process()" ] }, { @@ -430,11 +428,10 @@ "outputs": [], "source": [ "# Annulus diagnostics and plots in physical coordinates only\n", - "t3_last = max(sim3.spline_values.em_fields.phi_log.data.keys())\n", - "X3 = sim3.grids_phy[0][:, :, 0]\n", - "Y3 = sim3.grids_phy[1][:, :, 0]\n", + "X3 = run3.grids_phy[0][:, :, 0]\n", + "Y3 = run3.grids_phy[1][:, :, 0]\n", "\n", - "phi3_num = sim3.spline_values.em_fields.phi_log.data[t3_last][0][:, :, 0]\n", + "phi3_num = run3.fields.em_fields.phi_log.isel(t=-1, e3=0).values\n", "phi3_ref = phi3_exact(X3, Y3, 0.0)\n", "err3 = phi3_num - phi3_ref\n", "err3_max = np.max(np.abs(err3))\n", @@ -583,8 +580,8 @@ "\n", "# Run the full time-dependent simulation\n", "sim4.run()\n", - "sim4.pproc()\n", - "sim4.load_plotting_data()" + "run4 = sim4.output.with_time_units(\"normalized\")\n", + "run4.process()" ] }, { @@ -595,17 +592,17 @@ "outputs": [], "source": [ "# Extract and visualize time-dependent results\n", - "x4 = sim4.grids_phy[0][:, 0, 0]\n", - "phi4_log = sim4.spline_values.em_fields.phi_log.data\n", - "source4_log = sim4.spline_values.em_fields.source_log.data\n", - "t_times = sorted(phi4_log.keys())\n", + "x4 = run4.grids_phy[0][:, 0, 0]\n", + "phi4 = run4.fields.em_fields.phi_log.isel(e2=0, e3=0) # dims (t, e1)\n", + "source4 = run4.fields.em_fields.source_log.isel(e2=0, e3=0)\n", + "t_times = phi4.t.values\n", "\n", "print(f\"Solution saved at {len(t_times)} time points\")\n", "\n", "# Compute maximum error over all time steps\n", "err_max_global = 0.0\n", "for t in t_times:\n", - " phi_h = phi4_log[t][0][:, 0, 0] # Numerical solution\n", + " phi_h = phi4.sel(t=t).values # Numerical solution\n", " phi_e = phi4_exact(x4, t) # Exact solution\n", " err_local = np.max(np.abs(phi_h - phi_e))\n", " if err_local > err_max_global:\n", @@ -628,7 +625,7 @@ "for idx, t in enumerate(select_times):\n", " ax = axs_snaps[idx]\n", " \n", - " phi_num = phi4_log[t][0][:, 0, 0]\n", + " phi_num = phi4.sel(t=t).values\n", " phi_ref = phi4_exact(x4, t)\n", " \n", " ax.plot(x4, phi_ref, \"k--\", linewidth=2, label=\"Exact\")\n", @@ -654,7 +651,7 @@ "errors = []\n", "times_array = np.array(t_times)\n", "for t in t_times:\n", - " phi_h = phi4_log[t][0][:, 0, 0]\n", + " phi_h = phi4.sel(t=t).values\n", " phi_e = phi4_exact(x4, t)\n", " err = np.max(np.abs(phi_h - phi_e))\n", " errors.append(err)\n", @@ -667,9 +664,9 @@ "\n", "# Plot source and solution at a single point in space (center)\n", "center_idx = len(x4) // 2\n", - "phi_center = [phi4_log[t][0][center_idx, 0, 0] for t in t_times]\n", + "phi_center = phi4.isel(e1=center_idx).values\n", "phi_exact_center = [phi4_exact(x4[center_idx], t) for t in t_times]\n", - "source_center = [source4_log[t][0][center_idx, 0, 0] for t in t_times]\n", + "source_center = source4.isel(e1=center_idx).values\n", "source_exact_center = [rho4_exact(x4[center_idx], t) for t in t_times]\n", "\n", "ax2.plot(times_array, phi_exact_center, \"k--\", linewidth=2.5, label=\"$\\\\phi_{\\\\mathrm{exact}}$ at center\")\n", diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 2d5c2d367..f95da082d 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -38,16 +38,6 @@ " maxwellians,\n", " perturbations,\n", ")\n", - "from struphy.diagnostics.plotting import (\n", - " GrowthFit,\n", - " InteractiveSliceViewer,\n", - " View,\n", - " plot_marker_trajectories,\n", - " plot_panels,\n", - " plot_scalars,\n", - " plot_slice,\n", - " plot_timeseries,\n", - ")\n", "from struphy.models import VlasovAmpereOneSpecies" ] }, @@ -152,7 +142,7 @@ "id": "7", "metadata": {}, "source": [ - "Products are arranged into clear namespaces. VS Code and interactive shells can complete the available names after a run is opened: fields are grouped by field species, while distribution and density products are grouped by species and saved slice. Flat catalogs remain available for code that needs to iterate over arbitrary products." + "Products are arranged into clear namespaces. VS Code and interactive shells can complete the available names after a run is opened: fields are grouped by field species, while distribution and density products are grouped by species and saved slice. Every product can also be looked up by name, e.g. `run[\"electric_energy\"]` or `run[\"kinetic_ions/e1_v1_density/f_binned\"]`; flat catalogs remain available for code that needs to iterate over arbitrary products." ] }, { @@ -181,7 +171,9 @@ "source": [ "## Scalar overview and time series\n", "\n", - "`plot_scalars()` gives a quick overview of every recorded scalar. If `total_energy` is available, it can also be used for the conservation-error panel. Individual time series can be shown on linear or logarithmic axes, and `GrowthFit` restricts an exponential fit to a chosen time interval. Plot functions return an already-rendered `PlotResult`; calling `.save()` never draws a second figure." + "All standard plots are methods of `run.plot`, so no further imports are needed. They accept a product name or any array, and titles carry the run's numerical parameters.\n", + "\n", + "`run.plot.scalars()` gives a quick overview of every recorded scalar, with the relative error of the total energy below. `run.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`; calling `.save()` never draws a second figure." ] }, { @@ -191,11 +183,8 @@ "metadata": {}, "outputs": [], "source": [ - "scalar_plot, energy_error = plot_scalars(\n", - " run.scalars,\n", - " error_panel=\"total_energy\",\n", - " run_label=run.label,\n", - ")\n", + "scalar_plot = run.plot.scalars()\n", + "energy_error = scalar_plot.data[\"relative_error\"]\n", "scalar_plot.fig" ] }, @@ -206,18 +195,17 @@ "metadata": {}, "outputs": [], "source": [ - "electric_energy = run.scalars.electric_energy\n", - "energy_plot = plot_timeseries(\n", - " electric_energy,\n", - " logy=True,\n", - " fit=GrowthFit(\n", - " window=(0.0, 0.4 * run.sim.model.units.t),\n", - " amplitude_from_quadratic=True,\n", - " ),\n", + "t_fit = 0.4 * run.sim.model.units.t # in seconds, like every time coordinate of this run\n", + "energy_plot = run.plot.timeseries(\n", + " \"electric_energy\",\n", + " fit=(0.0, t_fit),\n", + " fit_amplitude=True,\n", " title=\"Electric-field energy\",\n", - " run_label=run.label,\n", ")\n", - "print(\"growth rate:\", energy_plot.fit_results[0].rate)" + "print(\"growth rate:\", energy_plot.fit_results[0].rate)\n", + "\n", + "# the same fit without a figure\n", + "print(\"growth rate:\", run.analysis.growth_rate(\"electric_energy\", window=(0.0, t_fit), amplitude=True).rate)" ] }, { @@ -227,7 +215,7 @@ "source": [ "## Two-dimensional data\n", "\n", - "Named xarray selection keeps plots readable. Use `.isel()` for an integer index and `.sel(..., method=\"nearest\")` for the point nearest a coordinate value. A `View` records the display axes, coordinate system, and reusable selections." + "Choose the displayed dimensions with `x` and `y`, and fix all others with `isel` (by index) or `select` (by nearest coordinate value). Arrays can also be sliced beforehand with xarray's `.isel()` and `.sel()`. `coords=\"physical\"` draws on the mapped coordinates instead of the logical ones." ] }, { @@ -237,13 +225,13 @@ "metadata": {}, "outputs": [], "source": [ - "final_distribution = phase_space.isel(t=-1)\n", - "plot_slice(\n", - " final_distribution,\n", - " view=View(x=\"e1\", y=\"v1\"),\n", + "run.plot.slice(\n", + " phase_space,\n", + " x=\"e1\",\n", + " y=\"v1\",\n", + " isel={\"t\": -1},\n", " equal_aspect=False,\n", " title=\"Final phase-space distribution\",\n", - " run_label=run.label,\n", ").fig" ] }, @@ -252,7 +240,7 @@ "id": "14", "metadata": {}, "source": [ - "For a compact view of the evolution, `plot_panels()` chooses evenly spaced snapshots. The same `View` can later drive an interactive viewer or animation. `shared_clim=True` makes panel colors directly comparable." + "For a compact view of the evolution, `run.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." ] }, { @@ -262,15 +250,13 @@ "metadata": {}, "outputs": [], "source": [ - "phase_view = View(x=\"e1\", y=\"v1\")\n", - "plot_panels(\n", + "run.plot.panels(\n", " phase_space,\n", - " view=phase_view,\n", + " x=\"e1\",\n", + " y=\"v1\",\n", " nrows=1,\n", " ncols=5,\n", - " shared_clim=True,\n", " title=\"Phase-space evolution\",\n", - " run_label=run.label,\n", ").fig" ] }, @@ -281,7 +267,7 @@ "source": [ "## Interactive plots\n", "\n", - "`InteractiveSliceViewer` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected." + "`run.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `run.plot.animation()` and `run.plot.frames()` sweep the same way." ] }, { @@ -291,11 +277,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_viewer = InteractiveSliceViewer(\n", - " phase_space,\n", - " view=phase_view,\n", - " run_label=run.label,\n", - ")\n", + "phase_viewer = run.plot.viewer(phase_space, x=\"e1\", y=\"v1\")\n", "phase_viewer.draw().fig" ] }, @@ -304,7 +286,7 @@ "id": "18", "metadata": {}, "source": [ - "Saved marker orbits are grouped by species. `plot_marker_trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." + "Saved marker orbits are grouped by species. `run.plot.orbits()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." ] }, { @@ -314,12 +296,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbit_plot = plot_marker_trajectories(\n", - " run.orbits.kinetic_ions,\n", - " max_markers=12,\n", - " show_paths=True,\n", - ")\n", - "orbit_plot.fig" + "run.plot.orbits(\"kinetic_ions\", max_markers=12, show_paths=True).fig" ] }, { @@ -329,7 +306,7 @@ "source": [ "## Save standard output\n", "\n", - "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `run.save_scalar_plots()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/scalars/`." + "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `run.save_report()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/report/`." ] }, { @@ -339,7 +316,7 @@ "metadata": {}, "outputs": [], "source": [ - "written = run.save_scalar_plots()\n", + "written = run.save_report()\n", "print(\"Wrote:\")\n", "for path in written:\n", " print(\" \", os.path.relpath(path, run.path_out))" diff --git a/tutorials/tutorial_pressureless_sph_shock.ipynb b/tutorials/tutorial_pressureless_sph_shock.ipynb index 5f9a3f8f5..5770cffa8 100644 --- a/tutorials/tutorial_pressureless_sph_shock.ipynb +++ b/tutorials/tutorial_pressureless_sph_shock.ipynb @@ -504,10 +504,9 @@ "outputs": [], "source": [ "print(\"Post-processing...\")\n", - "sim.pproc()\n", - "print(\"Loading plotting data...\")\n", - "sim.load_plotting_data()\n", - "print(\"Data loaded.\")" + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()\n", + "print(\"Post-processing complete.\")" ] }, { @@ -528,9 +527,9 @@ "outputs": [], "source": [ "# Extract binned outputs\n", - "rho_binned = sim.f.cold_fluid.e1_density.f_binned\n", - "current1_binned = sim.f.cold_fluid.e1_current_1.f_binned\n", - "t_grid = sim.t_grid\n", + "rho_binned = run.distributions.cold_fluid.e1_density.f_binned.values\n", + "current1_binned = run.distributions.cold_fluid.e1_current_1.f_binned.values\n", + "t_grid = run.time\n", "eta1_bins = np.linspace(0, 1, n_bins + 1)[:-1] # bin centers\n", "\n", "# Reconstruct velocity from binned current and density: u1 = j1 / rho\n", diff --git a/tutorials/tutorial_velocity_diffusion_sph.ipynb b/tutorials/tutorial_velocity_diffusion_sph.ipynb index 056b93324..f8fafee22 100644 --- a/tutorials/tutorial_velocity_diffusion_sph.ipynb +++ b/tutorials/tutorial_velocity_diffusion_sph.ipynb @@ -248,7 +248,9 @@ "sim.run()\n", "print(\"Simulation complete.\")\n", "\n", - "sim.pproc()\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "\n", + "run.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -267,13 +269,14 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()\n", "\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = sim.f.euler_fluid.e1_current_1.f_binned # shape (Nt+1, n_bins)\n", - "e1_binned = sim.f.euler_fluid.e1_current_1.grid_e1 # logical x in [0, 1]\n", - "n_binned = sim.f.euler_fluid.e1_density.f_binned # shape (Nt+1, n_bins)\n", + "density = run.densities.euler_fluid.view_0.n_sph\n", + "\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", + "n_sph = density.values # shape (Nt+1, plot_pts, 1, 1)\n", + "j1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.values # shape (Nt+1, n_bins)\n", + "e1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.e1.values # logical x in [0, 1]\n", + "n_binned = run.distributions.euler_fluid.e1_density.f_binned.values # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_viscous_euler_sph.ipynb b/tutorials/tutorial_viscous_euler_sph.ipynb index 6d070a35b..5f5f77f10 100644 --- a/tutorials/tutorial_viscous_euler_sph.ipynb +++ b/tutorials/tutorial_viscous_euler_sph.ipynb @@ -261,7 +261,8 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing\n", - "sim.pproc()\n", + "run = sim.output.with_time_units(\"normalized\")\n", + "run.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -282,12 +283,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", "# Extract particle positions and density\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph\n", + "density = run.densities.euler_fluid.view_0.n_sph\n", + "ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", + "n_sph = density.values\n", "\n", "# Physical coordinates\n", "x = ee1 * r1\n", @@ -688,7 +687,9 @@ "sim_damp.run()\n", "print(\"Simulation complete.\")\n", "\n", - "sim_damp.pproc()\n", + "run_damp = sim_damp.output.with_time_units(\"normalized\")\n", + "\n", + "run_damp.process()\n", "print(\"Post-processing complete.\")" ] }, @@ -711,12 +712,12 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "sim_damp.load_plotting_data()\n", + "density = run_damp.densities.euler_fluid.view_0.n_sph\n", "\n", - "ee1, ee2, ee3 = sim_damp.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim_damp.n_sph.euler_fluid.view_0.n_sph # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = sim_damp.f.euler_fluid.e1_current_1.f_binned # shape (Nt+1, n_bins)\n", - "e1_binned = sim_damp.f.euler_fluid.e1_current_1.grid_e1 # logical x in [0,1]\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", + "n_sph = density.values # shape (Nt+1, plot_pts, 1, 1)\n", + "j1_binned = run_damp.distributions.euler_fluid.e1_current_1.f_binned.values # shape (Nt+1, n_bins)\n", + "e1_binned = run_damp.distributions.euler_fluid.e1_current_1.f_binned.e1.values # logical x in [0,1]\n", "\n", "Nt = j1_binned.shape[0] - 1\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", From e53b9eedfb64a9432a28b97c1078a33de4c5138c Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 00:13:29 +0200 Subject: [PATCH 020/193] Remove extra .fig --- src/struphy/diagnostics/plotting.py | 35 ++++++++++++++++++- .../diagnostics/tests/test_plotting.py | 16 +++++++++ tutorials/tutorial_post_processing.ipynb | 12 +++---- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index caf816237..16e377f61 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -73,13 +73,18 @@ class View: @dataclass class PlotResult: - """Already-rendered Matplotlib objects; saving never redraws them.""" + """Already-rendered Matplotlib objects; saving never redraws them. + + As the last expression of a notebook cell it displays its figure once; there is no need + to write ``.fig``. + """ fig: object ax: object artists: list = field(default_factory=list) fit_results: list[FitResult | None] = field(default_factory=list) data: dict = field(default_factory=dict) + _shown: bool = field(default=False, init=False, repr=False, compare=False) def save(self, path, *, close=False, **kwargs): kwargs.setdefault("bbox_inches", "tight") @@ -90,8 +95,33 @@ def save(self, path, *, close=False, **kwargs): def show(self): plt.show() + self._shown = True return self + def __repr__(self): + return f"{type(self).__name__}(fig={self.fig!r})" + + def _ipython_display_(self): + if not self._shown: + _display_figure(self.fig) + + +def _display_figure(fig): + """Display a figure as a notebook cell result, exactly once. + + The inline backend shows every open figure again at the end of the cell, so the displayed + figure is closed. Interactive backends (e.g. ipympl) already show the figure when it is + created, so nothing is displayed twice there either. + """ + import matplotlib + + if "inline" not in matplotlib.get_backend(): + return + from IPython.display import display + + display(fig) + plt.close(fig) + def _label(data): return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" @@ -331,6 +361,9 @@ def __init__(self, data: xr.DataArray, *, view=None, vmin=None, vmax=None, run_l def show(self): return self.draw().show() + def _ipython_display_(self): + (self.result or self.draw())._ipython_display_() + def draw(self): base = _select(self.data, self.view) x, y = self.view.x, self.view.y diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 4bea38ac4..a0b44776b 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -152,3 +152,19 @@ def test_scalar_overview_and_export(tmp_path): "en_e.png", "en_tot.png", "scalars.csv", "scalars.png" ] assert plt.get_fignums() == [result.fig.number] + + +@pytest.mark.parametrize("shown", [False, True]) +def test_notebook_display_shows_the_figure_once(monkeypatch, shown): + import IPython.display + + displayed = [] + monkeypatch.setattr(matplotlib, "get_backend", lambda: "module://matplotlib_inline.backend_inline") + monkeypatch.setattr(IPython.display, "display", displayed.append) + monkeypatch.setattr(plt, "show", lambda *args, **kwargs: None) + result = plot_timeseries(scalar_dataset().en_tot, logy=False) + if shown: + result.show() + result._ipython_display_() + assert displayed == ([] if shown else [result.fig]) + assert (result.fig.number in plt.get_fignums()) == shown, "the inline backend must not show it again" diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index f95da082d..380df6639 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -173,7 +173,7 @@ "\n", "All standard plots are methods of `run.plot`, so no further imports are needed. They accept a product name or any array, and titles carry the run's numerical parameters.\n", "\n", - "`run.plot.scalars()` gives a quick overview of every recorded scalar, with the relative error of the total energy below. `run.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`; calling `.save()` never draws a second figure." + "`run.plot.scalars()` gives a quick overview of every recorded scalar, with the relative error of the total energy below. `run.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] }, { @@ -185,7 +185,7 @@ "source": [ "scalar_plot = run.plot.scalars()\n", "energy_error = scalar_plot.data[\"relative_error\"]\n", - "scalar_plot.fig" + "scalar_plot" ] }, { @@ -232,7 +232,7 @@ " isel={\"t\": -1},\n", " equal_aspect=False,\n", " title=\"Final phase-space distribution\",\n", - ").fig" + ")" ] }, { @@ -257,7 +257,7 @@ " nrows=1,\n", " ncols=5,\n", " title=\"Phase-space evolution\",\n", - ").fig" + ")" ] }, { @@ -278,7 +278,7 @@ "outputs": [], "source": [ "phase_viewer = run.plot.viewer(phase_space, x=\"e1\", y=\"v1\")\n", - "phase_viewer.draw().fig" + "phase_viewer" ] }, { @@ -296,7 +296,7 @@ "metadata": {}, "outputs": [], "source": [ - "run.plot.orbits(\"kinetic_ions\", max_markers=12, show_paths=True).fig" + "run.plot.orbits(\"kinetic_ions\", max_markers=12, show_paths=True)" ] }, { From 2e9fcd5c6dc004817d29bc5d6ce8e226450c6e4d Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 08:43:04 +0200 Subject: [PATCH 021/193] Write config.json and then use it for postprocessing --- doc/sections/userguide.rst | 8 +- src/struphy/io/output_handling.py | 7 +- .../post_processing/orbits/orbits_tools.py | 17 ++- .../post_processing/post_processing_tools.py | 105 +++--------------- src/struphy/simulation/base.py | 4 +- src/struphy/simulation/sim.py | 64 +++++++---- src/struphy/simulation/tests/test_output.py | 32 +++--- 7 files changed, 98 insertions(+), 139 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 4f9595d22..d1db359ef 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -516,10 +516,10 @@ Arrays are read from disk only when accessed. In a separate process, for example a plotting script on a laptop after a cluster run, open the output folder instead. Nothing is allocated and no MPI is needed; -``run.sim`` is restored from the ``parameters.py`` stored in the folder. A simulation -that was not created from a parameter file is restored from ``config.json``, which holds -the options and the model arguments (and thus the units), but not configuration applied -to the model afterwards, such as backgrounds or perturbations: +``run.sim`` is restored from the ``config.json`` that ``sim.run()`` writes to the folder; a +copied parameter file is never executed. ``config.json`` holds the options and the model +arguments (and thus the units), which is all that post-processing and plotting need, but +not configuration applied to the model afterwards, such as backgrounds or perturbations: .. code-block:: python diff --git a/src/struphy/io/output_handling.py b/src/struphy/io/output_handling.py index d906ad100..278457021 100644 --- a/src/struphy/io/output_handling.py +++ b/src/struphy/io/output_handling.py @@ -49,13 +49,16 @@ def __init__(self, path_out, file_name=None, comm=None): # dictionary with pairs (dataset key : object ID) self._dset_dict = {} - # get dataset keys if file already exists and set None object IDs + # get dataset keys if file already exists and set None object IDs; time series are + # chunked (see add_data), static datasets such as kinetic backgrounds are not if file_exists: dataset_keys = [] with h5py.File(self.file_path, "a") as file: file.visit( - lambda key: dataset_keys.append(key) if isinstance(file[key], h5py.Dataset) else None, + lambda key: dataset_keys.append(key) + if isinstance(file[key], h5py.Dataset) and file[key].chunks is not None + else None, ) for key in dataset_keys: diff --git a/src/struphy/post_processing/orbits/orbits_tools.py b/src/struphy/post_processing/orbits/orbits_tools.py index 472257270..a37170715 100644 --- a/src/struphy/post_processing/orbits/orbits_tools.py +++ b/src/struphy/post_processing/orbits/orbits_tools.py @@ -6,14 +6,13 @@ import h5py import yaml -from struphy.io.setup import import_parameters_py from struphy.post_processing.orbits.orbits_kernels import calculate_guiding_center_from_6d from struphy.utils.progress import tqdm logger = logging.getLogger("struphy") -def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): +def post_process_orbit_guiding_center(domain, equil, path_kinetics_species, species): """ Computes the Cartesian guiding center from saved full-orbit marker orbits (Particles6D) and writes them to a .npy files and to .txt files. @@ -37,8 +36,11 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): Parameters ---------- - path_in : str - Absolute path of simulation output folder. + domain : Domain + Domain of the simulation, for the markers' logical coordinates. + + equil : FluidEquilibriumWithB + Equilibrium of the simulation, for the magnetic field at the markers. path_kinetics_species : str Absolute path of where to store the .txt files. Will be saved in path_kinetics_species/guiding_center. @@ -47,12 +49,7 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): Name of the species for which the post processing should be performed. """ - # import parameters - params_in = import_parameters_py(os.path.join(path_in, "parameters.py")) - - # create domain for calculating markers' physical coordinates - domain = params_in.domain - equil = params_in.equil + assert equil is not None, "Guiding centers need an equilibrium with a magnetic field." # path for orbit data path_orbits = os.path.join(path_kinetics_species, "orbits") diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index afa89d85c..715eaaa09 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -16,7 +16,6 @@ from pyevtk.hl import gridToVTK from struphy.feec.psydac_derham import Derham, SplineFunction -from struphy.kinetic_background.base import KineticBackground from struphy.models.species import ParticleSpecies from struphy.models.variables import PICVariable, SPHVariable from struphy.pic.base import Particles @@ -38,7 +37,7 @@ def source_fingerprint(path_out: str) -> str: """Fingerprint the raw run files that determine post-processing products.""" digest = hashlib.sha256() - for name in ("config.json", "parameters.py", "meta.yml", "data/data_proc0.hdf5"): + for name in ("config.json", "meta.yml", "data/data_proc0.hdf5"): path = os.path.join(path_out, name) if not os.path.exists(path): continue @@ -117,6 +116,7 @@ def __init__(self, sim: "Simulation", parallel_pproc: bool = False): # struphy objects needed for post-processing self.domain = sim.domain + self.equil = sim.equil self.model = sim.model if self.parallel_pproc: @@ -424,7 +424,7 @@ def process_particles( if guiding_center: assert self.kinetic_kinds[n] == "Particles6D" - orbits_tools.post_process_orbit_guiding_center(self.path_out, path_kinetics_species, species) + orbits_tools.post_process_orbit_guiding_center(self.domain, self.equil, path_kinetics_species, species) if classify: orbits_tools.post_process_orbit_classification(path_kinetics_species, species) @@ -945,12 +945,11 @@ def _post_process_f( step : int, optional Time-step stride to process (default 1). compute_bckgr : bool, optional - If True, compute and add background contribution to the saved binned data. + If True, add the background stored by the simulation to the binned delta f. """ print(f"{self.rank} starting post-processing of distribution functions for {path_kinetic_species} ...") species = path_kinetic_species.split("/")[-1] - species_obj: ParticleSpecies = self.model.particle_species[species] # directory for .npy files path_distr = os.path.join(path_kinetic_species, "distribution_function") @@ -1051,90 +1050,18 @@ def _post_process_f( xp.save(os.path.join(path_slice, "delta_f_binned.npy"), data_df) if compute_bckgr: - # bckgr_params = params["kinetic"][species]["background"] - - # f_bckgr = None - # for fi, maxw_params in bckgr_params.items(): - # if fi[-2] == "_": - # fi_type = fi[:-2] - # else: - # fi_type = fi - - # if f_bckgr is None: - # f_bckgr = getattr(maxwellians, fi_type)( - # maxw_params=maxw_params, - # ) - # else: - # f_bckgr = f_bckgr + getattr(maxwellians, fi_type)( - # maxw_params=maxw_params, - # ) - - for _, var in species_obj.variables.items(): - assert isinstance(var, PICVariable | SPHVariable) - f_bckgr: KineticBackground = var.backgrounds - break - if f_bckgr is None: - raise ValueError( - f"The background of {species} is needed to post-process its delta-f distribution " - "function, but it is not configured. A simulation restored from config.json only " - "knows the model arguments; run from a parameter file to keep the background." - ) - - # load all grids of the variables of f - grid_tot = [] - factor = 1.0 - - # eta-grid - for comp in range(1, 4): - current_slice = "e" + str(comp) - filename = os.path.join( - path_slice, - "grid_" + current_slice + ".npy", - ) - - # check if file exists and is in slice_name - if os.path.exists(filename) and current_slice in slice_splits: - grid_tot += [xp.load(filename)] - - # otherwise evaluate at zero - else: - grid_tot += [xp.zeros(1)] - - # v-grid - for comp in range(1, f_bckgr.vdim + 1): - current_slice = "v" + str(comp) - filename = os.path.join( - path_slice, - "grid_" + current_slice + ".npy", - ) - - # check if file exists and is in slice_name - if os.path.exists(filename) and current_slice in slice_splits: - grid_tot += [xp.load(filename)] - - # otherwise evaluate at zero - else: - grid_tot += [xp.zeros(1)] - # correct integrating out in v-direction, TODO: check for 5D Maxwellians - factor *= xp.sqrt(2 * xp.pi) - - grid_eval = xp.meshgrid(*grid_tot, indexing="ij") - - data_bckgr = f_bckgr(*grid_eval).squeeze() - - # correct integrating out in v-direction - data_bckgr *= factor - - # Now all data is just the data for delta_f - data_delta_f = data_df - - # save distribution function - xp.save(os.path.join(path_slice, "delta_f_binned.npy"), data_delta_f) - # add extra axis for data_bckgr since data_delta_f has axis for time series - xp.save( - os.path.join(path_slice, "f_binned.npy"), - data_delta_f + data_bckgr[tuple([None])], - ) + # the background of a delta-f species is stored by the simulation on the bin centers + key_background = f"kinetic/{species}/f_background/{slice_name}" + with h5py.File(os.path.join(self.path_out, "data", "data_proc0.hdf5"), "r") as file: + if key_background not in file: + raise ValueError( + f"{key_background} is missing from the raw output; outputs of older versions " + "do not store the background of delta-f species." + ) + data_bckgr = file[key_background][()] + + # add extra axis for data_bckgr since data_df has axis for time series + xp.save(os.path.join(path_slice, "f_binned.npy"), data_df + data_bckgr[None]) def _post_process_n_sph( self, diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index c22b3a38c..12003e7cb 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -59,7 +59,9 @@ def export(self, file_path: str): if file_path.endswith(".yaml") or file_path.endswith(".yml"): dict_to_yaml(dct, file_path) elif file_path.endswith(".json"): + from struphy.simulation.sim import CuPyJSONEncoder + with open(file_path, "w") as f: - json.dump(dct, f, indent=4) + json.dump(dct, f, indent=4, cls=CuPyJSONEncoder) else: raise ValueError("Unsupported file format. Use .yaml, .yml or .json.") diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index a8856aee1..6b5f47201 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,7 +1,6 @@ # third party imports import dataclasses import glob -import hashlib import json import logging import os @@ -53,7 +52,6 @@ ) from struphy.geometry.base import Domain from struphy.io.output_handling import DataContainer -from struphy.io.setup import import_parameters_py from struphy.models import Maxwell from struphy.models.base import StruphyModel from struphy.models.species import ( @@ -1098,10 +1096,12 @@ def _remove_existing_output_files(self): logger.info("Removed existing file " + file) def _save_config(self): - """Save the parameter file (or, if there is none, the configuration as JSON) to the output folder.""" + """Save the configuration as ``config.json`` to the output folder, which is what + :meth:`from_output` reads. A parameter file is copied alongside for reference.""" if self.rank != 0: return + self.export(os.path.join(self.env.path_out, "config.json")) if self.params_path is not None: try: shutil.copy2( @@ -1110,8 +1110,6 @@ def _save_config(self): ) except shutil.SameFileError: pass - else: - self.export(os.path.join(self.env.path_out, "config.json")) def _create_clone_config(self) -> CloneConfig | None: """Setup domain cloning communicators, None if there is only one clone (or no MPI). @@ -1316,6 +1314,27 @@ def _allocate_propagators(self): logger.debug(f"\nAllocated propagator '{prop.__class__.__name__}'.") @profile + @staticmethod + def _binned_background(background, bin_plot) -> xp.ndarray: + """Evaluate a kinetic background on the bin centers of ``bin_plot``. + + Directions that are not binned are evaluated at zero; velocity directions that are not + binned are integrated out like the binned data (exact for Maxwellians). + """ + centers = { + dim: edges[:-1] + (edges[1] - edges[0]) / 2 for dim, edges in zip(bin_plot.slice.split("_"), bin_plot.bin_edges) + } + grids = [centers.get(dim, xp.zeros(1)) for dim in ("e1", "e2", "e3")] + factor = 1.0 + for component in range(1, background.vdim + 1): + dim = f"v{component}" + if dim in centers: + grids.append(centers[dim]) + else: + grids.append(xp.zeros(1)) + factor *= xp.sqrt(2 * xp.pi) + return background(*xp.meshgrid(*grids, indexing="ij")).squeeze() * factor + def _initialize_hdf5_datasets(self, data: DataContainer, size: int): """ Create datasets in hdf5 files according to model unknowns and diagnostics data. @@ -1442,6 +1461,17 @@ def _initialize_hdf5_datasets(self, data: DataContainer, size: int): be[:-1] + (be[1] - be[0]) / 2 ) + # the static background of a delta-f species, so that post-processing can + # reconstruct the full f without the simulation's configuration + if var.space == "DeltaFParticles6D": + key_background = os.path.join(key_spec, "f_background", slice) + if key_background in file: + del file[key_background] + file.create_dataset( + key_background, + data=DataContainer._as_numpy_array(self._binned_background(var.backgrounds, bin_plot)), + ) + for i, kd_plot in enumerate(species.saving_params.kernel_density_plots): key_n = os.path.join(key_spec, "n_sph", f"view_{i}") @@ -1654,25 +1684,21 @@ def convert_lists_to_tuples(obj): def from_output(cls, path_out: str) -> "Simulation": """Restore the simulation that wrote the output folder ``path_out``. - The configuration is read from the ``parameters.py`` copied there by :meth:`run`, or - from ``config.json`` when the simulation was not created from a parameter file. - ``config.json`` holds the options objects and the arguments of the model (and thus its - units), but not configuration applied to the model after construction, such as + The configuration is read from the ``config.json`` written by :meth:`run`; a copied + parameter file is never executed. ``config.json`` holds the options objects and the + arguments of the model (and thus its units), which is all that post-processing and + plotting need, but not configuration applied to the model after construction, such as markers, backgrounds, perturbations and propagator options. Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. """ path_out = os.path.abspath(path_out) - params_path = os.path.join(path_out, "parameters.py") config_path = os.path.join(path_out, "config.json") - if os.path.exists(params_path): - module_name = "struphy_run_" + hashlib.sha1(path_out.encode()).hexdigest()[:12] - sim = getattr(import_parameters_py(params_path, name=module_name), "sim", None) - if not isinstance(sim, Simulation): - raise ValueError(f"{params_path} does not define a Simulation named 'sim'") - elif os.path.exists(config_path): - sim = cls.from_file(config_path) - else: - raise FileNotFoundError(f"Neither {params_path} nor {config_path} exists; is {path_out} a Struphy output folder?") + if not os.path.exists(config_path): + raise FileNotFoundError( + f"{config_path} does not exist; is {path_out} a Struphy output folder? Outputs of older " + "versions can get one with sim.export(os.path.join(path_out, 'config.json')) from their parameter file." + ) + sim = cls.from_file(config_path) sim.env = dataclasses.replace( sim.env, out_folders=os.path.dirname(path_out), sim_folder=os.path.basename(path_out) ) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 44fc780ea..ed49da145 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -4,7 +4,7 @@ import pytest -from struphy import BaseUnits, EnvironmentOptions, Run, Simulation, open_run +from struphy import BaseUnits, EnvironmentOptions, Run, Simulation, Time, open_run from struphy.models import Maxwell, VlasovAmpereOneSpecies @@ -50,20 +50,24 @@ def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): assert sorted(os.listdir(tmp_path)) == ["moved"] -def test_from_output_prefers_the_parameter_file(tmp_path): - path_out = tmp_path / "sim_1" - os.makedirs(path_out / "data") - (path_out / "parameters.py").write_text( - "from struphy import EnvironmentOptions, Simulation, Time\n" - "from struphy.models import Maxwell\n" - "sim = Simulation(model=Maxwell(), env=EnvironmentOptions(sim_folder='elsewhere'), time_opts=Time(dt=0.123))\n" - ) - restored = Simulation.from_output(path_out) - assert restored.time_opts.dt == 0.123 - assert restored.env.path_out == str(path_out) - assert not os.path.exists(os.path.join(os.getcwd(), "elsewhere")) +def test_run_writes_config_json_and_copies_the_parameter_file(tmp_path): + params = tmp_path / "params_maxwell.py" + params.write_text("# a parameter file\n") + sim = make_sim(tmp_path, params_path=str(params)) + os.makedirs(sim.env.path_out) + sim._save_config() + assert sorted(os.listdir(sim.env.path_out)) == ["config.json", "parameters.py"] + + +def test_from_output_never_executes_the_parameter_file(tmp_path): + sim = make_sim(tmp_path, time_opts=Time(dt=0.123)) + os.makedirs(os.path.join(sim.env.path_out, "data")) + sim._save_config() + with open(os.path.join(sim.env.path_out, "parameters.py"), "w") as stream: + stream.write("raise RuntimeError('the parameter file was executed')\n") + assert Simulation.from_output(sim.env.path_out).time_opts.dt == 0.123 def test_from_output_requires_a_configuration(tmp_path): - with pytest.raises(FileNotFoundError, match="parameters.py"): + with pytest.raises(FileNotFoundError, match="config.json"): Simulation.from_output(tmp_path) From 04cab24322ab025e6630ddad4ead357cb8b68a71 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 10:28:25 +0200 Subject: [PATCH 022/193] Rename run to output --- .claude/skills/setup-simulation/SKILL.md | 24 +++---- doc/sections/quickstart.rst | 8 +-- doc/sections/userguide.rst | 58 ++++++++-------- .../cyclone/pproc_cyclone.py | 4 +- .../itg_cylindre/pproc_drift_kinetic.py | 4 +- .../diocotron_instability/pproc_diocotron.py | 4 +- src/struphy/__init__.py | 6 +- src/struphy/api/post_processing/__init__.py | 4 +- src/struphy/diagnostics/diagn_tools.py | 2 +- src/struphy/diagnostics/plotting.py | 4 +- .../diagnostics/tests/test_diagn_tools.py | 2 +- src/struphy/models/tests/utils_testing.py | 2 +- .../post_processing/{run.py => output.py} | 48 +++++++------- .../{run_accessors.py => output_accessors.py} | 52 +++++++-------- .../post_processing/post_processing_tools.py | 4 +- .../tests/{test_run.py => test_output.py} | 26 ++++---- ..._accessors.py => test_output_accessors.py} | 6 +- .../post_processing/tests/test_pproc.py | 4 +- src/struphy/simulation/base.py | 2 +- src/struphy/simulation/sim.py | 12 ++-- src/struphy/simulation/tests/test_output.py | 6 +- tutorials/tutorial_post_processing.ipynb | 66 +++++++++---------- 22 files changed, 174 insertions(+), 174 deletions(-) rename src/struphy/post_processing/{run.py => output.py} (93%) rename src/struphy/post_processing/{run_accessors.py => output_accessors.py} (87%) rename src/struphy/post_processing/tests/{test_run.py => test_output.py} (91%) rename src/struphy/post_processing/tests/{test_run_accessors.py => test_output_accessors.py} (94%) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 23cac9756..155042646 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -131,20 +131,20 @@ Key building blocks and where to look them up: ```python import params_ as params # importing does NOT re-run the sim (guarded above) -run = params.sim.output # or, from anywhere: struphy.open_run() -run.process(physical=True) # optional; products are otherwise processed with defaults on first access - -run.scalars. # xarray time series, no post-processing needed -run.fields.._log # dims (t, [component,] e1, e2, e3) -run.distributions...f_binned # dims (t, ) -run.orbits. # dims (t, marker, attribute) -run.sim.model.units # the Simulation, restored without allocating +out = params.sim.output # or, from anywhere: struphy.open_output() +out.process(physical=True) # optional; products are otherwise processed with defaults on first access + +out.scalars. # xarray time series, no post-processing needed +out.fields.._log # dims (t, [component,] e1, e2, e3) +out.distributions...f_binned # dims (t, ) +out.orbits. # dims (t, marker, attribute) +out.sim.model.units # the Simulation, restored without allocating ``` -Plots and analysis need no imports: `run.plot.scalars()`, `run.plot.timeseries("", fit=(t0, t1))`, -`run.plot.panels("//f_binned", x="e1", y="v1")`, `run.plot.viewer(...)`, -`run.plot.orbits("")`, `run.save_report()`, `run.analysis.growth_rate(...)`, -`run.analysis.dispersion(...)`. Names are looked up with `run[""]`. See +Plots and analysis need no imports: `out.plot.scalars()`, `out.plot.timeseries("", fit=(t0, t1))`, +`out.plot.panels("//f_binned", x="e1", y="v1")`, `out.plot.viewer(...)`, +`out.plot.orbits("")`, `out.save_report()`, `out.analysis.growth_rate(...)`, +`out.analysis.dispersion(...)`. Names are looked up with `out[""]`. See `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. ## Common pitfalls diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 5eb9648b5..9522de048 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -78,14 +78,14 @@ For periodic boundary conditions we will stabilize via ``options``. .. code-block:: python - run = sim.run(one_time_step=True) + out = sim.run(one_time_step=True) 7. Get the output. Fields are post-processed when first accessed and come as labeled :class:`xarray.DataArray` objects. .. code-block:: python - phi = run.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) + phi = out.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) 8. Compare to the exact solution, and save the figure. @@ -147,9 +147,9 @@ Full copy-paste script: grid = grids.TensorProductGrid(num_elements=(64, 1, 1)) sim = Simulation(model=model, domain=domain, grid=grid) - run = sim.run(one_time_step=True) + out = sim.run(one_time_step=True) - phi = run.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) + phi = out.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) x = phi.X.values phi_num = phi.values phi_exact = np.cos(k * x) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index d1db359ef..1e6817317 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -499,16 +499,16 @@ After initial conditions are set, launch the run: 11. Post-processing and visualization ------------------------------------- -The output of a simulation is a :class:`~struphy.Run`. ``sim.run()`` returns it, +The output of a simulation is a :class:`~struphy.Output`. ``sim.run()`` returns it, and it stays available as ``sim.output``: .. code-block:: python - run = sim.run() + out = sim.run() - run.scalars.total_energy # scalar time series, straight from the raw output - run.fields.em_fields.e_field_log # evaluated FEEC field (post-processed on first access) - run.sim # the Simulation that produced the output + out.scalars.total_energy # scalar time series, straight from the raw output + out.fields.em_fields.e_field_log # evaluated FEEC field (post-processed on first access) + out.sim # the Simulation that produced the output Every product is an :class:`xarray.DataArray` with named dimensions (``t``, ``component``, ``e1``, ``e2``, ``e3``, ``v1``, ...), coordinates and units. @@ -516,7 +516,7 @@ Arrays are read from disk only when accessed. In a separate process, for example a plotting script on a laptop after a cluster run, open the output folder instead. Nothing is allocated and no MPI is needed; -``run.sim`` is restored from the ``config.json`` that ``sim.run()`` writes to the folder; a +``out.sim`` is restored from the ``config.json`` that ``sim.run()`` writes to the folder; a copied parameter file is never executed. ``config.json`` holds the options and the model arguments (and thus the units), which is all that post-processing and plotting need, but not configuration applied to the model afterwards, such as backgrounds or perturbations: @@ -525,11 +525,11 @@ not configuration applied to the model afterwards, such as backgrounds or pertur import struphy - run = struphy.open_run("./runs/vm1s_scan_A/sim_1") - run.sim.domain, run.sim.model.units + out = struphy.open_output("./runs/vm1s_scan_A/sim_1") + out.sim.domain, out.sim.model.units -Choosing post-processing options: ``run.process()`` +Choosing post-processing options: ``out.process()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Scalars need no post-processing. Fields, binned distribution functions, SPH @@ -540,7 +540,7 @@ choose the options, call ``process`` first: .. code-block:: python - run.process( + out.process( step=1, # evaluate every N-th saved time step celldivide=1, # sub-divide each grid cell for smoother output physical=False, # also evaluate fields in physical coordinates (*_phy) @@ -558,32 +558,32 @@ serial processing runs on rank 0 while the other ranks wait, and ``parallel=True`` uses the allocated simulation on all ranks. -Standard plots and analysis: ``run.plot`` and ``run.analysis`` +Standard plots and analysis: ``out.plot`` and ``out.analysis`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The common diagnostics are methods of the run, so no further imports are needed. They -accept a product name, ``run[""]``, or any array (sliced, derived, or from another +accept a product name, ``out[""]``, or any array (sliced, derived, or from another run), and figures are titled with the run's numerical parameters: .. code-block:: python - run.plot.scalars() # overview + energy conservation error - run.plot.timeseries("en_phi", fit=(0.0, 40.0)) # exponential fit in a time window - run.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", isel={"t": -1}) - run.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4) - run.plot.viewer("em_fields/phi_phy", x="e1", y="e2", coords="physical").show() - run.plot.orbits("kinetic_ions") - run.save_report() # table + figures in post_processing/report/ + out.plot.scalars() # overview + energy conservation error + out.plot.timeseries("en_phi", fit=(0.0, 40.0)) # exponential fit in a time window + out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", isel={"t": -1}) + out.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4) + out.plot.viewer("em_fields/phi_phy", x="e1", y="e2", coords="physical").show() + out.plot.orbits("kinetic_ions") + out.save_report() # table + figures in post_processing/report/ - run.analysis.growth_rate("en_phi", window=(0.0, 40.0)).rate - run.analysis.dispersion("em_fields/e_field_log", slice_at=(0, 0, None), fit_branches=1) + out.analysis.growth_rate("en_phi", window=(0.0, 40.0)).rate + out.analysis.dispersion("em_fields/e_field_log", slice_at=(0, 0, None), fit_branches=1) Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of several runs are labeled by run: .. code-block:: python - run_a.plot.timeseries(run_a["en_phi"], run_b["en_phi"], fit=(0.0, 40.0)) + out_a.plot.timeseries(out_a["en_phi"], out_b["en_phi"], fit=(0.0, 40.0)) The sections below access the arrays directly for custom Matplotlib plots. @@ -599,7 +599,7 @@ components) or ``_phy`` (physical components, with import matplotlib.pyplot as plt - e_field = run.fields.em_fields.e_field_log # dims (t, component, e1, e2, e3) + e_field = out.fields.em_fields.e_field_log # dims (t, component, e1, e2, e3) snapshot = e_field.isel(t=-1, component=0, e2=0, e3=0) plt.figure() @@ -619,8 +619,8 @@ Binned particle data is grouped by species and the slice defined in .. code-block:: python - f = run.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) - run.plot.slice(f.isel(t=-1), x="e1", y="v1").show() + f = out.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) + out.plot.slice(f.isel(t=-1), x="e1", y="v1").show() Plotting particle orbits @@ -628,13 +628,13 @@ Plotting particle orbits If ``n_markers > 0`` was set in :class:`~struphy.particles.parameters.SavingParameters`, individual marker -trajectories are available under ``run.orbits``: +trajectories are available under ``out.orbits``: .. code-block:: python import matplotlib.pyplot as plt - orbits = run.orbits.kinetic_ions # dims (t, marker, attribute) + orbits = out.orbits.kinetic_ions # dims (t, marker, attribute) marker = orbits.isel(marker=0) plt.figure() @@ -648,7 +648,7 @@ trajectories are available under ``run.orbits``: VTK output for ParaView and PyVista ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you call ``run.process(create_vtk=True)``, Struphy writes structured-grid VTK +If you call ``out.process(create_vtk=True)``, Struphy writes structured-grid VTK files (``.vts``) inside the post-processing folder, grouped by species. Typical locations are: @@ -912,7 +912,7 @@ Gantt charts and flame graphs. Note that ``profiling_data.h5`` is a plain ``scope-profiler`` output file, so it is post-processed with ``scope-profiler`` itself rather than with -``run.process()`` — the two are independent post-processing paths. +``out.process()`` — the two are independent post-processing paths. Post-processing with the ``scope-profiler`` CLI diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index b77d8eff2..4a6264de2 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,7 +1,7 @@ import os import sys -from struphy import open_run +from struphy import open_output # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" @@ -19,7 +19,7 @@ def main(path_out): - run = open_run(path_out).process(physical=True) + run = open_output(path_out).process(physical=True) # growth rate of the electrostatic potential run.plot.timeseries( diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index edcbf20ce..9f1b60ebc 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,7 +1,7 @@ import os import sys -from struphy import open_run +from struphy import open_output # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" @@ -19,7 +19,7 @@ def main(path_out): - run = open_run(path_out).process(physical=True) + run = open_output(path_out).process(physical=True) # growth rate of the electrostatic potential run.plot.timeseries( diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index ec85fa21a..7efcb439f 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -7,7 +7,7 @@ import os import sys -from struphy import open_run +from struphy import open_output FIT_QUANTITY = "en_phi" FIT_WINDOW = (0.0, 42.0) @@ -23,7 +23,7 @@ def main(paths): - runs = [open_run(path).process(physical=True) for path in paths] + runs = [open_output(path).process(physical=True) for path in paths] run = runs[0] # growth rate of the electrostatic energy, one curve per run diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index 9ea4c320c..bbf73f1e2 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -167,7 +167,7 @@ def setup_logging(logging_level: int = logging.WARNING): WeightsParameters, ) from struphy.api.perturbations import perturbations -from struphy.api.post_processing import Run, open_run +from struphy.api.post_processing import Output, open_output from struphy.api.simulation import Simulation __all__ = [ @@ -191,7 +191,7 @@ def setup_logging(logging_level: int = logging.WARNING): "DerhamOptions", "FieldsBackground", "ButcherTableau", - "Run", - "open_run", + "Output", + "open_output", "Simulation", ] diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index 7cbab63b2..356559285 100644 --- a/src/struphy/api/post_processing/__init__.py +++ b/src/struphy/api/post_processing/__init__.py @@ -1,3 +1,3 @@ -from struphy.post_processing.run import Run, open_run +from struphy.post_processing.output import Output, open_output -__all__ = ["Run", "open_run"] +__all__ = ["Output", "open_output"] diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 12d157623..357291507 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -39,7 +39,7 @@ def power_spectrum_2d( Parameters ---------- field : xarray.DataArray - An evaluated FEEC field of a :class:`~struphy.Run`, with dims ``(t, [component,] e1, e2, e3)``, + An evaluated FEEC field of a :class:`~struphy.Output`, with dims ``(t, [component,] e1, e2, e3)``, e.g. ``run.fields.em_fields.e_field_log``. Its time coordinate must be uniform; use ``run.with_time_units("normalized")`` to compare with normalized dispersion relations. diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 16e377f61..f08e1eb9a 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -1,6 +1,6 @@ """Small, composable plotting functions for labeled Struphy output. -Users reach these through ``run.plot`` (see :class:`struphy.post_processing.run_accessors.RunPlots`); +Users reach these through ``run.plot`` (see :class:`struphy.post_processing.output_accessors.OutputPlots`); they remain importable for plotting arbitrary labeled arrays. """ @@ -134,7 +134,7 @@ def _items(data): def shared_run_label(data, default="") -> str: """The run description shared by all arrays (``attrs["run"]``), or ``default``. - Arrays loaded from a :class:`~struphy.Run` carry it; arrays from different runs share none. + Arrays loaded from a :class:`~struphy.Output` carry it; arrays from different runs share none. """ runs = {item.attrs.get("run") for item in _items(data)} if len(runs - {None, ""}) > 1: diff --git a/src/struphy/diagnostics/tests/test_diagn_tools.py b/src/struphy/diagnostics/tests/test_diagn_tools.py index 2a55f437e..d1146287b 100644 --- a/src/struphy/diagnostics/tests/test_diagn_tools.py +++ b/src/struphy/diagnostics/tests/test_diagn_tools.py @@ -10,7 +10,7 @@ def standing_waves(dt=0.05, tend=2 * LENGTH, nx=128): - """Standing waves of all resolved wavenumbers with phase speed SPEED along eta3, as a field of a Run. + """Standing waves of all resolved wavenumbers with phase speed SPEED along eta3, as a field of a Output. The time window holds whole periods of every wave, so the spectrum has no leakage. """ diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 5650aa951..f0053fead 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -99,7 +99,7 @@ def call_test(model: StruphyModel, test_profiling: bool = False): ) assert sim == spec.sim, "Simulation in generated script is not the same as the original simulation" - # Run the simulation from the generated script + # Output the simulation from the generated script # Export to json and import again with tempfile.NamedTemporaryFile(suffix=".json", mode="w+") as tmp: diff --git a/src/struphy/post_processing/run.py b/src/struphy/post_processing/output.py similarity index 93% rename from src/struphy/post_processing/run.py rename to src/struphy/post_processing/output.py index 2001bb71f..3d415d4c7 100644 --- a/src/struphy/post_processing/run.py +++ b/src/struphy/post_processing/output.py @@ -12,7 +12,7 @@ import xarray as xr from struphy.post_processing.arrays import data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits -from struphy.post_processing.run_accessors import RunAnalysis, RunPlots +from struphy.post_processing.output_accessors import OutputAnalysis, OutputPlots logger = logging.getLogger("struphy") @@ -86,11 +86,11 @@ def catalog(self): class FieldProducts(ProductNamespace): - """Fields grouped as ``run.fields..``.""" + """Fields grouped as ``out.fields..``.""" class DistributionProducts(ProductNamespace): - """Binned products grouped as ``run.distributions...``.""" + """Binned products grouped as ``out.distributions...``.""" class DensityProducts(ProductNamespace): @@ -101,11 +101,11 @@ class OrbitProducts(ProductNamespace): """Marker trajectories grouped by species.""" -class Run: +class Output: """The output of one Struphy simulation, loaded lazily from its output folder. Obtain it from :attr:`Simulation.output` (or the return value of :meth:`Simulation.run`) - or, in a separate process, from :func:`open_run`. Nothing is read at construction. + or, in a separate process, from :func:`open_output`. Nothing is read at construction. * :attr:`scalars` are read directly from the raw HDF5 output. * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` need @@ -114,7 +114,7 @@ class Run: * :attr:`sim` is the :class:`~struphy.Simulation` that produced the output: the live object for ``sim.output``, otherwise restored from disk without allocating anything. * :attr:`plot` and :attr:`analysis` draw and evaluate standard diagnostics, e.g. - ``run.plot.timeseries("en_phi", fit=(0, 40))``; ``run["en_phi"]`` looks up any product. + ``out.plot.timeseries("en_phi", fit=(0, 40))``; ``out["en_phi"]`` looks up any product. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. Parameters @@ -139,7 +139,7 @@ def __init__(self, path_out, *, sim=None, time_units: str = "physical"): def __repr__(self): return f"{type(self).__name__}({str(self.path_out)!r}, processed={self.is_processed})" - def with_time_units(self, time_units: str) -> "Run": + def with_time_units(self, time_units: str) -> "Output": """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" return type(self)(self.path_out, sim=self._sim, time_units=time_units) @@ -194,7 +194,7 @@ def process( create_vtk: bool = False, parallel: bool = False, force: bool = False, - ) -> "Run": + ) -> "Output": """Post-process the raw output; reuses existing products made with the same options. Call this on every MPI rank. Serial processing (the default) runs on rank 0 while @@ -221,8 +221,8 @@ def process( Returns ------- - Run - This run, so that ``run = open_run(path).process(physical=True)`` reads naturally. + Output + This run, so that ``run = open_output(path).process(physical=True)`` reads naturally. """ from struphy.post_processing.post_processing_tools import PostProcessor @@ -242,9 +242,9 @@ def _ensure_processed(self): if self.is_processed: return if self.sim.comm_size > 1: - raise RuntimeError(f"{self.path_out} has no post-processed data; call run.process() on all ranks first") + raise RuntimeError(f"{self.path_out} has no post-processed data; call out.process() on all ranks first") logger.warning("\nNo post-processed data in %s, processing with default options " - "(call run.process(...) to choose them)", self.path_out) + "(call out.process(...) to choose them)", self.path_out) self.process() def _product_mappings(self) -> dict[str, ProductMapping]: @@ -264,22 +264,22 @@ def _product_mappings(self) -> dict[str, ProductMapping]: @property def fields(self) -> FieldProducts: - """FEEC fields as ``run.fields..``.""" + """FEEC fields as ``out.fields..``.""" return FieldProducts(self.field_catalog) @property def distributions(self) -> DistributionProducts: - """Binned distribution functions as ``run.distributions...``.""" + """Binned distribution functions as ``out.distributions...``.""" return DistributionProducts(self.distribution_catalog) @property def densities(self) -> DensityProducts: - """SPH densities as ``run.densities...``.""" + """SPH densities as ``out.densities...``.""" return DensityProducts(self.density_catalog) @property def orbits(self) -> OrbitProducts: - """Marker trajectories as ``run.orbits.``.""" + """Marker trajectories as ``out.orbits.``.""" return OrbitProducts(self.orbit_catalog) @property @@ -299,14 +299,14 @@ def orbit_catalog(self) -> ProductMapping: return self._product_mappings()["orbits"] @property - def plot(self) -> RunPlots: - """Standard plots, e.g. ``run.plot.scalars()`` or ``run.plot.panels(name, x="e1", y="v1")``.""" - return RunPlots(self) + def plot(self) -> OutputPlots: + """Standard plots, e.g. ``out.plot.scalars()`` or ``out.plot.panels(name, x="e1", y="v1")``.""" + return OutputPlots(self) @property - def analysis(self) -> RunAnalysis: - """Quantitative diagnostics, e.g. ``run.analysis.growth_rate("en_phi", window=(0, 40))``.""" - return RunAnalysis(self) + def analysis(self) -> OutputAnalysis: + """Quantitative diagnostics, e.g. ``out.analysis.growth_rate("en_phi", window=(0, 40))``.""" + return OutputAnalysis(self) @property def time_scale(self) -> float: @@ -469,7 +469,7 @@ def _load_orbits(self, directory: Path): return wrap_orbits(values, self.time[:len(paths)], time_unit=self.time_unit) -def open_run(path_out, *, time_units: str = "physical") -> Run: +def open_output(path_out, *, time_units: str = "physical") -> Output: """Open the output folder of a finished simulation. Nothing is allocated and no MPI is needed; products are read on first access. @@ -484,4 +484,4 @@ def open_run(path_out, *, time_units: str = "physical") -> Run: path = Path(path_out) if not (path / "data").is_dir(): raise FileNotFoundError(f"{path.resolve()} is not a Struphy output folder (it has no data/ directory)") - return Run(path, time_units=time_units) + return Output(path, time_units=time_units) diff --git a/src/struphy/post_processing/run_accessors.py b/src/struphy/post_processing/output_accessors.py similarity index 87% rename from src/struphy/post_processing/run_accessors.py rename to src/struphy/post_processing/output_accessors.py index 62aab575a..794b1c8ab 100644 --- a/src/struphy/post_processing/run_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -1,7 +1,7 @@ -"""``run.plot`` and ``run.analysis``: plotting and analysis without extra imports. +"""``out.plot`` and ``out.analysis``: plotting and analysis without extra imports. Every method accepts a product name (``"en_phi"``, ``"em_fields/phi_log"``, -``"kinetic_ions/e1_v1_density/f_binned"``; see :meth:`Run.__getitem__`) or any labeled +``"kinetic_ions/e1_v1_density/f_binned"``; see :meth:`Output.__getitem__`) or any labeled array, including arrays derived from or belonging to another run. """ @@ -12,31 +12,31 @@ import xarray as xr if TYPE_CHECKING: - from struphy.post_processing.run import Run + from struphy.post_processing.output import Output Coordinates = Literal["logical", "physical"] Plane = Literal["XY", "XZ", "YZ", "RZ"] -class RunPlots: - """Standard plots of a run, as ``run.plot.(...)``. +class OutputPlots: + """Standard plots of a run, as ``out.plot.(...)``. Plots return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` and ``.save(path)``. Figures are titled with the run's numerical parameters; time series of different runs are labeled by run. """ - def __init__(self, run: "Run"): - self._run = run + def __init__(self, output: "Output"): + self._output = output def _array(self, data) -> xr.DataArray: - return self._run[data] if isinstance(data, str) else data + return self._output[data] if isinstance(data, str) else data - def _run_label(self, arrays) -> str: + def _label(self, arrays) -> str: from struphy.diagnostics.plotting import shared_run_label runs = {array.attrs.get("run") for array in arrays} - {None, ""} - return shared_run_label(arrays) if runs else self._run.label + return shared_run_label(arrays) if runs else self._output.label @staticmethod def _view(x, y, sweep, coords, plane, select, isel): @@ -64,11 +64,11 @@ def scalars(self, names=None, *, conservation: str | None = "auto", relative_to: """ from struphy.diagnostics.plotting import plot_scalars - scalars = self._run.scalars + scalars = self._output.scalars if conservation == "auto": conservation = next((name for name in ("en_tot", "total_energy") if name in scalars), None) return plot_scalars(scalars, names=names, relative_to=relative_to, error_panel=conservation, logy=logy, - run_label=self._run.label) + run_label=self._output.label) def timeseries(self, *data, logy: bool = True, fit: tuple[float | None, float | None] | bool | None = None, fit_amplitude: bool = False, title: str | None = None, ax=None): @@ -99,7 +99,7 @@ def timeseries(self, *data, logy: bool = True, fit: tuple[float | None, float | if fit is not None and fit is not False: window = (None, None) if fit is True else tuple(fit) growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) - return plot_timeseries(series, ax=ax, logy=logy, fit=growth, title=title, run_label=self._run_label(series)) + return plot_timeseries(series, ax=ax, logy=logy, fit=growth, title=title, run_label=self._label(series)) def slice(self, data, *, x: str | None = None, y: str | None = None, coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, isel: dict | None = None, vmin=None, vmax=None, @@ -121,7 +121,7 @@ def slice(self, data, *, x: str | None = None, y: str | None = None, coords: Coo array = self._array(data) return plot_slice(array, view=self._view(x, y, "t", coords, plane, select, isel), ax=ax, vmin=vmin, - vmax=vmax, equal_aspect=equal_aspect, title=title, run_label=self._run_label([array])) + vmax=vmax, equal_aspect=equal_aspect, title=title, run_label=self._label([array])) def panels(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, @@ -132,7 +132,7 @@ def panels(self, data, *, x: str | None = None, y: str | None = None, sweep: str array = self._array(data) return plot_panels(array, view=self._view(x, y, sweep, coords, plane, select, isel), nrows=nrows, - ncols=ncols, shared_clim=shared_clim, title=title, run_label=self._run_label([array])) + ncols=ncols, shared_clim=shared_clim, title=title, run_label=self._label([array])) def viewer(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, @@ -145,7 +145,7 @@ def viewer(self, data, *, x: str | None = None, y: str | None = None, sweep: str array = self._array(data) return InteractiveSliceViewer(array, view=self._view(x, y, sweep, coords, plane, select, isel), vmin=vmin, - vmax=vmax, run_label=self._run_label([array])) + vmax=vmax, run_label=self._label([array])) def animation(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, @@ -169,29 +169,29 @@ def orbits(self, species: str | None = None, *, max_markers: int = 200, show_pat """Three-dimensional trajectories of the saved markers of ``species``.""" from struphy.diagnostics.plotting import plot_marker_trajectories - available = tuple(self._run.orbits) + available = tuple(self._output.orbits) if species is None: if len(available) != 1: raise ValueError(f"choose a species from {available}") species = available[0] - return plot_marker_trajectories(self._run.orbits[species], ax=ax, max_markers=max_markers, + return plot_marker_trajectories(self._output.orbits[species], ax=ax, max_markers=max_markers, show_paths=show_paths) def equilibrium(self, ax=None): """Radial equilibrium profiles, from the geometry written at the start of the run.""" from struphy.diagnostics.plotting import plot_equilibrium_profile - return plot_equilibrium_profile(self._run.path_out, ax=ax) + return plot_equilibrium_profile(self._output.path_out, ax=ax) -class RunAnalysis: - """Quantitative diagnostics of a run, as ``run.analysis.(...)``.""" +class OutputAnalysis: + """Quantitative diagnostics of a run, as ``out.analysis.(...)``.""" - def __init__(self, run: "Run"): - self._run = run + def __init__(self, output: "Output"): + self._output = output def _array(self, data) -> xr.DataArray: - return self._run[data] if isinstance(data, str) else data + return self._output[data] if isinstance(data, str) else data def growth_rate(self, data, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): """Fit ``exp(rate * t + intercept)`` to a time series within ``window``. @@ -227,8 +227,8 @@ def dispersion(self, field, *, component: int = 0, slice_at: tuple = (None, 0, 0 from struphy.diagnostics.diagn_tools import power_spectrum_2d if isinstance(field, str): - run = self._run if self._run.time_units == "normalized" else self._run.with_time_units("normalized") + run = self._output if self._output.time_units == "normalized" else self._output.with_time_units("normalized") field = run[field] elif field.t.attrs.get("units") == "s": - raise ValueError("pass the field by name, or take it from run.with_time_units('normalized')") + raise ValueError("pass the field by name, or take it from out.with_time_units('normalized')") return power_spectrum_2d(field, component=component, slice_at=slice_at, physical=physical, **kwargs) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 715eaaa09..950eed6a6 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -81,7 +81,7 @@ def is_processed(path_out: str, options: dict | None = None) -> bool: class PostProcessor: """Post-process the raw output of a finished Struphy simulation. - Users do not call this directly; use :meth:`struphy.Run.process`, which also decides + Users do not call this directly; use :meth:`struphy.Output.process`, which also decides on which MPI ranks processing runs. Parameters @@ -186,7 +186,7 @@ def process( create_vtk: bool = True, force: bool = False, ): - """Run post-processing for fields and particle data in ``self.path_out``. + """Output post-processing for fields and particle data in ``self.path_out``. Parameters ---------- diff --git a/src/struphy/post_processing/tests/test_run.py b/src/struphy/post_processing/tests/test_output.py similarity index 91% rename from src/struphy/post_processing/tests/test_run.py rename to src/struphy/post_processing/tests/test_output.py index fb94ff037..1ddc009af 100644 --- a/src/struphy/post_processing/tests/test_run.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -1,4 +1,4 @@ -"""Tests for the lazy Run output API.""" +"""Tests for the lazy Output output API.""" import json import os @@ -9,7 +9,7 @@ import pytest from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint -from struphy.post_processing.run import Run, open_run +from struphy.post_processing.output import Output, open_output NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 @@ -64,7 +64,7 @@ def write_manifest(root, **options): class FakeSim: - """Just enough of a Simulation for Run: no configuration, a single rank.""" + """Just enough of a Simulation for Output: no configuration, a single rank.""" time_opts = grid = derham_opts = domain = None rank, comm_size = 0, 1 @@ -78,7 +78,7 @@ def Barrier(self): @pytest.fixture def run(tmp_path): - return Run(write_tree(str(tmp_path)), sim=FakeSim(), time_units="normalized") + return Output(write_tree(str(tmp_path)), sim=FakeSim(), time_units="normalized") def test_products_are_discovered_without_loading_arrays(run): @@ -128,10 +128,10 @@ def test_saving_scalars_and_bound_plot_accessor(run, tmp_path): assert result.ax.get_xlabel() == "$t$" -def test_open_run_needs_an_output_folder(tmp_path): +def test_open_output_needs_an_output_folder(tmp_path): with pytest.raises(FileNotFoundError, match="not a Struphy output folder"): - open_run(tmp_path) - run = open_run(write_tree(str(tmp_path))) + open_output(tmp_path) + run = open_output(write_tree(str(tmp_path))) assert run.path_out == tmp_path.resolve() @@ -141,7 +141,7 @@ def test_sim_is_restored_from_disk_only_on_access(tmp_path, monkeypatch): restored = FakeSim() calls = [] monkeypatch.setattr(Simulation, "from_output", classmethod(lambda cls, path: calls.append(path) or restored)) - run = open_run(write_tree(str(tmp_path))) + run = open_output(write_tree(str(tmp_path))) assert calls == [] assert run.sim is restored and run.sim is restored assert calls == [tmp_path.resolve()] @@ -150,7 +150,7 @@ def test_sim_is_restored_from_disk_only_on_access(tmp_path, monkeypatch): def test_products_trigger_default_processing_when_missing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = Run(root, sim=FakeSim(), time_units="normalized") + run = Output(root, sim=FakeSim(), time_units="normalized") calls = [] def fake_process(self, **options): @@ -159,7 +159,7 @@ def fake_process(self, **options): self._reset() return self - monkeypatch.setattr(Run, "process", fake_process) + monkeypatch.setattr(Output, "process", fake_process) assert set(run.scalars.data_vars) == {"en_tot"} assert calls == [], "scalars come from the raw output" assert tuple(run.fields) == ("em_fields",) @@ -172,7 +172,7 @@ def test_products_refuse_implicit_processing_on_many_ranks(tmp_path): sim = FakeSim() sim.comm_size = 2 with pytest.raises(RuntimeError, match="on all ranks"): - Run(root, sim=sim).fields + Output(root, sim=sim).fields def test_processing_options_are_part_of_the_manifest(tmp_path): @@ -206,7 +206,7 @@ def process(self, **options): monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) sim = FakeSim() sim.rank = rank - run = Run(write_tree(str(tmp_path)), sim=sim) + run = Output(write_tree(str(tmp_path)), sim=sim) assert run.process(physical=True) is run expected = [ ("construct", False), @@ -231,5 +231,5 @@ def process(self, **options): monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) sim = FakeSim() sim.rank = 3 - Run(write_tree(str(tmp_path)), sim=sim).process(parallel=True) + Output(write_tree(str(tmp_path)), sim=sim).process(parallel=True) assert calls == [True] diff --git a/src/struphy/post_processing/tests/test_run_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py similarity index 94% rename from src/struphy/post_processing/tests/test_run_accessors.py rename to src/struphy/post_processing/tests/test_output_accessors.py index 29b8f387c..1b1cc7159 100644 --- a/src/struphy/post_processing/tests/test_run_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -11,8 +11,8 @@ import pytest # noqa: E402 from matplotlib import pyplot as plt # noqa: E402 -from struphy.post_processing.run import Run # noqa: E402 -from struphy.post_processing.tests.test_run import NT, FakeSim, write_manifest, write_tree # noqa: E402 +from struphy.post_processing.output import Output # noqa: E402 +from struphy.post_processing.tests.test_output import NT, FakeSim, write_manifest, write_tree # noqa: E402 RATE = 2.0 @@ -25,7 +25,7 @@ def make_run(root, name="sim_1"): time = np.asarray(file["time/value"]) file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) write_manifest(path) - return Run(path, sim=FakeSim(), time_units="normalized") + return Output(path, sim=FakeSim(), time_units="normalized") @pytest.fixture diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index 8fc8bb83f..168883992 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -6,7 +6,7 @@ from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt -from struphy import Run, Simulation, set_logging_level +from struphy import Output, Simulation, set_logging_level from struphy.io.setup import import_parameters_py set_logging_level(logging.WARNING) @@ -20,7 +20,7 @@ @pytest.mark.mpi(min_size=2) def test_pproc_mpi(show_plot=False): - def do_plotting(run: Run, from_parallel=False): + def do_plotting(run: Output, from_parallel=False): e_field = run.fields.em_fields.e_field_log.isel(t=0, component=0, e2=0, e3=0) phi = run.fields.em_fields.phi_log.isel(t=0, e2=0, e3=0) f = run.distributions.kinetic_ions.e1_v1_density diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index 12003e7cb..c1965f8a3 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -29,7 +29,7 @@ def initialize_data_storage(self): @abstractmethod def run(self): - """Run the simulation.""" + """Output the simulation.""" pass @property diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 6b5f47201..767075637 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -64,7 +64,7 @@ from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.physics.physics import Units from struphy.pic.base import Particles -from struphy.post_processing.run import Run +from struphy.post_processing.output import Output from struphy.propagators.base import Propagator from struphy.simulation.base import SimulationBase from struphy.utils.clone_config import CloneConfig @@ -599,7 +599,7 @@ def initialize_data_storage(self): self.data.add_data({key_time: val}) self.data.add_data({key_time_restart: val}) - def run(self, one_time_step: bool = False, profiling_activated: bool | None = None) -> Run: + def run(self, one_time_step: bool = False, profiling_activated: bool | None = None) -> Output: """Main entry point to execute the simulation time loop. Responsibilities include allocation (when not restarting), @@ -618,7 +618,7 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No Returns ------- - Run + Output The output of this run, see :attr:`output`. """ if profiling_activated is None: @@ -884,14 +884,14 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No return self.output @property - def output(self) -> Run: - """The output of this simulation in ``env.path_out``, see :class:`~struphy.Run`. + def output(self) -> Output: + """The output of this simulation in ``env.path_out``, see :class:`~struphy.Output`. Scalars are available as soon as data is written; fields and particle products are post-processed on first access, or explicitly with ``sim.output.process(...)``. """ if self._output is None or self._output.path_out != Path(self.env.path_out).resolve(): - self._output = Run(self.env.path_out, sim=self) + self._output = Output(self.env.path_out, sim=self) return self._output # --------------------- diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index ed49da145..d74320ab2 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -4,7 +4,7 @@ import pytest -from struphy import BaseUnits, EnvironmentOptions, Run, Simulation, Time, open_run +from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time, open_output from struphy.models import Maxwell, VlasovAmpereOneSpecies @@ -22,7 +22,7 @@ def test_constructing_a_simulation_writes_nothing(tmp_path): def test_output_is_the_run_of_the_current_output_folder(tmp_path): sim = make_sim(tmp_path) run = sim.output - assert isinstance(run, Run) + assert isinstance(run, Output) assert run.sim is sim assert sim.output is run @@ -39,7 +39,7 @@ def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): moved = tmp_path / "moved" os.rename(sim.env.path_out, moved) - restored = open_run(moved).sim + restored = open_output(moved).sim assert restored.model.to_dict() == model.to_dict() assert restored.model.params["mass_number"] == 4.0 diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 380df6639..eefb521eb 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -7,9 +7,9 @@ "source": [ "# Post-processing and standard plots\n", "\n", - "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, get its output as an autocomplete-friendly `Run`, and make the plots most commonly used to inspect a simulation.\n", + "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, get its output as an autocomplete-friendly `Output`, and make the plots most commonly used to inspect a simulation.\n", "\n", - "For a production run you can skip the simulation setup and open its output folder with `struphy.open_run(\"path/to/sim\")` instead." + "For a production run you can skip the simulation setup and open its output folder with `struphy.open_output(\"path/to/sim\")` instead." ] }, { @@ -109,7 +109,7 @@ " grid=grids.TensorProductGrid(num_elements=(8, 1, 1)),\n", " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", ")\n", - "run = sim.run()\n", + "out = sim.run()\n", "print(f\"Raw output: {sim.env.path_out}\")" ] }, @@ -120,11 +120,11 @@ "source": [ "## Process and load the output\n", "\n", - "`sim.run()` returns the run's output as a `Run`, which is also available later as `sim.output`. Scalars are read directly from the raw output; fields and particle products need post-processing, which runs with default options the first time they are accessed.\n", + "`sim.run()` returns the run's output as a `Output`, which is also available later as `sim.output`. Scalars are read directly from the raw output; fields and particle products need post-processing, which runs with default options the first time they are accessed.\n", "\n", - "To choose options, call `run.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", + "To choose options, call `out.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", "\n", - "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Arrays are loaded only when accessed. The simulation that produced them is `run.sim`." + "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Arrays are loaded only when accessed. The simulation that produced them is `out.sim`." ] }, { @@ -134,7 +134,7 @@ "metadata": {}, "outputs": [], "source": [ - "run.process(physical=True)" + "out.process(physical=True)" ] }, { @@ -142,7 +142,7 @@ "id": "7", "metadata": {}, "source": [ - "Products are arranged into clear namespaces. VS Code and interactive shells can complete the available names after a run is opened: fields are grouped by field species, while distribution and density products are grouped by species and saved slice. Every product can also be looked up by name, e.g. `run[\"electric_energy\"]` or `run[\"kinetic_ions/e1_v1_density/f_binned\"]`; flat catalogs remain available for code that needs to iterate over arbitrary products." + "Products are arranged into clear namespaces. VS Code and interactive shells can complete the available names after a run is opened: fields are grouped by field species, while distribution and density products are grouped by species and saved slice. Every product can also be looked up by name, e.g. `out[\"electric_energy\"]` or `out[\"kinetic_ions/e1_v1_density/f_binned\"]`; flat catalogs remain available for code that needs to iterate over arbitrary products." ] }, { @@ -152,13 +152,13 @@ "metadata": {}, "outputs": [], "source": [ - "print(\"scalars:\", tuple(run.scalars.data_vars))\n", - "print(\"field species:\", tuple(run.fields))\n", - "print(\"distribution species:\", tuple(run.distributions))\n", - "print(\"particle species:\", tuple(run.orbits))\n", - "print(\"all field products:\", tuple(run.field_catalog))\n", + "print(\"scalars:\", tuple(out.scalars.data_vars))\n", + "print(\"field species:\", tuple(out.fields))\n", + "print(\"distribution species:\", tuple(out.distributions))\n", + "print(\"particle species:\", tuple(out.orbits))\n", + "print(\"all field products:\", tuple(out.field_catalog))\n", "\n", - "phase_space = run.distributions.kinetic_ions.e1_v1_density.f_binned\n", + "phase_space = out.distributions.kinetic_ions.e1_v1_density.f_binned\n", "print(phase_space)\n", "print(\"dimensions:\", phase_space.dims)\n", "print(\"time coordinate:\", phase_space.t)" @@ -171,9 +171,9 @@ "source": [ "## Scalar overview and time series\n", "\n", - "All standard plots are methods of `run.plot`, so no further imports are needed. They accept a product name or any array, and titles carry the run's numerical parameters.\n", + "All standard plots are methods of `out.plot`, so no further imports are needed. They accept a product name or any array, and titles carry the run's numerical parameters.\n", "\n", - "`run.plot.scalars()` gives a quick overview of every recorded scalar, with the relative error of the total energy below. `run.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." + "`out.plot.scalars()` gives a quick overview of every recorded scalar, with the relative error of the total energy below. `out.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] }, { @@ -183,7 +183,7 @@ "metadata": {}, "outputs": [], "source": [ - "scalar_plot = run.plot.scalars()\n", + "scalar_plot = out.plot.scalars()\n", "energy_error = scalar_plot.data[\"relative_error\"]\n", "scalar_plot" ] @@ -195,8 +195,8 @@ "metadata": {}, "outputs": [], "source": [ - "t_fit = 0.4 * run.sim.model.units.t # in seconds, like every time coordinate of this run\n", - "energy_plot = run.plot.timeseries(\n", + "t_fit = 0.4 * out.sim.model.units.t # in seconds, like every time coordinate of this run\n", + "energy_plot = out.plot.timeseries(\n", " \"electric_energy\",\n", " fit=(0.0, t_fit),\n", " fit_amplitude=True,\n", @@ -205,7 +205,7 @@ "print(\"growth rate:\", energy_plot.fit_results[0].rate)\n", "\n", "# the same fit without a figure\n", - "print(\"growth rate:\", run.analysis.growth_rate(\"electric_energy\", window=(0.0, t_fit), amplitude=True).rate)" + "print(\"growth rate:\", out.analysis.growth_rate(\"electric_energy\", window=(0.0, t_fit), amplitude=True).rate)" ] }, { @@ -225,7 +225,7 @@ "metadata": {}, "outputs": [], "source": [ - "run.plot.slice(\n", + "out.plot.slice(\n", " phase_space,\n", " x=\"e1\",\n", " y=\"v1\",\n", @@ -240,7 +240,7 @@ "id": "14", "metadata": {}, "source": [ - "For a compact view of the evolution, `run.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." + "For a compact view of the evolution, `out.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." ] }, { @@ -250,7 +250,7 @@ "metadata": {}, "outputs": [], "source": [ - "run.plot.panels(\n", + "out.plot.panels(\n", " phase_space,\n", " x=\"e1\",\n", " y=\"v1\",\n", @@ -267,7 +267,7 @@ "source": [ "## Interactive plots\n", "\n", - "`run.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `run.plot.animation()` and `run.plot.frames()` sweep the same way." + "`out.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." ] }, { @@ -277,7 +277,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_viewer = run.plot.viewer(phase_space, x=\"e1\", y=\"v1\")\n", + "phase_viewer = out.plot.viewer(phase_space, x=\"e1\", y=\"v1\")\n", "phase_viewer" ] }, @@ -286,7 +286,7 @@ "id": "18", "metadata": {}, "source": [ - "Saved marker orbits are grouped by species. `run.plot.orbits()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." + "Saved marker orbits are grouped by species. `out.plot.orbits()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." ] }, { @@ -296,7 +296,7 @@ "metadata": {}, "outputs": [], "source": [ - "run.plot.orbits(\"kinetic_ions\", max_markers=12, show_paths=True)" + "out.plot.orbits(\"kinetic_ions\", max_markers=12, show_paths=True)" ] }, { @@ -306,7 +306,7 @@ "source": [ "## Save standard output\n", "\n", - "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `run.save_report()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/report/`." + "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `out.save_report()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/report/`." ] }, { @@ -316,10 +316,10 @@ "metadata": {}, "outputs": [], "source": [ - "written = run.save_report()\n", + "written = out.save_report()\n", "print(\"Wrote:\")\n", "for path in written:\n", - " print(\" \", os.path.relpath(path, run.path_out))" + " print(\" \", os.path.relpath(path, out.path_out))" ] }, { @@ -334,11 +334,11 @@ "```python\n", "import struphy\n", "\n", - "run = struphy.open_run(\"/path/to/sim_1\").process(physical=True)\n", - "run.sim.domain, run.sim.model.units # the simulation, restored from disk without allocating\n", + "out = struphy.open_output(\"/path/to/sim_1\").process(physical=True)\n", + "out.sim.domain, out.sim.model.units # the simulation, restored from disk without allocating\n", "```\n", "\n", - "Use `run.scalars`, `run.fields`, `run.distributions`, `run.orbits`, and `run.densities`. Attribute access is the normal interactive API; the corresponding `*_catalog` mappings are intended for generic loops and tooling." + "Use `out.scalars`, `out.fields`, `out.distributions`, `out.orbits`, and `out.densities`. Attribute access is the normal interactive API; the corresponding `*_catalog` mappings are intended for generic loops and tooling." ] } ], From 5df6b2484764b9438dd938e0b50c76721a6dad53 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 10:50:25 +0200 Subject: [PATCH 023/193] Removed the rel error subplot --- doc/sections/userguide.rst | 2 +- src/struphy/diagnostics/plotting.py | 27 ++++--------------- .../diagnostics/tests/test_plotting.py | 3 +-- .../post_processing/output_accessors.py | 14 +++------- .../tests/test_output_accessors.py | 8 +++--- tutorials/tutorial_post_processing.ipynb | 6 ++--- 6 files changed, 16 insertions(+), 44 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 1e6817317..e0f1ceb3e 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -567,7 +567,7 @@ run), and figures are titled with the run's numerical parameters: .. code-block:: python - out.plot.scalars() # overview + energy conservation error + out.plot.scalars() # every scalar time series out.plot.timeseries("en_phi", fit=(0.0, 40.0)) # exponential fit in a time window out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", isel={"t": -1}) out.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index f08e1eb9a..2d87ddb6b 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -457,39 +457,22 @@ def save_frames(data: xr.DataArray, directory, *, view=None, step=1, prefix="fra def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, - error_panel="en_tot", logy=False, run_label=None): - """Plot a scalar overview and optional conservation-error panel. - - The relative error of ``error_panel`` is returned in ``result.data["relative_error"]``. - """ + logy=False, run_label=None): + """Plot every scalar time series in one axes.""" selected = scalar_names(scalars, names=names, exclude=exclude) if not selected: raise ValueError("no scalars to plot") run_label = shared_run_label([scalars[name] for name in selected]) if run_label is None else run_label - has_error = error_panel is not None and error_panel in scalars - fig, axes = plt.subplots(2 if has_error else 1, 1, sharex=has_error, - figsize=(8, 6.5) if has_error else None, - height_ratios=(2, 1) if has_error else None, - layout="constrained") - ax = axes[0] if has_error else axes + fig, ax = plt.subplots(layout="constrained") for name in selected: values = scalars[name] / scalars[relative_to] if relative_to else scalars[name] ax.plot(values.t, values, label=name) if logy: ax.set_yscale("log") units = {scalars[name].attrs.get("units", "") for name in selected} ylabel = f"quantity / {relative_to}" if relative_to else (f"[{units.pop()}]" if len(units) == 1 else "[a.u.]") - ax.set(ylabel=ylabel, title="Scalars") + ax.set(xlabel=axis_label(scalars[selected[0]], "t"), ylabel=ylabel, title="Scalars") ax.legend(fontsize="small") - artists, error = list(ax.lines), None - if has_error: - error = relative_error(scalars[error_panel]) - axes[1].plot(error.t, error) - if np.any(np.asarray(error) > 0): axes[1].set_yscale("log") - axes[1].set(xlabel=axis_label(error, "t"), ylabel=f"relative error of {error_panel}") - artists.extend(axes[1].lines) - else: - ax.set_xlabel(axis_label(scalars[selected[0]], "t")) if run_label: fig.suptitle(run_label, fontsize="small") - return PlotResult(fig, axes, artists, data={"relative_error": error}) + return PlotResult(fig, ax, list(ax.lines)) def save_all_scalars(scalars, directory, *, names=None, exclude=SCALARS_EXCLUDE, logy=False, diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index a0b44776b..c145f9241 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -144,8 +144,7 @@ def test_animation_and_frames_share_the_view(tmp_path): def test_scalar_overview_and_export(tmp_path): result = plot_scalars(scalar_dataset(), run_label="run") - error = result.data["relative_error"] - assert error is not None + assert sorted(line.get_label() for line in result.artists) == ["en_e", "en_tot"] assert result.fig._suptitle.get_text() == "run" paths = save_all_scalars(scalar_dataset(), tmp_path) assert sorted(__import__("os").path.basename(path) for path in paths) == [ diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index 794b1c8ab..fbbf93992 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -45,18 +45,13 @@ def _view(x, y, sweep, coords, plane, select, isel): return View(x=x, y=y, sweep=sweep, select=dict(select or {}), isel=dict(isel or {}), coordinates=coords, plane=plane) - def scalars(self, names=None, *, conservation: str | None = "auto", relative_to: str | None = None, - logy: bool = False): - """Overview of the scalar time series with a conservation-error panel. + def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): + """Overview of the scalar time series in one axes. Parameters ---------- names: Scalars to show; all by default. - conservation: - Scalar whose relative error is shown below, e.g. ``"en_tot"``; ``"auto"`` picks the - total energy if it was saved, ``None`` shows no panel. The error is returned in - ``result.data["relative_error"]``. relative_to: Show every scalar divided by this one. logy: @@ -64,10 +59,7 @@ def scalars(self, names=None, *, conservation: str | None = "auto", relative_to: """ from struphy.diagnostics.plotting import plot_scalars - scalars = self._output.scalars - if conservation == "auto": - conservation = next((name for name in ("en_tot", "total_energy") if name in scalars), None) - return plot_scalars(scalars, names=names, relative_to=relative_to, error_panel=conservation, logy=logy, + return plot_scalars(self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label) def timeseries(self, *data, logy: bool = True, fit: tuple[float | None, float | None] | bool | None = None, diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 1b1cc7159..a60d4bb1d 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -12,7 +12,7 @@ from matplotlib import pyplot as plt # noqa: E402 from struphy.post_processing.output import Output # noqa: E402 -from struphy.post_processing.tests.test_output import NT, FakeSim, write_manifest, write_tree # noqa: E402 +from struphy.post_processing.tests.test_output import FakeSim, write_manifest, write_tree # noqa: E402 RATE = 2.0 @@ -76,10 +76,10 @@ def test_timeseries_into_given_axes_keeps_the_figure_layout(run): assert fig._suptitle.get_text() == "mine" -def test_scalar_overview_picks_the_total_energy(run): +def test_scalar_overview_draws_every_scalar_in_one_axes(run): result = run.plot.scalars() - assert result.data["relative_error"].sizes["t"] == NT - 1 - assert run.plot.scalars(conservation=None).data["relative_error"] is None + assert sorted(line.get_label() for line in result.artists) == ["en_phi", "en_tot"] + assert result.fig.axes == [result.ax] def test_slices_panels_and_viewer_take_keyword_views(run): diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index eefb521eb..300934f92 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -173,7 +173,7 @@ "\n", "All standard plots are methods of `out.plot`, so no further imports are needed. They accept a product name or any array, and titles carry the run's numerical parameters.\n", "\n", - "`out.plot.scalars()` gives a quick overview of every recorded scalar, with the relative error of the total energy below. `out.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." + "`out.plot.scalars()` gives a quick overview of every recorded scalar. `out.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] }, { @@ -183,9 +183,7 @@ "metadata": {}, "outputs": [], "source": [ - "scalar_plot = out.plot.scalars()\n", - "energy_error = scalar_plot.data[\"relative_error\"]\n", - "scalar_plot" + "out.plot.scalars()" ] }, { From 5504b19ce15ff2f0e639028d55b18ecdc5562c0e Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 10:59:09 +0200 Subject: [PATCH 024/193] Restored old tutorials --- src/struphy/post_processing/output.py | 43 +++++++- src/struphy/simulation/sim.py | 67 +++++++++++ src/struphy/simulation/tests/test_output.py | 31 ++++++ tutorials/dev_tutorial_feec_bcs.ipynb | 12 +- tutorials/tutorial_beltrami_sph.ipynb | 37 +++++-- tutorials/tutorial_dam_break_sph.ipynb | 12 +- tutorials/tutorial_gas_expansion_sph.ipynb | 30 +++-- tutorials/tutorial_hagen_poiseuille_sph.ipynb | 9 +- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 27 +++-- tutorials/tutorial_maxwell.ipynb | 38 ++++--- tutorials/tutorial_particle_tracing.ipynb | 104 +++++++++++++----- tutorials/tutorial_poisson.ipynb | 53 ++++----- .../tutorial_pressureless_sph_shock.ipynb | 13 ++- .../tutorial_velocity_diffusion_sph.ipynb | 17 ++- tutorials/tutorial_viscous_euler_sph.ipynb | 25 ++--- 15 files changed, 373 insertions(+), 145 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 3d415d4c7..7c516ade1 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -4,6 +4,7 @@ import logging import pickle +import warnings from collections.abc import Callable, Iterator, Mapping from pathlib import Path @@ -308,6 +309,30 @@ def analysis(self) -> OutputAnalysis: """Quantitative diagnostics, e.g. ``out.analysis.growth_rate("en_phi", window=(0, 40))``.""" return OutputAnalysis(self) + @property + def f(self) -> DistributionProducts: + """Deprecated alias of :attr:`distributions`.""" + warnings.warn("Output.f is deprecated; use out.distributions instead.", DeprecationWarning, stacklevel=2) + return self.distributions + + @property + def spline_values(self) -> FieldProducts: + """Deprecated alias of :attr:`fields`.""" + warnings.warn("Output.spline_values is deprecated; use out.fields instead.", DeprecationWarning, stacklevel=2) + return self.fields + + @property + def n_sph(self) -> DensityProducts: + """Deprecated alias of :attr:`densities`.""" + warnings.warn("Output.n_sph is deprecated; use out.densities instead.", DeprecationWarning, stacklevel=2) + return self.densities + + @property + def t_grid(self): + """Deprecated alias of :attr:`time`.""" + warnings.warn("Output.t_grid is deprecated; use out.time instead.", DeprecationWarning, stacklevel=2) + return self.time + @property def time_scale(self) -> float: return float(self.sim.model.units.t) if self.time_units == "physical" else 1.0 @@ -326,20 +351,26 @@ def time(self): @property def grids_log(self): + """Logical evaluation grids of the fields; None for a run without FEEC fields.""" if self._grids_log is None: - self._ensure_processed() - with (self.path_pproc / "fields_data" / "grids_log.bin").open("rb") as stream: - self._grids_log = pickle.load(stream) + self._grids_log = self._load_grids("grids_log") return self._grids_log @property def grids_phy(self): + """Mapped evaluation grids of the fields; None for a run without FEEC fields.""" if self._grids_phy is None: - self._ensure_processed() - with (self.path_pproc / "fields_data" / "grids_phy.bin").open("rb") as stream: - self._grids_phy = pickle.load(stream) + self._grids_phy = self._load_grids("grids_phy") return self._grids_phy + def _load_grids(self, name): + self._ensure_processed() + path = self.path_pproc / "fields_data" / f"{name}.bin" + if not path.exists(): + return None + with path.open("rb") as stream: + return pickle.load(stream) + @property def scalars(self) -> xr.Dataset: """Scalar time series, read from the raw output; needs no post-processing.""" diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 767075637..41908f9af 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -7,6 +7,7 @@ import shutil import sysconfig import time +import warnings from pathlib import Path import cunumpy as xp @@ -894,6 +895,72 @@ def output(self) -> Output: self._output = Output(self.env.path_out, sim=self) return self._output + # ------------------------------------------------------------------ + # Deprecated post-processing entry points, superseded by self.output + # ------------------------------------------------------------------ + + def pproc( + self, + step: int = 1, + celldivide: int | tuple[int, int, int] = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = True, + parallel_pproc: bool = False, + force: bool = True, + load: bool = False, + ) -> Output | None: + """Deprecated, use ``sim.output.process(...)``, see :meth:`struphy.Output.process`.""" + warnings.warn( + "Simulation.pproc() is deprecated; use sim.output.process(...) instead.", + DeprecationWarning, + stacklevel=2, + ) + self.output.process( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + parallel=parallel_pproc, + force=force, + ) + return self.load_plotting_data() if load else None + + def load_plotting_data(self) -> Output | None: + """Deprecated, use :attr:`output`; attaches its products as attributes of the simulation. + + Returns the :class:`struphy.Output` on rank 0 and ``None`` on the other ranks. + """ + warnings.warn( + "Simulation.load_plotting_data() is deprecated; use sim.output (a struphy.Output) instead.", + DeprecationWarning, + stacklevel=2, + ) + if self.rank != 0: + return None + output = self.output + self.orbits = output.orbits + self.f = output.distributions + self.spline_values = output.fields + self.n_sph = output.densities + self.grids_log = output.grids_log + self.grids_phy = output.grids_phy + self.t_grid = output.time + return output + + @property + def plotting_data(self) -> Output: + """Deprecated alias of :attr:`output`.""" + warnings.warn( + "Simulation.plotting_data is deprecated; use sim.output instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.output + # --------------------- # Code specific methods # --------------------- diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index d74320ab2..45c5cb52e 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -71,3 +71,34 @@ def test_from_output_never_executes_the_parameter_file(tmp_path): def test_from_output_requires_a_configuration(tmp_path): with pytest.raises(FileNotFoundError, match="config.json"): Simulation.from_output(tmp_path) + + +def test_deprecated_pproc_delegates_to_the_output(tmp_path, monkeypatch): + sim = make_sim(tmp_path) + calls = [] + monkeypatch.setattr(type(sim.output), "process", lambda self, **options: calls.append(options)) + monkeypatch.setattr(type(sim), "load_plotting_data", lambda self: "loaded") + + with pytest.deprecated_call(): + assert sim.pproc(physical=True) is None + assert calls == [ + dict(step=1, celldivide=1, physical=True, guiding_center=False, classify=False, + create_vtk=True, parallel=False, force=True) + ] + with pytest.deprecated_call(): + assert sim.pproc(load=True) == "loaded" + + +def test_deprecated_load_plotting_data_attaches_the_products(tmp_path, monkeypatch): + sim = make_sim(tmp_path) + output = sim.output + for name, value in (("orbits", "o"), ("distributions", "f"), ("fields", "s"), ("densities", "n"), + ("grids_log", "gl"), ("grids_phy", "gp"), ("time", "t")): + monkeypatch.setattr(type(output), name, property(lambda self, value=value: value)) + + with pytest.deprecated_call(): + assert sim.load_plotting_data() is output + assert (sim.orbits, sim.f, sim.spline_values, sim.n_sph) == ("o", "f", "s", "n") + assert (sim.grids_log, sim.grids_phy, sim.t_grid) == ("gl", "gp", "t") + with pytest.deprecated_call(): + assert sim.plotting_data is output diff --git a/tutorials/dev_tutorial_feec_bcs.ipynb b/tutorials/dev_tutorial_feec_bcs.ipynb index 59ebe960c..701000673 100644 --- a/tutorials/dev_tutorial_feec_bcs.ipynb +++ b/tutorials/dev_tutorial_feec_bcs.ipynb @@ -344,7 +344,7 @@ "id": "24", "metadata": {}, "source": [ - "Post-processing the output of the run:" + "Post porcessing and loading the plotting data in `verbose` mode yields:" ] }, { @@ -354,8 +354,8 @@ "metadata": {}, "outputs": [], "source": [ - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()" + "sim.pproc()\n", + "sim.load_plotting_data()" ] }, { @@ -373,7 +373,7 @@ "metadata": {}, "outputs": [], "source": [ - "e1h = run.grids_log[0]\n", + "e1h = sim.plotting_data.grids_log[0]\n", "print(e1h)" ] }, @@ -384,7 +384,7 @@ "metadata": {}, "outputs": [], "source": [ - "phi = run.fields.em_fields.phi_log\n", + "phi = sim.plotting_data.spline_values.em_fields.phi_log\n", "print(phi)" ] }, @@ -404,7 +404,7 @@ "outputs": [], "source": [ "plt.plot(e1, mfct_solution(e1), label=\"exact\")\n", - "plt.plot(e1h, phi.isel(t=0, e2=0, e3=0), \"go\", label=\"numerical solution\")\n", + "plt.plot(e1h, phi.data[0.0][0][:, 0, 0], \"go\", label=\"numerical solution\")\n", "plt.xlabel('e1')\n", "plt.legend()" ] diff --git a/tutorials/tutorial_beltrami_sph.ipynb b/tutorials/tutorial_beltrami_sph.ipynb index 3dc68cf51..e66e02bf6 100644 --- a/tutorials/tutorial_beltrami_sph.ipynb +++ b/tutorials/tutorial_beltrami_sph.ipynb @@ -289,7 +289,8 @@ "Execute the simulation pipeline in order:\n", "\n", "1. `run()` to advance particles in time\n", - "2. `sim.output.process()` to build post-processed outputs, which are then loaded lazily from `run`\n", + "2. `pproc()` to build post-processed outputs\n", + "3. `load_plotting_data()` to bring diagnostics into memory\n", "\n", "Then plot marker trajectories to inspect how the Beltrami-driven flow evolves." ] @@ -311,8 +312,17 @@ "metadata": {}, "outputs": [], "source": [ - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()" + "sim.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "sim.load_plotting_data()" ] }, { @@ -326,14 +336,14 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = run.orbits.cold_fluid.values\n", + "orbits = sim.orbits.cold_fluid\n", "\n", "coloring = np.select(\n", " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", - "Nt = run.time.size - 1\n", + "Nt = sim.t_grid.size - 1\n", "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", @@ -456,8 +466,17 @@ "metadata": {}, "outputs": [], "source": [ - "run_tess = sim_tess.output.with_time_units(\"normalized\")\n", - "run_tess.process()" + "sim_tess.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "sim_tess.load_plotting_data()" ] }, { @@ -471,14 +490,14 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = run_tess.orbits.cold_fluid.values\n", + "orbits = sim_tess.orbits.cold_fluid\n", "\n", "coloring = np.select(\n", " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", - "Nt = run_tess.time.size - 1\n", + "Nt = sim_tess.t_grid.size - 1\n", "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", diff --git a/tutorials/tutorial_dam_break_sph.ipynb b/tutorials/tutorial_dam_break_sph.ipynb index f4602cc7e..0cb09045b 100644 --- a/tutorials/tutorial_dam_break_sph.ipynb +++ b/tutorials/tutorial_dam_break_sph.ipynb @@ -264,9 +264,7 @@ "sim.run()\n", "print(\"Simulation complete.\")\n", "\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "\n", - "run.process()\n", + "sim.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -285,15 +283,15 @@ "metadata": {}, "outputs": [], "source": [ + "sim.load_plotting_data()\n", "\n", "# KDE density field: shape (Nt+1, pts_e1, pts_e2, 1)\n", - "density = run.densities.euler_fluid.view_0.n_sph\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", - "n_sph = density.values\n", + "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", + "n_sph = sim.n_sph.euler_fluid.view_0.n_sph\n", "\n", "# Marker orbits: shape (Nt_orb, n_markers, n_attrs)\n", "# attrs for vdim=2: [x, y, z, v1, v2, w, diag, id]\n", - "orbits = np.asarray(run.orbits.euler_fluid.values)\n", + "orbits = np.asarray(sim.orbits.euler_fluid)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_gas_expansion_sph.ipynb b/tutorials/tutorial_gas_expansion_sph.ipynb index 2c8199454..433a8df9c 100644 --- a/tutorials/tutorial_gas_expansion_sph.ipynb +++ b/tutorials/tutorial_gas_expansion_sph.ipynb @@ -345,7 +345,7 @@ "source": [ "### Step 9: Initialize, Run, and Load Results\n", "\n", - "Attach the Gaussian background and execute the standard workflow: `sim.run()`, then `run.process()` on the simulation output `run = sim.output`.\n", + "Attach the Gaussian background and execute the standard workflow: `run()`, `pproc()`, `load_plotting_data()`.\n", "\n", "Performance note: early time steps are typically slower because particles are highly concentrated; runtime usually improves as the cloud expands. Running from a console script (especially with MPI/GPU support) is faster than notebook execution." ] @@ -378,8 +378,17 @@ "metadata": {}, "outputs": [], "source": [ - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()" + "sim.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "sim.load_plotting_data()" ] }, { @@ -407,21 +416,20 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "density = run.densities.euler_fluid.view_0.n_sph\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", + "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", - "bc_x = run.distributions.euler_fluid.e1_e2_density.f_binned.e1.values\n", - "bc_y = run.distributions.euler_fluid.e1_e2_density.f_binned.e2.values\n", + "bc_x = sim.f.euler_fluid.e1_e2_density.grid_e1\n", + "bc_y = sim.f.euler_fluid.e1_e2_density.grid_e2\n", "\n", "# markers\n", - "orbits = run.orbits.euler_fluid.values\n", + "orbits = sim.orbits.euler_fluid\n", "positions = orbits[0, :, :3]\n", "weights = orbits[0, :, 6]\n", "\n", "# binning and sph eval\n", - "n_sph = density.values[0]\n", - "f_bin = run.distributions.euler_fluid.e1_e2_density.f_binned.values[0]" + "n_sph = sim.n_sph.euler_fluid.view_0.n_sph[0]\n", + "f_bin = sim.f.euler_fluid.e1_e2_density.f_binned[0]" ] }, { @@ -491,7 +499,7 @@ "outputs": [], "source": [ "dt = time_opts.dt\n", - "Nt = run.time.size - 1\n", + "Nt = sim.t_grid.size - 1\n", "\n", "positions = orbits[:, :, :3]\n", "\n", diff --git a/tutorials/tutorial_hagen_poiseuille_sph.ipynb b/tutorials/tutorial_hagen_poiseuille_sph.ipynb index 104fe9e51..ada024c24 100644 --- a/tutorials/tutorial_hagen_poiseuille_sph.ipynb +++ b/tutorials/tutorial_hagen_poiseuille_sph.ipynb @@ -262,9 +262,7 @@ "sim.run()\n", "print(\"Simulation complete.\")\n", "\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "\n", - "run.process()\n", + "sim.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -283,9 +281,10 @@ "metadata": {}, "outputs": [], "source": [ + "sim.load_plotting_data()\n", "\n", - "e2_grid = run.distributions.euler_fluid.e2_current_1.f_binned.e2.values # logical y in [0, 1]\n", - "j1_binned = run.distributions.euler_fluid.e2_current_1.f_binned.values # shape (Nt+1, n_bins)\n", + "e2_grid = sim.f.euler_fluid.e2_current_1.grid_e2 # logical y in [0, 1]\n", + "j1_binned = sim.f.euler_fluid.e2_current_1.f_binned # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index e087f0729..5ff5496cc 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -63,6 +63,7 @@ " grids,\n", " perturbations,\n", ")\n", + "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", "from struphy.models import LinearMHD\n", "\n", "logger = logging.getLogger(\"struphy\")" @@ -220,8 +221,7 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()\n", + "sim.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -244,6 +244,13 @@ "metadata": {}, "outputs": [], "source": [ + "# Load plotting data\n", + "sim.load_plotting_data()\n", + "\n", + "# Extract velocity and pressure time-series\n", + "u_of_t = sim.spline_values.mhd.velocity_log.data\n", + "p_of_t = sim.spline_values.mhd.pressure_log.data\n", + "\n", "# Dispersion relation parameters\n", "gamma = 5 / 3 # Adiabatic index\n", "disp_params = {\n", @@ -257,9 +264,11 @@ "\n", "# 1. Shear Alfvén wave analysis from velocity\n", "print(\"\\n=== Shear Alfvén Wave Analysis ===\")\n", - "_1, _2, _3, coeffs_alfven = run.analysis.dispersion(\n", - " \"mhd/velocity_log\",\n", - " physical=True,\n", + "_1, _2, _3, coeffs_alfven = power_spectrum_2d(\n", + " u_of_t,\n", + " \"velocity_log\",\n", + " grids=sim.grids_log,\n", + " grids_mapped=sim.grids_phy,\n", " component=0,\n", " slice_at=[0, 0, None],\n", " do_plot=True,\n", @@ -293,9 +302,11 @@ "source": [ "# 2. Magnetosonic waves analysis from pressure\n", "print(\"=== Slow and Fast Magnetosonic Wave Analysis ===\")\n", - "_1, _2, _3, coeffs_sonic = run.analysis.dispersion(\n", - " \"mhd/pressure_log\",\n", - " physical=True,\n", + "_1, _2, _3, coeffs_sonic = power_spectrum_2d(\n", + " p_of_t,\n", + " \"pressure_log\",\n", + " grids=sim.grids_log,\n", + " grids_mapped=sim.grids_phy,\n", " component=0,\n", " slice_at=[0, 0, None],\n", " do_plot=True,\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index 421daf525..85af3dc2b 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -111,6 +111,7 @@ " grids,\n", " perturbations,\n", ")\n", + "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", "from struphy.models import Maxwell\n", "\n", "logger = logging.getLogger(\"struphy\")" @@ -238,8 +239,7 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()\n", + "sim.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -262,11 +262,19 @@ "metadata": {}, "outputs": [], "source": [ + "# Load plotting data\n", + "sim.load_plotting_data()\n", + "\n", + "# Extract electric field time-series\n", + "E_of_t = sim.spline_values.em_fields.e_field_log.data\n", + "\n", "# Compute power spectrum and fit dispersion relation\n", "print(\"\\n=== Light Wave Dispersion Analysis ===\")\n", - "_1, _2, _3, coeffs = run.analysis.dispersion(\n", - " \"em_fields/e_field_log\",\n", - " physical=True,\n", + "_1, _2, _3, coeffs = power_spectrum_2d(\n", + " E_of_t,\n", + " \"e_field_log\",\n", + " grids=sim.grids_log,\n", + " grids_mapped=sim.grids_phy,\n", " component=0,\n", " slice_at=[0, 0, None],\n", " do_plot=True,\n", @@ -564,8 +572,7 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing (with physical=True to extract physical fields)\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process(physical=True)\n", + "sim.pproc(physical=True)\n", "print(\"Post-processing complete.\")" ] }, @@ -586,11 +593,14 @@ "metadata": {}, "outputs": [], "source": [ + "# Load plotting data\n", + "sim.load_plotting_data()\n", + "\n", "# Extract time and field data\n", - "t_grid = run.time\n", - "grids_phy = run.grids_phy\n", - "e_field_phy = run.fields.em_fields.e_field_phy # dims (t, component, e1, e2, e3)\n", - "b_field_phy = run.fields.em_fields.b_field_phy\n", + "t_grid = sim.t_grid\n", + "grids_phy = sim.grids_phy\n", + "e_field_phy = sim.spline_values.em_fields.e_field_phy.data\n", + "b_field_phy = sim.spline_values.em_fields.b_field_phy.data\n", "\n", "# Extract coordinate arrays in the first (r-θ) plane\n", "X = grids_phy[0][:, :, 0] # Radial coordinate (Cartesian x for plotting)\n", @@ -676,9 +686,9 @@ "t_end = t_grid[-1]\n", "\n", "# Numerical fields (Cartesian components)\n", - "Ex_num = e_field_phy.isel(t=-1, component=0, e3=0).values\n", - "Ey_num = e_field_phy.isel(t=-1, component=1, e3=0).values\n", - "Bz_num = b_field_phy.isel(t=-1, component=2, e3=0).values\n", + "Ex_num = e_field_phy[t_end][0][:, :, 0]\n", + "Ey_num = e_field_phy[t_end][1][:, :, 0]\n", + "Bz_num = b_field_phy[t_end][2][:, :, 0]\n", "\n", "# Analytical fields\n", "Er_analytic = E_r_analytic(X, Y, grids_phy[0], m, t_end)\n", diff --git a/tutorials/tutorial_particle_tracing.ipynb b/tutorials/tutorial_particle_tracing.ipynb index b35342174..a793f94b2 100644 --- a/tutorials/tutorial_particle_tracing.ipynb +++ b/tutorials/tutorial_particle_tracing.ipynb @@ -321,8 +321,7 @@ "metadata": {}, "outputs": [], "source": [ - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()" + "sim.pproc()" ] }, { @@ -332,8 +331,27 @@ "metadata": {}, "outputs": [], "source": [ - "run_2 = sim_2.output.with_time_units(\"normalized\")\n", - "run_2.process()" + "sim_2.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "sim.load_plotting_data()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "sim_2.load_plotting_data()" ] }, { @@ -347,8 +365,8 @@ "\n", "fig = plt.figure(figsize=(10, 6))\n", "\n", - "orbits = run.orbits.kinetic_ions.values\n", - "orbits_uni = run_2.orbits.kinetic_ions.values\n", + "orbits = sim.orbits.kinetic_ions\n", + "orbits_uni = sim_2.orbits.kinetic_ions\n", "\n", "plt.subplot(1, 2, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -412,12 +430,12 @@ "model_3.kinetic_ions.var.add_background(background)\n", "\n", "sim_3.run()\n", - "run_3 = sim_3.output.with_time_units(\"normalized\")\n", - "run_3.process()\n", + "sim_3.pproc()\n", + "sim_3.load_plotting_data()\n", "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = run_3.orbits.kinetic_ions.values\n", + "orbits_standard = sim_3.orbits.kinetic_ions\n", "\n", "plt.subplot(1, 3, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -489,12 +507,12 @@ "model_3.kinetic_ions.var.add_background(background)\n", "\n", "sim_3.run()\n", - "run_3 = sim_3.output.with_time_units(\"normalized\")\n", - "run_3.process()\n", + "sim_3.pproc()\n", + "sim_3.load_plotting_data()\n", "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = run_3.orbits.kinetic_ions.values\n", + "orbits_standard = sim_3.orbits.kinetic_ions\n", "\n", "plt.subplot(1, 3, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -636,8 +654,17 @@ "metadata": {}, "outputs": [], "source": [ - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()" + "sim.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "sim.load_plotting_data()" ] }, { @@ -645,7 +672,7 @@ "id": "41", "metadata": {}, "source": [ - "Under `run.orbits.`, Struphy stores orbit data as an `xarray.DataArray` with dims `(t, marker, attribute)`; `.values` gives the 3D NumPy array:\n", + "Under `sim.orbits[]`, Struphy stores orbit data in a 3D NumPy array:\n", "\n", "- axis 0: time step,\n", "- axis 1: particle index,\n", @@ -661,7 +688,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = run.orbits.kinetic_ions.values\n", + "orbits = sim.orbits.kinetic_ions\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]\n", @@ -821,8 +848,17 @@ "metadata": {}, "outputs": [], "source": [ - "run_withB = sim_withB.output.with_time_units(\"normalized\")\n", - "run_withB.process()" + "sim_withB.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "sim_withB.load_plotting_data()" ] }, { @@ -832,7 +868,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = run_withB.orbits.kinetic_ions.values\n", + "orbits = sim_withB.orbits.kinetic_ions\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -1199,8 +1235,17 @@ "metadata": {}, "outputs": [], "source": [ - "run_asdex = sim_asdex.output.with_time_units(\"normalized\")\n", - "run_asdex.process()" + "sim_asdex.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "sim_asdex.load_plotting_data()" ] }, { @@ -1210,7 +1255,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = run_asdex.orbits.kinetic_ions.values\n", + "orbits = sim_asdex.orbits.kinetic_ions\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -1396,8 +1441,17 @@ "metadata": {}, "outputs": [], "source": [ - "run_gc = sim_gc.output.with_time_units(\"normalized\")\n", - "run_gc.process()" + "sim_gc.pproc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85", + "metadata": {}, + "outputs": [], + "source": [ + "sim_gc.load_plotting_data()" ] }, { @@ -1407,7 +1461,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = run_gc.orbits.kinetic_ions.values\n", + "orbits = sim_gc.orbits.kinetic_ions\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" diff --git a/tutorials/tutorial_poisson.ipynb b/tutorials/tutorial_poisson.ipynb index e55844251..506447def 100644 --- a/tutorials/tutorial_poisson.ipynb +++ b/tutorials/tutorial_poisson.ipynb @@ -136,8 +136,8 @@ "\n", "# For a stationary Poisson solve, one step is enough\n", "sim.run(one_time_step=True)\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()" + "sim.pproc()\n", + "sim.load_plotting_data()" ] }, { @@ -148,9 +148,10 @@ "outputs": [], "source": [ "# Extract 1D line data and compare to analytic solution\n", - "x = run.grids_phy[0][:, 0, 0]\n", + "x = sim.grids_phy[0][:, 0, 0]\n", "\n", - "phi_num = run.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0).values\n", + "t_last = max(sim.spline_values.em_fields.phi_log.data.keys())\n", + "phi_num = sim.spline_values.em_fields.phi_log.data[t_last][0][:, 0, 0]\n", "phi_ref = phi_exact(x, 0.0, 0.0)\n", "\n", "err = phi_num - phi_ref\n", @@ -281,8 +282,8 @@ " )\n", "\n", "sim2.run(one_time_step=True)\n", - "run2 = sim2.output.with_time_units(\"normalized\")\n", - "run2.process()" + "sim2.pproc()\n", + "sim2.load_plotting_data()" ] }, { @@ -293,10 +294,11 @@ "outputs": [], "source": [ "# 2D diagnostics and plots\n", - "X = run2.grids_phy[0][:, :, 0]\n", - "Y = run2.grids_phy[1][:, :, 0]\n", + "t2_last = max(sim2.spline_values.em_fields.phi_log.data.keys())\n", + "X = sim2.grids_phy[0][:, :, 0]\n", + "Y = sim2.grids_phy[1][:, :, 0]\n", "\n", - "phi2_num = run2.fields.em_fields.phi_log.isel(t=-1, e3=0).values\n", + "phi2_num = sim2.spline_values.em_fields.phi_log.data[t2_last][0][:, :, 0]\n", "phi2_ref = phi2_exact(X, Y, 0.0)\n", "err2 = phi2_num - phi2_ref\n", "err2_max = np.max(np.abs(err2))\n", @@ -416,8 +418,8 @@ " )\n", "\n", "sim3.run(one_time_step=True)\n", - "run3 = sim3.output.with_time_units(\"normalized\")\n", - "run3.process()" + "sim3.pproc()\n", + "sim3.load_plotting_data()" ] }, { @@ -428,10 +430,11 @@ "outputs": [], "source": [ "# Annulus diagnostics and plots in physical coordinates only\n", - "X3 = run3.grids_phy[0][:, :, 0]\n", - "Y3 = run3.grids_phy[1][:, :, 0]\n", + "t3_last = max(sim3.spline_values.em_fields.phi_log.data.keys())\n", + "X3 = sim3.grids_phy[0][:, :, 0]\n", + "Y3 = sim3.grids_phy[1][:, :, 0]\n", "\n", - "phi3_num = run3.fields.em_fields.phi_log.isel(t=-1, e3=0).values\n", + "phi3_num = sim3.spline_values.em_fields.phi_log.data[t3_last][0][:, :, 0]\n", "phi3_ref = phi3_exact(X3, Y3, 0.0)\n", "err3 = phi3_num - phi3_ref\n", "err3_max = np.max(np.abs(err3))\n", @@ -580,8 +583,8 @@ "\n", "# Run the full time-dependent simulation\n", "sim4.run()\n", - "run4 = sim4.output.with_time_units(\"normalized\")\n", - "run4.process()" + "sim4.pproc()\n", + "sim4.load_plotting_data()" ] }, { @@ -592,17 +595,17 @@ "outputs": [], "source": [ "# Extract and visualize time-dependent results\n", - "x4 = run4.grids_phy[0][:, 0, 0]\n", - "phi4 = run4.fields.em_fields.phi_log.isel(e2=0, e3=0) # dims (t, e1)\n", - "source4 = run4.fields.em_fields.source_log.isel(e2=0, e3=0)\n", - "t_times = phi4.t.values\n", + "x4 = sim4.grids_phy[0][:, 0, 0]\n", + "phi4_log = sim4.spline_values.em_fields.phi_log.data\n", + "source4_log = sim4.spline_values.em_fields.source_log.data\n", + "t_times = sorted(phi4_log.keys())\n", "\n", "print(f\"Solution saved at {len(t_times)} time points\")\n", "\n", "# Compute maximum error over all time steps\n", "err_max_global = 0.0\n", "for t in t_times:\n", - " phi_h = phi4.sel(t=t).values # Numerical solution\n", + " phi_h = phi4_log[t][0][:, 0, 0] # Numerical solution\n", " phi_e = phi4_exact(x4, t) # Exact solution\n", " err_local = np.max(np.abs(phi_h - phi_e))\n", " if err_local > err_max_global:\n", @@ -625,7 +628,7 @@ "for idx, t in enumerate(select_times):\n", " ax = axs_snaps[idx]\n", " \n", - " phi_num = phi4.sel(t=t).values\n", + " phi_num = phi4_log[t][0][:, 0, 0]\n", " phi_ref = phi4_exact(x4, t)\n", " \n", " ax.plot(x4, phi_ref, \"k--\", linewidth=2, label=\"Exact\")\n", @@ -651,7 +654,7 @@ "errors = []\n", "times_array = np.array(t_times)\n", "for t in t_times:\n", - " phi_h = phi4.sel(t=t).values\n", + " phi_h = phi4_log[t][0][:, 0, 0]\n", " phi_e = phi4_exact(x4, t)\n", " err = np.max(np.abs(phi_h - phi_e))\n", " errors.append(err)\n", @@ -664,9 +667,9 @@ "\n", "# Plot source and solution at a single point in space (center)\n", "center_idx = len(x4) // 2\n", - "phi_center = phi4.isel(e1=center_idx).values\n", + "phi_center = [phi4_log[t][0][center_idx, 0, 0] for t in t_times]\n", "phi_exact_center = [phi4_exact(x4[center_idx], t) for t in t_times]\n", - "source_center = source4.isel(e1=center_idx).values\n", + "source_center = [source4_log[t][0][center_idx, 0, 0] for t in t_times]\n", "source_exact_center = [rho4_exact(x4[center_idx], t) for t in t_times]\n", "\n", "ax2.plot(times_array, phi_exact_center, \"k--\", linewidth=2.5, label=\"$\\\\phi_{\\\\mathrm{exact}}$ at center\")\n", diff --git a/tutorials/tutorial_pressureless_sph_shock.ipynb b/tutorials/tutorial_pressureless_sph_shock.ipynb index 5770cffa8..5f9a3f8f5 100644 --- a/tutorials/tutorial_pressureless_sph_shock.ipynb +++ b/tutorials/tutorial_pressureless_sph_shock.ipynb @@ -504,9 +504,10 @@ "outputs": [], "source": [ "print(\"Post-processing...\")\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()\n", - "print(\"Post-processing complete.\")" + "sim.pproc()\n", + "print(\"Loading plotting data...\")\n", + "sim.load_plotting_data()\n", + "print(\"Data loaded.\")" ] }, { @@ -527,9 +528,9 @@ "outputs": [], "source": [ "# Extract binned outputs\n", - "rho_binned = run.distributions.cold_fluid.e1_density.f_binned.values\n", - "current1_binned = run.distributions.cold_fluid.e1_current_1.f_binned.values\n", - "t_grid = run.time\n", + "rho_binned = sim.f.cold_fluid.e1_density.f_binned\n", + "current1_binned = sim.f.cold_fluid.e1_current_1.f_binned\n", + "t_grid = sim.t_grid\n", "eta1_bins = np.linspace(0, 1, n_bins + 1)[:-1] # bin centers\n", "\n", "# Reconstruct velocity from binned current and density: u1 = j1 / rho\n", diff --git a/tutorials/tutorial_velocity_diffusion_sph.ipynb b/tutorials/tutorial_velocity_diffusion_sph.ipynb index f8fafee22..056b93324 100644 --- a/tutorials/tutorial_velocity_diffusion_sph.ipynb +++ b/tutorials/tutorial_velocity_diffusion_sph.ipynb @@ -248,9 +248,7 @@ "sim.run()\n", "print(\"Simulation complete.\")\n", "\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "\n", - "run.process()\n", + "sim.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -269,14 +267,13 @@ "metadata": {}, "outputs": [], "source": [ + "sim.load_plotting_data()\n", "\n", - "density = run.densities.euler_fluid.view_0.n_sph\n", - "\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", - "n_sph = density.values # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.values # shape (Nt+1, n_bins)\n", - "e1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.e1.values # logical x in [0, 1]\n", - "n_binned = run.distributions.euler_fluid.e1_density.f_binned.values # shape (Nt+1, n_bins)\n", + "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", + "n_sph = sim.n_sph.euler_fluid.view_0.n_sph # shape (Nt+1, plot_pts, 1, 1)\n", + "j1_binned = sim.f.euler_fluid.e1_current_1.f_binned # shape (Nt+1, n_bins)\n", + "e1_binned = sim.f.euler_fluid.e1_current_1.grid_e1 # logical x in [0, 1]\n", + "n_binned = sim.f.euler_fluid.e1_density.f_binned # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_viscous_euler_sph.ipynb b/tutorials/tutorial_viscous_euler_sph.ipynb index 5f5f77f10..6d070a35b 100644 --- a/tutorials/tutorial_viscous_euler_sph.ipynb +++ b/tutorials/tutorial_viscous_euler_sph.ipynb @@ -261,8 +261,7 @@ "print(\"Simulation complete.\")\n", "\n", "# Post-processing\n", - "run = sim.output.with_time_units(\"normalized\")\n", - "run.process()\n", + "sim.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -283,10 +282,12 @@ "metadata": {}, "outputs": [], "source": [ + "# Load plotting data\n", + "sim.load_plotting_data()\n", + "\n", "# Extract particle positions and density\n", - "density = run.densities.euler_fluid.view_0.n_sph\n", - "ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", - "n_sph = density.values\n", + "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", + "n_sph = sim.n_sph.euler_fluid.view_0.n_sph\n", "\n", "# Physical coordinates\n", "x = ee1 * r1\n", @@ -687,9 +688,7 @@ "sim_damp.run()\n", "print(\"Simulation complete.\")\n", "\n", - "run_damp = sim_damp.output.with_time_units(\"normalized\")\n", - "\n", - "run_damp.process()\n", + "sim_damp.pproc()\n", "print(\"Post-processing complete.\")" ] }, @@ -712,12 +711,12 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "density = run_damp.densities.euler_fluid.view_0.n_sph\n", + "sim_damp.load_plotting_data()\n", "\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing=\"ij\")\n", - "n_sph = density.values # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = run_damp.distributions.euler_fluid.e1_current_1.f_binned.values # shape (Nt+1, n_bins)\n", - "e1_binned = run_damp.distributions.euler_fluid.e1_current_1.f_binned.e1.values # logical x in [0,1]\n", + "ee1, ee2, ee3 = sim_damp.n_sph.euler_fluid.view_0.grid_n_sph\n", + "n_sph = sim_damp.n_sph.euler_fluid.view_0.n_sph # shape (Nt+1, plot_pts, 1, 1)\n", + "j1_binned = sim_damp.f.euler_fluid.e1_current_1.f_binned # shape (Nt+1, n_bins)\n", + "e1_binned = sim_damp.f.euler_fluid.e1_current_1.grid_e1 # logical x in [0,1]\n", "\n", "Nt = j1_binned.shape[0] - 1\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", From d87a64a9c973891c4bdf6d37bd226b9187f5a267 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 11:00:29 +0200 Subject: [PATCH 025/193] formatting --- .../cube_strong_scaling/params_poisson.py | 87 +++++--- src/struphy/diagnostics/plotting.py | 118 +++++++---- .../diagnostics/tests/test_plotting.py | 42 ++-- src/struphy/io/output_handling.py | 8 +- .../verification/test_verif_LinearMHD.py | 1 - src/struphy/post_processing/arrays.py | 89 +++++--- src/struphy/post_processing/output.py | 63 ++++-- .../post_processing/output_accessors.py | 191 ++++++++++++++---- .../post_processing/post_processing_tools.py | 17 +- .../post_processing/tests/test_arrays.py | 14 +- .../post_processing/tests/test_output.py | 10 +- src/struphy/simulation/sim.py | 3 +- src/struphy/simulation/tests/test_output.py | 23 ++- 13 files changed, 482 insertions(+), 184 deletions(-) diff --git a/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py b/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py index 22626741c..7d72a47bc 100644 --- a/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py +++ b/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py @@ -74,7 +74,7 @@ Lx = 2.0 Ly = 3.0 Lz = 4.0 -domain = domains.Cuboid(r1=Lx, l2=-Ly/2, r2=Ly/2, r3=Lz) +domain = domains.Cuboid(r1=Lx, l2=-Ly / 2, r2=Ly / 2, r3=Lz) # Fluid equilibrium (can be used as part of initial conditions) equil = None @@ -104,25 +104,31 @@ # ------------------ from struphy.linear_algebra.solver import SolverParameters + solver_params = SolverParameters(tol=1e-8, maxiter=3000, info=True, recycle=True) -model.propagators.poisson.options = model.propagators.poisson.Options(stab_eps=0.0, - solver="pcg", - precond=None, - solver_params=solver_params, - ) +model.propagators.poisson.options = model.propagators.poisson.Options( + stab_eps=0.0, + solver="pcg", + precond=None, + solver_params=solver_params, +) # ------------------ # Initial conditions # ------------------ import numpy as np + from struphy.initial.base import GenericPerturbation + def exact_solution(x, y, z): return np.sin(np.pi / Lx * x) * np.cos(12 * np.pi / Ly * y + 4 * np.pi / Lz * z) + def rhs_fun(x, y, z): return exact_solution(x, y, z) * ((np.pi / Lx) ** 2 + (12 * np.pi / Ly) ** 2 + (4 * np.pi / Lz) ** 2) + rhs_perturbation = GenericPerturbation(rhs_fun, given_in_basis="physical") model.em_fields.source.add_perturbation(rhs_perturbation) @@ -131,70 +137,94 @@ def rhs_fun(x, y, z): if __name__ == "__main__": run = sim.run(profiling_activated=True, one_time_step=True) run.process(create_vtk=True, parallel=True) - + def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): from matplotlib import pyplot as plt fig = plt.figure(figsize=(16, 12)) - + plt.subplot(3, 3, 1) plt.pcolor(y[slice_pt_x, :, :], z[slice_pt_x, :, :], num[slice_pt_x, :, :]) plt.colorbar() plt.xlabel("y") plt.ylabel("z") plt.title("{} from struphy, slice at x = {:.2f}".format(name, x[slice_pt_x, 0, 0])) - + plt.subplot(3, 3, 4) - plt.pcolor(y[slice_pt_x, :, :], z[slice_pt_x, :, :], exact(x[slice_pt_x, :, :], y[slice_pt_x, :, :], z[slice_pt_x, :, :])) + plt.pcolor( + y[slice_pt_x, :, :], + z[slice_pt_x, :, :], + exact(x[slice_pt_x, :, :], y[slice_pt_x, :, :], z[slice_pt_x, :, :]), + ) plt.colorbar() plt.xlabel("y") plt.ylabel("z") plt.title("{} exact, slice at x = {:.2f}".format(name, x[slice_pt_x, 0, 0])) - + plt.subplot(3, 3, 7) - plt.pcolor(y[slice_pt_x, :, :], z[slice_pt_x, :, :], np.abs(num[slice_pt_x, :, :] - exact(x[slice_pt_x, :, :], y[slice_pt_x, :, :], z[slice_pt_x, :, :]))) + plt.pcolor( + y[slice_pt_x, :, :], + z[slice_pt_x, :, :], + np.abs(num[slice_pt_x, :, :] - exact(x[slice_pt_x, :, :], y[slice_pt_x, :, :], z[slice_pt_x, :, :])), + ) plt.colorbar() plt.xlabel("y") plt.ylabel("z") plt.title("{} error, slice at x = {:.2f}".format(name, x[slice_pt_x, 0, 0])) - + plt.subplot(3, 3, 2) plt.pcolor(x[:, slice_pt_y, :], z[:, slice_pt_y, :], num[:, slice_pt_y, :]) plt.colorbar() plt.xlabel("x") plt.ylabel("z") plt.title("{} from struphy, slice at y = {:.2f}".format(name, y[0, slice_pt_y, 0])) - + plt.subplot(3, 3, 5) - plt.pcolor(x[:, slice_pt_y, :], z[:, slice_pt_y, :], exact(x[:, slice_pt_y, :], y[:, slice_pt_y, :], z[:, slice_pt_y, :])) + plt.pcolor( + x[:, slice_pt_y, :], + z[:, slice_pt_y, :], + exact(x[:, slice_pt_y, :], y[:, slice_pt_y, :], z[:, slice_pt_y, :]), + ) plt.colorbar() plt.xlabel("x") plt.ylabel("z") plt.title("{} exact, slice at y = {:.2f}".format(name, y[0, slice_pt_y, 0])) - + plt.subplot(3, 3, 8) - plt.pcolor(x[:, slice_pt_y, :], z[:, slice_pt_y, :], np.abs(num[:, slice_pt_y, :] - exact(x[:, slice_pt_y, :], y[:, slice_pt_y, :], z[:, slice_pt_y, :]))) + plt.pcolor( + x[:, slice_pt_y, :], + z[:, slice_pt_y, :], + np.abs(num[:, slice_pt_y, :] - exact(x[:, slice_pt_y, :], y[:, slice_pt_y, :], z[:, slice_pt_y, :])), + ) plt.colorbar() plt.xlabel("x") plt.ylabel("z") plt.title("{} error, slice at y = {:.2f}".format(name, y[0, slice_pt_y, 0])) - + plt.subplot(3, 3, 3) plt.pcolor(x[:, :, slice_pt_z], y[:, :, slice_pt_z], num[:, :, slice_pt_z]) plt.colorbar() plt.xlabel("x") plt.ylabel("y") plt.title("{} from struphy, slice at z = {:.2f}".format(name, z[0, 0, slice_pt_z])) - + plt.subplot(3, 3, 6) - plt.pcolor(x[:, :, slice_pt_z], y[:, :, slice_pt_z], exact(x[:, :, slice_pt_z], y[:, :, slice_pt_z], z[:, :, slice_pt_z])) + plt.pcolor( + x[:, :, slice_pt_z], + y[:, :, slice_pt_z], + exact(x[:, :, slice_pt_z], y[:, :, slice_pt_z], z[:, :, slice_pt_z]), + ) plt.colorbar() plt.xlabel("x") plt.ylabel("y") plt.title("{} exact, slice at z = {:.2f}".format(name, z[0, 0, slice_pt_z])) - + plt.subplot(3, 3, 9) - plt.pcolor(x[:, :, slice_pt_z], y[:, :, slice_pt_z], np.abs(num[:, :, slice_pt_z] - exact(x[:, :, slice_pt_z], y[:, :, slice_pt_z], z[:, :, slice_pt_z]))) + plt.pcolor( + x[:, :, slice_pt_z], + y[:, :, slice_pt_z], + np.abs(num[:, :, slice_pt_z] - exact(x[:, :, slice_pt_z], y[:, :, slice_pt_z], z[:, :, slice_pt_z])), + ) plt.colorbar() plt.xlabel("x") plt.ylabel("y") @@ -211,13 +241,15 @@ def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): print(phi_data) phi = phi_data.isel(t=-1).values x, y, z = run.grids_phy - + slice_pt_x = x.shape[0] // 2 slice_pt_y = y.shape[1] // 2 slice_pt_z = 0 - + fig_rhs = plot_slices(rhs, rhs_fun, "RHS", slice_pt_x=slice_pt_x, slice_pt_y=slice_pt_y, slice_pt_z=slice_pt_z) - fig_phi = plot_slices(phi, exact_solution, "Phi", slice_pt_x=slice_pt_x, slice_pt_y=slice_pt_y, slice_pt_z=slice_pt_z) + fig_phi = plot_slices( + phi, exact_solution, "Phi", slice_pt_x=slice_pt_x, slice_pt_y=slice_pt_y, slice_pt_z=slice_pt_z + ) rel_err_rhs = np.max(np.abs(rhs - rhs_fun(x, y, z))) / np.max(np.abs(rhs_fun(x, y, z))) rel_err_phi = np.max(np.abs(phi - exact_solution(x, y, z))) / np.max(np.abs(exact_solution(x, y, z))) @@ -226,9 +258,12 @@ def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): print(f"Max relative error in Phi: {rel_err_phi:.2e}") assert rel_err_rhs < 1e-3, f"The computed RHS does not match the exact RHS, max rel error = {rel_err_rhs}." - assert rel_err_phi < 1e-2, f"The computed solution does not match the exact solution, max rel error = {rel_err_phi}." + assert rel_err_phi < 1e-2, ( + f"The computed solution does not match the exact solution, max rel error = {rel_err_phi}." + ) import os + # `path_out` is the run's output folder; `sim_folder` alone is a bare name # resolved against the CWD. The profiling packaging picks these files up from # here and uploads them as `results-run`. diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 2d87ddb6b..54b9be3b5 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -29,8 +29,12 @@ logger = logging.getLogger("struphy") STRUPHY_STYLE = { - "figure.figsize": (8.0, 5.0), "figure.dpi": 110, "axes.grid": True, - "grid.alpha": 0.3, "axes.titlesize": "medium", "legend.frameon": False, + "figure.figsize": (8.0, 5.0), + "figure.dpi": 110, + "axes.grid": True, + "grid.alpha": 0.3, + "axes.titlesize": "medium", + "legend.frameon": False, "image.cmap": "viridis", } @@ -248,7 +252,11 @@ def _slice_data(data, view): if set(selected.dims) != {x, y}: raise ValueError(f"selection leaves dimensions {selected.dims}; expected only {x!r}, {y!r}") selected = selected.transpose(x, y) - grids = physical_grids(selected, plane=view.plane) if view.coordinates == "physical" else logical_grids(selected, x=x, y=y) + grids = ( + physical_grids(selected, plane=view.plane) + if view.coordinates == "physical" + else logical_grids(selected, x=x, y=y) + ) return selected, grids @@ -265,21 +273,30 @@ def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, t series = list(xr.align(*series, join="exact")) label_of = _label if len({item.attrs.get("run_name") for item in series}) > 1: + def label_of(item): - return " ".join(filter(None, (_label(item), f"({item.attrs['run_name']})" if item.attrs.get("run_name") else ""))) + return " ".join( + filter(None, (_label(item), f"({item.attrs['run_name']})" if item.attrs.get("run_name") else "")) + ) + run_label = shared_run_label(series) if run_label is None else run_label own_figure = ax is None with plt.rc_context(STRUPHY_STYLE): fig, ax = plt.subplots() if ax is None else (ax.figure, ax) artists, fits = [], [] for item in series: - line, = ax.plot(item.t, item, label=label_of(item) or None) + (line,) = ax.plot(item.t, item, label=label_of(item) or None) artists.append(line) result = growth_rate(item, fit) if fit is not None else None fits.append(result) if result is not None: - fitted, = ax.plot(result.time, result.fitted, "--", color=line.get_color(), - label=rf"fit: $\gamma$ = {result.rate:.4e}") + (fitted,) = ax.plot( + result.time, + result.fitted, + "--", + color=line.get_color(), + label=rf"fit: $\gamma$ = {result.rate:.4e}", + ) ax.axvspan(result.time[0], result.time[-1], alpha=0.12, color="grey") artists.append(fitted) if logy: @@ -293,8 +310,9 @@ def label_of(item): return PlotResult(fig, ax, artists, fits) -def plot_slice(data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, - equal_aspect=None, title=None, run_label=None): +def plot_slice( + data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, equal_aspect=None, title=None, run_label=None +): """Render one selected two-dimensional slice.""" view = view or View() run_label = shared_run_label(data) if run_label is None else run_label @@ -313,8 +331,7 @@ def plot_slice(data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, return PlotResult(fig, ax, [mesh]) -def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim=True, - title=None, run_label=None): +def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim=True, title=None, run_label=None): """Plot snapshots spread across a sweep coordinate.""" view = view or View() run_label = shared_run_label(data) if run_label is None else run_label @@ -327,8 +344,15 @@ def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim= if shared_clim: limits = (min(float(item.min()) for item in snapshots), max(float(item.max()) for item in snapshots)) with plt.rc_context(STRUPHY_STYLE): - fig, axes = plt.subplots(nrows, ncols, figsize=(3.5*ncols, 2.8*nrows), sharex=True, - sharey=True, squeeze=False, layout="constrained") + fig, axes = plt.subplots( + nrows, + ncols, + figsize=(3.5 * ncols, 2.8 * nrows), + sharex=True, + sharey=True, + squeeze=False, + layout="constrained", + ) meshes = [] for ax, index, snapshot in zip(axes.ravel(), indices, snapshots): local_view = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) @@ -339,9 +363,12 @@ def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim= ax.grid(False) if not shared_clim: fig.colorbar(mesh, ax=ax) - for ax in axes[-1]: ax.set_xlabel(xlabel) - for row in axes: row[0].set_ylabel(ylabel) - if shared_clim: fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) + for ax in axes[-1]: + ax.set_xlabel(xlabel) + for row in axes: + row[0].set_ylabel(ylabel) + if shared_clim: + fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) heading = title if title is not None else _label(data) fig.suptitle(" — ".join(filter(None, (heading, run_label)))) return PlotResult(fig, axes, meshes) @@ -382,21 +409,22 @@ def frame(): selected, (xg, yg, xlabel, ylabel) = _slice_data(selected, frame_view) with plt.rc_context(STRUPHY_STYLE): fig, ax = plt.subplots() - fig.subplots_adjust(bottom=0.13 + 0.05*len(controls)) + fig.subplots_adjust(bottom=0.13 + 0.05 * len(controls)) mesh = ax.pcolormesh(xg, yg, selected, shading="auto", vmin=self.vmin, vmax=self.vmax) colorbar = fig.colorbar(mesh, ax=ax, label=value_label(self.data)) ax.set(xlabel=xlabel, ylabel=ylabel) ax.grid(False) - if self.view.coordinates == "physical": ax.set_aspect("equal", adjustable="box") + if self.view.coordinates == "physical": + ax.set_aspect("equal", adjustable="box") state = {"mesh": mesh} def update(_=None): - for dim, slider in self.sliders.items(): indices[dim] = int(slider.val) + for dim, slider in self.sliders.items(): + indices[dim] = int(slider.val) item, item_view = frame() item, grids = _slice_data(item, item_view) state["mesh"].remove() - state["mesh"] = ax.pcolormesh(grids[0], grids[1], item, shading="auto", - vmin=self.vmin, vmax=self.vmax) + state["mesh"] = ax.pcolormesh(grids[0], grids[1], item, shading="auto", vmin=self.vmin, vmax=self.vmax) if self.vmin is None and self.vmax is None: state["mesh"].set_clim(float(item.min()), float(item.max())) colorbar.update_normal(state["mesh"]) @@ -405,8 +433,8 @@ def update(_=None): fig.canvas.draw_idle() for row, dim in enumerate(controls): - slider_ax = fig.add_axes([0.20, 0.05 + 0.05*row, 0.60, 0.025]) - slider = Slider(slider_ax, dim, 0, base.sizes[dim]-1, valstep=1) + slider_ax = fig.add_axes([0.20, 0.05 + 0.05 * row, 0.60, 0.025]) + slider = Slider(slider_ax, dim, 0, base.sizes[dim] - 1, valstep=1) slider.on_changed(update) self.sliders[dim] = slider update() @@ -418,6 +446,7 @@ def update(_=None): def animate_slices(data: xr.DataArray, *, view=None, interval=100, step=1, vmin=None, vmax=None): """Create an animation using the same :class:`View` as static slices.""" from matplotlib.animation import FuncAnimation + view = view or View() selected = _select(data, view) frames = range(0, selected.sizes[view.sweep], step) @@ -434,7 +463,8 @@ def update(index): item, item_grids = _slice_data(item, local) mesh.set_array(np.asarray(item).ravel()) ax.set_title(f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}") - return mesh, + return (mesh,) + return FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) @@ -448,38 +478,52 @@ def save_frames(data: xr.DataArray, directory, *, view=None, step=1, prefix="fra for frame, index in enumerate(range(0, selected.sizes[view.sweep], step)): item = selected.isel({view.sweep: index}) local = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) - result = plot_slice(item, view=local, - title=f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}") + result = plot_slice( + item, view=local, title=f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}" + ) path = directory / f"{prefix}_{frame:04d}.png" result.save(path, dpi=dpi, close=True) paths.append(str(path)) return paths -def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, - logy=False, run_label=None): +def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, logy=False, run_label=None): """Plot every scalar time series in one axes.""" selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: raise ValueError("no scalars to plot") + if not selected: + raise ValueError("no scalars to plot") run_label = shared_run_label([scalars[name] for name in selected]) if run_label is None else run_label fig, ax = plt.subplots(layout="constrained") for name in selected: values = scalars[name] / scalars[relative_to] if relative_to else scalars[name] ax.plot(values.t, values, label=name) - if logy: ax.set_yscale("log") + if logy: + ax.set_yscale("log") units = {scalars[name].attrs.get("units", "") for name in selected} ylabel = f"quantity / {relative_to}" if relative_to else (f"[{units.pop()}]" if len(units) == 1 else "[a.u.]") ax.set(xlabel=axis_label(scalars[selected[0]], "t"), ylabel=ylabel, title="Scalars") ax.legend(fontsize="small") - if run_label: fig.suptitle(run_label, fontsize="small") + if run_label: + fig.suptitle(run_label, fontsize="small") return PlotResult(fig, ax, list(ax.lines)) -def save_all_scalars(scalars, directory, *, names=None, exclude=SCALARS_EXCLUDE, logy=False, - run_label=None, table="csv", file_format="png", dpi=110): +def save_all_scalars( + scalars, + directory, + *, + names=None, + exclude=SCALARS_EXCLUDE, + logy=False, + run_label=None, + table="csv", + file_format="png", + dpi=110, +): """Write a table, scalar overview and one figure per scalar.""" selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: return [] + if not selected: + return [] directory = Path(directory) directory.mkdir(parents=True, exist_ok=True) paths = [] @@ -510,7 +554,7 @@ def plot_marker_trajectories(orbits: xr.DataArray, *, ax=None, max_markers=200, artists = [] if show_paths: for marker in range(count): - artists.extend(ax.plot(*positions[:, marker].T, lw=.8, alpha=.5)) + artists.extend(ax.plot(*positions[:, marker].T, lw=0.8, alpha=0.5)) artists.append(ax.scatter(*positions[-1].T, s=8)) ax.set(xlabel="X", ylabel="Y", zlabel="Z", title="Marker trajectories") return PlotResult(fig, ax, artists) @@ -523,14 +567,14 @@ def plot_equilibrium_profile(path_out, *, ax=None): equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) shape = equilibrium.dimensions grid = np.reshape(equilibrium.points, shape + (3,)) - radius = np.sqrt(grid[..., 0]**2 + grid[..., 1]**2) + radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2) pressure = np.reshape(equilibrium.point_data["p0"], shape) fig, ax = plt.subplots() if ax is None else (ax.figure, ax) ax.plot(radius[0, 0], pressure[0, 0], label=r"$p_0$") if "n0" in equilibrium.point_data: density = np.reshape(equilibrium.point_data["n0"], shape) ax.plot(radius[0, 0], density[0, 0], label=r"$n_0$") - ax.plot(radius[0, 0], pressure[0, 0]/density[0, 0], label=r"$T_0$") + ax.plot(radius[0, 0], pressure[0, 0] / density[0, 0], label=r"$T_0$") ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") ax.legend() return PlotResult(fig, ax, list(ax.lines)) diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index c145f9241..2e6d09361 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -37,9 +37,14 @@ def close_figures(): def phase_space(nt=6): - return data_array(np.arange(nt*4*5).reshape(nt, 4, 5), ("t", "e1", "v1"), - {"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, 4), - "v1": np.linspace(-2, 2, 5)}, name="f", label="$f$", coord_units={"t": "s"}) + return data_array( + np.arange(nt * 4 * 5).reshape(nt, 4, 5), + ("t", "e1", "v1"), + {"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, 4), "v1": np.linspace(-2, 2, 5)}, + name="f", + label="$f$", + coord_units={"t": "s"}, + ) def physical_field(): @@ -51,7 +56,7 @@ def physical_field(): def scalar_dataset(): t = np.linspace(0, 1, 6) - return xr.Dataset({"en_tot": ("t", 2 + .02*t), "en_e": ("t", 1 + .1*t)}, coords={"t": t}) + return xr.Dataset({"en_tot": ("t", 2 + 0.02 * t), "en_e": ("t", 1 + 0.1 * t)}, coords={"t": t}) def test_growth_rate_uses_only_valid_samples_inside_window(): @@ -68,15 +73,14 @@ def test_growth_rate_does_not_fall_back_outside_requested_window(): def test_growth_rate_of_quadratic_reports_amplitude_rate(): t = np.linspace(0, 4, 20) - result = growth_rate(data_array(np.exp(.6*t), ("t",), {"t": t}), - GrowthFit(amplitude_from_quadratic=True)) - assert result.rate == pytest.approx(.3) + result = growth_rate(data_array(np.exp(0.6 * t), ("t",), {"t": t}), GrowthFit(amplitude_from_quadratic=True)) + assert result.rate == pytest.approx(0.3) def test_diagnostics_preserve_time_coordinates(): data = data_array([2, 2.2, 1.8], ("t",), {"t": [0, 1, 2]}, label="E") - np.testing.assert_allclose(drift(data), [0, .2, -.2]) - np.testing.assert_allclose(relative_error(data), [.1, .1]) + np.testing.assert_allclose(drift(data), [0, 0.2, -0.2]) + np.testing.assert_allclose(relative_error(data), [0.1, 0.1]) np.testing.assert_array_equal(relative_error(data).t, [1, 2]) @@ -89,8 +93,7 @@ def test_logical_and_physical_grids_follow_selected_dimensions(): def test_plot_timeseries_renders_once_and_save_does_not_redraw(tmp_path): - data = data_array(np.exp(np.arange(4)), ("t",), {"t": range(4)}, label="energy", - coord_units={"t": "s"}) + data = data_array(np.exp(np.arange(4)), ("t",), {"t": range(4)}, label="energy", coord_units={"t": "s"}) result = plot_timeseries(data, fit=GrowthFit(), run_label="dt=.1") lines = len(result.ax.lines) result.save(tmp_path / "energy.png") @@ -100,14 +103,15 @@ def test_plot_timeseries_renders_once_and_save_does_not_redraw(tmp_path): def test_plot_slice_accepts_named_value_and_index_selection(): - result = plot_slice(phase_space(), view=View(x="e1", y="v1", select={"t": .52})) + result = plot_slice(phase_space(), view=View(x="e1", y="v1", select={"t": 0.52})) assert result.ax.get_xlabel() == r"$\eta_1$" assert len(result.artists) == 1 def test_plot_slice_physical_coordinates_are_intrinsic(): - result = plot_slice(physical_field(), view=View(x="e1", y="e2", isel={"t": 0, "e3": 2}, - coordinates="physical", plane="XY")) + result = plot_slice( + physical_field(), view=View(x="e1", y="e2", isel={"t": 0, "e3": 2}, coordinates="physical", plane="XY") + ) assert result.ax.get_xlabel() == "X" assert result.ax.get_aspect() == 1.0 @@ -118,8 +122,9 @@ def test_plot_slice_rejects_underspecified_selection(): def test_panels_use_one_recipe_and_keep_full_title(): - result = plot_panels(phase_space(), view=View(x="e1", y="v1"), nrows=1, ncols=2, - title="Distribution", run_label="dt=.1") + result = plot_panels( + phase_space(), view=View(x="e1", y="v1"), nrows=1, ncols=2, title="Distribution", run_label="dt=.1" + ) assert result.fig._suptitle.get_text() == "Distribution — dt=.1" assert len(result.artists) == 2 @@ -148,7 +153,10 @@ def test_scalar_overview_and_export(tmp_path): assert result.fig._suptitle.get_text() == "run" paths = save_all_scalars(scalar_dataset(), tmp_path) assert sorted(__import__("os").path.basename(path) for path in paths) == [ - "en_e.png", "en_tot.png", "scalars.csv", "scalars.png" + "en_e.png", + "en_tot.png", + "scalars.csv", + "scalars.png", ] assert plt.get_fignums() == [result.fig.number] diff --git a/src/struphy/io/output_handling.py b/src/struphy/io/output_handling.py index 278457021..3b03fc4bd 100644 --- a/src/struphy/io/output_handling.py +++ b/src/struphy/io/output_handling.py @@ -56,9 +56,11 @@ def __init__(self, path_out, file_name=None, comm=None): with h5py.File(self.file_path, "a") as file: file.visit( - lambda key: dataset_keys.append(key) - if isinstance(file[key], h5py.Dataset) and file[key].chunks is not None - else None, + lambda key: ( + dataset_keys.append(key) + if isinstance(file[key], h5py.Dataset) and file[key].chunks is not None + else None + ), ) for key in dataset_keys: diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 854c305c4..4b18fe891 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -81,7 +81,6 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - # first fft Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index be4518fe5..1210790ce 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -12,18 +12,37 @@ logger = logging.getLogger("struphy") DIM_LABELS = { - "t": r"$t$", "e1": r"$\eta_1$", "e2": r"$\eta_2$", "e3": r"$\eta_3$", - "v1": r"$v_1$", "v2": r"$v_2$", "v3": r"$v_3$", - "x": r"$x$", "y": r"$y$", "z": r"$z$", "R": r"$R$", "Z": r"$Z$", - "component": "component", "marker": "marker", "attribute": "attribute", + "t": r"$t$", + "e1": r"$\eta_1$", + "e2": r"$\eta_2$", + "e3": r"$\eta_3$", + "v1": r"$v_1$", + "v2": r"$v_2$", + "v3": r"$v_3$", + "x": r"$x$", + "y": r"$y$", + "z": r"$z$", + "R": r"$R$", + "Z": r"$Z$", + "component": "component", + "marker": "marker", + "attribute": "attribute", } BINNED_LABELS = {"f_binned": "$f$", "delta_f_binned": r"$\delta f$", "n_sph": "$n$"} SCALARS_EXCLUDE = ("time",) -def data_array(values, dims: Sequence[str], coords: Mapping | None = None, *, name: str | None = None, - label: str = "", unit: str = "", coord_units: Mapping[str, str] | None = None, - attrs: Mapping | None = None) -> xr.DataArray: +def data_array( + values, + dims: Sequence[str], + coords: Mapping | None = None, + *, + name: str | None = None, + label: str = "", + unit: str = "", + coord_units: Mapping[str, str] | None = None, + attrs: Mapping | None = None, +) -> xr.DataArray: """Construct a consistently annotated :class:`xarray.DataArray`.""" metadata = dict(attrs or {}) metadata.update(label=label, units=unit) @@ -105,8 +124,7 @@ def save_scalars(scalars: xr.Dataset | Mapping, path: str, *, names=None, exclud if fmt == "npz": np.savez(path, t=time, **{name: values[:, i] for i, name in enumerate(selected)}) else: - np.savetxt(path, np.column_stack((time, values)), delimiter=",", - header=",".join(("t", *selected)), comments="") + np.savetxt(path, np.column_stack((time, values)), delimiter=",", header=",".join(("t", *selected)), comments="") logger.info("Wrote %d scalars over %d time steps to %s", len(selected), len(time), path) return path @@ -125,15 +143,26 @@ def orbit_columns(n_columns: int) -> dict: def wrap_orbits(values, time, *, time_unit="") -> xr.DataArray: """Label marker orbits with time, marker and attribute dimensions.""" values = np.asarray(values) - return data_array(values, ("t", "marker", "attribute"), - {"t": time, "marker": np.arange(values.shape[1]), - "attribute": np.arange(values.shape[2])}, - name="orbits", label="marker orbits", coord_units={"t": time_unit}, - attrs={"columns": orbit_columns(values.shape[-1])}) - - -def wrap_field_data(values_by_time: Mapping, grids_log=None, *, grids_phy=None, name: str = "", - time_scale: float = 1.0, time_unit: str = "") -> xr.DataArray | None: + return data_array( + values, + ("t", "marker", "attribute"), + {"t": time, "marker": np.arange(values.shape[1]), "attribute": np.arange(values.shape[2])}, + name="orbits", + label="marker orbits", + coord_units={"t": time_unit}, + attrs={"columns": orbit_columns(values.shape[-1])}, + ) + + +def wrap_field_data( + values_by_time: Mapping, + grids_log=None, + *, + grids_phy=None, + name: str = "", + time_scale: float = 1.0, + time_unit: str = "", +) -> xr.DataArray | None: """Stack one field product and attach logical and physical coordinates.""" times = sorted(values_by_time) if not times: @@ -141,8 +170,14 @@ def wrap_field_data(values_by_time: Mapping, grids_log=None, *, grids_phy=None, first = values_by_time[times[0]] scalar = not isinstance(first, (list, tuple)) or len(first) == 1 if scalar: - values = np.stack([np.asarray(values_by_time[t] if not isinstance(values_by_time[t], (list, tuple)) - else values_by_time[t][0]) for t in times]) + values = np.stack( + [ + np.asarray( + values_by_time[t] if not isinstance(values_by_time[t], (list, tuple)) else values_by_time[t][0] + ) + for t in times + ] + ) dims = ("t", "e1", "e2", "e3") else: values = np.stack([np.stack([np.asarray(c) for c in values_by_time[t]]) for t in times]) @@ -162,9 +197,13 @@ def wrap_field_data(values_by_time: Mapping, grids_log=None, *, grids_phy=None, return data_array(values, dims, coords, name=name or None, label=name, coord_units={"t": time_unit}) -def wrap_binned_data(values, dims: Sequence[str], coords: Mapping, *, name: str, - time_unit: str = "") -> xr.DataArray: +def wrap_binned_data(values, dims: Sequence[str], coords: Mapping, *, name: str, time_unit: str = "") -> xr.DataArray: """Label a memory-mapped binned distribution or density product.""" - return data_array(values, ("t", *dims), coords, name=name, - label=BINNED_LABELS.get(name, name.replace("_", " ")), - coord_units={"t": time_unit}) + return data_array( + values, + ("t", *dims), + coords, + name=name, + label=BINNED_LABELS.get(name, name.replace("_", " ")), + coord_units={"t": time_unit}, + ) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 7c516ade1..e0e745011 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -71,7 +71,7 @@ def __getitem__(self, key): def __iter__(self): prefix = f"{self._prefix}/" if self._prefix else "" - children = {key[len(prefix):].split("/", 1)[0] for key in self._mapping if key.startswith(prefix)} + children = {key[len(prefix) :].split("/", 1)[0] for key in self._mapping if key.startswith(prefix)} return iter(sorted(children)) def __len__(self): @@ -156,8 +156,13 @@ def __getitem__(self, name: str) -> xr.DataArray: for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): if name in catalog: return catalog[name] - available = (*self.scalars.data_vars, *self.field_catalog, *self.distribution_catalog, - *self.density_catalog, *self.orbit_catalog) + available = ( + *self.scalars.data_vars, + *self.field_catalog, + *self.distribution_catalog, + *self.density_catalog, + *self.orbit_catalog, + ) raise KeyError(f"{name!r} not found; available products: {available}") def _stamp(self, array: xr.DataArray) -> xr.DataArray: @@ -227,8 +232,15 @@ def process( """ from struphy.post_processing.post_processing_tools import PostProcessor - options = dict(step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, - classify=classify, create_vtk=create_vtk, force=force) + options = dict( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + force=force, + ) sim = self.sim if parallel: PostProcessor(sim, parallel_pproc=True).process(**options) @@ -244,8 +256,10 @@ def _ensure_processed(self): return if self.sim.comm_size > 1: raise RuntimeError(f"{self.path_out} has no post-processed data; call out.process() on all ranks first") - logger.warning("\nNo post-processed data in %s, processing with default options " - "(call out.process(...) to choose them)", self.path_out) + logger.warning( + "\nNo post-processed data in %s, processing with default options (call out.process(...) to choose them)", + self.path_out, + ) self.process() def _product_mappings(self) -> dict[str, ProductMapping]: @@ -386,9 +400,16 @@ def scalars(self) -> xr.Dataset: time = np.asarray(file["time/value"]) * self.time_scale variables = {} for name, dataset in file["scalar"].items(): - variables[name] = self._stamp(data_array(np.asarray(dataset), ("t",), {"t": time}, name=name, - label=name.replace("_", " "), - coord_units={"t": self.time_unit})) + variables[name] = self._stamp( + data_array( + np.asarray(dataset), + ("t",), + {"t": time}, + name=name, + label=name.replace("_", " "), + coord_units={"t": self.time_unit}, + ) + ) self._scalars = xr.Dataset(variables) return self._scalars @@ -402,10 +423,12 @@ def label(self) -> str: self._label = self.path_out.name return self._label values = [] - for holder, attr, name in ((sim.time_opts, "dt", "dt"), - (sim.time_opts, "split_algo", "algo"), - (sim.grid, "num_elements", "Nel"), - (sim.derham_opts, "degree", "p")): + for holder, attr, name in ( + (sim.time_opts, "dt", "dt"), + (sim.time_opts, "split_algo", "algo"), + (sim.grid, "num_elements", "Nel"), + (sim.derham_opts, "degree", "p"), + ): value = getattr(holder, attr, None) if holder is not None else None if value is not None: values.append(f"{name}={value}") @@ -442,8 +465,14 @@ def _load_field(self, path: Path, key: str): physical = self.grids_phy except FileNotFoundError: physical = None - return wrap_field_data(raw, self.grids_log, grids_phy=physical, name=key.split("/")[-1], - time_scale=self.time_scale, time_unit=self.time_unit) + return wrap_field_data( + raw, + self.grids_log, + grids_phy=physical, + name=key.split("/")[-1], + time_scale=self.time_scale, + time_unit=self.time_unit, + ) def _discover_binned(self, category: str): loaders = {} @@ -497,7 +526,7 @@ def _load_orbits(self, directory: Path): raise FileNotFoundError(f"no orbit arrays in {directory}") # one small file per saved step: read them instead of keeping thousands of memory maps open values = np.stack([np.load(path) for path in paths]) - return wrap_orbits(values, self.time[:len(paths)], time_unit=self.time_unit) + return wrap_orbits(values, self.time[: len(paths)], time_unit=self.time_unit) def open_output(path_out, *, time_units: str = "physical") -> Output: diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index fbbf93992..51a2c4518 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -42,8 +42,9 @@ def _label(self, arrays) -> str: def _view(x, y, sweep, coords, plane, select, isel): from struphy.diagnostics.plotting import View - return View(x=x, y=y, sweep=sweep, select=dict(select or {}), isel=dict(isel or {}), - coordinates=coords, plane=plane) + return View( + x=x, y=y, sweep=sweep, select=dict(select or {}), isel=dict(isel or {}), coordinates=coords, plane=plane + ) def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): """Overview of the scalar time series in one axes. @@ -59,11 +60,19 @@ def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = Fa """ from struphy.diagnostics.plotting import plot_scalars - return plot_scalars(self._output.scalars, names=names, relative_to=relative_to, logy=logy, - run_label=self._output.label) - - def timeseries(self, *data, logy: bool = True, fit: tuple[float | None, float | None] | bool | None = None, - fit_amplitude: bool = False, title: str | None = None, ax=None): + return plot_scalars( + self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label + ) + + def timeseries( + self, + *data, + logy: bool = True, + fit: tuple[float | None, float | None] | bool | None = None, + fit_amplitude: bool = False, + title: str | None = None, + ax=None, + ): """One or more time series, optionally with an exponential growth-rate fit. Parameters @@ -93,9 +102,22 @@ def timeseries(self, *data, logy: bool = True, fit: tuple[float | None, float | growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) return plot_timeseries(series, ax=ax, logy=logy, fit=growth, title=title, run_label=self._label(series)) - def slice(self, data, *, x: str | None = None, y: str | None = None, coords: Coordinates = "logical", - plane: Plane = "XY", select: dict | None = None, isel: dict | None = None, vmin=None, vmax=None, - equal_aspect: bool | None = None, title: str | None = None, ax=None): + def slice( + self, + data, + *, + x: str | None = None, + y: str | None = None, + coords: Coordinates = "logical", + plane: Plane = "XY", + select: dict | None = None, + isel: dict | None = None, + vmin=None, + vmax=None, + equal_aspect: bool | None = None, + title: str | None = None, + ax=None, + ): """A two-dimensional color plot of one slice. Parameters @@ -112,23 +134,61 @@ def slice(self, data, *, x: str | None = None, y: str | None = None, coords: Coo from struphy.diagnostics.plotting import plot_slice array = self._array(data) - return plot_slice(array, view=self._view(x, y, "t", coords, plane, select, isel), ax=ax, vmin=vmin, - vmax=vmax, equal_aspect=equal_aspect, title=title, run_label=self._label([array])) - - def panels(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, - isel: dict | None = None, nrows: int = 3, ncols: int = 4, shared_clim: bool = True, - title: str | None = None): + return plot_slice( + array, + view=self._view(x, y, "t", coords, plane, select, isel), + ax=ax, + vmin=vmin, + vmax=vmax, + equal_aspect=equal_aspect, + title=title, + run_label=self._label([array]), + ) + + def panels( + self, + data, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + select: dict | None = None, + isel: dict | None = None, + nrows: int = 3, + ncols: int = 4, + shared_clim: bool = True, + title: str | None = None, + ): """Snapshots evenly spread along ``sweep`` (time by default), one panel each.""" from struphy.diagnostics.plotting import plot_panels array = self._array(data) - return plot_panels(array, view=self._view(x, y, sweep, coords, plane, select, isel), nrows=nrows, - ncols=ncols, shared_clim=shared_clim, title=title, run_label=self._label([array])) - - def viewer(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, - isel: dict | None = None, vmin=None, vmax=None): + return plot_panels( + array, + view=self._view(x, y, sweep, coords, plane, select, isel), + nrows=nrows, + ncols=ncols, + shared_clim=shared_clim, + title=title, + run_label=self._label([array]), + ) + + def viewer( + self, + data, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + select: dict | None = None, + isel: dict | None = None, + vmin=None, + vmax=None, + ): """An interactive slice viewer with one slider per non-displayed dimension. Call ``.show()`` on the result; keep it alive so that the sliders stay connected. @@ -136,26 +196,69 @@ def viewer(self, data, *, x: str | None = None, y: str | None = None, sweep: str from struphy.diagnostics.plotting import InteractiveSliceViewer array = self._array(data) - return InteractiveSliceViewer(array, view=self._view(x, y, sweep, coords, plane, select, isel), vmin=vmin, - vmax=vmax, run_label=self._label([array])) - - def animation(self, data, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, - isel: dict | None = None, interval: int = 100, step: int = 1, vmin=None, vmax=None): + return InteractiveSliceViewer( + array, + view=self._view(x, y, sweep, coords, plane, select, isel), + vmin=vmin, + vmax=vmax, + run_label=self._label([array]), + ) + + def animation( + self, + data, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + select: dict | None = None, + isel: dict | None = None, + interval: int = 100, + step: int = 1, + vmin=None, + vmax=None, + ): """A Matplotlib animation along ``sweep``.""" from struphy.diagnostics.plotting import animate_slices - return animate_slices(self._array(data), view=self._view(x, y, sweep, coords, plane, select, isel), - interval=interval, step=step, vmin=vmin, vmax=vmax) - - def frames(self, data, directory, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", select: dict | None = None, - isel: dict | None = None, step: int = 1, prefix: str = "frame", dpi: int = 110) -> list[str]: + return animate_slices( + self._array(data), + view=self._view(x, y, sweep, coords, plane, select, isel), + interval=interval, + step=step, + vmin=vmin, + vmax=vmax, + ) + + def frames( + self, + data, + directory, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + select: dict | None = None, + isel: dict | None = None, + step: int = 1, + prefix: str = "frame", + dpi: int = 110, + ) -> list[str]: """Write the slices along ``sweep`` as numbered PNG files; returns their paths.""" from struphy.diagnostics.plotting import save_frames - return save_frames(self._array(data), directory, view=self._view(x, y, sweep, coords, plane, select, isel), - step=step, prefix=prefix, dpi=dpi) + return save_frames( + self._array(data), + directory, + view=self._view(x, y, sweep, coords, plane, select, isel), + step=step, + prefix=prefix, + dpi=dpi, + ) def orbits(self, species: str | None = None, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): """Three-dimensional trajectories of the saved markers of ``species``.""" @@ -166,8 +269,9 @@ def orbits(self, species: str | None = None, *, max_markers: int = 200, show_pat if len(available) != 1: raise ValueError(f"choose a species from {available}") species = available[0] - return plot_marker_trajectories(self._output.orbits[species], ax=ax, max_markers=max_markers, - show_paths=show_paths) + return plot_marker_trajectories( + self._output.orbits[species], ax=ax, max_markers=max_markers, show_paths=show_paths + ) def equilibrium(self, ax=None): """Radial equilibrium profiles, from the geometry written at the start of the run.""" @@ -208,8 +312,9 @@ def relative_error(self, data, *, ref=None, skip_first: bool = True) -> xr.DataA return relative_error(self._array(data), ref=ref, skip_first=skip_first) - def dispersion(self, field, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, - **kwargs): + def dispersion( + self, field, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs + ): """Space-time power spectrum of a field and fitted dispersion branches. The spectrum is computed in normalized time. See @@ -219,7 +324,9 @@ def dispersion(self, field, *, component: int = 0, slice_at: tuple = (None, 0, 0 from struphy.diagnostics.diagn_tools import power_spectrum_2d if isinstance(field, str): - run = self._output if self._output.time_units == "normalized" else self._output.with_time_units("normalized") + run = ( + self._output if self._output.time_units == "normalized" else self._output.with_time_units("normalized") + ) field = run[field] elif field.t.attrs.get("units") == "s": raise ValueError("pass the field by name, or take it from out.with_time_units('normalized')") diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 950eed6a6..ea941f9d5 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -160,7 +160,8 @@ def _write_manifest(self, status, *, options=None, error=None): manifest["products"] = sorted( os.path.relpath(os.path.join(root, name), self.path_pproc) for root, _, files in os.walk(self.path_pproc) - for name in files if name != "manifest.json" + for name in files + if name != "manifest.json" ) path = os.path.join(self.path_pproc, "manifest.json") temporary = path + ".tmp" @@ -213,8 +214,14 @@ def process( bool Whether post-processing actually ran. """ - options = normalize_options(step=step, celldivide=celldivide, physical=physical, - guiding_center=guiding_center, classify=classify, create_vtk=create_vtk) + options = normalize_options( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + ) if not force and is_processed(self.path_out, options): logger.warning(f"\nReusing existing post-processing in {self.path_pproc}") return False @@ -424,7 +431,9 @@ def process_particles( if guiding_center: assert self.kinetic_kinds[n] == "Particles6D" - orbits_tools.post_process_orbit_guiding_center(self.domain, self.equil, path_kinetics_species, species) + orbits_tools.post_process_orbit_guiding_center( + self.domain, self.equil, path_kinetics_species, species + ) if classify: orbits_tools.post_process_orbit_classification(path_kinetics_species, species) diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index 6c9d7ef6f..62285a75e 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -18,8 +18,15 @@ def test_data_array_carries_names_coordinates_and_units(): - data = data_array(np.ones((3, 4)), ("t", "e1"), {"t": [0, 1, 2], "e1": np.arange(4)}, - name="density", label="$n$", unit="m^-3", coord_units={"t": "s"}) + data = data_array( + np.ones((3, 4)), + ("t", "e1"), + {"t": [0, 1, 2], "e1": np.arange(4)}, + name="density", + label="$n$", + unit="m^-3", + coord_units={"t": "s"}, + ) assert isinstance(data, xr.DataArray) assert data.sel(t=1).dims == ("e1",) assert axis_label(data, "t") == "$t$ [s]" @@ -62,8 +69,7 @@ def test_vector_field_has_named_component_dimension(): def test_binned_wrapper_keeps_memory_mappable_values(): values = np.ones((2, 3, 4)) - data = wrap_binned_data(values, ("e1", "v1"), {"t": [0, 1], "e1": range(3), "v1": range(4)}, - name="f_binned") + data = wrap_binned_data(values, ("e1", "v1"), {"t": [0, 1], "e1": range(3), "v1": range(4)}, name="f_binned") assert data.dims == ("t", "e1", "v1") assert data.attrs["label"] == "$f$" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 1ddc009af..01333a021 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -8,8 +8,8 @@ import numpy as np import pytest -from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint from struphy.post_processing.output import Output, open_output +from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 @@ -210,8 +210,12 @@ def process(self, **options): assert run.process(physical=True) is run expected = [ ("construct", False), - ("process", dict(step=1, celldivide=1, physical=True, guiding_center=False, classify=False, - create_vtk=False, force=False)), + ( + "process", + dict( + step=1, celldivide=1, physical=True, guiding_center=False, classify=False, create_vtk=False, force=False + ), + ), ] assert calls == (expected if rank == 0 else []) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 41908f9af..64c451cb6 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1389,7 +1389,8 @@ def _binned_background(background, bin_plot) -> xp.ndarray: binned are integrated out like the binned data (exact for Maxwellians). """ centers = { - dim: edges[:-1] + (edges[1] - edges[0]) / 2 for dim, edges in zip(bin_plot.slice.split("_"), bin_plot.bin_edges) + dim: edges[:-1] + (edges[1] - edges[0]) / 2 + for dim, edges in zip(bin_plot.slice.split("_"), bin_plot.bin_edges) } grids = [centers.get(dim, xp.zeros(1)) for dim in ("e1", "e2", "e3")] factor = 1.0 diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 45c5cb52e..dc0e41fd1 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -82,8 +82,16 @@ def test_deprecated_pproc_delegates_to_the_output(tmp_path, monkeypatch): with pytest.deprecated_call(): assert sim.pproc(physical=True) is None assert calls == [ - dict(step=1, celldivide=1, physical=True, guiding_center=False, classify=False, - create_vtk=True, parallel=False, force=True) + dict( + step=1, + celldivide=1, + physical=True, + guiding_center=False, + classify=False, + create_vtk=True, + parallel=False, + force=True, + ) ] with pytest.deprecated_call(): assert sim.pproc(load=True) == "loaded" @@ -92,8 +100,15 @@ def test_deprecated_pproc_delegates_to_the_output(tmp_path, monkeypatch): def test_deprecated_load_plotting_data_attaches_the_products(tmp_path, monkeypatch): sim = make_sim(tmp_path) output = sim.output - for name, value in (("orbits", "o"), ("distributions", "f"), ("fields", "s"), ("densities", "n"), - ("grids_log", "gl"), ("grids_phy", "gp"), ("time", "t")): + for name, value in ( + ("orbits", "o"), + ("distributions", "f"), + ("fields", "s"), + ("densities", "n"), + ("grids_log", "gl"), + ("grids_phy", "gp"), + ("time", "t"), + ): monkeypatch.setattr(type(output), name, property(lambda self, value=value: value)) with pytest.deprecated_call(): From c577b2ec3104b51b551027647e741acf320f4bfc Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 11:40:05 +0200 Subject: [PATCH 026/193] added remaining plots to tutorial --- src/struphy/diagnostics/diagn_tools.py | 34 ++-- src/struphy/diagnostics/plotting.py | 16 +- tutorials/tutorial_post_processing.ipynb | 198 +++++++++++++++++++---- 3 files changed, 196 insertions(+), 52 deletions(-) diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 357291507..c32e9f769 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -59,7 +59,8 @@ def power_spectrum_2d( Plot result if True, otherwise return things. disp_name : str - The name of the dispersion relation class in struphy.dispersion_relations.analytic to be used for analytic comparison. + The name of the dispersion relation class in struphy.dispersion_relations.analytic to be used for analytic + comparison. If None, only the computed spectrum is drawn. disp_params : dict Parameters needed for analytical dispersion relation, see struphy.dispersion_relations.analytic. @@ -202,24 +203,19 @@ def fun(k): ax.plot(kvec, fun(kvec), "r:", label=f"fit_{n + 1}") - # analytic solution: - disp_class = getattr(analytic, disp_name) - disp = disp_class(**disp_params) - - kpara = kvec - - branches = disp(kpara) - set_min = 0.0 - set_max = 0.0 - for key, branch in branches.items(): - vals = xp.real(branch) - ax.plot(kvec, vals, "--", label=key) - tmp = xp.min(vals) - if tmp < set_min: - set_min = tmp - tmp = xp.max(vals) - if tmp > set_max: - set_max = tmp + # analytic solution, when a dispersion relation is given + set_min = set_max = 0.0 + if disp_name is not None: + disp = getattr(analytic, disp_name)(**disp_params) + + branches = disp(kvec) + for key, branch in branches.items(): + vals = xp.real(branch) + ax.plot(kvec, vals, "--", label=key) + set_min = min(set_min, xp.min(vals)) + set_max = max(set_max, xp.max(vals)) + else: + set_min, set_max = 0.0, omega[-1] ax.legend() ax.set_xlim(0, kvec[-1]) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 54b9be3b5..50dfa4708 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -110,6 +110,14 @@ def _ipython_display_(self): _display_figure(self.fig) +def _detach_figure(fig): + """Take a figure out of pyplot under the inline backend, which would show it as a still image.""" + import matplotlib + + if "inline" in matplotlib.get_backend(): + plt.close(fig) + + def _display_figure(fig): """Display a figure as a notebook cell result, exactly once. @@ -261,7 +269,7 @@ def _slice_data(data, view): def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=None): - """Plot one or more aligned time series; series of different runs are labeled by run.""" + """Plot one or more time series, each on its own time grid; series of different runs are labeled by run.""" series = _items(data) if not series: raise ValueError("at least one time series is required") @@ -269,8 +277,6 @@ def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, t validate_array(item, required_dims=("t",)) if item.dims != ("t",): raise ValueError(f"time series must have dims ('t',), got {item.dims}") - if len(series) > 1: - series = list(xr.align(*series, join="exact")) label_of = _label if len({item.attrs.get("run_name") for item in series}) > 1: @@ -465,7 +471,9 @@ def update(index): ax.set_title(f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}") return (mesh,) - return FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) + animation = FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) + _detach_figure(fig) + return animation def save_frames(data: xr.DataArray, directory, *, view=None, step=1, prefix="frame", dpi=110): diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 300934f92..50e7a10c8 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -22,6 +22,8 @@ "import os\n", "import tempfile\n", "\n", + "from IPython.display import HTML\n", + "\n", "from struphy import (\n", " BinningPlot,\n", " BoundaryParameters,\n", @@ -34,6 +36,7 @@ " Time,\n", " WeightsParameters,\n", " domains,\n", + " equils,\n", " grids,\n", " maxwellians,\n", " perturbations,\n", @@ -58,32 +61,38 @@ "metadata": {}, "outputs": [], "source": [ - "model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", - "model.em_fields.e_field.save_data = True\n", - "model.em_fields.phi.save_data = True\n", - "model.kinetic_ions.var.save_data = True\n", + "def build_model():\n", + " model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", + " model.em_fields.e_field.save_data = True\n", + " model.em_fields.phi.save_data = True\n", + " model.kinetic_ions.var.save_data = True\n", "\n", - "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", - "model.propagators.coupling_va.options = model.propagators.coupling_va.Options()\n", - "model.initial_poisson.options = model.initial_poisson.Options(stab_mat=\"M0\")\n", + " model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + " model.propagators.coupling_va.options = model.propagators.coupling_va.Options()\n", + " model.initial_poisson.options = model.initial_poisson.Options(stab_mat=\"M0\")\n", + "\n", + " binplot = BinningPlot(\n", + " slice=\"e1_v1\",\n", + " n_bins=(32, 32),\n", + " ranges=((0.0, 1.0), (-5.0, 5.0)),\n", + " )\n", + " model.kinetic_ions.set_markers(\n", + " loading_params=LoadingParameters(ppc=32, seed=1234),\n", + " weights_params=WeightsParameters(control_variate=True),\n", + " boundary_params=BoundaryParameters(),\n", + " sorting_params=SortingParameters(boxes_per_dim=(4, 1, 1), do_sort=True),\n", + " saving_params=SavingParameters(n_markers=12, binning_plots=(binplot,)),\n", + " )\n", + "\n", + " background = maxwellians.Maxwellian3D(n=(1.0, None))\n", + " model.kinetic_ions.var.add_background(background)\n", + " density_mode = perturbations.ModesCos(ls=(1,), amps=(1e-3,))\n", + " model.kinetic_ions.var.add_initial_condition(maxwellians.Maxwellian3D(n=(1.0, density_mode)))\n", + "\n", + " return model\n", "\n", - "binplot = BinningPlot(\n", - " slice=\"e1_v1\",\n", - " n_bins=(32, 32),\n", - " ranges=((0.0, 1.0), (-5.0, 5.0)),\n", - ")\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=LoadingParameters(ppc=32, seed=1234),\n", - " weights_params=WeightsParameters(control_variate=True),\n", - " boundary_params=BoundaryParameters(),\n", - " sorting_params=SortingParameters(boxes_per_dim=(4, 1, 1), do_sort=True),\n", - " saving_params=SavingParameters(n_markers=12, binning_plots=(binplot,)),\n", - ")\n", "\n", - "background = maxwellians.Maxwellian3D(n=(1.0, None))\n", - "model.kinetic_ions.var.add_background(background)\n", - "density_mode = perturbations.ModesCos(ls=(1,), amps=(1e-3,))\n", - "model.kinetic_ions.var.add_initial_condition(maxwellians.Maxwellian3D(n=(1.0, density_mode)))" + "model = build_model()" ] }, { @@ -104,9 +113,10 @@ "sim = Simulation(\n", " model=model,\n", " env=env,\n", - " time_opts=Time(dt=0.1, Tend=0.4),\n", + " time_opts=Time(dt=0.05, Tend=2.0),\n", " domain=domains.Cuboid(r1=2 * 3.141592653589793),\n", - " grid=grids.TensorProductGrid(num_elements=(8, 1, 1)),\n", + " equil=equils.HomogenSlab(),\n", + " grid=grids.TensorProductGrid(num_elements=(16, 1, 1)),\n", " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", ")\n", "out = sim.run()\n", @@ -193,7 +203,7 @@ "metadata": {}, "outputs": [], "source": [ - "t_fit = 0.4 * out.sim.model.units.t # in seconds, like every time coordinate of this run\n", + "t_fit = 2.0 * out.sim.model.units.t # in seconds, like every time coordinate of this run\n", "energy_plot = out.plot.timeseries(\n", " \"electric_energy\",\n", " fit=(0.0, t_fit),\n", @@ -299,7 +309,102 @@ }, { "cell_type": "markdown", - "id": "20", + "metadata": {}, + "source": [ + "`out.plot.animation()` and `out.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." + ], + "id": "20" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "animation = out.plot.animation(phase_space, x=\"e1\", y=\"v1\", step=4)\n", + "HTML(animation.to_jshtml())" + ], + "execution_count": null, + "outputs": [], + "id": "21" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "frames = out.plot.frames(phase_space, \"frames\", x=\"e1\", y=\"v1\", step=10)\n", + "print(\"Wrote:\", [os.path.basename(path) for path in frames])" + ], + "execution_count": null, + "outputs": [], + "id": "22" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles." + ], + "id": "23" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "out.plot.equilibrium()" + ], + "execution_count": null, + "outputs": [], + "id": "24" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Derived quantities\n", + "\n", + "`out.analysis` computes without drawing, and every result is an array that the plots accept. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation." + ], + "id": "25" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "energy_error = out.analysis.relative_error(\"total_energy\")\n", + "energy_drift = out.analysis.drift(\"total_energy\")\n", + "print(f\"largest drift of the total energy: {abs(energy_drift).max().item():.3e}\")\n", + "\n", + "out.plot.timeseries(energy_error, title=\"Conservation of the total energy\")" + ], + "execution_count": null, + "outputs": [], + "id": "26" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`out.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." + ], + "id": "27" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "omega, kvec, spectrum, _ = out.analysis.dispersion(\n", + " \"em_fields/e_field_log\",\n", + " slice_at=(None, 0, 0),\n", + " do_plot=True,\n", + ")\n", + "print(\"spectrum:\", spectrum.shape)" + ], + "execution_count": null, + "outputs": [], + "id": "28" + }, + { + "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ "## Save standard output\n", @@ -310,7 +415,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -322,7 +427,42 @@ }, { "cell_type": "markdown", - "id": "22", + "metadata": {}, + "source": [ + "## Comparing runs\n", + "\n", + "Every plot accepts arrays of other simulations, so comparing runs needs nothing special. Series are labelled by the run they come from, and the runs may have different time grids." + ], + "id": "31" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "sim_coarse = Simulation(\n", + " model=build_model(),\n", + " env=EnvironmentOptions(out_folders=demo_root, sim_folder=\"vlasov_ampere_coarse\", save_restart=False),\n", + " time_opts=Time(dt=0.1, Tend=2.0),\n", + " domain=domains.Cuboid(r1=2 * 3.141592653589793),\n", + " equil=equils.HomogenSlab(),\n", + " grid=grids.TensorProductGrid(num_elements=(16, 1, 1)),\n", + " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", + ")\n", + "out_coarse = sim_coarse.run()\n", + "\n", + "out.plot.timeseries(\n", + " out[\"electric_energy\"],\n", + " out_coarse[\"electric_energy\"],\n", + " title=\"Electric energy: dt = 0.05 against dt = 0.1\",\n", + ")" + ], + "execution_count": null, + "outputs": [], + "id": "32" + }, + { + "cell_type": "markdown", + "id": "33", "metadata": {}, "source": [ "## Apply the workflow to another run\n", From 56a07790da9db3b210ec3c0a94ebf1ea2a76f1ad Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 11:44:20 +0200 Subject: [PATCH 027/193] Extend tutorial to show all the plots --- src/struphy/diagnostics/plotting.py | 4 +- .../diagnostics/tests/test_plotting.py | 8 + tutorials/tutorial_post_processing.ipynb | 187 +++++++++++++++++- 3 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 50dfa4708..622248b7e 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -249,8 +249,8 @@ def physical_grids(data: xr.DataArray, *, plane="XY"): def _slice_data(data, view): selected = _select(data, view) - if view.sweep in selected.dims: - raise ValueError(f"select one {view.sweep!r} value before drawing a static slice") + if view.sweep in selected.dims and view.sweep not in (view.x, view.y): + raise ValueError(f"select one {view.sweep!r} value before drawing a static slice, or display it as x or y") if view.x is None or view.y is None: if selected.ndim != 2: raise ValueError(f"view.x and view.y are required for remaining dims {selected.dims}") diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 2e6d09361..76f017eac 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -175,3 +175,11 @@ def test_notebook_display_shows_the_figure_once(monkeypatch, shown): result._ipython_display_() assert displayed == ([] if shown else [result.fig]) assert (result.fig.number in plt.get_fignums()) == shown, "the inline backend must not show it again" + + +def test_slice_can_display_the_sweep_dimension(): + data = phase_space() + result = plot_slice(data.isel(v1=slice(None)), view=View(x="t", y="e1", isel={"v1": 0})) + assert result.ax.get_xlabel() == "$t$ [s]" + with pytest.raises(ValueError, match="display it as x or y"): + plot_slice(data, view=View(x="e1", y="v1")) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 50e7a10c8..a1ca060c3 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -27,8 +27,10 @@ "from struphy import (\n", " BinningPlot,\n", " BoundaryParameters,\n", + " ButcherTableau,\n", " DerhamOptions,\n", " EnvironmentOptions,\n", + " KernelDensityPlot,\n", " LoadingParameters,\n", " SavingParameters,\n", " Simulation,\n", @@ -41,7 +43,7 @@ " maxwellians,\n", " perturbations,\n", ")\n", - "from struphy.models import VlasovAmpereOneSpecies" + "from struphy.models import Maxwell, ViscousEulerSPH, VlasovAmpereOneSpecies" ] }, { @@ -462,7 +464,188 @@ }, { "cell_type": "markdown", - "id": "33", + "metadata": {}, + "source": [ + "## Other models\n", + "\n", + "The interface is the same for every model; only the products differ. Two more short runs show the two product types the Vlasov–Ampère demo does not have: SPH densities, and vector fields on a mapped domain." + ], + "id": "33" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### SPH densities\n", + "\n", + "A standing sound wave discretized with SPH markers. `KernelDensityPlot` reconstructs the density on a grid, which appears under `out.densities`, while `BinningPlot` produces the binned quantities under `out.distributions`." + ], + "id": "34" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "sph_model = ViscousEulerSPH(with_B0=False, with_viscosity=False)\n", + "sph_model.propagators.push_eta.options = sph_model.propagators.push_eta.Options(\n", + " butcher=ButcherTableau(algo=\"forward_euler\"),\n", + ")\n", + "sph_model.propagators.push_sph_p.options = sph_model.propagators.push_sph_p.Options(kernel_type=\"gaussian_1d\")\n", + "sph_model.euler_fluid.set_markers(\n", + " loading_params=LoadingParameters(ppb=8, loading=\"tesselation\"),\n", + " weights_params=WeightsParameters(),\n", + " boundary_params=BoundaryParameters(),\n", + " sorting_params=SortingParameters(boxes_per_dim=(12, 1, 1), dims_mask=(True, False, False)),\n", + " saving_params=SavingParameters(\n", + " binning_plots=(BinningPlot(slice=\"e1\", n_bins=(32,), ranges=(0.0, 1.0)),),\n", + " kernel_density_plots=(KernelDensityPlot(pts_e1=41, pts_e2=1),),\n", + " ),\n", + ")\n", + "sph_model.euler_fluid.var.add_background(equils.ConstantVelocity())\n", + "sph_model.euler_fluid.var.add_perturbation(del_n=perturbations.ModesSin(ls=(1,), amps=(1.0e-2,)))\n", + "\n", + "sph = Simulation(\n", + " model=sph_model,\n", + " env=EnvironmentOptions(out_folders=demo_root, sim_folder=\"sph_soundwave\", save_restart=False),\n", + " time_opts=Time(dt=0.03125, Tend=2.5, split_algo=\"Strang\"),\n", + " domain=domains.Cuboid(r1=2.5),\n", + " grid=None,\n", + " derham_opts=None,\n", + ")\n", + "out_sph = sph.run()\n", + "\n", + "print(\"densities:\", tuple(out_sph.density_catalog))\n", + "print(\"binned:\", tuple(out_sph.distribution_catalog))" + ], + "execution_count": null, + "outputs": [], + "id": "35" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For a one-dimensional run, the clearest picture is a space-time map: the sweep dimension `t` may be used as a display axis." + ], + "id": "36" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "out_sph.plot.slice(\n", + " \"euler_fluid/view_0/n_sph\",\n", + " x=\"t\",\n", + " y=\"e1\",\n", + " isel={\"e2\": 0, \"e3\": 0},\n", + " title=\"SPH density of the sound wave\",\n", + ")" + ], + "execution_count": null, + "outputs": [], + "id": "37" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" + ], + "id": "38" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "density = out_sph[\"euler_fluid/view_0/n_sph\"].isel(e2=0, e3=0)\n", + "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" + ], + "execution_count": null, + "outputs": [], + "id": "39" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Vector fields on a mapped domain\n", + "\n", + "A coaxial waveguide mode of the Maxwell model, on an annulus. With `physical=True` the post-processing also computes the Cartesian field components (`*_phy`), and `coords=\"physical\"` draws them on the mapped grid, with the plane chosen by `plane`." + ], + "id": "40" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "a1, a2 = 2.326744, 3.686839\n", + "\n", + "maxwell_model = Maxwell()\n", + "maxwell_model.propagators.maxwell.options = maxwell_model.propagators.maxwell.Options(algo=\"implicit\")\n", + "maxwell_model.em_fields.e_field.add_perturbation(perturbations.CoaxialWaveguideElectric_r(m=3, a1=a1, a2=a2))\n", + "maxwell_model.em_fields.e_field.add_perturbation(perturbations.CoaxialWaveguideElectric_theta(m=3, a1=a1, a2=a2))\n", + "maxwell_model.em_fields.b_field.add_perturbation(perturbations.CoaxialWaveguideMagnetic(m=3, a1=a1, a2=a2))\n", + "\n", + "coaxial = Simulation(\n", + " model=maxwell_model,\n", + " env=EnvironmentOptions(out_folders=demo_root, sim_folder=\"coaxial\", save_restart=False),\n", + " time_opts=Time(dt=0.05, Tend=2.0),\n", + " domain=domains.HollowCylinder(a1=a1, a2=a2, Lz=2.0),\n", + " equil=equils.HomogenSlab(),\n", + " grid=grids.TensorProductGrid(num_elements=(24, 48, 1)),\n", + " derham_opts=DerhamOptions(degree=(2, 2, 1), bcs=((\"dirichlet\", \"dirichlet\"), None, None)),\n", + ")\n", + "out_coaxial = coaxial.run()\n", + "out_coaxial.process(physical=True)\n", + "\n", + "print(\"fields:\", tuple(out_coaxial.field_catalog))\n", + "print(\"dimensions:\", out_coaxial[\"em_fields/b_field_phy\"].dims)" + ], + "execution_count": null, + "outputs": [], + "id": "41" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "out_coaxial.plot.slice(\n", + " \"em_fields/b_field_phy\",\n", + " x=\"e1\",\n", + " y=\"e2\",\n", + " isel={\"t\": -1, \"component\": 2, \"e3\": 0},\n", + " coords=\"physical\",\n", + " plane=\"XY\",\n", + " title=\"$B_z$ of the coaxial mode\",\n", + ")" + ], + "execution_count": null, + "outputs": [], + "id": "42" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "out_coaxial.plot.panels(\n", + " \"em_fields/b_field_phy\",\n", + " x=\"e1\",\n", + " y=\"e2\",\n", + " isel={\"component\": 2, \"e3\": 0},\n", + " coords=\"physical\",\n", + " plane=\"XY\",\n", + " nrows=1,\n", + " ncols=4,\n", + " title=\"$B_z$ over time\",\n", + ")" + ], + "execution_count": null, + "outputs": [], + "id": "43" + }, + { + "cell_type": "markdown", + "id": "44", "metadata": {}, "source": [ "## Apply the workflow to another run\n", From e25b221085965c9b9925eec648fd8545626503c0 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 11:48:59 +0200 Subject: [PATCH 028/193] Remove the isel term --- .claude/skills/setup-simulation/SKILL.md | 2 +- doc/sections/userguide.rst | 4 +- .../post_processing/output_accessors.py | 60 +++++++++++-------- .../tests/test_output_accessors.py | 20 ++++++- tutorials/tutorial_post_processing.ipynb | 14 +++-- 5 files changed, 66 insertions(+), 34 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 155042646..9b380bcc3 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -142,7 +142,7 @@ out.sim.model.units # the Simulation, restore ``` Plots and analysis need no imports: `out.plot.scalars()`, `out.plot.timeseries("", fit=(t0, t1))`, -`out.plot.panels("//f_binned", x="e1", y="v1")`, `out.plot.viewer(...)`, +`out.plot.panels("//f_binned", x="e1", y="v1")` (name any other dimension to select it, e.g. `t="last"`, `component=0`), `out.plot.viewer(...)`, `out.plot.orbits("")`, `out.save_report()`, `out.analysis.growth_rate(...)`, `out.analysis.dispersion(...)`. Names are looked up with `out[""]`. See `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index e0f1ceb3e..f059c3f85 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -569,7 +569,7 @@ run), and figures are titled with the run's numerical parameters: out.plot.scalars() # every scalar time series out.plot.timeseries("en_phi", fit=(0.0, 40.0)) # exponential fit in a time window - out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", isel={"t": -1}) + out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", t="last") out.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4) out.plot.viewer("em_fields/phi_phy", x="e1", y="e2", coords="physical").show() out.plot.orbits("kinetic_ions") @@ -620,7 +620,7 @@ Binned particle data is grouped by species and the slice defined in .. code-block:: python f = out.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) - out.plot.slice(f.isel(t=-1), x="e1", y="v1").show() + out.plot.slice(f, x="e1", y="v1", t="last").show() Plotting particle orbits diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index 51a2c4518..fba314c91 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Literal +import numpy as np import xarray as xr if TYPE_CHECKING: @@ -38,13 +39,24 @@ def _label(self, arrays) -> str: runs = {array.attrs.get("run") for array in arrays} - {None, ""} return shared_run_label(arrays) if runs else self._output.label - @staticmethod - def _view(x, y, sweep, coords, plane, select, isel): + def _view(self, array, x, y, sweep, coords, plane, selection): from struphy.diagnostics.plotting import View - return View( - x=x, y=y, sweep=sweep, select=dict(select or {}), isel=dict(isel or {}), coordinates=coords, plane=plane - ) + select, index = {}, {} + for dim, value in selection.items(): + if dim not in array.dims: + raise TypeError(f"{dim!r} is not a dimension of {array.name!r}; its dimensions are {array.dims}") + if value == "first": + index[dim] = 0 + elif value == "last": + index[dim] = -1 + elif isinstance(value, (bool, str)): + raise TypeError(f"cannot select {dim}={value!r}; use a number, or \"first\"/\"last\"") + elif isinstance(value, (int, np.integer)): + index[dim] = int(value) + else: + select[dim] = float(value) + return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): """Overview of the scalar time series in one axes. @@ -110,13 +122,12 @@ def slice( y: str | None = None, coords: Coordinates = "logical", plane: Plane = "XY", - select: dict | None = None, - isel: dict | None = None, vmin=None, vmax=None, equal_aspect: bool | None = None, title: str | None = None, ax=None, + **selection, ): """A two-dimensional color plot of one slice. @@ -136,7 +147,7 @@ def slice( array = self._array(data) return plot_slice( array, - view=self._view(x, y, "t", coords, plane, select, isel), + view=self._view(array, x, y, "t", coords, plane, selection), ax=ax, vmin=vmin, vmax=vmax, @@ -154,12 +165,11 @@ def panels( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", - select: dict | None = None, - isel: dict | None = None, nrows: int = 3, ncols: int = 4, shared_clim: bool = True, title: str | None = None, + **selection, ): """Snapshots evenly spread along ``sweep`` (time by default), one panel each.""" from struphy.diagnostics.plotting import plot_panels @@ -167,7 +177,7 @@ def panels( array = self._array(data) return plot_panels( array, - view=self._view(x, y, sweep, coords, plane, select, isel), + view=self._view(array, x, y, sweep, coords, plane, selection), nrows=nrows, ncols=ncols, shared_clim=shared_clim, @@ -184,10 +194,9 @@ def viewer( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", - select: dict | None = None, - isel: dict | None = None, vmin=None, vmax=None, + **selection, ): """An interactive slice viewer with one slider per non-displayed dimension. @@ -198,7 +207,7 @@ def viewer( array = self._array(data) return InteractiveSliceViewer( array, - view=self._view(x, y, sweep, coords, plane, select, isel), + view=self._view(array, x, y, sweep, coords, plane, selection), vmin=vmin, vmax=vmax, run_label=self._label([array]), @@ -213,19 +222,19 @@ def animation( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", - select: dict | None = None, - isel: dict | None = None, interval: int = 100, step: int = 1, vmin=None, vmax=None, + **selection, ): - """A Matplotlib animation along ``sweep``.""" + """A Matplotlib animation along ``sweep``, taking the same arguments as :meth:`slice`.""" from struphy.diagnostics.plotting import animate_slices + array = self._array(data) return animate_slices( - self._array(data), - view=self._view(x, y, sweep, coords, plane, select, isel), + array, + view=self._view(array, x, y, sweep, coords, plane, selection), interval=interval, step=step, vmin=vmin, @@ -242,19 +251,22 @@ def frames( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", - select: dict | None = None, - isel: dict | None = None, step: int = 1, prefix: str = "frame", dpi: int = 110, + **selection, ) -> list[str]: - """Write the slices along ``sweep`` as numbered PNG files; returns their paths.""" + """Write the slices along ``sweep`` as numbered PNG files; returns their paths. + + Takes the same arguments as :meth:`slice`. + """ from struphy.diagnostics.plotting import save_frames + array = self._array(data) return save_frames( - self._array(data), + array, directory, - view=self._view(x, y, sweep, coords, plane, select, isel), + view=self._view(array, x, y, sweep, coords, plane, selection), step=step, prefix=prefix, dpi=dpi, diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index a60d4bb1d..3014d149f 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -84,9 +84,9 @@ def test_scalar_overview_draws_every_scalar_in_one_axes(run): def test_slices_panels_and_viewer_take_keyword_views(run): name = "kinetic_ions/e1_v1_density/f_binned" - assert run.plot.slice(name, x="e1", y="v1", isel={"t": -1}).ax.get_xlabel() == r"$\eta_1$" + assert run.plot.slice(name, x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" assert len(run.plot.panels(name, x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - viewer = run.plot.viewer("em_fields/E", x="e1", y="e2", isel={"component": 0}) + viewer = run.plot.viewer("em_fields/E", x="e1", y="e2", component=0) viewer.draw() assert set(viewer.sliders) == {"t", "e3"} @@ -113,3 +113,19 @@ def test_dispersion_rejects_fields_in_seconds(run): physical._sim.model = type("Model", (), {"units": type("Units", (), {"t": 2.0})()})() with pytest.raises(ValueError, match="normalized"): physical.analysis.dispersion(physical.fields.em_fields.E) + + +def test_selection_keywords_take_positions_values_and_ends(run): + name = "kinetic_ions/e1_v1_density/f_binned" + times = run[name].t.values + + by_position = run.plot.slice(name, x="e1", y="v1", t=-1) + by_value = run.plot.slice(name, x="e1", y="v1", t=float(times[-1])) + by_end = run.plot.slice(name, x="e1", y="v1", t="last") + for result in (by_value, by_end): + np.testing.assert_allclose(result.artists[0].get_array(), by_position.artists[0].get_array()) + + with pytest.raises(TypeError, match="not a dimension"): + run.plot.slice(name, x="e1", y="v1", time=-1) + with pytest.raises(TypeError, match='use a number'): + run.plot.slice(name, x="e1", y="v1", t="final") diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index a1ca060c3..4c62ad13a 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -225,7 +225,7 @@ "source": [ "## Two-dimensional data\n", "\n", - "Choose the displayed dimensions with `x` and `y`, and fix all others with `isel` (by index) or `select` (by nearest coordinate value). Arrays can also be sliced beforehand with xarray's `.isel()` and `.sel()`. `coords=\"physical\"` draws on the mapped coordinates instead of the logical ones." + "Choose the displayed dimensions with `x` and `y`, and pick one value for every other dimension by naming it: `t=\"last\"` (or `\"first\"`), `t=-1` for a position, and `t=0.35` for the nearest coordinate value. Arrays can also be sliced beforehand with xarray's `.isel()` and `.sel()`. `coords=\"physical\"` draws on the mapped coordinates instead of the logical ones." ] }, { @@ -239,7 +239,7 @@ " phase_space,\n", " x=\"e1\",\n", " y=\"v1\",\n", - " isel={\"t\": -1},\n", + " t=\"last\",\n", " equal_aspect=False,\n", " title=\"Final phase-space distribution\",\n", ")" @@ -537,7 +537,8 @@ " \"euler_fluid/view_0/n_sph\",\n", " x=\"t\",\n", " y=\"e1\",\n", - " isel={\"e2\": 0, \"e3\": 0},\n", + " e2=0,\n", + " e3=0,\n", " title=\"SPH density of the sound wave\",\n", ")" ], @@ -613,7 +614,9 @@ " \"em_fields/b_field_phy\",\n", " x=\"e1\",\n", " y=\"e2\",\n", - " isel={\"t\": -1, \"component\": 2, \"e3\": 0},\n", + " t=\"last\",\n", + " component=2,\n", + " e3=0,\n", " coords=\"physical\",\n", " plane=\"XY\",\n", " title=\"$B_z$ of the coaxial mode\",\n", @@ -631,7 +634,8 @@ " \"em_fields/b_field_phy\",\n", " x=\"e1\",\n", " y=\"e2\",\n", - " isel={\"component\": 2, \"e3\": 0},\n", + " component=2,\n", + " e3=0,\n", " coords=\"physical\",\n", " plane=\"XY\",\n", " nrows=1,\n", From 7e9d0ba1bbec6725c429c3c03087da07676c1a1e Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 12:05:40 +0200 Subject: [PATCH 029/193] Updated accessors --- .claude/skills/setup-simulation/SKILL.md | 16 +- doc/sections/userguide.rst | 29 +- src/struphy/diagnostics/plotting.py | 3 +- src/struphy/post_processing/output.py | 32 ++ .../post_processing/output_accessors.py | 345 ++++-------------- .../tests/test_output_accessors.py | 32 ++ .../post_processing/xarray_accessors.py | 196 ++++++++++ tutorials/tutorial_post_processing.ipynb | 61 ++-- 8 files changed, 387 insertions(+), 327 deletions(-) create mode 100644 src/struphy/post_processing/xarray_accessors.py diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 9b380bcc3..e958c19ea 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -141,11 +141,17 @@ out.orbits. # dims (t, marker, attrib out.sim.model.units # the Simulation, restored without allocating ``` -Plots and analysis need no imports: `out.plot.scalars()`, `out.plot.timeseries("", fit=(t0, t1))`, -`out.plot.panels("//f_binned", x="e1", y="v1")` (name any other dimension to select it, e.g. `t="last"`, `component=0`), `out.plot.viewer(...)`, -`out.plot.orbits("")`, `out.save_report()`, `out.analysis.growth_rate(...)`, -`out.analysis.dispersion(...)`. Names are looked up with `out[""]`. See -`examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. +Products sit under their species and plot themselves, no imports needed: + +```python +out...f_binned.struphy.slice(x="e1", y="v1", t="last") # also .panels/.viewer/.animation/.frames +out..orbits.struphy.trajectories() +out.scalars..struphy.timeseries(fit=(t0, t1)) # also .growth_rate/.drift/.relative_error +out.plot.scalars(), out.plot.equilibrium(), out.save_report() # plots of the whole run +``` + +Name any dimension to select it: `t="last"`, `t=-1` (position), `t=0.35` (nearest value), `component=0`. +See `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. ## Common pitfalls diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index f059c3f85..8f69d0a1e 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -561,21 +561,28 @@ serial processing runs on rank 0 while the other ranks wait, and Standard plots and analysis: ``out.plot`` and ``out.analysis`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The common diagnostics are methods of the run, so no further imports are needed. They -accept a product name, ``out[""]``, or any array (sliced, derived, or from another -run), and figures are titled with the run's numerical parameters: +Products sit under the species that produced them and plot themselves through their +``.struphy`` accessor, so no imports are needed and everything completes as you type. The same +methods are on ``out.plot`` and ``out.analysis``, which also take a product name: .. code-block:: python + # products plot themselves + f = out.kinetic_ions.e1_v1_density.f_binned + f.struphy.slice(x="e1", y="v1", t="last") + f.struphy.panels(x="e1", y="v1", nrows=3, ncols=4) + f.struphy.viewer(x="e1", y="v1").show() + out.em_fields.phi_phy.struphy.slice(x="e1", y="e2", t="last", coords="physical") + out.kinetic_ions.orbits.struphy.trajectories() + out.scalars.en_phi.struphy.timeseries(fit=(0.0, 40.0)) # exponential fit in a window + out.scalars.en_phi.struphy.growth_rate(window=(0.0, 40.0)).rate + + # plots of the whole run out.plot.scalars() # every scalar time series - out.plot.timeseries("en_phi", fit=(0.0, 40.0)) # exponential fit in a time window - out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", t="last") - out.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4) - out.plot.viewer("em_fields/phi_phy", x="e1", y="e2", coords="physical").show() - out.plot.orbits("kinetic_ions") out.save_report() # table + figures in post_processing/report/ - out.analysis.growth_rate("en_phi", window=(0.0, 40.0)).rate + # every method is also on out.plot / out.analysis, which take a product name + out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", t="last") out.analysis.dispersion("em_fields/e_field_log", slice_at=(0, 0, None), fit_branches=1) Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of @@ -583,7 +590,7 @@ several runs are labeled by run: .. code-block:: python - out_a.plot.timeseries(out_a["en_phi"], out_b["en_phi"], fit=(0.0, 40.0)) + out_a.scalars.en_phi.struphy.timeseries(out_b.scalars.en_phi, fit=(0.0, 40.0)) The sections below access the arrays directly for custom Matplotlib plots. @@ -620,7 +627,7 @@ Binned particle data is grouped by species and the slice defined in .. code-block:: python f = out.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) - out.plot.slice(f, x="e1", y="v1", t="last").show() + f.struphy.slice(x="e1", y="v1", t="last").show() Plotting particle orbits diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 622248b7e..4446448f1 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -216,7 +216,8 @@ def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataA if np.any(np.asarray(reference) == 0): raise ValueError("cannot take a relative error against a reference of zero") out = abs(data - reference) / abs(reference) - out.attrs = {"label": f"relative error of {_label(data)}".strip(), "units": ""} + out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + out.attrs.update(label=f"relative error of {_label(data)}".strip(), units="") return out.isel(t=slice(1, None)) if skip_first else out diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index e0e745011..f4356d213 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -275,8 +275,40 @@ def _product_mappings(self) -> dict[str, ProductMapping]: kind: ProductMapping({key: (lambda load=load: self._stamp(load())) for key, load in loaders.items()}) for kind, loaders in discovered.items() } + shadowed = {key.split("/")[0] for loaders in discovered.values() for key in loaders} & set(dir(type(self))) + if shadowed: + logger.warning("species %s shadow attributes of Output; reach them as out.fields, " + "out.distributions, out.densities or out.orbits", sorted(shadowed)) return self._products + @property + def species_catalog(self) -> ProductMapping: + """Every product of every species, keyed ``/``; see :meth:`__getattr__`.""" + catalogs = (self.field_catalog, self.distribution_catalog, self.density_catalog) + loaders = {key: (lambda catalog=catalog, key=key: catalog[key]) + for catalog in catalogs for key in catalog} + loaders.update({f"{species}/orbits": (lambda species=species: self.orbit_catalog[species]) + for species in self.orbit_catalog}) + return ProductMapping(loaders) + + def __getattr__(self, name: str) -> ProductNamespace: + """Products of one species or field group, as ``out..``. + + ``out.kinetic_ions.e1_v1_density.f_binned`` and ``out.kinetic_ions.orbits`` are the + products of that species, whatever kind they are; the grouped views :attr:`fields`, + :attr:`distributions`, :attr:`densities` and :attr:`orbits` show them by kind. + """ + if name.startswith("_"): + raise AttributeError(name) + catalog = self.species_catalog + if any(key.startswith(name + "/") for key in catalog): + return ProductNamespace(catalog, name) + raise AttributeError(f"{name!r}; available species: {tuple(sorted({key.split('/')[0] for key in catalog}))}") + + def __dir__(self): + catalog = self.species_catalog if self.is_processed else () + return sorted(set(super().__dir__()) | {key.split("/")[0] for key in catalog}) + @property def fields(self) -> FieldProducts: """FEEC fields as ``out.fields..``.""" diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index fba314c91..088484621 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -1,62 +1,42 @@ -"""``out.plot`` and ``out.analysis``: plotting and analysis without extra imports. +"""``out.plot`` and ``out.analysis``: diagnostics of a whole run. -Every method accepts a product name (``"en_phi"``, ``"em_fields/phi_log"``, -``"kinetic_ions/e1_v1_density/f_binned"``; see :meth:`Output.__getitem__`) or any labeled -array, including arrays derived from or belonging to another run. +Plots and diagnostics of a single array live on the array itself, see +:class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`. The methods here take a +product name as well (``"en_phi"``, ``"em_fields/phi_log"``; see :meth:`Output.__getitem__`), +and label arrays that carry no run with this run. """ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING -import numpy as np import xarray as xr +import struphy.post_processing.xarray_accessors # noqa: F401 (registers array.struphy) + if TYPE_CHECKING: from struphy.post_processing.output import Output -Coordinates = Literal["logical", "physical"] -Plane = Literal["XY", "XZ", "YZ", "RZ"] - class OutputPlots: """Standard plots of a run, as ``out.plot.(...)``. - Plots return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with - ``.show()`` and ``.save(path)``. Figures are titled with the run's numerical parameters; - time series of different runs are labeled by run. + Plots return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` + and ``.save(path)``, titled with the run's numerical parameters. The array-level methods are + the accessor methods of that array: ``out.plot.slice("em_fields/phi_log", ...)`` is + ``out.em_fields.phi_log.struphy.slice(...)``. """ def __init__(self, output: "Output"): self._output = output def _array(self, data) -> xr.DataArray: - return self._output[data] if isinstance(data, str) else data - - def _label(self, arrays) -> str: - from struphy.diagnostics.plotting import shared_run_label - - runs = {array.attrs.get("run") for array in arrays} - {None, ""} - return shared_run_label(arrays) if runs else self._output.label - - def _view(self, array, x, y, sweep, coords, plane, selection): - from struphy.diagnostics.plotting import View - - select, index = {}, {} - for dim, value in selection.items(): - if dim not in array.dims: - raise TypeError(f"{dim!r} is not a dimension of {array.name!r}; its dimensions are {array.dims}") - if value == "first": - index[dim] = 0 - elif value == "last": - index[dim] = -1 - elif isinstance(value, (bool, str)): - raise TypeError(f"cannot select {dim}={value!r}; use a number, or \"first\"/\"last\"") - elif isinstance(value, (int, np.integer)): - index[dim] = int(value) - else: - select[dim] = float(value) - return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) + array = self._output[data] if isinstance(data, str) else data + if not array.attrs.get("run"): + array = array.copy() + array.attrs["run"] = self._output.label + array.attrs["run_name"] = self._output.path_out.name + return array def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): """Overview of the scalar time series in one axes. @@ -72,218 +52,44 @@ def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = Fa """ from struphy.diagnostics.plotting import plot_scalars - return plot_scalars( - self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label - ) - - def timeseries( - self, - *data, - logy: bool = True, - fit: tuple[float | None, float | None] | bool | None = None, - fit_amplitude: bool = False, - title: str | None = None, - ax=None, - ): - """One or more time series, optionally with an exponential growth-rate fit. - - Parameters - ---------- - *data: - Names or arrays with the single dimension ``t``, e.g. ``"en_phi"``. - logy: - Logarithmic value axis. - fit: - Time window ``(t0, t1)`` of an exponential fit per series (``None`` for an open - end), or ``True`` for the whole series. Rates are in ``result.fit_results``. - fit_amplitude: - The series is quadratic in an amplitude (e.g. an energy); fit the amplitude's rate. - title: - Axes title; the first series' label by default. - ax: - Draw into these axes instead of a new figure. - """ - from struphy.diagnostics.plotting import GrowthFit, plot_timeseries + return plot_scalars(self._output.scalars, names=names, relative_to=relative_to, logy=logy, + run_label=self._output.label) + def timeseries(self, *data, **kwargs): + """One or more time series; see the ``timeseries`` accessor method of an array.""" if not data: raise TypeError("timeseries() needs at least one name or array") - series = [self._array(item) for item in data] - growth = None - if fit is not None and fit is not False: - window = (None, None) if fit is True else tuple(fit) - growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) - return plot_timeseries(series, ax=ax, logy=logy, fit=growth, title=title, run_label=self._label(series)) - - def slice( - self, - data, - *, - x: str | None = None, - y: str | None = None, - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - equal_aspect: bool | None = None, - title: str | None = None, - ax=None, - **selection, - ): - """A two-dimensional color plot of one slice. + first, *others = (self._array(item) for item in data) + return first.struphy.timeseries(*others, **kwargs) - Parameters - ---------- - data: - Name or array; select all but two dimensions, here or with ``select``/``isel``. - x, y: - Displayed dimensions, e.g. ``x="e1", y="v1"``; inferred for two-dimensional data. - coords: - ``"physical"`` draws on the mapped coordinates of ``plane`` instead of logical ones. - select, isel: - Selections by nearest coordinate value or by index, e.g. ``isel={"t": -1}``. - """ - from struphy.diagnostics.plotting import plot_slice - - array = self._array(data) - return plot_slice( - array, - view=self._view(array, x, y, "t", coords, plane, selection), - ax=ax, - vmin=vmin, - vmax=vmax, - equal_aspect=equal_aspect, - title=title, - run_label=self._label([array]), - ) - - def panels( - self, - data, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - nrows: int = 3, - ncols: int = 4, - shared_clim: bool = True, - title: str | None = None, - **selection, - ): - """Snapshots evenly spread along ``sweep`` (time by default), one panel each.""" - from struphy.diagnostics.plotting import plot_panels - - array = self._array(data) - return plot_panels( - array, - view=self._view(array, x, y, sweep, coords, plane, selection), - nrows=nrows, - ncols=ncols, - shared_clim=shared_clim, - title=title, - run_label=self._label([array]), - ) - - def viewer( - self, - data, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - **selection, - ): - """An interactive slice viewer with one slider per non-displayed dimension. - - Call ``.show()`` on the result; keep it alive so that the sliders stay connected. - """ - from struphy.diagnostics.plotting import InteractiveSliceViewer - - array = self._array(data) - return InteractiveSliceViewer( - array, - view=self._view(array, x, y, sweep, coords, plane, selection), - vmin=vmin, - vmax=vmax, - run_label=self._label([array]), - ) - - def animation( - self, - data, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - interval: int = 100, - step: int = 1, - vmin=None, - vmax=None, - **selection, - ): - """A Matplotlib animation along ``sweep``, taking the same arguments as :meth:`slice`.""" - from struphy.diagnostics.plotting import animate_slices - - array = self._array(data) - return animate_slices( - array, - view=self._view(array, x, y, sweep, coords, plane, selection), - interval=interval, - step=step, - vmin=vmin, - vmax=vmax, - ) - - def frames( - self, - data, - directory, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - step: int = 1, - prefix: str = "frame", - dpi: int = 110, - **selection, - ) -> list[str]: - """Write the slices along ``sweep`` as numbered PNG files; returns their paths. - - Takes the same arguments as :meth:`slice`. - """ - from struphy.diagnostics.plotting import save_frames - - array = self._array(data) - return save_frames( - array, - directory, - view=self._view(array, x, y, sweep, coords, plane, selection), - step=step, - prefix=prefix, - dpi=dpi, - ) - - def orbits(self, species: str | None = None, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): - """Three-dimensional trajectories of the saved markers of ``species``.""" - from struphy.diagnostics.plotting import plot_marker_trajectories + def slice(self, data, **kwargs): + """A two-dimensional slice; see the ``slice`` accessor method of an array.""" + return self._array(data).struphy.slice(**kwargs) + + def panels(self, data, **kwargs): + """Snapshots along a sweep; see the ``panels`` accessor method of an array.""" + return self._array(data).struphy.panels(**kwargs) + + def viewer(self, data, **kwargs): + """An interactive viewer; see the ``viewer`` accessor method of an array.""" + return self._array(data).struphy.viewer(**kwargs) + + def animation(self, data, **kwargs): + """An animation; see the ``animation`` accessor method of an array.""" + return self._array(data).struphy.animation(**kwargs) + def frames(self, data, directory, **kwargs): + """PNG files of a sweep; see the ``frames`` accessor method of an array.""" + return self._array(data).struphy.frames(directory, **kwargs) + + def orbits(self, species: str | None = None, **kwargs): + """Marker trajectories of ``species``, the only species with saved markers by default.""" available = tuple(self._output.orbits) if species is None: if len(available) != 1: raise ValueError(f"choose a species from {available}") species = available[0] - return plot_marker_trajectories( - self._output.orbits[species], ax=ax, max_markers=max_markers, show_paths=show_paths - ) + return self._array(self._output.orbits[species]).struphy.trajectories(**kwargs) def equilibrium(self, ax=None): """Radial equilibrium profiles, from the geometry written at the start of the run.""" @@ -293,7 +99,11 @@ def equilibrium(self, ax=None): class OutputAnalysis: - """Quantitative diagnostics of a run, as ``out.analysis.(...)``.""" + """Quantitative diagnostics of a run, as ``out.analysis.(...)``. + + Each method takes a product name or any array, and is the corresponding accessor method of + that array: ``out.analysis.growth_rate("en_phi")`` is ``out["en_phi"].struphy.growth_rate()``. + """ def __init__(self, output: "Output"): self._output = output @@ -301,45 +111,26 @@ def __init__(self, output: "Output"): def _array(self, data) -> xr.DataArray: return self._output[data] if isinstance(data, str) else data - def growth_rate(self, data, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): - """Fit ``exp(rate * t + intercept)`` to a time series within ``window``. + def growth_rate(self, data, **kwargs): + """Exponential growth rate; see the ``growth_rate`` accessor method of an array.""" + return self._array(data).struphy.growth_rate(**kwargs) - With ``amplitude=True`` the series is quadratic in an amplitude (e.g. an energy) and the - amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, - ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. - """ - from struphy.diagnostics.plotting import GrowthFit, growth_rate - - return growth_rate(self._array(data), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def drift(self, data, *, ref=None) -> xr.DataArray: - """Signed deviation of a time series from ``ref`` or from its first sample.""" - from struphy.diagnostics.plotting import drift + def drift(self, data, **kwargs) -> xr.DataArray: + """Deviation from the first sample; see the ``drift`` accessor method of an array.""" + return self._array(data).struphy.drift(**kwargs) - return drift(self._array(data), ref=ref) + def relative_error(self, data, **kwargs) -> xr.DataArray: + """Relative deviation; see the ``relative_error`` accessor method of an array.""" + return self._array(data).struphy.relative_error(**kwargs) - def relative_error(self, data, *, ref=None, skip_first: bool = True) -> xr.DataArray: - """Absolute relative deviation of a time series from ``ref`` or from its first sample.""" - from struphy.diagnostics.plotting import relative_error + def dispersion(self, field, **kwargs): + """Space-time spectrum; see the ``dispersion`` accessor method of an array. - return relative_error(self._array(data), ref=ref, skip_first=skip_first) - - def dispersion( - self, field, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs - ): - """Space-time power spectrum of a field and fitted dispersion branches. - - The spectrum is computed in normalized time. See - :func:`struphy.diagnostics.diagn_tools.power_spectrum_2d` for ``slice_at``, the fit options - and ``do_plot``. Returns ``(omega, kvec, spectrum, coeffs)``. + A field given by name is taken in normalized time, whatever this run's time units are. """ - from struphy.diagnostics.diagn_tools import power_spectrum_2d - if isinstance(field, str): - run = ( - self._output if self._output.time_units == "normalized" else self._output.with_time_units("normalized") - ) - field = run[field] - elif field.t.attrs.get("units") == "s": - raise ValueError("pass the field by name, or take it from out.with_time_units('normalized')") - return power_spectrum_2d(field, component=component, slice_at=slice_at, physical=physical, **kwargs) + output = self._output + if output.time_units != "normalized": + output = output.with_time_units("normalized") + field = output[field] + return field.struphy.dispersion(**kwargs) diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 3014d149f..0a1f2fdc7 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -129,3 +129,35 @@ def test_selection_keywords_take_positions_values_and_ends(run): run.plot.slice(name, x="e1", y="v1", time=-1) with pytest.raises(TypeError, match='use a number'): run.plot.slice(name, x="e1", y="v1", t="final") + + +def test_products_of_one_species_sit_on_the_output(run): + assert run.kinetic_ions.e1_v1_density.f_binned.dims == ("t", "e1", "v1") + assert run.kinetic_ions.view_0.n_sph.dims == ("t", "e1", "e2", "e3") + assert run.kinetic_ions.orbits.dims == ("t", "marker", "attribute") + assert run.em_fields.E.dims[:2] == ("t", "component") + assert {"kinetic_ions", "em_fields"} <= set(dir(run)) + with pytest.raises(AttributeError, match="available species"): + run.electrons + + +def test_arrays_plot_themselves(run): + phase_space = run.kinetic_ions.e1_v1_density.f_binned + assert phase_space.struphy.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" + assert len(phase_space.struphy.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + assert set(phase_space.struphy.viewer(x="e1", y="v1").sliders) == set() + assert run.kinetic_ions.orbits.struphy.trajectories(max_markers=2).ax.name == "3d" + + +def test_the_accessor_works_on_derived_arrays(run): + energy = run.scalars.en_phi + assert energy.isel(t=slice(1, None)).struphy.growth_rate().rate == pytest.approx(RATE) + error = energy.struphy.relative_error() + assert error.struphy.timeseries(logy=False).fig._suptitle.get_text() == run.label + + +def test_plot_accessor_and_array_accessor_agree(run): + by_output = run.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", t="last") + by_array = run.kinetic_ions.e1_v1_density.f_binned.struphy.slice(x="e1", y="v1", t="last") + np.testing.assert_allclose(by_output.artists[0].get_array(), by_array.artists[0].get_array()) + assert by_output.fig._suptitle.get_text() == by_array.fig._suptitle.get_text() == run.label diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py new file mode 100644 index 000000000..991b5b624 --- /dev/null +++ b/src/struphy/post_processing/xarray_accessors.py @@ -0,0 +1,196 @@ +"""``array.struphy.(...)``: plots and diagnostics of a single labeled array. + +Every product of an :class:`~struphy.Output` carries this accessor, and so does every array +derived from one, e.g. ``out.ions.eta1_v1.f.isel(v1=0).struphy.timeseries()``. Plots that need +the whole run (the scalar overview, the equilibrium profiles, the report) live on ``out.plot``. + +Dimensions that are neither displayed nor swept are selected by naming them: an integer is a +position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest +coordinate value (``t=0.35``). +""" + +from __future__ import annotations + +from typing import Literal + +import numpy as np +import xarray as xr + +Coordinates = Literal["logical", "physical"] +Plane = Literal["XY", "XZ", "YZ", "RZ"] + + +@xr.register_dataarray_accessor("struphy") +class StruphyAccessor: + """Struphy plots and diagnostics of one array, as ``array.struphy.(...)``.""" + + def __init__(self, array: xr.DataArray): + self._array = array + + def _view(self, x, y, sweep, coords, plane, selection): + from struphy.diagnostics.plotting import View + + select, index = {}, {} + for dim, value in selection.items(): + if dim not in self._array.dims: + raise TypeError( + f"{dim!r} is not a dimension of {self._array.name!r}; its dimensions are {self._array.dims}" + ) + if value == "first": + index[dim] = 0 + elif value == "last": + index[dim] = -1 + elif isinstance(value, (bool, str)): + raise TypeError(f'cannot select {dim}={value!r}; use a number, or "first"/"last"') + elif isinstance(value, (int, np.integer)): + index[dim] = int(value) + else: + select[dim] = float(value) + return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) + + # ---------- + # Plots + # ---------- + + def timeseries(self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, + title: str | None = None, ax=None): + """This time series, and any others given, in one axes. + + Parameters + ---------- + *others: + Further arrays with the single dimension ``t``; they may come from other runs and + need not share this array's time grid. + logy: + Logarithmic value axis. + fit: + Time window ``(t0, t1)`` of an exponential fit per series (``None`` for an open end), + or ``True`` for the whole series. Rates are in ``result.fit_results``. + fit_amplitude: + The series is quadratic in an amplitude (e.g. an energy); fit the amplitude's rate. + """ + from struphy.diagnostics.plotting import GrowthFit, plot_timeseries + + growth = None + if fit is not None and fit is not False: + window = (None, None) if fit is True else tuple(fit) + growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) + return plot_timeseries([self._array, *others], ax=ax, logy=logy, fit=growth, title=title) + + def slice(self, *, x: str | None = None, y: str | None = None, coords: Coordinates = "logical", + plane: Plane = "XY", vmin=None, vmax=None, equal_aspect: bool | None = None, + title: str | None = None, ax=None, **selection): + """A two-dimensional color plot of one slice. + + Parameters + ---------- + x, y: + Displayed dimensions, e.g. ``x="e1", y="v1"``; inferred for two-dimensional data. + The sweep dimension ``t`` may be displayed, which gives a space-time map. + coords: + ``"physical"`` draws on the mapped coordinates of ``plane`` instead of logical ones. + **selection: + One value per remaining dimension, e.g. ``t="last", component=2, e3=0``. + + Examples + -------- + >>> out.ions.eta1_v1.f.struphy.slice(x="e1", y="v1", t="last") + >>> out.em_fields.b_field_phy.struphy.slice(x="e1", y="e2", component=2, e3=0, coords="physical") + """ + from struphy.diagnostics.plotting import plot_slice + + return plot_slice(self._array, view=self._view(x, y, "t", coords, plane, selection), ax=ax, vmin=vmin, + vmax=vmax, equal_aspect=equal_aspect, title=title) + + def panels(self, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", nrows: int = 3, ncols: int = 4, + shared_clim: bool = True, title: str | None = None, **selection): + """Snapshots evenly spread along ``sweep`` (time by default), one panel each. + + Takes the same arguments as :meth:`slice`, except that ``sweep`` is not selected. + """ + from struphy.diagnostics.plotting import plot_panels + + return plot_panels(self._array, view=self._view(x, y, sweep, coords, plane, selection), nrows=nrows, + ncols=ncols, shared_clim=shared_clim, title=title) + + def viewer(self, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", vmin=None, vmax=None, **selection): + """An interactive viewer with one slider per dimension that is neither displayed nor selected. + + Takes the same arguments as :meth:`slice`. Call ``.show()`` on the result, and keep it + alive so that the sliders stay connected. + """ + from struphy.diagnostics.plotting import InteractiveSliceViewer + + return InteractiveSliceViewer(self._array, view=self._view(x, y, sweep, coords, plane, selection), + vmin=vmin, vmax=vmax) + + def animation(self, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", interval: int = 100, step: int = 1, + vmin=None, vmax=None, **selection): + """A Matplotlib animation along ``sweep``, taking the same arguments as :meth:`slice`.""" + from struphy.diagnostics.plotting import animate_slices + + return animate_slices(self._array, view=self._view(x, y, sweep, coords, plane, selection), + interval=interval, step=step, vmin=vmin, vmax=vmax) + + def frames(self, directory, *, x: str | None = None, y: str | None = None, sweep: str = "t", + coords: Coordinates = "logical", plane: Plane = "XY", step: int = 1, prefix: str = "frame", + dpi: int = 110, **selection) -> list[str]: + """Write the slices along ``sweep`` as numbered PNG files; returns their paths. + + Takes the same arguments as :meth:`slice`. + """ + from struphy.diagnostics.plotting import save_frames + + return save_frames(self._array, directory, view=self._view(x, y, sweep, coords, plane, selection), + step=step, prefix=prefix, dpi=dpi) + + def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): + """Three-dimensional paths of saved markers; for an orbit product.""" + from struphy.diagnostics.plotting import plot_marker_trajectories + + return plot_marker_trajectories(self._array, ax=ax, max_markers=max_markers, show_paths=show_paths) + + # ----------- + # Diagnostics + # ----------- + + def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): + """Fit ``exp(rate * t + intercept)`` to this time series within ``window``. + + With ``amplitude=True`` the series is quadratic in an amplitude (e.g. an energy) and the + amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, + ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. + """ + from struphy.diagnostics.plotting import GrowthFit, growth_rate + + return growth_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def drift(self, *, ref=None) -> xr.DataArray: + """Signed deviation of this time series from ``ref`` or from its first sample.""" + from struphy.diagnostics.plotting import drift + + return drift(self._array, ref=ref) + + def relative_error(self, *, ref=None, skip_first: bool = True) -> xr.DataArray: + """Absolute relative deviation from ``ref`` or from this series' first sample.""" + from struphy.diagnostics.plotting import relative_error + + return relative_error(self._array, ref=ref, skip_first=skip_first) + + def dispersion(self, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs): + """Space-time power spectrum of this field and fitted dispersion branches. + + The time coordinate must be normalized, see :meth:`struphy.Output.with_time_units`. See + :func:`struphy.diagnostics.diagn_tools.power_spectrum_2d` for ``slice_at``, the fit options + and ``do_plot``. Returns ``(omega, kvec, spectrum, coeffs)``. + """ + from struphy.diagnostics.diagn_tools import power_spectrum_2d + + if self._array.t.attrs.get("units") == "s": + raise ValueError( + "the spectrum needs normalized time; take the field from out.with_time_units('normalized')" + ) + return power_spectrum_2d(self._array, component=component, slice_at=slice_at, physical=physical, **kwargs) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 4c62ad13a..6384847bf 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -154,7 +154,7 @@ "id": "7", "metadata": {}, "source": [ - "Products are arranged into clear namespaces. VS Code and interactive shells can complete the available names after a run is opened: fields are grouped by field species, while distribution and density products are grouped by species and saved slice. Every product can also be looked up by name, e.g. `out[\"electric_energy\"]` or `out[\"kinetic_ions/e1_v1_density/f_binned\"]`; flat catalogs remain available for code that needs to iterate over arbitrary products." + "Products sit on the run under the species that produced them, so VS Code and interactive shells complete them as you type: `out.kinetic_ions.e1_v1_density.f_binned`, `out.kinetic_ions.orbits`, `out.em_fields.phi_log`. The grouped views `out.fields`, `out.distributions`, `out.densities` and `out.orbits` show the same products by kind, and `out[\"kinetic_ions/e1_v1_density/f_binned\"]` looks one up by name, which is handy in scripts and loops. Flat catalogs remain available for code that iterates over arbitrary products." ] }, { @@ -170,7 +170,7 @@ "print(\"particle species:\", tuple(out.orbits))\n", "print(\"all field products:\", tuple(out.field_catalog))\n", "\n", - "phase_space = out.distributions.kinetic_ions.e1_v1_density.f_binned\n", + "phase_space = out.kinetic_ions.e1_v1_density.f_binned\n", "print(phase_space)\n", "print(\"dimensions:\", phase_space.dims)\n", "print(\"time coordinate:\", phase_space.t)" @@ -183,9 +183,9 @@ "source": [ "## Scalar overview and time series\n", "\n", - "All standard plots are methods of `out.plot`, so no further imports are needed. They accept a product name or any array, and titles carry the run's numerical parameters.\n", + "Products plot themselves: every array has a `.struphy` accessor, so `out.kinetic_ions.e1_v1_density.f_binned.struphy.slice(...)` needs no imports and completes as you type. Plots of the whole run stay on `out.plot`, which also accepts a product name, e.g. `out.plot.slice(\"kinetic_ions/e1_v1_density/f_binned\", ...)`.\n", "\n", - "`out.plot.scalars()` gives a quick overview of every recorded scalar. `out.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." + "`out.plot.scalars()` gives a quick overview of every recorded scalar. `.struphy.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] }, { @@ -235,8 +235,7 @@ "metadata": {}, "outputs": [], "source": [ - "out.plot.slice(\n", - " phase_space,\n", + "phase_space.struphy.slice(\n", " x=\"e1\",\n", " y=\"v1\",\n", " t=\"last\",\n", @@ -250,7 +249,7 @@ "id": "14", "metadata": {}, "source": [ - "For a compact view of the evolution, `out.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." + "For a compact view of the evolution, `.struphy.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." ] }, { @@ -260,8 +259,7 @@ "metadata": {}, "outputs": [], "source": [ - "out.plot.panels(\n", - " phase_space,\n", + "phase_space.struphy.panels(\n", " x=\"e1\",\n", " y=\"v1\",\n", " nrows=1,\n", @@ -277,7 +275,7 @@ "source": [ "## Interactive plots\n", "\n", - "`out.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." + "`.struphy.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." ] }, { @@ -287,7 +285,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_viewer = out.plot.viewer(phase_space, x=\"e1\", y=\"v1\")\n", + "phase_viewer = phase_space.struphy.viewer(x=\"e1\", y=\"v1\")\n", "phase_viewer" ] }, @@ -296,7 +294,7 @@ "id": "18", "metadata": {}, "source": [ - "Saved marker orbits are grouped by species. `out.plot.orbits()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." + "Saved marker orbits sit under their species. `.struphy.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." ] }, { @@ -306,14 +304,14 @@ "metadata": {}, "outputs": [], "source": [ - "out.plot.orbits(\"kinetic_ions\", max_markers=12, show_paths=True)" + "out.kinetic_ions.orbits.struphy.trajectories(max_markers=12, show_paths=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "`out.plot.animation()` and `out.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." + "`.struphy.animation()` and `.struphy.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." ], "id": "20" }, @@ -321,7 +319,7 @@ "cell_type": "code", "metadata": {}, "source": [ - "animation = out.plot.animation(phase_space, x=\"e1\", y=\"v1\", step=4)\n", + "animation = phase_space.struphy.animation(x=\"e1\", y=\"v1\", step=4)\n", "HTML(animation.to_jshtml())" ], "execution_count": null, @@ -332,7 +330,7 @@ "cell_type": "code", "metadata": {}, "source": [ - "frames = out.plot.frames(phase_space, \"frames\", x=\"e1\", y=\"v1\", step=10)\n", + "frames = phase_space.struphy.frames(\"frames\", x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" ], "execution_count": null, @@ -343,7 +341,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles." + "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." ], "id": "23" }, @@ -363,7 +361,7 @@ "source": [ "## Derived quantities\n", "\n", - "`out.analysis` computes without drawing, and every result is an array that the plots accept. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation." + "The accessor also computes without drawing, and every result is an array that plots itself. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation. The same methods sit on `out.analysis` when the array is given by name." ], "id": "25" }, @@ -371,11 +369,12 @@ "cell_type": "code", "metadata": {}, "source": [ - "energy_error = out.analysis.relative_error(\"total_energy\")\n", - "energy_drift = out.analysis.drift(\"total_energy\")\n", + "total_energy = out.scalars.total_energy\n", + "energy_error = total_energy.struphy.relative_error()\n", + "energy_drift = total_energy.struphy.drift()\n", "print(f\"largest drift of the total energy: {abs(energy_drift).max().item():.3e}\")\n", "\n", - "out.plot.timeseries(energy_error, title=\"Conservation of the total energy\")" + "energy_error.struphy.timeseries(title=\"Conservation of the total energy\")" ], "execution_count": null, "outputs": [], @@ -433,7 +432,7 @@ "source": [ "## Comparing runs\n", "\n", - "Every plot accepts arrays of other simulations, so comparing runs needs nothing special. Series are labelled by the run they come from, and the runs may have different time grids." + "Time series accept arrays of other simulations, so comparing runs needs nothing special. Series are labelled by the run they come from, and the runs may have different time grids." ], "id": "31" }, @@ -452,9 +451,8 @@ ")\n", "out_coarse = sim_coarse.run()\n", "\n", - "out.plot.timeseries(\n", - " out[\"electric_energy\"],\n", - " out_coarse[\"electric_energy\"],\n", + "out.scalars.electric_energy.struphy.timeseries(\n", + " out_coarse.scalars.electric_energy,\n", " title=\"Electric energy: dt = 0.05 against dt = 0.1\",\n", ")" ], @@ -533,8 +531,7 @@ "cell_type": "code", "metadata": {}, "source": [ - "out_sph.plot.slice(\n", - " \"euler_fluid/view_0/n_sph\",\n", + "out_sph.euler_fluid.view_0.n_sph.struphy.slice(\n", " x=\"t\",\n", " y=\"e1\",\n", " e2=0,\n", @@ -558,7 +555,7 @@ "cell_type": "code", "metadata": {}, "source": [ - "density = out_sph[\"euler_fluid/view_0/n_sph\"].isel(e2=0, e3=0)\n", + "density = out_sph.euler_fluid.view_0.n_sph.isel(e2=0, e3=0)\n", "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" ], "execution_count": null, @@ -600,7 +597,7 @@ "out_coaxial.process(physical=True)\n", "\n", "print(\"fields:\", tuple(out_coaxial.field_catalog))\n", - "print(\"dimensions:\", out_coaxial[\"em_fields/b_field_phy\"].dims)" + "print(\"dimensions:\", out_coaxial.em_fields.b_field_phy.dims)" ], "execution_count": null, "outputs": [], @@ -610,8 +607,7 @@ "cell_type": "code", "metadata": {}, "source": [ - "out_coaxial.plot.slice(\n", - " \"em_fields/b_field_phy\",\n", + "out_coaxial.em_fields.b_field_phy.struphy.slice(\n", " x=\"e1\",\n", " y=\"e2\",\n", " t=\"last\",\n", @@ -630,8 +626,7 @@ "cell_type": "code", "metadata": {}, "source": [ - "out_coaxial.plot.panels(\n", - " \"em_fields/b_field_phy\",\n", + "out_coaxial.em_fields.b_field_phy.struphy.panels(\n", " x=\"e1\",\n", " y=\"e2\",\n", " component=2,\n", From 919fe94f24891371df113b226faeae2ce1d26e04 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 12:06:02 +0200 Subject: [PATCH 030/193] formatting --- src/struphy/post_processing/output.py | 18 ++- .../post_processing/output_accessors.py | 5 +- .../tests/test_output_accessors.py | 2 +- .../post_processing/xarray_accessors.py | 141 ++++++++++++++---- 4 files changed, 127 insertions(+), 39 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index f4356d213..7652e0b74 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -277,18 +277,24 @@ def _product_mappings(self) -> dict[str, ProductMapping]: } shadowed = {key.split("/")[0] for loaders in discovered.values() for key in loaders} & set(dir(type(self))) if shadowed: - logger.warning("species %s shadow attributes of Output; reach them as out.fields, " - "out.distributions, out.densities or out.orbits", sorted(shadowed)) + logger.warning( + "species %s shadow attributes of Output; reach them as out.fields, " + "out.distributions, out.densities or out.orbits", + sorted(shadowed), + ) return self._products @property def species_catalog(self) -> ProductMapping: """Every product of every species, keyed ``/``; see :meth:`__getattr__`.""" catalogs = (self.field_catalog, self.distribution_catalog, self.density_catalog) - loaders = {key: (lambda catalog=catalog, key=key: catalog[key]) - for catalog in catalogs for key in catalog} - loaders.update({f"{species}/orbits": (lambda species=species: self.orbit_catalog[species]) - for species in self.orbit_catalog}) + loaders = {key: (lambda catalog=catalog, key=key: catalog[key]) for catalog in catalogs for key in catalog} + loaders.update( + { + f"{species}/orbits": (lambda species=species: self.orbit_catalog[species]) + for species in self.orbit_catalog + } + ) return ProductMapping(loaders) def __getattr__(self, name: str) -> ProductNamespace: diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index 088484621..3d9f80668 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -52,8 +52,9 @@ def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = Fa """ from struphy.diagnostics.plotting import plot_scalars - return plot_scalars(self._output.scalars, names=names, relative_to=relative_to, logy=logy, - run_label=self._output.label) + return plot_scalars( + self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label + ) def timeseries(self, *data, **kwargs): """One or more time series; see the ``timeseries`` accessor method of an array.""" diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 0a1f2fdc7..7260fabd4 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -127,7 +127,7 @@ def test_selection_keywords_take_positions_values_and_ends(run): with pytest.raises(TypeError, match="not a dimension"): run.plot.slice(name, x="e1", y="v1", time=-1) - with pytest.raises(TypeError, match='use a number'): + with pytest.raises(TypeError, match="use a number"): run.plot.slice(name, x="e1", y="v1", t="final") diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index 991b5b624..b6041dbb7 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -52,8 +52,9 @@ def _view(self, x, y, sweep, coords, plane, selection): # Plots # ---------- - def timeseries(self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, - title: str | None = None, ax=None): + def timeseries( + self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, title: str | None = None, ax=None + ): """This time series, and any others given, in one axes. Parameters @@ -77,9 +78,20 @@ def timeseries(self, *others, logy: bool = True, fit=None, fit_amplitude: bool = growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) return plot_timeseries([self._array, *others], ax=ax, logy=logy, fit=growth, title=title) - def slice(self, *, x: str | None = None, y: str | None = None, coords: Coordinates = "logical", - plane: Plane = "XY", vmin=None, vmax=None, equal_aspect: bool | None = None, - title: str | None = None, ax=None, **selection): + def slice( + self, + *, + x: str | None = None, + y: str | None = None, + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + equal_aspect: bool | None = None, + title: str | None = None, + ax=None, + **selection, + ): """A two-dimensional color plot of one slice. Parameters @@ -99,23 +111,57 @@ def slice(self, *, x: str | None = None, y: str | None = None, coords: Coordinat """ from struphy.diagnostics.plotting import plot_slice - return plot_slice(self._array, view=self._view(x, y, "t", coords, plane, selection), ax=ax, vmin=vmin, - vmax=vmax, equal_aspect=equal_aspect, title=title) - - def panels(self, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", nrows: int = 3, ncols: int = 4, - shared_clim: bool = True, title: str | None = None, **selection): + return plot_slice( + self._array, + view=self._view(x, y, "t", coords, plane, selection), + ax=ax, + vmin=vmin, + vmax=vmax, + equal_aspect=equal_aspect, + title=title, + ) + + def panels( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + nrows: int = 3, + ncols: int = 4, + shared_clim: bool = True, + title: str | None = None, + **selection, + ): """Snapshots evenly spread along ``sweep`` (time by default), one panel each. Takes the same arguments as :meth:`slice`, except that ``sweep`` is not selected. """ from struphy.diagnostics.plotting import plot_panels - return plot_panels(self._array, view=self._view(x, y, sweep, coords, plane, selection), nrows=nrows, - ncols=ncols, shared_clim=shared_clim, title=title) - - def viewer(self, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", vmin=None, vmax=None, **selection): + return plot_panels( + self._array, + view=self._view(x, y, sweep, coords, plane, selection), + nrows=nrows, + ncols=ncols, + shared_clim=shared_clim, + title=title, + ) + + def viewer( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + **selection, + ): """An interactive viewer with one slider per dimension that is neither displayed nor selected. Takes the same arguments as :meth:`slice`. Call ``.show()`` on the result, and keep it @@ -123,29 +169,64 @@ def viewer(self, *, x: str | None = None, y: str | None = None, sweep: str = "t" """ from struphy.diagnostics.plotting import InteractiveSliceViewer - return InteractiveSliceViewer(self._array, view=self._view(x, y, sweep, coords, plane, selection), - vmin=vmin, vmax=vmax) - - def animation(self, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", interval: int = 100, step: int = 1, - vmin=None, vmax=None, **selection): + return InteractiveSliceViewer( + self._array, view=self._view(x, y, sweep, coords, plane, selection), vmin=vmin, vmax=vmax + ) + + def animation( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + interval: int = 100, + step: int = 1, + vmin=None, + vmax=None, + **selection, + ): """A Matplotlib animation along ``sweep``, taking the same arguments as :meth:`slice`.""" from struphy.diagnostics.plotting import animate_slices - return animate_slices(self._array, view=self._view(x, y, sweep, coords, plane, selection), - interval=interval, step=step, vmin=vmin, vmax=vmax) - - def frames(self, directory, *, x: str | None = None, y: str | None = None, sweep: str = "t", - coords: Coordinates = "logical", plane: Plane = "XY", step: int = 1, prefix: str = "frame", - dpi: int = 110, **selection) -> list[str]: + return animate_slices( + self._array, + view=self._view(x, y, sweep, coords, plane, selection), + interval=interval, + step=step, + vmin=vmin, + vmax=vmax, + ) + + def frames( + self, + directory, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + step: int = 1, + prefix: str = "frame", + dpi: int = 110, + **selection, + ) -> list[str]: """Write the slices along ``sweep`` as numbered PNG files; returns their paths. Takes the same arguments as :meth:`slice`. """ from struphy.diagnostics.plotting import save_frames - return save_frames(self._array, directory, view=self._view(x, y, sweep, coords, plane, selection), - step=step, prefix=prefix, dpi=dpi) + return save_frames( + self._array, + directory, + view=self._view(x, y, sweep, coords, plane, selection), + step=step, + prefix=prefix, + dpi=dpi, + ) def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): """Three-dimensional paths of saved markers; for an orbit product.""" From 2a4f4b1852383f5fa87c560e5551bedc97b2acf3 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 15:41:31 +0200 Subject: [PATCH 031/193] Improve accessors --- .claude/skills/setup-simulation/SKILL.md | 8 +- doc/sections/userguide.rst | 32 +- src/struphy/post_processing/arrays.py | 17 +- src/struphy/post_processing/output.py | 73 +- .../post_processing/output_accessors.py | 103 +- .../post_processing/tests/test_output.py | 29 +- .../tests/test_output_accessors.py | 65 +- .../post_processing/xarray_accessors.py | 45 +- tutorials/tutorial_post_processing.ipynb | 4327 ++++++++++++++++- 9 files changed, 4404 insertions(+), 295 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index e958c19ea..cd67a16d7 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -144,10 +144,10 @@ out.sim.model.units # the Simulation, restore Products sit under their species and plot themselves, no imports needed: ```python -out...f_binned.struphy.slice(x="e1", y="v1", t="last") # also .panels/.viewer/.animation/.frames -out..orbits.struphy.trajectories() -out.scalars..struphy.timeseries(fit=(t0, t1)) # also .growth_rate/.drift/.relative_error -out.plot.scalars(), out.plot.equilibrium(), out.save_report() # plots of the whole run +out...f_binned.struphy.plot.slice(x="e1", y="v1", t="last") # also .panels/.viewer/.animation/.frames +out..orbits.struphy.plot.trajectories() +out.scalars..struphy.plot.timeseries(fit=(t0, t1)) # also .growth_rate/.drift/.relative_error +out.plot.scalars(), out.plot.equilibrium(), out.save_report() # whole-run plots ``` Name any dimension to select it: `t="last"`, `t=-1` (position), `t=0.35` (nearest value), `component=0`. diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 8f69d0a1e..8eef568c3 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -558,39 +558,39 @@ serial processing runs on rank 0 while the other ranks wait, and ``parallel=True`` uses the allocated simulation on all ranks. -Standard plots and analysis: ``out.plot`` and ``out.analysis`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Standard plots and analysis +^^^^^^^^^^^^^^^^^^^^^^^^^^^ Products sit under the species that produced them and plot themselves through their -``.struphy`` accessor, so no imports are needed and everything completes as you type. The same -methods are on ``out.plot`` and ``out.analysis``, which also take a product name: +``.struphy`` accessor, which holds ``.plot`` and ``.analysis``. No imports are needed and +everything completes as you type: .. code-block:: python # products plot themselves f = out.kinetic_ions.e1_v1_density.f_binned - f.struphy.slice(x="e1", y="v1", t="last") - f.struphy.panels(x="e1", y="v1", nrows=3, ncols=4) - f.struphy.viewer(x="e1", y="v1").show() - out.em_fields.phi_phy.struphy.slice(x="e1", y="e2", t="last", coords="physical") - out.kinetic_ions.orbits.struphy.trajectories() - out.scalars.en_phi.struphy.timeseries(fit=(0.0, 40.0)) # exponential fit in a window - out.scalars.en_phi.struphy.growth_rate(window=(0.0, 40.0)).rate + f.struphy.plot.slice(x="e1", y="v1", t="last") + f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4) + f.struphy.plot.viewer(x="e1", y="v1").show() + out.em_fields.phi_phy.struphy.plot.slice(x="e1", y="e2", t="last", coords="physical") + out.kinetic_ions.orbits.struphy.plot.trajectories() + out.scalars.en_phi.struphy.plot.timeseries(fit=(0.0, 40.0)) # exponential fit in a window + out.scalars.en_phi.struphy.analysis.growth_rate(window=(0.0, 40.0)).rate # plots of the whole run out.plot.scalars() # every scalar time series out.save_report() # table + figures in post_processing/report/ - # every method is also on out.plot / out.analysis, which take a product name - out.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", t="last") - out.analysis.dispersion("em_fields/e_field_log", slice_at=(0, 0, None), fit_branches=1) + # the same products by name, which suits scripts and loops + out["kinetic_ions/e1_v1_density/f_binned"].struphy.plot.slice(x="e1", y="v1", t="last") + out["em_fields/e_field_log"].struphy.analysis.dispersion(slice_at=(0, 0, None), fit_branches=1) Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of several runs are labeled by run: .. code-block:: python - out_a.scalars.en_phi.struphy.timeseries(out_b.scalars.en_phi, fit=(0.0, 40.0)) + out_a.scalars.en_phi.struphy.plot.timeseries(out_b.scalars.en_phi, fit=(0.0, 40.0)) The sections below access the arrays directly for custom Matplotlib plots. @@ -627,7 +627,7 @@ Binned particle data is grouped by species and the slice defined in .. code-block:: python f = out.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) - f.struphy.slice(x="e1", y="v1", t="last").show() + f.struphy.plot.slice(x="e1", y="v1", t="last").show() Plotting particle orbits diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 1210790ce..86e163422 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -43,13 +43,24 @@ def data_array( coord_units: Mapping[str, str] | None = None, attrs: Mapping | None = None, ) -> xr.DataArray: - """Construct a consistently annotated :class:`xarray.DataArray`.""" + """Construct a consistently annotated :class:`xarray.DataArray`. + + ``label`` is also stored as the CF attribute ``long_name``, so that xarray's own plots + (``array.plot()``) label their axes the same way Struphy's do. + """ metadata = dict(attrs or {}) - metadata.update(label=label, units=unit) + metadata["label"] = label + if unit: # an empty unit would render as "[]" in xarray's own plots + metadata["units"] = unit + if label: + metadata.setdefault("long_name", label) out = xr.DataArray(values, dims=tuple(dims), coords=coords, name=name, attrs=metadata) for dim, value in (coord_units or {}).items(): - if dim in out.coords: + if dim in out.coords and value: out.coords[dim].attrs["units"] = value + for dim in out.dims: + if dim in out.coords and "long_name" not in out.coords[dim].attrs and dim in DIM_LABELS: + out.coords[dim].attrs["long_name"] = DIM_LABELS[dim] return validate_array(out) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 7652e0b74..0271325a6 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -13,7 +13,7 @@ import xarray as xr from struphy.post_processing.arrays import data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits -from struphy.post_processing.output_accessors import OutputAnalysis, OutputPlots +from struphy.post_processing.output_accessors import OutputPlots logger = logging.getLogger("struphy") @@ -114,8 +114,8 @@ class Output: default options; call :meth:`process` beforehand to choose options. * :attr:`sim` is the :class:`~struphy.Simulation` that produced the output: the live object for ``sim.output``, otherwise restored from disk without allocating anything. - * :attr:`plot` and :attr:`analysis` draw and evaluate standard diagnostics, e.g. - ``out.plot.timeseries("en_phi", fit=(0, 40))``; ``out["en_phi"]`` looks up any product. + * Products plot themselves, e.g. ``out["en_phi"].struphy.plot.timeseries(fit=(0, 40))``; + :attr:`plot` holds the plots that need the whole run. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. Parameters @@ -146,6 +146,7 @@ def with_time_units(self, time_units: str) -> "Output": def _reset(self): self._time = self._grids_log = self._grids_phy = self._scalars = self._products = self._label = None + self._species = None def __getitem__(self, name: str) -> xr.DataArray: """Any product by name: a scalar (``"en_tot"``), a field (``"em_fields/phi_log"``), a binned @@ -306,14 +307,31 @@ def __getattr__(self, name: str) -> ProductNamespace: """ if name.startswith("_"): raise AttributeError(name) - catalog = self.species_catalog - if any(key.startswith(name + "/") for key in catalog): - return ProductNamespace(catalog, name) - raise AttributeError(f"{name!r}; available species: {tuple(sorted({key.split('/')[0] for key in catalog}))}") + # the raw output names the species, so an unknown name never starts post-processing + if name not in self._raw_species(): + raise AttributeError(f"{name!r}; available species: {tuple(sorted(self._raw_species()))}") + return ProductNamespace(self.species_catalog, name) def __dir__(self): - catalog = self.species_catalog if self.is_processed else () - return sorted(set(super().__dir__()) | {key.split("/")[0] for key in catalog}) + return sorted(set(super().__dir__()) | self._raw_species()) + + def _raw_species(self) -> set[str]: + """Species and field groups of this run; cheap, and never starts post-processing. + + They are named in the raw output, and in the products of a run that is already processed. + """ + if self._species is None: + names = set() + path = self.path_out / "data" / "data_proc0.hdf5" + if path.exists(): + with h5py.File(path) as file: + for group in ("feec", "kinetic"): + if group in file: + names.update(file[group]) + if self.is_processed: + names.update(key.split("/")[0] for key in self.species_catalog) + self._species = names + return self._species @property def fields(self) -> FieldProducts: @@ -353,13 +371,11 @@ def orbit_catalog(self) -> ProductMapping: @property def plot(self) -> OutputPlots: - """Standard plots, e.g. ``out.plot.scalars()`` or ``out.plot.panels(name, x="e1", y="v1")``.""" - return OutputPlots(self) + """Plots of the whole run: ``out.plot.scalars()`` and ``out.plot.equilibrium()``. - @property - def analysis(self) -> OutputAnalysis: - """Quantitative diagnostics, e.g. ``out.analysis.growth_rate("en_phi", window=(0, 40))``.""" - return OutputAnalysis(self) + A single product plots itself, e.g. ``out.em_fields.phi_log.struphy.plot.slice(...)``. + """ + return OutputPlots(self) @property def f(self) -> DistributionProducts: @@ -473,6 +489,33 @@ def label(self) -> str: self._label = ", ".join(values) or self.path_out.name return self._label + def info(self) -> str: + """A table of everything this output holds, printed by ``print(out.info())``. + + Names are listed as they are reached, e.g. ``out.kinetic_ions.e1_v1_density.f_binned`` + and ``out["kinetic_ions/e1_v1_density/f_binned"]``. Nothing is loaded. + """ + lines = [f"Output of {self.path_out}", f" {self.label}", ""] + scalars = tuple(self.scalars.data_vars) + lines += ["scalars (no post-processing needed)"] + lines += [f" out.scalars.{name}" for name in scalars] or [" (none)"] + if not self.is_processed: + lines += ["", "products (not post-processed yet; run out.process(...) to choose options)"] + lines += [f" out.{name}.*" for name in sorted(self._raw_species())] + return "\n".join(lines) + for kind, catalog in ( + ("fields", self.field_catalog), + ("distributions", self.distribution_catalog), + ("densities", self.density_catalog), + ("orbits", self.orbit_catalog), + ): + lines += ["", kind] + entries = [f" out.{key.replace('/', '.')}" for key in catalog] + if kind == "orbits": + entries = [f" out.{key}.orbits" for key in catalog] + lines += entries or [" (none)"] + return "\n".join(lines) + def save_scalars(self, path=None, **kwargs) -> str: """Write the scalar time series as CSV (or NPZ); ``post_processing/scalars.csv`` by default.""" path = Path(path) if path else self.path_pproc / "scalars.csv" diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index 3d9f80668..559a22827 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -1,17 +1,15 @@ -"""``out.plot`` and ``out.analysis``: diagnostics of a whole run. +"""``out.plot``: plots that need a whole run. -Plots and diagnostics of a single array live on the array itself, see -:class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`. The methods here take a -product name as well (``"en_phi"``, ``"em_fields/phi_log"``; see :meth:`Output.__getitem__`), -and label arrays that carry no run with this run. +Plots and diagnostics of a single array live on the array, see +:class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`: +``out.em_fields.phi_log.struphy.plot.slice(...)``, or by name +``out["em_fields/phi_log"].struphy.plot.slice(...)``. """ from __future__ import annotations from typing import TYPE_CHECKING -import xarray as xr - import struphy.post_processing.xarray_accessors # noqa: F401 (registers array.struphy) if TYPE_CHECKING: @@ -19,25 +17,16 @@ class OutputPlots: - """Standard plots of a run, as ``out.plot.(...)``. + """Plots of a whole run, as ``out.plot.(...)``. - Plots return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` - and ``.save(path)``, titled with the run's numerical parameters. The array-level methods are - the accessor methods of that array: ``out.plot.slice("em_fields/phi_log", ...)`` is - ``out.em_fields.phi_log.struphy.slice(...)``. + They return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` + and ``.save(path)``, titled with the run's numerical parameters. Plots of one product are + methods of that product, e.g. ``out.kinetic_ions.orbits.struphy.plot.trajectories()``. """ def __init__(self, output: "Output"): self._output = output - def _array(self, data) -> xr.DataArray: - array = self._output[data] if isinstance(data, str) else data - if not array.attrs.get("run"): - array = array.copy() - array.attrs["run"] = self._output.label - array.attrs["run_name"] = self._output.path_out.name - return array - def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): """Overview of the scalar time series in one axes. @@ -56,82 +45,8 @@ def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = Fa self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label ) - def timeseries(self, *data, **kwargs): - """One or more time series; see the ``timeseries`` accessor method of an array.""" - if not data: - raise TypeError("timeseries() needs at least one name or array") - first, *others = (self._array(item) for item in data) - return first.struphy.timeseries(*others, **kwargs) - - def slice(self, data, **kwargs): - """A two-dimensional slice; see the ``slice`` accessor method of an array.""" - return self._array(data).struphy.slice(**kwargs) - - def panels(self, data, **kwargs): - """Snapshots along a sweep; see the ``panels`` accessor method of an array.""" - return self._array(data).struphy.panels(**kwargs) - - def viewer(self, data, **kwargs): - """An interactive viewer; see the ``viewer`` accessor method of an array.""" - return self._array(data).struphy.viewer(**kwargs) - - def animation(self, data, **kwargs): - """An animation; see the ``animation`` accessor method of an array.""" - return self._array(data).struphy.animation(**kwargs) - - def frames(self, data, directory, **kwargs): - """PNG files of a sweep; see the ``frames`` accessor method of an array.""" - return self._array(data).struphy.frames(directory, **kwargs) - - def orbits(self, species: str | None = None, **kwargs): - """Marker trajectories of ``species``, the only species with saved markers by default.""" - available = tuple(self._output.orbits) - if species is None: - if len(available) != 1: - raise ValueError(f"choose a species from {available}") - species = available[0] - return self._array(self._output.orbits[species]).struphy.trajectories(**kwargs) - def equilibrium(self, ax=None): """Radial equilibrium profiles, from the geometry written at the start of the run.""" from struphy.diagnostics.plotting import plot_equilibrium_profile return plot_equilibrium_profile(self._output.path_out, ax=ax) - - -class OutputAnalysis: - """Quantitative diagnostics of a run, as ``out.analysis.(...)``. - - Each method takes a product name or any array, and is the corresponding accessor method of - that array: ``out.analysis.growth_rate("en_phi")`` is ``out["en_phi"].struphy.growth_rate()``. - """ - - def __init__(self, output: "Output"): - self._output = output - - def _array(self, data) -> xr.DataArray: - return self._output[data] if isinstance(data, str) else data - - def growth_rate(self, data, **kwargs): - """Exponential growth rate; see the ``growth_rate`` accessor method of an array.""" - return self._array(data).struphy.growth_rate(**kwargs) - - def drift(self, data, **kwargs) -> xr.DataArray: - """Deviation from the first sample; see the ``drift`` accessor method of an array.""" - return self._array(data).struphy.drift(**kwargs) - - def relative_error(self, data, **kwargs) -> xr.DataArray: - """Relative deviation; see the ``relative_error`` accessor method of an array.""" - return self._array(data).struphy.relative_error(**kwargs) - - def dispersion(self, field, **kwargs): - """Space-time spectrum; see the ``dispersion`` accessor method of an array. - - A field given by name is taken in normalized time, whatever this run's time units are. - """ - if isinstance(field, str): - output = self._output - if output.time_units != "normalized": - output = output.with_time_units("normalized") - field = output[field] - return field.struphy.dispersion(**kwargs) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 01333a021..9932bb582 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -47,6 +47,8 @@ def write_tree(root): os.makedirs(data_dir) with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as file: file.create_dataset("time/value", data=t) + file.create_group("feec/em_fields") # the raw output names the species, + file.create_group("kinetic/kinetic_ions") # as a real run does file.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) write_manifest(root) return root @@ -121,10 +123,10 @@ def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): np.testing.assert_allclose(run.scalars.en_tot.t, run.time) -def test_saving_scalars_and_bound_plot_accessor(run, tmp_path): +def test_saving_scalars_and_plotting_a_product(run, tmp_path): path = run.save_scalars(tmp_path / "scalars.csv") assert os.path.exists(path) - result = run.plot.timeseries(run.scalars.en_tot, logy=False) + result = run.scalars.en_tot.struphy.plot.timeseries(logy=False) assert result.ax.get_xlabel() == "$t$" @@ -237,3 +239,26 @@ def process(self, **options): sim.rank = 3 Output(write_tree(str(tmp_path)), sim=sim).process(parallel=True) assert calls == [True] + + +def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): + root = write_tree(str(tmp_path)) + os.remove(os.path.join(root, "post_processing", "manifest.json")) + run = Output(root, sim=FakeSim(), time_units="normalized") + calls = [] + monkeypatch.setattr(Output, "process", lambda self, **options: calls.append(options)) + + with pytest.raises(AttributeError, match="available species"): + run.typo_here + assert not hasattr(run, "anything") + assert calls == [], "a typo must not post-process the run" + assert {"em_fields", "kinetic_ions"} <= set(dir(run)), "species are known before processing" + + +def test_info_lists_products_without_loading(run): + text = run.info() + assert "out.scalars.en_tot" in text + assert "out.kinetic_ions.e1_v1_density.f_binned" in text + assert "out.kinetic_ions.orbits" in text + assert "out.em_fields.E" in text + assert run.field_catalog._cache == {}, "listing must not load arrays" diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 7260fabd4..5ca3f84f0 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -57,14 +57,14 @@ def test_every_array_carries_its_run(run): def test_timeseries_by_name_with_growth_fit(run): - result = run.plot.timeseries("en_phi", fit=True) + result = run["en_phi"].struphy.plot.timeseries(fit=True) assert result.fit_results[0].rate == pytest.approx(RATE) assert result.fig._suptitle.get_text() == run.label def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): first, second = make_run(str(tmp_path), "sim_1"), make_run(str(tmp_path), "sim_2") - result = first.plot.timeseries(first.scalars.en_phi, second.scalars.en_phi) + result = first.scalars.en_phi.struphy.plot.timeseries(second.scalars.en_phi) labels = [text.get_text() for text in result.ax.get_legend().get_texts()] assert labels == ["en phi (sim_1)", "en phi (sim_2)"] @@ -72,7 +72,7 @@ def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): def test_timeseries_into_given_axes_keeps_the_figure_layout(run): fig, ax = plt.subplots() fig.suptitle("mine") - run.plot.timeseries("en_tot", ax=ax, logy=False) + run["en_tot"].struphy.plot.timeseries(ax=ax, logy=False) assert fig._suptitle.get_text() == "mine" @@ -84,15 +84,15 @@ def test_scalar_overview_draws_every_scalar_in_one_axes(run): def test_slices_panels_and_viewer_take_keyword_views(run): name = "kinetic_ions/e1_v1_density/f_binned" - assert run.plot.slice(name, x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(run.plot.panels(name, x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - viewer = run.plot.viewer("em_fields/E", x="e1", y="e2", component=0) + assert run[name].struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" + assert len(run[name].struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + viewer = run["em_fields/E"].struphy.plot.viewer(x="e1", y="e2", component=0) viewer.draw() assert set(viewer.sliders) == {"t", "e3"} -def test_orbits_default_to_the_only_species(run): - assert run.plot.orbits().ax.name == "3d" +def test_orbits_plot_their_trajectories(run): + assert run.kinetic_ions.orbits.struphy.plot.trajectories().ax.name == "3d" def test_report_is_written_below_post_processing(run): @@ -102,33 +102,33 @@ def test_report_is_written_below_post_processing(run): def test_analysis_by_name(run): - assert run.analysis.growth_rate("en_phi", window=(0.0, None)).rate == pytest.approx(RATE) - assert run.analysis.growth_rate("en_phi", amplitude=True).rate == pytest.approx(RATE / 2) - np.testing.assert_allclose(run.analysis.relative_error("en_tot"), 0.0) - np.testing.assert_allclose(run.analysis.drift("en_phi").isel(t=0), 0.0) + assert run["en_phi"].struphy.analysis.growth_rate(window=(0.0, None)).rate == pytest.approx(RATE) + assert run["en_phi"].struphy.analysis.growth_rate(amplitude=True).rate == pytest.approx(RATE / 2) + np.testing.assert_allclose(run["en_tot"].struphy.analysis.relative_error(), 0.0) + np.testing.assert_allclose(run["en_phi"].struphy.analysis.drift().isel(t=0), 0.0) def test_dispersion_rejects_fields_in_seconds(run): physical = run.with_time_units("physical") physical._sim.model = type("Model", (), {"units": type("Units", (), {"t": 2.0})()})() with pytest.raises(ValueError, match="normalized"): - physical.analysis.dispersion(physical.fields.em_fields.E) + physical.fields.em_fields.E.struphy.analysis.dispersion() def test_selection_keywords_take_positions_values_and_ends(run): name = "kinetic_ions/e1_v1_density/f_binned" times = run[name].t.values - by_position = run.plot.slice(name, x="e1", y="v1", t=-1) - by_value = run.plot.slice(name, x="e1", y="v1", t=float(times[-1])) - by_end = run.plot.slice(name, x="e1", y="v1", t="last") + by_position = run[name].struphy.plot.slice(x="e1", y="v1", t=-1) + by_value = run[name].struphy.plot.slice(x="e1", y="v1", t=float(times[-1])) + by_end = run[name].struphy.plot.slice(x="e1", y="v1", t="last") for result in (by_value, by_end): np.testing.assert_allclose(result.artists[0].get_array(), by_position.artists[0].get_array()) with pytest.raises(TypeError, match="not a dimension"): - run.plot.slice(name, x="e1", y="v1", time=-1) + run[name].struphy.plot.slice(x="e1", y="v1", time=-1) with pytest.raises(TypeError, match="use a number"): - run.plot.slice(name, x="e1", y="v1", t="final") + run[name].struphy.plot.slice(x="e1", y="v1", t="final") def test_products_of_one_species_sit_on_the_output(run): @@ -143,21 +143,26 @@ def test_products_of_one_species_sit_on_the_output(run): def test_arrays_plot_themselves(run): phase_space = run.kinetic_ions.e1_v1_density.f_binned - assert phase_space.struphy.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(phase_space.struphy.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - assert set(phase_space.struphy.viewer(x="e1", y="v1").sliders) == set() - assert run.kinetic_ions.orbits.struphy.trajectories(max_markers=2).ax.name == "3d" + assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" + assert len(phase_space.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + assert set(phase_space.struphy.plot.viewer(x="e1", y="v1").sliders) == set() + assert run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=2).ax.name == "3d" def test_the_accessor_works_on_derived_arrays(run): energy = run.scalars.en_phi - assert energy.isel(t=slice(1, None)).struphy.growth_rate().rate == pytest.approx(RATE) - error = energy.struphy.relative_error() - assert error.struphy.timeseries(logy=False).fig._suptitle.get_text() == run.label + assert energy.isel(t=slice(1, None)).struphy.analysis.growth_rate().rate == pytest.approx(RATE) + error = energy.struphy.analysis.relative_error() + assert error.struphy.plot.timeseries(logy=False).fig._suptitle.get_text() == run.label -def test_plot_accessor_and_array_accessor_agree(run): - by_output = run.plot.slice("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", t="last") - by_array = run.kinetic_ions.e1_v1_density.f_binned.struphy.slice(x="e1", y="v1", t="last") - np.testing.assert_allclose(by_output.artists[0].get_array(), by_array.artists[0].get_array()) - assert by_output.fig._suptitle.get_text() == by_array.fig._suptitle.get_text() == run.label +def test_products_by_name_and_by_attribute_agree(run): + by_output = run["kinetic_ions/e1_v1_density/f_binned"].struphy.plot.slice(x="e1", y="v1", t="last") + by_attribute = run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.slice(x="e1", y="v1", t="last") + np.testing.assert_allclose(by_output.artists[0].get_array(), by_attribute.artists[0].get_array()) + assert by_output.fig._suptitle.get_text() == by_attribute.fig._suptitle.get_text() == run.label + + +def test_selection_rejects_unknown_dimensions(run): + with pytest.raises(TypeError, match="not a dimension"): + run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.slice(x="e1", y="v1", time=-1) diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index b6041dbb7..a4f8b3bcd 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -1,8 +1,8 @@ """``array.struphy.(...)``: plots and diagnostics of a single labeled array. Every product of an :class:`~struphy.Output` carries this accessor, and so does every array -derived from one, e.g. ``out.ions.eta1_v1.f.isel(v1=0).struphy.timeseries()``. Plots that need -the whole run (the scalar overview, the equilibrium profiles, the report) live on ``out.plot``. +derived from one, e.g. ``out.ions.eta1_v1.f.isel(v1=0).struphy.plot.timeseries()``. Plots that +need the whole run (the scalar overview, the equilibrium profiles) live on ``out.plot``. Dimensions that are neither displayed nor swept are selected by naming them: an integer is a position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest @@ -22,11 +22,35 @@ @xr.register_dataarray_accessor("struphy") class StruphyAccessor: - """Struphy plots and diagnostics of one array, as ``array.struphy.(...)``.""" + """Struphy diagnostics of one array: ``array.struphy.plot`` and ``array.struphy.analysis``.""" def __init__(self, array: xr.DataArray): self._array = array + @property + def plot(self) -> "ArrayPlots": + """Plots of this array, e.g. ``array.struphy.plot.slice(x="e1", y="v1", t="last")``.""" + return ArrayPlots(self._array) + + @property + def analysis(self) -> "ArrayAnalysis": + """Diagnostics of this array, e.g. ``array.struphy.analysis.growth_rate()``.""" + return ArrayAnalysis(self._array) + + +class _ArrayAccessor: + def __init__(self, array: xr.DataArray): + self._array = array + + +class ArrayPlots(_ArrayAccessor): + """Plots of one array, as ``array.struphy.plot.(...)``. + + Dimensions that are neither displayed nor swept are selected by naming them: an integer is a + position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest + coordinate value (``t=0.35``). + """ + def _view(self, x, y, sweep, coords, plane, selection): from struphy.diagnostics.plotting import View @@ -48,10 +72,6 @@ def _view(self, x, y, sweep, coords, plane, selection): select[dim] = float(value) return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) - # ---------- - # Plots - # ---------- - def timeseries( self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, title: str | None = None, ax=None ): @@ -106,8 +126,8 @@ def slice( Examples -------- - >>> out.ions.eta1_v1.f.struphy.slice(x="e1", y="v1", t="last") - >>> out.em_fields.b_field_phy.struphy.slice(x="e1", y="e2", component=2, e3=0, coords="physical") + >>> out.ions.eta1_v1.f.struphy.plot.slice(x="e1", y="v1", t="last") + >>> out.em_fields.b_field_phy.struphy.plot.slice(x="e1", y="e2", component=2, e3=0, coords="physical") """ from struphy.diagnostics.plotting import plot_slice @@ -234,9 +254,10 @@ def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None return plot_marker_trajectories(self._array, ax=ax, max_markers=max_markers, show_paths=show_paths) - # ----------- - # Diagnostics - # ----------- + +class ArrayAnalysis(_ArrayAccessor): + """Quantitative diagnostics of one array, as ``array.struphy.analysis.(...)``.""" + def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): """Fit ``exp(rate * t + intercept)`` to this time series within ``window``. diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 6384847bf..1c745b612 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "1", "metadata": {}, "outputs": [], @@ -58,10 +58,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", + " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", + " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" + ] + } + ], "source": [ "def build_model():\n", " model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", @@ -99,10 +110,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time stepping: 100%|██████████| 40/40 [00:09<00:00, 4.26step/s]\n", + "Raw output: /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo\n" + ] + } + ], "source": [ "demo_tmp = tempfile.TemporaryDirectory(prefix=\"struphy_postprocessing_\")\n", "demo_root = demo_tmp.name\n", @@ -141,10 +168,80 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "Post-processing path /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo\n", + "\n", + "Reading hdf5 data of following species:\n", + "em_fields:\n", + " e_field: \n", + " phi: \n", + "Creation of Struphy Fields done.\n", + "\n", + "Evaluating fields ...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100%|██████████| 41/41 [00:00<00:00, 142.83it/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluation of 12 marker orbits for kinetic_ions\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "100%|██████████| 41/41 [00:00<00:00, 1353.10it/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluation of distribution functions for kinetic_ions\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "0 starting post-processing of distribution functions for /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo/post_processing/kinetic_data/kinetic_ions ...\n", + "100%|██████████| 1/1 [00:00<00:00, 546.99it/s]\n", + " 0%| | 0/1 [00:00 Size: 336kB\n", + "memmap([[[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + "...\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]]], shape=(41, 32, 32))\n", + "Coordinates:\n", + " * t (t) float64 328B 0.0 1.668e-10 3.336e-10 ... 6.504e-09 6.671e-09\n", + " * e1 (e1) float64 256B 0.01562 0.04688 0.07812 ... 0.9219 0.9531 0.9844\n", + " * v1 (v1) float64 256B -4.844 -4.531 -4.219 -3.906 ... 4.219 4.531 4.844\n", + "Attributes:\n", + " label: $f$\n", + " units: \n", + " run: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)\n", + " run_name: vlasov_ampere_demo\n", + "dimensions: ('t', 'e1', 'v1')\n", + "time coordinate: Size: 328B\n", + "array([0.000000e+00, 1.667820e-10, 3.335641e-10, 5.003461e-10, 6.671282e-10,\n", + " 8.339102e-10, 1.000692e-09, 1.167474e-09, 1.334256e-09, 1.501038e-09,\n", + " 1.667820e-09, 1.834603e-09, 2.001385e-09, 2.168167e-09, 2.334949e-09,\n", + " 2.501731e-09, 2.668513e-09, 2.835295e-09, 3.002077e-09, 3.168859e-09,\n", + " 3.335641e-09, 3.502423e-09, 3.669205e-09, 3.835987e-09, 4.002769e-09,\n", + " 4.169551e-09, 4.336333e-09, 4.503115e-09, 4.669897e-09, 4.836679e-09,\n", + " 5.003461e-09, 5.170243e-09, 5.337026e-09, 5.503808e-09, 5.670590e-09,\n", + " 5.837372e-09, 6.004154e-09, 6.170936e-09, 6.337718e-09, 6.504500e-09,\n", + " 6.671282e-09])\n", + "Coordinates:\n", + " * t (t) float64 328B 0.0 1.668e-10 3.336e-10 ... 6.504e-09 6.671e-09\n", + "Attributes:\n", + " units: s\n" + ] + } + ], "source": [ "print(\"scalars:\", tuple(out.scalars.data_vars))\n", "print(\"field species:\", tuple(out.fields))\n", @@ -183,17 +358,28 @@ "source": [ "## Scalar overview and time series\n", "\n", - "Products plot themselves: every array has a `.struphy` accessor, so `out.kinetic_ions.e1_v1_density.f_binned.struphy.slice(...)` needs no imports and completes as you type. Plots of the whole run stay on `out.plot`, which also accepts a product name, e.g. `out.plot.slice(\"kinetic_ions/e1_v1_density/f_binned\", ...)`.\n", + "Products plot themselves: every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f_binned.struphy.plot.slice(...)` needs no imports and completes as you type. `out[\"kinetic_ions/e1_v1_density/f_binned\"]` looks the same product up by name, which suits scripts and loops. The plots that need a whole run, `out.plot.scalars()` and `out.plot.equilibrium()`, stay on the run.\n", "\n", - "`out.plot.scalars()` gives a quick overview of every recorded scalar. `.struphy.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." + "`out.plot.scalars()` gives a quick overview of every recorded scalar. `.struphy.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "10", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAosAAAHrCAYAAACn9tfQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAATEtJREFUeJzt3Qm8jPX////Xse+yli1LSkSKRGUvfbRQKdImQmnTQotKKKGPKKQNbfRJpVWpUCSFZM2SlLLve/bl/G/P9+9/zXfOca5zZuYsM3M87rfbOGeuua5rrrnOOPM8r/dyJSQmJiYaAAAAkIIcKS0EAAAACIsAAABIFZVFAAAA+CIsAgAAwBdhEQAAAL4IiwAAAPBFWAQAAIAvwiIAAAB8ERYRF37//Xd74IEH7GTz9ttv2/jx46N9GAiRflb6mYXr33//tXvuuceyyzUSPvjgA/vyyy+j9vzxdj6jfb6AtORKcw0gBmzbts2+/fbbwP0xY8ZYwYIFrX379hHtb8mSJfbaa6+5D5Xrr7/eWrVqFfH63333nb366qtJ1u/Xr5+dc845ll4LFy60QoUKWVaGnfXr11uPHj1OeEwhKF++fKme8+eee84WLFiQ4mMXXXRRivtNyXvvvWdHjhyxjh07proso3322Wc2btw469Wrl9WtWzew/MYbb7QXX3zRypYtm+b7RO+RcL3wwgtWunRpS0hIcPdnz57tlkn+/Plt7NixJ2xz9OhRe+utt+zHH3+0mjVr2oMPPmh58uQJ6flC2X96ttfP+tJLL7X//Oc/ljt3bstqyc+nQuMnn3zifofoePUe1jGGKrufLyAtVBYRlxRI9MEciT///NMuvvhi90v7/PPPt9tuuy3V6l1a6//999+2Zs0a9wHk3fRBFY90TmfNmpXiY3rttWrVSnX7Jk2aBM7Brl27bN++fYH7l1xyScjH8dtvv7mgnNayzKhgf/rppy4sBvv4449tz549mfKchw8fdn+I3HHHHYFlFSpUcOdMgVXHk5Jrr73WhUWd1/3794ccxEPdf3q2P/300+2MM86wzz//3LJaSufzrrvucv9nL7jgAitevLgLZuFU8rLz+QJCQWURMUu/OCdOnOh+0QZXeVRJ+eabb9xf4PpwP/fcc+3pp58Oeb/Dhw93f8EPHjzY3dd++vfv71sxC2V9VZxuuOGGiF6ngsj7779vOXLksEqVKrkPNn1wpEQfNF999ZU7J5dffrn7ABw2bFjg8e+//95VUFRRadeunTVq1MgyMqCrsuhVTBXcVO3bvXu3XXXVVXbNNddYw4YNA+vrZ6T1dV7mzZvnXueBAwfc8dWrV886dOhgM2fOtA8//NCOHTvmKrbNmze3uXPnugqflq1bt87OOussu+66605YNmDAAPc8y5Yts3feece2b9/ufk5t27Z1y9999113TlWBmzFjht17771J3kd+FCQWL17stmncuHGK6/g9ZySmT59up556qgsMnnLlyrnzpoqUqrXJKegsX77cvf+9StTOnTtDfs609p8R2+s9oZ9tqP8vvJ/XwYMH3R8s1atXd11Pwq20pXQ+n332WbfMo+qvKuVXX3113J4vICtRWURMUgjp3LmzC4I5c+a0Ll26BB6rWLGinXnmmS60KLA1a9YsrH3rg+iyyy4L3G/RooUtXbrUt3IUyvoKD6o49uzZ0+bPnx/W8XivQx8Sai6rX7++CyEpfZgqSNauXdt9gCpcBTfNex80VapUcR+UairPyH5QCodeNVdN7zoPp5xyil144YX26KOP2uuvv+677caNG+3ll192AU9BUVVKHZs+rPVBWrVqVRe41HdL9xUUqlWr5s6LnielZd7PRtXMAgUKuKa8Z555xoYMGeIeU+C7//777eeff3bBWvsIhfb1+OOP21NPPZXi46k9ZyTmzJnjfqbhUPDWz/+ll16y22+/3f1Bk5XdFUJRp04d3yp1SvTzuu+++1xIVyVf72e9tow4n8FBUfbu3WtFihSxeD5fQFaisoiYNGjQIPcBePPNN7v76qumIBHcZKOgEvxXuCpZo0eP9t2nHtM2W7dutVKlSgWWe03GWp7SB0ha6ytIar+qYP3666+uujZhwgS78sorQ3qtZ599truJqoEKVnqt6qAf7L///a+rIt50003uvqpsCtXB1ZOhQ4cG+vSp2VzLFMhCPTeheuKJJ9zPSIFezjvvPBf2FGZTo8qoF2ruvPNOV6FVQJBixYq54KUgrlCoClPwzzelZQp0unmDn9Qkq2qq1ySrUPrGG29YuLp16+b6mCmMq3IYLK3nDK5wKSD70WOnnXaabdq0Kcn7KxTq+qAKs85506ZNXZ/ZH374wYXIWKHXpNcWDlWM9UeRV2lTpf2ff/5xXzPqfKo7w//+9z9XhY8l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48eP2+DBg90l9TS5ddeuXQOTcd9///324IMP2jXXXGOlS5d2cxrqKirhbNu6dWt33eYZM2a4y/VdccUVbl+XX365aya/44473PoffvihNWrUKMmxDRkyxG688UaLB4RFAACyicTERNt/ZH+m3/Q8qVEIUrhSYNq1a5d9/vnnbnnfvn1t+vTpNnnyZJs0aZJNnDjRnn/+ed/1vfvbtm2zV1991R599FH77bffQj4fL774or333ns2fvx4mz9/vu3du9ftw6tkvvPOOy74LV++3IVC77FQth0zZozdc889tnr1amvYsKHdfPPNLlTqGs49evRw4VfhWBQqlyxZ4q7W4nnzzTfjJixyBRcAALKJA0cPWP3/1c/055lz8xwrkLuA7+M5cuSwnDlzupsqgx5d41gh6ayzznL3n332WRfAdFm7lNb3lkmTJk3slltusa+//tpq1aoV0nG+8sorLrTVrl3b3X/hhRfctlouHTp0sMsuu8x9f9ddd1m7du1C3vamm26yli1buu83btzoruH8xRdfWJEiRey0005zx+pVIvWa2rdv7yqjqpbOmTPHbXP11VdbPKCyCAAAMp2qkQpIZ5xxRpJrMa9bt853G1X0FOZKlSrlAteIESNcc2+oz6eq37XXXmuFChWyggUL2plnnukqlV6I0349+fPntwMHDoS8bYUKFQLb6piKFy/ugqIn+fWc1SQ9duxYO3bsmAuNqirmyZPH4gGVRQAAson8ufK7ql9WPE9aEhISTrh/6qmnuhDmBUZ9X6ZMmRTX9yp/HTt2dM3BCmKPPfaY6+MYCu1P+1Z/yOTXgg6uXka6bULQ8ep17dy50/bt2+eCpaxduzbJdvXq1XOBUk3vatpWM3y8oLIIAEA2oQCj5uHMvqUU7JLTQI+VK1cm6d+oZt4+ffrYli1bXDVOfRjVPOu3/u7du61cuXKuAqi+igpZ4VA/SDVxr1+/3nLlymWLFi1y/SAzetvy5cu7UKkBO6pO/vLLLy7gJtepUyfr1q2bGxRz0UUXWbwgLAIAgAynPntLly61vHnzBkY39+/f3zXnVqtWzWrWrGl169a1p556ynd9DTLR/ItqCtZXr39hqHr16mXNmzd3/R21D41gDnVQSbjbjhs3zvVFLFasmN1777123XXXndDMfNttt9mOHTvc13iSkJjWkCaky549e6xo0aLur6Pg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sR9//DHahxezVKlW1VXnKVykijjVokUL+/LLL12TtOjrF1984ZYDkdL8iqqOqb/U6NGjCYo+9u3bd8IyNUVL8u4hJ7P8+fO7CmLwrXv37laoUCH3/dVXXx3tQ4zp95X646lvnpqnkbSyqD9ok4do/bGmQS84kZrtFRZvu+02N81QuOizGKcee+wx1zShH/yNN97o5udS886ECROifWgxRx2f9UHu/WJZvHixq3hoAldVYvH/aCS9fgHrnKj56+effw6cGs37VqBAAU5Vsr6KmqNMFQ29pwYNGmS33367Va5cmfOEiGjAhvrdaa5OBcWhQ4e6YPTwww9zRpPRYDI122ueYTXVax5dfQ6qco0TTZkyxdasWRP2wBYPU+fEsT/++MONwFRfDX1A9ezZ06pXrx7tw4o5mlpB5ym5u+++22655ZaoHFMsUjjUhMAp0cTchKCkvv76a/cHm/orKmBriiF92HPFm9Rpui+NHtf5Q1KaVuiVV15xwUfNrPoj7b777nOTTuNECxcudFMN6Q9dTY5/1113uVlBcCJdmEJdjPSHbiQIiwAAAPBFn0UAAAD4IiwCAADAF2ERAAAAvgiLAAAA8EVYBAAAgC/CIgAAAHwRFgEAADLxmswrVqzI8iu2aBLujLr+OmERAGLUddddZxdccIG76XrKadE63vraFkD0fPDBB+6KPLpYxqWXXpplz/v6669byZIlrWHDhlatWjV3Za4tW7aka5+ERQCIgv/973920003pbrOb7/9Zpdffrm99tprljdv3jT3qXW0rrbRtgCi5/vvv7dnn33Wnn766TTX1VVoNm7cmO7n1GVIdXWyN954I1BZLF68eLqvVkZYBIAo0OXudEm3tJx++umuUpgjR9q/rrWO1tU2AKLr9ddftyZNmqQZ7s4880z3/7Zu3bpWtWpVmzlzZsTP+c0337jrZd9www2BPyAfeughmzp1qrtEcKQIiwCQxf7zn//YZ599ZlOmTHEfEpdddlnI2x46dMgGDRrk9tGyZUsbMmSIHTlyJFOPF0Dm9GVs3bq1qz6qAqjrzCvYXX/99bZr166I9lm4cGHbvXu3HT16NLBs27Zt7uvcuXMjPtZcEW8JAIhInz59XH+i4cOHW61atSx//vwhb/v444+7KsFzzz1nBQsWdIGzf//+1q9fP34aQBwZPXq0qySed955tnz5cktMTLRmzZpZr1697Oeff7Yrr7zSrbd06VL3mJ8CBQpYlSpV3Pc333yz+2OyY8eOds8999jWrVvtiSeesJw5cwZCYyQIiwCQxRQO9cu/TZs2VrRo0bC2nTVrlt1xxx2uIiHqOK9qI4D4snz5clu1alWgydijbiT79u0L3Fd/w+BKYXK1a9e29957z31fqVIlmzNnjj3//POuSqn+iurHrBCaL1++iI+VsAgAWWzRokXul3q4QVHUZK0PAn14tGjRwn1QhDL4BUBsyZMnj9WrV88mT56c6noLFy4Ma78aff32228H7i9YsMCOHTtmZ599dsTHSp9FAIhCWDz33HMj2lZNzq+++qotWbLEWrVqZRUrVnSd2gHEl4YNG9pPP/1k69evP2GORIW7SCXvwzxq1Cj3e0LPFynCIgBkscWLF0ccFkVzKL7zzjtuaoy2bdtat27dMvT4AKTfP//84/6o0+AVtQToe90OHz7sHu/UqZOr9mmqqwkTJtjs2bNtzJgxVqdOHdfXMFIa+Kb9/fLLL/boo4/aW2+95W7qtxgpmqEBIItpglwNTom0stilSxc77bTTLCEhwTVlpacvEoDM8dxzz7k+xqJJstu3bx+YNqtChQqu77KmznnppZdsxIgRru+x/ohU/0P9/47UyJEj7ZlnnrGVK1dajRo1XB/G9PxxKoRFAMhit912mxsRrSs86OoKmv4mVGXLlnXT7SggqrlJcyuqygggtowaNSrNdQoVKmRPPfWUu2UUVSs16X9GSkhMbTw2ACDTqovr1q1zFQe/SbQ1rYYCYalSpezHH38MTLGjX9saRalmJW3rTdity/01atTINWHlzp3b/vzzT356ANKNsAgAMUr9mw4ePOi+Vz+mtK7ioo7x8+fPd9+r8lizZs0sOU4A2RthEQAAAL4YDQ0AAABfhEUAAAD4IiwCAADAF2ERAAAAvgiLAAAA8EVYBAAAgC/CIgAAAHwRFgEAAOCLsAgAAABfhEUAAAD4IiwCAADA/Px/V6p18Qo5w/kAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out.plot.scalars()" ] @@ -203,19 +389,37 @@ "execution_count": null, "id": "11", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "growth rate: -202872114.0880568\n", + "growth rate: -202872114.0880568\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2QAAAIgCAYAAADqVqoGAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAsTZJREFUeJzs3Qd0VMXbBvAnvffeCBB6CxA6goj0jgVQAVEUQeyK9bP3P1ZUioqioCKCFEFsNAHpJaEFEkJCQkJ67+0774QNSQiQhCS7m31+59yTzd3N3dl7Q9hnZ+Ydo9LS0lIQERERERFRozNu/KckIiIiIiIiwUBGRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGRERERERERawkBGRERERESkJQxkREREREREWsJARkR1tnXrVqxevdogz2BsbCzeeustbTeD6llISAgWL158Q8f46aefcPLkSTQFBw8exPr167XdDL06p7pyzohIfzCQEVGdMZDpXiBjUNRuIDt37hzmzZsHf39/NAXyOmbPno2kpCSttUHfzqkunDMi0i+m2m4AETUNMTEx+P777/Hiiy/ecMjbv38/3N3dcfvtt8PBwaFOjy8pKcEbb7xxxeNfeeUVGBvr92dRKSkpWLBgAR577DE4OzvX+Tj//fcf/vrrr6ve36dPH4wYMaLGxzt69Ki6FrNmzbrmvoZw5swZ/Pjjj+jbty+GDx9e6fm3b9+OJ554Ao3ho48+wt133w0bGxv1vbwp//zzz8vvHzNmDHr06HHFz50/fx6bNm1CUVERbrvtNvj4+NTo+Wp6/Lr+vJubmzqfX3zxBV599VVoQ9VzKhISErB582YkJycjMDAQt956a42PZwjnjIj0i36/KyEinQpk77zzzg0d45lnnsFdd92FCxcuYNmyZejatSvi4+Pr9HgJZK+//nqT/JRaApm8Nvlalbe3N/7v//6v1sfMyspSx7x48WKd2yXh58svv7zuvoYKZNL+qVOnIj09vdLzf/LJJ2gMEqZ++OEHTJo0qdr7pR0ynK0q+ZmOHTvi33//VcFs9OjRCA0NrfXzX+34N/rzd9xxh/r3pQ3VndMVK1agX79+2LZtG6KjozFt2jSMHz9e/ZuvraZ4zohI/7CHjIhqLC8vD7/++isSExNx0003le/PycnB119/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G651/KqSk5PVsDwZaqchr10KY0jAqOlru1pb4uLiVEiRUCbtktciwxhr0oN0reer7pzI8aWgiMzjkjmAN3IsDelllF5LCwsL+Pr6qoIgcoxffvnlisf26dNHBb2Kc/WIiKh6DGRERER0TbJItPSsacKq9JjJUEwJY7fffnulx/7zzz8YOnToNecIEhHRZSzqQURERNfUsmVLFbykx1B636S37JNPPqkUxqRHTuabnT17Fs8++yzDGBFRDbGHjIiIiGokMTERhYWFarhqVfn5+SqUSVEVKSJCREQ1w0BGRERERESkJayySEREREREpCUMZERERERERFrCQEZERERERKQlrLJYR1Jl6tixY2rysqkpTyMRERERkSEqKipSRY86d+4MS0vLWv88k0QdSRjr1atXXX+ciIiIiIiakP3796Nnz561/jkGsjqSnjHNiffy8oI2ZWRkIC4uTpUZNjbmKFRdJev2pKWlwdHRkddJR/Ea6QdeJ93Ha6QfeJ10H6/R1eXk5MDe3h42NjbQtpiYGPTt27c8H9QWA1kdaYYpShjz9fWFNqWnp6O0tFS1hYFMt/+oWllZwdnZmddJR/Ea6QdeJ93Ha6QfeJ10H6/R1WVnZ8PBwQG2trbQtuLiYvW1rtOY2J1CRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGRERERERERawkBGRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGREREREpNe2bNmCs2fPGnwbSD8xkBERERGRXnvsscewYcOGej3mP//8g4iICK22gQwDAxkRERERURVz587Fpk2banxebr31VrRq1YrnkWrNtPY/QkRERETUeEpLS3H8+HHExcXB19cXHTp0qJefkcecOnUKsbGxaN26Nfz9/cuHQObn5yM0NLQ8lPXv3x+Ojo74888/0a5dO3U7JCQERUVFuOWWWzB+/Hg0b968Rsevabt9fHzg4eFR6X7N87u6uqpjSzs7d+4Me3v7Wp+Dq70WERMTo47v5OSErl274sCBA7C1tVXPVVxcrH5Wbvv5+VU6ZnBwMDIzM3HTTTdd97VSGQayJmDuzydwITUL9tbJsDIzgZWZMazNy75qvrdS31e8bVzlsWW3bS1MYWJspO2XRERERPWs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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "t_fit = 2.0 * out.sim.model.units.t # in seconds, like every time coordinate of this run\n", - "energy_plot = out.plot.timeseries(\n", - " \"electric_energy\",\n", + "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", " fit=(0.0, t_fit),\n", - " fit_amplitude=True,\n", + " #fit_amplitude=True,\n", " title=\"Electric-field energy\",\n", ")\n", "print(\"growth rate:\", energy_plot.fit_results[0].rate)\n", "\n", "# the same fit without a figure\n", - "print(\"growth rate:\", out.analysis.growth_rate(\"electric_energy\", window=(0.0, t_fit), amplitude=True).rate)" + "print(\"growth rate:\", out.scalars.electric_energy.struphy.analysis.growth_rate(window=(0.0, t_fit), amplitude=True).rate)" ] }, { @@ -230,12 +434,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "13", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzwAAAIgCAYAAABAlWy6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAVcdJREFUeJzt3Qd8FNX2wPGT0IuhBJFeDCpKiUoREAsgiqIoWEBBwaeCBQtgVwQUfT4LYm+APLEhKiqoWJAiYgQBg0oVpCO9d5L5f871bf67qbvJ3cxk9vf9fOaT7GTn7rTdzNlz75k4x3EcAQAAAAAfind7BQAAAAAgWgh4AAAAAPgWAQ8AAAAA3yLgAQAAAOBbBDwAAAAAfIuABwAAAIBvEfAAAAAA8C0CHgAAAAC+RcADAAAAwLcIeADk2/fffy8fffRRTO7BDRs2yPDhw91eDVi2cOFCee211wrUxvvvvy+LFi0SP/jll1/ks88+c3s1itQ+9co+A/D/CHgA5BsBj/cCHgIxdwOev/76S+655x6pW7eu+IFux8033yxbt251bR2K2j71wj4DEKp4pscAkC/r1q2Tt99+Wx588MECB1Fz5syRqlWryuWXXy4VKlTI1/PT09Pl0UcfzfL8Rx55ROLji/Z3Pdu3b5cXXnhB7rjjDqlcuXK+25k9e7Z88803Of69VatW0qlTp7Db+/XXX82x6Nu3b67zomHZsmXy3nvvSevWreWCCy4Ief3p06fLXXfdJYVhxIgRcs0110i5cuXMY73ofemllzL+fvHFF0vz5s2zLLdmzRr54osv5OjRo9KtWzepWbNmWK8Xbvv5Xf7YY481+/Pll1+WIUOGiBsy71O1efNm+eqrr2Tbtm2SnJwsHTp0CLu9WNhnAEIV7f/6ADwV8DzxxBMFauPuu++Wq6++WtavXy9jx46VU089VTZt2pSv52vAM2zYMF9+y6oBj26b/sysRo0a8vDDD0fc5t69e02bf//9d77XS4OLN954I8950Qp4dP179eolu3btCnn9kSNHSmHQYOXdd9+Vq666Ktu/63pod6fMdJlGjRrJzJkzTeDTuXNnWbJkScSvn1P7BV3+iiuuMO8vN2S3T9955x1p06aNTJs2TdauXSvXXnutXHrppeY9Hyk/7jMAWZHhARC2gwcPyieffCJbtmyRtm3bZszfv3+/jBo1Sg4fPixDhw418/Rb6qZNm4bd9tKlS803uT/99JOcccYZkpaWZi5q/v3vf2d7wRru8/v37y8NGzaM+Cjv3Lkzox39Zlm/RT7//PNzXebAgQMyceJEs390Xfbs2WOCEr34UbqO+i2+Xpxrt5dLLrlESpcuLba7tI0ZMyZL0KNZjgULFpiMkL6u/tR11ElpoPPss8+arjgaOO7evdvsX9233333nVln7VakF6Cff/65CXBPPvlkufDCC03GTC/UP/30U/P6gXNAL9wzz9NMjwZlui/0G3q9sK9du7a5YA3si/nz55tJs0vffvutmXf99dfnue2a3dNjrYH3f/7znxyfl9trF4Sus75HdP8FVKlSJWPbs7sAXrVqldxwww3mfXXRRReZeRqwaQAajrzat7G8niO6ntq1rH79+nm2GTh++n75+uuvTSCi55wedxv79JRTTjFdD8uWLZvxHm/QoIH5LDjzzDOL5D4DEF0EPADCcujQIRPk6MVLu3bt5KabbpJ9+/bJaaedluMyGgDllfXRLE358uVN16p69eqZC2xVrFgx6d69e47ZgXCfr4OdtX29ENYLSn1epDRrdOutt5rtfvPNN7N9jl6U6UWOtn/uuefKLbfcYoKG008/3QQ8ui90eb2Y1e4uWuxBu7v8+OOPUqlSpYj2VThjeAIBj+M45vV///130/VGX0+7Hc6aNSvXCzFdd82YaADXrFkzqVOnjtkP7du3l+OOO87sdw1yn3vuOXNRGwkNBLULUlxcnJx99tkm+NB11gtW3T69yNX1f/755+W8886TJk2ahN22Bm3nnHOO2f96fkT62tntz9wyVBoo3X///eb3xYsXm/1UvHj4/1o1e6HnZlJSkjz99NOSmJhoAsVwu7QVBg2O9RzVogHhBjx6/J555hkTtK5YsUIeeughmTFjhslkFXSf6nsqWMWKFc3x1EC2qO4zANFFwAMgLHpxqxeLv/32m5QsWdIEQHrxovSb1htvvFE++OCDjG8+lV7Eh0u7ptSqVStknj7W+fl5vmYdAv3n9QJLB6Lr+B296EpISMhzffQiKnhbNEjQCxe9ENOL08w0ENJ9ot8868WZbntg/wT+rlkRzSqUKVPGXJzpGJknn3wy12xEQem4Ku3Wpcct8I24jmfRbfvvf/+b5/IDBgyQPn36mN9vv/128y29diUK7N8TTjjBvIZmYC67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AACAbxHwAAAAAPAtAh4AAAAAvkXAAwAAAMC3CHgAAAAA+BYBDwAAAADfIuABAAAA4FsEPAAitmHDBmncuHG2019//SUzZ840v+/Zs8f63rXZ9pQpU0xbaWlpVtYNkTnvvPNk7NixGY+jed5k135hHf9obxcAIHcEPAAidvjwYfnjjz+kV69e8sEHH4RMNWvWlN27d5u/R+NC0mbbO3fuNG05jmNl3RCZJUuWyNatW/N9bL///nsTSBw4cCCs52du3/bxz2l9ovl+AADkrXgYzwGAbNWqVctc4GV2zjnnyG+//SYJCQnsOYQt0vMm0kAi2udlTuvD+wEA3EWGB4B1CxYskB49esi+fftCug6lpqZKnz59pHXr1tKlSxeZNm1ayHJ6sRjoGnfqqafKJZdcIm+88Yakp6dH9PqB1/vll19MFuqMM86QSy+9VGbPnp3t8/V1C7pehw4dkqeeesp009J2+vXrZ7r3Bdu2bZs8+OCD0rZtW2nZsqUMGjRIduzYkeu25NVuuNsa7r7VdXz44Yfl7LPPNus5dOjQkIxFfrZBLVq0yJwTp59+unTu3DnLPs7uvMlt27Wb2J133ml+1/XQ7briiitC9sn8+fPlyiuvNK85evToLO0Hv25u++7999+Xs846K8v6nnvuufLOO+/kuT7Zve7KlSvN9rRo0ULOPPNMGTx4sAmYAsJ9zwAAwuAAQIT++usv7QPkjBs3Ltu/T5o0yfx9x44d5vH7779vHp944onOe++956SkpDh33HGHU7JkSWfZsmUZyx04cMD57bffzLRgwQLTfu3atZ377rsvx7azE3i94447znnrrbecWbNmOTfffLN5PW038/NsrNddd93lJCUlOZ9++qkzZ84cZ9SoUU7r1q0z/r5582anfv36TuPGjZ3PPvvM+f7775127do5zZo1c9LS0nLclrzaDXdbw9mGv//+26lbt67TvHlz55NPPjFtDR061Bk0aFCBtmH9+vVOhQoVnCuvvNKZMWOG8+GHHzp16tRxihUr5jz99NM5Htvctn337t3O888/b56vf9Pt+vPPP0P2Sb169Zx33nnH+fXXX50tW7bkeF7mte9efPFFJzExMct26XLPPfdcnuuT+XV1P2t7F110kdmHui9POukkp2XLlhn7MdxzEwCQNwIeAPkOeGrVquU0atQoY9ILMpXTheXMmTMz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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "phase_space.struphy.slice(\n", + "phase_space.struphy.plot.slice(\n", " x=\"e1\",\n", " y=\"v1\",\n", " t=\"last\",\n", @@ -249,17 +464,28 @@ "id": "14", "metadata": {}, "source": [ - "For a compact view of the evolution, `.struphy.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." + "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "15", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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fsBXL3oka80qSj9dA0F2IZ7KdpT1unXd6HxWGWDvvfdO+3f9c07BWf2uAuzKc1T20NjDQfWaUdLx1bAjyrMif0fXoO6DW221lS0PlUZFPGYAAAAoAx4AAABCb7fddlO/pt7GjRujpn/11Vd2+lFHHVU0bauttvL23ntv+9F3kZ+zzz47avlDDz202Dz6dOvWzVu3bl3RfAsXLvSaN28ed95HH320aL7x48d7DRo0iDvfww8/7LSvrr+l/dxrr728Hj16FJvvsMMO8woLC1PeTxk3blyJ+zB27NhA99V1HTfccIOd9u233xZbxxlnnOFVqVLFW7BgQdG0ESNGeNWqVSu2zho1angffvhh1PKdOnXydtlll6hpmvfMM88s9lvPPPOM/W7MmDHeypUr4263PjqGJa07lW1L5VzOlJdeesnLyckpti8XX3yx/ffHH3/slFbXXXedTS+Xz9VXX130+1OnTrXrOfroo4tt2/vvv2+/i5w/nlTXcc4559hp/fv397p27erts88+3vXXX1+0L6WRyrUa7/x68cUX4x6Piy66qOh4pHMupnt8EuXfseLtT7r5TMOGDb22bdtGTbvggguKjmXken744QfviSeeKJZu+tSrV89ug8ycObPE8zjRd76PPvrIq1WrVrHvlX+9+uqrUduqaYcffrjXoUOHovl69uzppUPpoI+uwyZNmth03rx5c9H3xx13nF3/n3/+mda2Kq379esXNU3XiOv5cs8990Qtq/tao0aNku7XrbfeWuyc9s2dO9d+17Rp06TnXWkl2o7yWPf06dPtPIMGDSrVb+nY6ji2a9eu2HEfMGBAuR/fSC1btvR23HFHLygV5ZgBAAAgeHRPDQAAkKWWLFlibr31Vvvv7bbbLuo7td5Qq7DnnnvOtpZRK0K1ZnzqqadsN4c+tay78MILbUsztT5TCyyNsffdd9/ZFnm+d99917Z2U6tmtSLTvGp9eOSRR5r169fbeTQepFrkLF++3LZOU6tQtdB64oknbCsZtd7R2JvJuPyW74svvrDzPvvss+brr7+2+6cWd9oPLZPqfqo1mfbh77//tq0qR4wYYdNv2LBhZvfddw90X1NZh9+NZWwrO7Xqe/31121LJL+loVoinnnmmbZlslogaxxErVut7NT95amnnlosHdOhFpHq7lZd4KqFVGQXuGoBH0862+Z6LmeC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Wvlj5rrwz20/T9D1outCQXSNx+nzW1lrHOVEglhHZJAt2UsXQeVJsfwXSvTSSWwPBDpGpTkX0z0+pRFEPuMiMi3US0OytEh0Hpf0nb8vat3qj5MdK9nLDUHRNiqorxeRFDwui231x7x1ETvmbbt27eyLF6Wxww472LJC48aNi5UZ/Osk2UsY2RR8XLNmjU230lIvCQoOR7YE1rH8448/osoEZXl8Nea4WsBrfHb1uqLztaIJ8pgBAAAgWASNAQAAKqkVK1bY/6tr40MPPbSoNdnjjz9ugz+RXV6re1RVoqobXr8ljipWFWD67rvv7N/qxlUtaFTJ2a1bN9t1tk8tGLVedTetQJACECUFB11+y6eAsVqKPfXUU7YFz/Lly23X0qoMP/DAA1PeT+2DWlH++9//tsETte5Vha1anqm1jyqNFRBKZV9Lks461EX1K6+8Yj9qcaxtVJfAkfbbbz8b9NM+qLXnnnvuaad/+OGHNoiqAELv3r1NIkoTtWJWoE1dD/stn7RNsRSw//XXX23LO+1PIkFsWyoSnWdBULehOj/UhfWOO+5oj6MMGzbMflJJq9tuu83861//cvpddePu03mvVoPvvfeeufbaa+161CpNLX7Vta7OZ78785Kksg4FTxXEPP/884uCa7o+HnnkEXtO6pr1z5lUpXKtJjoegwYNskFuP3j76quvmhdeeKFU52K6x6c0gshnXOjYq8viq666ynb5778goHxG+Z5eAogMrOs8njFjhg3ix7aALOm7I444wl4nOp+UX0e+ZKH5df7o3CsvBx10kNlnn33MfffdV3TdBrmtjz32mL0/uWjUqFHU3zoX9RvqLSPdc0kvGem46qWPZ5991t47dG3ohSN/25VnVQT+CzolvViRCvVOcfHFF9thP/TCioLFellN/DJFWR1fvWyhIRV03ulaHDx4sKmogjxmAAAACFgZjJMMAACAgO22225quuVt3Lgx6bxbbbWV17t372LTP/74Y7uOl156yf79008/eVWrVrXTmjVr5m277bZerVq1vIYNG3odO3b02rZtW7TswIED7Xw5OTleixYtvG222cbLzc210/r161c03/3332+n6ZOfn++1a9fOy8vLK5r27bffJt1+19/SfhYUFNj1V6tWzc6n/2u+5s2be0uWLEl5P+W2224r2l7Np/X6y48ZMybQfU11HZs2bbL70LJlS/v9oEGD4q73/PPPL1pH06ZNvcaNGxf9feWVV0bN26lTJ2+XXXaJmvboo48WHYNWrVp59evXt3/vtddexdLhzDPPtNPq1atnj4fW99dff5W47lS2zfVcziR///XR9uo46t89e/a0/9e2uqRVaUyaNMmrUaOGXXeDBg3sOetv0/XXXx81r84Z/e748ePTWsfMmTOjztk2bdp4W2yxRdG0++67L2q9L774ov29F154Iel+pHqtxju/TjvttKjj4Z+7u+66q/3/6NGj0zoXg8i/tb3xPq+88kqJ+5NuPqM0i02vCy64wC6zcuXKYvMfeeSRRevccssti9JNnyFDhkTN26dPn6LzxD+PXb67+uqri9ap7dO+1K5du2javHnziuatUqWK179//7j79sADD9j1vv/++54LpUNsWsg333xT9Nv6/Pnnn2ltq76PvDeVlp+/vf7668W+e+ONN4rOmyZNmtj5WrduXTRN16fvhx9+sNeP5tHxVBpUr17d/q17699//512mqayHWW5brnkkku8mjVrRu1POr+rc27nnXe2aaS8UL+pafr9Dh06eGvXrvXK8vi+++67drp+s6S8Yv369RX6mAEAACDzCBoDAABU0qCxPP/8816jRo2KKsL9YJKCg5GV7FOnTvXOOOMMr27dulEB1XPOOadYpd9rr73mde7cOaoyXoGlO+64w1u1alXS7Xf9LX8/FcDceuuti+bt3r27XUck1/30PfHEEzZY6s+vys0LL7zQW7ZsWaD7ms46Lr/88qL5YitiI4PLN9xwgw1O+vMqiKOAeGFhYdS88QJVOs8U4PQrzOvUqePddddd3jPPPFMsaDx79mxvhx12iNr+hQsXlrjuVLYtG4LGqsQ/99xzi15YULDhmmuusedcbNA4UVqVloKhCqz661VgUQHi2DT1A9eRxzCVdaxbt8679957ve222y5qPxQgHD58eLF1al59/9RTTzntRyrXarzzS9s3YMCAqONx7bXXenfeeaf9+7vvvkvrXAwi/y7po5c0StqfdPOZVIPGSreLL744KjCqwP0jjzxSbF4FI7XuyO1x+c7PW/UygP+dXkzRuTN06FBvw4YNTkHjK664wi773nvveaUJGstRRx0VN2icyrYGHTTWuacg9eGHH17sO21TonNp8uTJUfN/+eWX3k477VT0vdL1kEMOsXlR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RzrzHXzwwUW/v/3229vAdYcOHQJ7/nbdDr3w2bdvXxs079Gjh3nxxRdtXq+gC4Dsc//995t777233H/3wQcftM+/eslPLyLq+fzII48MbNnevXsXy7s137vvvmuaNGlSNN/bb79tG8F89dVX5uuvvzZffPFFoPupvFF55Msvv2z22GMPU1hYaPPO9957L2q+Tz/91Jaf9aKS9k0vlqucq20CgIzwgBCpXbu29+ijj5brb/bs2dM76KCDiv7esGGDt+2223pnnHFGYMsGPV867rvvPu+5554r1/0BEE6nnnqq17t373L9zfnz53srVqyw/9Zvp/L7P/74o1e1alXv9ddfj5q+aNGitOY799xzvebNm3vr1q0rmvbQQw8p8mG3s7Tuvfder1atWt7ixYuLpl1++eVekyZNon6za9eu3j777BO17HnnnWfz08LCwlJvB4DyNWLECJuPrFy5MhTb8dVXX5XZsvHm27Rpk1dQUOCddNJJUfOuWbOmKP8vjRkzZnhVqlTxhg0bVjTtiy++sNsxevToomm33nqrV7NmTW/hwoVF0z788EMvJyfH++GHH0q9HQDKn679Z555plx/c6+99vLOOeectJbt0qWLd/jhhxf9rfJfq1atvPPPPz+t+WIpT61fv7536aWXprV96WzvTz/9ZPPRp556qmja6tWrvWbNmnk33nhjINsBoHwdddRRXr9+/cr1N7/++msvNzfXe//99xM+swe5rPJMPZ+fdtppUdPnzZvnrVq1yv57l112CTwtbr75Zi8/P99bunRp0TTdV1q2bOlt3ry5aFr79u29vn37Ri178sknezvuuGOg2wMArmhpjNBQiwW1zFArMbXS0EctWcuS3szVW7UnnXRS0TS1CDvuuOPMG2+8kbCFhuuyQc8X2dpXLSbUukJdBupNtES+/PLLoi76ymN/AITT448/bltqTJkypSivVZfRZU1dLKXbhd7QoUPNtttua4466qio6erOOZ351O1e69ato7oWVGsN/7tIanFx9913254wLrroIpuXJqP0POCAA6LeYlaeqS6x1TOD6H73/fffm/333z9qWf2trqoT5dcAwkdd0fstNZRfKG+9+uqrM7ItaoUmJXXLF8Sy8eZ7//33bQuJ66+/PmpedVsam/9v2rTJtlBTl6vqUlBdom7evDnhb44YMcJUrVrVHH300UXT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bT/xfPzxx95xxx3nLVq0qGja119/7eXk5Hhvv/120bT777/fq1atmjd58uSU59NvFxQUeCtWrCia9vjjj3sqFn3xxRdF00466SRvv/3281avXl00bfPmzTavXbZsWYn7N3LkSLsdn332mf1706ZN3v777+9169Ytar4JEyZ4c+bMKfr7p59+8ho2bOjdcccdJa4bQHh99dVXNh/59ddfy+03P/jgA2/+/PlR09588027HcOHDy+apjxLeavyp1SXdZ1v/fr1XuvWrb0zzjjDKywstNOmTZvm1ahRI6qM+Prrr9u8burUqVHrnDhxojd27NgS91X7oOUuu+yyomlDhw71qlevHpXmyrPff//9or9XrVpl8/1evXrZPBxA9mnevLl31113ldvvKU/573//GzVtxIgRtnwXux033nijd/3110dNU37bsWNHm//IzJkzvVq1atlyaarz/fLLL0VlSt/vv//ubb311t4RRxxRNG3JkiVegwYNvIcffjhqXk1/+eWXE+6v6/a+88473oYNG4r+vueee+x806dPT7h+AOF03nnn2fJReVLeqjLkJ598ElXfqvKi8h6fyqzKmyKfu12Xjay7bNy4cVS+VZJddtnF69evX9zvDj/8cK9v377e2rVri6bpGf/ZZ5/11q1bV+I6X3rpJa9KlSre+PHj7d/ajt12283be++9o+ZTPUZkWVtl4rp163oPPvhg0u0GgLJA0BihctNNN3l16tSxN2oVDubNm1fmv6lga8+ePb0WLVrY361fv753zDHHeBs3boyaT5VNO+64Y1rLBj2fChAdOnTw2rVr5x122GG20KGHxkceeaRUQeOgtxNAOH377bdezZo1vT333NPmta+99lqZ/6YegvRb+jRt2tR+/L8jA8SPPvqofRCMfejTA5O2ed9997V5nh6inn766WK/4zKfKt+23357+wCphz89IOpBM/alpZUrV3rHHnusrYA78MADvYMOOshr2bKld+KJJ0Y9MMZz3XXX2e3o06ePrYRr06aNN2PGjKh5FMju3Lmz3VYFp2vXrm2X84MtALKLgqadOnWy5TOVi2JfNiwLH374of3NHj162DKZyqp6kU9BjMi8ZOHChTZvvfvuu1Ne1nU+/2UYlQ+VtynPVB6sCrvYMuJ9993n1atXz+bTKstq/Xqx5ocffki4v3qxSOVOlUOVdypY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119/bebMmWM6duyYVef5999/b8/Xxo0bm7333ttsscUWTsvp/Pjmm2/M7NmzTYMGDcyuu+5q6tWrZ7LFxIkT7adbt25mp512SulYjx071u73NttsY3r16mVyc3PLPf1Lu2wmlSYNFy1aZL777juzceNGs8MOO5i2bduabLF+/Xp7P/zzzz9N586dUzrvfPPmzTMffvih2Xrrrc0BBxxgssGKFSvMmDFjzJo1a0yPHj1SPmbKm7/99lvTsGFDe0/KlvN88eLF5vPPP7d55V577WWaNm2a0j7/8MMP9t877rhjVp3n2m+do3Xr1jVHHHFESsv+8ssv9j5cu3Zts++++9p1pEt5hdI/JyfHpn+TJk3KZdlMSzcN165da6+z+fPn23xZ12pp7m3lbdKkSbbcrWOl+2H16tVTWl7X6QsvvGCfW04//XRTpUoVkw330q+++sqWPdu0aWPvpTpnU8mbVd7XvUnl9VTyqLDcS7fffnubR7patmyZLbsuWbLEptkuu+ySUppler8/+OADu9/HH3+8qVOnjvOyy5cvt/dhXecqr+saz0T6l2bZTCtNGv74449m6tSpthyjvDWbnpcWLlxo74fKE3U/1DNHqpRuKtfonqTrLhvMmDHDPm/oHqp6gVSut82bN9vnHJXZdZ7rky0mTJhgpk2bZu8HupdWq1Yt5bqQRo0a2WsklbqUTFP5R+Vu3RNSOV7ab9V7/fbbb/bc7tmzZ6nuKemmf2mXzaTSpOGCBQvsdap16H5SmntbeVu3bp3NG1UeUZ2GPqnS9fbJJ5/Y/e7du7fJBiqHab+1/zrWrVu3Tjlv1rmu8r7KrqmW9zNF5+oXX3xh76W6PpVPprLPKkdoWdUzp5pmmd7vjz76yO7voYcemtKy06dPt9e3yky6D+vZFshKXgU2d+5cb7vttvM6derknXjiiV6DBg28/v37e5s3by7TZTPtm2++8Zo0aeL17NnTO/roo71atWp5gwYNSrrcxIkTvW7dutl9PfbYY71evXp59erV855++mkvG7z66qte7dq1vYMOOsjr06eP3e9hw4altI5ff/3Vy8vL83RpPPHEE142uP32272aNWt6Rx11lLf77rt7DRs29L788kunZX/55Rdvhx128LbddlvvpJNO8nbbbTe7nrDTdXjKKad49evX90444QSvc+fOdh9mz56ddNk///zT69Kli9e8eXPv+OOP97p3727XM3LkSC/s3nzzTXu8tP05OTnerbfe6rzsihUrvD322MPbeuutbV6m/+vvlStXlmv6l2bZTEs3DdesWWPPtS222MLmT4cffri9Zk8//XRvw4YNXtjp2OgY6VjpmOnY6Rimcj/UvEornbfKn7OB8lHlp8pXlS/qmCm/daHjetppp3n5+fl2WR3zHXfc0Zs1a5YXdi+99JK9fx588MHeIYccYv+taS4GDhxoz/PDDjvMfvTviy++2As73fu1vVtttZXXokULe6xSccstt9h0UplL99HGjRt748aNS2tbVG7RunSdKL9QuUblm7JeNtPSTcMHHnjAa9Sokb03Kp9t2bKlPX7Tp0/3wk75ou4lKnfrWUPPHHr2+O2331Jaz5AhQ2zeqrLr2rVrvbBbvny5fcZo1aqV3X9dc3vvvbe3atUqp+Wfe+45+3yy77772uXbt2/vDR8+3As75f9t2rSx56rupbo/nHHGGV5hYWHSZd966y07f48ePex53qxZM/vM9tdff3lhp3uAytwqQ+gcTaW899lnn9nrY8899/SOPPJIex++8847yz39S7NspqWbhmPHjrX7q7z4uOOOs+ee1uNaHsi0Z5991u7roYce6h144IH2fvjGG2+ktI6pU6fa+5LO22zZ7+uuu85u8zHHHOPtuuuuXtOmTb3vvvvOeX87duzodejQwdYL7LLLLt6pp57qhd2mTZvsOaoyu+6lBQUFdh9+//33pMsuXLjQ23777e19SHnrzjvvbNfz4YcfemH3yiuv2HJD165d7Tl69913Oy+7bNkye360bt3a3kdV/tU9dfXq1eWa/qVZNtPSTUPVJ6isq2ckPWfp+aNGjRreOeecY9Mj7GbOnGn3WWVtXTMqjw0YMCCl+6H2U/mLyq7ZUPcoY8aMsXUge+21l3fEEUfY+8u9997rtOy6devsOaLldez79etn6/MUcwi7p556yu5r3759vQMOOMCrU6eOLZO60Hmhc1vppWfT6tWre9dcc40XdtOmTbP1IHqmVPlVz6WpuOqqq2yZQzEVlZ20jkmTJpXZ9gJlqUIHjffff397I/cr5VWBpEzrkUceKdNlM2njxo32oVY3Jd9HH31kC5KjR49OuOyjjz5qCy0qAEVW4lWpUsUWDsJMBX49IEVW6KsSTYUx18o3Vd7pgfrcc8/N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hJn2O16LEpf91oOQAh8K4mSb0hxvVVaOHz/ePP/887YloIwcOdIGUvfee297gw+j0l6ffiWt3nRTixVVNuphOswtBUoryDytMuetQW27WvWrolcPTGF/Iac0+638RS019FZttr2QUdq8VW+fq7WuAlhaRg+JeqtWlW96YKqI+63vVVHg31tE/1YAXd9V1Mq3IMtdpVlXkNtR3oLadt3PVUmha+3WW281FTVv9YMaesmvR48eJpuUZr+Vn8iXX35pWyepJZheTlFgQ+VY9Zaj/KailiFUVlWPV6po1wtJCuz4aVIRBZ23VtayaxBpqPWceuqptvJbL8GFuReH0u63yuZa9vrrrzfZprRl16+++sq2MNZ9VF566SVz4okn2noBtZILo9Jen/pe9xL13KBegdRCVC3pqBco+/TP9rw1qG3XS2933323fdE6toViRdrvL774wjzwwANm7NixUS1XK0Peqhf+lM+oNzKtR4FFPasoiP7www+bilovoN721Lpc9QJKB61PdUL6f2xPJhVFNj+PA/GEu8RfCmptKwoGRr79q791EaswWBbLZpq2PV43bdp2f79c6O1avWH52muv2Tf5w0779ttvv6W838q477jjDnPSSSfZApvoLTm/UkoFGj0kh5X2TfuYzvFWCx51g6N99ylQrDez1d1fWIPGehtPN93Y/fb/TrbfRx55pH0JQl2S+5UeQ4cOtQ/IqoTUG/QVTWSelk6aBZX+pT12FSENdW3179/fnHHGGVnRQihyvyPfHNbfOpaJ3o5V3qquidSFlz6iNyzVKufJJ5+0b/RvueWWpiLmrf6LV/4Dkd7I1RvG7777rgkz7dsff/wRd7+bNWuWcNkhQ4bYSg8FNfyWgupCVoENvV17xRVXmIqoNOdKKutK1mKhNMtWhDTUixkHHHCAvb9/8MEHoQ0e+vQsoWeKdO4pevFIvaSoVZjyUtFLcPLss8/arr3D+kJSae6lft6q8poq30RpqLz2lVdesWW4sAbRI/c7srcq/a2yqHr+KYnun4cffrh58MEHzXnnnWenqcXGXnvtZY4++mgb6KmIgs5b003/0ixbUdJQ551at6tVVNiDGsn2O1GvQKoD0DOh8hS17vd7hRC9AKnKYAVRs2m/9WLNihUrnMqu2j8/YCx6Iefcc881n3zySWiDxn69XLrPZ6rvUPlW9QL+y4133nmnfYlegeSwl58yXS9QmvQv7bGrCGmoVu2nnXaa7UnkhhtuMGEXud+RPY3pb/V6o+e9ktx+++32mVBDlekjfg+PKsvqWgxrb55B1Aucc845RcFE5Tn9+vWz9UJhVppnS9VzPfroo7ahTsuWLe00/Vs9leja98uzFU28NFPMQeWLMOdpQEkq5usd/xsLTVQAjKS/1Qd/5Hh9QS6badp2dR/ljzkUue3+fiWjt9zuv/9+2wJVlRTZQPum7oQi397RQ5IqWhLtt27cKqjpoV8Vbvr443GoxYI/jlVYad9iz1O1tNGLA8mOtwptetMr9k0oVbjqjbCw0jZrTKp416ck2m+1ztGbbnoQjHxLXsEcVcDpzceKSN3jqiAfL800Xd+XR/qXZtmKkIY6v1QZo0rexx57LO5biGGT6H6oFywSdYuqCiZVLvp5qz4KGKubcv07zONQar9V4F+wYEHK91LlrRLbQ0fY81bRvum+GdnCQv9WxVmy/VbrI3VrGNm1rK53TdN3FVW8+7BaqqiLtlTzND/9I8e50pvZKt8kW1dpls32NFSFuMa5VflXveOEtdIpkoLa6mY4nfuhuopTS2O9oOHnrf74eWoplWhc4EzTOPEqf8Xbb+WRsWOvxu63Wi3Ey1slzPlronupgliJWm4qKKznG7306NNLrSrLqothlV8rIqWZKpRju/NL5Zk2cl3+sqmmf2mWrQhpqBe+nn76afPqq6/aYSiygfZN49RG0nWiF9kS7beChnqpU/dOP2/1ux7VsmEeY7Wke6m68Ne+u5Rd4/Usp2e3MOetemlDL1Clcy9VOV+96ikvjewNR/UC2mf/ZayKRi/0an/jpZnKJqrvLI/0L82yFSEN1UWx6gRUntNLYdkg0f1QaZKo9ej+++9v85nIegGV8/Vijl9HEOb91n3Uf4moMtULxB5rbbPqyV3qBbp3714UMPbXp/qjylYvoHoUlePDnKcBJfIqsE6dOnnHHHNM0d/Lly/3GjVq5F133XVR87355pveqFGj0lo2bFasWOHl5+d7N9xwQ9G0GTNmeLm5ud4LL7wQNe8zzzzjjR07NmrakCFDvJycHPtdNpkyZYrd7ldeeaVo2ttvv62aU2/ixIlRx/GJJ57wpk2bVuK6/vzzT7uc5gu7V1991e639t932223eXXq1PH+/vvvomlz5syx+7No0aKiaZ9++qndz3HjxhVN++mnn+y58uyzz3phNmjQIK9Bgwbe0qVLi6adcMIJXocOHaLmmzRpkt3v9evXF01r1qyZ179//6j53nrrLZsWSpNsUaVKFe/WW2+N+93o0aO9F198MWrakUce6e20007e5s2b7d/6v/4+6qijyjX9XZcNI9c0jJf+yofq1atn7yubNm3yskn79u3tMfLp2NWvX9+78cYbo+Z74403vA8++CDhunbbbTevT58+Xtgp/1Q+qvw09j7z2muvRc2rc/ybb74p+nvdunVe06ZNvauuuqpomo65yhWHH364F2Y//PCD3UcdS5/+rWk//vhj0bRly5bZ/f7555+Lpl144YW2nKRySOQ9t2HDhva7bHHcccd5O+64Y9zvZs+ebff7jz/+KJqma133zchyxc033+zl5eXZ/U/FhAkT7L1I5RefyjVav+7PkeentkPlu1SXDSPXNIyX/qtXr/Z23313r2XLlvb7bHLttdfaayZyH3WP6Ny5c9R8Orba740bN5a4rvvuu88e/7Vr13phd9hhh3ldu3Ytupcqf9Q+69qL9Mknn3gvv/xy1LSzzjrLlhci00LPZrVr17b5UlgVFhZ62267rXfyyScXTVuyZIktF8SW5XSP+fDDD6PSQcf2/fffj5rv6KOP9lq0aOFli5deesnuR7zrVOmjc3z8+PFRz2O1atXy7rzzzqJp33//vV3HiBEjyi39U1k2bFzTMF76yy233GKfNyKfr7OB9kP7+N577xVNGz58uL3PTJ8+Peo4ar9/+eWXEtel81Xr0vkbdk8//bRXtWpVb9asWUXTrrnmGvvcpXulT/ur/db++959991iZb1vv/3W7vvrr7/uhdmVV15py92R5c8jjjiiWFnuu+++s/sd+Rym55kzzzwzaj7dd7TfX3/9tZcNdD/U9t59991xv//4449tnVGkQw45xNtll13ste+vo2PHjsXqSMo6/V2XDSPXNIyX/npu1HOm5vXLQtlA27rNNtt4p59+etR9pm7dut4dd9wRNa/2WfueiMqC6dRFlbfFixd7NWvW9O69995iz10jR44smqZzW+e4znXfqlWr7LNwZL3Jhg0bbLni+OOP98JMsQLtY2SZ6LnnnrPlgsj7ps4B7Xfkvee0006zZdQ1a9ZEzafz/uqrr/ayheqtVH8Vj57Dtd+R9e6PP/64V61aNVv/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QGLTpk0za9euNQcffLDNX31vvfWW/U5Wr15tX86R2rVr2xcjAQDJy6516tSxLzgq75TffvvNvPTSS0XzqDcyvy1H27ZtTU5ODskKAEnyVr2E88ADD9jecUQvPvbs2dPmqV26dLF1sKtWrbLfNWvWzObFAAA3RKmAgK1fv962zrjsssuighVqxRHZCu6II44wBx10kH0jzg9wAAASPxzus88+RQFjP28VP39VyzjlrTvttBPdVgNAChVv1157bdT0yLLruHHjbN662267mQEDBpCuAJCEghUzZ840V1xxRVHAODZvFfXucOCBB9p6AdUlAACSl10POeSQooCxn7eKn7/edttttuy6ww47mHfffZckBYAUEDQGAjZ58mSzceNGG9iI9NNPP5nOnTsX/f3DDz+Yn3/+mfQHgBSDxrF5q4YAUMsMufPOO+148hrTCADglrfuuOOOpkGDBlHBDrWG88uuylOVtyqPBQAk9/3339uexZLVC6hOQHUDAAA3kyZNipu35ufnm6222sr+/eCDD9qya69evUhWAEgRQWMgYBMmTLDdnmy33XbFKuTURQoAIHV///23mTt3rtl5552L5a3qnrpqVUbcAIB0y66xeasf7KDsCgDp560NGzY0rVq1KhbsIG8FgPSoTuCvv/6KWy9A3goAwSBoDARMBRV1ixo5FlFJwQ4AgHvFm8R7OCRvBYD0aBxNtXCLl7c2bdrUbLnlliQtAKQhXgBjwYIFdpxNyq4AkH7eqvpW1bvGTidvBYBgEDQGAhavoKJpwltvAJB+3qrghYIYsa3hyFsBID0zZswwK1eupLUGAJRTvYCCHRoSAACQXt6qMeDr1q1bNG3Tpk12qEDqBQAgGASNgYD17t3bHHvsscUKNVtvvbXtngoAkDp17XfddddFTfv111/N0qVLeaMYANKkSraBAweaHXbYIWo6rTUAoHQOOuggc+SRRyYNdgAA3G277bbmiiuuiJo2depUs27dOuoFACAgDAAIBGzw4MHFpsUbt+j33383q1evtv+ePXu2adSokWnZsiXHAwDiOP744+PmrRrLWGMa+5YtW2bHOFL++ueff5pffvnFBpyrVatGugJAjE6dOpn7778/atr69evNtGnTzKBBg4qmbdy40Q618scff5g1a9bYvFVl1/z8fNIUAOK4++67neoF5s2bZ1+C9F+IzMvLMy1atCBNASCOU089NW7eWqtWLdO+ffuoYQL1Wbt2rVm8eLEtu26zzTamSpUqpCsAJEFLY6AcxGutcdFFF5k+ffqYtm3bmmOOOcacc845HAsASDFv7dixo6lRo0bRtFdeecW27NDLOE899ZT9tyrjAABufvzxR9sCObLsqnxU+enjjz9ufvvtN/tv5bcAgNLVC6geQC2SVS9w2GGH2XoCAEBqeau6/c/N/f8wx3PPPWfLqwoYP/jgg/bfeqkcAJAcLY2BMrZ582bTuXNns99++0VNHzFiBGkPAKWgrv1OOeWUYhVvvIQDAOlbtWqVfaFRrTF8bdq0sS00AADpUS8OChjvu+++UdNHjRpFkgJAKdSvX9/0798/atqll15qPwCA1OV4nuelsRwAAAAAAAAAAAAAoAKge2oAAAAAAAAAAAAAqMQIGgMAAAAAAAAAAABAJUbQGAAAAAAAAAAAAAAqMYLGAAAAAAAAAAAAAFCJETQGAAAAAAAAAAAAgEqMoDEAAAAAAAAAAAAAVGIEjQEAAAAAAAAAAACgEiNoDAAAAAAAAAAAAACVGEFjAAAAAAAAAAAAAKjECBoDAAAAAAAAAAAAQCVG0BgAAAAAAAAAAAAATOX1f40M0PY+D9nBAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "phase_space.struphy.panels(\n", + "phase_space.struphy.plot.panels(\n", " x=\"e1\",\n", " y=\"v1\",\n", " nrows=1,\n", @@ -275,17 +501,28 @@ "source": [ "## Interactive plots\n", "\n", - "`.struphy.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." + "`.struphy.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "17", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAt0AAAIWCAYAAACLGDGFAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAWKlJREFUeJzt3Qd8FNX2wPGTUKQmoXcR6SAdQVBUFHgqUkSwoaIoduWJiqhPBSwgCIr6twsoolhBURFBRUCqEBWN9CJIJ5DQS3b+n3Pf281uenZ2srOb39fPuOzszN3ZmU1y9uy598ZYlmUJAAAAAMfEOtc0AAAAAIJuAAAAoACQ6QYAAAAcRtANAAAAOIygGwAAAHAYQTcAAADgMIJuAAAAwGEE3QAAAIDDCLoBAAAAhxF0AxHglVdekX/++UcKo2+//VZ++umncB8GQuyDDz6QVatWBb3/oUOH5NVXX5VoMWnSJNmzZ09YjyHSzqkbzhmQHwTdQAT4z3/+I5s2bZLCaNq0aTJz5kxxGz4M2PPSSy/J0qVLg97/+eefl7Vr10q02LZtm4wcOTKsxxBp59QN5wzIj6L52hpA2H3zzTdStmxZ6dSpU9BtHDlyxASymiVq3769nH322UFvv3LlSvnhhx8Ctm/ZsqV06dJFIt2PP/4oycnJcuWVV2Z6LCUlRU6dOpWndt566y2zfXZuvfVWSUhIyPNxvf/++9KqVStp2rRpjuuc8Nlnn5kPgLfddpvExcUFPH+jRo2kbdu24jR9P06YMEF++eUX37rvv/9eEhMTzb9jY2NlyJAhWe67ZMkS856tV6+edOvWLc/Pmdf2g93/jjvukDPOOEOefPJJqVixohS0rM6p0m8j9JyVKVPG/ExXqlQpz21G+zkD8otMNxCBX8t//fXXQe+/b98+E5xpVmvFihXyr3/9S4YPHx709vPnz5eXX35Zdu7c6VtyCjAjyRdffGEC5qxcffXVcvnll+epHf2w4j03GrS++OKLAecrLS0tX8el+y9fvjzXdU7Q8/HQQw/JqFGjMj3/vHnzpCBMnz7dBM1169b1rTt48KA5l4sWLZKhQ4dm2ufEiRPSt29f6devnwksX3vtNbnxxhvz/Jy5tW93fw1mO3bsKFOnTpVwyOqc9uzZU2644QbzvnrvvfekTp068t133+W5zWg/Z0B+kekGXGjLli0my1OuXLmAjLH+8fnrr79k+/btJgguUaKE3HPPPflqW4MlzVppW8WKFTP10hdddJEJQM4888ygtq9fv745nmBoe/pHPSYmRqpVqyadO3c2tznRTKtmofX8XHzxxSYguOKKK6RGjRrm8d27d5vHtUa1Q4cO0qRJE3GivKRkyZJywQUX+Nbp82nWf8eOHdKiRQs555xzzPpHH300oFRIg1Pv+VqwYIEps9AMtf5bM3aXXHKJeY16X8+LPsfpp59utv/qq69k165d5vn37t0r1atXNxnnjOuuu+4637nQc6wfhPRc+GfC9bzptxaHDx82H6guvPBCadiwYa6vXd+TmhXVTGPt2rWz3S6n57Zj7ty5Jtjy17t3b7Po+fnyyy8z7TN69Gj5888/5Y8//pD4+HizTl9zXuXWfij2P/fcc83P/eDBg/PUpvf66fn9/fffTdCsP5uhOqd33XWXeS963X333ea9nNdvCNx4zoBwIugGXEaDxV69eknXrl2lVKlSJqt8/PhxX1B39OhRKVq0qMkAadCnli1bZjLO2dFAqkePHubfmiW/5ZZbTACtNKCrWbOmCdj0j2xGedleM7naAUuDP/0jqBmx/GazLMuShQsXyr333iuzZ8/OtkxB/8BqgK1/+L3nR+tQtaRFg24NVPX86XFoAHv//febbbxfTefnXOVWa67te4Nu/RpeM9+6v2YL9cOKBgyaAc6Jnt/PP/9cihcvLueff75pTzN3GtDqcWgWXM+z94OFBlgnT540t3reTjvtNHPuMq5Teh6vv/56026FChXkkUceMedCgyc1fvx4E9TreWzXrp0pHcoL3U7b0wAsuyxjbs+dkb6fNCDOjgbtek2VBs+agc1vhl4/8CxevNh8oNEPAHpsbtKgQQNznfNKr5/S660f8vT86s/F5MmTQ3JO/QNupR/mjh07JpF8zoBwIugGXOa+++6Thx9+WB577DFfcHfttdeaf+sf1NatW5ugVzN3Xpqp1GArO5UrV/b9+++///ZlTb30vq7PSm7bt2nTxnRoWr9+vVkGDRok48aNyzKAz4oGqv5lGmPGjDGdo7LLbGlGS4Mn79fNn3zyiVx11VW+xzWou/32233lDxr4XXrppaa0QI87P+cqP/Q5Nfv/1FNPmfuacdaAQNfpNcvJ/v37Zd26daauW49Pr6+OWDNgwADz+P/93/+Z9rt37y79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AQAAAIcRdAMAAAAOI+gGAAAAHEbQDQAAADiMoBsAAABwGEE3AAAA4DCCbgAAAMBhBN0AAACAwwi6AQAAAIcRdAMAAAAOI+gGAAAAHEbQDQAAADiMoBsAAABwGEE3AAAA4DCCbgAAAMBhBN0AAACAwwi6AQAAAIcRdAMIqbS0NPF4PJxVAAD8EHQDsO3gwYNyzTXXSPny5aVEiRIyYcKEoNrRYD2vAXuog/vRo0dLrVq1pFixYtK0aVOZPn16nvbbsmWL9OzZU0qVKiVlypSRfv36yc6dOzNt98MPP0jr1q1N+1WqVJFHHnlETp06lWm7cePGSe3atc12jRs3lo8++kgKip7P4cOHS/Xq1c3zt2jRQmbNmpVpuz179sh1110nZcuWlZIlS8pll10mGzduLLDjBIBIRNANwLbHH39c/vrrL1m7dq2cPHlS7r///qDaGThwoHTr1i1P25577rly3333SSg8//zz8swzz8iUKVPkyJEjcs8995jgef78+Tnud/z4cXO8+gHg77//lvXr18vu3bule/fuAR8I/vjjDxOY9u7dWw4dOiTffPONTJw4UYYNGxbQ3iuvvGLO5dtvv22O46GHHjLB7Zw5c2y/xrx8oBk5cqS89NJL8umnn8rhw4flhhtukF69esny5ct921iWZT5kbN68WdasWSP//POPnHbaadKlSxc5evSo7eMEgKhlAUCQPB6PdfLkSatGjRrWqFGjzL/T0tKy3PbUqVPmcb3Niu534403WhdddJHZLre22rdvb911112+bfVYgqH7li9f3nr00UcD1nfu3Nnq1q1bjvu+++67VmxsrLVt2zbfuj///NPSX61ffvmlb90NN9xgNWnSJGDfCRMmWMWLF7f279/ve/1VqlSxhgwZErDdpZdeap1//vmWXQ888IDVq1evbB8/fPiwVbp0aevZZ58NWH/22Wdbffr08d2fPXu2eX0rV670rdu1a5dVtGhR680337R9nAAQrch0AwjapEmTTDmJZjv/85//mH+PHTs2y20bNmxoHtesaLly5UxmNDEx0ff4nXfeaTLNP/74o9lOl6effjrLti6++GJZunSpvPbaa75tP/nkE/OYZnO1bCOnRTPTXitXrpTk5GRzPP66du0q8+bNy7IExGvu3LnSqFEjqVGjhm9dkyZNTHmGPua/XVbtnzhxQhYsWODLhu/a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7wsvvFAqVapkeiDnRj8UTZgwwQyQr0ujRo3MZEdTpkzhchSAMWPGmGvVuXNnqVKlitx+++0moMuPjz76SBo0aGCGeILzJk+ebK5Vp06dpHr16tKvX788f2DS8W+vuuoq83N6/vnnm2mrp02b5vgxF3azZs2S008/Xc455xxzzrt06WJmn8yL0aNHmwlPzj77bNPGWWedJQsWLHD8mAszTRpp3KCzJzds2NCc+/xYvny5+VvWsmVLady4sbRp00ZWr17t2PGiAFhR7Pbbb7dOP/10a+vWreb+9OnTrZiYGGvWrFmO7ovgjRo1yoqLi7NWrVpl7i9ZssQqUaKE9frrr+e436ZNm6ySJUtab731lpWWlmbWTZgwQdNv1tdff80lcdCHH35oFS1a1Pr+++/N/XXr1lmVKlWyhg0bluc2tm/fblWoUMG68MILrdNOO83Bo4X66aefrNjYWGvq1Knm/o4dO6z69etb119/fa4n6MiRI1bTpk2tXr16WcnJyWbdnj17rGeffZaT6yD9udLfhc8884y5n5qaanXs2NHq1KlTrvt+/PHH5neh3qrjx49b1157rVWuXDnfNUTorV+/3rrjjjuspUuXWg8//LAVHx+f5333799vVaxY0ex/6tQpc82uvPJKq27duubfiExRG3TrG7ZYsWLWyy+/HLD+oosusrp27erYvgieBstVqlSxHnjggYD1AwcONAFBTjRomDdvXqb1Z5xxhjVgwAAui4POPvts88fA38iRI62yZctax44dy1Mbl156qfnj8uSTTxJ0F4ArrrjCat++fcC6iRMnmkBcf5Zy8txzz1kJCQlWSkqKw0cJf/fff79VvXp1X1JB/fDDDyaY1qAuJ7fddptVp06dgHW//PKL2XfOnDmc6AKQ36Bbk0bFixe3Dhw44Fu3ceNGc80++ugjh44STova8pIlS5aYkgP96tOf3v/5559zrEG0sy+Cp7W8u3btyvK8ay2cPpYdbxmQP71O+vVeqVKluCwO1t6vWLEiy2um/SF+/fXXXNt44403ZNWqVfLcc89xnQrI/Pnzs7xmWt6lv+Ny8vHHH8u//vUvM5nH9u3bJTk52eGjhfeanXvuuRIbm/5n23s/tzKRChUqSGpqakDJl7cmXB+DO693ixYtJD4+3reuTp06UrNmTcqCIljUBt3auUfVqFEjYL3WLmqgkFMn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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "phase_viewer = phase_space.struphy.viewer(x=\"e1\", y=\"v1\")\n", + "phase_viewer = phase_space.struphy.plot.viewer(x=\"e1\", y=\"v1\")\n", "phase_viewer" ] }, @@ -294,114 +531,3815 @@ "id": "18", "metadata": {}, "source": [ - "Saved marker orbits sit under their species. `.struphy.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." + "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "19", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "out.kinetic_ions.orbits.struphy.trajectories(max_markers=12, show_paths=True)" + "out.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=12, show_paths=True)" ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ - "`.struphy.animation()` and `.struphy.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." - ], - "id": "20" + "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." + ] }, { "cell_type": "code", + "execution_count": 12, + "id": "21", "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "animation = phase_space.struphy.animation(x=\"e1\", y=\"v1\", step=4)\n", + "animation = phase_space.struphy.plot.animation(x=\"e1\", y=\"v1\", step=4)\n", "HTML(animation.to_jshtml())" - ], - "execution_count": null, - "outputs": [], - "id": "21" + ] }, { "cell_type": "code", + "execution_count": 13, + "id": "22", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote: ['frame_0000.png', 'frame_0001.png', 'frame_0002.png', 'frame_0003.png', 'frame_0004.png']\n" + ] + } + ], "source": [ - "frames = phase_space.struphy.frames(\"frames\", x=\"e1\", y=\"v1\", step=10)\n", + "frames = phase_space.struphy.plot.frames(\"frames\", x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" - ], - "execution_count": null, - "outputs": [], - "id": "22" + ] }, { "cell_type": "markdown", + "id": "23", "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." - ], - "id": "23" + ] }, { "cell_type": "code", + "execution_count": 14, + "id": "24", "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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Cq8ullahQgWZM2eOWXskNDQ0eV8JAADwrXVGUhPrjAAAkP54e/zmRHkAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACs8k/Knfbs2SOHDh2ScuXKSalSpe54+5iYGNm2bZucPn1aihQpIpUqVUrK0wIAAF8PI9HR0dKtWzdZtGiRVK1aVcLDw6Vnz54yfvx48fPzize4tG7dWq5duyb33nuvbN26VYoWLSoLFy6UwoULJ9frAAAAvtBN89FHH8nSpUtly5YtsmbNGnOZMmWKfPPNN/HeZ8iQIZI7d27566+/ZMmSJebnpUuXZOTIkcnRfgAA4EthZNq0adKpUydX10z16tWlRYsWZnt8Ll++bCoi/v7/KcLkyJFDSpcubbYDAAD4J6aLZvv27dK3b1+P7RpIJk+eHO/93n77bRNgRowYYcaKrFu3Tvbv3y/jxo2L9z5RUVHm4qSVFAAA4ONh5MqVK3Lr1i3JkyePx/a8efPKxYsX472fVkUaNmwokyZNMv/euXOndOjQQUqUKBHvfcLCwuStt97ytmkAAMAXumkCAwPNz8jISI/tet35u7h07txZTp48acaKLFu2TPbu3Str166VPn36xHufoUOHSkREhOty5MgRb5sJAAAyahjJnj27FChQQI4ePeqxXYOCjgGJi1ZSli9fLl26dHEFlpw5c8rjjz8uP//8c7zPlSVLFgkODva4AACAjClRA1hbtmwp8+bNE4fDYa7fuHFD5s+fb7a7T+V1Bo3MmTObAKNjRNzt27fPrDcCAACQqHVG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Vppx/vx5R5s2bRw5c+Z01KpVy1GiRAnHzJkzHRmBT1ZG+vXrZ5an1WSZM2dO+fDDD6V79+5y//338w3lvxWjr7/+Wn788UcZP3687T9XmrJ37155//335cEHHzTX9Ztt3bp15Z577pH/+7//E1938OBBmThxolSrVs1c379/v9x3333mMnDgQNvNSzPeeustKVy4MOfbisM//vEP6dmzZ+r/UdI4h8Mh7dq1kxs3bsihQ4ckT548cuXKFZk7d65kBJl8cU3/OXPmyEsvvWSCiHrhhRckR44cMmPGDNvNSxPee+8918EEnvRD0hlEVMWKFaVRo0ayevVqdpWIdOrUyeO9U6ZMGXPA1eCP/1ixYoVMmzZNJk2axC6Jw9mzZ+WPP/4wXRH4n19++UVWrlwpn3zyiQkiSo9hPXr0kIzA58LItm3b5ObNm1KzZk3XNn9/f6levbps2rTJatuQ/uh7afPmzVK2bFnbTUkz9JubhjP98NRqiH57e/755203K80caPXg8cUXX0hISIjt5qRJb7/9tjz77LOm2qhB/8CBA7ablCYsXbpUihYtaqqMu3btku3bt982viY987lumvPnz5ufefPm9diu152/A7w1dOhQMxBxwIAB7LT/0kHhQ4YMMVVI7abR7isdyAqRp556Sp588klzkEXcQUTDmn5B1ODWtm1b6dy5s/z+++8JnvHVFxw/ftwcp5o3by6HDx82Z7PXY9bHH38sXbp0kfTO5/66AQEB5uf169c9tl+7dk0CAwMttQrp0ejRo80HwezZsznYusmfP7+pjOhsLK0aTZgwQd58803xddo1o7NFHn74YbN/9KLfbPXA8ttvv9luXprw9NNPmyCi8uXLJ++8847ZZxpqfV1AQID5P9WmTRvZvXu3Gb+moV+7jvU9lN75XBhxThGL3Yet15leB2/poOdhw4bJ999/b76pIG7afdW+fXtZtGgRu+i/++P11183BxG9XLhwQZYsWWLOM4LbFSxYMM7Pa19UqlQp8fPzk969e7u2afenBtr169dLeudzYaR8+fJmQN0PP/zg2qYjk8PDwzmowCtjx4413TM6ir1ly5bsNTdXr16Nc3ZW7G5RX6TdD86KiPNSqFAhUw1YvHix+Lq43ju6XzJnzmwGivu6Fi1amBk12l3jdPToUVc1Mr3zuTEjmizDwsJMaUv/gDraX0uBderUMdOm8J9BvhcvXjQhzTkYUdWqVSvjLLCTRFOmTJEXX3xRXnvtNQkKCnLtG/13aGio+LpWrVpJ06ZNpUaNGhIdHW26sHQGAAdb3In+X9KZfN26dTMDNdeuXWuu65gjZ4XEl9WpU0c6duxoLjr9WYPJiBEjpF69elK/fn1J73z2rL0LFiyQqVOnmsF2OlVz8ODBEhwcbLtZaYJOe9Z+2thmzZplPiR82fDhw2X58uW3ba9QoYJ89tln4ut0/R6derhmzRoT/HW/9OnThzE18dADyyOPPGKqIxAzUPXzzz83X4S0gt21a1cTbvG/2Xu69pOGex3jqEGkf//+GWIFcZ8NIwAAIG3wuTEjAAAgbSGMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqn1uBFUDa8Mcff5hVWlXOnDnNasgZYfEmAInHomcAUp2eIC5PnjxSuXJlE0ROnjxpTkEwefJk6dSpE38RwMfQTQMg1f3555/m58KFC80S4AcOHDDnhnr22WclJiaGvwjgYwgjAFLdxo0bzbmgSpYsaa7reWyaNGlizm1z/vx5/iKAjyGMALASRqpUqeKxTc/SWrp0acmXLx9/EcDHMIAVgJUwUrFiRdNFc+3aNVm0aJF89913MnfuXP4agA9iACuAVBURESG5c+eWcuXKmZ/Hjx+XEydOyOzZs6Vt27b8NQAfRBgBkKqWL19uxods2bJFqlatKg6HQ3r06CErVqyQgwcPSubMmfmLAD6GMSMAUr2LJkeOHFKpUiXX4NWhQ4fK0aNHZfHixfw1AB9EGAGQ6mGkZs2aHhUQDSa65oiOGwHgewgjAFKVVkJ0TZHY+vTpYxY/A+B7GDMCAACsojICAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAADEpv8HHVmCw+8BK9gAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out.plot.equilibrium()" - ], - "execution_count": null, - "outputs": [], - "id": "24" + ] }, { "cell_type": "markdown", + "id": "25", "metadata": {}, "source": [ "## Derived quantities\n", "\n", - "The accessor also computes without drawing, and every result is an array that plots itself. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation. The same methods sit on `out.analysis` when the array is given by name." - ], - "id": "25" + "`.struphy.analysis` computes without drawing, and every result is an array that plots itself. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation." + ] }, { "cell_type": "code", + "execution_count": 15, + "id": "26", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "largest drift of the total energy: 1.586e-06\n" + ] + }, + { + "data": { + "image/png": "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EwIZAAAAALgJgQwAAAAA3IRABgAAAABuQiADAAAAADchkAEAAACAmxDIAAAAAMBNCGQAAAAA4CYEMgAAAABwEwIZAFSirVu3ygMPPCB9+/aV7t27y1VXXSWvvvqqnDp1ivNgZ/r06XLBBRd43DFZvHixXHbZZdKpUye56aabnHrtu+++KxdffLH4Gk89VwDgLQhkAFBJPvroI2nfvr0cPXpUHnnkEXnzzTdl6NCh8sYbb0i/fv388jxMmzZNevfufcb9R44ckY0bN4onyczMlOHDh0urVq3kww8/lEmTJjn1M6WkpMjmzZsrrH2lvW9Fv9YTzxUAeJNgdzcAAPzBqlWrZMKECfL000/Lww8/bLu/a9euppfs0UcfFX+UmpoqmzZtOuP+a665RgYMGCCe5M8//5Rjx47JDTfcIG3btnX6Z6po5Xlfd7UZAEAPGQBUihdffFFiYmLkwQcfPOOxyMhI87jV6tWr5frrrzdhTXvOXnjhBcnOzj5j6NuiRYtk7Nixct5555lQt2bNmiL7PXHihDz22GNmHxdeeKHcf//9kpycXOQ5e/bskXvuuUd69uwp559/vjz55JNy+vTpM95rwYIFMmLECDNUb8qUKdKuXTvZsGFDkX1pG3v06GF6j9S8efPM8/TWpUsXGTVqlHz++edFhrr985//lOPHj9uep21R33//vQmw9lx1XEpztv2//fbbpndM6c+hbf3f//53xj7O9jNZOdK+c50XZ9/3bD/b2V57rnPoDEd/11xxfEr6vZ07d6557K233rINGb799tvNedSfLz093fbaXr16SW5ubpH3/O2338zztm/fXqafHwBKZQEAVLiGDRtarrjiinM+b926dZbw8HDLjTfeaPnll18sn376qSUuLs5y5ZVX2p7z/PPPW4KCgiydO3e2fP3115YlS5ZYRo8ebalZs6blyJEjtueNHDnS0q1bN8vcuXMtS5cutbzyyiuW4cOH2x7fsmWLJSYmxtK3b1/zHL116NDBMmrUqDPeq23btpYvv/zSsnbtWsuxY8csTZs2tdx///1F2v7ZZ5+Z5+7du9d8f+LECfPz6O3333+3vPnmm5bq1atbpkyZYh7Xtj700EPmPuvzdu/ebR579dVXLdHR0RVyXMpy3JOTky1ffPGFRf+3OWvWLPN8PQ7Fne1ncrR9jpwXZ973XD/b2V57rnNY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pM24uLgyH5yNGzfKc889V6ZApj1j1v2UtQ15eXnmZ42JiZGgoKAy7QOVi3PmfThn3odz5n04Z96Hc+Z9OGfeRTONdt5YM4NXBLIZM2bIe++9Z4YOnjx5UrZt2ybNmjU743k//vijjB8/3oSyjIwMadWqlUyfPt2sFWL15ptvSmpqaonv88gjj0hOTo7cd9998vnnn5e5vdZhihrGdB2Tsv5hhYWFmRNFIPMOnDPvwznzPpwz78M58z6cM+/DOfNOjkxt8piiHnPnzpUHH3xQ3n333VKfs3fvXrnsssvkb3/7m+zfv98kz2rVqsmoUaNMQLPKysoyc7xKuumUuddff90EIA2AWtBDF4h8++23ZevWrZX00wIAAACAB/WQffDBB+brF198Uepz3nrrLTNMcdKkSeZ73dYhhzou8+eff5YBAwaY+++5556zvpdOrjt27JgJaEpDmoY4/eQBAAAAAPwukDni119/lfPOO6/I3LKuXbtKRESEecwayM5l4MCB5mY1depUueuuu6R169alviYtLc3crLR3TmmIK2uQ09dpzx5B0HtwzrwP58z7cM68D+fM+3DOvA/nzLs4c33vVYFMhywWD10BAQFSt25d81hZ6Xyy2NjYsz7nxRdflCeffPKM+7Uoh84DKwsNYydOnDDbgYEeM3oUZ8E58z6cM+/DOfM+nDPvwznzPpwz76IZwScDmQ4rDAkJOeN+7THTx8rKkSqL999/v0ycOPGMyilaIdGR6ilnS861atWiqIeX4Jx5H86Z9+GceR/OmffhnHkfzpl3cSabeFUgi4qKMgU4itOqjPpYRb93Se+hxUHKUyFRe8bKuw9ULs6Z9+GceR/OmffhnHkfzpn34Zx5D2eu7b1qnFzLli1l+/btRe7T0vfaW6XrlgEAAACAN/GqQDZs2DBZsWKFKXlv9c0335gxtUOHDnVr2wAAAADAawPZn3/+KevXr7cV59CFofX7Q4cO2Z5zww03SJs2bWT06NGmquLXX39tStzfeuut0rx5cze2HgAAAACc5zFzyJ566inT+6Xatm0riYmJZnv8+PHywAMP2Ip36HpjTz/9tLlP1yF7+OGH5c4773Rr2wEAAADAqwPZ+++/79DzatSoYUrQAwAAAIC385ghi94iKSnJrFnWsWNHdzfF42mxFR1W6oqFr125L39y/PhxWbBggTl2qamp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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "total_energy = out.scalars.total_energy\n", - "energy_error = total_energy.struphy.relative_error()\n", - "energy_drift = total_energy.struphy.drift()\n", + "energy_error = total_energy.struphy.analysis.relative_error()\n", + "energy_drift = total_energy.struphy.analysis.drift()\n", "print(f\"largest drift of the total energy: {abs(energy_drift).max().item():.3e}\")\n", "\n", - "energy_error.struphy.timeseries(title=\"Conservation of the total energy\")" - ], - "execution_count": null, - "outputs": [], - "id": "26" + "energy_error.struphy.plot.timeseries(title=\"Conservation of the total energy\")" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "`out.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." - ], - "id": "27" + "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." + ] }, { "cell_type": "code", + "execution_count": 16, + "id": "28", "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/diagn_tools.py:220: UserWarning: No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.\n", + " ax.legend()\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy8AAANWCAYAAAD3ALM5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAb0FJREFUeJzt3QeYZFWdN+DTPRlmpocchygZlAEESQIiYRUliIKIqyK4yxowgMB+oKDAGEBxdRFRQCWouARBAQUEBQeWLMmRDCNhEJjQk3pSf8+5bDXdMx2qq6vqpvd9nnpmqupW1e3Tt6rPr/7nnNvS2dnZGQAAADKuNe0dAAAAqIbwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAjTc3Llzw5e//OXwjne8I2y++ebhsssuC7fcckvy/7/+9a+Dfr477rgjeez9998/4LZPPPFEsu11113XsNcAAJpDeAEa7rjjjgu//vWvwze/+c1wzTXXhPe+972hvb09/P3vfw/z588f9PPNmTMneey8efMG3LajoyPZdtasWQ17DYqhlkAdj+PLL788HHTQQcljTz755IbuI0DZDU97B4Diu/LKK8MnPvGJsMcee3Td9u53vzv87W9/CxtssEGq+wYVgw3US5YsCRtvvHFyLB922GHhiCOOCC+99JIGBWgg4QVoqNgRnDlzZhg/fnyP28eOHZt8Uw15NWzYsPD888+H0aNHJ5U6ABrPsDGgV4sWLQo//OEPw/777x/e+ta3hr333jucd955ybfN1frBD34Qtt12267/x7Cy7GXZITpDed3p06eHz372s2G77bYLe+65ZzK3pt7i8LPTTjst7L777uFtb3tbOPDAA8PVV1+93HYvv/xy+PSnP91jX2qdf9O9bX70ox8lw+7ia7/vfe9Lnrezs3NQ+3f99dcn+zF16tTwy1/+Muy1115hp512Sob1VZ7rF7/4RdL2cf/jUKiFCxf2+Rxx2/gccdvPfOYzye+hlnbr/pxxeGGsaEyaNCkce+yx4Z///Gev7THQsVLtc/785z9P9j06/PDDu47P3n633cXgAkDzqLwAvc4TiR3CRx99NHz9618P22+/fXjsscfCCSeckExkj2P8qxE7gbvsskvy+Pj/SuewMr8gXu8+RGcor/v666+HnXfeOfk2/Fvf+lZYY401wqWXXhruuuuuuv2GYwUpvkb8lv3b3/52MuTt2muvDR/4wAfC//t//y/Z5+i1115LFicYNWpUEgjivsQOftyXWubfRAsWLAj77bdfEvZOP/30ZD9i5/tXv/pVEizisLxq92/27NnJfpx//vmhpaUlTJ48OWnn+PtYvHhxmDBhQnL/GWeckfwbQ1h8jXPOOadrfyrP8d///d/JY84888zwyiuvJL+rGBjuvffesPLKKw+q3SrPedFFFyWLPMSfMz5nfP1HHnkk3H777YM+Vqp9zhgE47Cx2AbxdxYDVrTWWmsN4YgBoO46AZZx1llndba0tHROmTKlx+2///3v49fynTfddFPVbdbe3p485qtf/WqP26+++urk9jvvvHPQr3vDDTck12+//faubY4//vjOYcOGdT755JM9HnvIIYck215yySWD+j339honnHBCsn8PPvhgj22PO+645PbHHnssuf7FL34x2Zenn366x3aHHXZYTfsSfe1rX0se++c//3m5+xYuXDio/fvFL36RPNcRRxzRY7tjjz22c8KECZ0f//jHe9z+2c9+tnOFFVboXLBgQddtlec4+OCDe2wb2z/+7LENKga7X4cffniP7S6++OLk9nvuuWfQx8pgnrO3Y3Kwx/nHPvaxQT8WgOoZNgYs54orrghbbrll8m15d/vuu29YccUVww033JC51/3d736XVDviBOrujjzyyLrtX3yNOAyu8q18Rax6xOFW8f4oVh7iz7Dhhhv22C5Wn2oVV2vbZpttkmFXyxoxYsSg9q/iQx/6UI/rcdhXrJIccMABy90eV12bNm3acq8dJ6l3F9t/1113Db/97W+7bhvsfsXJ793tuOOOyb+xglLrsVLNcwKQfYaNAct57rnnkvkEW2+9dXI9djArlzh8KM7nyNrrxlWelu0cRxMnTqzb/r344os9VkyrWH/99bvuj+J+Vub6dLfuuuvW/Nr/+Mc/eg0utexfX/sTh4v1d3scDveWt7xlwPaNtz3wwAM179eyz1kZfvbqq6/WfKxU85wAZJ/wAixn3LhxSQf2wgsv7LV1ll05LAuvG1cvi/NeltXbbUPZvxkzZix3e+zUV+6v7Etv2/V2W7Xizz5QR7va/auI84N609ft3RcGqOirzbu/Vr32q/vrD/ZYGczPBEB2CS/Act71rnclqyzFieYrrbRSLl43Lgxw8803J5Oy47ChiltvvbVu+xdfIw5xih3x7vsXXzeKw6WiOJTpj3/8YzLUaoUVVuja7s9//vOQ2iauDBarFGuvvfaQ9q+ebrvttvAv//IvXdfjz3z33Xcnq481cr8acYxWVg6LCxAAkE3mvADL+epXv5rMo4hzIp599tmu2+PqVt/4xjd6rPqUldeNq0zFlaXiKlJxZa5K5ziuelUvJ554YjIk6eijj+46r0d8/rjfsYO+zz77JLd9+ctfTlYUiytXxVWxKp38e+65p9fnjUspx5XE+nPKKacknes4byYOmYriksBXXXVV15yRavevnu6///5w0003Jf+PP2ts//izx32paMR+NeIYrcyXevDBBwf9WACaQ3gBlhOXso3L+sYKxmabbRbWXHPNsNpqqyXn0ojLyW611VaZe90ddtgh6cjHJZjjHI342LiMb2UZ3nqI5we58cYbw5NPPpnMmYjL6MaqQTx3SJygHpcdruzLlVdemXTq29rakn2J57mJAaT7BPuKuMTvQEPCNtpoo/CXv/wlCTCxkx2rL7HiEKsxlbk+1e5fPcVzt8Rzz8QKSPxZY2CMP3tsg8G2W9rH6CabbBI+97nPhS984QvJ81dznpfKOWHiUs1R3L5yjphaz+cDQN9a4pJj/dwPlFz8Nj0OVYqBoJbhOfEjJq7otOqqqyaXivgNfJyEHjuJvZ3or7/XjUPD4spXccL3mDFjlnu9eNbzOOchPi5+4//0008nnf3BzNXp7zWiGDZipSd2xHu7P1q6dGnyHJV9iRWSuJJX7ODHEypG8cSVm266aXKyyWVX7upLbLtYYVhnnXXCyJEje92mv/2LnfsXXnghWQ0tnotm2duX/Z309ruKoenDH/5wMjE/Lk4Qh4TF14sT41tb+/5erJb9ihWm2E59DRHr71ip5Tnj7z4uABGHj8X9jKGsL7EK1v1cRd0N9pgDYGDCC0CTxG/0YzUonoG+0mGOJ4qMZ4WPQ5X66/RnzbLhBQCaIT9/KQFyJM67qJwXJVaD4nla4vCqT37ykz2+6T/kkEOSoW55Ci4AkBarjQGDFofivP3tb+93m/htfPx2vqz7vd566yXnNolDiuKwqxhgjjnmmPDtb3+7x3arr776kF8LAMrCsDFg0OJcjscff7zfbeJ8hspJCMu833EuSFx9K85PWXaifp71NZcEABpJeAEAAHLBIGsAACAXSjHnJQ4Victojhs3riHnOQAAYGji3MA4JDUuM561RUziyY/j0vtZNHLkyF5POVBUpQgvMbjEcw8AAJBtcaXGddddN2QpuGywwdgwffqSkEVrrrlmeOaZZ0oTYEoRXmLFJaqcLA4AgGypnOi20m/LilhxicHlb3/bKIwbl62KUHv70rDFFk8n+yi8FEhlqFgMLsILAEB2ZXWIfwwu48cPS3s3Si9b8REAAKAPwgsAAJALwgsAAJALpZiwDwBA409N0d9ywiNGjAjDhpkzwtAILwAADEkMLXG53hhg+jNhwoRkad+sTson+4QXAACGdHLJl156KamqxKWOezvBZNxm3rx54ZVXXkmur7XWWlqcmggvAADUbPHixUkwWXvttcMKK6zQ53ZjxoxJ/o0BZvXVVzeEjJqYsA8AQM2WLHnjzPMjR44ccNtKuFm0aJEWpybCCwAAQ1bNPBZzXRgq4QUAAMgF4QUAAMgF4QUAAMgF4QUAAMgF4QUAgCGL53KpxzbQH+EFAICaxZNTRgsXLhxw23g+mGjEiBFanJo4SSUAADUbPnx4cv6Wf/7zn0koaW1t7bXiEoNLPEHlhAkTnKCSmgkvAADULJ67Za211grPPPNMeO655/rdNgaXNddcU2tTM+EFAIAhGTlyZNhkk036HToWqzKVIWZQK+EFAIAhi8PFRo8erSVpKBP2AQCAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXHCSSgAAGMDImUvDyCUtmWqnke1LQ9movAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkwPGTAE088Ef70pz+FFVZYIbz3ve8NbW1tPe6fN29e+N3vfhemT58ettlmm7DHHnuktq8AAEBJKy8nn3xymDRpUrj11luTy+677x6efvrprvtfeuml8La3vS2ceeaZ4b777gsf/OAHw5FHHpnqPgMAACWrvPz85z8P55xzTpgyZUrYYYcdusLK4sWLu7Y58cQTw7hx48Kdd94ZRo0aFR555JEkzBx66KHhoIMOSnHvAQCA0lReYnA57LDDuoJLtNZaa4WJEycm/1+yZEm46qqrwsc+9rEkuERbb7112G23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gTJZOGdM6Fg0ImTJwo7ydeXL9xOTO1Ycq52wAgAUifBSEnlcJpnBE1Yaw9Axysa8FyCrWtPeAWBoneruF0hj6BgA+fLoo4+GO++8s+7P29nZmTz3X//617BkyZI+t2tvbw/3339/eP311wf9GsIL5IiwAgDU6tJLLw077LBD2H333cM+++wT6mnq1Klh8803D3vvvXc44IADwgYbbBDuvvvuHtssXbo0nHjiiWGNNdYIn/zkJ8N2220XTj755EG9jvACOaGyov0BYKgB47zzzgtnnXVWv9vFkBErKPGycOHCqiouhx12WNhyyy3Diy++GKZNmxb222+/cOihh4aOjo6u7f7f//t/4eKLLw733HNPeOCBB8IzzzwT1ltvvUH9DMILANDrvBegWM4444yw44479rvNrbfemlRNDjzwwCSQrLPOOuHqq6/u9zH33ntveOihh8Ipp5wSWlvfiBennnpqEmJuuumm5HocInbuuecmt2+11VbJbS0tLeHYY48d1M8gvJALFhyA7DLvBSBds2fP7nHpXu0YjFg1Ofjgg8M555wTnnzyyfDII4+Eiy66KHz0ox9NgkhfYhUlhpZJkyZ13bb++uuH1VdfPbkv+stf/hIWLFgQ3ve+94UXXnghuT3u62AJLyWg4w/1YegeUE+CP/UyceLE0NbW1nWZPHlyTc9zySWXhFVXXTV5vjihf8qUKWGVVVYJ48ePD7fddlufj4tVlQkTJnRVXSriYyuT8mMwivd/97vfTao/H//4x5O5L//5n/85qH20VDIAAOTYtGnTkoBRMWpUbSfIfvzxx8OMGTPC8ccf3+P2jTbaqCuYxHkwcZso3rbLLruEESNGJFWVZc2fPz+MHDky+X/cJs6lefXVV8Nzzz0Xhg8fHm6//faw1157he233z584AMfqGofhZdBnCkdwDlfKBPne4F8GD9+fI/wUqsxY8Yk813uuOOOPreJE+7vuuuurpB0yy23JEPE5s2bF2bOnJlUYKLFixeHV155pWtCfnze6Oijj06CSxRXPYvzX2KIqTa8GDYGwJAZ/gKQf3vuuWdyjpaHH364x+0xiFQqK2effXYSbuIlBpfK42Jl5dprr+16TJyoHwPNu9/97uT6zjvvnASsON+lYtGiRUnAWW211areR5UXAAAoyVLJr776anjqqaeSIVyVCsu2224bxo4dGw455JBkieP9998/WTksDheLj/nJT34SrrnmmrDxxhv3+rxxnkwcavb5z38+CTqxIhPP53LUUUeFzTbbrKuq8/Wvfz2ccMIJyfLLa621VrjwwguTk1nG7aolvAAMkqFjAOTRZZddliyFHMUTRJ500knJ/2OIiCEjzmH5zW9+kwwNu+GGG8LcuXOTYV1XXXVVn8Gl4swzz0zCTtw2BpgYZj7zmc/02OZzn/tcsvRy3I9Yldl6663Df/3XfyVBplotnfGsMgUXl2GLKy+89qMdwvgxw0Nr2/ITioo656VoK421zsxf3u6cNfhJc0tn9Ty/whLnW8icxe1j0t6FzHFekOIyLLBxvG/eNLtjUdjg7N+GWbNm1WX+Rr37kY//+wfDuFEjQpa0dywKm57/68y1WSOZ8wJAXejgAtBowgtADZzzBQBKHF7ipKE5c+YkE3j6ElckAAAAyikz4eW4444L48aNC1/+8peXuy+uTBDP0Dl69OiwxRZbhJtvvjmVfQSAMjIvA8iKTISXuKrBn/70p7D55psvd99///d/h29/+9vhyiuvTCozRxxxRHjf+96XLPEGkCZDx5Zn3gsMjvcM5Cy8/OMf/wj/8R//kSyZFteEXta5554bPvnJTyYnv4nrQ5966qlh9dVXD+eff34q+5snRVtprKg/EwAAOQgv8aQ0sZIST1azzTbbLHf/a6+9Fp588snwzne+s8fte+yxR7jrrruauKcAAECpw8vpp5+ezGOJ8116M3369OTf1VZbrcft8forr7zS5/N2dHQka3J3v0Ba8nqOIKpj6NjyDIMpJvNegFKHlylTpiTzWeIlnr0zzmeJK47FFcXi/7uLty97vaWlpc/nnjx5cnIyocpl4sSJDfs5AACAgoeXRx55JKmQTJo0Kay55prJ5dFHHw0//vGPk//HIWVrr712su2yVZZ4fa211urzuU8++eTkTKOVy7Rp0xr+8wBAUStplQtAacPLpz71qaTC0v0S573Eyfvx/8OGDQsTJkwIW221Vbjlllt6VF3++Mc/ht12263P544T/8ePH9/jAtAoho5RpICy7AUgS4aHjDvppJPC0Ucfnaw2tvPOO4dvfetbYf78+eHYY49Ne9cyrcircsWfrXVm5g/dIWltWxCWzhqd9m4ABSGEAEWRqR7gCiussNxyyUceeWSYN29eMrk/TuCP1Zl4ksrKkDIAQEAByiFT4SVO4u9riFm8AAA9qaoAZZKp8AIAVEdoAcpIeAGAHBFaIB0dc0eHkYtGZqr5OxYOC2UjvFTBSQYBSJPAAvAG4aWAirzSWJlWHAMQWgB60vsDgIwRWgB6J7wAQEYILQD9E14AIGVCC0B1hBcASInQAjA4wkvBlGGyfoVJ+0BeCS0AtRFeAKBJhBaAoRFeAKDBhBaA+hBeAOrxYTpufljcPkZb0kVgAai/1gY8JwAlV+aOe/zZy/zzAzSSygs0QUtbR+icNUpbQ4EJLACNJ7wUSJlWGquw4hiQNqEFoHmEFwCogdAC0HzCCwAMgtACkJ7STdhvbTOJEoDBMxGfRhGIoXoqLwDQDx1LgOwQXgCgF0ILQPYILwVRxpXGKqw4BtSLwAKQbcJLFefnAKDYhBaAfBBeACgtoQUgX4QXAEpHaAHIJ+EFgNIQWgDyrXTneSmiMk/Wr9AGZMHwcfPT3gX64BwtAMWg8gJAYam0ABSL8AJA4QgtkD8L54xJexfIAeEFgEIQWCC/OpLgsijt3SAHhBcAck1oASgP4QWAXBJaoEhVl+zrmDs6jFg4MmRJx6Lyrb0lvOScVbZ6tkXrTIc0FJ3QAsWRl+BCdpQvrkFKWto6tD0MkeBCUTm2oTrCCwC5oHNH0ZXxGB81dn5ygWoJLwB15ESVwFCU9YSqAgzVEl4AaIh6dsDK2Jmj3Mp4zI9UgaEKwkuOmayvTQAorrJWYaA/wgsAmabzRtl5D8CbhBeAOjPvBag3VRh4g/ACQGb5xhmWf094X1BmwgsAmaSDBt4fsCzhpR9OKggAZJUqDGUkvOSUlca0Ddlm3svQqieqLuD9Ar0RXgAAck4VhrIQXgDIFFUX8P6BvggvAAAFogpDkQkvAA1i3svgqbpA/Xg/UUTCSw6ZrA8AVEMVhqIRXigkAQ/yx7fE0Nj3l/cYRSC8AJA6nSrwXoNqCC8ADWTei2ACWaMKQ54JLwCkStUFvPegWsILNFFLW4f2BiAzVGHIG+ElZ0xE11ZQJKoukA3ei+SF8ALQYOa9AHmgCkMeCC8ApMI3vZBN3ptkmfACAEAPqjBklfACQNO/yfXNLuSDEEPWCC85YrI+5Jd5L28SXCB/vG/JCuGlD5a0LQaBDwDqQxWGLBie9g4AUB6+vYVivI8Xto8OZTN/7ugwbMSokCXzF5WvDlG+nxgAgCFRhSEtwgtAk5R93ouqCxSP9zXNJrzkhLkbAEAWqcKQuTkvjz76aLjnnnuqftKtt9467LDDDkPZL6hr8GudaXoXADRSWefC0FxV9ehuuOGGcPrpp4e2trYBt50zZ044+uijhRcAgJKpDCMTYmiUqr+O/rd/+7dw9tlnD7hd3Obll18e6n5B6bW2LQhLZ/kGq4jzXha3j0l7NwAaShWGVMPLTjvtFNrb20O9twUAoJhUYUgtvOy+++5VP+FgtgUAoNiEGOrJamM5YKUxKJayL5kMAKmHl+uvvz4ceeSR4Re/+EW9nhLqRgAEgHTEyfsm8JO58PL666+HqVOnhldeeaVeTwmF1NLWkfYukAGqL0AZAovQQr3V7eQXseoSL0D9DRu3ICyxdj4AGSao0AzO3AeQEssmA3knsJD58DJlypTwhz/8oc/7d9lll7DvvvsOdb/4P+ZqAABZI7SQm/Dy/PPPh5tvvrnHbXPnzg2PPfZYmDBhQlhttdWEFzIbBFtnKjaSLaovQF4ILGTBoHtyhx9+eHJZ1lNPPRXe8573hPe///312jcAAFIksFDY1cY23njjcPDBB4frrruuXk8JUApWHgOyxEphZFldx9DMmzcvvPrqqyHvLGULAJSJCguFDS9///vfwwMPPNDjtiVLloSHH344XHDBBf1O5mdwTNaH8jD3BUiD0ELhw8stt9wSvvKVr/R8kuHDw3rrrZeEl3e+85313D+oK5P2ASg7gYVShZf/+I//SC4A1JfqC9AoAgtFYd1YAIACElgoorqFlwcffDDccccdYdKkSWHXXXet19MClIrqCzAUAgtFV7elkuOJKz/72c+Gq6++OmRV6/iOtHcBYECWTgZqXd4Yiq5ulZcvfelL4fOf/3xoba1bHio1K401tm1bZxoxCUC+CSuUUd16cC0tLcmqYwAMneFjQG8EFsqu5rQxa9as8Pjjj4fXX389dHZ2dt2+4YYbhs0226xe+wcAUGoCCwwxvHzve98LJ598cpg/f35ScamEl/j/E044IXzzm9+s5WkB6P4BPW5+WNw+RptACQksUKfw8re//S2ceuqp4YYbbgh33313mD59ejjppJPCRRddFL7//e8n/wf619LWETpnjdJMAPQgtGRXx/zRYfiibP3t7ljcEspm0LPr77rrrvD+978/7LHHHsnk/MWLF4dVV101fPnLX05u+9WvftWYPS0Rk/WBCiuPQXlWChNcoAHh5bXXXgtrrrlm8v9VVlklvPzyy133bbLJJmHatGmDfUpoOgERgDQJLFCbIa1rvMMOO4Q//OEPYcqUKclwsl/+8pdho402GspTArAM1RcoBoEFUpjzsvXWW4eJEyd2/f8Tn/hE2HXXXZPre+65ZzjyyCPrsFsAAPlnKBikHF7233//HtfPOeeccMopp4TZs2eH9ddfv577BsD/sfIY5IvQAo1Rl7NKrrTSSsmFoTMXAwDySWCBjMx5+e1vfxsuu+yyUO9tIU2CInlj7gtkj3kskMHwMnXq1HD//fcnyyIPdHnsscfCAw88EPJ8/g0AgL4ILJCD1ca+853vhBEjRgx4OfHEExu7xwAlpvoC6YYVQ8MgB3NeDjvssGRZ5GpVViMDAMgDoQQKFF5iGBFIGs8cDKAaVh6D2ggokH91WW0M8hwYW2d6GwAUhYACxabXBpBDqi+UlXAC5Sa8AOSUAENRCShAX4QXAKDpBBSgaeHllVdeSc77MnPmzDBu3Liw/fbbhzXXXLOmHeANJusDtVB9IcsEFCD18PLCCy+Eddddt8dtLS0tYa+99gpnn312mDRpUj33DxrOpH2A+hBWgMycpLJipZVWCjfeeGN4/vnnw6uvvhqefPLJcMkllyT37bbbbuGhhx5qxH5C4bS0daS9CwB1NXLcgq4LQCbCyworrBD222+/5Lwvq6yySth4443DRz7ykXDLLbeEQw89NHz3u99tyI4CsLzF7WM0C5kkyACZCC/LWrRoUdf/N9988/DPf/6z6sfOnTs3fOtb3wp777132GmnncJRRx0VHnnkkeW2+93vfpcEpm233TZ89KMfDU8//fRQdxsASCHIqMoAqYaXWInZcMMNw/rrrx/OOuus8IlPfKLqx/7bv/1bmDdvXjjttNPCueeeG5YsWRJ22WWXHuHk+uuvDwcddFDYZ599wn//93+H+fPnh9133z3MmDFjqLsOAKRAkAFSWyr5rrvuCn/84x/DpZdeGt7//veHAw44oOrHXnzxxWHEiBFd19/+9reHyy+/PNx2221ho402Sm6LweaII44Ixx9/fNc2cWWz888/P5x88smhCKw0BkBZda/EmPAP1L3yEpdHvv3227uux2WSTzjhhGTp5OnTp4fJkydX/Vzdg0ulylIJKNGcOXPCvffeG/bff/+ubUaOHBne/e53h1tvvXWwuw59EiAB0qciA9Q9vMQ5LnEI18EHHxz+/Oc/h87OzuT2YcOGhR133DH87//+76Ce74477kjmyqy99trhYx/7WLjuuuvCNttsk9z3j3/8I3n+tdZaq8djYuUl3teXjo6OMHv27B4XACA/zJMB6hJeVltttfDAAw8kAWGPPfZIVhzbeeedk7kqJ510UlfwqFY8L8w111wTrrjiimTYWQwwcfnlKM6BqVRbuhs1alRYvHhxn88Zqz9tbW1dl7gyGgCQX6oyQM0T9rfYYotkiFc8p8sXvvCFrmWTTz311HD66acP6rlWXHHFpPISzxHz05/+NKy88srh+9//fnJffM4onk+mu3i9cl9v4lyYWbNmdV2mTZtW1b447waQJ5ZJpqwEmYL+Psc6PxANnrAfqyyDrbT0p6WlJamUVIZ5xeFh6667brIoQKzKVNx5553J0LW+xMpMvOSBuRYAUDsT/rPLsthkcqnkWsUlj2OlphJU4tyWn//85+Gee+4JBx54YI/llH/yk5+EJ554Irl+0UUXJcPKjjnmmLR2nYISJAHyzTyZ5rdxfxfI5FLJtYqVkVhl2XTTTZM5LXF4Vxwy9uMf/zg5r0tFnEfz/PPPh6233jrZPs51icPL3vrWt6a16wBADqjK1NZWkGWphZfW1tbk3C3x8sILLyRzXyZMmLDcdsOHDw8XXHBBOPvss8Nrr72WDCNbdollAID+lDXICCX1M2/umNAyPFvHzrzFqQ2iKl946W6dddYZcJvx48cnFwCAsgcZoYSyykR4KStzLAAgWyEgzTAjkMDAhBdYJlC2zvS2IPsskwz5qcoIJVA/emkAADUEGaEEmk94AQAYgKAC2VC+JQoAAIBcEl5SYrI+UUtbh4YAAKiS8AIAAOSC8ALLUBUDAMgm4QUgZyyTDEBZCS8AAEAuCC8pMCwJAAAGT3j5P1Z9AgCAbBNeIMPVsda2N8/uDABQdsILAACQC8ILAACQC8PT3gEAymFh++ge10eOMywSgMERXko6lwLIp7ye42XZ4NLXbQINAP0RXqCfoNk601sEmkmgAaA/emYAND2QDPXxKjQAtXv00UfD7Nmzw8477xzqqbOzMzz22GNh8eLFYeuttw7Dhg1bbpsFCxaEJ598Mrlv8803Dy0tLYN6DeEFgNwxfwZg8C699NJw7rnnhqeffjosXLgwzJkzJ9TL1KlTw4EHHhhmzZoVRowYkdx25ZVXhh133LFrmx/+8IfhpJNOChMnTkzC07hx48LVV18dNt1006pfx2pjABQizCx7AWD5gHHeeeeFs846K/Rn6dKlSXUmXmLIqabicthhh4Utt9wyvPjii2HatGlhv/32C4ceemjo6OhItrnrrrvCpz/96XDhhReGRx55JDz33HNh7733TraJj6+W8NJEJusDZZNmiBBoAHo644wzelRCenPrrbeGDTbYIKmixECyzjrrJNWR/tx7773hoYceCqecckpobX0jXpx66qlJiLnpppu6nne11VZLwkoUh4sde+yx4eGHHw533nlnqJbwAkDDpD03Jb5+9wtAEc2ePbvHpVLtGKxYNTn44IPDOeeck8xLiRWSiy66KHz0ox9NgkhfHnjggSS0TJo0qeu29ddfP6y++urJfdHKK6+c7Fv3oWovvPBC8u99991X9T4KL9AP1TKyZvi4+WnvQqYsG06EFaCMJk6cGNra2roukydPrul5LrnkkrDqqqsmzxerIVOmTAmrrLJKGD9+fLjtttv6fNzrr78eJkyY0FV1qYiPjfdFH/zgB5MAE8PRb3/723D55ZeH4447LowePbprm2qYsA8pa2nrCJ2zRqW9G+QswOTpfC8xUNQ6fEy1BGBg06ZNSwJGxahRtfUrHn/88TBjxoxw/PHH97h9o4026gomcR5M3CaKt+2yyy7JBP24itiy5s+fH0aOHJn8PwaXOLzsO9/5TvjBD36Q7O/FF18c3v3ud4cxY6r/mya8AJBagBFOAIZu/PjxPcJLrWKIiPNd7rjjjj63iYEjTr6vhKRbbrklGSI2b968MHPmzKQCE8Xlkl955ZWw3nrrdT12rbXWCt/+9rd7hKX29vZkWeVqCS9NYvgRUObqSySoAGTbnnvuGc4///xkEv0222zTdXsMIvESh3idffbZvT4uVl+uvfba8K//+q/JbXGifgw0sbLSvRLTvcoSl05ee+21e2wzEOEFIKfyGGAASHep5FdffTU89dRTyXLIlQrLtttuG8aOHRsOOeSQZInj/fffP1k5LA4Xi4/5yU9+Eq655pqw8cYb9/q8cZ5MHGr2+c9/Pgk5sSJz4oknhqOOOipsttlmXdsddNBByeUtb3lL+N3vfhcuuOCCcN1113UNLauG8AIA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RJ1hQVjamb19KptMh5fXX389+XeVVVbpcXu8XrmvN5MnT06Gmy1r4sSJDdhLAADqpb29PbS1tWWmQUeOHJnMuX755TNCFq255prJPpZFpsPLiBEjuibtdzd//vx+f0knn3xy+OIXv9h1febMmWH99dcPzz//fKbeDFkUq1Mx5MXV3azqpp0cU95/WeWzSls5ror3HowVlxhc1l577ZAlo0ePDs8880xYuHBhyKKRI0cm+1gWmQ4v8U3R2toaXnjhhR63x+vrrbden48bNWpUcllWDC465NWJ7aSttFM9Oaa0VSM4rrSV46pY78Gsfskcw0GZAkKWZXqp5BVWWCHssssu4dprr+26be7cueHmm28O++yzT6r7BgAANFemKy/RGWeckQSVOBRs5513Ts7xsvrqq4dPfepTae8aAADQRJmuvER77LFHuPXWW8Nzzz0Xvve974Wtttoq3HHHHWHs2LFVP0ccQvbVr36116FkaKtaOKa0VSM4rrSV4ypd3oPaiuxr6czqenQAAAB5qrwAAABEwgsAAJALwgsAAJALmV9tbKhee+218PTTTyfnjIlnIGV58QRTcUGE7oYPHx7e8Y53aK4QwtKlS8N9992XrO++zTbb9NomHR0d4dFHHw0rrrhi2GyzzUrbbosWLUraauWVVw6bbrppj/vmzJkTHnzwweUe87a3vS2MGzculM0//vGP8Prrr4eNN944OW56E0+w++STTyafXeuuu24oozgt89lnnw3z5s1L2mrZ8yy89NJL4amnnlrucbvttlsoo/hZPmvWrLDhhhv2+b6K50qL7Rbbc6WVVgplPa7icRNPgr3RRhslp2bo7u9//3v45z//udz5R/r6G1AGlffaJptsEtZYY43l7n/88ceTz/m4sJIFkmiozgL7yle+0jlq1KjOLbfcMvn3k5/8ZOeSJUvS3q3M+frXv945bty4zl133bXrsv/++3eW3cKFCzsnT57cudFGG3W2tbV17rHHHr1ud/3113eussoqyXYrrbRS5/bbb9/54osvdpbJ7NmzO0855ZTOddddt3Ps2LGdH/nIR5bb5p577omLg3S+4x3v6HGsPfroo51lcu2113ZuvfXWneuss07nNtts07niiismn1XL+va3v905evTozi222KJzzJgxnYcddlhnR0dHZ5n87Gc/69x44407N9xww6Qd4vvwBz/4QY9tvv/97yft0/2YipeyufHGG5PjKrZX/De2yfHHH9+5dOnSHp9pRxxxRNdxFf/9xje+0Vk2l19+efJ5vfnmm3duttlmyd+/b37zmz22ie+3NdZYo8cxdeyxx3aW1dy5c5O+VPwM//GPf9zjvpdeeqlzxx137JwwYULSriuvvHLnb3/729T2leIrbHi55pprOkeMGNH5l7/8Jbk+derU5A/f9773vbR3LZPhZaeddkp7NzJn5syZnSeeeGLnU089lQTf3sLLP//5z+QP39e+9rXk+vz585PO+b/8y790lklso9gGMbTtt99+/YaXGTNmdJZZ/Ax65JFHuq7/6U9/Sj6rfvWrX3Xddtttt3W2tLR0/v73v0+uP/fcc52rr7565+mnn95ZJmeddVbnM88803X9iiuuSNrljjvu6BFeYge07C688MLkfVhx1113JW119dVXd912xhlnJMfRs88+m1z/wx/+kGxzyy23dJZJDMDdv2C68sork8+m+BnVPbzEz33e8IlPfKLzS1/6UuewYcOWCy8HHHBA0oeYN29eV58ifok1ffp0zUdDFHbOy0UXXRT23nvvsMsuuyTX41CeD37wg8ntLG/x4sXhr3/9a1Iqj//njSEC3/jGN5IhBX359a9/HZYsWRK+9KUvJdfjkJbjjz8+3HjjjUmJvSxiG5166qlhrbXWGnDbOAwqDh+bO3duKKPPfe5zybCKine+851d56+qiJ9TO+20U9h3332T6+utt17413/919J9fsWTE2+wwQZd1+Nn+CqrrBL+8pe/9Nguvgcffvjh8Le//S0ZulhGRx11VI/Pqh133DEZjhiHiFXE4yceR+uvv35yPZ4AOh5nZTuuPv3pT/f4rNprr72Sf7u3VRSHKt57773JsMUyn1XiF7/4Rbj//vvDmWeeudx906dPD7/73e/CCSecEMaMGZPcFv8exva64oorUthbyqCw4eWBBx4I22+/fY/b4of5I488Uto/bgO11xFHHJEEvji+/pJLLkl7l3LTbltssUWP8dLxOIsf3L3N7yCEQw45JHz4wx9Oxtp//vOfL31YnjFjRjKO/C1vecuAn19xPkPcvqzi/MU4T6h7W0Wx/T70oQ+F/fffP6y66qrh/PPPD2U0e/bsJATfcMMN4cgjj0yCX3yvVe6L7dfbcRWPt7J59dVXk7a67rrrwkc+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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spectrum: (20, 8)\n" + ] + } + ], "source": [ - "omega, kvec, spectrum, _ = out.analysis.dispersion(\n", - " \"em_fields/e_field_log\",\n", + "normalized = out.with_time_units(\"normalized\") # a spectrum needs normalized time\n", + "omega, kvec, spectrum, _ = normalized.em_fields.e_field_log.struphy.analysis.dispersion(\n", " slice_at=(None, 0, 0),\n", " do_plot=True,\n", ")\n", "print(\"spectrum:\", spectrum.shape)" - ], - "execution_count": null, - "outputs": [], - "id": "28" + ] }, { "cell_type": "markdown", @@ -415,10 +4353,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "30", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote:\n", + " post_processing/report/scalars.csv\n", + " post_processing/report/scalars.png\n", + " post_processing/report/electric_energy.png\n", + " post_processing/report/kinetic_energy.png\n", + " post_processing/report/total_energy.png\n" + ] + } + ], "source": [ "written = out.save_report()\n", "print(\"Wrote:\")\n", @@ -428,17 +4379,49 @@ }, { "cell_type": "markdown", + "id": "31", "metadata": {}, "source": [ "## Comparing runs\n", "\n", "Time series accept arrays of other simulations, so comparing runs needs nothing special. Series are labelled by the run they come from, and the runs may have different time grids." - ], - "id": "31" + ] }, { "cell_type": "code", + "execution_count": 18, + "id": "32", "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", + " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", + " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", + "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time stepping: 100%|██████████| 20/20 [00:04<00:00, 4.05step/s]\n" + ] + }, + { + "data": { + "image/png": "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Q1lPNox1ZB9YT2YJScna2fBoqz4khszU0ZUrV7QiRYpotWrV0tauXavt2rVLa9++vVa5cuWUff/880/1/zUyMjLDr5H/xYUKFdIaN26srVq1Sjtw4ID20UcfaZMmTdJiY2O1GTNmqGP++++/2rlz57TLly+r1y1YsECtL126tLZkyRLt9OnT2s2bN++K4UHHv5+MxG15/woVKmjLli3Tjhw5or388suak5OTijUrx0p/LnPmzNFcXFy0Tz75RDt06JD23XffaXnz5lX77tmzR9WTlFFAQID2wQcfpDmHyZMna0WLFn3g/62vv/5a8/DwSLNPdHS0eo/PP/88zb5vvvmm5u/vryUmJqrn48aN04KDg1O2S7xSP/KQ858+fbrm6uqqLV++PGWfsmXLat27d9d2796t7d+/X/vss8+0kSNHpmz/448/NIPBoL311lvawYMH1fn7+Pio98pMbA+TkVgHDx6s6fV6rUuXLtrOnTu1bdu2aRUrVtSKFy+ufkqdHz58WHv22We1fPnyaTdu3LjrtXXr1tUWL16sbdmyRWvVqpVWrFgxdQ4WK1eu1JydndUxpExWrFihlSpVShs7duxdx2rdurW2efNmFXNCQoI2atQoVXdfffWV+mxImbi5uWnr1q3TMqpnz55aYGCgOu99+/alvJd8DlPLyHt5e3urc5k6daoql3nz5qnfhaCgIK1bt26qDLdu3aqVL19e69GjR5rj9+nTR/2OLlq0SP1+Tpw4UX0ONmzYkGa/H374QXN3d9fi4+O17HT+/PlHyguYkDlAQnZqSjUt/nVfLfSNktrRN2to299qrq16r7u2eNpQbf7nL2mzvp2mzfjpR236wuXaV8t3aF+vO6XN3nZO+21PiPb3oSvahhNh2u5zEdq/l29poTfjNGOyydqn5JB4EWn7WEf2gfVkJglRicnLrfqQGDJTR3KRKBfD165dS1l3+/ZtdTE6e/Zs9Tx9MpSR18gFeeHChbWYmJi74hByQSvHjIqKSrPdksRIkpVa+hgedvx7yUjclvffvn17yj4mk0krUKCASqCycqz055L+4lxIspI6IZPzeOWVV7SSJUtqycnmJFsu2CVBmTJlivYgo0eP1ipVqnTXerkwr169eprzkuNPmDAhZV36hOx+x+/QoYNalkTkXueYuh7k4r1///5ptktCLhfnFy9ezHBsWZE6Vsv7+Pn5aXFxcWkSAjmHNWvWpKy7fv26Wvf333+nea0k0hcuXEipI0kCvby8tI8//jhlP0nmGzZsmCYOSUIkkZEkwXIs+fzcunUrZR9JIuU9U3/OxJgxY7T69etn6Hwl8ZFjpE6qpCyrVKmSJiHL6Ht5e3trAwcOTLPPk08+qX4f5PNo8e2332p58uRJeS6JtxxfEtzUJAFt0KBBmnWbNm1S+8qXH7aUkDlZu4WOHp2PKRKuOiMKIgIFtQhAWuXlkXDv/ZM0AyLgheuat3qEwxtHLcuaNyJ1PtDy+MPZuwA8fQJQ0McDhbzdUdjHDQXlp7cb/DxdodfrWH1ERJRh0rWtadOmCAgISFmXN29e1T1JunDJoPusvGbLli1q3JeHh0ea1xoMhgzFJcd6kKwcPzPnWqNGjZRlnU6HggULpumOmJljpT6XqKgonD179q4Z76Rb5Ouvv55m3YgRI/Dhhx+qCTqky+Pvv/+OyMhIDBky5KETc/j4+Ny1ftCgQaq73f79+9X5Sfcx6W4n6+9HuoB+/vnnWL16Na5evaq6od26dQuFCxdW2/PkyaO6Bj777LOq25qUiXRJlK5olvOVLpTpz61169aqy+KhQ4dQrFixLMWW2VgtpEuiJT5RoEAB9VO6XFrkz59ffZbSd8OVrndFihRJ6Urq7e2tuszu27dPPb9+/TqOHTuGzz77LM3rmjRpoo63a9culChRQq2T7oypx0TKZ0r06tXrrrKS7pjS5dHJ6cFpgsQh+8j7pf78yucrdffNzLxX9VTlYikvid3FxSXNupiYGPWQz4SlPKQbZPrjv/HGG6q7pMQlLJ9V+dzaEiZkDiC0zbc4EXoWebRoGOKuwykuHC7xEXBLiIB70g14Jt2Ai/ZfduasS0ZBRKKgLvL+B00EEA4Yw/S4kZK8eeEMvLFT88YN+CDR3Q96zwA4eRWEh29B+OQviAI+niji444yAZ5wd8nYP0IiIso8+Vu7/oVmVo8hM2QsyMaNG9W4m9TkwrJx48ZZfo1cDLu7Zy6W1B722qwcPzPneq/ETi4is3Ks1HFaxvK4urqm2Sf9cyGJikxBLxfIkpDJTxlvkz7BSE8Sw9Rj7SwkUZJkRMYtSdIjP+vWravGAD1o0oWQkBA1HX6ZMmXUxfYnn3yixrhZyBgwudiXREjG9EkiNXXqVIwcOVKVk0idAKV+bimPrMSWlVgflLQ/rM5F6iQkdd1ZzsNyvjJ2SsbJpSZJjoyLs0j/+bW8VsZlpZaQkKCS18uXL6ckc/cjx5BEKv25pC//zLyX4R7lcr8ytJSXlIfs4+zsfFccSUlJqiws2yyfVfnc2hImZA6gYt026tsTf3//e39o5QObGA1EhwEx4Xd+hgHR4eqnKUoe19SyLiYcBuN/0wI76UwIwE0E6NIOmFWSAETeeVwAkjUdbiAvwrR8WK4FIiRPZSQVDEb+klVRsUg+VCrsDW+PtL8sRESUNS5OepT0y2NXxScD/+XiVQbop3e/hCcjr5HB/zt37kzzTXhqlv+N6S94M+phx89q3BmV1WNJi5q0CEjLkLQsWRw8ePCe+48ePVpNMCGTikir4Nq1ax8am0wQIa1p9yItWdLq9tprr2HRokV3teSkJhfO0jo3d+5cdO7cOWV9+kksZPpySRwt9y+TSSDGjh2rZnmU6yBfX1/VYiKTZ1hIS5iQJCwrsWU11kclrX2pb9UgicWRI0fUuYpChQqp850wYYKatCO9fPnyPfAzJZ9lmXHwXi2c0jL3MNKCJ8nW8ePH05StpcUqO9/rQeS9LS2g0oKYut7l85k6UZMkWj5D6b/csDbOspgbyD8P17xA/tJA8XpAxU5A7SFA85eBDp9C//Q8OA1bA6fxh2D4XyjwyhXguQPA4NVA73lqHzR7BaZaQxAf1AHRBWojxrMEEg1pp7c16DT4626jkv4Ceho2YkL8dEw+Pwi91zVG8uzOmP3eELzw7jQ8/+M6fL7mFNYcvYbQW3FZ/gdJRET2RWZgk5Ye6cIkLTJyUSTdtWS2tfSzzGXmNTLTnswY+PLLL6d8Gy8XhZZZBuV1Qi4cs+Jhx8+uc82JY0myIl3rLEnY6dOnVYvKvUi3TLnIltYfmQlPZsR7GNlHun9J8pCedKWUrn1y02i5IL9X0mAhF83SFU3uZyaJh2VWvtRd3yThkbKQFhUhF+HSzU9aOyytfhMnTsTXX3+Nbdu2qedyPyyZZVASTUkMshJbVmLNDhLfpEmTVAIo5ypdMWV2QZlh0/JFw6uvvqo+h1IOllkfpdVq+vTpqgvl/UhdS0uhfBakJUleJ58t6X4p5few7opCusJWq1ZNzf5pea85c+ao1svsfq8Hka6rtWrVUrMzSquxkJlE582bpz4PqcmMptIt8l6JoTWxhYzu5pIH8C1pfqTL3t3u+poo7k6r23+tbVHXziDpwm7kjTgI1+QY5NXFobHhCBrjCJC0RLWmnTlXCPu1IHxpCsJZ1wpwKVwJFYv4olJhL/UIzJ+HY9SIiBxM7dq11UWrXGTKRaWMaZHugJIAdO3aNcuvke5mso+0FEiLiewj34zPmDFDbZcLsGeeeQaNGjVSF9JyYZ6+a9mDPOz42XWuOXEsuWCXrmtywSpjkCT5kPFX0jp0ry5xo0aNUknclClTMtQaKMeVJG7x4sVq+vPUZCycdH+UKfklAXrYfd1kev6BAweqVh9JciQplHFHe/fuTWlJ8fPzQ/369dX4ISkDmape3tvSCioxSIIoCZgkaTKuTGL47rvvHim2zMaaHaS1R5Iy+bxK4if1t2DBAhQvXjxlH/lMSpLTs2dPNeZPkhtJUCVJkha0+5G6tXymgoKCVEurdP2TqefffPPNDMUnLU0LFy5USa3Ui3TblHKQ55L4ZOd7PSwOaaWVz7Wcs9wPT+pEPvupx0DKuqVLl2LMmDGwNTqZ2cPaQdgj+cZFsntp+ixatKhVY5FvTR7YZdFaTMlA+Ang0i7En9uJ5Is7kef2mXvuGq254ZCpFPZpQdhnCsIJp3IoVrQYGgf5o3GQn+ruaLDzSURstp4oBevIPrCeHKOO5EJZumNZJjmwkAs1uXmrXHTKhVZGXpOaXJTKe97rAltatyQu2S5jo+SiXp6n7770oBgedPz7uV/c93v/K1euqIvWe3U5y+yxUm+Xi3t5nVy8yn3DpDVBxvGkrqdvv/1WXeRLK9SDurylJq+RLn/Sipi+vKTlRMpMWvTSj9uRxEneP33iIK09kkxJAiKvl3E/6bu1SUuRJCKSBNyLtCpJeUhLSPrJWDISW0Y9KFaJUeKQ5O9hny25cbnEYbm5tiQS0pop3UalJUlIOaUv39SkPqX1TmJJ7V5xpCbJnpSVvP+9kvSMsPxeSYJ6vzp72HtdvHhRfeZS14Wck/w9Sf15l8+/lLuMO0v/pYG8r+Vznv5vj5SldGWVltbsbiGT+pPfvazmBUzIsogJWRbFRQKX9wIhu2G8uBO4tAdOSVH33PWcqYBK0PabgnDapQICytREo7IF0SjID4UzOZDcFvAi0vaxjuwD68n2sY5sx/nz51XXRmnNkQRGum3KZB0y7kbGTqVOnCXZk0kuZLyZdHPMKLnIllZI6RrYr1+/HD2f3CJ1QsYvc7OHtJBLQiaTwWS3R03I2GWRHi/3fECZVuqhPnwmE3D9pGpFQ8gumC7thi78BHTQUFJ/DSVxDd0NWwANiD3pikMnSmGJKQiheavAq0x9BFcqi7ol8yOPKz/KRERE6UmrikwEIa0j0p1NWheke1v6Wfmk6+OaNWtUl7PMdiOT48okIJKYEdkik8mkJmGxjCe1NWwhyyK2kOWg+Fuq5Uwe2qVdMF3cDUPivQemXjAF4ACCcCNfNeQp3QAVqtdHpaL5bXL8Gb8xtn2sI/vAerJ9rCPbvCCV7l/SGmbp5pW6nqQLmKyXrl4ZnUnSEUn3wAfdl65du3ZqIoqcZulmKHVjjRayWbNmqSn97+fLL79Ex44dH1s8to4tZOR43LyBMi3VQ/4lGKQVLeK0akUzXtiJhPM74XHzpGpFK6EPQwmEAbe2Avu+RtxeFxzQlUaEb3W4l6qHoJrNUaDwg++jQURE5Ohk7FHqG0un96AJIHITSXw2bNhw3+33G7OW3WSMlSVptgbp2ic3eH5QOVH2YT8vsn0ygNW/rHo41ehn/tDG31Zj0aLPbkf06e3Ie/0A8iTfhrsuETVxDLghjwXAHuCkU1mElemFcq0Gwt+Pf0CIiIjo3qQVytbuUWUNMmlNZmeepKxjQkb2yc0LKN0cnvJobb75tRZxGqH/bsKtE1vhEb4fRRPPqXujlTWeRNnj7yDu2FRsztMUydWfQa0m7eDpxptUExEREZF1MSEjx6DTQecXhMJN5TFYrYqPvol/d62Eaf88VLy9RbWeNY5dDWxbjXNbC2GNfyfkb9Af9apVhLOB90gnIiIiosePCRk5LDdPH1Rt0Rto0RsxkVdxYs1M+J78BUWSLqKkLhQlr8+Acen32LwsGJdK9kTFJl1RM/C/wc5ERERERDmNCRnlCnnyFUSVnq8C2iu4eWorwjb8gGJX/oG7Lh7NsRs4txvXzr6Huc4tkVilD5rWr4cyAeabMxIRERER5RQmZJS76HTwKdtIPZAQhfCdvyBp92wUjjqMArqbeMa4CNi/CNv3VMRS77bwrdUD7WuWQoCXm7UjJyIiIiIHxISMci/XvPBvMhRoMhRa2DGEb/oBeY7/jjzGm6hvOIr60Udxe/03WLq2IU4U7oJmTVujRfkAm7zHGRERERHZJ85kQCQNZwEVENDjY+R56RSSe8xGRKEmMEEHL10snjGsxjvXRqPgL23w5QcvYuGmQ4hNNLLciIhywMGDBzFw4EDExsbm6hjsjdwsedGiRZl6zfz58zFlypQci4koIzdMf+6553D8+HFYExMyotScXGCo3AX5h/8J/fOHkdj4JcS6F1abKuvPY1zid+i8tgXWv9cZCxbOxdWb/GdNRJSdQkJCMHv2bCQmJmbbMfft26cSrISEBKvF4MjkYvbFF19ErVq1MvW6Xbt24c8//8yxuIgycsN0b29vPP/887AmJmRE9+NTDC4tX4bHpH+BZ5YgslQnJOmc4aZLQntswdPHRiPx02r466sJOHbiBMuRiMhGXbx4USVYSUlJGdq/evXq+PHHH+Hh4ZHjsTmCt99+G126dEGJEiWsHQpRpkkL2bp167B9+3ZYC8eQET2MXq9uQp2vdHMg9gZu7ZyHhF0/IiDuDIrrwlD8+kwkz5+FvW61YQjujyrNe8Pg7MJyJSK6TxehJUuWYNOmTaoFqnHjxujdu7f6pvpRXiP7LFu2TO0jy61atUKHDh1U98MvvvhC7TNixAg4OTmhePHieOutt7Bz50588803+Pjjj/HDDz/gzJkz6Ny5M4oWLYoNGzagV69ecHFxeeDxH+RhcVve/7PPPsPcuXNx4sQJ+Pv7Y/DgwShSpEiWjpX+XNq3b69asH7++WdERESgUqVK6Nixo+oq+MYbb8Dd3R2bN2/GnDlzMH36dLi5/TeJ1a1bt1TLwciRI1GnTp17nmN4eDh+//33NC1dGzduVO8n3RhdXV1T1t++fRvjxo1T9VC3bt27jvXXX3/ht99+U8sSV6lSpdCvXz8UKlQoZR8594ULF2Lv3r2qbpo0aXJXPVy+fFm9/7lz5+Dn54fu3bsjODj4kWLLSqyS1F+7dg1PPvkk/v77b9XyKuU/dOhQ1R121qxZOHXqlPo8Shnny5fvrte2bdsWCxYsQGRkJGrUqIFBgwalfCZTxz5v3jwcOnRIfYnQrVs3NGzY8K5jderUSe139epVvPbaawgMDFSfDSlPKTOJQ45fuLC5Z1BGyedWyuLSpUsoV66cOoa0Olls2bJF/e7I5ykoKEi1Vku9WEyZMkXVlfD19UW1atXQt29f9btqMWrUKFWP0dHRKnmSMpg2bRquXLmifnfOnz+v4pZ9KlSokCa+h52j/M5JHcnvT/369WENbCEjygwPX3g3H4uAF/cibsBqnCjSAzFwh0GnIThhF6pvG4Nb75bBkZ/GIfbKMZYtEeUcYyIQcca6D4khE+Ri+oknnlAJh5eXF0qWLImXX35ZXXw9ymvi4+PRunVrdaErF8eyz4wZM9SFXv78+VG5cmW1nyQxzZo1S+laJxeB0nJWu3ZtdZEqF+JywZa+y+KDjv8ocVveX5IdWS5fvrxKGKSFTpKnrBwr/blIoiYX8kePHlXlIM8bNWqk9r1x44Z6bcWKFdV4LkuCYSH7SLIlcd3PmjVrkJycnCYBKFu2rHqtXISn9ssvv6iHbL8XiVfqRx5VqlTB7t271QW+xG4hyca7776rkma5qJ45cyYmTJiQsl2SEkl65KJdzjcsLEyV708//fRIsWUl1q1bt2Lq1Kkqsbd8EfD666+jXbt2qFevnkomJMbFixerOjEajWleKwmH7CvJuLQ+vvPOOypB0zQtZT9JvOUYkmCWLl1aJSryZYEk5emPJYm4fH7l9yBv3rwqMZXYjxw5on5K115JhuSYGSVJpbz/4cOHVblLUtagQQMVs5AvCJo3b65+h+Rz9scff6j9LAmYCA4OTilLSWglVkm0LccQ8vns37+/+vKiTJky6vMt71W1alXVsiXxS/nJ74R8mWKR0XOU95akOXXZPlYaZUlISIjUmPppbUajUQsNDVU/yQrlHxelHVr+tfbvuw007Q2vNI+LHzXWbmyZpWkJ0awnO8DfJfvAerrj+um7/uY89ofEkIk6+uCDDzS9Xq/t3bs3Zd3Zs2c1Z2dnbd26der5n3/+qf6/RkZGZvg1b7/9tubu7q6dO3cuzftdunRJ/Vy8eLE6ZlRUVJrtCxYsUOt/+OGHNOvTx/Cw499LRuK2vP+vv/6ask9MTIyWN29e7dtvv83SsdKfS8OGDbWOHTumWffss8+qfffs2ZNSTwMGDNDq16+fZr8KFSpow4cP1x5kwoQJWtmyZe9a3759e+3JJ59Ms65u3bra008/nfJ83LhxWnBw8AOP361bN61v375qOTo6WsW9Zs2aNPukvhZr2rSp1qRJE81kMqWsmzx5subj45NSnxmJLStSxyoGDx6seXh4aJcvX05ZN23aNHUOP/30U8q6U6dOqXXr169P81pZt23btpQ6OnHihGYwGFRdW7Rq1UorUaKEKhuLH3/8UfP19dVu3ryZciz5rMj7WFy9elVzc3PTRowYkeYcpGx69eqVofOV83JxcdGmTJly13opf6kXeY/PPvssZVtiYqL6XHXv3v2+x71586b6HZDfWwtvb2+tTp06aep19uzZql5Tk+PLuWX2HOUzJeV9+vS9/6Y9zPnz5x8pL2ALGdEjMrh5okr7kaj4ylac6LEea32fRrhmbqovFnUQ+VY/j/gPyiBy4Sg4XbfuLD5ERNa0dOlS9e18zZo1U9ZJa4+0Dsi2rL5GustJS4J0wUotfbe/+5GuXA+SleNn5lylu5SFdDmT95Fxb1k5VupzkUlMpPXgqaeeSrPPvVokx4wZo/aVLp5CWhmOHTumutI9iHSFk1bI9KRb2KpVq1Q3MSHHktY5aeV7EOk+Ka1Iw4YNU13bzp49m9LqlCdPHtU68sEHH2D9+vWIi4tT66W1zNKSKN3jnnnmGeh0/92iZsCAAbh586ZqHXmU2DITq4W0HqXuHmdpgWvTpk3KOmnZkq6n0jKbmpxr6q6i8lpp3V27dq16HhUVpVoChwwZosrGok+fPqp7oMRnId345HgWK1euVK1W0k0zNWmFktbDjLQUyTGkzKU7YWpyvlL+0jIn7yHlb+Hs7Iynn35axW0RHx+vusxOmjRJ1Y3EJC2K6ctSuqamrlcpW+mu+eqrr6p9pUVNjl+gQIFMn6OlC6W0LlsDx5ARZaNylWuiXOVvcfVGFBatmA+/kwvRSNsHN1Ms3E7+AtPJhbhRuhv8Or8LeP3Xz5yIKNO8iwFjzReYVo0hE2S8kZCL19Rk/Ienp2eWXyPjazI7w19qqcfu3EtWjp+Zc5VuZKnJxbl0A8zKsVKfi3R7lIvU9AnTvRIoOT+5+JdxNN9++23KeBrp3vUgcgF8r8lSpHucvI90D3zllVdU1zZJNFu0aHHfY7300kvqvZ999ln1vpJkSGIh3eEsJFGUbnAyEYOMv5JEVbrySRe269evq3JLPT5JWJ5L8pjV2LIS6/3qNv16STLkkbrO71dPci7SDVNIl1Op39WrV+P06dN31YskiPf7jFs+U++9916asZhSRpLEhIaGPnQsmfxeGAyGe8ZpOZYkVqnHk1nOQV4ryZx8dhrc6eIoXTtljJl0u5SusJJspZb+HOQLCklOpftwy5Yt1bGkS+v777+vYsrMOVo+w1Ju1sCEjCgHFPTNi+59hiM2cTAWb92HG9tmo13iChTVXYffmUWI//QvxNQei/ytJwLOaf9YExFliJMLkL+0XRWWjKGRizAZr5GaPL/fxV9GXiP7nDx58r7vm/pb9azG/aDjZ9e5ZvexChYsqC5uJXFLLfX4nfStZNLaMXHiRDWuScZnPUyxYsXUxXN6cmErk1zIhBLS8iEtIHLs+9WFXJB/9dVX+Pzzz9O0VMmYuvStkp988olalgRMYpVxVZKkyPnKpCSpExFheS6tilmJLauxPqoLFy7c1VIldSfJh5CWIJmYRMb4pZ+MQj4blolM7veZkvOVVtb0k4TIZDEyxuxh5BiSREr5SitfelLeMq5LWv5kXwvZX+pR3leSySNHjqi6tCRcUr4ZnYbeMvbMMn5QJvWQOpUkOzPnaEnWLa2tjxu7LBLlIA8XJ/RoXgdDXv4KR7qswddO/XFbc4ebFo/8u6bh5tSqiNw5XwZzsh6IyOHJpBgyI5tcIElrj+UhLTM+Pj5Zfo10c5JvymXmOwuZxc7SLcrSQpJ6oozMeNjxs+tcs/tY0irQo0cP1dols9NZujHKxAj3Ii0U0nIj3R5l8pCePXs+NDaZzEMmV7Bc0KYvN2m5kQlIpLUifQvfvVgmGhHS2iSz41nIe/zzzz8pz6VemzZtqs5NWjjkfKV7pkxwYTmOJAzSxVG6+6Xu8pmV2DITa3aQ7nOWyUiEdE+VrnmWLqiSfEpiKZ8NSTBSfzZk5kBJUO9HuuDKBBrSlTP16+QzIN1mM5KQSSIs7yGthanv2SeTY0hSZZmkQ1owLYmlfFbknKRbpZCEyWQyqRYzC5nUQ1obH0ZmHE09OYdM8CEtbJZ6ycw57tmzRyWQmf2yJLuwhYzoMdDrdWhdtThCC7yCNacHQdvwProkr4JPUhjwz0hc3jwd3l0+gmcZ60y3SkT0OMiFpLTWyDf3MhugdCuSCyr5Fl1aK7L6Grm4k4tS6a4kSYpcqMu35TIbn5DXyoWaTAEvF+XSPU2mvc+ohx0/u841J44ls/xJVzyZ4U5iP3DggJqiXUh3s9SktUVafCSBeeGFF9JMgX8/khBJPDJeR8bmpCZjfCRe6WIoY6ZSt5KkJ8nUm2++qboQSqIrsW3btk0dw3KBLfFJy5SMCZIZ82QMmYyTknqwdAG0zEwoM/lJVzhJYOSCXG4ZkLqVJDOxZSXW7CD1JPUnMwXK+cm4ucmTJ6vzspBWOuk2KS2Vsl66CEpyKK1m0p3zfqSLpSROkshJgiSxSznJFPEPGzeY+hgyvlKSfsv4NvmcSpImyZokPNItVN5DbgIuCY8kUdLFU8beCZm9tGnTpqrLqczGKK2C8sVJRhIjSeak+6ll5lFJsKU1zvLFSWbOccWKFap1zVp0MrOH1d7djkmGLx9+qXhrNW9ayLc/8u2OfBuS/o8r2Y7U9ZRkApatWoNiu99BfRxK2edkQFsU7/0h3PLz5prWriP+Ltku1pP915FctMoFbExMjLqQk2neLd3F5P+rdIGTJCj1BfSDXpO6RUEmppD3lC5c8v4WMmZELgallUXGtEgrkHT/km5mMulA6mPdL4YHHf9+HhT3/d5fJhyQLl3pu5xl5ViWVjG5mJdWCEnMpAxkGn05TzmX1PUkU9/LBax00Uw9CcSDSCvTjh071HukJxNpSPIqF+uW2w9YyEW6dDVMfR8xeV9JGiX5kgRDxvnIhbYkwxaSjMo+kjDKcS2TOFjIpa3EI2UiyaJMoZ5+LNfDYsuIh8UqdSXjoFJP2nK/z5YkXXIMS5nLRB1yrOXLl6vWWalzqW+pv3uRboByPtIdU5JVS9J9vzgspHVK6kHKSspRvrDIbAuutE7KBB7y+yGJoMSZmkw+IomzJEOWxC19fW3ZskVdU0uLlrS6SoKU+ndApr2X2NLfgkH+1sg96SzxSxmm7574sHOUBE3KTL50SV1umSGJpHzRk9W8gAlZFjEho+y4QLkVk4hVS39G8ImPUUp3Ra2LhwtOl3kW5bv/D07uXizox4gX+vaB9WT7WEe2QxIeeVgSDula1qVLF3UPLLmQTf9/SVor5GL1frNe3osketICKYlk6tYbyjpLQibJGL8ozFkyK6e0ssmN0bPqURMydlkksiLvPC7o2WcIrt7ojWW/T0OTyz/ARxeDyqdn4PrU33AleBKqtBsOnZ4tn0RElHnSYiKtZjJmRloH5AbG0qLz66+/ptnv7bffVt3v9u/fr6aAzwyZjEG6rj3q5ClEj5u0nkmX1dSttNbAhIzIRmZl7DTsLZy9OBT7Fr2OxjeXwg834LfnZZw+MBPxLd9F5fp3dzUgIiJ6WLIk3bVk0gL5Fl/GhkmXLenWlXqadVknrVwy7iij929LLf0sf/ZKWvvGjx9/3+0y7mz06NE5HodMOpJ+2vfHSe7TJt0E70fGYMm4L3un1+tVC5m1MSEjsiGlihdDqfE/4ujhPYhb/gqCE3aijPE0sLI3dm9uDJ9OHyCofOb7uRMRUe4l3RHl4vlBF9Ay4QmZJw5Jf3uB1NLfHDynWLp+pr832eMi3e4eVA4BAQGPNR5Hx4SMyAZVrFILWuWVOLhxMbw2vYmSpguoHbsZCQuaYk3+nqjU+y0UKsA/hkRERNlJunZmZQp8RyOTh9xvAhHKfrwPGZGNkr741Zp1Q4lX9uJA1ddxE15w1RnR6sYCOH9dC1sXfoRko9HaYRIRERHRI2BCRmTj9E7OqN5tIjxeOITDJQYgCQb46W6h4bG3cfH9Wji/+7+bZBIRERGRfWFCRmQnXDzzocqzX+D2oK046Gm+2WPJ5HMI/OspnPysI+KvnrR2iERERESUSUzIiOxM/uIVUO2F5djfYi5O6kqqdWVvboLh2/oI+WU8EHfT2iESERERUQYxISOyUzWadETRyTuxvOSrCNN84Awjih2fheiPqiB68zdAMseXEREREdk6JmREdszDzRUdBryI6wO34Vf3XkjQnOGZfBuea1/CrU/rQDu1xtohEhEREdEDMCEjcgAVSxZBtxdmYEmjpfhbM9+c0zv6DHTzuiNmVlcg/IS1QyQiIiKie2BCRuQgnAx69G7dENXGL8Z7BT/HAVNptT7PxXUwTa+P5OUvALE3rB0mEREREaXChIzIwRTxccfLwwfgSo8/8brhOYRqvtAjGYY938P4WTVg+9eAMdHaYRIREREREzIixyQ3lW5XtQgmvvA6ZlRZiE+TuiNWc4VT4m1g5ctInl4XOLnS2mESERER5XpsISNyYN7uznizRx00HvoRBuf9BouSG6n1hsizwPxewF8vAMYEa4dJRERElGsxISPKBWoF+mL2811xpdln6G58F/tNZcwbdn+P5JlPAJEXrB0iERERUa7EhIwol3Bx0mNsyyBMGzcQUwt/hhnG9mq9IXQ/jN82Bk6ssHaIRERERLkOEzKiXKaUvyfmDmuEpBZTMDxpIm5rHnBKuAUs6A1t1Ru8oTQRERHRY8SEjCgXMuh1GNMiCMOGjcFg149x2BSo1uu2fYbEHzsAUVetHSIRERFRrsCEjCgXCy7hix/G98QPQTMw19hSrXO5tB2J0xsAZzdaOzwiIiIih8eEjCiXk5kYP+tXF86dP8MLpjFqenyX+AiYfu4C4/qpgMlk7RCJiIiIHBYTMiJS9y3rXbs4Rox5Cc97fYJTpiLQwwSnje8i7qduQEwES4mIiIgoBzAhI6IUZQLy4ovnnsZvNWZjcXJDtc794nrEftUACNnNkiIiIiLKZkzIiCgNN2cDXulaG3mfmoV3dMOQoDnBI+6qul9ZwpbpgKaxxIiIiIiyCRMyIrqnVpUKYsjzb+MN/09x0eQPA5LhuuYV3Jr9NBB/i6VGRERElA2YkBHRfRX0dsO7o57Bika/YpWpllrnff4f3Pq8IUxXDrHkiIiIiB4REzIieug9y4a1qYn8g37Dl04DYdT08I4LgfH7lojaNotdGImIiIgeARMyIsqQ4EBf9J/4ET4v/hlCNV+4aInIu2o8rs0ZBCTGshSJiIiIsoAJGRFl6p5lEwb1x87Wi7FVq6rWFTj7ByK/aAwt/CRLkoiIiCiTcnVCFhYWhrlz52L69On4888/rR0Okd3cs6xLo+ooMPJP/OTyNEyaDvmiTyPxmyYwHvrd2uERERER2ZVcm5Bt3rwZZcuWxa+//opjx44hJCTE2iER2ZUyBX3QbcKX+CjgfURoeeFqioPTH4MRv3Q8YEywdnhEREREdsEJuZDJZMKIESMwbdo0DB061NrhENktLzdnTBgxHF8uqYBGByehtv4k3PbPQvzlvXB7eg6Qr4S1QyQiIiKyaTbXQhYVFYVLly4hOTn5gftFRkYiNjZrEwmcOnUKFy9eRJs2bTBr1iysWbMGGm92S5QlTgY9xndvhrPtfsH3yR3UOrewg0j6pjFwYgVLlYiIiMgeWshWrFihkqO//vpLJVqSNJUpU+au/bZv344hQ4aoLoZGoxFNmzZVrytUqFDKPvPmzVMJ272MHDlSJXweHh7o1asXKlWqpI5ZoUIF/PHHHzl6jkSOrHe90tgR8AXGz6mAKabp8Eq8BSzoDa3h89C1eA0w2MyfGyIiIiKbYTMtZDNmzECPHj3w3Xff3Xefq1evom3btqplSxKu0NBQREdHo1u3bmn2O3fuHI4fP37Ph7SE+fj4qAk9FixYoJK5Xbt2qVaykyc5SxzRo6hXKj/Gj5mA0Xk/wxFToFqn2/oZTLM7AlFXWbhERERE6djMV9aLFy9WP3///fcHJm3i/fffh8FggLe3N6ZOnYoGDRpg06ZNaNKkidr+v//974HvJZN55MmTB15eXuq5u7s7XFxckJiYmI1nRJQ7Fc/vga/HdMOE+UXQ7Own6Ou0FvqL22D6phH0PWYCpZpaO0QiIiIim2EzCVlGbNy4EXXr1oWbm1vKOnkuCZVssyRkD5M3b17VdbFDhw7o3r071q5di/Lly6NixYr3fc3t27fVw0Ja54SMdXvYeLecJu8vE5VYOw56sNxUTx7Oekx/ph4+XPk2xm0rh/edZ8IjNhzanC7Qmr4ErdEEQGczDfS5so7sGevJ9rGO7APryfaxjuzDo1432FVCduHCBTRv3jzNOr1er8aPybbM+PDDDzF//nwcOHAAXbp0wcCBA9Wx7ueTTz7BlClT7lofEREBV1dXWJNcQN66dUstP+gcyLpyYz0NrZUff7r3Rbe1JfGl02cI0l+GbsN7SDizGTdbTIXm7gtbkhvryB6xnmwf68g+sJ5sH+vIPty4cSP3JGTx8fGqa2F6khDJtsyQi61+/fqpR0ZMmDBBTSaSuoWsTp06yJ8/P/z9/WELWbmfn5/qykm2KbfW06Bm/qgSWAAD5xbAC8YZ6GrYCteQzQhY3AOm7rOAorVhK3JrHdkb1pPtYx3ZB9aT7WMd2YeszvxulwmZp6fnPU84JiZGbctJMt7MMuYsNblos4ULN0kwbSUWur/cWk/1Svvjl7GtMeQnH+yJWILXnX6G6+3L0M9uD12bd4C6IwCdDrYgt9aRvWE92T7WkX1gPdk+1pHte9RrBrvqkxMUFIQzZ86kWSctY1euXFHbiMh2FfP1wKLRDXGtbB90T3wTF03+0JmMwIqXgN8GAIkx1g6RiIiI6LGzq4RMpryXKerDw8NT1q1cuVLdj0y2EZFt83R1woxnaqFhk1bokPguViUHmzccXQrM7QEkRFs7RCIiIqLcmZBJkiU3bLYMipN7jslzywB7MWjQIAQGBqJv3744fPgwNmzYgHHjxuGZZ5554AyJRGQ7DHodXm5bAa/3bIgxphcwLamXecPFbcDc7kBClLVDJCIiInpsbGYM2ahRo7B9+3a1XKRIETz11FNqecSIESn3FZPp7SUJe/XVV9V09TL9/bPPPouXX37ZqrETUeb1CC6Kkn4eGPazM+LjXfCa81wgZAcwpxvQbxHgdveYTSIiIiJHYzMJ2W+//Zah/WSK+1mzZuV4PESU84JL+GLxqIbo84MBybf1eNP5Z+DSLmCuJSnzZjUQERGRQ7OZLotElDsVz++B30bUx2bf7ngtaaB55aXdwJyuQNxNa4dHRERElKOYkBGR1RXydsfC4fWxN6AH/pf0rHnl5b3AnC5AXKS1wyMiIiLKMUzIiMgm+Hm6YsGwejhapCdeThpsXnllP7SfOwOx5sl+iIiIiBwNEzIishne7s6YM7guLpbshclJQ2HSdNCFHoT2cycmZUREROSQmJARkU3J4+qEmQNqI6Jsb0w23knKrh6GNrsjEBNh7fCIiIiIshUTMiKyOW7OBnzTLxjxlftgUtJwc1J27Qi02R2AmOvWDo+IiIgo2zAhy6Rp06YhICAA1apVy75aIKK7OBv0+Kx3dTgH98WEpJFIlqQs7ChMP7UHosNYYkREROQQmJBl0qRJkxAWFoaDBw/mTI0QUQqDXof3u1WBX4NnMD5plErK9OHHYfqxPRB1jSVFREREdo8JGRHZNJ1Oh1fbV0DJ5gMxLmkMjJoe+oiTSFZJ2VVrh0dERET0SJiQEZFdJGXjW5dFtScH4bk7SZnhxikYZ7UDbodaOzwiIiKiLGNCRkR2Y2iTUmjUeSjGGp9DkmaAU+SZO0nZFWuHRkRERJQlTMiIyK70qVscT/YchueM48xJ2c2zSJrZFrh1ydqhEREREWUaEzIisjudqxdB1z7DMTZ5PBI1A5xvnUfizHbAzRBrh0ZERESUKUzIiMgutalUEP0GjMQ400QkaE5wuX0BiT+0BSIvWDs0IiIiogxjQkZEdqtRkB+GDBmJ8boXkKA5wyU6BInSfTHyvLVDIyIiIsoQJmREZNeCS/hi1NBRGK+fdCcpuwyjJGU3zlo7NCIiIqKHYkJGRHavchFvDB80HGO0SYjXnOEUfQXJs9oDEWesHRoRERHRAzEhIyKHUK2YD54dMBgjTC8iTnOBQZIyuXk0kzIiIiKyYUzIiMhhNCjth75PD8BQ46Q7SVkoTD+2Ba6fsnZoRERERPfEhIyIHErrigXQvUcfDEycjFjNFfroa9CkpSz8hLVDIyIiIroLEzIicjhdaxRF+049MCBxMmI0V+hirkH7qQMQdtzaoRERERGlwYSMiBxS//qBaNKqk0rKojU36GLCoP3UHrh21NqhEREREaVgQpZJ06ZNQ0BAAKpVq5bZlxLRYzamRRlUb9gW/RNfQpTmDl3sdWizOwBXj7AuiIiIyCYwIcukSZMmISwsDAcPHsyZGiGibKPT6fBq+woICm6pkrLbKimLAGZ3BEIPsaSJiIjI6piQEZHDJ2XvdauCQpUb45nEl3Fb8wDibgA/dwJC+cUKERERWRcTMiJyeAa9Dp/2rg6vMvXQN/EV3FJJWSQwuxNwZb+1wyMiIqJcjAkZEeUKrk4GzHgmGC7Fg9En8VXc1PIA8TeBnzsDl/daOzwiIiLKpZiQEVGu4eHihFkDa8NUsBr6Jr6KSM0TiL8F/NwVuLTH2uERERFRLsSEjIhyFW93Z/w8qA5i81dSLWWRWl4g4RYwR5KyXdYOj4iIiHIZJmRElOv453XFnMF1cNOrHJ5OfBU3VFJ2G/p5PeB8dZ+1wyMiIqJchAkZEeVKRfN5YM7gugjzKIOnEv+HG/CCLjEa+f4azJYyIiIiemyYkBFRrlUmwBOzn62DKy4l0Svhf4iAD/RJsdD/2h+4ddna4REREVEuwISMiHK1KkW9MXNALYQYiqNPwkuIhRt0MWHAwn5AUry1wyMiIiIHx4SMiHK9uqXy49t+wTijK4HxiSPM5XFlH7B8PKBpub58iIiIKOcwISMiAtC8fACm9aiClaY6+MLYxVwmB+cDO2ewfIiIiCjHMCEjIrqjU7XCGFS3ED419sCa5BrmlStfAc5tYhkRERFRjmBCRkSUypB6hfBk5UIYnzQaZ0yFAC0Z+HUAEHmB5URERETZjgkZEVHqP4o6HaZ1r4oSRQpiaNJERGnuQNwNYGFfIDGWZUVERETZigkZEVE67i4GfN+/FqI9S2Jc0miYoAOuHgaWjeEkH0RERJStmJBl0rRp0xAQEIBq1aplb00QkU0p5O2O7/rXwlZ9LXyS1MO88sgiYNsX1g6NiIiIHAgTskyaNGkSwsLCcPDgwZypESKyGdWL+WBaz2qYntwZ/yTXVuu0NW8Cp9dYOzQiIiJyEEzIiIgeMvPi2JblMDFpJI6bikGnmYDfBwERZ1huRERE9MiYkBERPcTzLYPQrEoghiVNwC3NA4i/BfzSF0iIYtkRERHRI2FCRkT0sD+Ueh0+7lkdXoWDMDZpLJI1HRB+DFg8AjCZWH5ERESUZUzIiIgyMfPi8Tx18KHxKfPK48uBzR+z/IiIiCjLmJAREWVi5kVJymbrOmFpcgO1Tlv/LnDiH5YhERERZQkTMiKiTKhWzAcf9ayOyUlD8a+pBHTQoC0aCoSfZDkSERFRpjEhIyLKpI7VCmNYy8oYljgBEVpe6BKjgF+eNk/2QURERJQJTMiIiLI482K1KlUwJuk5GDU9EHEakJYyTvJBREREmcCEjIjoEWZejC7UAO8Y+5lXnloJbHiP5UlEREQZxoSMiOgRZ178270TfjM2Ma/cNA04upRlSkRERBnihEw4evQo1q1bl5mXoFKlSmjevHmmXkNEZC8Kervh+wG10W/GEASZLqG6/iy0xSOhy18GKFDJ2uERERGRIyVk27Ztw8SJE+Hu7p6h/RMTE9GnTx8mZETk8DMvvtezNkYsGI8/Xf8H/6Rb0H7pA93Q9YCHr7XDIyIiIkfqsvjMM8/g5s2bGXp88cUXORM1EZENzrzYu2U9jEh8HomaAbrI88Dvg4Bko7VDIyIiIkdJyAIDA1GjRo0c25+IyJ6NaxmEgpWb4Q3jQPOKs+uBtW9aOywiIiJylISsVatWGD16dI7tT0Rk7zMvftSzGg4GdMU8Y0vzym1fAod+s3ZoREREZKM4yyIRUTbPvDi9b01MMwzGblNZtU5bNga4coDlTERERDmfkGmaBqPRCBNvjkpEuVRJvzx4u1sNjEp8Hle1fNAZ44GF/YCY69YOjYiIiBw9IZs5cyacnZ0xbNiw7D40EZFdTfLRpm5VDE8cjwTNGbgVAvw2EEhOsnZoRERE5MgJWbVq1TB58mS0a9cuuw9NRGRXXutQEYkFa+JV4yDzivObgZWvWjssIiIistf7kGVE7dq11cNRTZs2TT2Sk5OtHQoR2Tg3ZwOm96mBjl/GoJLxPJ51WgnsmgEUqgrU6Gft8IiIiMgGcFKPTJo0aRLCwsJw8ODBnKkRInIopfw98V63KnjX2BfbkyuaVy4fD1zaY+3QiIiIyAYwISMiymGdqxdBzzolMTrpOVzS/IDkRPMkH1HXWPZERES5XJYTsh9++AE6ne6+jyFDhmRvpEREduyNjpUQULAIhidOQDxcgKhQ4NdnAGOCtUMjIiIiexxD1qJFCyxYsCDNutjYWCxfvhx79uzB8OHDsyM+IiLHGU/WtyY6fhmLFxOH4QuXr4CQncA/LwIdP7d2eERERGRvCVmpUqXUI71Bgwahffv2uHr16qPGRkTkUEr7e+LdrpUxfmEyKhrPY4TTcmDvT0ChakCtOzMxEhERUa6SI2PIWrduje3bt+fEoYmI7FrXGkXRu1YxTDU+hU2mKuaVf78IXODfTCIiotwoRxKykydPQtO0nDg0EZHde7NTJQQV8MbYxLEIQUHAlAT82h+4ddnaoREREZG9dFk8cOCAGi+Wmtyb6+jRo1i0aBE2bNiQHfERETkcdxcZTyb3J4vF4ITxWOb2BtxiwswzLz77D+DsZu0QiYiIyNYTslOnTuGnn35Ks85gMKB48eL47bff0KhRo+yIj4jIIZUJyIt3ulTGxN+SMS5hBGa4fAZc2We+R1mXrwGdztohEhERkS0nZD179lQPIiLKmu7BRbH9bAR+3wt8aeyCsU5LgIPzzZN81BvBYiUiIsoFeGNoIiIreqtzJQQFeOITYw9s0gWbV658BTi3ifVCRESUC2R7QiaTeRiNRphMpuw+NBGRw/FwcVL3J3N1dsLouJG44lQU0JKBXwcAkResHR4RERHZW0I2c+ZMODs7Y9iwYdl9aCIih1S2QF683bkyouCBfjHPI8GQB4i7AfzSF0iMtXZ4REREZE8JWbVq1TB58mS0a9cuuw9NROSwetYqhm41i+CsVhij40dCgw64dtg8yQcRERE5rCxP6nE/tWvXVg8iIsocaSU7GHITa8JrYoZrb4ww/QIc+gWo2Bkozy+5iIiIHBEn9SAishF5XJ3wdd9guDnrMTW2A065VDRvWP48EHvD2uERERGRrbWQyY2g5V5kO3fuRHh4uJrQw6J169YYPXp0dsRIRJRrlCuYF1M6VcLkRYcxPGoQVrm9Aqfoa+aZF7t+a+3wiIiIyJZayLp164bXXnsNu3fvxqFDh3Dz5k2sWLEC27ZtUxN7EBFR5vWqVQxda5jHk32U1MO88uAC4ORKFicREZGDyXJCduDAAWzduhVHjhxRLWHNmzfHhg0bcPDgQeh0OtSsWTN7IyUiyiXkb+jbXSqjiI87vjO2w3FDWfOGP8cBcTetHR4RERHZQkJ24sQJtGjRAr6+vjAYDEhISFDry5Urh+HDh2PZsmXZGScRUa7i6eqEaT2rwgQ9xsQOgVHnDESFAqtetXZoREREZAsJWVRUFLy8vNSyv78/Ll++nLLNx8cHN25wADoR0aNoUNoPA+qXwGmtKD5N6mZeuX8ucGoNC5aIiMhBZMssi9I9USb2+OOPP7Bv3z58//33KFv2ThcbIiLKssltyyMwvwe+NXbACX1p88o/nwPib7FUiYiIcnNCVqZMGdSvX18tFy5cWN0Munv37ggODoaHhwcGDRqUnXESEeVKHi5O+KhnNZh0BoyNG4ZknRNw+zKw6jVrh0ZERETWTMiaNWuGwYMHpzx//fXXcfXqVTXJx65du+Dp6QlHNG3aNAQEBKBatWrWDoWIcolagb4Y0qgkTmrF8FlSV/PKfbOBM+utHRoRERHZ0o2hCxQogEqVKqlJPhzVpEmTEBYWpmaTJCJ6XCa2KYfS/nnwjbEjTupLmVcuGwskRLESiIiIcktCFhoaqmZXzKn9iYjo3tycDfi4V3WYdE4YFzcUyTAAt0KA1W+wyIiIiHJLQvbXX3+pLns5tT8REd1f9WI+GNmsNI5pJfClsbN55Z6ZwNmNLDYiIiI75ZTZF8gsim+++WaG95XxVkRElD2eaxmEtcfCMP1qF7R33ocg7by56+LIbYCrY47dJSIicmSZSsjk/mKxsbH45ZdfMvwaTn5BRJR9XJ2k62I1dP5qK56PH4plrq/DcPMCsHYK0I49EoiIiBw6IevRo4d6EBGR9VQq7I2xLYLw6RoN040d8ZzTEmDXd0DFzkBgI1YNERFRbp1lkYiIHo9RzUujShFvfGXsijO6YuaVS8cAibGsAiIiIjvChIyIyA45G/Sq6yIMrng+frh51sXIc8C6t60dGhEREWUCEzIiIjtVtkBeTGhTFoe1UphhbG9eueMb4OIOa4dGREREGcSEjIjIjg1tXAo1i/vgc2M3nEVRABqwZBSQFGft0IiIiCgDmJAREdkxg16Hj3pWg87ZDRMThsIkf9ZvnAHWvWPt0IiIiCgnE7Lw8HAkJSVl9eVERJRNSvl74sUnymO/FoTvjW3NK3d8DYTsZhkTERE5akK2dOlSFCtWDP/73/9w8eLF7I2KiIgyZWCDQNQt6YtPjD1xHoUAzQQsla6L8SxJIiIiR0zIOnXqhGHDhuHHH39EyZIl1fN//vkHJpMpeyMkIqKH0ut1mNajGgwu7piQMBwm6IDrJ4EN77P0iIiIHDEhCwgIwFtvvYULFy7g119/RWxsLNq3b48yZcrgww8/VF0aiYjo8Sme3wOvtKuAfVpZzDI+aV657Qvg8l5WAxERkaNO6uHk5ITu3btjzZo1OH78OCpVqoSXXnoJRYsWxYABA3D69OnsiZSIiB6qb93iaBzkh4+MvXABBc1dF2XWRWMCS4+IiMhRZ1k0Go1YtGgRRo0aheXLl6NWrVp4/fXXcfjwYVSvXh179/LbWSKix0Gn0+HD7lXh7JoHLyQMM3ddDD8ObJzKCiAiInK0hOzSpUt44403ULx4cfTt2xeFChXCjh07sHv3brz66qvYt28fnnrqKcydOzf7IiYiogcq7OOO1zpWxG6tPGYb25hXbvkUuLKfJUdEROQoCdnixYsRGBiIn376CWPGjEFISAjmzJmDunXrptmvcePGnB6fiOgx6xlcFC3LB2CqsTcuagUALRlYMhowJrIuiIiIHCEh8/f3x2+//YazZ8/ilVdeUc/vRcaRffXVV48SIxERZaHr4vvdqsDFPS9eTBpqXhn2L7D5I5YlERGRIyRkjRo1QteuXWEwGLI3IiIiyhYBXm54tX0F7DBVxM/G1uaVmz8GQg+xhImIiOw9IdM0TU3mca9HcnJy9kZJRERZ0qNmUdQJ9MUHxqcRCn/AZDTfMDo5iSVKRERkzwnZzJkz4ezsfM+HTIXv6emJNm3aYM+ePdkbMRERZeqG0e90rYxEvTsmJt7punj1sHmSDyIiIrLfhKxVq1Zo1qyZmtjjnXfeURN6fPrpp6hTp466B9nUqeYpltu2bYuIiIjsjJmIiDKhbIG8GNK4FLaZKmNBckvzSpkG/9q/LEciIiJ7TciSkpJw5swZNbW9THHfr18/PP/889i+fTuCgoKQL18+rFq1ChUrVsSCBQuyN2oiIsqU51qWQREfd7yb9DTC9dJ1MQlYMpJdF4mIiOw1Idu1axfq16+vEq80B9Tr0aFDB5WYCVmWxI2IiKzHw8UJb3WuhGh4YGL8IPPK0IPA1s9ZLURERPaYkLm6uuLYsWMwmUx3bTty5IjaLqKjo1GgQIFHi5KIiB5ZywoF8ESlAthkqobFaG5eufFDIOwYS5eIiMgex5BdvnwZvXv3xrZt29TywYMHMXnyZMyePRs9evRQsy0uX74cHTt2zN6oiYgoS97oWAkeLga8Ed8HN538geREYInMumhkiRIREdlTQubj44N//vkHJ0+eRMOGDdVEHtWrV1eTe8ybNw9169ZVk3l89tlnqFSpEhzFtGnTEBAQgGrVqlk7FCKiTCvs447xrcriNvLg+diB5pVX9gHbv2JpEhER2VNCFhISAjc3N+zfv191XVy5cqVaPnfuHJ566im1jyQujRs3hiOZNGkSwsLCVGsgEZE9GtgwEOUL5sUGUw2sdGphXrn+PSD8pLVDIyIiynWynJCtXbsWX3zxhZrEo3z58uqeY9JCZhk7RkREtsnZoMe7Xauo5UnRTyHGxQ9ITjDfMNqUbO3wiIiIcpUsJ2RFihTB+fPnszcaIiJ6LIJL5MPTdYrhNjwxMe5Z88pLu4EdX7MGiIiI7CEha9q0qRoj9vPPP2dvRERE9FhMfrI8fPO4YEVSDWzLc+eG0eveAa6fZg0QERHZekK2bNkyXLlyBQMGDIC3tzfKlSunui5aHq+99lr2RkpERNnKx8MFr7aroJZHRfRCgqsfYIwHlo5m10UiIqLHxCmrLyxZsiSGDh163+3BwcFZPTQRET0m3WoWwa97QrDzHPBa8iBMxVQgZAew6zug3kjWAxERka0mZJJwMekiIrJvOp0O73atjLafb8av0dXRv1BrVI5cDayZAgS1AfKXtnaIREREDi3LXRYtoqOj1X3HpkyZgoULF6pxZSdOnMie6IiIKMeVCciLYU1KqeUBV7vD6JYfMMYBy8YCJhNrgIiIyFYTsiVLlqiuiyNHjsSMGTOwevVq9W1ru3btVKJGRET2YUzzIBTzdUeE5oWPXYabV17YCuz+wdqhERERObQsJ2Ryc+R+/frhlVdeUa1ib731llrv6+uLli1bYv78+dkZJxER5SB3FwPe6lRZLX8TVhkXCrQ2b1jzJnDjHMueiIjI1hKyDRs2oEGDBhg/fjycnZ3TbKtYsSIOHTqUHfEREdFj0rx8ANpWLqiW+1/rBZObL5AUw66LREREtpiQGY1GuLq63nNbbGzsfbcREZHter1jReRxMeBCfB7M9R1jXnl+M7D3R2uHRkRE5JCynJDVrFkTa9aswZ49e9Ks1zRN3aOsXr162REfERE9RoW83TGhTTm1/PrZcogodqfr4urXgZsXWRdERES2kpDJzZ+7dOmiui327dsXK1aswLFjx9ChQwfEx8erbUREZH8G1C+BioW8ZFJ8DIvoC83NB0iMBpY9J9+6WTs8IiIih/JIsyzOnj0bkyZNwrp167Bo0SIcPHgQ+fLlU7Mtph9XRkRE9sHJoFf3JtPpgL03XLC6xATzhrPrgRN/Wzs8IiIih/JICZmLiwveffddhIaGIiEhQU11P3fuXPj7+2dfhERE9NjVKJ4PfeoUV8tj/g1CfOG6/3VdTE5ijRAREdnKjaFTJ2dEROQ4XnyiPPw8XZBo1PCesZ95ZcRpYM8sa4dGRETkMJwe9QAyvf2JEydUC1lqZcqU4cQeRER2zNvDGf9rXxHPLzyAny/mx/CgDigSshzY8AFQtTfg7mPtEImIiHJ3QiY3hp43bx50Oh30+rSNbYMHD2ZCRkRk5zpXL4xf94Rg25kIjAxtj6WG1dDF3QA2fwS0ecfa4REREeXeLovbt29X09uvX78eiYmJ6r5kqR8zZszI3kiJiOixky/c3u5SGU56HQ5Fe2N3wafMG3bOACLPs0aIiIislZCdO3cObdu2RbNmzeDk9Mg9H4mIyEaV9vdE//qBannkxaZIds8PJCcCa6ZYOzQiIqLcm5AFBgYiMjIye6MhIiKbNK5lEHw8nBGR5IY/vPqbV/77BxCy29qhERER5c6ErE6dOrhx4waWLFmSvREREZFNTvAhSZl46UJNxHuXNm9Y+QpvFk1ERGSNhEwSsZCQEHTt2hUFChRA+fLl0zxee+21R4mLiIhsTL96JVDKPw+SYcAnumfMKy/tAo7yizkiIqKsyvLgr5IlS2Lo0KH33R4cHJzVQxMRkQ1yNujxarsKGDx7D767GoRhxerBL3wHsPoNoFw7wMnV2iESERHlnoRMEi4mXUREuUuL8gFoVMYPW05fxwu3e+FH7ITu5gVg13dAg7HWDo+IiCj3dFm0iI6OVvcimzJlChYuXIiIiAh1o2giInLMafD/16EC9Dpgw62COF6wg3nDpmlA7A1rh0dERJS7EjIZRyZdF0eOHKnuO7Z69Wr1z7pdu3YqUSMiIsdTvqAXetcuppZHh7aH5uQBxN8CNn5o7dCIiIhyT0IWFhaGfv364ZVXXlGtYm+99ZZa7+vri5YtW2L+/PnZGScREdmQCa3LwdPVCWcTvLDOt5d55e4fgOunrR0aERFR7kjINmzYgAYNGmD8+PFwdnZOs61ixYo4dOhQdsRHREQ2yD+vK0Y1N099Py6kCYweAYDJCKx5w9qhERER5Y6EzGg0wtX13jNqxcbG3ncbERE5hkENS6JoPndEa26Y5drXvPL4cuD8FmuHRkRE5PgJWc2aNbFmzRrs2bMnzXpN07Bs2TLUq1cvO+IjIiIb5eZswEtty6vlD0KDEeVdzrxh5auAyWTd4IiIiBw9IZObP3fp0kV1W+zbty9WrFiBY8eOoUOHDoiPj1fbiIjIsbWvUgjBJfLBBD3eSuxjXhl6ADj8m7VDIyIicvxZFmfPno1JkyZh3bp1WLRoEQ4ePIh8+fKp2RbTjysjIiLHIzPrvtaholr+LTIIl/I3NG9Y+xaQFGfd4IiIiBw9IXNxccG7776L0NBQJCQkqKnu586dC39//+yLkIiIbFr1Yj7oUr2wWn4uojs0nR64fQnYPt3aoRERETn+jaFTJ2dERJQ7vfhkebg567EvviD25u9kXrnlUyA6zNqhERER5Y6EjIiIcq/CPu4Y1riUWh595QmYnPMAidHAhvetHRoREZFNY0KWSdOmTUNAQACqVauWMzVCRGSnhjctjYC8rrhm8sZSz97mlXt/AsKOWzs0IiIim8WELJNkEpOwsDA1gQkREf0nj6sTXnjCPPX9y6GNkeBRENBMwOrXWExERET3wYSMiIiyTY+aRVGpsBfi4Yov8LR55alVwJn1LGUiIqLsTsi2bt16142hL126pKbAJyKi3Eev1+F/7c3T4H99Ixg3vMzLWPU/wJRs3eCIiIgcKSG7efMmnn32WQQGBqZZX7hwYTXOau/evdkRHxER2Zn6pfOjTcUC0KDHSzFPmVdeOwIcmG/t0IiIiBwnIdu8eTMqVKgAPz+/tAfU69G5c2csXbo0O+IjIiI79Eq7CnA26LAqpgxO5WtiXrnuHSAh2tqhERER2ZQsJ2QxMTFITEy85za5SfStW7ceJS4iIrJjgX55MKC+uQfF6PAu0PROQPRVYNuX1g6NiIjIMRKy4OBgbNy4EcePp53OOCoqCnPmzFHbiYgo9xrbMgj5PJxx0lgQG73u3Cx62xfA7VBrh0ZERGT/CVlQUBA6deqEunXrYsKECfjmm2/wxhtvoEqVKnB1dUWvXr2yN1IiIrIr3u7OGN+6rFoef7UNjC55gaRYc9dFIiIievRZFmfPno3hw4dj3rx5GDVqFD755BM0bNgQ69evh5ub26McmoiIHMDTdYqjtH8eRMILc5zvfFF3YB4QesjaoREREdl/QiYtYVOnTsW1a9cQHx+vuitKclagQIHsi5CIiOyWs0GfMg3++xFNEONRFIBmngZf06wdHhERkePcGFqSMyIiovSalfNH4yA/JMIZ7yf2Nq88t9F8w2giIqJcLlMJ2YIFC1C0aFG8+OKLKcv3e8g+REREOp35ZtF6HTA3uiZCvaqaC2XVa0CykQVERES5mlNmdq5UqRLGjBmDatWqoUiRImr5fmQfIiIiUa5gXjxVpzjm77yISbd7YS4OAddPAPt+AmoPYSEREVGulamErGrVquohQkJC0K5du5TnREREDzKuZRAW7b2ELfGlcLxwS5S/sRZY/z5QpRfg5sXCIyKiXCnLY8jWrl2LL774InujISIih1XAyw0DG5hvFj02vDM0gwsQex3Y8qm1QyMiIrK/hEy6LJ4/fz57oyEiIoc2omlpeLo64VSSH7bn725eueNr4GaItUMjIiKyr4SsadOmiIiIwM8//5y9ERERkcPKl8cFQxqXVMtjL7dAsls+wBgPrH3L2qERERHZV0K2bNkyXLlyBQMGDIC3tzfKlSuH8uXLpzxee+217I2UiIgcwuBGJZHPwxkRyXnwp88z5pWHfwUu77V2aERERLY9qUdqJUuWxNChQ++7PTg4OKuHJiIiB5bXzRmjmpXBu38fw+QLtdAuoCRcbp0DVv4PePZvQKezdohERES2n5BJwsWki4iIsuKZ+iXww5azuHYb+NFjEIbfeg24uA04vhyo0JGFSkREuUaWuyxu2LABM2fOzPQ2IiIiN2cDnmsZpAri/XOlEFOwrrlQVr8OGBNZQERElGtkOSE7ffo0tm/fft9tO3fufJS4iIjIwfWqVQzFfT0A6PAR7owlu3EW2MMv9IiIKPfIckL2IMePH4efn19OHJqIiByEs0GPCa3LquUfz/vieqku5g0bPwTiIq0bHBERka2OIVu4cCEmT56M6OhoJCQkYM2aNWm2x8bG4vr169i4cWN2xklERA6oY7XC+GbDGZy4FoXXorrja6cV0Ekytukj4Il3rR0eERGR7SVkMr39kCFDsHfvXoSEhKBLlzvfaN7h5eWFxo0bo0aNGtkZJxEROSCDXocJbcpi+Jy9+CfEgIvBz6LEv98AO2cAtQcDvqWsHSIREZFtJWTVq1dXj0OHDuHy5cto27ZtzkRGRES5QpuKBVCtqDcOXrqFF0Kb49c8v0MXEw6seRPo9bO1wyMiIrLNMWRVq1ZVrWF79uxJs/7SpUtYtGhRdsRGRES5gE6nw6Qnyqvl3VeM+LfsaPOGo0uBizusGxwREZGtJmQ3b97Es88+i8DAwDTrCxcujGnTpqkujURERBnRsEx+1C+VXy1POF0Nmr85QcPKVwFNYyESEZHDynJCtnnzZlSoUOGu2RT1ej06d+6MpUuXZkd8RESUS1rJXniinFo+GR6HbSXHmTdc3gMcYa8LIiJyXFlOyGJiYpCYeO+bd8rsi7du3XqUuIiIKJcJLpEPrSoEqOXJhwrAVLKZecOaKUBSvHWDIyIisrWELDg4WE1tL/ccSy0qKgpz5sxR24mIiDJjYhtzK9mlm/H4u+AoddNo3LoI7JrBgiQiIoeU5YQsKCgInTp1Qt26dTFhwgR88803eOONN1ClShW4urqiV69e2RspERE5vAqFvNCpWmG1PGWPAcZqfcwbNn0MxERYNzgiIiJbSsjE7NmzMXz4cMybNw+jRo3CJ598goYNG2L9+vVwc3PLviiJiCjXGN+6rLo/WXhUAubn6Q84ewAJt4CNH1g7NCIiIttKyKQlbOrUqbh27Rri4+NVd0VJzgoUKJB9ERIRUa5S0i8PegYXVcsfb7+N+LpjzBv2zAKun7JucERERLaUkIno6GiVhH3wwQdYuHAhIiIicOLEieyJjoiIcqXnWgbBxaDHrbgkfGfsAOQtBJiMwOrXrR0aERGR7SRkS5YsQcmSJTFy5EjMmDEDq1evVlMXt2vXTiVqREREWVHYxx396pVQyzO2hSK6wUvmDSf+Bs5vYaESEZHDcMrqC8PCwtCvXz+8/fbbGDNmjBpPtmPHDvj6+qJly5aYP38+hg0blr3REhFRrjGqeWn8svsiYhKT8fn1YLxaoApw7TD0q18DOv9i7fCIiFLEJhpxOTIOl27G4crNOLV8+WYcbsQkqsYKvQ7Qq5+plvXmezBa1hl0urT7SrOJBiQmxCO/9w345XWFbx5X5M/jAt87j/yeLvBwyfLlPNmILNfghg0b0KBBA4wfP/6ubRUrVsShQ4ceNTYiIsrF/DxdMbhRSXy57jRm77yEEb1fR/5FPaG7ehBuJ5cBAUOtHSIR5QKapqnE6nKqRCv1siRgkbFJ2f6+LkhCHsQhjy4B8ZoLbiAvTPfo3ObmrIevhwt8PV3uTthSLcvf1CL53OFseOQRS2QrCZnRaFSTetxLbGzsfbcRERFl1JDGpfDz9gtqLNlHp4ri/aAngFMrkXfXJ0DdvoAhLwuTiLLN7fgkbD11HVvPXMeFiNiUhCs+yfTQ1+phQh7Eo1geE0p6mVDC04QA1yS4JsfCOTkWLskxd36al11NcXBNjoGLKVbt42qKhZssm+LUTycY0xw/GXpEwBthJm+Ea/LwQTi8EW7yQXiUD8JveyMEPtineSMa7ub7OKYjM9gW9/VAKb88KOUvD0+1XNI/D/w9XVULHdlRQlazZk0MHjwYe/bsQa1atdJ8i7Bs2TJMnDgxu2IkIqJcytvdGSOalsaHK47j1z0hGDPwFRQ+vQaGmGsw7ZkJNHre2iESkR2T69bTYdFYfyIM646HYc/5SLiZYlBffxRFdLdRFnHwRDzyOMXDU5Z18cjvnIh8TgnIq09Q69y0OLgYY2BIjjMfNBlA5J1HNjLAhABEIkD/8AMnwBU3dD4I07xxNdkb1yR5kyQOPgi/4Y3wCB8cPe6D6/BG0p10IK+r011JWik/TzXzrbuLIXtPhrInIStfvjy6dOmiui327NkTCQkJCA0NRYcOHdQU+LKNiIjoUQ1oUAIzt5zD9egETN2n4dNqT0F3YB50Wz8Hag0C3LxYyESUqfFe289EqCRs/fFw1QomXQNb6vfha8MONHU+BFfdA7ogSmNZYiYKXGcAXD0Bl7x3fnqm+pn3/s9d8yLZyQORMYnI5+kKQ+x1IPoaEB2W7uc1IOoakJyQ8pauSEAh7RoK4RqqPSSXitQ8zcmatLRd80b4VXPytlbzwS+SwGneMHgVQn6/AigZkBflCuZFnUBflAnwZItaNnmkUYAykUepUqUwa9YsXL16FXny5FGJ2E8//QRnZ+fsipGIiHIxGbA+tkUZvLHsXyw7eAVjBoxCmUO/Qhd3A9jxDdBssrVDJCIbd/56jDkBOxGOHWcjkGg0wQPxKgl7zXknmhsOwBWpkjCdHvDwS5soPSh5etBzZ3eZvSNrgScnwxgeDvj7A4YHZFaaBiTcvjtJu1cCFxNuni3kjny6aPUoi8v3P34CkHTJgOuXvHFRC8Dfpoo44lwFziXqoWbpgqgd6ItKhb3gxPFpWaLTpK02GyQmJsLFxQW5xaVLl1CsWDGEhISgaFHzDUytJTk5GeHh4fD394fhQb+sZFWsJ9vHOrJdCcZktPhoo/omu1WFAHye5yfkOTIXcPUCxh0EPHytHSKlwt8l++DI9SR/M3adu6FawDacCMPZ6zFqvTvi0UJ/AO0NO9BSJWGJaZOwkk2ASl2B8h2BPPnhkHWUbARiI+5uZUuTuF2FFn0NuoSoBx4qQXPGPlMQtpsq4oChEgzFa6NmyYKoXdIX1Yv5wM3ZsT5X93PhwgUEBgZmOS/Itnkyc1MyRkREj5erkwHPtwrCpN8PYc2xMOzt/AwaH18EnXwjvO0LoNWbrBKiXC4+KRl/HQrFP0euYuvp64hLksFcgBsS0FZ/AF2kJUy/Hy5aQtokLLDRf0mYpz8cnsEJyFvA/HgA1aaXGAvEhP2XrEVdhSn0EJLObITr7Quqa2d9w1H1AH5HXIgL9l4IwuZ1lfCZrhJQuAZqlCqAOiV9EVwiH7zc2IPukROyBQsWYNKkSRnat0+fPpg6dWpmDk9ERHRfXWsUwbcbz+BMeAy+2JeIRrWHQLf9S2DnDKDuyIdeXBCRYwqPSsDcHRcwb+cFXI9OTEnCntQfxNN59qB+8h64mOLNO6t+Ybo7SVgXoEInwDPAqvHbNBcPwCUQyBeYskomzVdzqd+6BJzbDO38ZhjPbIJzVAjcdYloZPhXPUTsNVfsCS2L7Zsr4UutAhIDqqNWKX+VoDUs7QdvDyZomU7IKlWqpG4CnRHVqlXLVH0TERE9iIxNmNimHEbN24c9IVHY3fQZ1HX5CUiMArZ8CrT9gAVIlIscvXIbs7aew7IDV5CYbFLdD58wHMSz3vsRnLATzjLrYcqwMB1QouF/SRi/wHl03kWB6k9DV/1pqLQq8gJwfrNK0pLPboIh+go8dAloYjisHiI60g27d5fD9p0V8T0qwTOwJlpXLoLWFQugkLdM1Z87ZSohq1q1qnoQERFZw5OVCqJCwbw4djUKn2y9gYX1RwEbPwRkCvwGY8wXCETksJJNmpqeftaWc9h+NkIlYU31h9DZdSdaG/are3kh1rK3Dihe39wdsaIkYQWtG7yjy1fC/KjRDwaZoiLynErOJEkznd0Efcw1dduA5oaD6iFuX3LHrovl8cPyiojwr4OgKvXQpnKRXDeD4yOPIYuOjsbSpUtx+vRpNRV+q1atcP36dZQrVy57IiQiIrpDr9dhbIvSGDX/AHaeu4E9TZ9GLbcZQPxNYONUoNMXLCsiBxSdYMTve0Lw47bzCI24hSb6Q/jUeQfaGPapKesVy72bi9X7LwnzKmzNsHMvSaZ8S5kfwQOglwQt4gxwfpNK0kznt0AfEwYvXRxaGfarB27Ow61NHti5oQJWuteAe9mmqF6rAWoUz6/+9juyR0rIlixZgqFDh6p7kHl6eqJdu3Zo3bq1+nnw4EG1joiIKDu1rlAApf3cceZ6HD7dcg3z5ObQa94E9s8FGo4D8pdmgRM5iJAbsfh5+3ks2n0W1RP34znDDrR23asu5NMoWudOEtYZ8C5irXDpQQmaXxnzo9Ygc4J2/SRwbpPq3ph8bjNcEiLhrYtFG8NetEncCxz5ATcOe2KDvhKiCzVAwWqtUK1mPbg6Z9uchDYjy2cUFhaGfv364e2331bjyuSeZDt27ICvry9atmyJ+fPnY9iwYdkbLRER5XryTemgOoXw6t9nsfV0BPY17Ymaeb42zwQm3Re7fZfry4jInskdmfZeiMTsLScRc2wN2ul3YoN+D7xcUvoimhWtnSoJY3dlu0vQ/Muph6HOUBhMJiD8OEznNuHm0XVwu7wdHsm34auLRgttJ3BFHp8i4m8v/Ju3BpxKN0Wp2k/Cs0jFrN/jzRESsg0bNqBBgwYYP378XdsqVqyIQ4cOPWpsRERE99Q8yAdBAZ44FRaNTzddxpzGE4EVk4FDvwKNxgMBFVhyRHYmKdmEfw5exL4NS1Dxxjq8Y9gNb+d0SViR4P+SMJ/i1gqVspteDxSoCH2BivCtNwIwmaBdO4Kww2sQfXw9CkTuhacWg/y628gfvRE4KI+3cFPvi6s1n0f5DuNyZ0JmNBrh6qomvbxLbGzsfbcRERE9Kr1OhzHNS2PcwoPYfOo69jXvgppeX8oIcWD9e0DvOSxkIjthTDbh7/UboG37Ck2Td6CTLibNFaqpUA3oK99JwlJNv04OTK+HrlBVFJBHmwmAKRnXTu3GhT0r4XRxC4LiDyOvLg4+phs4HK9HeSB3JmQ1a9bE4MGDsWfPHtSqVStNM/OyZcswceJEOKJp06aph9w5nYiIrKdt5YL4cv0ZnA6LxucbLmJ200nAn+OAY8uAKweAwtVZPUQ2bvuJyzj3xxT0iP8dLrrkO3cjBqLyVYJnzZ7QVeoCvW9Ja4dJ1qY3oEC5euohbkTFYu2ujbj571o0qN8R9k6nSQaVRU8//TQWLVqEnj17qok9QkND4ePjg8uXL2P37t1wdnbcm71dunQJxYoVQ0hICIoWtW6/ZUkOw8PD4e/vD4PBYNVY6P5YT7aPdWR/9bT88FWM++WAWr9kZB1UX9LaPNVymdZAv9+tHWquxd8l+2DNeroQEYNFv89D18sfoaT+mloX6eSH5OAh8Kvb2zw7H/F3yU5cuHABgYGBWc4L5GbbWSYTeUyaNAnr1q1TiZnMrJgvXz6sXr3aoZMxIiKyDR2qFkYp/zxq+fN154Dmr5g3nF4NXNxh3eCI6J7T13++bAf2ff4UJoROUsmYCTpcrTAQ+SYdgF/bl5mMUa7zSAmZi4sL3n33XdUyJi1kck+yuXPnqm9aiIiIcppB3ZesjFpefyIch3xaAv53JvRY+7b0o2clENkAk0nDb7sv4uOpr+OZvd3RVb9JrY/0Kg9t8FoU7P054JrX2mES2V9Clj45IyIietw6Vi2Mkn7mVrIv1p/9r5Xswhbg7AZWCJGV7Tl/A6O+/BVFlvXGG8nT1VTmiXo3xDefgnzjtsJQLNjaIRI5RkJGRERkDU4GPUY3N7eSrTkWhiNeTYBCdyb0WMdWMiJruXIzDhPm7cSm7yfh8xuj0MBwVK2PLdESLmN3wa3p84DB8W7yS5RZTMiIiMjudaleGCXye6jlz9edBlq8Zt5weS9wcoV1gyPKZeISk/HZmpOY9PE3GHniWUxw/h2uOiMS3fyBHj/CY+AiIF8Ja4dJZDOYkBERkUO1kq0+eg3/etQCipmnR8a6d9RNRokoZ6lbHx28gi4fLUeBDS9inmEKgvSX1TZTzYFwGbcHqNwN0N2Z256IFCZkRETkELrWKIJivu5q+ct1Z4CWd1rJrh0Bji62bnBEDu7wpVvo+c02rFk4HXMTxuBpp/VqfbJfeWDQKug7fQ64+1g7TCKbxISMiIgcgrO0kjUzt5Kt+PcqjrlWBUo1N29c/z6QbLRugEQO6FZcEib/fgijpy/C2NCX8IXLV/DX3YbJ4Kq6DhtGbAaK17V2mEQ2jQkZERE5jG41i6KIj6WV7NR/Y8kiTgGHFlo3OCIHs/fCDXT8bD3y7Z+OlS6T0dRwSK3XSjWDftR2oMkLgBNn4SZ6GCZkRETkMFyc/htL9vfhqzjhVBYo1868ceMHgDHRugESOYBkk4bP15zCe9/NwYy4CXjJ+Re46xKheeQHun4H3TNLgPylrR0mkd1gQkZERA6lR3BRFPZ2+6+VrPmr5g03LwL7f7ZucER27vLNOAz6di3ybXgZvzm9gQr6EPOGGv2gG7MHqNabk3YQZRITMiIicrhWspF3Wsn+OhyKU7oSQKVu5o2bPgKS4qwbIJGd+ufQFXz82VRMvTYY/Z1WQ6/TYPItAwz8C+g8HfDwtXaIRHaJCRkRETmcXrWKopC3GzRNWslOA81fAXR6ICoU2P2DtcMjsiuxiUZ88MtqOP3WF5/gExTQ3USy3hlo+hL0o7YBgY2sHSKRXWNCRkREDsfVyYCRzcxjWP48dAWnTYWAan3MG7d8CiREWTdAIjvx76UI/PTxJIw91hetDfvUuvgi9WEYuQ1o/jLg5GrtEInsHhMyIiJySL1qFUMBL1fVSjZ9/Wmg6YuAfKsfGwHs+Nba4RHZ/E2el/z9F7TvW2JUwizk0SUg3skLyR2/hNuQfwD/stYOkchhMCEjIiKH5OZswIim5laypQcu46wxPxA8wLxx25dAXKR1AySyUddvRGDVJ4PQcWdfVNadM68r1Rluz++DIbg/J+0gymZMyIiIyGE9Xac4/PO6wqQBX0krWWO5L5IbkHAL2PqFtcMjsjlH1v0C4xd18ETUHzDoNIQ7F0JMr9/h1/9nwNPf2uEROSQmZERElEtaya7gfKIXUGeoeePOb4HocOsGSGQjEiMv49jnXVB503AUxHUkaQYcLT0EfpP2IU/F1tYOj8ihMSEjIiKH1qdOcfh5uqqb2aqxZA3HAy6eQFIssOUTa4dHZF2mZNzYMB3GL4JRIXK9WnXUUB6hT61ExWc+hs7FgzVElMOYkBERkUNzdzFgeJNSavmP/ZdxMd4dqDfKvHH3TODWZesGSGQlWtgx6Bf0gP/m1+ChxeG25o6/ik9C6Ze2oHiF2qwXoseECRkRETm8vvWKI38el/9ayeqPBtx8gOQEYNM0a4dH9NglnlwL4/etEBB1VD1frauPw13XoP2g/8HV2Zk1QvQYMSEjIiKH5+HihGF3WskW7buEkDgXoOFz5o375wA3zDPJEeUGMXsXQj+/F9xMcQjXvPCR39uoPmEpGlavbO3QiHIlJmRERJQr9KtXAr55XGA0afh6wxmg7gggjz9gMgIbPrB2eESPReT6L+H+53A4wYgLpgDMLz8d44aPUrOREpF1MCEjIqJcIY+rE4Y0LqmWf98bgsuxeqDxRPPGQwuBsOPWDZAoJ2kari1+Bfk2/g96aDhiCsTelr+gd/M60Ot1LHsiK2JCRkREuUb/+oHw8XBGUrKGr2UsWfCzgFcRuVoFNrxn7fCIckayEZd/HoICB6erpzu0yojs9Qc6N6rOEieyAUzIiIgo1/CUVrJG5layX/eE4EqMBjSZZN54dCkQetC6ARJlt8RYhHzbHUXO/a6ertHVh9eQxWhc2Xx/PiKyPiZkRESUqwxoEAhvd3Mr2bcbzwA1+gH5As0b171r7fCIso0pJhIhXzyBYuEb1POlLu1QYezvqFgsgKVMZEOYkBERUa6S180Zg++0kv2yKwRXo5OBZi+bN55aCYTssm6ARNkgPuIirn7eDMWiD6nnv3oNQPPxs1HE15PlS2RjmJAREVGuM7BhILzcnJCYbMI3G04DVXoC/uXNG9e+Ze3wiB7JzYtHEDW9BQonnkeypsOvhV5El+c+g5e7C0uWyAYxISMiolzHy80ZQxqb70u2QFrJopKA5q+YN57fDJzdaN0AibIo9N9N0M16Ev6mcMRrzlheYRp6DnsFLk685COyVfztJCKiXOmuVrIKnYCCVc0b172tpgknsient/4Bn9+6wxtRuKXlwbYGP6DzU0Oh03FaeyJbxoSMiIhypbtayW4nAC1eM2+8tBs4udK6ARJlwqG/vkXgqsFwRyKuaflwuv2vaPFEF5YhkR1gQkZERLnWXa1kQa2BYnXNG9e/A5hM1g6R6KF2znsTVXdPhpPOhPMogtt9/0ZwnUYsOSI7wYSMiIhyrQe2kl09DBxbat0AiR4gOTkZ274ZgbqnPlXPjxvKwm3EKgSVrchyI7IjTMiIiChXu6uVrGRjoGRT88b17wGmZGuHSHSXuLh47P60FxpcW6CeH3SrhSLjVqNgwaIsLSI7w4SMiIhytbtayW7FAy1fN2+8fhI4tNC6ARKlcyPyBo592g71oteo53u926DihL+R18uHZUVkh5iQERFRrndXK1nRWkDZtuZy2fABYEzM9WVEtiEyPBRhXz2Bmol71fO9RZ5BzXG/wNnF1dqhEVEWMSEjIqJc756tZJb7kt28AOyfk+vLiKzv1pUziPmmJconn1TPD1aYiOChX0GnN1g7NCJ6BEzIiIiI7jWWrFBVoFJXc9lsmgYkxbGcyGpunT+ApO9boajpMpI0A3ZXfx/Vet/pWktEdo0JGRER0Z2xZIMbpWsla/YKoNMDUaHAnlksJ7KKqBMbYZjdDn7aDcRqrthZdzpqdxnF2iByEEzIiIiI7tdK5l8WqPqUeePmT4CEaJYVPVYxB5fAdUF3eGoxuKF5YnujH9Go3dOsBSIHwoSMiIjoDm/3e7WSTQb0zkDsdWDnNywremxit8+E2+Jn4YIkXNb8sLP5fLRs3Z41QORgmJARERE9qJUsXyBQs79549YvgbhIlhflLE1D3Jr34bFyAgww4bipGPa1Woi2ze7cH4+IHAoTMiIiooe1kjWZBDi5AQm3gG1fsbwo55iSkbBsIty3fKCe7jKVw7Enf0HHxrVY6kQOigkZERHRw1rJvAoBtYeYN+74BogOZ5lR9jMmIGnhQLjun6merkoOxsV2c9G1QWWWNpEDY0JGRET0oFay3XdayRqNB1w8gaQYYOtnLDPKXvG3Yfy5G5xPLDN/7ozNEdn+B/SoV5YlTeTgmJARERE9qJXMaMK3G88AefyAeiPNG3d9D9y+wnKj7BF1Dcmz2sHp4hb19AtjF6Dj5+hdz/ylABE5NiZkRERED2klm7/rormVrP4YwM0bSE4w3yya6FFFnIFpZhsYwg7DpOnwetIA+HV8G0/XLcGyJcolmJARERFltJXM3Qdo8Jx5476fgcjzLDvKuisHYJr5BPQ3zyNBc8LYpLEo12ki+tQtzlIlykWYkBEREWWmlazuCMDDDzAZgQ0fsuwoa85ugPZTO+hjwxGluWNg0mTU7zQEfdkyRpTrMCEjIiLKTCuZqyfQeIJ546FfgPATLD/KnCN/QJvbA7rEGIRrXngq8X9o16k3+tVjN0Wi3IgJGRERUWZbyWoNBvIWBjQTsOF9lh9l3K7vof0+CDpTEi6YAtA9cQqe6tQBzzAZI8q1mJARERFltpXM2Q1oOsm88d/FwJX9LEN6uN0zgb9fgA4ajpgCVTI2pFMLPFM/kKVHlIsxISMiIspKK1n1fkC+OxfSS8cCxkSWI93fod+g/TVRLe42lVXdFJ/r3AD9mYwR5XpMyIiIiLLSSubkAnS4c4Poa4eBzR+xHOneTvwDbfHwlJaxQYkvYlKn2kzGiEhhQkZERJTVVrLSzc3jycSmj9h1ke52bjO0XwdApyXjtKkw+ie+hBFP1MCABuymSERmTMiIiIiy2komWr8F+JQAtGRg8UjAmMDyJLPLe4EFT0GXnIBLmh/6Jb6M9vWqYFSz0iwhIkrBhIyIiOhRWslkGvwuX5t3CD/GWRfJ7NpRYG53IDEa4Zq3SsaqV6qENztVgk6nYykRUQomZERERI/aShbYyHzDaLH1c+DSHpZpbnbjLDCnKxAXiVuaB55JfBn+JSris6eqw6BnMkZEaTEhIyIietRWMtHyDcC3lPneZEtGAklxLNfc6PYV4OcuQPRVxGiuGJg4GUb/ivi+fy24ORusHR0R2SAmZERERNnRSubiAXT5BoAOuH4SWPcOyzW3iYkwt4zdvIBEOGFo0kSE5q2C2YPqwMfDxdrREZGNYkJGRESUXa1kxesB9Uebl7dPBy7uYNnmFvG3gXndgfDjMEKPMYljcdilOn4aVBtFfNytHR0R2TAmZERERI/YSvbp6pP/bWjxP8CvLADN3HUxMYbl6+ike+qCp1JuezApcTg26Oriu2dqoXxBL2tHR0Q2jgkZERFRFlvJRjUvo5Z/3RuCf6/cMm9wdjd3XdTpzZM7rH2L5evIjInAr/2BC1vV09eTBmCJ1hif9K6G+qXzWzs6IrIDTMiIiIiy6NmGgSjm6w5NA95ZfgyaLIiitYCGz5uXd36rbg5MDsgk954bDpxapZ5OS+qFn5OfwGvtK6JD1cLWjo6I7AQTMiIioixydTLg5bYV1PL2sxFYffTafxubvQQEVDQvLx0FJESznB2JJN9/TQD+/UM9/dbYAdOTO2N4k1IY1KiktaMjIjvChIyIiOgRtK1cEHUCfdXye38fU2PKFCfXO10XDcDNi8Dq11nOjpSMSX3u/Uk9nW9sjg+MT6NL9SKY/GR5a0dHRHaGCRkREdEj0Ol0+F8HcyvZ+YhY/Lz9/H8bC1cHmrxgXt4zEziznmXtCDZ/DGz7Qi3+mVwP/zMORqMy/pjaoxr0vPEzEWUSEzIiIqJHVLWoD7rVLKKWP197CjdiEv/b2PgFoGAV8/LSMebp0cl+7foeWPe2WtxgqoEJSaNQvpAPvulXEy5OvKwioszjXw4iIqJs8OIT5eHubEBUvBGfrUk1Db6Ti7nrot4ZuH0JWPkKy9teHVwI/G1u8dyDChiR+BwK5Mur7jWW183Z2tERkZ1iQkZERJQNCnq7YUTT0mp53s6LOHUtKtXGKkDTyebl/XOAU6tZ5vbm+F/m+8oBOKYrjYHxE+Hu4YnZg+ogIK+btaMjIjvGhIyIiCibDGtSCoW83ZBs0vDu38fSbmw0HihU3by8bCwQF8lytxdnNwC/DQS0ZFzQF0OfuEkwOnti5sDaKO3vae3oiMjOMSEjIiLKJu4uBrz4ZDm1vOFEODaeDP9vo8EJ6PotYHABokKBFS+z3O1ByG5gQR8gORHX9AXQK3Yybum88NXTNVGzeD5rR0dEDoAJGRERUTbqXK0IqhX1VsvvLD8KY/KdafBFQAWg+avm5YMLgON/s+xt2dUjwLzuQFIMbhl80TPuJVyDL97tWgWtKhawdnRE5CCYkBEREWXnP1a9Dq91MN8Q+lRYNBbsDkm7Q4OxQNHa5uU/xwGxN1j+tijiDDCnKxB/C3EGL/SMnYyLWgE81zIIT9cpbu3oiMiBMCEjIiLKZrUCfdG+aiG1/MmqE7gVl5TqP6/BPOuikxsQEwb8PYnlb2tuXQZ+7qLqJ8nggadiX8BJrRi61SiC8a2CrB0dETkYJmREREQ54KUny6v7UkXGJuGrdafSbvQLAlq+bl4+8jtwdCnrwFbEXAfmdAFuXYRJ74KB8RNwUCuDOiV98X73KupG4ERE2YkJGRERUQ4o5uuBwY1KquWftp3H+esxaXeoOwIoXt+8vHw8EJ1qAhCyjvhb5m6K109C0xnwXPLz2JpcEaX88uC7Z4Lh6mRgzRBRtmNCRkRElENGNSsNP09XJCVreP+fdNPgS9fFztMBZw8gNgL4awKgaawLa0mMBeb3Bq4eggYdpjg9h+UJ1ZHPwxmzBtaGj4cL64aIcgQTMiIiohyS180ZL7Qpq5ZX/nsN289EpN0hf2mg1RTz8rFlwJFFrAtrMCYCvz4DXNyunn7rOQo/RdWGi0GP7/rXQqBfHtYLEeUYJmREREQ5qGetYqhQyEstv/PXUXXT6DRqDwECG5uX/34BiLrG+nicTMnAH0OB02vU08X5h+DD6w3V8rSeVVE70Jf1QUQ5igkZERFRDjLINPjtK6jlf6/cxqJ9l9L9J9abuy66eAJxkcDy59l18XGRLqJy64GjS9TT7YWewfjLLdTyhNZl0bl6kccWChHlXrkyITt37hzKly+f5lG9enVrh0VERA6qQRk/tKpgvpHwtJUnEJNgTLtDvhJAm3fMyyf+Bg4ttEKUuTAZW/U/YP8c9fRU8V54+tyTarlbzSIY26KMlQMkotwiVyZkRYoUwZIlS1Iebdu2RYUK5m8viYiIcsIr7crDSa9DeFQCvtlw5u4dggcCpZqbl/9+Ebh9hRWRkzZNA7Z/pRbDAjuh7enOAHSoW9IXH3SryuntieixyZUJmYuLS5rWsZUrV2Lw4MHWDouIiBxYKX9P9K8fqJa/33wWl2/Gpd1B7m/V+SvA1QtIuAUse45dF3PKjm+B9e+qxegSrfDEuadgNOnU9PYznglW948jInpcbOovzpUrV/DJJ5/gpZdeQkREupmoUlm/fj3efvttTJs2DcePH3+k99y6dSvi4uLQsmXLRzoOERHRw4xrGQQfD2ckGE348J97/P/yLgo8+b55+fRqYP9cFmp2OzAfWDFZLSYWa4iOV4cgMgFqevsfn+X09kSUixOyzp07o169elixYgU+/PBDREZG3nO/0aNHo3v37oiKisKJEyfU2K/58+en2addu3Z3jRGzPBITE9Ps+8MPP2DQoEHsmkBERDnO28MZz7cMUsvLDl7Bvov3+F9XvS8Q1Ma8vOJl4GYIaya7HF0GLB2tFk2FaqJ/7Hicu2VS09t/378WSuTn9PZE9Pg5wUaMGjUKrVq1wuLFi7F69ep77rN27Vp8/fXX2LRpExo3Nk8RXLx4cYwcOVKNA8uXL59aN336dCQkJNzzGM7OzinLktQtWrQI//77b46cExERUXp965XAnB0XcCY8Bm8vP4o/RjZI+6WgLHf8Avi6LhAvXRfHAM8sMa+nrDuzDlg0GNBM0Pwr4EW317DjXFzK9Pa1OL09EeX2FrInnngCBoPhgfvMmzdPtXJZkjExbNgw3L59G3/++WfKupIlS963hSz1Pz1pWWvYsCGKFSuWQ2dFRESUlrNBj/+1r6iW91+8qVrK7uJVCGg7zbx8dgOwZxaL8VFc3An80hdITgTyBeKrolPx+zFzMjaR09sTkZXZTAtZRhw4cABVq1ZNs65gwYLw9/fHwYMHM3086a44ebK5H/nDSNInD4vQ0FD1Mzk5WT2sSd7fZDJZPQ56MNaT7WMd2QdHqKfGZXzRuEx+bD4dgQ/+OY5W5f3h5pzuS8lK3aE/uhS6E39BW/UaTCWbm6fHtwM2VUen10D/xxDokmKh5S2EpVWm4+NVt9SmbjUKY2TTkrYRZ26vJ7on1pF9eNTfIbtKyK5fv466devetd7X11dty6y5c+eidOnSGdpXJhuZMmXKXetl8hFXV1dYk/wxvXXL/M9FLzcYJZvEerJ9rCP74Cj1NKJeAWw9E4HQW/H4YtW/eLZOobv20dd9BX4XtkIffxPGP4YjsuNsQGf752wTdWQywnPPl/Dc9635qZsPNlT7DBNXm+OqWdQT4xsVyNL1g6OwiXqiB2Id2YcbN27knoRMaHIjx3usS9P/PoPKlSuX4X0nTJiAIUOGpGkhq1OnDvLnz69a6GwhK/fz83tot0+yHtaT7WMd2QdHqSf51/FU7SjM3xWCOXuuYUDjsijg5ZZ+L6DdR8AfQ+B6ZRcCzi+FVmcYbJ3V6yjqKvSLh0N3YYt6qhWojLPNvsJzv4QhWYOa3v77AXXg4+GC3Mzq9UQPxTqyD7GxsbknIZPE517T4cu6nE6KvLy81CM9+QNmC3/E5JstW4mF7o/1ZPtYR/bBUeppYpty+PNgKKISjPhkzWl81LPa3TtV7QkcXw4cXQL92ilA2TZA/oz17siVdXRuE/D7YCAmzPw8eCDCG76JAd/tR3SCEb55XNT09vnzuj/euGyUo/wuOTLWke171N8fu2qfDg4OVuPIUgsJCVEJWY0aNawWFxERUVbk93TFmBZl1PLvey9hxRHz+OS7tP8Y8PADjHHAklGAiWN+7mIyARunAT93Nidjzh5A1+8Q+8THGDr/X3Ujbrnh83fPBHN6eyKyKXaVkPXv3x+nT5/GypUrU9bJFPcyhqxjx45WjY2IiCgrnm1YEtWKeqvlF347hLPh0XfvlMcP6PiZeTlkB7DjGxZ2ajERwLwewPp31LT28CsHDF2PuAo9MPinPTh4yTxOSlogOb09Edkam+my+N133+Hs2bPqZs/io48+go+PD5o0aaJu9CwaNWqEl19+GT169ECfPn1Uy9g///yjpq/Pmzevlc+AiIgo86TV5ut+wejwxWZExiZh5Nx9WDy6ATxc0v2LrtARqNITOPwbsPYt882j/cuyyGVK+9+fBW5fNpdF1d5Ah08Rr3PDsJ/3YPtZ81CHV9tVQKdqhVleRGRzbKaFzNPTUyVgMovi+++/j8DAQPXc3T1tH+/33nsPGzduRKVKldCmTRuVwHXu3NlqcRMRET2qIj7u+OypGurezyeuReHVxUfuOYkV2k4FPAsAyQnAkhFAsjH3Fr6Uz7YvgZ/amZMxgyvQ8XOg6wwk6t0xat4+bD5lnkFx0hPlMLRJKWtHTERk2y1k0uKVUTVr1lQPIiIiR9G0rD+eb1kWn645icX7LyO4RD70q5fuvmMevuakY8FTwOW9wPYvgUbjkevERQJLRgMn/jI/9y0F9JwNFKqKpGQTxszfh3XHzZN6jGsZhNHNzeP0iIhskc20kBEREeV2Y1uUQbNy5lmD3/rzKA6E3Lx7p3JtgWp3vsRc/x5w7ShylSv7gRlN/0vGKnQChm1QyZgx2YTnfzmAVUevqU2jmpXG862CrBsvEdFDMCEjIiKyEXq9Dp/2qq66MCYmmzB63j7ciEm8e8cn3wfyFgaSE4ElI4HkJOSKLoq7vgdmtgFuXgD0zuYunL1+Bty8kWzSMPG3g/jrsHmmyiGNSqquilm5TykR0ePEhIyIiMiG5Mvjgm/61YSLQa+mah/3y36VbKTh7gN0+tK8HHoA2PIpHFpCFLBoMPD3C+Yk1LsYMGgFUHc4ZOCdyaRh8qJDWHrgitp9QP0SeLV9BSZjRGQXmJARERHZmKpFffBmp0pqWSam+GLtqbt3CmoF1BxgXt74IRB6CA7p2r/Ad82AI4vMz4OeAIZvAorWUk8lGXt1yWF1HzfRp25xVXZsGSMie8GEjIiIyAY9XacYutcsqpa/WHcK60+YJ6lIo8075tYik9HcddF4j+6N9mz/XOD7lkDEaUBnAFpNAZ7+xTy5ierFqOHNP//Fgl0h6nnP4KJ4p3NlJmNEZFeYkBEREdkgaeF5p0tllC+YVw2fGr/wAEJuxKbdyc0L6PyVefnaEWDTNDiExFhgyShg6WjAGAd4FgQG/Ak0el4G2qUkY+/8dQw/b7+gnnetUQQfdK+qxuEREdkTJmSZNG3aNAQEBKBatWo5UyNERER3uLsY8G2/YOR1dcLN2CR1b634pOS05VOqGVB7iHl588fA7plAjPlmyHbp+ingh5bAgXn/nd+ILUBgw5RdJBn7cMUJzNxyTj1vX7UQpvWoCgOTMSKyQ0zIMmnSpEkICwvDwYMHc6ZGiIiIUgn0y4OPe5m/BDx8+RbeWn6Pae6lK1++QEBLBv6aAHwUBMzuaJ6V8LZ51kG7cPh383ixMDlHHdDsZaDfH4Cn+VYAFp+uOYVvN55Ry09UKoDPeleHk4GXNERkn/jXi4iIyMa1qVQQI5qWVsvzd15MmcAihasn0Oc384QXBhdzYnZuk3lWwk/KAz+0BrZ9CUSeh00yJgDLJ5hnUkyMBjz8gGf+AJq9BOgNaXb9at2plElOWpYPwJdP14QzkzEismNO1g6AiIiIHu6FNmVxICQSO87ewKuLD6NiIS9ULOz13w7+ZYG+vwLxt4FTq4CjS4HTa4CkWODSLvNj1f+AglWBip3MN1T2L2f9or9xDvhtoHn6flG8PtBjFuBV+K5dZ2w8g49WnVTLTcr6Y3rfmnBx4nfLRGTf+FeMiIjIDkiXPGkNCsjrigSjCSPn7cWtuHvcEFom+qjSA+g9B5h0Bug9F6jaG3D1Nm+/eghY9w4wvQ7wVR1g7dtA6EHzjZcft2PLgRlN/0vGGj4PDFh+z2Rs1pZzeP+f42q5Qen8+O6ZYLg5p209IyKyR0zIiIiI7IR/Xld83bcmnPQ6XIiIxQu/HVQTXNyXiwdQoSPQ7Ttg0mmg7yKgZn/AI795+/UTwOaPgBlNgM+rAStfBUJ2yc29cvZEkpPM77WwL5BwC3DzAZ5eCLSeAhju7rwzZ8eFlLFzdQJ98cOAWkzGiMhhMCEjIiKyI7UCffFyuwpqefXRa5ix6WzGXujkYr6ZdKcvgYknzS1RdYYDee+0Rt28AGz/CpjZGvi0IvDXC8DZjUCyMXtP4NYl4Md25vcShWuab/Rc7sl77r5w90W8tuSIWq5Z3Aeznq0NDxeOuCAix8G/aERERHZmUMNA7LsQib8Oh2LqiuOoVtQH9UvfafXKCGmFKtnY/HjyA+DyXuDYMvNDJv6ICgV2f29+SGtauXbmMWelmgJOrlkP/NQa4I+hQNwN83NJCOXm1pIspiMtf3LD51eXHFbPqxX1xk+D6sDTlZcuRORY+FeNiIjIDm8a/WGPqjh29TbOhsdg7IJ9+Ou5xijg5Zb5g8mNlovVNj9av2W+wfTRO8lZ+HEgNgLYP8f8cPUCyj5hTs7KtARc8mTsPaSVbcP75vukQQNc8gKdvwQqdb3n7hHRCfjfkiP458hV9bxSYS/8PKguvNycM39+REQ2jgkZERGRHZKWohn9gtF5+lZcj07E6Hn7sGBYvUebAl6nAwpWMT9avAqEn7zTcvaneeKNhNvA4d/MDyd3cxdISc4kSXO7M2lIelHXzNPZn99sfl6gCtBrNpDfPI1/etIN8+U/DqlzEo3K+OGLp2vA24PJGBE5JiZkREREdiqoQF68360Kxv1yAHsuROKDf47jtQ4Vs+8NZCp9/xeAJi8AkReA48vNrWchOwFjnDlRk4fc+6xUM/MEIuXamyfpEOe3AH8MAWLCzM9rDgDafgg4u9/1VlHxSXh7+VH8usd8jzU3Zz1eaVcB/eqWgF6vy75zIiKyMUzIiIiI7Fjn6kWw/+JN/L+9e4GKslobOP6goIBCioKRoOQtywteMTUvmaUpal5Weiz9XJV0MUNXUWp+eTodv9bK8mh9pWWmpa5V+WV4K/OWKWpZmnjB1AwLVATBu4EC861n43DGRA8MwvuO/H9rzYIZmZntu9fM+z57P/vZ87cclrkJyVLT30diuja88ftz1awv0nFMwe1sWkFwpsFY8iaRvIsFe5/pzStWKtXvLIH+YVJp32cijnwRH3+R6H+JRA4r8qW3Hso0FSOPnPrT3G8VXkOmPxwpDYKr39j/AwDYEAEZAAAeTmeSdqWekh1/nDIbJ8fvPCr/6N9MOjWqXTZvGHCrSPsnCm4XskT2f12Q2nhovQnOvA5vEn/n39a+oyBFMaSgMqSr7Et5Mu2b/SaQVFrOf1zPxvJUt4Zm3zUAqAgIyAAA8HA6G/bRqPbyP1/tMyl/v6afk+Ef/iD9Im+TyX3vdK/YR3H5B4m0fqTgln3GzJI5kpaK47eNIk0flEp93hSpevVM1+7U0zL+852mrapJneoy/eFW0rzuNdaiAcBNioAMAICbQA3/KvLGkEgZ2j5c/jt+ryQdOyPLE4/K+n3HZfz9TeS/OkWUruBHcfgGirQYIvl3DZSMjAwJDg4WqVz5ij+5lJcv7317SN5Zf1By8x2mjkhMlwamjb4+V/4tAFQE5AOU0LRp0yQkJEQiIyPLpkcAACiFtvWDZNmzneXV/s0kwNdbzl/Mk3+u3CfRbyfID79lWnpsdTZs8Kwt8q+1B0wwFh7kJ5/FdDQbXROMAaioCMhKKC4uTtLT0yUxMbFsegQAgFLS9Vc6I7b++e4yqE1d89j+42dl6Affy/jPdkr62exyPcb5+Q75KCFZ+r69SXalnjaP/S2qnnwd21Wibg8q17YAgN2QsggAwE0qOKCqWZc1rH09eWXpHvkl7ax8+fMRWZtUkMY4smP9Mi+ekXrygsQt3iVbL8/OaZveGNxS7m0aUqbvCwCeghkyAABucjoLtWLsPWaPMt1Q+mxOrvxjRZJEv5MgPx3OKpP3dDgc8n87UqX3jE2FwVjfFqGyelxXgjEAcMEMGQAAFYDOhD1+z+3Sr2WoqcaopfF1xmzI7K0yuE2YTOzTVGpXr+r262sJ+9STf0rKyQvyx4nzsnrPEdmcXJCeGOjrLa891Fz6R94mXlrFAwBQiIAMAIAKJCTQV2YMay3DogrSGA8cPydf7EiV1UlpEtfrDnmkQ32pXOnqoCk3L1+Onc42AVdqVkHglZJ1QVI0CMu6IOlnc4p8v65Ngk2K4q23lGHpfQDwYARkAABUQHc3qCUrn+si8zcflhlrD8jZ7Fx5Zele+ezHFLO2LONsjqQ4A6+TF+ToqWzJy3cU67VDAqrKrQHeMjQqQoZ3qM+sGABcBwEZAAAVlO5LNrprA7OB9D9XJsmKXcdk79Ez8tIXu6/7vFv8fEzJ+vCa/hIe5C/hNf0kzPz0l7CafuJTSQr3ISNFEQCuj4AMAIAKTtMJ/3d4G/lb1Amzviz5xHkTWIVpwFXTzwRd5ncNwoL8JdDX57qvl5eXV25tBwBPR0AGAACMzo1qmzRGAED5oew9AAAAAFiEgAwAAAAALEJABgAAAAAWISADAAAAAIsQkAEAAACARQjIAAAAAMAiBGQAAAAAYBECMgAAAACwCAFZCU2bNk1CQkIkMjKybHoEAAAAQIVBQFZCcXFxkp6eLomJiWXTIwAAAAAqDAIyAAAAALAIARkAAAAAWISADAAAAAAsQkAGAAAAABYhIAMAAAAAixCQAQAAAIBFCMgAAAAAwCLeVr2xp8vNzTU/jx07ZnVTJC8vTzIzMyUnJ0cqV65sdXNwDfST/dFHnoF+sj/6yDPQT/ZHH3kGZzzgjA9KioDMTRkZGeZnVFSUuy8BAAAA4CaRnJwsERERJX6el8PhcJRJi25y2dnZsnv3bgkODhZvb2/Lo3INDLdt2yahoaGWtgXXRj/ZH33kGegn+6OPPAP9ZH/0kWdISUmRTp06ycGDB6VRo0Ylfj4zZG7y9fWV9u3bi51oMBYWFmZ1M/Af0E/2Rx95BvrJ/ugjz0A/2R995DnxgTso6gEAAAAAFiEgAwAAAACLEJDdBAIDA2XKlCnmJ+yLfrI/+sgz0E/2Rx95BvrJ/uijitFPFPUAAAAAAIswQwYAAAAAFiEgAwAAAACLEJABAAAAgEUIyAAAAADAImwM7eF+//13WbNmjeTl5Un37t3ljjvusLpJKEJCQoL88MMP0q5dO+nWrRvHyGZyc3Nl06ZNsn//fgkODjZ9VLt2baubBRdLly6VgwcPmt/9/PykYcOGct9994mPjw/Hyaa2bNlibh07dpTOnTtb3Rxc9v3335tz0l899NBD0qhRI46TzezcudNcP9SoUUN69+4tt9xyi9VNwmXz58+XEydOSFFGjBghderUkeJihsyDLVq0SJo2bWoCss2bN0vr1q1l5syZVjcLLuLj402QPGHCBJk8ebIsX76c42Mzq1atkgYNGkhMTIwkJibKu+++ay72FyxYYHXT4OLkyZOSlpZmbrt375Znn33WfLYOHTrEcbKhjIwMGTRokLz44ovy9ddfW90cuFi7dq1MnTq18PPkvOXk5HCcbET7Y+jQofLAAw+YgGz16tXSqVMn2bNnj9VNw2WZmZlXfY7mzJljrvlKihkyD6WdrheQr776qjnhqZ49e8pjjz0mvXr1MoEarFezZk1ZsWKFNG7cmBkXG48WDxw4UN566y3x9i74SnzppZdk9OjR5kRYkhEulJ1Ro0ZddbGig1Bjx46Vr776ikNvM3p+GjBggBk4hP3UqlVL3nzzTaubget47rnnzOzYvn37TH+p48ePy7lz5zhuNvH8889flW3zySefyIMPPljiawdmyDzU4sWL5eLFi/L0008XPvbII4+YDekWLlxoadvwb5r6psEY7Es/Nzqz7AzGlI7s6wW/ngxhT1WrVjWfr127dlndFPzFxx9/LD/99JNMmzaNYwO4ITU1VebOnSt///vfC4MxpRf5msEBe1q5cqXJDtDJkZJihsxDbd++3XwoAwICCh+rXLmytGzZ0vwbgOIpKmBOSkoyP+vXr89htCmHwyE7duyQVq1aWd0UuEhJSZHY2FgzMKgDhLCnCxcuyIcffij5+fkm9bdLly5SqRJj9HZKK9XaAD169DBLHfRzpan1er9KlSpWNw/XWVMWEhIi0dHRUlIEZB4qPT29yBQ4HUnRQh8A3JOVlSVTpkyRu+++m9Rfmzlz5ox88MEH5mJSL1h8fX1l1qxZVjcLLkGyppbqxYg7FyQoPxp8bdy40fTZpEmTzODT559/zuyLTRw+fNhkbQwfPlyys7OlefPmMn36dBOM6brnevXqWd1EFLGeTGfINI3enWJTBGQezMvLq8jH9AsWQMlpmuLgwYPNBT9FPexHR4x1/ez58+dNcKYXlZraEx4ebnXTICLvvPOOKbiiF/awL13bFxcXZ9J+nYNQUVFRMnLkSFMgDNbTmUtdj6RVfz/99FPzmH7vtWjRQsaPHy9ffPGF1U3EX+h62UuXLrmVrqgIyDzUtWbC9IvVNd8YQPHoyU8rWmka3Pr16yn/bNMiOa6FCPQCUi8uNZ3HeXEJ62hhnDZt2si8efMKH9MLFC2co/02ZswYs2UBrKUX9a6CgoLkySefNAXCtJqpfs5gLWcGlJZOd6pWrZoZMHT9fMFea2c7dOggzZo1c+v5JAx7KF038euvv8qff/5Z+JjOjO3du1ciIyMtbRvgiaORmmqlaXBaortt27ZWNwnF0K9fP7OAWtN7YD2tTKqpVa4loPW8pCP7zt9hT86iRjoLDetpBVlnbQBXep8+sh/dikAHc92dHVMEZB5qyJAh5uSm5TWdli1bZtaWac4xgOLTkfslS5aYLQp0nxfYi37XFbX3zjfffGMKG5GyaA+616LOhLnedM2LbuCtv/v7+1vdRIiY/RZd6cCuFiPQmbOi1qaj/Ol5SIt4fPnll1dkceg5SqvLwl501lK/34YNG+b2a5Cy6KF0Aa6WFB43bpypCKfpOrNnz5aJEycWjqzAevv37y/cDFpPeloK2nmRonuMwHozZswwn50+ffqY/tGbk+4l4m76AW5sQKYjj6GhoaaSrN7XggR6YamV4rjQB4pPrxu0II6ml+r2OboeST9TOigFe9CZME2B0/OSZgHo954WjNDZZj1nwT40UNb1Y5pOWprKsl4Ocgg8mpa4101RdQpbRyG1dC3sQ/exKmpfOA2gp06dakmbcKX4+HhJSEgo8rDoaFe7du04ZDaxbt062bZtm/m+i4iIkP79+1Na3eZefvllc17q3bu31U2Biw0bNsjWrVvN702aNDHpv5RTt5+jR4+afWe1gp9u0aIX/QxA2YuuYda9THW9X2mWDBGQAQAAAIBFWEMGAAAAABYhIAMAAAAAixCQAQAAAIBFCMgAAAAAwCIEZAAAAABgEQIyAAAAALAIARkAAAAAWMTbqjcGAAAAgOL67bffZOHChWbT7EmTJkm9evWkPK1Zs0YSEhLk/Pnz0r17d4mOjr4hr8sMGQAAJeBwOOTUqVOFt5JwfZ6+DgCgeAYMGCD333+/7NmzR95//31JT0+X8jRq1CgZOnSo5OXlSWBgoMTExMjIkSNvyGt7OTgjAABg5OTkyMWLFyUgIOCaRyQtLU1CQ0OlWrVq4u3tXaKgrEaNGpKbm2tGV1NSUiQsLIwjDwDFsG3bNmnfvr2sW7fOBGY//vijtGvXTsrDsmXLTED43XffSdeuXc1j+/btk+bNm0t8fLz069evVK/PDBkAAJc988wzMmjQoGIdjxUrVrg1Q6bPAwCUTFRUlHh5eRXrb7ds2SKvvPKKxMbGyrx58+TSpUulOtybN282A3XOYEzdeeed0qBBA1m0aJGUFgEZAKDCc6Yh6km8devWpU4r1Jk2AED5i42NlW7duklGRobJZpg+fbrce++9JjvBXT4+Piao++s5QTMqEhMTS91mAjIAQIWXlJRkFof/8ssvMmvWLImIiJAmTZqU6ASen58vL7zwgtSqVcukJtatW1emTJlCcAYA5WTlypXy9ttvm1kx/S6fMGGCmd06cOCAecxdPXv2lOzsbFmwYEHhY6tXr5Y//vhDMjMzS91uqiwCACq8Zs2amRO5pqNoUKbBVEnNnz9fPvroI9mwYYO0bNnSjM7qwnNdZ9CqVasKf4wBoKwtWbJEQkJC5NFHHy18TAtwaCq6/tvo0aPNY+PGjTMB1vU8/PDD0qNHD/O7VlScPHmyPPHEE7J48WIzY6Zr2Dp37izJycmlbjcBGQAAIrJjxw6pU6eOW8GY0jLMtWvXlrvuusvcDw4ONidwAED5yMjIMOvMnnrqqSse3759u5w5c6bwfosWLf7jujL9Dnf12muvmUqLeq7QjIg5c+aYAO1G1EckIAMA4PIJu02bNm4fCy1/rLNkjRs3NhW3unTpIr169TKjswCAsle3bl1TAfevWQl6v3r16oX3H3/8cbdev2HDhuamdIZNqy6OHz++lK1mDRkAAIaOerZt29bto6Fr0DQ9Udcp6EzZG2+8YSpwaVoLAKDsjRo1So4dO2YG13SWzHnTlMXSbjOi5faddFZs4sSJUqVKFRkzZkyp280MGQCgwtORTl07phUWS0PXFehaA71pyWUdlZ07d67ZOwcA4L7Zs2fLzp075ciRI+b+66+/btIK77nnnsI1Yx06dJD33nvPZCfoAJsGYbrn4+HDh021xdLQgh4ahGla+s8//yznzp2TVatWSVBQUKm7lYAMAFDhOdcAaCWurKwsM+rpmt5SHHFxceY50dHR5iJAT9h6ERATE1Phjy8AlNbtt99ufupAV9++fQsfDw8Pv2qWbODAgbJx40Y5ceKEeZ4Gan5+fqV6f01J14BQy9xrAKil9XUQ7kbwctyIlWgAAHg4LZE8Y8YMSU9Plz59+lxzs8+0tDSzt823335rZsKcTp8+LTNnzpSlS5eaAh96kTBixAgZO3bsFc/XKoy6J46O2pY2hQYA4PkIyAAAKAFnQLZ8+XKTKqN7jhWXbjadkJBgin4QkAEACMgAACghnUHTTaNdg6zicg3edDPq2267jeMPABUcM2QAAAAAYJFKVr0xAAAAAFR0BGQAAAAAYBECMgAAAACwCAEZAAAAAFiEgAwAAAAALEJABgAAAAAWISADAAAAAIsQkAEAAACARQjIAAAAAMAiBGQAAAAAYBECMgAAAACwCAEZAAAAAIg1/h/gQuT3bpLuYQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "sim_coarse = Simulation(\n", " model=build_model(),\n", @@ -451,38 +4434,105 @@ ")\n", "out_coarse = sim_coarse.run()\n", "\n", - "out.scalars.electric_energy.struphy.timeseries(\n", + "out.scalars.electric_energy.struphy.plot.timeseries(\n", " out_coarse.scalars.electric_energy,\n", " title=\"Electric energy: dt = 0.05 against dt = 0.1\",\n", ")" - ], - "execution_count": null, - "outputs": [], - "id": "32" + ] }, { "cell_type": "markdown", + "id": "33", "metadata": {}, "source": [ "## Other models\n", "\n", "The interface is the same for every model; only the products differ. Two more short runs show the two product types the Vlasov–Ampère demo does not have: SPH densities, and vector fields on a mapped domain." - ], - "id": "33" + ] }, { "cell_type": "markdown", + "id": "34", "metadata": {}, "source": [ "### SPH densities\n", "\n", "A standing sound wave discretized with SPH markers. `KernelDensityPlot` reconstructs the density on a grid, which appears under `out.densities`, while `BinningPlot` produces the binned quantities under `out.distributions`." - ], - "id": "34" + ] }, { "cell_type": "code", + "execution_count": 19, + "id": "35", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time stepping: 100%|██████████| 80/80 [00:01<00:00, 40.45step/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "No post-processed data in /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave, processing with default options (call out.process(...) to choose them)\n", + "\n", + "Post-processing path /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave\n", + "\n", + "No feec fields found in hdf5 file, skipping post-processing of fields.\n", + "Evaluation of 3 marker orbits for euler_fluid\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "100%|██████████| 81/81 [00:00<00:00, 1507.65it/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluation of distribution functions for euler_fluid\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "0 starting post-processing of distribution functions for /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave/post_processing/kinetic_data/euler_fluid ...\n", + "100%|██████████| 1/1 [00:00<00:00, 450.90it/s]\n", + " 0%| | 0/1 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "out_sph.euler_fluid.view_0.n_sph.struphy.slice(\n", + "out_sph.euler_fluid.view_0.n_sph.struphy.plot.slice(\n", " x=\"t\",\n", " y=\"e1\",\n", " e2=0,\n", " e3=0,\n", " title=\"SPH density of the sound wave\",\n", ")" - ], - "execution_count": null, - "outputs": [], - "id": "37" + ] }, { "cell_type": "markdown", + "id": "38", "metadata": {}, "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" - ], - "id": "38" + ] }, { "cell_type": "code", + "execution_count": 21, + "id": "39", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "density = out_sph.euler_fluid.view_0.n_sph.isel(e2=0, e3=0)\n", "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" - ], - "execution_count": null, - "outputs": [], - "id": "39" + ] }, { "cell_type": "markdown", + "id": "40", "metadata": {}, "source": [ "### Vector fields on a mapped domain\n", "\n", "A coaxial waveguide mode of the Maxwell model, on an annulus. With `physical=True` the post-processing also computes the Cartesian field components (`*_phy`), and `coords=\"physical\"` draws them on the mapped grid, with the plane chosen by `plane`." - ], - "id": "40" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "41", "metadata": {}, + "outputs": [], "source": [ "a1, a2 = 2.326744, 3.686839\n", "\n", @@ -598,16 +4682,16 @@ "\n", "print(\"fields:\", tuple(out_coaxial.field_catalog))\n", "print(\"dimensions:\", out_coaxial.em_fields.b_field_phy.dims)" - ], - "execution_count": null, - "outputs": [], - "id": "41" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "42", "metadata": {}, + "outputs": [], "source": [ - "out_coaxial.em_fields.b_field_phy.struphy.slice(\n", + "out_coaxial.em_fields.b_field_phy.struphy.plot.slice(\n", " x=\"e1\",\n", " y=\"e2\",\n", " t=\"last\",\n", @@ -617,16 +4701,16 @@ " plane=\"XY\",\n", " title=\"$B_z$ of the coaxial mode\",\n", ")" - ], - "execution_count": null, - "outputs": [], - "id": "42" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "43", "metadata": {}, + "outputs": [], "source": [ - "out_coaxial.em_fields.b_field_phy.struphy.panels(\n", + "out_coaxial.em_fields.b_field_phy.struphy.plot.panels(\n", " x=\"e1\",\n", " y=\"e2\",\n", " component=2,\n", @@ -637,10 +4721,7 @@ " ncols=4,\n", " title=\"$B_z$ over time\",\n", ")" - ], - "execution_count": null, - "outputs": [], - "id": "43" + ] }, { "cell_type": "markdown", @@ -664,13 +4745,21 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3.final.0)", + "display_name": ".venv-1 (3.14.4)", "language": "python", "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.12.3" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.4" } }, "nbformat": 4, From 48a54fcd5d26181bcc29f48a76827db7bab16a6e Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 16:20:59 +0200 Subject: [PATCH 032/193] Updated examples --- doc/sections/userguide.rst | 4 + .../cyclone/pproc_cyclone.py | 9 +- .../itg_cylindre/pproc_drift_kinetic.py | 9 +- .../diocotron_instability/pproc_diocotron.py | 9 +- .../bump_on/pproc_bump_on.py | 4 +- .../pproc_strong_Landau_damping.py | 4 +- .../two_stream/pproc_two_stream.py | 10 +- .../pproc_weak_Landau_damping.py | 8 +- .../pproc_weibel_instability.py | 2 +- src/struphy/post_processing/output.py | 31 +- .../post_processing/tests/test_output.py | 26 ++ tutorials/tutorial_post_processing.ipynb | 269 ++++++------------ 12 files changed, 170 insertions(+), 215 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 8eef568c3..8ecfc44fd 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -514,6 +514,10 @@ Every product is an :class:`xarray.DataArray` with named dimensions (``t``, ``component``, ``e1``, ``e2``, ``e3``, ``v1``, ...), coordinates and units. Arrays are read from disk only when accessed. +Time is in Struphy units, in which the models' analytic results are written; seconds come +along as the coordinate ``t_seconds``. Pass ``time_units="physical"`` to +:func:`~struphy.open_output` to make ``t`` itself seconds. + In a separate process, for example a plotting script on a laptop after a cluster run, open the output folder instead. Nothing is allocated and no MPI is needed; ``out.sim`` is restored from the ``config.json`` that ``sim.run()`` writes to the folder; a diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 4a6264de2..a2a1aa30b 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -22,8 +22,7 @@ def main(path_out): run = open_output(path_out).process(physical=True) # growth rate of the electrostatic potential - run.plot.timeseries( - FIT_QUANTITY, + run[FIT_QUANTITY].struphy.plot.timeseries( fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", @@ -33,10 +32,10 @@ def main(path_out): run.plot.equilibrium() for name, component, plane in SWEEPS: - isel = None if component is None else {"component": component} - run.plot.viewer(name, x="e1", y="e2", isel=isel, coords="physical", plane=plane).show() + selection = {} if component is None else {"component": component} + run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection).show() - run.plot.orbits("kinetic_ions", max_markers=1000).show() + run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 9f1b60ebc..e6caafed5 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -22,8 +22,7 @@ def main(path_out): run = open_output(path_out).process(physical=True) # growth rate of the electrostatic potential - run.plot.timeseries( - FIT_QUANTITY, + run[FIT_QUANTITY].struphy.plot.timeseries( fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", @@ -33,10 +32,10 @@ def main(path_out): run.plot.equilibrium() for name, component, plane in SWEEPS: - isel = None if component is None else {"component": component} - run.plot.viewer(name, x="e1", y="e2", isel=isel, coords="physical", plane=plane).show() + selection = {} if component is None else {"component": component} + run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection).show() - run.plot.orbits("kinetic_ions", max_markers=1000).show() + run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 7efcb439f..1c7a7a55a 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -27,8 +27,9 @@ def main(paths): run = runs[0] # growth rate of the electrostatic energy, one curve per run - plot = run.plot.timeseries( - *(each[FIT_QUANTITY] for each in runs), + first, *rest = (each[FIT_QUANTITY] for each in runs) + plot = first.struphy.plot.timeseries( + *rest, fit=FIT_WINDOW, title=f"Evolution of {FIT_QUANTITY}", ).show() @@ -43,9 +44,9 @@ def main(paths): run.plot.equilibrium() for name in SWEEPS: - run.plot.viewer(name, x="e1", y="e2", coords="physical", plane="XY").show() + run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() - run.plot.orbits("kinetic_ions", max_markers=1000).show() + run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 71f763d67..a9fbefc48 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -12,10 +12,10 @@ def main(): plt.show() # electric field energy - run.plot.timeseries("electric_energy", title="Electric energy").show() + run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index 72edc147c..afe95b38f 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -5,10 +5,10 @@ def main(): run = params.sim.output # electric field energy - run.plot.timeseries("electric_energy", title="Electric energy").show() + run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.plot.panels("kinetic_ions/e1_v1_density/f_binned", x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index b359e52f8..97a5ece94 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -9,16 +9,16 @@ def main(): # electric field growth against the analytical rate (0.2845 in units of m/c) energy = run.scalars.electric_energy - analytical = energy.copy(data=10 ** (0.2845 / run.sim.model.units.t * energy.t - 5.3)) + analytical = energy.copy(data=10 ** (0.2845 * energy.t - 5.3)) # t is in Struphy units analytical.attrs["label"] = "analytical" - run.plot.timeseries(energy, analytical, title="Electric energy").show() + energy.struphy.plot.timeseries(analytical, title="Electric energy").show() # phase space evolution - f = "kinetic_ions/e1_v1_density/f_binned" - run.plot.panels(f, x="e1", y="v1", nrows=3, ncols=4).show() + f = run.kinetic_ions.e1_v1_density.f_binned + f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4).show() # interactive alternative to dumping a frame sequence - run.plot.viewer(f, x="e1", y="v1").show() + f.struphy.plot.viewer(x="e1", y="v1").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 382b55963..90d029337 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -18,13 +18,15 @@ def main(): # electric field energy against the analytical damping energy = run.scalars.electric_energy.copy() energy.attrs["label"] = "numerical" - analytical = energy.copy(data=E_exact(energy.t.values / run.sim.model.units.t)) + analytical = energy.copy(data=E_exact(energy.t.values)) # t is in Struphy units analytical.attrs["label"] = "analytical" - run.plot.timeseries(energy, analytical, title="Electric energy").show() + energy.struphy.plot.timeseries(analytical, title="Electric energy").show() # full f and delta f in the e1-v1 plane at four times for quantity, title in (("f_binned", "full-$f$"), ("delta_f_binned", r"$\delta f$")): - run.plot.panels(f"kinetic_ions/e1_v1_density/{quantity}", x="e1", y="v1", nrows=1, ncols=4, title=title).show() + getattr(run.kinetic_ions.e1_v1_density, quantity).struphy.plot.panels( + x="e1", y="v1", nrows=1, ncols=4, title=title + ).show() if __name__ == "__main__": diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index d9b322f04..87502013d 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -93,7 +93,7 @@ def field_energy(field): distributions = run.distributions.kinetic_ions for bin_name, x, y in (("e1_v1_density", "e1", "v1"), ("v1_v2_density", "v1", "v2")): for quantity in ("f_binned", "delta_f_binned"): - run.plot.panels(f"kinetic_ions/{bin_name}/{quantity}", x=x, y=y, nrows=5, ncols=4).show() + getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels(x=x, y=y, nrows=5, ncols=4).show() # ------------------ # EM field at selected times diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 0271325a6..6f9912551 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -125,11 +125,12 @@ class Output: sim: The simulation that wrote ``path_out``, if it is at hand. time_units: - ``"physical"`` converts every time coordinate to seconds. ``"normalized"`` - consistently leaves every product in Struphy time units. + ``"normalized"`` (the default) keeps Struphy time units, in which the analytic + results of the models are expressed; every product then also carries seconds as the + coordinate ``t_seconds``. ``"physical"`` makes ``t`` itself seconds. """ - def __init__(self, path_out, *, sim=None, time_units: str = "physical"): + def __init__(self, path_out, *, sim=None, time_units: str = "normalized"): if time_units not in {"physical", "normalized"}: raise ValueError("time_units must be 'physical' or 'normalized'") self.path_out = Path(path_out).resolve() @@ -147,6 +148,7 @@ def with_time_units(self, time_units: str) -> "Output": def _reset(self): self._time = self._grids_log = self._grids_phy = self._scalars = self._products = self._label = None self._species = None + self._seconds = None def __getitem__(self, name: str) -> xr.DataArray: """Any product by name: a scalar (``"en_tot"``), a field (``"em_fields/phi_log"``), a binned @@ -168,8 +170,22 @@ def __getitem__(self, name: str) -> xr.DataArray: def _stamp(self, array: xr.DataArray) -> xr.DataArray: array.attrs.update(run=self.label, run_name=self.path_out.name) + if self.time_units == "normalized" and "t" in array.dims and self.seconds_per_time is not None: + seconds = np.asarray(array.coords["t"]) * self.seconds_per_time + array = array.assign_coords(t_seconds=("t", seconds)) + array.coords["t_seconds"].attrs.update(long_name="$t$", units="s") return array + @property + def seconds_per_time(self) -> float | None: + """One Struphy time unit in seconds; None when the configuration is missing.""" + if self._seconds is None: + try: + self._seconds = float(self.sim.model.units.t) + except FileNotFoundError: + self._seconds = False + return self._seconds or None + @property def path_pproc(self) -> Path: return self.path_out / "post_processing" @@ -307,6 +323,9 @@ def __getattr__(self, name: str) -> ProductNamespace: """ if name.startswith("_"): raise AttributeError(name) + attribute = getattr(type(self), name, None) + if isinstance(attribute, property): + attribute.fget(self) # the property raised AttributeError itself; show its own error # the raw output names the species, so an unknown name never starts post-processing if name not in self._raw_species(): raise AttributeError(f"{name!r}; available species: {tuple(sorted(self._raw_species()))}") @@ -403,6 +422,7 @@ def t_grid(self): @property def time_scale(self) -> float: + """Factor from Struphy time units to :attr:`time_units`.""" return float(self.sim.model.units.t) if self.time_units == "physical" else 1.0 @property @@ -610,7 +630,7 @@ def _load_orbits(self, directory: Path): return wrap_orbits(values, self.time[: len(paths)], time_unit=self.time_unit) -def open_output(path_out, *, time_units: str = "physical") -> Output: +def open_output(path_out, *, time_units: str = "normalized") -> Output: """Open the output folder of a finished simulation. Nothing is allocated and no MPI is needed; products are read on first access. @@ -619,8 +639,7 @@ def open_output(path_out, *, time_units: str = "physical") -> Output: ---------- path_out: The simulation output folder (``sim.env.path_out`` of the run). - time_units: - ``"physical"`` (seconds) or ``"normalized"`` time coordinates. + ``"normalized"`` (the default) or ``"physical"`` (seconds) time coordinates. """ path = Path(path_out) if not (path / "data").is_dir(): diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 9932bb582..b460f4c24 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -65,10 +65,19 @@ def write_manifest(root, **options): json.dump(manifest, stream) +class FakeUnits: + t = 2.0 + + +class FakeModel: + units = FakeUnits() + + class FakeSim: """Just enough of a Simulation for Output: no configuration, a single rank.""" time_opts = grid = derham_opts = domain = None + model = FakeModel() rank, comm_size = 0, 1 def __init__(self): @@ -262,3 +271,20 @@ def test_info_lists_products_without_loading(run): assert "out.kinetic_ions.orbits" in text assert "out.em_fields.E" in text assert run.field_catalog._cache == {}, "listing must not load arrays" + + +def test_normalized_time_carries_seconds_as_a_coordinate(run): + energy = run.scalars.en_tot + assert "units" not in energy.t.attrs, "normalized time has no unit" + np.testing.assert_allclose(energy.t_seconds, energy.t * FakeUnits.t) + assert energy.t_seconds.attrs["units"] == "s" + + seconds = Output(run.path_out, sim=FakeSim(), time_units="physical").scalars.en_tot + np.testing.assert_allclose(seconds.t, energy.t * FakeUnits.t) + assert "t_seconds" not in seconds.coords + + +def test_a_failing_property_reports_its_own_error(tmp_path): + run = Output(write_tree(str(tmp_path))) # no sim, no config.json + with pytest.raises(FileNotFoundError, match="config.json"): + run.sim diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 1c745b612..a189c3e27 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -163,7 +163,7 @@ "\n", "To choose options, call `out.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", "\n", - "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Arrays are loaded only when accessed. The simulation that produced them is `out.sim`." + "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `struphy.open_output(path, time_units=\"physical\")` makes `t` itself seconds. Arrays are loaded only when accessed. The simulation that produced them is `out.sim`." ] }, { @@ -256,104 +256,48 @@ }, { "cell_type": "code", - "execution_count": 5, - "id": "8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "scalars: ('electric_energy', 'kinetic_energy', 'total_energy')\n", - "field species: ('em_fields',)\n", - "distribution species: ('kinetic_ions',)\n", - "particle species: ('kinetic_ions',)\n", - "all field products: ('em_fields/e_field_log', 'em_fields/e_field_phy', 'em_fields/phi_log', 'em_fields/phi_phy')\n", - " Size: 336kB\n", - "memmap([[[0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " ...,\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.]],\n", - "\n", - " [[0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " ...,\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.]],\n", - "\n", - " [[0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " ...,\n", - "...\n", - " ...,\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.]],\n", - "\n", - " [[0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " ...,\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.]],\n", - "\n", - " [[0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " ...,\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.]]], shape=(41, 32, 32))\n", - "Coordinates:\n", - " * t (t) float64 328B 0.0 1.668e-10 3.336e-10 ... 6.504e-09 6.671e-09\n", - " * e1 (e1) float64 256B 0.01562 0.04688 0.07812 ... 0.9219 0.9531 0.9844\n", - " * v1 (v1) float64 256B -4.844 -4.531 -4.219 -3.906 ... 4.219 4.531 4.844\n", - "Attributes:\n", - " label: $f$\n", - " units: \n", - " run: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)\n", - " run_name: vlasov_ampere_demo\n", - "dimensions: ('t', 'e1', 'v1')\n", - "time coordinate: Size: 328B\n", - "array([0.000000e+00, 1.667820e-10, 3.335641e-10, 5.003461e-10, 6.671282e-10,\n", - " 8.339102e-10, 1.000692e-09, 1.167474e-09, 1.334256e-09, 1.501038e-09,\n", - " 1.667820e-09, 1.834603e-09, 2.001385e-09, 2.168167e-09, 2.334949e-09,\n", - " 2.501731e-09, 2.668513e-09, 2.835295e-09, 3.002077e-09, 3.168859e-09,\n", - " 3.335641e-09, 3.502423e-09, 3.669205e-09, 3.835987e-09, 4.002769e-09,\n", - " 4.169551e-09, 4.336333e-09, 4.503115e-09, 4.669897e-09, 4.836679e-09,\n", - " 5.003461e-09, 5.170243e-09, 5.337026e-09, 5.503808e-09, 5.670590e-09,\n", - " 5.837372e-09, 6.004154e-09, 6.170936e-09, 6.337718e-09, 6.504500e-09,\n", - " 6.671282e-09])\n", - "Coordinates:\n", - " * t (t) float64 328B 0.0 1.668e-10 3.336e-10 ... 6.504e-09 6.671e-09\n", - "Attributes:\n", - " units: s\n" - ] - } - ], "source": [ - "print(\"scalars:\", tuple(out.scalars.data_vars))\n", - "print(\"field species:\", tuple(out.fields))\n", - "print(\"distribution species:\", tuple(out.distributions))\n", - "print(\"particle species:\", tuple(out.orbits))\n", - "print(\"all field products:\", tuple(out.field_catalog))\n", + "print(out.info())\n", "\n", "phase_space = out.kinetic_ions.e1_v1_density.f_binned\n", - "print(phase_space)\n", - "print(\"dimensions:\", phase_space.dims)\n", - "print(\"time coordinate:\", phase_space.t)" - ] + "print(phase_space)" + ], + "execution_count": null, + "outputs": [], + "id": "8" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Products are xarray arrays\n", + "\n", + "A product is an `xarray.DataArray`, so xarray's own plotting already draws it, with the labels and units Struphy stored:" + ], + "id": "9" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" + ], + "execution_count": null, + "outputs": [], + "id": "10" }, { "cell_type": "markdown", - "id": "9", + "metadata": {}, + "source": [ + "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." + ], + "id": "11" + }, + { + "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ "## Scalar overview and time series\n", @@ -366,7 +310,7 @@ { "cell_type": "code", "execution_count": 6, - "id": "10", + "id": "13", "metadata": {}, "outputs": [ { @@ -387,7 +331,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "14", "metadata": {}, "outputs": [ { @@ -410,21 +354,23 @@ } ], "source": [ - "t_fit = 2.0 * out.sim.model.units.t # in seconds, like every time coordinate of this run\n", + "t_fit = 2.0 # Struphy time units, like every time coordinate of this run\n", "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", " fit=(0.0, t_fit),\n", " #fit_amplitude=True,\n", " title=\"Electric-field energy\",\n", ")\n", - "print(\"growth rate:\", energy_plot.fit_results[0].rate)\n", + "print(\"growth rate of the energy:\", energy_plot.fit_results[0].rate)\n", "\n", - "# the same fit without a figure\n", - "print(\"growth rate:\", out.scalars.electric_energy.struphy.analysis.growth_rate(window=(0.0, t_fit), amplitude=True).rate)" + "# the same fit without drawing; amplitude=True fits the amplitude of a quadratic quantity,\n", + "# so the rate comes out half as large for an energy\n", + "rate = out.scalars.electric_energy.struphy.analysis.growth_rate(window=(0.0, t_fit), amplitude=True).rate\n", + "print(\"growth rate of the amplitude:\", rate)" ] }, { "cell_type": "markdown", - "id": "12", + "id": "15", "metadata": {}, "source": [ "## Two-dimensional data\n", @@ -435,7 +381,7 @@ { "cell_type": "code", "execution_count": 8, - "id": "13", + "id": "16", "metadata": {}, "outputs": [ { @@ -461,7 +407,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "17", "metadata": {}, "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." @@ -470,7 +416,7 @@ { "cell_type": "code", "execution_count": 9, - "id": "15", + "id": "18", "metadata": {}, "outputs": [ { @@ -496,7 +442,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "19", "metadata": {}, "source": [ "## Interactive plots\n", @@ -507,7 +453,7 @@ { "cell_type": "code", "execution_count": 10, - "id": "17", + "id": "20", "metadata": {}, "outputs": [ { @@ -528,7 +474,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "21", "metadata": {}, "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." @@ -537,7 +483,7 @@ { "cell_type": "code", "execution_count": 11, - "id": "19", + "id": "22", "metadata": {}, "outputs": [ { @@ -557,7 +503,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "23", "metadata": {}, "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." @@ -566,7 +512,7 @@ { "cell_type": "code", "execution_count": 12, - "id": "21", + "id": "24", "metadata": {}, "outputs": [ { @@ -4204,7 +4150,7 @@ { "cell_type": "code", "execution_count": 13, - "id": "22", + "id": "25", "metadata": {}, "outputs": [ { @@ -4216,13 +4162,13 @@ } ], "source": [ - "frames = phase_space.struphy.plot.frames(\"frames\", x=\"e1\", y=\"v1\", step=10)\n", + "frames = phase_space.struphy.plot.frames(os.path.join(demo_root, \"frames\"), x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" ] }, { "cell_type": "markdown", - "id": "23", + "id": "26", "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." @@ -4231,7 +4177,7 @@ { "cell_type": "code", "execution_count": 14, - "id": "24", + "id": "27", "metadata": {}, "outputs": [ { @@ -4251,7 +4197,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "28", "metadata": {}, "source": [ "## Derived quantities\n", @@ -4262,7 +4208,7 @@ { "cell_type": "code", "execution_count": 15, - "id": "26", + "id": "29", "metadata": {}, "outputs": [ { @@ -4294,7 +4240,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "30", "metadata": {}, "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." @@ -4303,7 +4249,7 @@ { "cell_type": "code", "execution_count": 16, - "id": "28", + "id": "31", "metadata": {}, "outputs": [ { @@ -4333,8 +4279,7 @@ } ], "source": [ - "normalized = out.with_time_units(\"normalized\") # a spectrum needs normalized time\n", - "omega, kvec, spectrum, _ = normalized.em_fields.e_field_log.struphy.analysis.dispersion(\n", + "omega, kvec, spectrum, _ = out.em_fields.e_field_log.struphy.analysis.dispersion(\n", " slice_at=(None, 0, 0),\n", " do_plot=True,\n", ")\n", @@ -4343,7 +4288,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "32", "metadata": {}, "source": [ "## Save standard output\n", @@ -4354,7 +4299,7 @@ { "cell_type": "code", "execution_count": 17, - "id": "30", + "id": "33", "metadata": {}, "outputs": [ { @@ -4379,7 +4324,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "34", "metadata": {}, "source": [ "## Comparing runs\n", @@ -4390,7 +4335,7 @@ { "cell_type": "code", "execution_count": 18, - "id": "32", + "id": "35", "metadata": {}, "outputs": [ { @@ -4442,7 +4387,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "36", "metadata": {}, "source": [ "## Other models\n", @@ -4452,7 +4397,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "37", "metadata": {}, "source": [ "### SPH densities\n", @@ -4463,7 +4408,7 @@ { "cell_type": "code", "execution_count": 19, - "id": "35", + "id": "38", "metadata": {}, "outputs": [ { @@ -4568,7 +4513,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "39", "metadata": {}, "source": [ "For a one-dimensional run, the clearest picture is a space-time map: the sweep dimension `t` may be used as a display axis." @@ -4576,34 +4521,18 @@ }, { "cell_type": "code", - "execution_count": 20, - "id": "37", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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29mcamtr5ZJLLpGtttrK7P8PPvggp/ybb765PPLII7K8a+5zCQDQcpU19woAKF3xeFy23HJLWbx4sVx++eWy+uqry+zZs+Xll18209ixY+vm/e6772TFFVeUG2+80eR766235K9//atMmjRJbr/99rp5KioqGl3WV199Jb/88kvO6/jrr7/Kt99+K81lypQpMnfu3LrXw4YNkyOPPNJMxXLXXXeZgEADox49esjKK6/c6HyZ1u3rr7+W33//XZZ3zX0uAQBaLoIqABm9++678tlnn8n48eNl++23N+9ttNFGphaqurp6mfk7depkakqU1lbNnDnTBFRXX321dOjQoVXu6ddee026deuWFmT99ttvRV2HTz75xNQibrvtttb5mmPdAABYHtD8D0BGbdu2NX9nzJixTFplZaVzz22wwQam+d/3339fkL2sTQ6vuOIK2XTTTU2TtSuvvNI0fWvMo48+KnvssYdZBw0C//vf/6ala/7777/flLHjjjvK1ltv3Wh5WuN2wAEHyNChQ005jz32WFq61sY9++yzdU0Btcbn5ptvrmsK+fnnn8sZZ5whRx999DLreN9998lmm22WcRtUIpGQ2267TbbbbjvTRPKggw6SDz/8sC5d10uXr+upy9PaqMZkWrcUDZJd+yKb/doY1z50bWMymTTrO27cuLR8d955p/zf//1fXsc1l3OpvhtuuMGsX8qCBQtknXXWkWOPPbbuPa1x1fX99NNPzWudP7XPdX1OPvlk+fHHH+vmP/XUU+W4445bZlljxoyRTTbZJG298tn/AIAiCAAgA9/3g3333TfwPC/Yeuutg/PPPz944YUXgqqqqmXmXWuttYIDDzww7b2bbrop0NvM7NmzM86TUlFREVx44YXWY3HYYYcFffv2DR599NHgv//9b3DQQQcFsVgsWHXVVdPmO+uss0x51113XfDee+8FV199dVBZWRm88847dfN06tTJzKPbpPM89thjQbdu3YLzzjuvbp4vvvjClH/llVcGEyZMCJ5//vng4IMPDp566qm6eVZYYYXg2muvNf+eMmWKKePMM880eXVaunRp8OKLLwaRSCSYNm1a2nquscYawQknnGDd5mOOOSbo3r178MADD5j11/l1nXSd1ddffx3ssssuwXbbbWeWp+vQmEzrlu2+yHa/NpTNPnRtYzweN+fR2LFj08q+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mY4//ngCKgAAWhlqqgCgiWnHDNqtuDYT1J7wAABA60JQBQAAAAAh0PsfAAAAAIRAUAUAAAAAIRBUAQAAAEAI9P5XT3V1tXzxxRfSo0ePjOODAAAAoHQ7BtIx5NZee+1Gx7trbjoWno5bV0g65p0Oqo7mVVKRwyeffCL33XeffPbZZzJixAgzUny2NM/o0aNlxowZZqT5U089VYYMGZLT8jWgGjp0aB5rDgAAgFIxYcIE2XjjjaXUAqpVV+kmf8zzC1quDnauA4cTWDWvkgmqNAh655135Mgjj5S//e1vMnz48JwunG222UZOOOEEOf/88+Xpp582A2y+++67st5662VdjtZQqT6nnyplnfIcpd4L7OmBJyXJtd5NqVT3SWs+Juzz1qU5r99mtHxudTit9m4b9p5WyidTqR60oBm3y3LPSyxYKHNG3V73na6UaA2VBlQfvLSi9OkZLUiZc35Nyqa7zjJlE1Q1r5IJqi699FITaatTTjklp7xnn322bL311nLrrbea1zvssIOpdTrvvPPk5ZdfzrqcVJM/DajK8q1GJajKHV/w8zuXwmCfty7La1BVql82S1irPVUIqpphn5dmUJVSyo9x9OoZkb59CxNU+SX9i8DypWQ6qkgFVLn6448/5O2335b99tsv7X19PX78+IK3WwUAAACA+ko3jM/SlClTJAgCWX311dPe19fJZFKmTp2asQngwoULzZQyZ86cJl9fAAAALL+SgW+mQpWF0tDig6pUUNShQ4e09zt2/POZqAULFmTMO2rUKLn88subeA0BAACA/99kr1DN9mj+VzpKpvlfvmKxWF0XmvXF4/G09MacccYZMmvWrLpJO7wAAAAAgOWqpmqFFVZotOle6nW/fv0y5tXarFSNFgAAANDUtJ5K/ytUWSgNLb6mSp+d0m4z33vvvbT3tTt1DahWXHHFZls3AAAAoL5kEBR0QmlocUHVxRdfLPvvv3/d60gkIieddJL84x//kOnTp5v3vv32W3nwwQdl5MiR4nn0uQsAAABgOWj+99RTT5mOI1LuvvtueeGFF6RPnz4yduzYuvcnT54sEydOTMt70UUXyYwZM2TttdeWwYMHm3kOOeQQOeuss4q6DQAAAIANHVW0TiUTVG222WZy3XXXLfN+ZWVl2uurrrpqmbGndIC3MWPGmPza4cRKK60kPXv2zHtdyud5EkvmV8MVlHLNmHXVQq63F2JcxpCLtpYdCTdeZOCqy7XkDyKOKnnXukUcK2cp3zkOpmPdnPltAy+G2GfOssOcZ66yHeU3+aXdUkdlbcIReJu1VUvIZXu+7ebgyGzLGza/34TbpWyXr2vZfv7LdpUdetlhzocgZNlBE56nQUsdfNtyLixscY2w0EqUTFClNVI6uWhNVCa9e/c2EwAAAFCKNIZPFqxLdZSKkgmqAAAAgNaO5n+tE3WkAAAAABACNVUAAABAkRSyK3S6VC8d1FQBAAAAQAjUVAEAAABF4hewgwk6qigdBFUAAABAETuqKFzvfy10OI5WiOZ/AAAAABACNVUAAABAkSRNBxOFKwulgaCqEeXzRcoTTbTHvSYcndxrurzOdQuRP3DUl7rSbfWtQdSe1XekB2X5r1sQte8Uvyxotn0q0SBUehDJnO65ynasmxfJ3ELcsyz3z/T8yzbpln3qXLYX7hMy7OVvE2bNgrA3pqD5lm3rXCvw7SeLq2OuwPfyT3fkdaUHSXu6Z/mWFTi+gXmOsl1sy3aVbcvrLNvxYImr7EiYZbvyuh56ca17EKJs18dM0IQ3j7C3jjzze4vDLRfIF0EVAAAAUCR0VNE6EVQBAAAAReKLJ8kCtU/QslAa6KgCAAAAAEKgpgoAAAAoEj/4cypUWSgNBFUAAABAkSQL2PyvUOUgPJr/AQAAAEAI1FQBAAAARUJNVetETRUAAAAAhEBNFQAAAFAkOri4H3Zg9XploTQQVDWicp4vFTUZhil3XAPOa8TzQuQNu2xL3ki4soOofQZb+UHUXrbvSA/K8s8rsXA3Kz+W/z51jWQf6nyI2gsPYr696Jg9fySaOX+kzF521JUeyZxeZlmuWbYlbzb5o5aDYluvbNIjjgMekab7ZHSNYxLmwz0I+cXAtmxX2UnfkW65CF15E0n7zSOZtF/gvqV8Z96E4+aRdNxvLfk9z5FXwt23bJ9xrsK9pD09YkmPxB1lJxxlJ/Jft0jYsl35k0He+8yz35bEa86u4xznoo3t1hBZUvpRBs3/Wiea/wEAAABACNRUAQAAAEWSlIiZClUWSgNHAgAAAABCoKYKAAAAKBJ9XrRwHVUw+G+pIKgCAAAAioSOKlonmv8BAAAAQAjUVAEAAABFosM92IZ8yLUslAaOBAAAAACEQE0VAAAAUCQ6ILtfoHoN1+DuKB6CqkZUzEtKZVWGIc4d566zExbLCOLOvFH7DK4a4CCSOb8fzT+vSS+zj2Dul1mW7TgLbXlNesySWC6h+I5j4kXyS1POMd9d51okyPt4eDF7eiSWtKaXWdJjjryxMnt6ZVki83Kj9rwV0cx5Vbkjf3kkmVeainj2fVrmyu8+I/LmWrcwvVC5PtBdZScsN5+E46ZWm7TfuGotZccdeWuS9htTIpn/usUT9mUnyuzpybgj3bbLnTcmx73efhqL7VTz/PzzmvREfmkqEnekO/JHa21lB6GWHXXlr82c7ritiJcMQqXbbkuu49VcItWOg1kC9L6pnVUUqiyUBpr/AQAAAEAI1FQBAAAARUJHFa0TQRUAAABQ1GeqaP7X2tD8DwAAAABCoKYKAAAAKBLt+S9ZsN7/8i+npqZGZsyYId26dZPu3bvnlLeqqkp+/vln6dWrl7Rt2zbv+aZMmZIx7+DBg+v+PWfOHFmwYMEy86yyyipSXh6yV7ICIagCAAAAlhNff/213HXXXfLEE0/IL7/8IpdccolcdtllWee/8MIL5bbbbjOB2G+//SYjRoyQG264QbwGPVy75ksmk7LXXnstU/7MmTMlGo3KwoULJRL5M2g8//zzzfr269cvbd7nn39eVlttNSkFBFUAAADActJRxSuvvCKDBg2Sr776Svr27ZtT3jvuuENuueUW+e9//ysbbbSRTJo0Sbbcckvp3bu3nHnmmTnNp4FTw5qqRYsWmXn222+/uoAqRfO//PLLUqp4pgoAAAAo8uC/hZly7/Di9NNPl5EjR0rXrl1zzjtq1Cg5/PDDTaCk1l13XTn66KNNABUEQc7zNfT444/L0qVLzbwtDUEVAAAAAKvZs2fL9OnTZeutt057X1//+OOPMm3atJzma8wDDzwgAwcOXCav0mBMy547d25JHima/zWifH51/g+9NWhP2lBgS4868kYc6WX2GDmwlO/H7Hn9Mvuy/XJX/sxpyQrHryyWXzT+lDl/4PoBx7Vox8DsXtSWWcJxDVdvWbYXtef1or41vSyWtKbHLOmV5XFr3jYxe3q7WG3GtLZl9rxtyzLnVeUR+wGtsKTb0lQs4tinnn2fRiVz/ljEnjfiONminn3dwjRDiQfRUOk1lpuDLU3VOtJrkpnTq5Mxa95qS95s0qsSmcuvituXXRu3H88aa6pIYDncvu+Fu+84PuOs91TX/di1aMt2OS5PidhvHRJ17NRobZBX2p/p9rIjcfv1GbGcD5FEEKpsL+n4rLDk93x72ZZbWniWW5Zf47pCmp8feJJ0fkHJvqxURw71dezY0UyFNGvWLPN3hRVWSHs/1YRw1qxZsuqqq2Y9X0MabL3zzjty1VVXNbr8V1991dR8LVmyxHR6oc9snXzyycs8y9VcqKkCAAAAWrChQ4fKiiuuWDdp87tCq66uNn8bVjxUVFSkpWc7X2O1VPoc1RFHHLFMmtZcaQcb2pOgdmChgddpp51mOr4oFdRUAQAAAEWSLGCX6qlyJkyYIH369Kl7v9C1VKpDhw7m7+LFi9PeT73u+L9lZjtfw6Z9Dz74oOy8887L1HCp+s9Yac3UcccdZzqtuPvuu+Xcc8+VUkBNFQAAAFAkfhAp6KQ0oNLuxlNTUwRV2nW5BjRTp05Nez/1erX/dW2e7Xz1vfnmm2bMrFw6qFh55ZXl119/lVJBUAUAAAAgjT6nVT8w6tSpk+nW/Nlnn02b77nnnjPND3v06JHTfA2b/ul4VnvssccyaTqeld/gGT59rYHYmmuuWTJHjeZ/AAAAQAtu/peLBQsW1HVsoc3udGBeHS+qrKzM9LyXcvbZZ8v48ePNc0wp1157rWy77bZy6aWXyt577y0vvPCCvPjii6YTifqynU9pxxNPPvmkHHvssY12FDdv3jzZaaedTKcUQ4YMMet/++23yxdffGHKLBUEVQAAAECRaJ1LwXr/yyPPuHHj5KKLLqprQqeBk05ag/T222+n9dbXsKneFltsIa+//rrcdNNN8swzz8hKK61kyhs2bFhe86m33nrLPEd1zDHHNLq+WoP18MMPy2233WaeoWrTpo2stdZapuOKxnoRbC4EVQAAAMByYr/99jOTS6ae9bRpn04uW2Y532677WYmm8GDB8tdd90lpYygCgAAACgSXyJmKlRZKA0cCQAAAAAIgZqqRkQWLpVIWVSaRMQSx0btMW5gy6ti9nUOLOl+uT2vKz3pGpW93LLurnbFrnRLchCx5w0ch9lzXSH2zQ7F2dzasyw8al+xSJm9FXY0ak+vLI9nTGtXXmvN2y5mT29fljm9fazxAQPr0qP2sts40tta0ttG7HkrIpn3iYp5SWt6uZfIO6/+5tlUXL+CJv/XnW8m1UHMml7jZ06vDuwX4NLknwNJZlLlL/vAc8riROY0tSRhL3tp0p4/Yrs+HQLHxZ9IOo6J7bMkYV8vx+G03m+dHLvEc5zGtsvAcYlIJPPlZUTj9pWL1mZOj9bkn9esW63jfmxJ9xL2vJG4Pd2LJ/NOd+WVhCO9QW9uy6Zb9pvlsz2aWCKlTu+brntnLmWhNBBUAQAAAEXii2emQpWF0kB4CwAAAAAhUFMFAAAAFIlfwOZ/WhZKA0cCAAAAAEKgpgoAAAAokqR4kixQvYaWhdJAUAUAAAAUiR94ZipUWSgNNP8DAAAAgBCoqQIAAACKRMf/K1TzP9dYgigejgQAAAAAhEBNFQAAAFAk2g16obpCp0v10kFQ1ZjFS0SiTfTgn2cpNxq1Z405DlcsZk8vz5weSdjL9vzAXnbgWHdbds9+Ywki9mPhW1bd861ZRYKQ6SGEfbbUdj/2rDtcTzX7jomVJa3p5dHM6RXRhDVv27K4I702Y1r7aOY01bGsyprevqzamt4hkjm9Q9Set9JzbFekxppe7mXebzHPfjyiIU9UW+9RrrFU4mK/9uOB/d5S7We+L1UH9nvaokilNX2pX5ExrcJrY80bc9w8Io5rzPbweMK379OE47Mg7khPWq5vP+K63zrOJcd2i+VccmV13a9t6Y5LRCKJkOmWyzuSsG9YJO64H9c6zjVLuldr3/BIrX3DPEe6xC3p8Xj+eVXCkW753hEEmfdJkFwsLaP3v8J8z6T3v9JB8z8AAAAACIGaKgAAAKBIaP7XOhFUAQAAAEXiF7DZnuspBxQPzf8AAAAAIARqqgAAAIAioflf60RNFQAAAACEQE0VAAAAUCQ6VIVruIpcykJpIKgCAAAAiiQQT/wCdVShZaE0lFx4+9tvv8mnn34qv//+e855586dK5MmTTJlAAAAAMByVVOVTCblxBNPlIcfflhWW201+e677+TII4+U0aNHS8QxAvyPP/4ohx12mHz00Uey8sory/fffy9bbLGFPPTQQ9KrV6+c18VfslT8SJ6Rv2fP50WjmRNtaZo34ThcCfvI6l7SMiq7b++UMwjsI8I7WfanH7Xvs0iZY7T6ZOb8ScdqeyHTQ3GcYs4a/UjmlfMsadmkRxzp0Ujm86XMkqbKIwlreptobca0dmU11rzty6qt6Z2iVdb0DpHM6R2j1XnnVZVe3JGeeb/EvObrNNcP7CdqrdjvW3HHiVwdiWVMW+JXhNqn85P2e6JN0vGbo2u7av3M+6U6ar+X10Tt6x2N+nlf365r38lxPoiteMdp7IVId14irs0O8VngXG/HB5GXsBcQqcl8b/Bq7NeAZ8lrxB3ptZnvx5Kw5w0S4ZYd2K5fy/eZILDfq0sBzf9ap5KpqbrmmmvkiSeeMDVNn332mXz88ccmKLr55pudeY8//niZPXu2zJgxQz7//HOZPn26TJ061QRpAAAAANDqgyrf9+WOO+6QY489VgYOHGjeGzx4sBx++OGmpsrl7bfflgMOOEC6detmXvfs2VP2339/eeutt5p83QEAAIBs6fNU2gqgIBPPVJWMkgiqtKnfr7/+aprs1aevZ82aJT/88IM1f58+fWTOnDlp7/3000/St2/fJllfAAAAIB9J8UwT48JMdFRRKkrimSp9Jko1DII0WEql9+/fP2P+W265RQ4++GBZffXVZejQofLee+/JCy+8IGPHjrUud+HChWZKaRiYAQAAAECLCKpq//cgZFlZ+uqUl5enpWey4YYbym677SbXXnutrLTSSubZqj333FPWXXdda75Ro0bJ5ZdfHnr9AQAAgGwE/2u6V6iyUBpKovlfx44dzd9FixalvZ+qRUqlN0YDrq222koWL15smvxpRxdas/Xzzz/LtttuKwlL7zNnnHGGaV6YmiZMmFCwbQIAAAAa8iVS0AmloSSOxKBBg8TzPPNsVX36OhqNmi7WM9GeAnW+kSNHStu2bc177du3l7/+9a/y1VdfyeTJkzPm1WCtX79+dVOquSEAAAAAtKigSnvt23zzzeXJJ59Me19fb7fddiZIStEASgcHTunRo4f5q12q16c1T/XTAQAAgOaWDLyCTigNJfFMlbrxxhtl2LBhctZZZ8nw4cPl6aeflg8//FDeeeedtPnOP/98mThxonluSulgv/vss4+ce+65Ul1dLWuuuaYZq+rSSy81nVf07t27mbYIAAAAwPKgZIKqzTbbzPTad9ttt5mASIOlDz74YJnOJrSHv2SDUbYfffRReeCBB2TcuHHy+OOPm0BKO6E47LDD8loXv6ZWfK/xXeNFHL8IeI7Kv2jmUcC9WBBqQHhtQumYwbJejvUus6d7iagjPfN2R5L2sn3XaPV+kPdI99KUv/B4YdODvA+nFwlCLTriWLYtvcyx011lxyz5Y176td9QpZf5GUrVLlJjTe8Yrc6Y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bM3/6KOPluwB5JkqAAAAoMhdqhdqytV+++1XF8Tkwvd9uffee+Woo44ygZLq3r27HHPMMTJmzBhJJBI5zacB3euvv562Lptuuqmss8468tprr0lLQ1AFAAAAFMmfwZBXoKl4h02fjdIap6FDh6a9r6/nz58v33zzTU7ztW/fXtZYY420eZLJpPz000/Sp0+fZZa/cOFCeeGFF2TcuHHy448/SqkhqAIAAABasDlz5sjs2bPrJg1AmmIZqmfPnmnvp17//PPPOc3XmFGjRpn0/ffff5m0KVOmyOjRo03TQu34Qp+9qqqqklJBRxUAAABAkQRSwN7/5M9yGtYKXXrppXLZZZdJIWktkoo0eA4wGv3z2bxUs75s52tIO6K44IILZOTIkbLDDjukpR166KFy55131nVyoc9ebbfddtKuXTsTaJUCgioAAACgBZswYUJak7mOHTsWfBldunSp6wK9Pm3Sp7p27ZrTfPW99957prfAffbZx/T811DDIEufvTrwwANl7NixJRNU0fwPAAAAKJKgwJPSgKpfv351U1MEVdojX3l5uUyePDntfX0djUZl0KBBOc2X8tlnn8nuu+9uAqd///vfy9RwZaLbuGTJEikVBFUAAABAkRSuk4rCNSNsjDax03GlUnQcKQ1+Hn74YdPD35/bEphAaOedd64L5LKdL/Wc1E477WRqnh5//PG0salSdDyr33//Pe29xYsXy/PPP2/GwCoVNP8DAAAAlhPaO582F1Qa9Hz55Zdm/CcNhvbcc8+6+e644w4ZP3582ns33HCDbLbZZqapnr7/4osvmsDovffeS1tGNvPNmzdPdtxxR/MMljble+qpp+rStImgBltKO6PQsnbddVczgLA2K9Qu22tra9PGxmpuBFUAAABAsdRvt1eIsnI0c+ZMeeaZZ8y/9Rkmpa87d+6cFkBpIKPdntc3cOBAmTRpktxzzz3yxhtvmC7Rtce+FVdcMef5NFjaYostzL9ffvnltPyrrrpqXVCl6/Xpp5/Kv/71L5k4caIJ/s4880zTeYX+u1R4gdbHlQg9yLrzZ8yYIaussoqccMIJpl1otl566SXTf71WE2r1YmPdMdpoF5R6sDfd6nyprOyU5x61J1traT17ZteI8YFjZHXbqOzOsqOOsh3huW9ZtzB5TXosv7Ss0svt6UlLeuDYrmS5/dILXOtWZslf/md1e0Yxe7rnSC8rT2YuOmYfzb48ljmvqizLnL/ckmaWHbGXXR61p5d5ft55I45PtrJI/suOeM13m/YdTUt8x00v4Uft6ZabT8K335hc6bWWZbvyxl3rnbSn11rS4wlHXkd6Im5PT9Za0hOOm32tPd2rtR/vSDxzesSZ175qkdr80lTUVXaIZUfjQbiyE678mdMdtxXxkkGodNttrclvS3l+Pa2pXiAfvHWtzJo1K6fvkMWQ+p454O4zJdYtz++ZDcR/XyAzTry5JLd3eVMyz1R9/fXXsu6665oqSa3e04fZ9PV3333nzKtVlxqtHn/88abf+mHDhpk2oBdeeGFR1h0AAADISiGfp2rCZ6rQQpv/aTWetpN85JFHzOtDDjlENt54Yzn33HPT2lg2RrtS1BoqbROaitI1yJo7d25R1h0AAADIthKuUO3ESqe9GUqipkpHfR43bpwcfPDBde95nid/+ctfzINtrtGStY3m4Ycfvky1Z48ePZpsnQEAAACgZGqqtKmf9vyhD7HVp6+1Zw9tArjOOus0mveHH34wk3bFeNddd5leRXr27Gm6ctSRlgEAAIBSUciu0JuyS3W0wJoq7VJRdeqU/tBe6nUqvTE///yz+XvRRReZ/uo1kNI+7vW5rGuuucZZQ6YPDaamOXPmFGBrAAAAgAxSz0IVakJJKImaqtRAX1pbVV/qdWMDgaWkRl3WLh+1qaA2G1QdOnSQyy67TI477riMzQC12eDll19esO0AAAAAsPwpiZqqPn36mL+//PJL2vup16n0xvTt29f81RqqVECltt9+e4nH4/LVV19lzHvGGWeYLihTU2ogNAAAAKApO6oo1ITSUBI1VYMGDTIDe2lQM3z48Lr39bU+H6XdpNuCqv79+8vixYvT3k+9btiksL6OHTuaCQAAAABadE2VNu876qij5N57763rBl2fbxozZowce+yxdU381M033ywnnXRSWn59/eSTT5oOK1QikTDdrOtozmuttVaRtwYAAADIICjwhJJQEjVV6qqrrjIDAGsQNHToUPnggw9k8803l0suuSRtvvfff18mTpyY9t5ZZ51l8q699tqmF8Dvv/9eotGoPP7441JeXl7kLQEAAAAaR+9/rVPJBFVt27aVl19+WT7//HOZOXOmXHfddWYw4IY0gGrYG6AGUA888IBMnTpVpkyZYpoEaoAVi8XyWpeaLlGRtk20ayydtLg6cAkijhmc+fNLyy7dvvAgmjnNt6SZvI5D4VvWzY+FLLssxPEMWw/s+PXJ8y1ZE67egOwr5/rhK+5nLj+ZsJcdj1tWXESqo5kPWjRqz1sWTTrS7fmjXuYtj0YceR3pEUvZJr0Zf270LSey77gxubrzDZPfndeaLEnLRZi0nMNm2bYbi7aISNrTk5b8ftKxbMc15EoX27q57g32S0gcp7H15uH8jHOlR/O/lzs2K9Sy/Zg9cyRhL9tznA+RZJDX50A26c7PmSD/C9B5rrjY8lt2Wc1Sx5cKoLUHVSk6HlWmMamU1kRlos39dAIAAABKFs32Wp2SeKYKAAAAAFqqkqupAgAAAFornqlqnQiqAAAAgGIpZK99NCMsGTT/AwAAAIAQqKkCAAAAika7L/QKWBZKATVVAAAAABACNVUAAABAsfBMVatEUAUAAAAUC0FVUf3nP/+R119/Pac8u+++u2y77bY55SGoakR1l4j47ZuoZWSIpq+uEd9DcY0mHwmZ38tvpPpslm3L7yy7KZcd9niFGek+6YXrLMix4bbkZNye14/alx63bJgXsef1HGW78kejviWzNatErAfEzbPk1+53wwizZs5lh+x5Ksy2BUHTLTfww223n7RcB65tdl2/ltNUebb8jrI953aHOBddWUPcb12r7bnu9Y5vQ37SUrbreDjSxZXfss+dt52Q6dbyHedCyFuilW3RteU82YJ0//3vf+Xhhx+Wnj17SjbmzJkj3bt3J6gCAAAASrumqkC/lNOlelZOOeUUOe+887KaN9v5GqKmCgAAACgSrWUPU9PesCzYbbnlltKmTZsmmz+FoAoAAABAqzR8+PAmnT+FoAoAAAAoFjqqaJV4mg8AAADAcuvbb7+Vl19+WaZPn553GQRVAAAAQLFoJxWFnBDafffdJ7vuuqs89thjeZdB8z8AAACgSLwCdjlPSFUYxx57rOywww6y2mqr5V0GQRUAAACA5dbAgQPNFAZBFQAAAFAsdFTRKhFUNaK2s0jQMb8d2mKbtnpNm9+6X8LkdeQPXE8Nhl12JAiRtwmPiaNZgZdwFJ50pHv57RNnXkd64Ggv4Tv3qT2/bbd4zvUu4cFCWuyNqQnHZwmadp9aT4egCctWviWv77rpSShBNP8VTzquX9u6e5ZtziZdXPmb81xrytuO81wMseymvCVaTuM432xh8dxzz8mrr76aMX3PPfeUHXfcUfLBqQcAAAAUSyE7mGilP5o1lZ9++kk+++yztPcWLFggkydPlt69e8tmm22Wd9kEVQAAAECx0Pyv2Zx44olmauijjz6SQw45RPbYY4+8y6ZLdQAAAADLrY033lg22WQTef311/Mug5oqAAAAoFioqSpJ7dq1k1mzZuWdn6AKAAAAKBaCqmZTXV1tpvri8bhMmDBBHn30UXniiSfyLpugCgAAAECrd9lll8n111/faNqRRx5pBgDOF0EVAAAAUCz0/tdsjj76aBk2bFjae+Xl5bLqqqvKSiutFKrsJg2qNBLs2LGjjBgxoikXAwAAAABWq6++upmaQuigauHChWZqzA8//CArrrhi2EUAAAAArYIOnFyo8eJLedz55U3ooOqaa66Rm2++WaLRZYdRTyQSctVVV0lLU9slEL9zUPyzt5QHcCvlq7aU91sz7fPQh8u379MwqWhl118zXr6cacUXRIK8T9Ug5CAugXm6P1MiZ0OGnVa6muiQJaKlvNGl0VFFVVWVPPXUU3LffffJJ598Iueee66cd955Wed/9tln5brrrpNp06bJgAED5Oyzz5b99tuvZObLxz//+U955JFH5Nhjj5WDDjqo+capuvLKK+t606g/nXXWWYUoHgAAAEABaAD10ksvyfnnn28CrIa94dm8/PLLsu+++8rhhx8ukyZNkuOOO84EIc8++2xJzJevJUuWyG+//Wb2R768IAhCxcpXX321dOrUSUaOHLlM2j333CPt27eXgw8+WFqC2bNnm+aKK152sZR17tz4TNRUlZ7W+gtlc9ZOtNZ92lJRU4UWotlOVe5ZGfaLLH81VfPny6zLrjTjDfXr109K8Xtmv8svkrIuGb5n5igxb77MvvSqnLZXv/p73p8HoLKy0gRZ2itetoPk9u/fX5588sm69/R7/pdffimff/55s8/XnELXVF144YWNBlTq+OOPbzEBFQAAANDapQKqXP3+++8yceJE2XXXXdPe19dffPGFzJkzp1nna250qQ4AAAAUiYY0Beuo4n9/GwYW2vu2ToX0/fffm78rr7xy2vv6fFMqvU+fPs02X65835elS5emvVdRUSGxWEyKWlN10003Sffu3c0BW2utteTQQw+VUaNGyRtvvCHz58/Pt1gAAACg9Y9TVahJRIYO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9hBuC86aA0EVAAAA0ILp81CzZ8+um/Q5psb89ttv5m+3bt3S3u/atWtaemM0EOvcubN53illyZIl5vmp1LNauczXkA6jNGvWLDn66KMbTdcaNc37zTffyIEHHiibbrqpGb+2VPBMFQAAAFAseXYwkbEsERk6dGja25deeql5filjtga1PNnU+nTo0EHuuece0zRPa5vWXHNNeemll8wwSDNnzqzrWCLb+Rpr+qfBnfYS2NhzYNrxRUrqObBTTz1VHnvsMSkFBFUAAABAkWh/dYXqsy5VzoQJE8xYrikdO3ZsdP4ePXqYv9oMr74//vgjLT0TfU5qs802M7VKv/76q9x///3y7bffyptvvin9+/fPeb6URYsWmS7ejznmGKmoWHas2PoBlerbt6/st99+abVhzY2gCgAAAGjBNKDq16+fc76BAweamqRJkyal1Qh99tln0qZNm2WCl8bocg4//PC619oF+rrrrrtMQJbtfEo7z9CxszI1/cv0DBZdqpe4lWK/SY/yZQcxy0YysD+mFpfMw9XHA3uMW+3b12mJX2HPH2TOvzj5Zw8wmSz1y63prvwL45nTI54vTSXp249HMmpPTyQyHy/lWfoyddak+47fqRz5bbvNSzryJuzpEVd6bea0aI19xaOWvK780bi97EjCkV5rP9c8S/5IwpXXkZ50pVvW3XdcI45krwkf5g1cY4RE7elBhmYgJi1mvz59R7otf9JVdoXj3uH4iEhar+8g1D51fMxYr1/Hx4z7XPKbsO9nx6nkRSz3hoh9xcqj9ptiRdR+02sfy3zjah+rsebtUFZtTe/oSG8fzVx+h6g9b4do5p7cVIUXt6a3i2RedszxQVPu+qAJIWnpEqB9zL5NrbX5X7Z08N2DDjrINLU766yzpG3btqaDivvuu8/ULqV6BFRPPPGEfP/993LuueemBV/6zFSqCd+HH35omt890qDGKNv5UnR91l9/fdOkryF9HktruTQ9RZsR6vrtvPPOUiqoqQIAAACKpYDjVOUTnF177bXy/vvvyyabbGKCktdee80EPzfeeGPafM8884yMHz8+LajSfFqbtPXWW5tOI3RQ3iuvvFL23XfftLzZzqemTZsm77zzjowePbrxTQwCOemkk0wAOGTIEDOAsA4qrEHWbbfdJqWCoAoAAABYTmjPfxMnTjS98U2fPl3OP/982WOP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AADActD876effpLvvvtOttlmm2Vqi3744QeZPn16xryzZs0yf1dcccW09/X1Z599JvPnz89pvvr0vaVLlxZ8nYuJoAoAAABowebMmSOzZ8+umxYuXNjofDNnzmw04EnVKKXSG7PaaquZvxoY1Zd6PWPGjJzmS9Gmgbo+nTt3llVXXVXuu+++gq1zMfFMFQAAAFBMBR4TbujQoWmvL730UrnsssuWma+6utr8LS8vT3u/oqIiLb0xAwYMkL/85S+mWV/Pnj1lzTXXlLFjx8rbb79t0mtqanKaT22yySbmmaiNNtpIEomE3HHHHXLMMcfIokWL5NRTTw29zsVETRUAAADQgpv/TZgwwTS7S01nnHFGo8tu3769+duwqV2qW/NUeiZjxoyR0047TW666SbT8cTUqVPl9ttvN2nazXmu840YMUI23nhj0916LBYzvQHuscceMnr06IKtc7FQU9WIeJCUeJDfiNy+46cHLTuTpCuvs2z7OtdK5v78k44RyJOOsn1Xfku674jtncvO81g1OVdD55Crbd1sV9klustC88L9jBREMxfgRRyZo45035Ee8TOnZb5tZMe17iG2y3ONE9LIOCPp5UdD5HVsl2XslMCS1txctzRnekHXppiFt0xhP4Nc+ZOWG5ctTcUDe3qt50i3fGeJeb79tmPJa9JdN+w8d6vre1hr1adPn7rmcDbafbkGMNpl+Q477FD3/rRp09Ka7mWitUUXXHCBmVK0Rqrz/5ru5TpfY1ZffXV57bXXCrbOxUJNFQAAALAc9P7XpUsX0+TuxRdfTHv/hRdekPXXX9+MJ1V/YF59VqvhYL71VVVVyYMPPmh64ovU+wEvm/m06/Vld00g77zzjgms8lnn5kRNFQAAALCc0G7Nd9ppJ7nxxhtl7733NsHJ008/bf7Wd/LJJ8v48ePl559/rntPa560wwgdI2ru3Lmm9qljx47LDMSbzXw6QPCee+5pmvwNGTJEFixYILfddptMnDhRnn322bzWuTlRUwUAAAAsB12qq+22205eeukleeWVV2TnnXeW5557zky77LJL2nz67FPfvn3T3jv//PNNMzwdc0o7kthiiy1MzVKHDh1ynk8HAtaOKXTZOiDwyJEjTbNB7SVw9913z2udmxM1VQAAAECxaCBUqEe/8ixHB+PVySbVsUR9nTp1klGjRjnL75TlfBts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0meudt99d9lss81M5xarrbZaVs+BFQvPVDVienUP+a2qbV471HfEqUnLyOoJ3z6qetwxlL1rVPa4pfxax7JrffupUpu0569OZs4fd+StSea/7HjCXnbCsWw/ad/ngW9J9zMf6z8z25PFkV0imQsIovbMjsMZatwLyyluREKsW+TPFgOZ08vtZXtJR/5kkHdezw/X7t2zJReo6X1eXKex6zyt17yk0fyWS8hxS5Mg4jiXovmX7TvO08BxDdnOYz/mWrYjPZZ/unu7Q96XwpwsjntmkMycnkzY79U14thpDr5l3W2fra7PP7U4WmFNr4xmvvGVW9JMuuOmWeFIj1lubGUR+00xar2phWP7LrWoeqksjyZMmCB9+vSpe90waEopK/vzfEwm049f6nXDmqP6KioqTHO9gw8+2DTR0+nrr7+WvffeW+6//34TDOUyX0Mnn3yy6eHvmWeekX79+qWl3XTTTaa5YIrWVmlPgaeffrpptlgKCKoAAACAFjz4rwZUDQORxvTt29f8/fnnn9PeT72uH5g1RmvEtPdAnX799VfTa6D22PfAAw/IyiuvnPN8KRdeeKH8/e9/N51W7Lbbbsuk1w+olD6HpbVW9957r5QKgioAAABgORinSgOaXr16yYcffij7779/3fsffPCBdO3aVVZffXVnGVqbtc4669S9fv75500nFA1rx7Kd7/rrr5frrrvO1GIdeOCBWW+L9jLYcDyr5sQzVQAAAMByQHv8O+GEE0wAkxoLSjuH+Oc//ynHHnusRKP/vzmrjh912GGHpeXXwKj+c1djxoyRcePGybXXXpvXfHfddZecf/75cvfdd8vhhx/e6DprOVrDpc9cpbz++uumiWEuQVhTo6YKAAAAWE7GqdKmdtosb+211zaD7n7yySdmvKgrrrgibb5PP/3UPOPUMCjbYIMNpH///jJ37lwzcLB2k77JJpvkPJ8OBDxy5Ejp3LmzPPvss2ZK0U4odADhVFO/d99914xxNXDgQNPrn/YEqIP/ag1XqSCoAgAAAJYT5eXl5vmmqVOnml75VlpppUab/elgwEceeWTaezr47nbbbScff/yx6ZBCA6dU5xe5zte9e/e0QKo+DbRSNP8999xjeg388ssvTZClQVfD56yaG0EVAAAAUCx+8OdUqLLypLU+OmWy3nrrNfp+27ZtZauttnKW39YxX7t2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4hwrPVyX7C+0LnRUAQAAAAAhUFMFAAAAACEQVAEAAABACARVAAAAABACQRUAAAAAhMDgvxnMnDlTxo0bJ8lkUoYNGyaDBg0Ks5+XCx9//LG89dZbMnjwYNltt92ae3VKno4KP2nSJDMS+mabbVbSgxU2N91Pej2qsrIyM/CjXpfdu3dv7lVrEebPny///Oc/TW9jxx13XHOvTknSe/0tt9yyzPt6rh100EHNsk4txeLFi831+fPPP8uGG24oQ4cObe5VKklz586VBx54oNG0bt26yVFHHVX0dWopPvroI/M5EI1GzTm2zjrrNPcqAcugpqoR//73v01goB8S7777rqy//vpy2223NTYrROTNN980N7jjjz9err76ann44YfZLxZffvml2V/Dhw83gdXjjz8ua6yxhlx22WXstwyqqqrMFzadpk+fbq7HlVde2ew7uI0cOVLOOeccc32icfF4XM4++2x555136s41nX7//Xd2meP+P3DgQLn11lvl66+/NucZgXvmc6z+uaXTjz/+aPYZ97LG+b4vBxxwgGy//fby3nvvyWuvvSabbLKJnHTSSVyXKD0B0syZMydo27ZtcP3119e998ADDwTRaDSYPHkye6sRH3zwQTBp0iTz77XWWis45JBD2E8WY8eODYYPHx4sWLCg7r0HH3ww0MvxnXfeYd9lacSIEeZaraqqYp9ZPPHEE0Hfvn2Dgw46KFhppZXYVxnoeaTX4NNPP80+ytLMmTODDh06BDfccEPa+++++y77MEsvvPCCOe8effRR9lkjHn/8cbN/3nvvvbr3nnnmGfPem2++yT5DSaGmqoGxY8dKbW2tjBgxou69Qw45xDSb+de//lXsmLdF0F+NqIrP3sYbbyxPPfVU2qDS++yzj/n7wQcfNMERap123nlnWbp0qUydOrW5V6Vk/fLLL3LiiSfKXXfdJZ06dWru1UErc/PNN5tma2eddVba+5tvvnmzrVNLM2bMGOnatavstddezb0qJWnKlClSXl5umsinbLvttuav1owCpYRnqhp5LmjVVVeVDh061L2nbXg1aNA0IKyVVlppmfdSHw6NpaFxej1qoMCzaJlpM6wddthB/u///k/+85//cCpl4e2335ZZs2bJCiusYIKD3r17s98yeOWVV0yzLN1fL7/8slRWVpof2XgGOTvz5s2T559/3lynFRUVnGeNWG+99cwP3V988YWsvfbadc9XqQ022IB9hpJCUNXAr7/+2ujD7/prnHZeARRaIpGQ008/3Xx509oXZHb//feb2hf9gE09j6YdfWBZ2jHF+++/z6+5OdAf0D7//HNTA/rII4+YB+OvueYaOfXUUznFGjFjxgzzebnpppuae5d2WKEBwvnnn88zolnQc6ympkaOPvpozq8M9NnjCy+80LTm2H///c1zaU8++aSMGjXKBPBAKSGoaoTneY2+FwTajBcoLH3gVgME/aW3fg0pGu89a86cOabzgOrqatP0b6eddmJXNaA/AGmgfvfdd0uPHj3YP1nQXiUnTpxofhlP0Y49TjvtNNNklyZtjXcioIG73r+0R7bUDx/HHHOM6QGWXgDdTf+0Iyyd0Di9z+v9Xnvn1B+99UdI/bd+Dui/9boFSgVnY5Y1Un/88YdJAwrpvPPOk/vuu08effRR2W677di5DtpLVv1eOg899FDzJZgvvOnuuece05zop59+kptuusm8p7UuCxcuNK/1XKPpTDr9clY/oFLaG6D2yqlNJznHlqW1VPq5mAqoUs8gH3vssWZ4DYKqzCZPnmyasY0ePZr7voVef/qsu+6v1A9E+mPakCFDpG/fvuZHD6BU0FFFA/qhqhesduGcojVUX331lay77rrFPj5oxbRZ0Y033mh+rdxvv/2ae3VaHH1OqH77evx/W2yxhRxxxBFpXTfrPU1/4dV/L1myhN2VhUgkYpoE6i/iWJbWsOj+abjPdNJaLGSm93394ePggw9mN1lo5016ntWvcdcu/HWiYyeUGoKqBvTLrQZRDz74YN17zz33nKl25uaHQrnjjjvkoosukr///e+mtgV2+pxLYw/JqzXXXJPd14A2vdIaqfqTPvfSpUsX8++tttqKfdZIL2P6fEt9Wous71GL3LjDDjvM/OD47bff1r2nHS9oELr11ltzjmWgP25ob8La45/2/IfMVl99dTO246JFi+re0xp4Ha9Q04BSQvO/BrT3Na090Cpl7ZFNf0nS5xL0wVvaPTdOe3567LHHzL+17bNW06eaHOkD3rFYrGnP4hZGB5U+5ZRTzPk0f/78un2l9NmNbbbZplnXrxRpUz8dXFqbYGlgoF/knn32WTn55JNlxx13bO7VQysJ3PVheL3++vTpI5999pm88MILpskpz+017sADDzQ/OmoAddRRR5mOKjQQPffcc9O6wMaynwEaGNBBhdsll1xifkDTz0b90VsDdv080O9q+twoUEo8HayquVeiVLtr1nb0+ouSdhnLL7uZff/992YcnExN3HSMCfx/2mThiSeeaHSXDBs2zPR2hGVpsD5+/HjTWYW2pdcvuqussgq7KkvaY9Y333wjF1xwAfssA20aqYHUDz/8YLpU13u/NjOCnXayox1WaEc7GmDxLJWd/iD04YcfylVXXWWaSsLdWcXTTz8t3333nWluusYaa5jm33RSgVJDUAUAAAAAIfATCQAAAACEQFAFAAAAACEQVAEAAABACARVAAAAABACQRUAAAAAhEBQBQAAAAAhEFQBAAAAQAhlYTIDAABg+ZJIJOSll16SV155xQzCfsYZZxR9HWbPni2PP/64+bveeuvJwQcfzIDAaFbUVAFAK7d06VKZP3++mWpqarLOV1VVVZevurq6SdcRQMvwzjvvyMorryx///vfZdy4cfLcc88VfR2eeuopWXPNNeWzzz6TlVZaSd5//33ZZZddir4eQH0EVQDQgmnAo78a2xx//PHSo0cPGTBggPzjH//IuuwrrrjC5OnWrZtcdNFFBVhbAC3diiuuKB999JG88MILJrgqtqlTp8qhhx4qt9xyizz44INy6qmnyl133SX33ntv0dcFqI+gCgBaqC+++EK6dOki33zzjXPenXfe2QRgI0eOzLr8a6+91uRZe+21Q64pgNZCa4Z69+6d1byLFy82wc7pp59ufpjRmqWwbr/9dunVq5ccffTRae/rD0BAcyKoAoAW2qTvjTfekA4dOkjfvn1DNdFLJpNmAoBC1iittdZapkape/fusnDhQtl0003lscceC1Wu3ve22mor+fjjj+W8884zwVpzNEEEGiKoAoAW6KCDDpIzzzzTBFLaBEd/pb3//vtz/nKywQYbSNu2baVr166y4447ypdfftlk6wxg+XHccceZjiMmTJggF154oalhuvHGG+W0004zPwrl66effjLND/fZZx9z71JHHHGEeQ00J3r/A4AWSH+Z3XDDDWW77bYzX1Ry5fu++RIyYsQI+eCDD8x77777ril3yJAhTbDGAJYXf/zxh7z11lty3XXXSbt27ereP+qoo+SUU04xHUtsv/328vbbb8u///1va1nRaFT+9re/pb333XffmebPa6yxhnm90047yTbbbCPPPPOM7LXXXk20VYAdQRUAtEDxeNzUKp199tl55V+yZIlpMrjJJptIeXm5eW/bbbc1EwCE8dtvv0kQBPLaa6/JtGnT0tJisZh5T4Mq7QRHu0O3iUTSG1Xp81yaLxVQqa233to0hdZaMYIqNBeCKgBogfRX2traWtN8Lx/6BUSbDx544IGy6667mi8l+muvPgMBAGFo4KM1TNo0uWHQpM0A9dkqpd2i65SLoUOHyuuvv572ngZw+kNTqjkg0Bx4pgoAWqBPPvnEBEarrbZa3mXcdNNNMmXKFNl9993NQ9/6ZeXkk08u6HoCWP507NhR9t13X5k1a5Ycc8wxcuKJJ9ZNGmj1798/1LNaP/zwg7z66qt17z300EPmRyb9YQhoLtRUAUAL9Omnn5pfgD3PC1WOdnBx7LHHmmnMmDGmm+Lrr7+eX3wBNGru3Lly8cUXm39//fXXZpw8DZbU5Zdfbro7Vzo48F/+8hdzj9lyyy1NM77PP//cBFRjx47Ne+9uvvnmZrgHbeanA/5qDZU2M9RnS/WHIaC5EFQBQAukzV1+//1380uw1ljppM1tsqVfhrT5n9ZMrbPOOqY3rueff97UfNGEBkAmFRUVdU36Gjbt07SUzp07y0svvWSe/dQfgTTtsssuk0GDBoXeudqV+gEHHCDvvfeeuV/p4L/9+vXjoKFZEVQBQAt01llnybfffisbbbSR1NTU1P0CnC19jkG7Nh49erRMmjRJKisrZYsttpBXXnmlSdcbQMtv2peqmcqG9ibaFD2KrrLKKmYCSgVBFQC0QPplYvz48VnPr010tLe/Nm3a1P2avPPOO5spk6qqKhOwMTAwAAB2dFQBAK2cjhOjY1Hpsw3/+Mc/ss53xRVXmDzaxFCDMQAA0Dgv0Ib5AAAAAIC8UFMFAAAAACEQVAEAAABACARVAAAAABACQRUAAAAAhEBQBQAAAAAhEFQBAAAAQAgEVQAAAAAQAkEVAAAAAIRAUAUAAAAAIRBUAQAAAEAIBFUAAAAAEAJBFQAAAABI/v4f1+ZEcVhGCSgAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "out_sph.euler_fluid.view_0.n_sph.struphy.plot.slice(\n", - " x=\"t\",\n", - " y=\"e1\",\n", - " e2=0,\n", - " e3=0,\n", - " title=\"SPH density of the sound wave\",\n", - ")" - ] + "density = out_sph.euler_fluid.view_0.n_sph.isel(e2=0, e3=0)\n", + "density.plot(x=\"t\", y=\"e1\")" + ], + "execution_count": null, + "outputs": [], + "id": "40" }, { "cell_type": "markdown", - "id": "38", + "id": "41", "metadata": {}, "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" @@ -4611,41 +4540,17 @@ }, { "cell_type": "code", - "execution_count": 21, - "id": "39", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "density = out_sph.euler_fluid.view_0.n_sph.isel(e2=0, e3=0)\n", "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "42" }, { "cell_type": "markdown", - "id": "40", + "id": "43", "metadata": {}, "source": [ "### Vector fields on a mapped domain\n", @@ -4656,7 +4561,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -4687,7 +4592,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "45", "metadata": {}, "outputs": [], "source": [ @@ -4706,7 +4611,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -4725,7 +4630,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "47", "metadata": {}, "source": [ "## Apply the workflow to another run\n", From 008f6ec0c3aeb31631ded748bcd66ca7f16038ee Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 16:41:40 +0200 Subject: [PATCH 033/193] Cleanup --- .claude/skills/setup-simulation/SKILL.md | 6 +- doc/sections/quickstart.rst | 4 +- doc/sections/userguide.rst | 24 +- .../cyclone/pproc_cyclone.py | 8 +- .../itg_cylindre/pproc_drift_kinetic.py | 8 +- .../diocotron_instability/pproc_diocotron.py | 6 +- .../bump_on/pproc_bump_on.py | 4 +- .../pproc_strong_Landau_damping.py | 2 +- .../two_stream/pproc_two_stream.py | 2 +- .../pproc_weak_Landau_damping.py | 2 +- .../pproc_weibel_instability.py | 8 +- pyproject.toml | 1 + src/struphy/diagnostics/plotting.py | 7 +- ...est_verif_IncompressibleNavierStokesSPH.py | 20 +- .../verification/test_verif_LinearMHD.py | 4 +- .../tests/verification/test_verif_Maxwell.py | 22 +- .../tests/verification/test_verif_Poisson.py | 4 +- .../test_verif_ViscousEulerSPH.py | 24 +- src/struphy/post_processing/arrays.py | 25 +- src/struphy/post_processing/output.py | 139 +- .../post_processing/post_processing_tools.py | 176 +- src/struphy/post_processing/store.py | 44 + .../post_processing/tests/test_arrays.py | 7 +- .../post_processing/tests/test_output.py | 66 +- .../tests/test_output_accessors.py | 24 +- tutorials/tutorial_post_processing.ipynb | 4125 +---------------- 26 files changed, 411 insertions(+), 4351 deletions(-) create mode 100644 src/struphy/post_processing/store.py diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index cd67a16d7..298915355 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -135,8 +135,8 @@ out = params.sim.output # or, from anywhere: struphy.open_output( # xarray time series, no post-processing needed -out.fields.._log # dims (t, [component,] e1, e2, e3) -out.distributions...f_binned # dims (t, ) +out.fields.. # dims (t, [component,] e1, e2, e3) +out.distributions...f # dims (t, ) out.orbits. # dims (t, marker, attribute) out.sim.model.units # the Simulation, restored without allocating ``` @@ -144,7 +144,7 @@ out.sim.model.units # the Simulation, restore Products sit under their species and plot themselves, no imports needed: ```python -out...f_binned.struphy.plot.slice(x="e1", y="v1", t="last") # also .panels/.viewer/.animation/.frames +out...f.struphy.plot.slice(x="e1", y="v1", t="last") # also .panels/.viewer/.animation/.frames out..orbits.struphy.plot.trajectories() out.scalars..struphy.plot.timeseries(fit=(t0, t1)) # also .growth_rate/.drift/.relative_error out.plot.scalars(), out.plot.equilibrium(), out.save_report() # whole-run plots diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 9522de048..29a593b50 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -85,7 +85,7 @@ For periodic boundary conditions we will stabilize via ``options``. .. code-block:: python - phi = out.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) + phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) 8. Compare to the exact solution, and save the figure. @@ -149,7 +149,7 @@ Full copy-paste script: sim = Simulation(model=model, domain=domain, grid=grid) out = sim.run(one_time_step=True) - phi = out.fields.em_fields.phi_log.isel(t=-1, e2=0, e3=0) + phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) x = phi.X.values phi_num = phi.values phi_exact = np.cos(k * x) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 8ecfc44fd..0939415b9 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -507,7 +507,7 @@ and it stays available as ``sim.output``: out = sim.run() out.scalars.total_energy # scalar time series, straight from the raw output - out.fields.em_fields.e_field_log # evaluated FEEC field (post-processed on first access) + out.fields.em_fields.e_field # evaluated FEEC field (post-processed on first access) out.sim # the Simulation that produced the output Every product is an :class:`xarray.DataArray` with named dimensions @@ -547,7 +547,7 @@ choose the options, call ``process`` first: out.process( step=1, # evaluate every N-th saved time step celldivide=1, # sub-divide each grid cell for smoother output - physical=False, # also evaluate fields in physical coordinates (*_phy) + physical=False, # also evaluate fields in physical coordinates (*_xyz) guiding_center=False, # compute guiding-center coordinates for markers classify=False, # classify particles by trapping/passing etc. create_vtk=False, # write VTK files for 3D visualization @@ -572,11 +572,11 @@ everything completes as you type: .. code-block:: python # products plot themselves - f = out.kinetic_ions.e1_v1_density.f_binned + f = out.kinetic_ions.e1_v1_density.f f.struphy.plot.slice(x="e1", y="v1", t="last") f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4) f.struphy.plot.viewer(x="e1", y="v1").show() - out.em_fields.phi_phy.struphy.plot.slice(x="e1", y="e2", t="last", coords="physical") + out.em_fields.phi_xyz.struphy.plot.slice(x="e1", y="e2", t="last", coords="physical") out.kinetic_ions.orbits.struphy.plot.trajectories() out.scalars.en_phi.struphy.plot.timeseries(fit=(0.0, 40.0)) # exponential fit in a window out.scalars.en_phi.struphy.analysis.growth_rate(window=(0.0, 40.0)).rate @@ -586,8 +586,8 @@ everything completes as you type: out.save_report() # table + figures in post_processing/report/ # the same products by name, which suits scripts and loops - out["kinetic_ions/e1_v1_density/f_binned"].struphy.plot.slice(x="e1", y="v1", t="last") - out["em_fields/e_field_log"].struphy.analysis.dispersion(slice_at=(0, 0, None), fit_branches=1) + out["kinetic_ions/e1_v1_density/f"].struphy.plot.slice(x="e1", y="v1", t="last") + out["em_fields/e_field"].struphy.analysis.dispersion(slice_at=(0, 0, None), fit_branches=1) Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of several runs are labeled by run: @@ -602,15 +602,15 @@ The sections below access the arrays directly for custom Matplotlib plots. Plotting field data ^^^^^^^^^^^^^^^^^^^^ -Fields are grouped by species and named ``_log`` (logical -components) or ``_phy`` (physical components, with +Fields are grouped by species and named ```` (logical +components) or ``_xyz`` (physical components, with ``physical=True``): .. code-block:: python import matplotlib.pyplot as plt - e_field = out.fields.em_fields.e_field_log # dims (t, component, e1, e2, e3) + e_field = out.fields.em_fields.e_field # dims (t, component, e1, e2, e3) snapshot = e_field.isel(t=-1, component=0, e2=0, e3=0) plt.figure() @@ -625,12 +625,12 @@ Plotting distribution function slices ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Binned particle data is grouped by species and the slice defined in -``BinningPlot(slice=...)``. ``f_binned`` is the full distribution function, -``delta_f_binned`` the perturbation with respect to the background: +``BinningPlot(slice=...)``. ``f`` is the full distribution function, ``delta_f`` the +perturbation with respect to the background: .. code-block:: python - f = out.distributions.kinetic_ions.e1_v1_density.f_binned # dims (t, e1, v1) + f = out.distributions.kinetic_ions.e1_v1_density.f # dims (t, e1, v1) f.struphy.plot.slice(x="e1", y="v1", t="last").show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index a2a1aa30b..d12a0a627 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -11,10 +11,10 @@ # products to sweep interactively, as (name, displayed component or None, physical plane) SWEEPS = [ - ("kinetic_ions/e1_e2_density/delta_f_binned", None, "RZ"), - ("em_fields/phi_phy", None, "RZ"), - ("diagnostics/rho_phy", None, "RZ"), - ("diagnostics/rho_phy", None, "XY"), + ("kinetic_ions/e1_e2_density/delta_f", None, "RZ"), + ("em_fields/phi_xyz", None, "RZ"), + ("diagnostics/rho_xyz", None, "RZ"), + ("diagnostics/rho_xyz", None, "XY"), ] diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index e6caafed5..4f7062ded 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -11,10 +11,10 @@ # products to sweep interactively, as (name, displayed component or None, physical plane) SWEEPS = [ - ("kinetic_ions/e1_e2_density/f_binned", None, "XY"), - ("kinetic_ions/e1_e2_density/delta_f_binned", None, "XY"), - ("em_fields/phi_phy", None, "XY"), - ("diagnostics/rho_phy", None, "XY"), + ("kinetic_ions/e1_e2_density/f", None, "XY"), + ("kinetic_ions/e1_e2_density/delta_f", None, "XY"), + ("em_fields/phi_xyz", None, "XY"), + ("diagnostics/rho_xyz", None, "XY"), ] diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 1c7a7a55a..0d3a770c7 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -16,9 +16,9 @@ # products to sweep interactively in the physical XY plane SWEEPS = [ - "kinetic_ions/e1_e2_density/f_binned", - "kinetic_ions/e1_e2_density/delta_f_binned", - "em_fields/phi_phy", + "kinetic_ions/e1_e2_density/f", + "kinetic_ions/e1_e2_density/delta_f", + "em_fields/phi_xyz", ] diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index a9fbefc48..38e40b2a5 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -6,7 +6,7 @@ def main(): run = params.sim.output # initial velocity distribution - initial = run["kinetic_ions/v1_density/f_binned"].isel(t=0) + initial = run["kinetic_ions/v1_density/f"].isel(t=0) ax = initial.plot()[0].axes ax.set(xlabel="velocity $v$", ylabel="distribution $f(v)$", title="Initial velocity distribution") plt.show() @@ -15,7 +15,7 @@ def main(): run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index afe95b38f..0c2c67cf5 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -8,7 +8,7 @@ def main(): run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 97a5ece94..e7e0dc3be 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -14,7 +14,7 @@ def main(): energy.struphy.plot.timeseries(analytical, title="Electric energy").show() # phase space evolution - f = run.kinetic_ions.e1_v1_density.f_binned + f = run.kinetic_ions.e1_v1_density.f f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4).show() # interactive alternative to dumping a frame sequence diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 90d029337..e3e3bfbc3 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -23,7 +23,7 @@ def main(): energy.struphy.plot.timeseries(analytical, title="Electric energy").show() # full f and delta f in the e1-v1 plane at four times - for quantity, title in (("f_binned", "full-$f$"), ("delta_f_binned", r"$\delta f$")): + for quantity, title in (("f", "full-$f$"), ("delta_f", r"$\delta f$")): getattr(run.kinetic_ions.e1_v1_density, quantity).struphy.plot.panels( x="e1", y="v1", nrows=1, ncols=4, title=title ).show() diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 87502013d..8368608a0 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -31,8 +31,8 @@ def main(): # ------------------ # progression of EM-field energy along different directions # ------------------ - e_field = run.fields.em_fields.e_field_log - b_field = run.fields.em_fields.b_field_log + e_field = run.fields.em_fields.e_field + b_field = run.fields.em_fields.b_field spatial = ("e1", "e2", "e3") unit_volume = xp.prod([1 / (e_field.sizes[dim] - 1) for dim in spatial]) @@ -92,7 +92,7 @@ def field_energy(field): # ------------------ distributions = run.distributions.kinetic_ions for bin_name, x, y in (("e1_v1_density", "e1", "v1"), ("v1_v2_density", "v1", "v2")): - for quantity in ("f_binned", "delta_f_binned"): + for quantity in ("f", "delta_f"): getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels(x=x, y=y, nrows=5, ncols=4).show() # ------------------ @@ -130,7 +130,7 @@ def current_1D(time_step: float): fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True) for i in range(3): for j in range(3): - current = getattr(distributions, f"e{i + 1}_current_{j + 1}").f_binned + current = getattr(distributions, f"e{i + 1}_current_{j + 1}").f current = current.sel(t=time_step, method="nearest") ax[i, j].axhline(color="red", alpha=0.5) ax[i, j].plot(current[f"e{i + 1}"], current) diff --git a/pyproject.toml b/pyproject.toml index c230ad71e..cbe4b9315 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "feectools >= 0.1.10, <=0.1.10", "scipy<=1.18.0", "h5py<=3.16.0", + "h5netcdf<=1.8.1", "xarray<=2026.2.0", "matplotlib<=3.11.0", "pyyaml<=6.0.3", diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 4446448f1..1f0e1c23d 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -19,7 +19,6 @@ from struphy.post_processing.arrays import ( SCALARS_EXCLUDE, axis_label, - orbit_columns, save_scalars, scalar_names, validate_array, @@ -552,13 +551,11 @@ def save_all_scalars( def plot_marker_trajectories(orbits: xr.DataArray, *, ax=None, max_markers=200, show_paths=None): """Plot a static 3-D trajectory overview; interactive marker UI is intentionally separate.""" - validate_array(orbits, required_dims=("t", "marker", "attribute")) - columns = orbits.attrs.get("columns", orbit_columns(orbits.sizes["attribute"])) + validate_array(orbits, required_dims=("t", "marker", "quantity")) count = min(orbits.sizes["marker"], max_markers) - values = np.asarray(orbits.isel(marker=slice(0, count))) + positions = np.asarray(orbits.isel(marker=slice(0, count)).sel(quantity=["x", "y", "z"])) fig = plt.figure() if ax is None else ax.figure ax = fig.add_subplot(111, projection="3d") if ax is None else ax - positions = values[..., columns["position"]] show_paths = count <= 200 if show_paths is None else show_paths artists = [] if show_paths: diff --git a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py index be85c42f3..5d55ea0e5 100644 --- a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py +++ b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py @@ -103,8 +103,8 @@ def test_chorin_projection_periodic_1d(nx: int, do_plot: bool = False): run.process() if MPI.COMM_WORLD.Get_rank() == 0: - e1_grid = run.distributions.fluid.e1_current_1.f_binned.e1.values.flatten() - j1_binned = run.distributions.fluid.e1_current_1.f_binned.values # (Nt+1, n_bins) + e1_grid = run.distributions.fluid.e1_current_1.f.e1.values.flatten() + j1_binned = run.distributions.fluid.e1_current_1.f.values # (Nt+1, n_bins) amp_initial = 0.5 * (np.max(j1_binned[0]) - np.min(j1_binned[0])) amp_final = 0.5 * (np.max(j1_binned[-1]) - np.min(j1_binned[-1])) @@ -209,8 +209,8 @@ def test_chorin_projection_reflect_1d(nx: int, do_plot: bool = False): run.process() if MPI.COMM_WORLD.Get_rank() == 0: - e1_grid = run.distributions.fluid.e1_current_1.f_binned.e1.values.flatten() - j1_binned = run.distributions.fluid.e1_current_1.f_binned.values # (Nt+1, n_bins) + e1_grid = run.distributions.fluid.e1_current_1.f.e1.values.flatten() + j1_binned = run.distributions.fluid.e1_current_1.f.values # (Nt+1, n_bins) amp_initial = np.max(np.abs(j1_binned[0])) amp_final = np.max(np.abs(j1_binned[-1])) @@ -320,9 +320,9 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): run.process() if MPI.COMM_WORLD.Get_rank() == 0: - e2_grid = run.distributions.fluid.e2_current_1.f_binned.e2.values.flatten() - j1_binned = run.distributions.fluid.e2_current_1.f_binned.values # (Nt+1, n_bins) - j2_binned = run.distributions.fluid.e2_current_2.f_binned.values # (Nt+1, n_bins) + e2_grid = run.distributions.fluid.e2_current_1.f.e2.values.flatten() + j1_binned = run.distributions.fluid.e2_current_1.f.values # (Nt+1, n_bins) + j2_binned = run.distributions.fluid.e2_current_2.f.values # (Nt+1, n_bins) # Analytische Profile U = 0.5 @@ -357,9 +357,9 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): plt.tight_layout() plt.show() - e2_grid = run.distributions.fluid.e2_current_1.f_binned.e2.values.flatten() - j1_binned = run.distributions.fluid.e2_current_1.f_binned.values # (Nt+1, n_bins) - j2_binned = run.distributions.fluid.e2_current_2.f_binned.values # (Nt+1, n_bins) + e2_grid = run.distributions.fluid.e2_current_1.f.e2.values.flatten() + j1_binned = run.distributions.fluid.e2_current_1.f.values # (Nt+1, n_bins) + j2_binned = run.distributions.fluid.e2_current_2.f.values # (Nt+1, n_bins) # --- DEBUG: Check marker velocities --- markers = model.fluid.density.particles.markers diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 4b18fe891..7e380cffb 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -88,7 +88,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} _1, _2, _3, coeffs = run.analysis.dispersion( - "mhd/velocity_log", + "mhd/velocity", physical=True, component=0, slice_at=[0, 0, None], @@ -109,7 +109,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): # second fft _1, _2, _3, coeffs = run.analysis.dispersion( - "mhd/pressure_log", + "mhd/pressure", physical=True, component=0, slice_at=[0, 0, None], diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 8f62fa1bc..e311a3f85 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -73,7 +73,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: # fft _1, _2, _3, coeffs = run.analysis.dispersion( - "em_fields/e_field_log", + "em_fields/e_field", physical=True, component=0, slice_at=[0, 0, None], @@ -157,13 +157,13 @@ def test_coaxial(do_plot: bool = False): modes = m # load data at the final time in the plane eta3 = 0 - e_field_phy = run.fields.em_fields.e_field_phy.isel(t=-1, e3=0) - b_field_phy = run.fields.em_fields.b_field_phy.isel(t=-1, e3=0) - t_end = float(e_field_phy.t) + e_field_xyz = run.fields.em_fields.e_field_xyz.isel(t=-1, e3=0) + b_field_xyz = run.fields.em_fields.b_field_xyz.isel(t=-1, e3=0) + t_end = float(e_field_xyz.t) - X = e_field_phy.X.values - Y = e_field_phy.Y.values - Z = e_field_phy.Z.values + X = e_field_xyz.X.values + Y = e_field_xyz.Y.values + Z = e_field_xyz.Z.values # define analytic solution def B_z(X, Y, Z, m, t): @@ -213,7 +213,7 @@ def to_E_theta(X, Y, E_x, E_y): ax2.contourf( X, Y, - to_E_theta(X, Y, e_field_phy.isel(component=0).values, e_field_phy.isel(component=1).values), + to_E_theta(X, Y, e_field_xyz.isel(component=0).values, e_field_xyz.isel(component=1).values), cmap="plasma", levels=100, vmin=vmin, @@ -226,11 +226,11 @@ def to_E_theta(X, Y, E_x, E_y): plt.show() # assert - Ex_tend = e_field_phy.isel(component=0).values - Ey_tend = e_field_phy.isel(component=1).values + Ex_tend = e_field_xyz.isel(component=0).values + Ey_tend = e_field_xyz.isel(component=1).values Er_exact = E_r(X, Y, Z, modes, t_end) Etheta_exact = E_theta(X, Y, Z, modes, t_end) - Bz_tend = b_field_phy.isel(component=2).values + Bz_tend = b_field_xyz.isel(component=2).values Bz_exact = B_z(X, Y, Z, modes, t_end) error_Er = xp.max(xp.abs((to_E_r(X, Y, Ex_tend, Ey_tend) - Er_exact))) diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index ce2a5e337..1a1e185d5 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -83,8 +83,8 @@ def test_poisson_1d(do_plot=False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - phi = run.fields.em_fields.phi_log.isel(e2=0, e3=0) - source = run.fields.em_fields.source_log.isel(e2=0, e3=0) + phi = run.fields.em_fields.phi.isel(e2=0, e3=0) + source = run.fields.em_fields.source.isel(e2=0, e3=0) x = phi.X.values interval = 2 diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index abcf54e5c..1ffa4fe88 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -114,7 +114,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - density = run.densities.euler_fluid.view_0.n_sph + density = run.densities.euler_fluid.view_0.n ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") n_sph = density.values @@ -245,10 +245,10 @@ def test_damped_sound_wave(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - e1_binned = run.distributions.euler_fluid.e1_density.f_binned.e1.values - n_binned = run.distributions.euler_fluid.e1_density.delta_f_binned.values - j1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.values - density = run.densities.euler_fluid.view_0.n_sph + e1_binned = run.distributions.euler_fluid.e1_density.f.e1.values + n_binned = run.distributions.euler_fluid.e1_density.delta_f.values + j1_binned = run.distributions.euler_fluid.e1_current_1.f.values + density = run.densities.euler_fluid.view_0.n ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") n_sph = density.values @@ -451,12 +451,12 @@ def test_velocity_diffusion(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - density = run.densities.euler_fluid.view_0.n_sph + density = run.densities.euler_fluid.view_0.n ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") n_sph = density.values - e1_binned = run.distributions.euler_fluid.e1_density.f_binned.e1.values - n_binned = run.distributions.euler_fluid.e1_density.f_binned.values - j1_binned = run.distributions.euler_fluid.e1_current_1.f_binned.values + e1_binned = run.distributions.euler_fluid.e1_density.f.e1.values + n_binned = run.distributions.euler_fluid.e1_density.f.values + j1_binned = run.distributions.euler_fluid.e1_current_1.f.values print(f"{e1_binned.shape = }") print(f"{n_binned.shape = }") print(f"{j1_binned.shape = }") @@ -662,8 +662,8 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ run.process() if MPI.COMM_WORLD.Get_rank() == 0: - e2_grid = run.distributions.euler_fluid.e2_current_1.f_binned.e2.values # logical y in [0, 1] - j1_binned = run.distributions.euler_fluid.e2_current_1.f_binned.values # shape (Nt+1, n_bins) + e2_grid = run.distributions.euler_fluid.e2_current_1.f.e2.values # logical y in [0, 1] + j1_binned = run.distributions.euler_fluid.e2_current_1.f.values # shape (Nt+1, n_bins) import numpy as np @@ -928,7 +928,7 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo Nt = int(time_opts.Tend / dt) times = np.linspace(0.0, time_opts.Tend, Nt + 1) - density = run.densities.euler_fluid.view_0.n_sph + density = run.densities.euler_fluid.view_0.n ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") n_sph = density.values # (Nt+1, pts_e1, pts_e2, 1) diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 86e163422..422f25b52 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -26,7 +26,7 @@ "Z": r"$Z$", "component": "component", "marker": "marker", - "attribute": "attribute", + "quantity": "quantity", } BINNED_LABELS = {"f_binned": "$f$", "delta_f_binned": r"$\delta f$", "n_sph": "$n$"} SCALARS_EXCLUDE = ("time",) @@ -151,17 +151,32 @@ def orbit_columns(n_columns: int) -> dict: return columns +def orbit_quantities(n_columns: int) -> list[str]: + """Name every saved marker column, so that orbits are self-describing.""" + columns = orbit_columns(n_columns) + names = [""] * n_columns + for axis, name in enumerate(("x", "y", "z")): + names[axis] = name + velocity = columns["velocity"] + indices = range(*velocity.indices(n_columns)) if isinstance(velocity, slice) else [velocity] + for number, index in enumerate(indices, 1): + names[index] = f"v{number}" + if "weight" in columns: + names[columns["weight"]] = "weight" + names[columns["id"]] = "id" + return [name or f"column_{index}" for index, name in enumerate(names)] + + def wrap_orbits(values, time, *, time_unit="") -> xr.DataArray: - """Label marker orbits with time, marker and attribute dimensions.""" + """Label marker orbits with time, marker and named quantity dimensions.""" values = np.asarray(values) return data_array( values, - ("t", "marker", "attribute"), - {"t": time, "marker": np.arange(values.shape[1]), "attribute": np.arange(values.shape[2])}, + ("t", "marker", "quantity"), + {"t": time, "marker": np.arange(values.shape[1]), "quantity": orbit_quantities(values.shape[2])}, name="orbits", label="marker orbits", coord_units={"t": time_unit}, - attrs={"columns": orbit_columns(values.shape[-1])}, ) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 6f9912551..68d646c51 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -import pickle import warnings from collections.abc import Callable, Iterator, Mapping from pathlib import Path @@ -12,7 +11,8 @@ import numpy as np import xarray as xr -from struphy.post_processing.arrays import data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits +from struphy.post_processing import store +from struphy.post_processing.arrays import data_array, save_scalars from struphy.post_processing.output_accessors import OutputPlots logger = logging.getLogger("struphy") @@ -146,7 +146,10 @@ def with_time_units(self, time_units: str) -> "Output": return type(self)(self.path_out, sim=self._sim, time_units=time_units) def _reset(self): + if getattr(self, "_tree", None) is not None: + self._tree.close() # an open store would block the next process() from writing it self._time = self._grids_log = self._grids_phy = self._scalars = self._products = self._label = None + self._tree = None self._species = None self._seconds = None @@ -282,12 +285,7 @@ def _ensure_processed(self): def _product_mappings(self) -> dict[str, ProductMapping]: if self._products is None: self._ensure_processed() - discovered = { - "fields": self._discover_fields(), - "distributions": self._discover_binned("distribution_function"), - "densities": self._discover_binned("n_sph"), - "orbits": self._discover_orbits(), - } + discovered = {kind: self._discover(kind) for kind in ("fields", "distributions", "densities", "orbits")} self._products = { kind: ProductMapping({key: (lambda load=load: self._stamp(load())) for key, load in loaders.items()}) for kind, loaders in discovered.items() @@ -437,6 +435,13 @@ def time(self): self._time = np.load(self.path_pproc / "t_grid.npy", mmap_mode="r") * self.time_scale return self._time + def _first_field(self) -> xr.Dataset | None: + """The dataset of a field species, which carries the evaluation grids.""" + for group, dataset in self._groups().items(): + if "/" not in group and any("e1" in dataset[name].dims for name in dataset.data_vars): + return dataset + return None + @property def grids_log(self): """Logical evaluation grids of the fields; None for a run without FEEC fields.""" @@ -452,12 +457,14 @@ def grids_phy(self): return self._grids_phy def _load_grids(self, name): - self._ensure_processed() - path = self.path_pproc / "fields_data" / f"{name}.bin" - if not path.exists(): + dataset = self._first_field() + if dataset is None: return None - with path.open("rb") as stream: - return pickle.load(stream) + if name == "grids_log": + return [np.asarray(dataset[dim]) for dim in ("e1", "e2", "e3")] + if not all(coordinate in dataset.coords for coordinate in ("X", "Y", "Z")): + return None + return [np.asarray(dataset[coordinate]) for coordinate in ("X", "Y", "Z")] @property def scalars(self) -> xr.Dataset: @@ -551,84 +558,44 @@ def save_report(self, directory=None, **kwargs) -> list[str]: directory = Path(directory) if directory else self.path_pproc / "report" return save_all_scalars(self.scalars, directory, run_label=self.label, **kwargs) - def _discover_fields(self): - loaders = {} - root = self.path_pproc / "fields_data" - for path in sorted(root.glob("*/*.bin")) if root.exists() else (): - key = f"{path.parent.name}/{path.stem}" - loaders[key] = lambda path=path, key=key: self._load_field(path, key) - return loaders - - def _load_field(self, path: Path, key: str): - with path.open("rb") as stream: - raw = pickle.load(stream) - try: - physical = self.grids_phy - except FileNotFoundError: - physical = None - return wrap_field_data( - raw, - self.grids_log, - grids_phy=physical, - name=key.split("/")[-1], - time_scale=self.time_scale, - time_unit=self.time_unit, - ) + @property + def tree(self) -> xr.DataTree: + """The product store as an :class:`xarray.DataTree`, read lazily.""" + if self._tree is None: + self._ensure_processed() + self._tree = store.open_tree(store.store_path(self.path_pproc)) + return self._tree - def _discover_binned(self, category: str): - loaders = {} - root = self.path_pproc / "kinetic_data" - pattern = f"*/{category}/*/*.npy" - for path in sorted(root.glob(pattern)) if root.exists() else (): - if path.stem.startswith("grid_"): - continue - species, slice_name = path.parents[2].name, path.parent.name - key = f"{species}/{slice_name}/{path.stem}" - loaders[key] = lambda path=path, slice_name=slice_name: self._load_binned(path, slice_name) - return loaders + def _groups(self) -> dict[str, xr.Dataset]: + """Every group of the store that holds products, by path without the leading slash.""" + return {path.lstrip("/"): node.ds for path, node in self.tree.subtree_with_keys if node.ds.data_vars} - def _load_binned(self, path: Path, slice_name: str): - grid_paths = sorted(path.parent.glob("grid_*.npy")) - grids = {p.stem.removeprefix("grid_"): np.load(p, mmap_mode="r") for p in grid_paths} - # binned slices are named after their dimensions (e1_v1_density); SPH views (view_0) are not - dims = tuple(part for part in slice_name.split("_") if part in grids) or tuple(sorted(grids)) - values = np.load(path, mmap_mode="r") - expected = (len(self.time), *(len(grids[dim]) for dim in dims)) - if values.shape != expected: - raise ValueError(f"{path} has shape {values.shape}; expected {expected} from its coordinates") - coords = {"t": self.time, **{dim: grids[dim] for dim in dims}} - logical_dims = tuple(dim for dim in dims if dim in {"e1", "e2", "e3"}) - try: - if len(logical_dims) == 2: - mesh = np.meshgrid(*(np.asarray(grids[dim]) for dim in logical_dims), indexing="ij") - arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} - arguments.update(dict(zip(logical_dims, mesh))) - physical = self.sim.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) - elif len(logical_dims) == 3: - physical = self.sim.domain(*(np.asarray(grids[dim]) for dim in ("e1", "e2", "e3"))) - else: - physical = () - for coordinate, grid in zip(("X", "Y", "Z"), physical): - coords[coordinate] = (logical_dims, np.asarray(grid)) - except (FileNotFoundError, TypeError, ValueError): - logger.debug("Could not attach physical coordinates to %s", path, exc_info=True) - return wrap_binned_data(values, dims, coords, name=path.stem, time_unit=self.time_unit) - - def _discover_orbits(self): + def _discover(self, kind: str) -> dict: + """Loaders for one kind of product, keyed as ``[/]/``.""" loaders = {} - root = self.path_pproc / "kinetic_data" - for directory in sorted(root.glob("*/orbits")) if root.exists() else (): - loaders[directory.parent.name] = lambda directory=directory: self._load_orbits(directory) + for group, dataset in self._groups().items(): + for name in dataset.data_vars: + if self._kind(group, name) != kind: + continue + key = group if name == "orbits" else f"{group}/{name}" + loaders[key] = lambda group=group, name=name: self._load(group, name) return loaders - def _load_orbits(self, directory: Path): - paths = sorted(directory.glob("*.npy"), key=lambda p: int(p.stem.rsplit("_", 1)[-1])) - if not paths: - raise FileNotFoundError(f"no orbit arrays in {directory}") - # one small file per saved step: read them instead of keeping thousands of memory maps open - values = np.stack([np.load(path) for path in paths]) - return wrap_orbits(values, self.time[: len(paths)], time_unit=self.time_unit) - + @staticmethod + def _kind(group: str, name: str) -> str: + """Which catalog a variable belongs to; the store keeps them apart by shape and name.""" + if name == "orbits": + return "orbits" + if "/" not in group: + return "fields" + return "densities" if name == "n" else "distributions" + + def _load(self, group: str, name: str) -> xr.DataArray: + array = self.tree[group].ds[name] + if self.time_units == "physical" and "t" in array.dims: + array = array.assign_coords(t=array.t * self.time_scale) + array.coords["t"].attrs["units"] = "s" + return array def open_output(path_out, *, time_units: str = "normalized") -> Output: """Open the output folder of a finished simulation. diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index ea941f9d5..6d9f4e44f 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -2,7 +2,6 @@ import json import logging import os -import pickle import shutil from collections.abc import Sequence from contextlib import ExitStack @@ -10,6 +9,7 @@ import cunumpy as xp import h5py +import xarray as xr import yaml from feectools.ddm.mpi import MockComm from feectools.ddm.mpi import mpi as MPI @@ -19,6 +19,8 @@ from struphy.models.species import ParticleSpecies from struphy.models.variables import PICVariable, SPHVariable from struphy.pic.base import Particles +from struphy.post_processing import store +from struphy.post_processing.arrays import wrap_binned_data, wrap_field_data, wrap_orbits from struphy.post_processing.orbits import orbits_tools from struphy.utils.progress import tqdm @@ -227,6 +229,9 @@ def process( return False self._reset_pproc_dir() + if self.rank == 0: + store.create(store.store_path(self.path_pproc), options=json.dumps(options)) + self.comm.Barrier() self._write_manifest("processing", options=options) logger.warning(f"\nPost-processing path {self.path_out}") @@ -235,6 +240,7 @@ def process( if self.rank == 0: # save time grid at which post-processing data is created xp.save(os.path.join(self.path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) + self.t_grid = xp.asarray(file["time/value"][::step]) if "feec" in file.keys(): self.exist_fields = True @@ -348,40 +354,27 @@ def process_fields( point_data[species][name][t] = val point_data_phy[species][name][t] = vals_phy[species][name] - # directory for field data + # directory for the vtk files path_fields = os.path.join(self.path_pproc, "fields_data") if self.rank == 0: - try: - os.mkdir(path_fields) - except: - shutil.rmtree(path_fields) - os.mkdir(path_fields) - - # save data dicts for each field + # one group per species in the product store, with the mapped grids as coordinates for species, vars in point_data.items(): + variables = {} for name, val in vars.items(): - try: - os.mkdir(os.path.join(path_fields, species)) - except: - pass - - with open(os.path.join(path_fields, species, name + "_log.bin"), "wb") as handle: - pickle.dump(val, handle, protocol=pickle.HIGHEST_PROTOCOL) - + variables[name] = wrap_field_data(val, grids_log, grids_phy=grids_phy, name=name) if physical: - with open(os.path.join(path_fields, species, name + "_phy.bin"), "wb") as handle: - pickle.dump(point_data_phy[species][name], handle, protocol=pickle.HIGHEST_PROTOCOL) - - # save grids - with open(os.path.join(path_fields, "grids_log.bin"), "wb") as handle: - pickle.dump(grids_log, handle, protocol=pickle.HIGHEST_PROTOCOL) - - with open(os.path.join(path_fields, "grids_phy.bin"), "wb") as handle: - pickle.dump(grids_phy, handle, protocol=pickle.HIGHEST_PROTOCOL) + variables[name + "_xyz"] = wrap_field_data( + point_data_phy[species][name], grids_log, grids_phy=grids_phy, name=name + "_xyz" + ) + store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset(variables)) - # create vtk files if create_vtk: + try: + os.mkdir(path_fields) + except FileExistsError: + shutil.rmtree(path_fields) + os.mkdir(path_fields) self._create_vtk(path_fields, t_grid, grids_phy, point_data) if physical: self._create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) @@ -873,8 +866,9 @@ def _post_process_markers( os.mkdir(path_orbits) self.comm.Barrier() - # temporary array + # temporary array, plus every step of it for the product store temp = xp.empty((n_markers, len(save_index)), order="C") + orbits = [] lost_particles_mask = xp.empty(n_markers, dtype=bool) logger.warning(f"Evaluation of {n_markers} marker orbits for {species}") @@ -927,6 +921,7 @@ def _post_process_markers( temp[~lost_particles_mask, :3] = pos_phys if self.rank == 0: + orbits.append(temp.copy()) # save numpy xp.save(file_npy, temp) # move ids to first column and save txt @@ -934,6 +929,10 @@ def _post_process_markers( xp.savetxt(file_txt, temp[:, (0, 1, 2, 3, -1)], fmt="%12.6f", delimiter=", ") self.comm.Barrier() + if self.rank == 0: + values = wrap_orbits(xp.stack(orbits), self.t_grid[: len(orbits)]) + store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset({"orbits": values})) + def _post_process_f( self, path_kinetic_species, @@ -960,51 +959,21 @@ def _post_process_f( species = path_kinetic_species.split("/")[-1] - # directory for .npy files - path_distr = os.path.join(path_kinetic_species, "distribution_function") - - if self.rank == 0: - try: - os.mkdir(path_distr) - except: - shutil.rmtree(path_distr) - os.mkdir(path_distr) - self.comm.Barrier() - logger.warning("Evaluation of distribution functions for " + str(species)) - # Create grids + # the bin centers of every slice, as saved by the simulation + slice_grids = {} with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: - slice_names = [] for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - slice_names += [slice_name] - # create a new folder for each slice - path_slice = os.path.join(path_distr, slice_name) - if self.rank == 0: - os.mkdir(path_slice) - self.comm.Barrier() - - # Find out all names of slices - slice_splits = slice_name.split("_") - - # save grid - for n_gr, (_, grid) in enumerate(file_0["kinetic/" + species + "/f/" + slice_name].attrs.items()): - grid_path = os.path.join( - path_slice, - "grid_" + slice_splits[n_gr] + ".npy", - ) - if self.rank == 0: - xp.save(grid_path, grid[:]) - self.comm.Barrier() + dims = [part for part in slice_name.split("_")] + centers = [grid[:] for _, grid in file_0["kinetic/" + species + "/f/" + slice_name].attrs.items()] + slice_grids[slice_name] = dict(zip(dims, centers)) + slice_names = list(slice_grids) # compute distribution function for slice_name in tqdm(slice_names): logger.info(f"Processing slice {slice_name} for species {species}") - # path to folder of slice - path_slice = os.path.join(path_distr, slice_name) - - # Find out all names of slices - slice_splits = slice_name.split("_") + grids = slice_grids[slice_name] for rank in self.range_ranks: print(f"{rank = } ----------------------------") @@ -1054,10 +1023,7 @@ def _post_process_f( print(f"{self.rank =} done.") if self.rank == 0: - # save distribution functions - xp.save(os.path.join(path_slice, "f_binned.npy"), data) - xp.save(os.path.join(path_slice, "delta_f_binned.npy"), data_df) - + full_f = data if compute_bckgr: # the background of a delta-f species is stored by the simulation on the bin centers key_background = f"kinetic/{species}/f_background/{slice_name}" @@ -1070,7 +1036,38 @@ def _post_process_f( data_bckgr = file[key_background][()] # add extra axis for data_bckgr since data_df has axis for time series - xp.save(os.path.join(path_slice, "f_binned.npy"), data_df + data_bckgr[None]) + full_f = data_df + data_bckgr[None] + + store.write_group( + store.store_path(self.path_pproc), + f"/{species}/{slice_name}", + self._binned_dataset(grids, {"f": full_f, "delta_f": data_df}), + ) + + def _binned_dataset(self, grids: dict, variables: dict) -> xr.Dataset: + """One binned product per variable, with time, bin centers and mapped coordinates.""" + dims = tuple(dim for dim in grids) + coords = {"t": self.t_grid, **grids} + coords.update(self._mapped_coords(grids)) + return xr.Dataset({name: wrap_binned_data(values, dims, coords, name=name) for name, values in variables.items()}) + + def _mapped_coords(self, grids: dict) -> dict: + """``X``, ``Y``, ``Z`` on the logical directions of ``grids``, when there are two or three.""" + logical = tuple(dim for dim in grids if dim in ("e1", "e2", "e3")) + if len(logical) not in (2, 3) or self.domain is None: + return {} + try: + if len(logical) == 2: + mesh = xp.meshgrid(*(xp.asarray(grids[dim]) for dim in logical), indexing="ij") + arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} + arguments.update(dict(zip(logical, mesh))) + mapped = self.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) + else: + mapped = self.domain(*(xp.asarray(grids[dim]) for dim in logical)) + except (TypeError, ValueError): + logger.debug("Could not map the coordinates of %s", logical, exc_info=True) + return {} + return {name: (logical, xp.asarray(grid)) for name, grid in zip(("X", "Y", "Z"), mapped)} def _post_process_n_sph( self, @@ -1088,40 +1085,18 @@ def _post_process_n_sph( """ species = path_kinetic_species.split("/")[-1] - # directory for .npy files - path_n_sph = os.path.join(path_kinetic_species, "n_sph") - - if self.rank == 0: - try: - os.mkdir(path_n_sph) - except: - shutil.rmtree(path_n_sph) - os.mkdir(path_n_sph) - self.comm.Barrier() - logger.warning("Evaluation of sph density for " + str(species)) + # the evaluation points of every view, as saved by the simulation + view_grids = {} with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: - views = list(file_0["kinetic/" + species + "/n_sph"]) - - # Create grids - for view in views: - # create a new folder for each view - path_view = os.path.join(path_n_sph, view) - if self.rank == 0: - os.mkdir(path_view) - self.comm.Barrier() - - # save the 1d evaluation points, one file per logical direction + for view in file_0["kinetic/" + species + "/n_sph"]: attrs = file_0["kinetic/" + species + "/n_sph/" + view].attrs - if self.rank == 0: - for direction in ("1", "2", "3"): - xp.save(os.path.join(path_view, f"grid_e{direction}.npy"), attrs["eta" + direction][:]) + view_grids[view] = {f"e{direction}": attrs["eta" + direction][:] for direction in ("1", "2", "3")} + views = list(view_grids) # compute sph density for view in tqdm(views): - path_view = os.path.join(path_n_sph, view) - for rank in self.range_ranks: with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: if self.parallel_pproc: @@ -1149,5 +1124,8 @@ def _post_process_n_sph( ) if self.rank == 0: - # save sph density - xp.save(os.path.join(path_view, "n_sph.npy"), data) + store.write_group( + store.store_path(self.path_pproc), + f"/{species}/{view}", + self._binned_dataset(view_grids[view], {"n": data}), + ) diff --git a/src/struphy/post_processing/store.py b/src/struphy/post_processing/store.py new file mode 100644 index 000000000..216489f81 --- /dev/null +++ b/src/struphy/post_processing/store.py @@ -0,0 +1,44 @@ +"""The post-processed products of a run, as one self-describing netCDF file. + +``post_processing/output.nc`` holds one group per species, with the products as variables of +that group's dataset:: + + /em_fields e_field, phi, and e_field_xyz, phi_xyz with physical=True + /kinetic_ions orbits + /kinetic_ions/e1_v1_density f, delta_f + /kinetic_ions/view_0 n + +Coordinates (time, the logical grids, and the mapped ``X``, ``Y``, ``Z``), units and labels +travel with the data, so reading needs nothing but the file. Groups are written one at a time +and read lazily, through xarray's ``h5netcdf`` engine. +""" + +from __future__ import annotations + +import os + +import xarray as xr + +STORE_NAME = "output.nc" +SCHEMA_VERSION = 1 +ENGINE = "h5netcdf" + + +def store_path(path_pproc) -> str: + """Path of the product store inside a post-processing directory.""" + return os.path.join(str(path_pproc), STORE_NAME) + + +def create(path, **attrs): + """Start a new store, discarding an existing one.""" + xr.Dataset(attrs={"schema_version": SCHEMA_VERSION, **attrs}).to_netcdf(path, mode="w", engine=ENGINE) + + +def write_group(path, group: str, dataset: xr.Dataset): + """Add one group to the store; its coordinates and attributes are stored with it.""" + dataset.to_netcdf(path, group=group, mode="a", engine=ENGINE) + + +def open_tree(path) -> xr.DataTree: + """Open the store lazily; arrays are read from disk when they are used.""" + return xr.open_datatree(path, engine=ENGINE) diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index 62285a75e..c45de7b80 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -74,10 +74,11 @@ def test_binned_wrapper_keeps_memory_mappable_values(): assert data.attrs["label"] == "$f$" -def test_orbits_store_column_semantics_as_metadata(): +def test_orbits_name_their_columns(): data = wrap_orbits(np.zeros((2, 5, 8)), [0, 1]) - assert data.attrs["columns"]["weight"] == 6 - assert data.sel(marker=2).dims == ("t", "attribute") + assert list(data.quantity.values) == ["x", "y", "z", "v1", "v2", "v3", "weight", "id"] + assert data.sel(marker=2).dims == ("t", "quantity") + assert data.sel(quantity="weight").dims == ("t", "marker") def test_scalar_alignment_is_exact(): diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index b460f4c24..9b8ca5e5c 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -2,47 +2,51 @@ import json import os -import pickle import h5py import numpy as np import pytest +import xarray as xr from struphy.post_processing.output import Output, open_output +from struphy.post_processing import store +from struphy.post_processing.arrays import orbit_quantities from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 def write_tree(root): + """A small but complete output folder: raw HDF5 plus the product store.""" pproc = os.path.join(root, "post_processing") - fields = os.path.join(pproc, "fields_data") - kinetic = os.path.join(pproc, "kinetic_data") - os.makedirs(os.path.join(fields, "em_fields")) - os.makedirs(os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density")) - os.makedirs(os.path.join(kinetic, "kinetic_ions", "orbits")) + os.makedirs(pproc) t = np.linspace(0, 1, NT) np.save(os.path.join(pproc, "t_grid.npy"), t) - logical = [np.linspace(0, 1, n) for n in (N1, N2, N3)] - physical = np.meshgrid(*logical, indexing="ij") - for name, value in (("grids_log", logical), ("grids_phy", physical)): - with open(os.path.join(fields, f"{name}.bin"), "wb") as stream: - pickle.dump(value, stream) - values = {time: [np.full((N1, N2, N3), i + time) for i in range(3)] for time in t} - with open(os.path.join(fields, "em_fields", "E.bin"), "wb") as stream: - pickle.dump(values, stream) - slice_dir = os.path.join(kinetic, "kinetic_ions", "distribution_function", "e1_v1_density") - np.save(os.path.join(slice_dir, "grid_e1.npy"), np.linspace(0, 1, N1)) - np.save(os.path.join(slice_dir, "grid_v1.npy"), np.linspace(-3, 3, NV)) - np.save(os.path.join(slice_dir, "f_binned.npy"), np.ones((NT, N1, NV))) - view_dir = os.path.join(kinetic, "kinetic_ions", "n_sph", "view_0") - os.makedirs(view_dir) - for direction, n in zip("123", (N1, N2, 1)): - np.save(os.path.join(view_dir, f"grid_e{direction}.npy"), np.linspace(0, 1, n)) - np.save(os.path.join(view_dir, "n_sph.npy"), np.ones((NT, N1, N2, 1))) - orbit_dir = os.path.join(kinetic, "kinetic_ions", "orbits") - for step in range(NT): - np.save(os.path.join(orbit_dir, f"kinetic_ions_{step}.npy"), np.full((N_MARKERS, 8), step)) + + logical = {f"e{axis + 1}": np.linspace(0, 1, n) for axis, n in enumerate((N1, N2, N3))} + mapped = np.meshgrid(*logical.values(), indexing="ij") + path = store.store_path(pproc) + store.create(path) + store.write_group(path, "/em_fields", xr.Dataset( + {"E": (("t", "component", "e1", "e2", "e3"), + np.stack([np.stack([np.full((N1, N2, N3), i + time) for i in range(3)]) for time in t]))}, + coords={"t": t, "component": [0, 1, 2], **logical, + **{name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), mapped)}}, + )) + store.write_group(path, "/kinetic_ions/e1_v1_density", xr.Dataset( + {"f": (("t", "e1", "v1"), np.ones((NT, N1, NV))), "delta_f": (("t", "e1", "v1"), np.zeros((NT, N1, NV)))}, + coords={"t": t, "e1": logical["e1"], "v1": np.linspace(-3, 3, NV)}, + )) + store.write_group(path, "/kinetic_ions/view_0", xr.Dataset( + {"n": (("t", "e1", "e2", "e3"), np.ones((NT, N1, N2, 1)))}, + coords={"t": t, "e1": logical["e1"], "e2": logical["e2"], "e3": np.zeros(1)}, + )) + store.write_group(path, "/kinetic_ions", xr.Dataset( + {"orbits": (("t", "marker", "quantity"), + np.stack([np.full((N_MARKERS, 8), step) for step in range(NT)]))}, + coords={"t": t, "marker": np.arange(N_MARKERS), "quantity": orbit_quantities(8)}, + )) + data_dir = os.path.join(root, "data") os.makedirs(data_dir) with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as file: @@ -109,13 +113,13 @@ def test_field_has_named_and_curvilinear_coordinates(run): def test_binned_products_have_coordinates(run): - data = run.distributions["kinetic_ions/e1_v1_density/f_binned"] + data = run.distributions["kinetic_ions/e1_v1_density/f"] assert data.dims == ("t", "e1", "v1") np.testing.assert_allclose(data.v1, np.linspace(-3, 3, NV)) def test_sph_density_views_take_dimensions_from_their_grids(run): - data = run.densities.kinetic_ions.view_0.n_sph + data = run.densities.kinetic_ions.view_0.n assert data.dims == ("t", "e1", "e2", "e3") assert data.shape == (NT, N1, N2, 1) np.testing.assert_allclose(data.e2, np.linspace(0, 1, N2)) @@ -123,8 +127,8 @@ def test_sph_density_views_take_dimensions_from_their_grids(run): def test_orbit_product_keeps_column_semantics(run): data = run.orbits["kinetic_ions"] - assert data.dims == ("t", "marker", "attribute") - assert data.attrs["columns"]["weight"] == 6 + assert data.dims == ("t", "marker", "quantity") + assert list(data.quantity.values) == ["x", "y", "z", "v1", "v2", "v3", "weight", "id"] def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): @@ -267,7 +271,7 @@ def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): def test_info_lists_products_without_loading(run): text = run.info() assert "out.scalars.en_tot" in text - assert "out.kinetic_ions.e1_v1_density.f_binned" in text + assert "out.kinetic_ions.e1_v1_density.f" in text assert "out.kinetic_ions.orbits" in text assert "out.em_fields.E" in text assert run.field_catalog._cache == {}, "listing must not load arrays" diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 5ca3f84f0..6b56a7ca2 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -42,9 +42,9 @@ def close_figures(): def test_products_are_found_by_name(run): assert run["en_tot"].dims == ("t",) assert run["em_fields/E"].dims[:2] == ("t", "component") - assert run["kinetic_ions/e1_v1_density/f_binned"].dims == ("t", "e1", "v1") - assert run["kinetic_ions/view_0/n_sph"].dims == ("t", "e1", "e2", "e3") - assert run["kinetic_ions"].dims == ("t", "marker", "attribute") + assert run["kinetic_ions/e1_v1_density/f"].dims == ("t", "e1", "v1") + assert run["kinetic_ions/view_0/n"].dims == ("t", "e1", "e2", "e3") + assert run["kinetic_ions"].dims == ("t", "marker", "quantity") with pytest.raises(KeyError, match="available products"): run["t"] @@ -83,7 +83,7 @@ def test_scalar_overview_draws_every_scalar_in_one_axes(run): def test_slices_panels_and_viewer_take_keyword_views(run): - name = "kinetic_ions/e1_v1_density/f_binned" + name = "kinetic_ions/e1_v1_density/f" assert run[name].struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" assert len(run[name].struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 viewer = run["em_fields/E"].struphy.plot.viewer(x="e1", y="e2", component=0) @@ -116,7 +116,7 @@ def test_dispersion_rejects_fields_in_seconds(run): def test_selection_keywords_take_positions_values_and_ends(run): - name = "kinetic_ions/e1_v1_density/f_binned" + name = "kinetic_ions/e1_v1_density/f" times = run[name].t.values by_position = run[name].struphy.plot.slice(x="e1", y="v1", t=-1) @@ -132,9 +132,9 @@ def test_selection_keywords_take_positions_values_and_ends(run): def test_products_of_one_species_sit_on_the_output(run): - assert run.kinetic_ions.e1_v1_density.f_binned.dims == ("t", "e1", "v1") - assert run.kinetic_ions.view_0.n_sph.dims == ("t", "e1", "e2", "e3") - assert run.kinetic_ions.orbits.dims == ("t", "marker", "attribute") + assert run.kinetic_ions.e1_v1_density.f.dims == ("t", "e1", "v1") + assert run.kinetic_ions.view_0.n.dims == ("t", "e1", "e2", "e3") + assert run.kinetic_ions.orbits.dims == ("t", "marker", "quantity") assert run.em_fields.E.dims[:2] == ("t", "component") assert {"kinetic_ions", "em_fields"} <= set(dir(run)) with pytest.raises(AttributeError, match="available species"): @@ -142,7 +142,7 @@ def test_products_of_one_species_sit_on_the_output(run): def test_arrays_plot_themselves(run): - phase_space = run.kinetic_ions.e1_v1_density.f_binned + phase_space = run.kinetic_ions.e1_v1_density.f assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" assert len(phase_space.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 assert set(phase_space.struphy.plot.viewer(x="e1", y="v1").sliders) == set() @@ -157,12 +157,12 @@ def test_the_accessor_works_on_derived_arrays(run): def test_products_by_name_and_by_attribute_agree(run): - by_output = run["kinetic_ions/e1_v1_density/f_binned"].struphy.plot.slice(x="e1", y="v1", t="last") - by_attribute = run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.slice(x="e1", y="v1", t="last") + by_output = run["kinetic_ions/e1_v1_density/f"].struphy.plot.slice(x="e1", y="v1", t="last") + by_attribute = run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", t="last") np.testing.assert_allclose(by_output.artists[0].get_array(), by_attribute.artists[0].get_array()) assert by_output.fig._suptitle.get_text() == by_attribute.fig._suptitle.get_text() == run.label def test_selection_rejects_unknown_dimensions(run): with pytest.raises(TypeError, match="not a dimension"): - run.kinetic_ions.e1_v1_density.f_binned.struphy.plot.slice(x="e1", y="v1", time=-1) + run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", time=-1) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index a189c3e27..2297b5666 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -10,11 +10,12 @@ "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, get its output as an autocomplete-friendly `Output`, and make the plots most commonly used to inspect a simulation.\n", "\n", "For a production run you can skip the simulation setup and open its output folder with `struphy.open_output(\"path/to/sim\")` instead." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "1", "metadata": {}, "outputs": [], @@ -54,25 +55,15 @@ "## Create a compact demonstration run\n", "\n", "Post-processing operates on a completed run. The small setup below saves an electric field, a few marker trajectories, scalar diagnostics, and a binned $(\\eta_1,v_1)$ distribution. These are the main output types handled by the plotting interface." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "3", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" - ] - } - ], + "outputs": [], "source": [ "def build_model():\n", " model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", @@ -110,26 +101,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "4", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 40/40 [00:09<00:00, 4.26step/s]\n", - "Raw output: /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo\n" - ] - } - ], + "outputs": [], "source": [ "demo_tmp = tempfile.TemporaryDirectory(prefix=\"struphy_postprocessing_\")\n", "demo_root = demo_tmp.name\n", @@ -164,84 +139,15 @@ "To choose options, call `out.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", "\n", "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `struphy.open_output(path, time_units=\"physical\")` makes `t` itself seconds. Arrays are loaded only when accessed. The simulation that produced them is `out.sim`." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "6", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "Post-processing path /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo\n", - "\n", - "Reading hdf5 data of following species:\n", - "em_fields:\n", - " e_field: \n", - " phi: \n", - "Creation of Struphy Fields done.\n", - "\n", - "Evaluating fields ...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "100%|██████████| 41/41 [00:00<00:00, 142.83it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of 12 marker orbits for kinetic_ions\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "100%|██████████| 41/41 [00:00<00:00, 1353.10it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of distribution functions for kinetic_ions\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "0 starting post-processing of distribution functions for /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo/post_processing/kinetic_data/kinetic_ions ...\n", - "100%|██████████| 1/1 [00:00<00:00, 546.99it/s]\n", - " 0%| | 0/1 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.plot.scalars()" ] @@ -333,26 +232,7 @@ "execution_count": null, "id": "14", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "growth rate: -202872114.0880568\n", - "growth rate: -202872114.0880568\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2QAAAIgCAYAAADqVqoGAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAsTZJREFUeJzs3Qd0VMXbBvAnvffeCBB6CxA6goj0jgVQAVEUQeyK9bP3P1ZUioqioCKCFEFsNAHpJaEFEkJCQkJ67+0774QNSQiQhCS7m31+59yTzd3N3dl7Q9hnZ+Ydo9LS0lIQERERERFRozNu/KckIiIiIiIiwUBGRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGRERERERERawkBGRERERESkJQxkREREREREWsJARkR1tnXrVqxevdogz2BsbCzeeustbTeD6llISAgWL158Q8f46aefcPLkSTQFBw8exPr167XdDL06p7pyzohIfzCQEVGdMZDpXiBjUNRuIDt37hzmzZsHf39/NAXyOmbPno2kpCSttUHfzqkunDMi0i+m2m4AETUNMTEx+P777/Hiiy/ecMjbv38/3N3dcfvtt8PBwaFOjy8pKcEbb7xxxeNfeeUVGBvr92dRKSkpWLBgAR577DE4OzvX+Tj//fcf/vrrr6ve36dPH4wYMaLGxzt69Ki6FrNmzbrmvoZw5swZ/Pjjj+jbty+GDx9e6fm3b9+OJ554Ao3ho48+wt133w0bGxv1vbwp//zzz8vvHzNmDHr06HHFz50/fx6bNm1CUVERbrvtNvj4+NTo+Wp6/Lr+vJubmzqfX3zxBV599VVoQ9VzKhISErB582YkJycjMDAQt956a42PZwjnjIj0i36/KyEinQpk77zzzg0d45lnnsFdd92FCxcuYNmyZejatSvi4+Pr9HgJZK+//nqT/JRaApm8Nvlalbe3N/7v//6v1sfMyspSx7x48WKd2yXh58svv7zuvoYKZNL+qVOnIj09vdLzf/LJJ2gMEqZ++OEHTJo0qdr7pR0ynK0q+ZmOHTvi33//VcFs9OjRCA0NrfXzX+34N/rzd9xxh/r3pQ3VndMVK1agX79+2LZtG6KjozFt2jSMHz9e/ZuvraZ4zohI/7CHjIhqLC8vD7/++isSExNx0003le/PycnB119/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G651/KqSk5PVsDwZaqchr10KY0jAqOlru1pb4uLiVEiRUCbtktciwxhr0oN0reer7pzI8aWgiMzjkjmAN3IsDelllF5LCwsL+Pr6qoIgcoxffvnlisf26dNHBb2Kc/WIiKh6DGRERER0TbJItPSsacKq9JjJUEwJY7fffnulx/7zzz8YOnToNecIEhHRZSzqQURERNfUsmVLFbykx1B636S37JNPPqkUxqRHTuabnT17Fs8++yzDGBFRDbGHjIiIiGokMTERhYWFarhqVfn5+SqUSVEVKSJCREQ1w0BGRERERESkJayySEREREREpCUMZERERERERFrCQEZERERERKQlrLJYR1Jl6tixY2rysqkpTyMRERERkSEqKipSRY86d+4MS0vLWv88k0QdSRjr1atXXX+ciIiIiIiakP3796Nnz561/jkGsjqSnjHNiffy8oI2ZWRkIC4uTpUZNjbmKFRdJev2pKWlwdHRkddJR/Ea6QdeJ93Ha6QfeJ10H6/R1eXk5MDe3h42NjbQtpiYGPTt27c8H9QWA1kdaYYpShjz9fWFNqWnp6O0tFS1hYFMt/+oWllZwdnZmddJR/Ea6QdeJ93Ha6QfeJ10H6/R1WVnZ8PBwQG2trbQtuLiYvW1rtOY2J1CRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGRERERERERawkBGRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGREREREpNe2bNmCs2fPGnwbSD8xkBERERGRXnvsscewYcOGej3mP//8g4iICK22gQwDAxkRERERURVz587Fpk2banxebr31VrRq1YrnkWrNtPY/QkRERETUeEpLS3H8+HHExcXB19cXHTp0qJefkcecOnUKsbGxaN26Nfz9/cuHQObn5yM0NLQ8lPXv3x+Ojo74888/0a5dO3U7JCQERUVFuOWWWzB+/Hg0b968Rsevabt9fHzg4eFR6X7N87u6uqpjSzs7d+4Me3v7Wp+Dq70WERMTo47v5OSErl274sCBA7C1tVXPVVxcrH5Wbvv5+VU6ZnBwMDIzM3HTTTdd97VSGQayJmDuzydwITUL9tbJsDIzgZWZMazNy75qvrdS31e8bVzlsWW3bS1MYWJspO2XRERERPWs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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "t_fit = 2.0 # Struphy time units, like every time coordinate of this run\n", "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", @@ -376,25 +256,15 @@ "## Two-dimensional data\n", "\n", "Choose the displayed dimensions with `x` and `y`, and pick one value for every other dimension by naming it: `t=\"last\"` (or `\"first\"`), `t=-1` for a position, and `t=0.35` for the nearest coordinate value. Arrays can also be sliced beforehand with xarray's `.isel()` and `.sel()`. `coords=\"physical\"` draws on the mapped coordinates instead of the logical ones." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "16", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzwAAAIgCAYAAABAlWy6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAVcdJREFUeJzt3Qd8FNX2wPGT0IuhBJFeDCpKiUoREAsgiqIoWEBBwaeCBQtgVwQUfT4LYm+APLEhKiqoWJAiYgQBg0oVpCO9d5L5f871bf67qbvJ3cxk9vf9fOaT7GTn7rTdzNlz75k4x3EcAQAAAAAfind7BQAAAAAgWgh4AAAAAPgWAQ8AAAAA3yLgAQAAAOBbBDwAAAAAfIuABwAAAIBvEfAAAAAA8C0CHgAAAAC+RcADAAAAwLcIeADk2/fffy8fffRRTO7BDRs2yPDhw91eDVi2cOFCee211wrUxvvvvy+LFi0SP/jll1/ks88+c3s1itQ+9co+A/D/CHgA5BsBj/cCHgIxdwOev/76S+655x6pW7eu+IFux8033yxbt251bR2K2j71wj4DEKp4pscAkC/r1q2Tt99+Wx588MECB1Fz5syRqlWryuWXXy4VKlTI1/PT09Pl0UcfzfL8Rx55ROLji/Z3Pdu3b5cXXnhB7rjjDqlcuXK+25k9e7Z88803Of69VatW0qlTp7Db+/XXX82x6Nu3b67zomHZsmXy3nvvSevWreWCCy4Ief3p06fLXXfdJYVhxIgRcs0110i5cuXMY73ofemllzL+fvHFF0vz5s2zLLdmzRr54osv5OjRo9KtWzepWbNmWK8Xbvv5Xf7YY481+/Pll1+WIUOGiBsy71O1efNm+eqrr2Tbtm2SnJwsHTp0CLu9WNhnAEIV7f/6ADwV8DzxxBMFauPuu++Wq6++WtavXy9jx46VU089VTZt2pSv52vAM2zYMF9+y6oBj26b/sysRo0a8vDDD0fc5t69e02bf//9d77XS4OLN954I8950Qp4dP179eolu3btCnn9kSNHSmHQYOXdd9+Vq666Ktu/63pod6fMdJlGjRrJzJkzTeDTuXNnWbJkScSvn1P7BV3+iiuuMO8vN2S3T9955x1p06aNTJs2TdauXSvXXnutXHrppeY9Hyk/7jMAWZHhARC2gwcPyieffCJbtmyRtm3bZszfv3+/jBo1Sg4fPixDhw418/Rb6qZNm4bd9tKlS803uT/99JOcccYZkpaWZi5q/v3vf2d7wRru8/v37y8NGzaM+Cjv3Lkzox39Zlm/RT7//PNzXebAgQMyceJEs390Xfbs2WOCEr34UbqO+i2+Xpxrt5dLLrlESpcuLba7tI0ZMyZL0KNZjgULFpiMkL6u/tR11ElpoPPss8+arjgaOO7evdvsX9233333nVln7VakF6Cff/65CXBPPvlkufDCC03GTC/UP/30U/P6gXNAL9wzz9NMjwZlui/0G3q9sK9du7a5YA3si/nz55tJs0vffvutmXf99dfnue2a3dNjrYH3f/7znxyfl9trF4Sus75HdP8FVKlSJWPbs7sAXrVqldxwww3mfXXRRReZeRqwaQAajrzat7G8niO6ntq1rH79+nm2GTh++n75+uuvTSCi55wedxv79JRTTjFdD8uWLZvxHm/QoIH5LDjzzDOL5D4DEF0EPADCcujQIRPk6MVLu3bt5KabbpJ9+/bJaaedluMyGgDllfXRLE358uVN16p69eqZC2xVrFgx6d69e47ZgXCfr4OdtX29ENYLSn1epDRrdOutt5rtfvPNN7N9jl6U6UWOtn/uuefKLbfcYoKG008/3QQ8ui90eb2Y1e4uWuxBu7v8+OOPUqlSpYj2VThjeAIBj+M45vV///130/VGX0+7Hc6aNSvXCzFdd82YaADXrFkzqVOnjtkP7du3l+OOO87sdw1yn3vuOXNRGwkNBLULUlxcnJx99tkm+NB11gtW3T69yNX1f/755+W8886TJk2ahN22Bm3nnHOO2f96fkT62tntz9wyVBoo3X///eb3xYsXm/1UvHj4/1o1e6HnZlJSkjz99NOSmJhoAsVwu7QVBg2O9RzVogHhBjx6/J555hkTtK5YsUIeeughmTFjhslkFXSf6nsqWMWKFc3x1EC2qO4zANFFwAMgLHpxqxeLv/32m5QsWdIEQHrxovSb1htvvFE++OCDjG8+lV7Eh0u7ptSqVStknj7W+fl5vmYdAv3n9QJLB6Lr+B296EpISMhzffQiKnhbNEjQCxe9ENOL08w0ENJ9ot8868WZbntg/wT+rlkRzSqUKVPGXJzpGJknn3wy12xEQem4Ku3Wpcct8I24jmfRbfvvf/+b5/IDBgyQPn36mN9vv/128y29diUK7N8TTjjBvIZmYC67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AACAbxHwAAAAAPAtAh4AAAAAvkXAAwAAAMC3CHgAAAAA+BYBDwAAAADfIuABAAAA4FsEPAAitmHDBmncuHG2019//SUzZ840v+/Zs8f63rXZ9pQpU0xbaWlpVtYNkTnvvPNk7NixGY+jed5k135hHf9obxcAIHcEPAAidvjwYfnjjz+kV69e8sEHH4RMNWvWlN27d5u/R+NC0mbbO3fuNG05jmNl3RCZJUuWyNatW/N9bL///nsTSBw4cCCs52du3/bxz2l9ovl+AADkrXgYzwGAbNWqVctc4GV2zjnnyG+//SYJCQnsOYQt0vMm0kAi2udlTuvD+wEA3EWGB4B1CxYskB49esi+fftCug6lpqZKnz59pHXr1tKlSxeZNm1ayHJ6sRjoGnfqqafKJZdcIm+88Yakp6dH9PqB1/vll19MFuqMM86QSy+9VGbPnp3t8/V1C7pehw4dkqeeesp009J2+vXrZ7r3Bdu2bZs8+OCD0rZtW2nZsqUMGjRIduzYkeu25NVuuNsa7r7VdXz44Yfl7LPPNus5dOjQkIxFfrZBLVq0yJwTp59+unTu3DnLPs7uvMlt27Wb2J133ml+1/XQ7briiitC9sn8+fPlyiuvNK85evToLO0Hv25u++7999+Xs846K8v6nnvuufLOO+/kuT7Zve7KlSvN9rRo0ULOPPNMGTx4sAmYAsJ9zwAAwuAAQIT++usv7QPkjBs3Ltu/T5o0yfx9x44d5vH7779vHp944onOe++956SkpDh33HGHU7JkSWfZsmUZyx04cMD57bffzLRgwQLTfu3atZ377rsvx7azE3i94447znnrrbecWbNmOTfffLN5PW038/NsrNddd93lJCUlOZ9++qkzZ84cZ9SoUU7r1q0z/r5582anfv36TuPGjZ3PPvvM+f7775127do5zZo1c9LS0nLclrzaDXdbw9mGv//+26lbt67TvHlz55NPPjFtDR061Bk0aFCBtmH9+vVOhQoVnCuvvNKZMWOG8+GHHzp16tRxihUr5jz99NM5Htvctn337t3O888/b56vf9Pt+vPPP0P2Sb169Zx33nnH+fXXX50tW7bkeF7mte9efPFFJzExMct26XLPPfdcnuuT+XV1P2t7F110kdmHui9POukkp2XLlhn7MdxzEwCQNwIeAPkOeGrVquU0atQoY9ILMpXTheXMmTMz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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.struphy.plot.slice(\n", " x=\"e1\",\n", @@ -411,25 +281,15 @@ "metadata": {}, "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "18", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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fsBXL3oka80qSj9dA0F2IZ7KdpT1unXd6HxWGWDvvfdO+3f9c07BWf2uAuzKc1T20NjDQfWaUdLx1bAjyrMif0fXoO6DW221lS0PlUZFPGYAAAAoAx4AAABCb7fddlO/pt7GjRujpn/11Vd2+lFHHVU0bauttvL23ntv+9F3kZ+zzz47avlDDz202Dz6dOvWzVu3bl3RfAsXLvSaN28ed95HH320aL7x48d7DRo0iDvfww8/7LSvrr+l/dxrr728Hj16FJvvsMMO8woLC1PeTxk3blyJ+zB27NhA99V1HTfccIOd9u233xZbxxlnnOFVqVLFW7BgQdG0ESNGeNWqVSu2zho1angffvhh1PKdOnXydtlll6hpmvfMM88s9lvPPPOM/W7MmDHeypUr4263PjqGJa07lW1L5VzOlJdeesnLyckpti8XX3yx/ffHH3/slFbXXXedTS+Xz9VXX130+1OnTrXrOfroo4tt2/vvv2+/i5w/nlTXcc4559hp/fv397p27erts88+3vXXX1+0L6WRyrUa7/x68cUX4x6Piy66qOh4pHMupnt8EuXfseLtT7r5TMOGDb22bdtGTbvggguKjmXken744QfviSeeKJZu+tSrV89ug8ycObPE8zjRd76PPvrIq1WrVrHvlX+9+uqrUduqaYcffrjXoUOHovl69uzppUPpoI+uwyZNmth03rx5c9H3xx13nF3/n3/+mda2Kq379esXNU3XiOv5cs8990Qtq/tao0aNku7XrbfeWuyc9s2dO9d+17Rp06TnXWkl2o7yWPf06dPtPIMGDSrVb+nY6ji2a9eu2HEfMGBAuR/fSC1btvR23HFHLygV5ZgBAAAgeHRPDQAAkKWWLFlibr31Vvvv7bbbLuo7td5Qq7DnnnvOtpZRK0K1ZnzqqadsN4c+tay78MILbUsztT5TCyyNsffdd9/ZFnm+d99917Z2U6tmtSLTvGp9eOSRR5r169fbeTQepFrkLF++3LZOU6tQtdB64oknbCsZtd7R2JvJuPyW74svvrDzPvvss+brr7+2+6cWd9oPLZPqfqo1mfbh77//tq0qR4wYYdNv2LBhZvfddw90X1NZh9+NZWwrO7Xqe/31121LJL+loVoinnnmmbZlslogaxxErVut7NT95amnnlosHdOhFpHq7lZd4KqFVGQXuGoBH0862+Z6LmeC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Wvlj5rrwz20/T9D1outCQXSNx+nzW1lrHOVEglhHZJAt2UsXQeVJsfwXSvTSSWwPBDpGpTkX0z0+pRFEPuMiMi3US0OytEh0Hpf0nb8vat3qj5MdK9nLDUHRNiqorxeRFDwui231x7x1ETvmbbt27eyLF6Wxww472LJC48aNi5UZ/Osk2UsY2RR8XLNmjU230lIvCQoOR7YE1rH8448/osoEZXl8Nea4WsBrfHb1uqLztaIJ8pgBAAAgWASNAQAAKqkVK1bY/6tr40MPPbSoNdnjjz9ugz+RXV6re1RVoqobXr8ljipWFWD67rvv7N/qxlUtaFTJ2a1bN9t1tk8tGLVedTetQJACECUFB11+y6eAsVqKPfXUU7YFz/Lly23X0qoMP/DAA1PeT+2DWlH++9//tsETte5Vha1anqm1jyqNFRBKZV9Lks461EX1K6+8Yj9qcaxtVJfAkfbbbz8b9NM+qLXnnnvuaad/+OGHNoiqAELv3r1NIkoTtWJWoE1dD/stn7RNsRSw//XXX23LO+1PIkFsWyoSnWdBULehOj/UhfWOO+5oj6MMGzbMflJJq9tuu83861//cvpddePu03mvVoPvvfeeufbaa+161CpNLX7Vta7OZ78785Kksg4FTxXEPP/884uCa7o+HnnkEXtO6pr1z5lUpXKtJjoegwYNskFuP3j76quvmhdeeKFU52K6x6c0gshnXOjYq8viq666ynb5778goHxG+Z5eAogMrOs8njFjhg3ix7aALOm7I444wl4nOp+UX0e+ZKH5df7o3CsvBx10kNlnn33MfffdV3TdBrmtjz32mL0/uWjUqFHU3zoX9RvqLSPdc0kvGem46qWPZ5991t47dG3ohSN/25VnVQT+CzolvViRCvVOcfHFF9thP/TCioLFellN/DJFWR1fvWyhIRV03ulaHDx4sKmogjxmAAAACFgZjJMMAACAgO22225quuVt3Lgx6bxbbbWV17t372LTP/74Y7uOl156yf79008/eVWrVrXTmjVr5m277bZerVq1vIYNG3odO3b02rZtW7TswIED7Xw5OTleixYtvG222cbLzc210/r161c03/3332+n6ZOfn++1a9fOy8vLK5r27bffJt1+19/SfhYUFNj1V6tWzc6n/2u+5s2be0uWLEl5P+W2224r2l7Np/X6y48ZMybQfU11HZs2bbL70LJlS/v9oEGD4q73/PPPL1pH06ZNvcaNGxf9feWVV0bN26lTJ2+XXXaJmvboo48WHYNWrVp59evXt3/vtddexdLhzDPPtNPq1atnj4fW99dff5W47lS2zfVcziR///XR9uo46t89e/a0/9e2uqRVaUyaNMmrUaOGXXeDBg3sOetv0/XXXx81r84Z/e748ePTWsfMmTOjztk2bdp4W2yxRdG0++67L2q9L774ov29F154Iel+pHqtxju/TjvttKjj4Z+7u+66q/3/6NGj0zoXg8i/tb3xPq+88kqJ+5NuPqM0i02vCy64wC6zcuXKYvMfeeSRRevccssti9JNnyFDhkTN26dPn6LzxD+PXb67+uqri9ap7dO+1K5du2javHnziuatUqWK179//7j79sADD9j1vv/++54LpUNsWsg333xT9Nv6/Pnnn2ltq76PvDeVlp+/vf7668W+e+ONN4rOmyZNmtj5WrduXTRN16fvhx9+sNeP5tHxVBpUr17d/q17699//512mqayHWW5brnkkku8mjVrRu1POr+rc27nnXe2aaS8UL+pafr9Dh06eGvXrvXK8vi+++67drp+s6S8Yv369RX6mAEAACDzCBoDAABU0qCxPP/8816jRo2KKsL9YJKCg5GV7FOnTvXOOOMMr27dulEB1XPOOadYpd9rr73mde7cOaoyXoGlO+64w1u1alXS7Xf9LX8/FcDceuuti+bt3r27XUck1/30PfHEEzZY6s+vys0LL7zQW7ZsWaD7ms46Lr/88qL5YitiI4PLN9xwgw1O+vMqiKOAeGFhYdS88QJVOs8U4PQrzOvUqePddddd3jPPPFMsaDx79mxvhx12iNr+hQsXlrjuVLYtG4LGqsQ/99xzi15YULDhmmuusedcbNA4UVqVloKhCqz661VgUQHi2DT1A9eRxzCVdaxbt8679957ve222y5qPxQgHD58eLF1al59/9RTTzntRyrXarzzS9s3YMCAqONx7bXXenfeeaf9+7vvvkvrXAwi/y7po5c0StqfdPOZVIPGSreLL744KjCqwP0jjzxSbF4FI7XuyO1x+c7PW/UygP+dXkzRuTN06FBvw4YNTkHjK664wi773nvveaUJGstRRx0VN2icyrYGHTTWuacg9eGHH17sO21TonNp8uTJUfN/+eWX3k477VT0vdL1kEMOsXlR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RzrzHXzwwUW/v/3229vAdYcOHQJ7/nbdDr3w2bdvXxs079Gjh3nxxRdtXq+gC4Dsc//995t777233H/3wQcftM+/eslPLyLq+fzII48MbNnevXsXy7s137vvvmuaNGlSNN/bb79tG8F89dVX5uuvvzZffPFFoPupvFF55Msvv2z22GMPU1hYaPPO9957L2q+Tz/91Jaf9aKS9k0vlqucq20CgIzwgBCpXbu29+ijj5brb/bs2dM76KCDiv7esGGDt+2223pnnHFGYMsGPV867rvvPu+5554r1/0BEE6nnnqq17t373L9zfnz53srVqyw/9Zvp/L7P/74o1e1alXv9ddfj5q+aNGitOY799xzvebNm3vr1q0rmvbQQw8p8mG3s7Tuvfder1atWt7ixYuLpl1++eVekyZNon6za9eu3j777BO17HnnnWfz08LCwlJvB4DyNWLECJuPrFy5MhTb8dVXX5XZsvHm27Rpk1dQUOCddNJJUfOuWbOmKP8vjRkzZnhVqlTxhg0bVjTtiy++sNsxevToomm33nqrV7NmTW/hwoVF0z788EMvJyfH++GHH0q9HQDKn679Z555plx/c6+99vLOOeectJbt0qWLd/jhhxf9rfJfq1atvPPPPz+t+WIpT61fv7536aWXprV96WzvTz/9ZPPRp556qmja6tWrvWbNmnk33nhjINsBoHwdddRRXr9+/cr1N7/++msvNzfXe//99xM+swe5rPJMPZ+fdtppUdPnzZvnrVq1yv57l112CTwtbr75Zi8/P99bunRp0TTdV1q2bOlt3ry5aFr79u29vn37Ri178sknezvuuGOg2wMArmhpjNBQiwW1zFArMbXS0EctWcuS3szVW7UnnXRS0TS1CDvuuOPMG2+8kbCFhuuyQc8X2dpXLSbUukJdBupNtES+/PLLoi76ymN/AITT448/bltqTJkypSivVZfRZU1dLKXbhd7QoUPNtttua4466qio6erOOZ351O1e69ato7oWVGsN/7tIanFx9913254wLrroIpuXJqP0POCAA6LeYlaeqS6x1TOD6H73/fffm/333z9qWf2trqoT5dcAwkdd0fstNZRfKG+9+uqrM7ItaoUmJXXLF8Sy8eZ7//33bQuJ66+/PmpedVsam/9v2rTJtlBTl6vqUlBdom7evDnhb44YMcJUrVrVHH300UXT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bT/xfPzxx95xxx3nLVq0qGja119/7eXk5Hhvv/120bT777/fq1atmjd58uSU59NvFxQUeCtWrCia9vjjj3sqFn3xxRdF00466SRvv/3281avXl00bfPmzTavXbZsWYn7N3LkSLsdn332mf1706ZN3v777+9169Ytar4JEyZ4c+bMKfr7p59+8ho2bOjdcccdJa4bQHh99dVXNh/59ddfy+03P/jgA2/+/PlR09588027HcOHDy+apjxLeavyp1SXdZ1v/fr1XuvWrb0zzjjDKywstNOmTZvm1ahRI6qM+Prrr9u8burUqVHrnDhxojd27NgS91X7oOUuu+yyomlDhw71qlevHpXmyrPff//9or9XrVpl8/1evXrZPBxA9mnevLl31113ldvvKU/573//GzVtxIgRtnwXux033nijd/3110dNU37bsWNHm//IzJkzvVq1atlyaarz/fLLL0VlSt/vv//ubb311t4RRxxRNG3JkiVegwYNvIcffjhqXk1/+eWXE+6v6/a+88473oYNG4r+vueee+x806dPT7h+AOF03nnn2fJReVLeqjLkJ598ElXfqvKi8h6fyqzKmyKfu12Xjay7bNy4cVS+VZJddtnF69evX9zvDj/8cK9v377e2rVri6bpGf/ZZ5/11q1bV+I6X3rpJa9KlSre+PHj7d/ajt12283be++9o+ZTPUZkWVtl4rp163oPPvhg0u0GgLJA0BihctNNN3l16tSxN2oVDubNm1fmv6lga8+ePb0WLVrY361fv753zDHHeBs3boyaT5VNO+64Y1rLBj2fChAdOnTw2rVr5x122GG20KGHxkceeaRUQeOgtxNAOH377bdezZo1vT333NPmta+99lqZ/6YegvRb+jRt2tR+/L8jA8SPPvqofRCMfejTA5O2ed9997V5nh6inn766WK/4zKfKt+23357+wCphz89IOpBM/alpZUrV3rHHnusrYA78MADvYMOOshr2bKld+KJJ0Y9MMZz3XXX2e3o06ePrYRr06aNN2PGjKh5FMju3Lmz3VYFp2vXrm2X84MtALKLgqadOnWy5TOVi2JfNiwLH374of3NHj162DKZyqp6kU9BjMi8ZOHChTZvvfvuu1Ne1nU+/2UYlQ+VtynPVB6sCrvYMuJ9993n1atXz+bTKstq/Xqx5ocffki4v3qxSOVOlUOVdypY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119/bebMmWM6duyYVef5999/b8/Xxo0bm7333ttsscUWTsvp/Pjmm2/M7NmzTYMGDcyuu+5q6tWrZ7LFxIkT7adbt25mp512SulYjx071u73NttsY3r16mVyc3PLPf1Lu2wmlSYNFy1aZL777juzceNGs8MOO5i2bduabLF+/Xp7P/zzzz9N586dUzrvfPPmzTMffvih2Xrrrc0BBxxgssGKFSvMmDFjzJo1a0yPHj1SPmbKm7/99lvTsGFDe0/KlvN88eLF5vPPP7d55V577WWaNm2a0j7/8MMP9t877rhjVp3n2m+do3Xr1jVHHHFESsv+8ssv9j5cu3Zts++++9p1pEt5hdI/JyfHpn+TJk3KZdlMSzcN165da6+z+fPn23xZ12pp7m3lbdKkSbbcrWOl+2H16tVTWl7X6QsvvGCfW04//XRTpUoVkw330q+++sqWPdu0aWPvpTpnU8mbVd7XvUnl9VTyqLDcS7fffnubR7patmyZLbsuWbLEptkuu+ySUppler8/+OADu9/HH3+8qVOnjvOyy5cvt/dhXecqr+saz0T6l2bZTCtNGv74449m6tSpthyjvDWbnpcWLlxo74fKE3U/1DNHqpRuKtfonqTrLhvMmDHDPm/oHqp6gVSut82bN9vnHJXZdZ7rky0mTJhgpk2bZu8HupdWq1Yt5bqQRo0a2WsklbqUTFP5R+Vu3RNSOV7ab9V7/fbbb/bc7tmzZ6nuKemmf2mXzaTSpOGCBQvsdap16H5SmntbeVu3bp3NG1UeUZ2GPqnS9fbJJ5/Y/e7du7fJBiqHab+1/zrWrVu3Tjlv1rmu8r7KrqmW9zNF5+oXX3xh76W6PpVPprLPKkdoWdUzp5pmmd7vjz76yO7voYcemtKy06dPt9e3yky6D+vZFshKXgU2d+5cb7vttvM6derknXjiiV6DBg28/v37e5s3by7TZTPtm2++8Zo0aeL17NnTO/roo71atWp5gwYNSrrcxIkTvW7dutl9PfbYY71evXp59erV855++mkvG7z66qte7dq1vYMOOsjr06eP3e9hw4altI5ff/3Vy8vL83RpPPHEE142uP32272aNWt6Rx11lLf77rt7DRs29L788kunZX/55Rdvhx128LbddlvvpJNO8nbbbTe7nrDTdXjKKad49evX90444QSvc+fOdh9mz56ddNk///zT69Kli9e8eXPv+OOP97p3727XM3LkSC/s3nzzTXu8tP05OTnerbfe6rzsihUrvD322MPbeuutbV6m/+vvlStXlmv6l2bZTEs3DdesWWPPtS222MLmT4cffri9Zk8//XRvw4YNXtjp2OgY6VjpmOnY6Rimcj/UvEornbfKn7OB8lHlp8pXlS/qmCm/daHjetppp3n5+fl2WR3zHXfc0Zs1a5YXdi+99JK9fx588MHeIYccYv+taS4GDhxoz/PDDjvMfvTviy++2As73fu1vVtttZXXokULe6xSccstt9h0UplL99HGjRt748aNS2tbVG7RunSdKL9QuUblm7JeNtPSTcMHHnjAa9Sokb03Kp9t2bKlPX7Tp0/3wk75ou4lKnfrWUPPHHr2+O2331Jaz5AhQ2zeqrLr2rVrvbBbvny5fcZo1aqV3X9dc3vvvbe3atUqp+Wfe+45+3yy77772uXbt2/vDR8+3As75f9t2rSx56rupbo/nHHGGV5hYWHSZd966y07f48ePex53qxZM/vM9tdff3lhp3uAytwqQ+gcTaW899lnn9nrY8899/SOPPJIex++8847yz39S7NspqWbhmPHjrX7q7z4uOOOs+ee1uNaHsi0Z5991u7roYce6h144IH2fvjGG2+ktI6pU6fa+5LO22zZ7+uuu85u8zHHHOPtuuuuXtOmTb3vvvvOeX87duzodejQwdYL7LLLLt6pp57qhd2mTZvsOaoyu+6lBQUFdh9+//33pMsuXLjQ23777e19SHnrzjvvbNfz4YcfemH3yiuv2HJD165d7Tl69913Oy+7bNkye360bt3a3kdV/tU9dfXq1eWa/qVZNtPSTUPVJ6isq2ckPWfp+aNGjRreOeecY9Mj7GbOnGn3WWVtXTMqjw0YMCCl+6H2U/mLyq7ZUPcoY8aMsXUge+21l3fEEUfY+8u9997rtOy6devsOaLldez79etn6/MUcwi7p556yu5r3759vQMOOMCrU6eOLZO60Hmhc1vppWfT6tWre9dcc40XdtOmTbP1IHqmVPlVz6WpuOqqq2yZQzEVlZ20jkmTJpXZ9gJlqUIHjffff397I/cr5VWBpEzrkUceKdNlM2njxo32oVY3Jd9HH31kC5KjR49OuOyjjz5qCy0qAEVW4lWpUsUWDsJMBX49IEVW6KsSTYUx18o3Vd7pgfrcc8/N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hJn2O16LEpf91oOQAh8K4mSb0hxvVVaOHz/ePP/887YloIwcOdIGUvfee297gw+j0l6ffiWt3nRTixVVNuphOswtBUoryDytMuetQW27WvWrolcPTGF/Iac0+638RS019FZttr2QUdq8VW+fq7WuAlhaRg+JeqtWlW96YKqI+63vVVHg31tE/1YAXd9V1Mq3IMtdpVlXkNtR3oLadt3PVUmha+3WW281FTVv9YMaesmvR48eJpuUZr+Vn8iXX35pWyepJZheTlFgQ+VY9Zaj/KailiFUVlWPV6po1wtJCuz4aVIRBZ23VtayaxBpqPWceuqptvJbL8GFuReH0u63yuZa9vrrrzfZprRl16+++sq2MNZ9VF566SVz4okn2noBtZILo9Jen/pe9xL13KBegdRCVC3pqBco+/TP9rw1qG3XS2933323fdE6toViRdrvL774wjzwwANm7NixUS1XK0Peqhf+lM+oNzKtR4FFPasoiP7www+bilovoN721Lpc9QJKB61PdUL6f2xPJhVFNj+PA/GEu8RfCmptKwoGRr79q791EaswWBbLZpq2PV43bdp2f79c6O1avWH52muv2Tf5w0779ttvv6W838q477jjDnPSSSfZApvoLTm/UkoFGj0kh5X2TfuYzvFWCx51g6N99ylQrDez1d1fWIPGehtPN93Y/fb/TrbfRx55pH0JQl2S+5UeQ4cOtQ/IqoTUG/QVTWSelk6aBZX+pT12FSENdW3179/fnHHGGVnRQihyvyPfHNbfOpaJ3o5V3qquidSFlz6iNyzVKufJJ5+0b/RvueWWpiLmrf6LV/4Dkd7I1RvG7777rgkz7dsff/wRd7+bNWuWcNkhQ4bYSg8FNfyWgupCVoENvV17xRVXmIqoNOdKKutK1mKhNMtWhDTUixkHHHCAvb9/8MEHoQ0e+vQsoWeKdO4pevFIvaSoVZjyUtFLcPLss8/arr3D+kJSae6lft6q8poq30RpqLz2lVdesWW4sAbRI/c7srcq/a2yqHr+KYnun4cffrh58MEHzXnnnWenqcXGXnvtZY4++mgb6KmIgs5b003/0ixbUdJQ551at6tVVNiDGsn2O1GvQKoD0DOh8hS17vd7hRC9AKnKYAVRs2m/9WLNihUrnMqu2j8/YCx6Iefcc881n3zySWiDxn69XLrPZ6rvUPlW9QL+y4133nmnfYlegeSwl58yXS9QmvQv7bGrCGmoVu2nnXaa7UnkhhtuMGEXud+RPY3pb/V6o+e9ktx+++32mVBDlekjfg+PKsvqWgxrb55B1Aucc845RcFE5Tn9+vWz9UJhVppnS9VzPfroo7ahTsuWLe00/Vs9leja98uzFU28NFPMQeWLMOdpQEkq5usd/xsLTVQAjKS/1Qd/5Hh9QS6badp2dR/ljzkUue3+fiWjt9zuv/9+2wJVlRTZQPum7oQi397RQ5IqWhLtt27cKqjpoV8Vbvr443GoxYI/jlVYad9iz1O1tNGLA8mOtwptetMr9k0oVbjqjbCw0jZrTKp416ck2m+1ztGbbnoQjHxLXsEcVcDpzceKSN3jqiAfL800Xd+XR/qXZtmKkIY6v1QZo0rexx57LO5biGGT6H6oFywSdYuqCiZVLvp5qz4KGKubcv07zONQar9V4F+wYEHK91LlrRLbQ0fY81bRvum+GdnCQv9WxVmy/VbrI3VrGNm1rK53TdN3FVW8+7BaqqiLtlTzND/9I8e50pvZKt8kW1dpls32NFSFuMa5VflXveOEtdIpkoLa6mY4nfuhuopTS2O9oOHnrf74eWoplWhc4EzTOPEqf8Xbb+WRsWOvxu63Wi3Ey1slzPlronupgliJWm4qKKznG7306NNLrSrLqothlV8rIqWZKpRju/NL5Zk2cl3+sqmmf2mWrQhpqBe+nn76afPqq6/aYSiygfZN49RG0nWiF9kS7beChnqpU/dOP2/1ux7VsmEeY7Wke6m68Ne+u5Rd4/Usp2e3MOetemlDL1Clcy9VOV+96ikvjewNR/UC2mf/ZayKRi/0an/jpZnKJqrvLI/0L82yFSEN1UWx6gRUntNLYdkg0f1QaZKo9ej+++9v85nIegGV8/Vijl9HEOb91n3Uf4moMtULxB5rbbPqyV3qBbp3714UMPbXp/qjylYvoHoUlePDnKcBJfIqsE6dOnnHHHNM0d/Lly/3GjVq5F133XVR87355pveqFGj0lo2bFasWOHl5+d7N9xwQ9G0GTNmeLm5ud4LL7wQNe8zzzzjjR07NmrakCFDvJycHPtdNpkyZYrd7ldeeaVo2ttvv62aU2/ixIlRx/GJJ57wpk2bVuK6/vzzT7uc5gu7V1991e639t932223eXXq1PH+/vvvomlz5syx+7No0aKiaZ9++qndz3HjxhVN++mnn+y58uyzz3phNmjQIK9Bgwbe0qVLi6adcMIJXocOHaLmmzRpkt3v9evXF01r1qyZ179//6j53nrrLZsWSpNsUaVKFe/WW2+N+93o0aO9F198MWrakUce6e20007e5s2b7d/6v/4+6qijyjX9XZcNI9c0jJf+yofq1atn7yubNm3yskn79u3tMfLp2NWvX9+78cYbo+Z74403vA8++CDhunbbbTevT58+Xtgp/1Q+qvw09j7z2muvRc2rc/ybb74p+nvdunVe06ZNvauuuqpomo65yhWHH364F2Y//PCD3UcdS5/+rWk//vhj0bRly5bZ/f7555+Lpl144YW2nKRySOQ9t2HDhva7bHHcccd5O+64Y9zvZs+ebff7jz/+KJqma133zchyxc033+zl5eXZ/U/FhAkT7L1I5RefyjVav+7PkeentkPlu1SXDSPXNIyX/qtXr/Z23313r2XLlvb7bHLttdfaayZyH3WP6Ny5c9R8Orba740bN5a4rvvuu88e/7Vr13phd9hhh3ldu3Ytupcqf9Q+69qL9Mknn3gvv/xy1LSzzjrLlhci00LPZrVr17b5UlgVFhZ62267rXfyyScXTVuyZIktF8SW5XSP+fDDD6PSQcf2/fffj5rv6KOP9lq0aOFli5deesnuR7zrVOmjc3z8+PFRz2O1atXy7rzzzqJp33//vV3HiBEjyi39U1k2bFzTMF76yy233GKfNyKfr7OB9kP7+N577xVNGz58uL3PTJ8+Peo4ar9/+eWXEtel81Xr0vkbdk8//bRXtWpVb9asWUXTrrnmGvvcpXulT/ur/db++959991iZb1vv/3W7vvrr7/uhdmVV15py92R5c8jjjiiWFnuu+++s/sd+Rym55kzzzwzaj7dd7TfX3/9tZcNdD/U9t59991xv//4449tnVGkQw45xNtll13ste+vo2PHjsXqSMo6/V2XDSPXNIyX/npu1HOm5vXLQtlA27rNNtt4p59+etR9pm7dut4dd9wRNa/2WfueiMqC6dRFlbfFixd7NWvW9O69995iz10jR44smqZzW+e4znXfqlWr7LNwZL3Jhg0bbLni+OOP98JMsQLtY2SZ6LnnnrPlgsj7ps4B7Xfkvee0006zZdQ1a9ZEzafz/uqrr/ayheqtVH8Vj57Dtd+R9e6PP/64V61aNVv/7rviiivs8142PKMBsSp00PjLL7+0D0qnnHKK98ADD9ib0vbbb1+sEk+Fkt69e6e1bBgpOFy9enXv0ksvtTc23dgPPvjgYgWSLbbYwrvggguK/tbDox4W9tprL5v5RX5UkZwNlW+qYFRFox7iFTy/7LLLouaZOXOmvfE9+uijFSJorEKqghBbb721fVD417/+ZY/rk08+GTWfjq326Ysvvoiafu6559obmNJMwZHmzZvHPVfCRg8Wum67dOniDR061BZKVJD77LPPoubTeaD9jgxQDhs2zD5QH3vssfba1sOKzhs9oPiF/rBShbp/TariQ8de/44NUKridauttoqa9uuvv9qA+UEHHeQ9+OCD3oEHHmj/1vTyTH/XZcPINQ1j018BjsaNG9tr7ZFHHonKWyMDPGGlY6NjpGOlY6Zjp2MY+YAvCooqTSpC0FiUjyo/Vb6q/FX5rK652HxC5/jll18eNe2dd97xatSo4Q0YMMAGc/bcc0+bv0Y+TIWV8kRVhquyWB/9W9Ni86LY++Rvv/1m91FlpbvuustWUisQpGnz5s3zwkyBfv+a7N69uw1A+n9HBigVTIh92Ur3y0MPPdRr1aqVd88999hzQWWwdF++UrlF5Rfln37gVOWbSJMnT7bbEfuCn8uyYeSahvHSX4EzTdNLGrFl18hK8jDSM4WuFz1jqDyiZw49e+gZJJIqmrSPK1eurBBBY1U0NWnSxFa46l66//7723wisoJFVJGocyKSXn5s27atfVFAL7qec845Np9WsCTs9EKZ7gtnnHGGvZfqPrrzzjvbCsXYF7Ui75O65/Tt29de26po07mitFFZNjaoHkaqUNX1ePbZZ9tzdPDgwcUClH7QI/JlK3nsscfs8dU9SPdhVUCmW8GcbvqnsmwYuaRhvPTX85KmqVwXm7eG/UUkufjii22w9N///rfNQ1VRrRdXY19u1T7GvlifrUFjBSwOOOAAm0eqDmjgwIH2Xhq77dpf7ZP2P9JJJ51kn2uUZir/6dlF99iw03NmQUGBLb8pf9R+6EWiyLKC/4JR7H3yqaeesgGQE0880S6r8ofOlbAHc/yXWXU96hrXfumlM/0dG6Ds16+fPSci6eUJPZeq7KX7sOpC9eyqsnx5pr/rsmHkmoax6b9gwQKbNylY/p///Ccqb9XLG2GnQLDuKXqJT/dDlWO7detWrMytfda+V4SgsTz88MO2HKD7pJ5zt9xyy2IvO+rc1jke29BMzy9KM9W9quzaq1cvey9O53orb+eff74NequuWPdQXZ+6P0TyXzCKvNeo3l3Xh46xyh56qUDlK5XrFYQPMz1v+dfkDjvsYAP8/t+RAWI9h2u/9VweWZ5SXqBldKxVFtF9ODZfBrJFePtTCoDG81K/+RqTRl0CqLshdUUcO86ZuvuKHX/Bddkw0hi16h5E4xGri7pbbrnFjkkT213I6aefbnr16hXV1Zn2U2K749lqq61sF5Nhdtttt5m9997bvP/++7Z7Ro1RfPDBB0fNozGqzzzzTNs9XknUVYjmSTTmUVioe1uNbaex3NRFnbpO/+KLL2xXIJE05oT2KXZMSo0zofFDNFaRjv9DDz1ku88Ne7e56p5Q+6ux+9T9h7o2UvdSkd2iSpcuXex+q2stn8aT1Zh3b775pu2yS2NqvPzyy8XOlTBStzj+tanrV/S3xsfQNR7ZLfDWW29drFtHpdEzzzxju2zTPMrfGjduXK7p77psGLmmYWz6q1s4dX0msV3bqfsq/7uw2nPPPe1+Dxs2zF4zAwYMsPfD2KEa1J2bPz5VSZTfZMN9VHTu6r73+uuvm99++80MHjzYHHfcccXyR823yy67FNtPdWn40ksv2eEijj32WHPyySeHeuxDn8Zy0zmscWG1r7q/xHZJqbGsY++T6n5K54f2WeUmLXvxxRfbvCnRGFdhoGvUz1t1zPXx/1Za+Ne48gDttz+eqqhspbEedR/RMjq/1RW9xmxKx7333mv2228/89FHH9k01PmnLt0iKc/XdqiLr1SXDSPXNIyX/joHNU1dsPpjo0XmSWHOb5QfaH/9+6G6q9azh/4faeedd7b7qHJaSTp37mznCXNXuZH3Pe2n9ltdqmssapXZNfZ7JJ3LsWUDdT2uvPW5556z+Yzutd99953d/7DbZ599iu6l6jZeY4XqXhrbZeExxxwTdY7rWtb1MWrUKHu+KJ9VOVZ5dSrdiGaKzm117Ss6R/VvfZSP+c8rygP0XeyY1BpbVl07qsyu+/A999xj76flmf6pLBtGLmkYL/1VxtO0ePUCOu9U1xBmGptYeYuecXUNvfXWW6Z3797FxprUPiZ6BtFzi+bJhmtN3eUqnxg+fLgdYkvbriELdtppp6j5tL/ap8gxSUXPNMpr1G2uzu2nnnrKlmfDTmVSDT+le4qezzTMge4xKjNE0pA5sfdJ1X2pPkznh/JW5UuqW9G5E3YLFy4sujYjr1XlX8rHfCoHxl6vKj9G3odVD6Lyfuw5Udbp77psGLmmYWz6b9iwoWjs8Nih8DTcVZ8+fUyYqWym4VGUz+iaufDCC82pp54aVd8jus+oDjkRDYUYW/YLK407rfK47qXz5s0z9913X9R1Jjq3dY7rXI+kbshV/6zzY86cOba+XvUCYX8+locfftgOAaThf3SPGTlypC0TRdJzcux9UveZX375xZbvVebTsldeeaWtFwjzcJ8SOTyBX071/z7kkEOK6ruUB2i/I8cq1jmgOhT/PqxnPf0/7LEUoCQ5ihyX+C0AAAAAAAAAAAAAoEIreaR6AAAAAAAAAAAAAECFR9AYAAAAAAAAAAAAACoxgsYAAAAAAAAAAAAAUIkRNAYAAAAAAAAAAACASoygMQAAAAAAAAAAAABUYgSNAQAAAAAAAAAAAKASI2gMAAAAAAAAAAAAAJUYQWMAAAAAAAAAAAAAqMQIGgMAAAAAAAAAAABAJUbQGAAAAAAAAAAAAAAqMYLGAAAAAAAAAAAAAFCJETQGysCoUaPMwIEDzS+//GJOO+00s/POO5ujjjrKzJo1i/QGgDT99ddf5qSTTjJTpkwx//73v80uu+xi9t13X/Pqq6+SpgBQCtddd53NS9944w1zwAEHmB49ephrrrnGbNq0iXQFgDS9/fbb5l//+peZPn26Ofnkk229wDHHHGPmzp1LmgJAmn7//XdbL6C8ddCgQaZ79+5mv/32M++88w5pCgAByPE8zwtiRQD+3xlnnGE++ugj07x5c9O/f39To0YNc9NNN5kOHTqYTz/9lKQCgDRfyOnTp4/p1q2bDRZ37NjRDBs2zIwZM8YsXLjQNGnShHQFgBRt3rzZ1K1b12yzzTa2rKp8durUqebee+81TzzxhDnrrLNIUwBIw4knnmi++uor07RpU1svUK1aNXPjjTearl27mg8++IA0BYA06EXH4447zr5E3rt3b9OuXTvz9NNPm2+++cb8+eefJi8vj3QFgFKoWpqFAcQ3ceJEU6tWLfPf//63qLCyaNEi8+ijjxbN8+uvv5rCwkL77zZt2pjcXBr+A0CyvFV55T333GP22msvO00B5O23396+Zayg8R9//GFWrFhhv9PfPDACQGLTpk0za9euNQcffLDNX31vvfWW/U5Wr15tX86R2rVr2xcjAQDJy6516tSxLzgq75TffvvNvPTSS0XzqDcyvy1H27ZtTU5ODskKAEnyVr2E88ADD9jecUQvPvbs2dPmqV26dLF1sKtWrbLfNWvWzObFAAA3RKmAgK1fv962zrjsssuighVqxRHZCu6II44wBx10kH0jzg9wAAASPxzus88+RQFjP28VP39VyzjlrTvttBPdVgNAChVv1157bdT0yLLruHHjbN662267mQEDBpCuAJCEghUzZ840V1xxRVHAODZvFfXucOCBB9p6AdUlAACSl10POeSQooCxn7eKn7/edttttuy6ww47mHfffZckBYAUEDQGAjZ58mSzceNGG9iI9NNPP5nOnTsX/f3DDz+Yn3/+mfQHgBSDxrF5q4YAUMsMufPOO+148hrTCADglrfuuOOOpkGDBlHBDrWG88uuylOVtyqPBQAk9/3339uexZLVC6hOQHUDAAA3kyZNipu35ufnm6222sr+/eCDD9qya69evUhWAEgRQWMgYBMmTLDdnmy33XbFKuTURQoAIHV///23mTt3rtl5552L5a3qnrpqVUbcAIB0y66xeasf7KDsCgDp560NGzY0rVq1KhbsIG8FgPSoTuCvv/6KWy9A3goAwSBoDARMBRV1ixo5FlFJwQ4AgHvFm8R7OCRvBYD0aBxNtXCLl7c2bdrUbLnlliQtAKQhXgBjwYIFdpxNyq4AkH7eqvpW1bvGTidvBYBgEDQGAhavoKJpwltvAJB+3qrghYIYsa3hyFsBID0zZswwK1eupLUGAJRTvYCCHRoSAACQXt6qMeDr1q1bNG3Tpk12qEDqBQAgGASNgYD17t3bHHvsscUKNVtvvbXtngoAkDp17XfddddFTfv111/N0qVLeaMYANKkSraBAweaHXbYIWo6rTUAoHQOOuggc+SRRyYNdgAA3G277bbmiiuuiJo2depUs27dOuoFACAgDAAIBGzw4MHFpsUbt+j33383q1evtv+ePXu2adSokWnZsiXHAwDiOP744+PmrRrLWGMa+5YtW2bHOFL++ueff5pffvnFBpyrVatGugJAjE6dOpn7778/atr69evNtGnTzKBBg4qmbdy40Q618scff5g1a9bYvFVl1/z8fNIUAOK4++67neoF5s2bZ1+C9F+IzMvLMy1atCBNASCOU089NW7eWqtWLdO+ffuoYQL1Wbt2rVm8eLEtu26zzTamSpUqpCsAJEFLY6AcxGutcdFFF5k+ffqYtm3bmmOOOcacc845HAsASDFv7dixo6lRo0bRtFdeecW27NDLOE899ZT9tyrjAABufvzxR9sCObLsqnxU+enjjz9ufvvtN/tv5bcAgNLVC6geQC2SVS9w2GGH2XoCAEBqeau6/c/N/f8wx3PPPWfLqwoYP/jgg/bfeqkcAJAcLY2BMrZ582bTuXNns99++0VNHzFiBGkPAKWgrv1OOeWUYhVvvIQDAOlbtWqVfaFRrTF8bdq0sS00AADpUS8OChjvu+++UdNHjRpFkgJAKdSvX9/0798/atqll15qPwCA1OV4nuelsRwAAAAAAAAAAAAAoAKge2oAAAAAAAAAAAAAqMQIGgMAAAAAAAAAAABAJUbQGAAAAAAAAAAAAAAqMYLGAAAAAAAAAAAAAFCJETQGAAAAAAAAAAAAgEqMoDEAAAAAAAAAAAAAVGIEjQEAAAAAAAAAAACgEiNoDAAAAAAAAAAAAACVGEFjAAAAAAAAAAAAAKjECBoDAAAAAAAAAAAAQCVG0BgAAAAAAAAAAAAATOX1f40M0PY+D9nBAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.struphy.plot.panels(\n", " x=\"e1\",\n", @@ -448,25 +308,15 @@ "## Interactive plots\n", "\n", "`.struphy.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "20", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAt0AAAIWCAYAAACLGDGFAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAWKlJREFUeJzt3Qd8FNX2wPGTUKQmoXcR6SAdQVBUFHgqUkSwoaIoduWJiqhPBSwgCIr6twsoolhBURFBRUCqEBWN9CJIJ5DQS3b+n3Pf281uenZ2srOb39fPuOzszN3ZmU1y9uy598ZYlmUJAAAAAMfEOtc0AAAAAIJuAAAAoACQ6QYAAAAcRtANAAAAOIygGwAAAHAYQTcAAADgMIJuAAAAwGEE3QAAAIDDCLoBAAAAhxF0AxHglVdekX/++UcKo2+//VZ++umncB8GQuyDDz6QVatWBb3/oUOH5NVXX5VoMWnSJNmzZ09YjyHSzqkbzhmQHwTdQAT4z3/+I5s2bZLCaNq0aTJz5kxxGz4M2PPSSy/J0qVLg97/+eefl7Vr10q02LZtm4wcOTKsxxBp59QN5wzIj6L52hpA2H3zzTdStmxZ6dSpU9BtHDlyxASymiVq3769nH322UFvv3LlSvnhhx8Ctm/ZsqV06dJFIt2PP/4oycnJcuWVV2Z6LCUlRU6dOpWndt566y2zfXZuvfVWSUhIyPNxvf/++9KqVStp2rRpjuuc8Nlnn5kPgLfddpvExcUFPH+jRo2kbdu24jR9P06YMEF++eUX37rvv/9eEhMTzb9jY2NlyJAhWe67ZMkS856tV6+edOvWLc/Pmdf2g93/jjvukDPOOEOefPJJqVixohS0rM6p0m8j9JyVKVPG/ExXqlQpz21G+zkD8otMNxCBX8t//fXXQe+/b98+E5xpVmvFihXyr3/9S4YPHx709vPnz5eXX35Zdu7c6VtyCjAjyRdffGEC5qxcffXVcvnll+epHf2w4j03GrS++OKLAecrLS0tX8el+y9fvjzXdU7Q8/HQQw/JqFGjMj3/vHnzpCBMnz7dBM1169b1rTt48KA5l4sWLZKhQ4dm2ufEiRPSt29f6devnwksX3vtNbnxxhvz/Jy5tW93fw1mO3bsKFOnTpVwyOqc9uzZU2644QbzvnrvvfekTp068t133+W5zWg/Z0B+kekGXGjLli0my1OuXLmAjLH+8fnrr79k+/btJgguUaKE3HPPPflqW4MlzVppW8WKFTP10hdddJEJQM4888ygtq9fv745nmBoe/pHPSYmRqpVqyadO3c2tznRTKtmofX8XHzxxSYguOKKK6RGjRrm8d27d5vHtUa1Q4cO0qRJE3GivKRkyZJywQUX+Nbp82nWf8eOHdKiRQs555xzzPpHH300oFRIg1Pv+VqwYIEps9AMtf5bM3aXXHKJeY16X8+LPsfpp59utv/qq69k165d5vn37t0r1atXNxnnjOuuu+4637nQc6wfhPRc+GfC9bzptxaHDx82H6guvPBCadiwYa6vXd+TmhXVTGPt2rWz3S6n57Zj7ty5Jtjy17t3b7Po+fnyyy8z7TN69Gj5888/5Y8//pD4+HizTl9zXuXWfij2P/fcc83P/eDBg/PUpvf66fn9/fffTdCsP5uhOqd33XWXeS963X333ea9nNdvCNx4zoBwIugGXEaDxV69eknXrl2lVKlSJqt8/PhxX1B39OhRKVq0qMkAadCnli1bZjLO2dFAqkePHubfmiW/5ZZbTACtNKCrWbOmCdj0j2xGedleM7naAUuDP/0jqBmx/GazLMuShQsXyr333iuzZ8/OtkxB/8BqgK1/+L3nR+tQtaRFg24NVPX86XFoAHv//febbbxfTefnXOVWa67te4Nu/RpeM9+6v2YL9cOKBgyaAc6Jnt/PP/9cihcvLueff75pTzN3GtDqcWgWXM+z94OFBlgnT540t3reTjvtNHPuMq5Teh6vv/56026FChXkkUceMedCgyc1fvx4E9TreWzXrp0pHcoL3U7b0wAsuyxjbs+dkb6fNCDOjgbtek2VBs+agc1vhl4/8CxevNh8oNEPAHpsbtKgQQNznfNKr5/S660f8vT86s/F5MmTQ3JO/QNupR/mjh07JpF8zoBwIugGXOa+++6Thx9+WB577DFfcHfttdeaf+sf1NatW5ugVzN3Xpqp1GArO5UrV/b9+++///ZlTb30vq7PSm7bt2nTxnRoWr9+vVkGDRok48aNyzKAz4oGqv5lGmPGjDGdo7LLbGlGS4Mn79fNn3zyiVx11VW+xzWou/32233lDxr4XXrppaa0QI87P+cqP/Q5Nfv/1FNPmfuacdaAQNfpNcvJ/v37Zd26daauW49Pr6+OWDNgwADz+P/93/+Z9rt37y79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AQAAAIcRdAMAAAAOI+gGAAAAHEbQDQAAADiMoBsAAABwGEE3AAAA4DCCbgAAAMBhBN0AAACAwwi6AQAAAIcRdAMAAAAOI+gGAAAAHEbQDQAAADiMoBsAAABwGEE3AAAA4DCCbgAAAMBhBN0AAACAwwi6AQAAAIcRdAMIqbS0NPF4PJxVAAD8EHQDsO3gwYNyzTXXSPny5aVEiRIyYcKEoNrRYD2vAXuog/vRo0dLrVq1pFixYtK0aVOZPn16nvbbsmWL9OzZU0qVKiVlypSRfv36yc6dOzNt98MPP0jr1q1N+1WqVJFHHnlETp06lWm7cePGSe3atc12jRs3lo8++kgKip7P4cOHS/Xq1c3zt2jRQmbNmpVpuz179sh1110nZcuWlZIlS8pll10mGzduLLDjBIBIRNANwLbHH39c/vrrL1m7dq2cPHlS7r///qDaGThwoHTr1i1P25577rly3333SSg8//zz8swzz8iUKVPkyJEjcs8995jgef78+Tnud/z4cXO8+gHg77//lvXr18vu3bule/fuAR8I/vjjDxOY9u7dWw4dOiTffPONTJw4UYYNGxbQ3iuvvGLO5dtvv22O46GHHjLB7Zw5c2y/xrx8oBk5cqS89NJL8umnn8rhw4flhhtukF69esny5ct921iWZT5kbN68WdasWSP//POPnHbaadKlSxc5evSo7eMEgKhlAUCQPB6PdfLkSatGjRrWqFGjzL/T0tKy3PbUqVPmcb3Niu534403WhdddJHZLre22rdvb911112+bfVYgqH7li9f3nr00UcD1nfu3Nnq1q1bjvu+++67VmxsrLVt2zbfuj///NPSX61ffvmlb90NN9xgNWnSJGDfCRMmWMWLF7f279/ve/1VqlSxhgwZErDdpZdeap1//vmWXQ888IDVq1evbB8/fPiwVbp0aevZZ58NWH/22Wdbffr08d2fPXu2eX0rV670rdu1a5dVtGhR680337R9nAAQrch0AwjapEmTTDmJZjv/85//mH+PHTs2y20bNmxoHtesaLly5UxmNDEx0ff4nXfeaTLNP/74o9lOl6effjrLti6++GJZunSpvPbaa75tP/nkE/OYZnO1bCOnRTPTXitXrpTk5GRzPP66du0q8+bNy7IExGvu3LnSqFEjqVGjhm9dkyZNTHmGPua/XVbtnzhxQhYsWODLhu/a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7wsvvFAqVapkeiDnRj8UTZgwwQyQr0ujRo3MZEdTpkzhchSAMWPGmGvVuXNnqVKlitx+++0moMuPjz76SBo0aGCGeILzJk+ebK5Vp06dpHr16tKvX788f2DS8W+vuuoq83N6/vnnm2mrp02b5vgxF3azZs2S008/Xc455xxzzrt06WJmn8yL0aNHmwlPzj77bNPGWWedJQsWLHD8mAszTRpp3KCzJzds2NCc+/xYvny5+VvWsmVLady4sbRp00ZWr17t2PGiAFhR7Pbbb7dOP/10a+vWreb+9OnTrZiYGGvWrFmO7ovgjRo1yoqLi7NWrVpl7i9ZssQqUaKE9frrr+e436ZNm6ySJUtab731lpWWlmbWTZgwQdNv1tdff80lcdCHH35oFS1a1Pr+++/N/XXr1lmVKlWyhg0bluc2tm/fblWoUMG68MILrdNOO83Bo4X66aefrNjYWGvq1Knm/o4dO6z69etb119/fa4n6MiRI1bTpk2tXr16WcnJyWbdnj17rGeffZaT6yD9udLfhc8884y5n5qaanXs2NHq1KlTrvt+/PHH5neh3qrjx49b1157rVWuXDnfNUTorV+/3rrjjjuspUuXWg8//LAVHx+f5333799vVaxY0ex/6tQpc82uvPJKq27duubfiExRG3TrG7ZYsWLWyy+/HLD+oosusrp27erYvgieBstVqlSxHnjggYD1AwcONAFBTjRomDdvXqb1Z5xxhjVgwAAui4POPvts88fA38iRI62yZctax44dy1Mbl156qfnj8uSTTxJ0F4ArrrjCat++fcC6iRMnmkBcf5Zy8txzz1kJCQlWSkqKw0cJf/fff79VvXp1X1JB/fDDDyaY1qAuJ7fddptVp06dgHW//PKL2XfOnDmc6AKQ36Bbk0bFixe3Dhw44Fu3ceNGc80++ugjh44STova8pIlS5aYkgP96tOf3v/5559zrEG0sy+Cp7W8u3btyvK8ay2cPpYdbxmQP71O+vVeqVKluCwO1t6vWLEiy2um/SF+/fXXXNt44403ZNWqVfLcc89xnQrI/Pnzs7xmWt6lv+Ny8vHHH8u//vUvM5nH9u3bJTk52eGjhfeanXvuuRIbm/5n23s/tzKRChUqSGpqakDJl7cmXB+DO693ixYtJD4+3reuTp06UrNmTcqCIljUBt3auUfVqFEjYL3WLmqgkFMn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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_viewer = phase_space.struphy.plot.viewer(x=\"e1\", y=\"v1\")\n", "phase_viewer" @@ -478,25 +328,15 @@ "metadata": {}, "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "22", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZkAAAGhCAYAAACzjMkOAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsvQeYbGldJv6eyrlzvn1zzmHynciQZmCGYQAFAVFAQWVVdNfw6LMsuuKu4qqrK/wFAUEFQZJDGCbncGfm5hz69u3bOVXOdc75P+9X/VWfrq6qrg5143mhntvTXXXqxN/7/dL7U3Rd12HChAkTJkzUAJZabNSECRMmTJgwScaECRMmTNQUpidjwoQJEyZqBpNkTJgwYcJEzWCSjAkTJkyYqBlMkjFhwoQJEzWDSTImTJgwYaJmMEnGhAkTJkzUDCbJmFhS9PT04Gtf+xpCodA1eWa/853v4IUXXsDViqt9/01cfTBJ5hrD6OioMPJ89fb2lnzP0aNHC+9JJpNL+v0vvfQSfvmXfxn9/f24HPjWt74l9qFW+PSnP40vf/nLNdv+1b7/JkwUwzbrNyauapw+fVoYeeKTn/wkvvCFL8x6z+/+7u/iscceEz+//e1vh9vtxrWCT33qU3jooYdw22231WT7P/dzP4fNmzejVrja99+EiWKYJHONYs2aNWJV/Nd//ddwuVyF39PDeOKJJ7B27VqcPXv2su7j1Yj/83/+D65mXO37b+Lqg0ky1yg+9KEP4U//9E/xwx/+ED//8z9f+P0///M/o76+Hg888IAgICMmJibwyCOPiJ8VRRHktG7dOuzevXtW3uW5554TK26fz4dnn30WAwMDM76nGIcPH8b+/ftxww03YOvWrYXfHz9+XITvLBYLbrrpJixfvnzG5/71X/9VEOLNN9+MU6dO4eDBg9i0aRO2b98+4325XA7/8i//gnQ6jTNnzohQINHW1ob77rtvzm396Ec/wvj4uHifzWZDa2srbrnlFgQCgVk5jY6ODtx+++2zjnGuYzF6m0eOHIHD4cCNN96I9vb2qvafUFUV+/btE6FQ7tutt96KxsbGqs/ZYvef2ztx4oR4z7Zt27Bq1aqSx2jChIRJMtcoli1bhje/+c3CWBmNP//7F37hF4SBK0Y0GsUzzzxT+G8aXZIJiYbhtaamphl5l5aWFvzJn/yJ+JfG6f777y+5LzRsH/nIR/De975XfDcxPDyMD37wg8Jg0uBpmoYPf/jDIsTH1TZJjvjEJz6BX/qlX8K//du/4dVXX4XX68Wb3vSmWSRD48t9z2azGBoaKhzH+vXrC0a60rbeeOMNnD9/XryPeSqSIr2+r371q2K/jTkNnlejka72WAYHB/GLv/iL4vyRwPx+P37zN38Tv/IrvyJCmHPtP0ngPe95j1gMMJx24cIFYfT//M//HL/1W79V2J9Kx7nQ/ed+MdRGL/juu+8W9w/PERcg/B6r1VrFXWniugSl/k1cO3j++ec5ukH/0pe+pP/bv/2bbrVa9YGBgRl/e+ONN/Tf//3fFz8PDQ1V3N7Y2Ji+evVq/Zd+6ZcKv/vGN74hPnv77bfrFy5cKLwvFosV/nbkyBHx+8997nO6xWLR/+RP/qTweVVV9d27d4vt9vX1FX7/4osv6jabTf/7v//7wu+8Xq++YsUK/R/+4R8Kv+vt7S27v01NTfrHPvaxkn+b77Z+7/d+T/f7/eLYJLq6uvSPfOQj8z6WXC6n79ixQ1+5cqV+5syZwvvS6bT+/e9/f879T6VS4rNbt26dsT9//Md/LM73I488UtVxLnT/v/71r4vvOX78+Iz9+s53vqNns9my59CECZNkrmGSSSaTel1dnf6//tf/En/76Ec/qm/btk38XIlkjh07pv/Hf/yH/rWvfU3/6le/qt99993CwElIIvn85z8/67PybyQyGjOXy6V/61vfmvGeH/3oR+I9JMFivO9979M3btw4w2CuW7dO1zStquOfi2QqbWtiYkJ/9NFHxTHwuD/72c+K/eTvJIqNdLXH8sMf/lC871/+5V8WtP///u//Lj5Po24ESaq9vV2/9957qzrOhe7/X/7lX+qKoug9PT0V99+EiWKY4bJrGMypMFTGPAyrlr797W+L8FY5MGzy7ne/W+QLGOtnPoD5iZGREfG3YhTnaox4//vfLz7z1FNPiW0ZwRAOwTwO8xCcmydn56VSKRECYvhLhmB27dpVCDktFuW2xfPyZ3/2ZyJ3wRCVx+NBOBwWfyt17PM9ltdee038bqFVYwxNEczhGMGw1Y4dO0S4r5rjXOj+M0zHsNzOnTvx8MMPi5DZvffeK8KyJkxUgkky1zgYm//Hf/xH/Lf/9t+E0WDsvRyYF2DS+eTJkzOMB+PzTPYXg7mYctiyZYvYFosCikmGyW2ChQDFuSEmsZm3YCJckkyl75kvSm3r+eefx2c+8xlRCPHbv/3bhd+/+OKL+MEPflAwuqVQ7bEwp0EYK/3mA/l5p9M562/cpvy7RLXnrNr9Z4Kf98XXv/51sXDgeWLD7fve9z584xvfKLlfJkwQJslc46CB37Bhg+iXede73iWqpiqtavfu3TtrdUrPZr5gZRsrlP7wD/8QwWAQ//t//+/C3+gpEEx6MwG+lFiIxyNX88XVcdJ7qIRqj4XXgDh27Jio7prv/q9evbpQmcZqNCPobbBkfSGYz7UgcXEhwhe9m7/7u78ThQT0aFhsYMJEKZgd/9cBGApidReNQyWQXM6dOyeqiyS+//3vL7ifhgTzxS9+EZ///OeFEZLbZbVWc3Oz2C8aq2LQkC4UNIQktflAkio9L4lIJIIvfelLc3622mNhiInVeSRf6T1IsEpsrv1nGJNNs6z2MnpWLDmnh1HJQ12K/ed9Yfw7vUwuWohrVULIxNLA9GSuA7D0lK+58Ad/8Ad45zvfiQcffFCUzbIfgqt5hstYyrsQkFzYl8Nt0Bgx7l9XV4f//M//FIaT+QTuG40rS4h/9rOf4S1veYsgpoWAZdQkts997nPo7Oyc1WdSCuz3YXjvAx/4gFjRkwxZdv2rv/qr+LVf+7WKn632WPi+733ve+J97BXi+1jCTB0x9pwwX1Zp/+mBfuUrXxGLhbe+9a3iOpGc/uEf/kFcL+73QlDt/rOP6O///u/F97KkPR6Pi1wfw2jcJxMmysEkmWsMNEp86GUYpBz27Nkj3meUlKHEDGPzNHgM6zB5/Jd/+ZfCm2FcXoKhGX62oaFh1nZL/Y1hKBLNN7/5TXz3u98VRQEM49FD+o//+A8cOHBAFBcwJESVAibfjU2l8wmp0Thv3LhRHAc9ExpESTLltsWcBntE/umf/kkQK1f23A+SwCuvvCIaGyuh2mO58847xT7x9+wrYl8S90l6BHPtP88bQ5D8PK8PmzFJXMX9SZXOWSlZmWr2n3049MZ4L/C72XtDT5UFAQvNM5m4PqCwxOxy74QJE1cL6F284x3vqCqUZsKECTMnY8JE1chkMqLbvquryzxrJkxUCTNcZsJEFWApM8OIdPwZtjJhwkR1MKvLTJioAsxVMMn9+uuvi5yJCRMmqoOZkzFhwoQJEzWD6cmYMGHChImawSQZEyZMmDBRM5gkY8KECRMmagaTZEyYMGHCRM1gkowJEyZMmKgZTJIxYcKECRM1g0kyJkyYMGGiZjBJxoQJEyZM1AwmyZgwYcKEiZrBJBkTJkyYMFEzmCRjwoQJEyZqBpNkTJgwYcKESTImTJgwYeLqg+nJmDBhwoSJmsEkGRMmTJgwUTOYJGPChAkTJmoGk2RMmDBhwkTNYJKMCRMmTJioGUySMWHChAkTNYNJMiZMmDBhomYwScaECRMmTNQMJsmYMGHChImawSQZEyZMmDBRM5gkY8KECRMmagaTZEyYMGHCRM1gkowJEyZMmKgZTJIxYcKECRM1g0kyJkyYMGGiZjBJxoQJEyZM1AwmyZgwYcKEiZrBJBkTlwW6rptn3oSJ6wC2y70DJq4/cslms0gmk7BarbDZbOJfviwWc81jwsS1BkU3l5QmLhE0TUMmkxH/ptPpmTeiogjCkaTDf/k7EyZMXN0wScZEzcF1jKqqwoPhzyQPko30XPg7Eg//lX/n3yTZSOIxSceEiasPJsmYuCThMZKMuOEUpfC7cqRRTDr8eXJyEp2dnbDb7YXwmkk6Jkxc+TBzMiZqBum9kCTomUhSkORBlCIK/o4kIsH8zYkTJ9Dc3Cw8IOnpGL0ck3RMmLgyYZKMiSUHCSSXy4kXYSSYhUCG1Ugoxfkd5nZM0jFh4sqFSTImlhQ0/vRe9u3bh46ODnR3dy8peUnCkZ6O9Igk6fBFmJ6OCRNXBkySMbEkkLkTGR6jd7FUORNjmK3c34ykI1/0coykI/M59IgW612ZMGGiOpgkY2LJk/vSgC9Vdfx8yMBIbiQUI+mkUqnCeyTpyJyOSTomTNQGJsmYWBSk90KCMRrqSiQTCoVw4cIF+P1+NDY2wuv1VkUkCyEtk3RMmLi8MEnGxKJ6X5jcL64eK0cy/O/z58/j3LlzaGtrE2XJ/G9+tr6+Hg0NDeLl8XhmbWupUA3pRCIR4eVwX0xPx4SJxcEkGRNLFh4zophkmB85cuQI4vE4brzxRrjd7sK2otGoIJyxsTGcPXtWGHZJONLQy/cuNUqRzsjICFwuV2EfeXzFhQRmeM2EiepgkoyJJel9KYaRZCYmJnD48GFBGLfddpsw1MaEfF1dnXjJ7dOTCAaDGBoawqlTp+BwOMTfaPzZK0MCqBWM4T56M9LL4X7xRW/HJB0TJqqHSTImatL7wr+RiE6fPi3yLxs3bsSyZcvmLAiglyA9GIKGnV4OvaDBwUGxPXoYRk9HklAtID0dowSOkXRkn45R7FPqrpnVayZMmCRjogoYS5OJagwoDfDFixeFwb3llltEkn8hoNFuamoSP+/cuVMYexYO0NMheR07dkwUDpBsZF6HHsilJh2Sr5TKMZKOUXfNJB0T1yNMT8ZEVb0vUriyGkPJsBbzKySWm2++eYZEzGLAfaDRZsiML4L7RsIh8bCI4OjRo/D5fAUvh8RjVAq4nKRj1F0zxxqYuF5gkoyJqoUt5yIYEtLJkydFWEsa+aUgmErfS8Pd2toqXgTDV9LTOXPmjMihkOykl8N/l4r0yu2rSTomTEzDJBkTVfe+VAKrxg4dOiR+ZnKfXsVSV4NVsz2n0ynKo/kiSDIkHL5IgCw4CAQCBRLkz5eTdAgWOtD74sv0dExcazBJxkTVvS/lQM+FuRHqlK1fv/6ydvwXg5Vo1FDjS/bCSNLhfvNYWdlGD4eNoQvNHS2GdHp7e9He3l44b8UD3MzwmomrGSbJmKi696UYNNDHjx8X+ZcdO3YUQlbEUpLMUm2P22BlGl+cTcPtJRKJAun09/cLcqVxZ9iNHgZJp5YJe2OPjiyZlp6k9HTMqaEmrmaYJGOi6t4XI2iAGR5j+fDevXtn9a4sNckQS7097iMr0/hieTW3z7Afw2okmYMHD4r3GdUIqpXAmQ9kUYXcJ2P4zkg6cpaOkXTMqaEmrnSYJHMdQ+YG2PDIMmEa0bkMKD/D0mR+ZtWqVVizZk3ZwWOy5LkU5ktCl6L8l9/BvAiJhKS5cuVKoUZAL4cNpT09PXNK4NRin+YiHTmq2pwaauJKhEky1yloqEgw9GLGx8eFcZ3LWNKwsUSY1Vt79uwROYxyuBo8mbnAY2BhAF8rVqwQ52wuCRyS00JIp9rPVEs65tRQE1cKTJK5zlCq94VGaS4DztU8w2PMUTA8NleXfS1yMpcb1UrgFJPOXFjMeTKSTvEAN3NqqIkrASbJXMfSMDK+Xym0ZVROXrdunVjRV2PwrwVPZi6UksChl8fXwMCAyO2QZKqRwFkKEi01wK0U6fD68z0MC8rqtSuBxE1cmzBJ5jrsfTGW0BLlPBkaJQpbJpNJ3HTTTYUVfDW4Fj2ZaiVwpAwOjXk1Eji1Is9yU0NJgCzl3rBhg/i9GV4zUUuYJHONo5rel1KEwDwNCYZ5l127di1ImuVa92TmQrUSOCRz5nqMYw1qAaPnKj0Y46hqY3jNnBpqYqlgksw1jGp7X/h7GS7jv0xoc+W9adMmdHV1LciLqCbPc615MnOhnAQOczlsDJXTQmstgWPUoTNHVZuoNUySuUYh4/DV9L5IT4ZhMSb36fUsRjlZ4nr3ZKqVwGHH/9q1a0Uo7VJI4Bj7coyolnSKFabNAW4mKsEkmWs0PCarx6oxAPw7q6TYB0KjRw9mscbseszJLBTyPC1EAmchas7lSKZa0uHCRZKOOcDNxFwwSeY6l4aRZbj0YrZt2yYM3FLgeqguW0qUypNVI4FD0pGeTrUSOHLxsZB9LEc6DP2ZU0NNlIJJMtexNAwlVCidwrAMZVWWimCqJZlqV9Rye9cqqiHPchI4knSYz6lWAmc+532ufTJup9zUUFlIYE4NvT5hksx1NhZZgmWsFLekcjINwlInmK92T+ZyqAvM9/1yPACvIfe3WgmcpSKZasca8P7av3+/6LGSM4bMqaHXD0ySuYbGIlcTAjEqJ3OccUtLC06cOFET8cml3t61Gi5biuMqJ4FD0imWwJFhrVqRTSnSobcsF0Dm1NDrCybJXOXSMPNVTmZ4jAlmo3LyXGKWC0EttnktY6mNvVECh0KfRgkcaq+Fw2FBPvOVwFko5H0qX4Q5qvr6gEky10Fyn5/p6+vD6dOnSyon18JLqGZU83xVmE1PZmkkcFjkwdAZvZ6FSOAsBKW8pmqmhpZSI1hI0YKJyweTZK7xscjVKCcvdePkXJ7M8PCw2CcZvuE+8V/2jVTa3rWMS3l8vNYLlcBZrCdTCeVIRypMEybpXH0wSeYqgEyesmmPq1AKVVZjlKpVTq5VuKwY/A6umNnzwV4cGgyGbTifhnkiadTkq1hixfRkaudVVCuBI68NiWc+EjgLyf+UIh1zaujVB5NkrqLwGFdzJJlqwmOsLuKrGuXkWnkyxm2yx0NOmrztttuEgaLBoFFj+I7HSIPGfAEVn3mcJEejyvG1jEvtycz1feUkcEg8Z86cEcUD85HAWWhvjhHm1NCrEybJXEW9L0aNsXJYiHJyrTwZSTIjIyM4cuSIaCrcuHGjOA4ZczcaNVa68SWPg4RDo8bqN/43yYcVU7Lb/VoJoV1qD20hXoWUwOGLMKoRzCWBIz2QWlQcVhrgxrCsLH4wp4ZePpgkcxX1vszlcUjlZMbZ56OcXKvEvwyPsTt969ataG9vn5dRM0qsvPrqq8JwkWRYxEBIeZVLMQb5evNk5sJ8JHD4ImqdsC8mHS5uSC68N8ypoZcPJslc4b0vxq7qcp4Mf8cQBo3vQpSTaxEu4zHQm2Jj4K233iryLQuFJFiSJz0d2XhYPAZZEk6ty3GvR0+mEqqRwCGYd5PX6FJ4onwuZGWaOTX08sEkmSt4LHLxQ1iKZIzKyTTmTM7OF0sdLqPhZ3US95dqzkuhJlBcci0bD409ICQdWY5Lg2csIlhMZdS1hkvRhGmUwKEX8cILLwiCYaHHfCRwFgNjRVu1U0PNAW5LD5NkrsDel1IEU4pkZCkwQxbMdSzUmC+VJ2P0qFhswLn35fZpIQal3D4Wj0Em4cpVtKyMkkUENHQM49RiTsv1FC5bCCh/w/tiPhI4i0GlsulKpEPCqVQyfTWHZS8HTJK5inpfJMnwvXLQ1XxzHbXyZBiTp0fFY6FHxX+5f0uF+TzYNAjFRQSSdFhEQAMi1YsXI5m/VLjaw2VzwRj6na8EjjH8Od99rqY3pxTpFM/SKTU1VOqvmbN05oZJMlfwWORi8D18/yuvvCJ+ZikwV3yLxWI9GVlwQKPOnBAfPpa7VtrmQr5vofvIIgISMV9yOJuUV2GPDn9nNGiXox/nWvZkyoV/q5HAoTfMBRV7vOYrgTMfklnoADdzVPXcMEnmKpGGIRhaYEKV0jDsf1mq1fdCq8v4Ga462SRKcmH8fbHbXOp9LLUdEjNfLJDgNmOxmCAcnl+WSRPM6UiZFf5cS1wPnsx8vq84/MnnhIuW+UrgLIZkFko6ssKNCxtzamgeJslcBXNfpHLy6OioeJg2bNiwpPu0kHCZ7MfhA1ZqVPNSkAKNPkNbSznnphjcT+47XzJ0w+PidZKraGnQGFpj3mCxml56VoM6nIB12XSy+1r3ZBZj7BcigSObfWsVBi1HOnKhQgknSTi263xUtUkyl6H3pdqxyMXKydu3bxcP1FJjvuEyrvqZf6HRLdePsxiSoXFgsp4eBo0HSeZSCWTyXHAVytfq1atnGDR6bNwnVvDJUty5Ot2LoYXSyJ4Ki59JMoTpycwP1UrgEPwd3zcfCZylIB1ZNq2ZU0NNkrkU4I1GY7VQ5WQaO76YHK2FfH61noxRrobeFKuFyh3HQkmBRpyeBMFwFr2Ly4lig0bPSuZz6OXQo2MCW5IOfy61etY1HeqFGHKDCUDTYVs1s0/E9GQWjmIJHF4j5gkZViPpsNhjPhI4i4EcAFicg9Kv46mhpidzmXtfSoEPCVdj9GKMysnVyMrUypPhPtH4Myd08803C2NaCfMlGR4XPQWWZRM7duyY0cB5KaX+K10jhsqM8iosIpDyN2w65HEYS3G5otaTKnInWQgB2Ff7kT0fhbV9Os9jejJLC14jGVrjvSoXBtVI4CwWskq0WoVpdar4x5jPudamhpokc4mkYaq9WaRyMm98Vo8Z4/+1Ipm5DLjcJ1b/sDy5msbGubZpPBecVc/tEwwLkmBq3seSiQGKFbAvLqnPogB6XLKIgMciSed8z3l4kja0Jv1wLvMjsKUNyvk0bB0eKLaZhuha9mRqmRup9J3SqM9HAmexJe383mruXWWOWTrFpMP8IImzGi3CKw0mydS498V4Iy1WOVl6HLWQBSlFXvweehesIFu/fj2WL19e9fdW43nwO7n6l7IjPGbKkpT6jqX2ZKz9rwIWO9SVdy7ZNrmP9Fz46u5YhszpEJKjUYS7shjITuLMCxfQNuGBts2HhpGmy6ZEcLlKmC8lyhFbNRI4/Kzso5qvBE45T2axpPOhD30In/jEJ/CRj3wEVxtMkllCSPeXcWCuOqrtWq5WOVlua6kf2lLhMt7YVE5myO7GG28siBxWi7n2lbkXqa5MsKm0UgiueBvc7oF4GicTafxC6/xXd0oqBK1xLWoBmdxXvHbU7V2GeocVzCylT0wi3pzApDs1oyqKoIFjn1GtE9RXY3VZLb2nYgkc6Y1K0pmvBI7MySwWShHpcJ8Wo/93OWGSTA16X0gyJJhqbgp2ONOYV6OcLG+4pQ4/FHsJrMhhRRtXcMUhu/lss5RB476zoIEPL3/md1ASp5rvkPuY0jQ8HozjYjqHdzTOX6tN7F8qDN21tKGHQnJ/IC4S+9bO6UWGnlahT2RQv7sdjZ78NWZugKE1lqfznmG+wJgr4GKjFsb5Su+TWarvXKhHUfBGu7vnLYFTbbhsPpDEtxBdwisBJsksAaTInryxZd38XJ+Zr3KykWSWEjLXw32m8ed+rV27VnRfL9Q4GElGgg8rq+UYniAYI+d3VLviJIYzOfw0GENa0/FQkw+dzgWEm3QNSjoC3bl0JKMlc8idDAuicexqgsU7c79IPJZGJyxTBEOQWFkRRZK54YYbxAKlOFcgjRlzBTQyS2Gsr5eczFJ853wlcOiZ10IBPBaLzepFu1pgkswShMdk9Zhx7kslIqCRZaKbn52PcnKtSEZ6MvRe6MXQ4C12GqWRZKT3wm5t+fAz70QDWq0hIFcdz6g4PR6BQ1HwcJMfrQ7bwpP+ugY4K1fIVbdfOrSRJLLn8hVjdpYmW4pCezkN6lASjm2zz6mRhItzBcawDXNjPKeScKQSwULI4nrOySwWc0ngUGWapMNF51KOnUgkEma47HpDJWkYejLy98VYjHKyjNMuNclwlURw5bx3795Fd7QT8lzwAWQoiOdDlmVu3rxZNDtKpdtK4Dl+Zf9+HFCcCDs0dHkseKjJj0b7wkMSIlRGgrEsLqxB8sidiUALZWDfVA9ro7Pk+9TBBCw+GyyB8ue12AgXh22MK2gO46JHyOtknKHDc1rVfpuezJKhWAKHuVVeFxZ0zEcCpxLMcNl1iLmkYUp5MvwMbziudhajnLyUZcy8eSkQyf0i5jNNs5ptk2BIqGxkpIGku88qNX6HLO2uBBrTN86dxz6LEymrHW0K8N5mPwK2xZHDUuRjtHAG2ZMhKF4bHHuaoDis5fM0gwnY1pf+vmor5kqtoLlqliKfDLlJaRX5KnUta1GdeDUn/mvxvbzPGf6uJIFTb8jpzFVhyIIgud2rEWa4bBG9L+U694uJgJ4Cw2NLoZy8VLNfeAy84Wmkdu7cif3792OpQHJhNzw9FeYcmDBlPLvaiZ3ct8OHj+BEOovDFjc6lnUhMz6G+1yWRRMMoaTpySyMZARp9MWQ65+d3C8FhtJgs8DSUHn1Ol+jzxU0vRjZqCulVfiidhYNk3GGTnHDoZn4rw2KE/+2EhI4knSMEjiScEg+xYsDhk2JpUj8v/jii/jSl74kSrW/9a1vFfarEh577DF8/etfF4sa2q/f/u3fnpdorEkyCxyLXGmVxL9xpUkyoMvMUl32mCyFcvJShMvoVUg9NN408qZe7HZ5zFyt0VsjodDYUd6Dnlu1JdC8kQ8eO4aDihPJ+hasqKsXobF1VhXuJVp8K6kgdE9+1sx8oCdzyDK5r+pw7GyCxWefe1FCMlpePmG/VL0/xdIqxoZDLiaMDYeXGteTJzNXCbPdbp8x68ioRsCCG143owQOiYUkI5tKFwP22PA72Cbxz//8zwX16EogEX34wx/GZz7zGaH+/ud//ud4/PHH8eSTT1a9UDFJZh7SMNUqJ/Mmy6/GD4tVPD0FeVMtFosNl3EFQ9JjyIUVZEbSWozBo/fCPAEJi9VyJBs+cNUSDN/L8tCzYxPYZ3FjRVcXGhxOeKwKHmj04+zo0jVjMlw2nx4ZcQ+MppA9G4G1bSq5b537AdMm0kKnzNIyt3FYas+iuMtdNhzyfiReffXVGaG1pZhEeS2UMC8W823GdBTJFBkXB3xOSQxSa+25554Ti8Jqc2/F+Mu//EuxCHnmmWfwt3/7t3O+n/fN7/3e7+HTn/40/viP/1j8jj1z1C38yU9+gne84x1Vfa9JMjWY+0KCYTWVlIZZypLGhZIMj4Fxe5ZdMvdidJONNf6L8V4YEqMbzVAZSZW9P6WSnDI/xRUaQzlc3TF00Gex45jDh7tXLceYpsNjseD+Bh/sRdVai0U+8V85XCYJTST3z0agBTOwb6yDtclVfeXhxTisXd5Z1WalvqeWMDYcMhdIY7Vt2zbhNcoyXF4DSTi8Jgs1ZFeKJ7PQzvvFYrF9Mi7D4oDb+tGPfoR/+qd/EuGqD37wg4J8WJzz2c9+Vvw7H0gvt1qQ5Jjve+ihhwq/Y051y5YtIoRmkswlHIssIftM+OAyNMFS4KVewS2EZJgTYniMhqQU6cmqtfkaPBopWeVE4uJxk2DoIfGGpmdSvE2uqA8cOCCOg8aMIbX+4RGc9dUj29SKtzfX4Xwqg1a3C29r8MJqmLeyJAY5lwZySeiu+uqS+6dCUNyVk/uloEeyonfGbhDCvBIgz6Fs+jQWEdCAyfAuPRvjDJ3FyN+YnszCwGeEUYH7778fTz31lHi2GO7iz1IAtJZg2TxhHEYo/1v+rRqYnswSjEUuVk7mapEPZS1CBPMlGTb1MSZPL4PGv9zqbj65Hp4fqZjM7dLV5wPAZLNRPbmYGDh0jSFE5mtIQqI4wOVG76oN6Kyvx06bjifGQ/AkB7DeDpwzlOcuFdiECasTsFXwSHQd9pEcMkOTVSX3SyF3MVZSCLPsfl2icJK8HsbvK1VEIJPTXCjQ4zQWEXABNZ/V+vWUk6lFx388Hhekz+NhqGqphxaWg2wxKE7yc1/4rFcLk2SWIDzGCi0aTxkeozcjNbmWGtWSAY+BK1KWAVeTE6q2ao3Gh6sp6b1wX+iZMEFZXAItSYYvfobnhT0yPDck5F5NwXFXAPd0d2JHgx8/nIjh9pZ27PU6xPfwvDKUwzg1t0vyWqxKbqF8uVwiPpmDu1cFUy6O2+dO7huRSJ6F1eKFPduc750pU7Z8uUcvE5Xu6+LkNK+XnKHDe4rPiHGGzlzXoxaG90okGblAXervjU0NyrvUkIs7Xnej/WBeT5ZoVwOTZBYxFtmonGxUKa6VJH+1ZMCVD8NjfLBJetWUG84VjpKabCQthljYlc6feexsFqQLXaqhkKuh119/XRAF8wAsPIin0zigODDh9eIj61YgYLfjexNRbPU4cYs/38VuNHJcNTGfJIVEeW6Nne/zSlpXKF9WR5Miua+5FKSW2edFMEQq2QurzQ/LoBvWVve8wmuX05OZC8zP0Dvni5/n9ZCkw9wjf2csIigWkLxcnsylVriWz3wtPBnfZSAZPq88FrY3SO9JCudWm48hrnuSqbb3pRg0mnLGffEQr0od/4vFXAQmFQVo9El81T7clbYrvRcam927d4t/6WFwRcOYcblwFs8t+4MY02cik/mX82k7Xs/YsG55Mz7V3YaUYsH3J6K4wefCHn9pMuT3MezH68L4tJzZYkxay3DPXB3VVF8ubsScTu6nYd9Qh/RYcIHFBhq0VALqeAqO3XP3H1wuT2YxhMbPktT5kjN0uNLm9ZDz7Y1aXrwm10tOppYk41lEb9188Lu/+7uC0FhYwGv3wAMP4G/+5m9E8p+L1S984Qsir/r+97+/6m1e1yRT3PtS7WAxqZzMCi0a3eLmqVp7MqW2zd+xYovGmCsQWRJZLUp5MtJ7YS5FznqR3gTBMFypyjk5OprEzdwL903oO3kD+PFQDDvbm/CrG1bifDSOn4bj2BtwY7u3dI5EqgZwm/x+Hj/DM3xxn7iPxR3VfEgk6RTnD9iIqTWsnj5vEXbuh6G4rYIYFKcVGJvXqZtx3LmJCLxFQphzfeZSezJLPYvIeD14rWURASsOmXfjNaP3Q0PJBcdSyBbNhcsRojOG2a9ET+ZrX/sa/uVf/kVcG+IDH/iAWMD94R/+Ie69917xuzfeeGNGy8EXv/hFsbDjtaUnS3vA7TBqUy2uS5JZ6Fhko3Iycwvl4pKXmmS4smB4jFiookBxrkc2h5FEmGvhKoa/o9FgZcuaNWtKPkxSSYCrWjmauHvFCjwfSaInrWGTvx67m+rRm8rgx8EE7m3wYqNndrksrwu9FBI6sXr16pKERkPC/ZHVNlI+X/YZ8BqTaAqeTjIMvb0uHz/vi+cT9Cv9sHYtvk9EVzWooShsW+Y39+NaEqvkPWEs1uD9wHuTv6eBkgZTep2yB+Ra8GTmU4l6OUjmjjvumFUpRrAkWeKv/uqvZoQZuVhluFsWNW3fvn3e8ja26zW5z5NGQ0kmr+amkMrJvHnnUk6WHf+XgmSYF6FXxVU+BTcX+mDJXA+NAo0BjTtzLwxzEayX54vnrJyXxIeBRQDcP54fekDdGzfhP8ajcHs8+OjKZfjp4RFcSGRwYjKGtzZ4sK4EwRhJkzc0HwISTjXgKtmYP+C2JOn0nj+P9cOHEcluQENSg9fpgXtHMyz+pYnda8EkdGeuohDm5UYtSCbN+T6hONa7HVjvnnk96eXTaNHr56LMuAjggoWesXEKJcPOS0EOlytcVgvCjE/1ky0WfHb5qoQ9e/bM+h3vF0ZHFgrb9dr7wrASXcBqGiVlnoOGnAmwuW4k/r1Wnoz0OPhijwoT6YsR3DRul2EOJvKN3gtJh8aAxpqrmHLkynNEsuNn+HCTiI5MhPCvIyHsXdaBd63uxqnhGEYyOYSjGn5nUzM6LLMNIK+LnEZYTGjzzV8YmxCFknEyDPXJw9BH6zBhDeGY2gf3MfcMJeNSopKqSu0ozglyV9Q004IpYH79blddTqYYwZyKH03G4FQUrCgz28eYkzEuAgjp7RpHHxtn6FSaQnmlejJLjUQiIe7fqxW267X3pZrk/EKVk2sdLuNqcN++fYV5NIsdy8rzwgedNzNXLDxOPthcQbGqi8TB/Eupah1JdvR++D6G6latWYOf9A3hVc2KD3S14p61HD4MPD8UxrCqYq/PgxUuxwypfxZQkMykGGAxoVVraOKhDKx2C1xe2+yZLifHgeQqtN2zBp3NLqzL5QpVUlJUkitpnleeU3mvRKP7oSg21NXdWvZ71eEkdCugu+ZWl75WwmUX01n8ZDKGRpsVDzb54SxTLFHJ4POeoYdjLCKQsiq8p2T4zThD53rzZGKx2FU7S+a6IJlyvS9zkYzskufKdr55jlqGyxheYL6DXhUruxZ7U8teFO4vvTQZHmOoi79nDJerqFKGifvC8Bj3hw8Bw2vupmZ86WQPNEXBg14bttb7xTV4KZLAofE43rqsHkp2evXOv8lSaILEwuMqRWhzrfr7jgYxOZhA6wofOjfUzUzunwrDkk7AtTYFtTnvvfLaGkulSXQ8H2w0ZbiQ4p75EulReL0eBAKlDbRYxAzEYW10QNXD0PWcIKVr2ZM5Ek/hmXAC7Q4bHmwsTzDyO6udfiqLCJhYLhSMGIoIGN42kg7vk8zhw9CTSbhuueWyezK1Cpf5rtLRy9c8yVTqfSlHMkuhnFyLcJksOqDh4wNGz2qx3gsNO7dHdVWeCz6w/B7+nkaWOZ5ysWA++K+99pogGoa0SAznEil88chpbK7z4ec3rMFrr7wstvdMKI4T0SRW22zY0uzDkYGI2AY/y9Uqq8MIEidDmMVGUFU5T2MIitJeNlR16IlB8XNDu7tAMFI/LNcXg22FDw4tAiVbfhomw4TcB6pU8xryuEg64+MhDA31oqcnULJUWhvPC2HCbwOyvFZpWK22a9KT0XQdL0SSOBhLoctpE+KljjnKvRdawsznjqEzvniP8p6VM3REJSErPM+fh5+LnPvfAVsuN0NR/FoKl3lNT+bK7X0xjkWeK6RlrIwqFpGcD5Y6XMYVNosOSJjVzmSpxnth6IEl2DSuTOrL8mSeMx5/ufJkrvTpwfDvJLtl3d145PxFvDIWxIPL2nD78qmqO4sFz8QziKt23OF04aA3CZ/ThqyqCU+JYTZ5ntjTU+58ZzKDyGYPwW6fXXCQSak4/tyw+HnFtgY0dOQ9Tj2lCt0xPaPBsaNJJPctZ8LQ3dUlUI2r6kBgFTKZIOz2TdMGTpZKNzSgacQJ7/IGKEreK8mTjPea82SY4H80GMeFVBbdTjve2VideOlSNWOSQGQloZZKIfrTnyIeiyG+YQPOxWM49vzzBU02PiuXGrVM/PtMT+bKAS80yWIuaZhiT4YrJBpyGl6qmy5GhXYpSYaeBg0/DTArP2jg56MbZAQfPHoO3CZLgrlSl+dG5EROn0aqexVWdy8rSTA8r+z+5Ta44qdOmeZ04f8dOY1IOo1PbV6D5fV5L0LVdbwKOxxZFb+8og5HLoTRXueCruUwODSMs7mMOP883wzTzb1Smz2qOTKWQs+BvHT9ptvb4JzqTVHHksieicDa7IJ9K2X5LdMS//Wr5n3ehDQOMjP0vWSVVKR/EqMXgxjO9MBXdwEuVwoe94QwdNUa86vBkwlNJfgnsyqWu+x4xzzUsZe6GVMNhZB49GdAJIy6DRvRcd/bsU5RCkoEfPFe53NDL0h6njTUtTzXtfBkdF0vaMddrbBdr70vkmSkcjJDUTS8fC32RjRue6Hbkn0iJBWGomR9+0IJjN4Zt0djTrKSJCrDg8MjI4itXo+wrwF6KovtRfc0Q0iUiOcNTy+HlV9nIzF84+AJLHdY8Su7tsDrzIePspqORyYiSMCCh30O+KxWDEdSaLbncPzYWaSzujhH3Bd6MNXJf2SgadOLgv4TIYxfnCoSuLcTFqsi+lRyZ6PQJlJCN4wkMwMLHrusQ9dmDniSVVKN4w4ot9iwrNWK/oGziMVCOHr0ABRluJA74KtcFeOV7skMhFL4ztFhHA4n0dTswd41DcKDsc1jG0spK5MbGEDiiSfyc3p8PrjfdE/heLhg4YsLIIZ7pU6esYjASDp875KSX41yMolEwvRkrjRpmGqaK3kzcDXKlTkNKGX5l0rtVz5QCyUZGbaid3HLLbfMWMXMdzImSZc5Fq66Gdc2ei88XwxZRaIxDHetRNrlxZsbfDgcm+kp8QF9+eWXxQP61re+FYG6Ojx2cQhP9Q/jzS31ePOalYVjZkjlh2MR0HTepWThVIA0Fap7BnBjYxaN9XVI9Y+jra1bFBlU81Dq8n96WuRfjj07jFxWQ6DZhVW7GvPnZCq5z459x56pzn0j1AyUbHyGxH80Sn03VsTNpWrLBUwaus4V+bSx1GJZIYTpXF8Hu8OKhvo6eL1Z1K/filyuU5xzmbCmQTMmrIuFRC8V5nNPBhNZ/PefncHZiQT3EsODcby9wQtb8/z2d6k8mczJk0i99BLsa9Yge+4cPG9+MywVyJueCxUnRPm6ponnnNeEhSZyRIWxfH2xM3RqGS7zmjmZK6P3RYpTzid0xJuM1WNLKXUh92EhyUc+BAzbcb+KVY3ltqslGem98GGTmmPGG5fFDXanCyMr1yHSdxEPuSzocNnxQjhe8AxfeeUVUd7L3AvDY0kd+Mfj5zAYDuPja5Zjfdu0OmtS1fD9sTBcVgseaApgf2++9+bI2QvQVAUrO5rQNzSOhsYmUYlmLGGeCwoUqNlUIcHfvbkeTcu8+QXGxRhyF/LJfeuy0n0VeYl/B2CbLoHN5Sahqo45SUaHJkgun2uZ/rzaHxeTMqUQJt9nURyCDGXCOv89+VJpvoyl0tKwXUpvplqSYYL/2+fGcXYyCY/PAoeSgzNtxb7zIdy7fn65ysV6MrqmIb1vHzIk6zvvQurll+G6+WZYywzhksrfxu/kz2z65EsWukg5IuYjWapPQ24U+izVM3Wpw2Wappk5matt7gs/xwedNxcTiDTkS72SlKsZ7l+1N6pR0Zk5inJlw9WoMBu9F4b/uJozbouhBIYH2zo7cchTj6Sm4y12HR4F8HN0tKZjOBjCK88+I7ZF74WhoZ5oHF8/dR5NWg6/s30j6n3TeZSYquJ7o2E02m14e5OfBy9yP9wHR/taNEVCiEWj2LF9K04dGhdGbD7IpjVMnIuiuR7YcGsr3H479LSK7Mmp5P72xsqd9nIaplEdWBiwueecizcKw5uGFe5CYYEQwtxjMLg0bFa3ICMjypVK88UGRDmqW3o6tRyFXA3JpDUdjwZjeD18kXcx7LoFXiWBtO6Ayz5/I7oYT0bPZpF8+mlowSC873oXUs+/AGtXJ+ybN1f8PqKSV1EsR8T7vHghYJyhw0XBXF4Kv3e+xFRNqIzXzMzJXCVzX4zKyTSa1UrKzBfzHWfMFT33izdUsaJzMebyZCp5L/ycFLxcuW49ntdsYEHUe1rrcHo0L5BpV4DoxDi+98IhbGhtxp133ilyJs8Mj+Onvf24zevEOzZtnPEwhXMqvjsaRpfTjrc0+hCZmphJ48lBac+eG0Ozz4WdO7cjp/PcjAsiqxZjvTEkJuipZrDtTR2w2ixQx1LInQnD0jQzuT8f9WWSglYNySB/vjU1Vchi5gbisFAI0z19HujtWC1uQUaVIEul+eJqmteeK2ySMg2cVJWWBm4pvey5SGYgEcafn30Feztuge4JoqXJg3TQinHVitUNDty3eZ6SBovwZLRYDInHHoPicAiCSR/K98N43v62iscgn4/5fCfPORdjckQxbYRs1GUlIe9lqYEnZ+jMLrVXl1z8M5FIFPrHrlZcdYl/3kA0yvPxXuTqnQ8zV5M0vlzp16rMUYbtqmnI5I3M8BhvYHbvz5UEL0cyPBbpoTEpz+M0nhvmeahewH1av207Ho1l4LEqeGdTQFQJSaVcjnYdj2Vx0/YduHfLRqQ0Hf96tg/nRsfwgWWt2LF8poc1mc3hu2NhrHE7caffhfM9PSLmzX4XPiAsqsjau3Dz9rWCmJicJ1RNx1zRa03TcfLFEaTScVgdCpo35MCCpuzpsPAi7OsCsLZU1wFO9eXZSf/qSIbkkd+f/Hv1rCY6/B3binJ4Ou9Jd5XENbVfU43BPF9SVVr2glCIlWGcSqrSS0UyT/U/hZDqwGmtAwEljla7DQ22FB6+0Y5Q2I/BWAI/v2Utuhvcl8STUcfGkPjZY7B1d8N1+17kLvYjw5DWux+CMsczYlRVXyi4EGDOkC+pgSdJR8oeGUNr9D5rkfiPx+PiuVlsvuhywna1hcdk9Vi1BCNlTxh3NSon82bgaqVWmKshU/ab0OswDjybC6VIhitgboceECvHildTNFokGOYI2lauwg8mY2iZCmtZpyT+ZWyaCepbd92MlqZ6DKQy+MbpXjgTMfyXTWvQ1jjTqI5ksvj+WARbvS5sU/Jqu3wgKEFDoiGxr163ET1BLzrqprrsp8pecypX/TOPjcQzGc/AbrXAa7WIBD/RusqHcMYOLZFGZv8EFIcFTsryu+YxFEyUL68svgrQtGrKwadIRs/fL+pQAhafrUR4ThPhslwujIWieBQyF1TFUymNqtLzLcstJpkvH/8y3rPmPXg5NIpzahPe26ZhMDmJOwMe9FjH4HYEsHy5Da0JLIhgZGh3Pl5F9vx5JJ95Bs7de+DYvg16JCL+233nHbAaZOjnIrWlyo8YNfBY5cntyxk6xplG8vpxQbdUpBCbkpS51PN4rjuSWWh4rJJyci2lX+T2y5GMnC5HyYwbb7xxxvyG+WyXBojeC0MupbwXWZ7MlZeQym9qwXfHI6LP4d4GHyyKIoiW+8L38PP33HMPXopn8NRYGMGJceywaHhw5+ZZmlED6Sx+OBbGHp8LbaExHB0YEETJ6jWprsu8UlJxot5jh8tunV65WxQRLnMabEA8ncMPDo/g7FgcyOpYlgJ213ux/uYWwJFA5JgG51gK1q1uWLsrP3SZzAisVsbQ3bPHLs9AleGyKUPJcJmuUkImAZtBtqY4XJbRRubeZmHTlcOGXDDwnPJVvKLmIsWo7VWpVNr4fTPvEeCZUBTn1UZsto5gjWs5WFpB75ZvsygWaPTQDFV188F8vAruGyVi0vv3w33PPbCvXAk9lxMly/Z160RVWbXfWctuf26bCzq+WMAivU8uAvgsvvjii8Kzkdelvr5+wVM6r3bdsquCZBYyFplg6ShjqeWUk2uplFyJZHgTcsXPmO5CqtpkCTNXUCSYct4Lz9uhYyeRScaE6GXa5cG3x8LY5HXijjpvQTmZJEyyY8UNHwrNasPLYyPCO3pPoxe3rF8764G9kMrgkfEI9jgU2HtOY3JqgBmNBI+NZM7/JnkNxXNobyiSf7dYkBPnZvpavtobwsH+MOo1CyYnUzig6bj9zk543DZETkRgSWpItaRhWz53bDoSeR0u13L4fFPy5DSsrC4rGrssKsZ0Fo5kYbFUMgI0zDZBSOpIEnBYYGkocd2Ehz078b9UJcWlVtSyLLeaUuni70urGk6orVidAXbYBuFWcoJQjLAq1jzJIF9wolEINKXCtsq/pJ6MrqpIvfCCCIt5H3gA1in1h9SLL0Kx2eC65WZUi0stKSO9Tz6DDHnyvMvKtXPnzomFAZ93eU3mE/K82iVlrmiSWehYZBpXrigYrqk0IbKWI5JLbd/Y9MlkOFdAC3GBuU0m7nlD03uhEkDxdnhj8hw81ZfFAzdvRNzpxg9GQ9jt9+CmgFt4QFI5mft59913C0M1nMnhp6d6MRoMobupAbdtnN0dfzaRxk8nItiaiUO7MICmKQFNGbIzCmryNRrLYt3KmatrhswYGjOSzHgsg1yURs6CZq8DIbeC2EQKmfMJKPUK1EYb1Gy1xrvIQ8nGAS1bIvGf/4fvrUQyLE22Wlx5khmIC6Irfe3yOZmqKtaWAMVluZVKpWWFlCQZFmv850QEGhQ80GDH49FcSc+q4MnoFuROhaGOJqsi+vl4MnoqhcTjT0DPZuB96F2iyZJgyXLuwgV4H34YyjxyHZdDt0x+L58nei3GasL0VEOoMeTJa2EsIii3v7JHZinCZbwf+Nzze6sdHUAPjXaL+8tIxULO6xVJMrxYvCi8WHxVe2DFysmVZMFrTTJGT0YOSePqZqFNn3z46b0wEUyweKGUF0RjzxupqaVNLNwHMxqeHAtjb50XO3wuQVBc8XJfeLNxOzxPL45M4Gl6J54U3r1pDR5NzJasPxFP4acjQawPjcKvZbFhSo6fZEVSLxbUpLkaT+TQHpjpyYhwmTptzLIpFcpoGumMBrS7kODQs4SKhpE0bNuaoPoiUHr5kFVPMqoh1yJCZQ4fYCm+3Y0J/Qor8ykPJReO5jvNW8o0ADJcxhLmqryjpZfer1QqzbAp70eG0yYUK54bHMcmnwsp6wjclmkDruoznwl6MEhqaDnrhObJiBJwa2f1iuRzeTKUiEn+7GewMHnOqrGpsJI6Pi68GM9b31ognSudZMr1yTidzhmD9IwzdFjcwd/JAoLiEnbatKWoLPu3f/s3fPKTnxT3BiMYrBr9zne+U3bbPBa+/1//9V/Foph2g2T4jW98QzSIX7UkY5SGIVlQAbmaWfVG5WS6qzwpc91kl4pkGIrisfDGoSbaQkoc6XnQS5BhLW63eDs8BzT2vIEYHowrbgTP9+OpySh+YUMb1tgUQVAkA3o6zNFQriarKPje4DheGp3AjekYfm7vPSJklo5NiO5959R5PBRN4McXh7ExOILNHW2FSiiGxHi9GB4rJvVIWhe+SqPHMcuTOddzHmciI/DY6pAec2JrRx38y704fCEMX0TFnR0BrLq1HYrLBiQAxcKmxbyMTDXQ1CKSMXT6G85a/r1VJP9JMtnxYL7Zs5xml6guyxMQGzKB6uLwtUrqGkul5ayWJ8/14qVkDpt7z8JqyWJCG8e4fzyvwm2zFyrpCtsIWeC7AMSbdFi4WNB0ZNU0Jk73oWP9xqqT8KWOMTc4iMTjj8OxcSOcN91UeI+eTgvPxrlzJ2wlxgVf6Z5MJSiKImwBX/T6eV0Y8iThsP2AHigXC3yWXnrpJfG3xYbLGD35yEc+gi984Qv4+Mc/Lr6HrRL/9b/+V3zxi18s+RmSyde//nUhhMuCKd4fH/jAB/DLv/zLws5elSRTnNznia6GBBgioJfAlcF8lJNrOVhM3kz0PLhvC9VEk94Lbzyucph7oevNm6aYhFg9Jo09b+CnTozgTDKN93rr0ByPYP+5c+J98j28wUczOXy7bxDx0RHc7wSaGtsEefF7Kd8eyWlocVjw4lgQj14YwK1aCnu3bhbhGd78vNnkeORSD9dkSkezxwqLwShzH/ov9sHmzWFryyoMnQ7DUhfDUGQQHRMebM544dlSj7rNbVDs8vbUYVGc0BGqykMQEjRGTyYdmpWPke8j5kr+i07+lB1aLgVre6UKK27PAovFOaUOMPcK9FJ1+/NbDmlWnHL68DZrEndv3o6RyRHsO7UPgwOD6Jvsg9vpRl+6D0k1KZ6rtpEA/FkrwityyLK/9nxSKFpPjvZjov/CvEimGAyF0VNxMS+5cXo7YqX/7LOiisyxa9eCjvVK82QqgedGFhFw4cZ9Z4iKdoNeBJ8xPs+/9mu/hnvvvVcU5sgG0mrxz//8z8KL+tjHPib+m5//1Kc+hT/+4z/G//2//7fkwpeLUb6PBEPw+eYi+Qnqxs0TtitNGkbmXqohmcUoJ9fSk+EDSm+BhpjhqPneFATJhN4LV5/06CR5yiIICXo3vBFp+KWxPxRN4rmRMNa77EgNDuLMeKJwrKyyIzEcjibxwwsD6IxM4ufWrhSxX57Pwo1vsyKcy+GV/kG8ODyGhxr9uHFdfkgab0DuGx+KSqMHJpIamrz2GfvKlZHNYkejawXSEwp237sWXo8NqROTSFDlty2FgcRFpF44UyjV9XrjgOKsKn9SKicztyczVw5FZ/8oENAqNn2StKhtRkKcb69MLcEO/seCMQRzGu5zAjY9f//X1efP7+7Vu3HuzDmoGRXZXBbh0RCOf3cfNDWNno1jUBw2tI3X5ccl+O1I9IaFzM9CGjH530Ii5uRJeN7+dtg6O2e8n9Vl6vgEfA+/e8HnpVZzXaqJwiy2T8YyVS14xx13CEmnP/qjPxKEQ9v22c9+Fu9///sLBFAtqM/IBarxfNKT4TPP0Hqp2VT0fL7yla/gN3/zN/Hud79bFJb89V//Nf7sz/7s6iKZStIwlUjAmERn8pshpPnekLUiGRILw2PcRyb350sw/Bzjn2wWlTkTY/mjlJXh+WJojCEyfg9DIsS+SAL7wnG0JtJwxENI+OxI62lhsDnSWLfa8KPREPb3D+IGLYW7d2wR7jjLYY2rareu4SsnzsGazeDja1dgbWuz+E6Si1S4nSu3NJnUsLYl/7APDg6Kar/lXSvhHsshndKw9tY22BI5ZPaPw9bgROOdK9Bks2CdYfY7Xfu+vhOwWMbA1MHQcC9aW9ZXXlCI82MIgZFk6laUeF9eE21OTyaVA+I26I1zKGuLyiyl4MlcCYjkVDwyGYPHYsHPtfgxlJhESoalpkiWyX0+Dx6fB6ts3WgasGH1xjV4Kv4qurMejJ8dg3N0GXrWuBAYTCM8Pir6q6qB0ZPJS8Q8A3VyUnTwF/e8MHyWfuMNeN/5TihzlGLP9Z2Xg2T4WupmTE3TRK7zb/7mbwo51/naFD5H0iORkNvg81UK9Hw+/elP4w//8A9FgzafeUaKHnzwwauHZObqfSlHAgy3MA+wWOXkWpBMb99FPPbaSdyzPe8ZzJf4jN4LGzRL3UyyhJnJe3oGrKCTVUMUtjwciWPd+DiemQxidYcXw2OTuOvGfLhuIqviu33DiIwM4x1eB3Zu3FEocZXhQzEOeXQUL50fhNvtwm/v3Iwml1Ocd3pMPGel8i/FSGRUxLM6GpwW8TnmzNav3IJwnwaPO4mO9fWw9MeRG2Pnfh2sre6ys99jMQcuXIghEtUwPNSHM6fH5uyCN6om57v9S3syzLWoJUjGSLhaMAV7Uz2SVv4+C0VxlJk5M+XJTFWiVYNahssG01n8OBjDWpcDd9Z5Co23BdkjY7myDjRNeOGLAOeao/Bsa4HvpB/r29aja7AFAc0BZ5tXLBYuHDsGp8cLa+epsqXSxuMT91Y8jsTPfgbF7shXkBWRCP+efPIpMUK5nPBltbhcUzGJpf7eeDw+IwWwkEGKXKTSthghZ1KV699h/ub3f//3hTdFgqIjwHzOm970JuFZzafv57KQjGi6yuSHVpUrTeZNK8uXixWK2dzE8NhCG5yKPYLF3hg8DibVj10YRcjVIQoPuGqvNuczl/diBG8Wrj7ouXBlwXgqxSafCsZwbCKIzeNDyGhONPsciIeDWL5ylfD2jsVT+NHFYTROjuJ9XW1YUaQwwJ/FZNATJ/F4OIGmpmasra8TBCNDcjzvPLZqVmtD4RR8DmBybBg2mx3rl+3AxPkUujc1YGBEQfp0EFqjG45dTbB45pLSUcRxOuw+bNy0Bg77mkLVlCwJ5b6RlHn+5GgAkReBDUomBt1VSg9OClomK3oxlPS3ba6DErdMhetKFW9IslCmSCZ9WcNlJxJpPB1KYG/ALaoKC3tZimRyGpYNNKAu40VoUw7BdAK6Mu3leMatsHS7sGr1anS1t0O72APFkdf9M4pJStLnoseoRG6NRBD/wQ9EEt91++2zypGpskyCsXV1wb5p06KP/XKQTDWinAslGUZqFgOGtbnIM4KLBfm3UvjP//xPvOUtbyl4QLTHv/7rvy7yO3zmGBW5oklGSj5UCj0YPQ2pnMzQUCWF4vlA3gyLvSFl2TRJoWvdFowNxpFVp1ZvJUiG/SHfOzCI586Mw+uw4V3bW+BPjYqbqZz3IkFyYYKfoSJWhtHwcgLlj0dDODE0jBsSIaxb1oUf7++Fz6Jg1/btGIyqeHQ8jDcGR7A5FsRdG9fNKDOWIJGc7+vD+LqtaFq+AvfWeQUxMRZL8pMhuWrPe8/QBCypCCz1TnR61yM0mMHaG1rgiueg9kahd3ph394Apcpzn5/j4hRVY3aPfVYXPM+NrM5xOM/C407BovSh1VsHB3M4dm+Z/pfKWmO5gQQUrxUWlw2WpGuKkEoRlrzWMvF/eTwZbu+laBJH42m8o9GHFa6ZBF5MMs6UDTgcg0VT0L82jC6vX1SKSwKyxpjTUpCdCnsmIiFxb3v9PnG/GkulWSHFKAPve1mSaxkagm/fPjje9S44duwoef+kX3tNVJS57nv7khDu5fJk5tMsXi2WohmTBQMsR+Y1ks/+D3/4Q2FLpcwW7Rg9FOZnGCWgx8SoSilimq83ddnCZdVMrqS3I+fb8+e5FIprLcdfjEKeYflykZz/0dG8nEhG1cqSzE+OjuDrr/SJXpFEOoOjF0bwX+9ow94K3ovMQfH7+GAzVCjCjZqOf78wiPPDI3hHwIX2lSvEKiNtDeDtN2wQI3OfGZ9EV3ZcDBDbs2v7rDCXHAtwvr8fJ1u6sKmjHQ+11GEik8W54VGsSgZFQcF8pG8oX//y4VNY1+iFOuZCzqljww3N0M9HkU3k4FldBz1gn3+1HRyzvA5jFzyvA6/nxYv9iEQvoL//HC6OB7E8G0JyanaQcZUtpfmzudJxaSmEaVlmn1E1Voks8gsoF1S1+nLrpTJMGSb4QxyRrOF9zQE0Tkn5FO9nIUcymsaqC83Qt9nRF5hEwBYokIv81zWqYLQhA68tv61kJDxlTC0VS6UnJyYQfnUfcocOIrZlC85xQNjIyCxVaeqUZY6fqEr48mogmaVGPB5fdJ/MBz/4QZG0f+ihh/B7v/d7YiHw5S9/Gd/+9rcL7yHBsCiIwwnZB0NSuuuuu/Abv/EbePjhh4UnxGo0bkPmf6+q6rJyJMATTB0gNhCxOmIpZzXIVcdC8jJGVQHmJ7h/IpcRyRugTC5fZVIcByUO94fZaoAGmwqXpmIya0fK3VyWYEiuzL/wX1mezP2OpdL45qleRONxfGRVF1LRiCC8jZs24+TZNGIOC348OI7c5ATu72rChnWbZ7nyXNlwtWJxe9DbvRaeUBAPt9RDy2Zw4cQJRLJWbNmxA/458i/Gh5vnZXBoCHZvG9yTOTiaLFi9NoDs4UlYG51w39AK+4Ug1PmWjyv8v7Nk/sQIkcD2emCzM3SzHs7RCSQHs5hMJgur7IIApZKB1+GGli69TXUwIaqp4LIYwmDlvn+mJ5PNliauWnky0akEv8uiiAS/q4zBEzkSKMieCQNDSVzsCmJLtws4Ny0hI45G12DLWmEP6oiszMA/RSokGZdvtsy9BH/v83hg238AjakU4h/4AM6Hw4JY5HAwmU+rZ3f8M8/Ac9ddVQlfXu4JlVfrVEyHw4Gnn34af/7nf47Pf/7z4tw/8sgjuO+++wrvYbiTi3g5t4bpiH379oncDCvKmPdkX80nPvGJeX//FUkyUpuLJbV036RLt9RYSPKfF53hMZKUUVUgmsohndPgtFmRreDJUCySITOLzQ6b1QEncgiw4bAEjL0oDI9Jko1lsvj7I6fQ6PPik1vX49ypkyIuzhVISLWhL9uH6Mgolk0Oob2uDps2bJhhFHjM9F54jqnK/CJFLBMx7EQGiWhEhOQa6+vR5W5A1lbd6pIeJ88Lt93UuBnpvlF0r3PAn1KRPRGEY309bO2eQjNmIle9cc1XQTFp7ZjRZFnpEwTf69AScLQvx5ZVWwqrbIbVuECIx9n4loLH2w+bbWxmAlvToQ7GYdtQD2Tyq38p4y9yiqoOp81SJidzaUuYhzI5/GgyijUuB+6aSvCXRVqDa1CH3ppDbpsb8aH0zKISA8k0Br3QGqzIOtV8578Il4XhqatHtoyCuZCIeeIJEf5igj8Rj8OeSIhc3gxV6dFRDPzkp0jX18EeDqOht1cYv1JzWq4WT6ZWJONbgo5/huFJMOXAaAWT/EYwN/ylL31p0d99WcNllZST6QXwhqsVwSxEJJMlw3Qr2cjIsJXxRh6JpoXmFgmGZFOOZB7e1YkTw1H0TiSEsb1rXTNuWjmzQo5GjN9FIjD2ojAxf+TsOTyl2bC7sQHv6mzG4f1viFUGCS+mK/jGkQFE03G8LZJF18Z1ePlicsa5JnGzNp55nbXbd+DH0TQ6HXZsVxw4FImI42OikXM0Xh4OiobMxjl4hkaDBNNQ3wi/3oFDFyPYvLoOLZG4CB26bm+dMdxLapfNx5jk+zJovJNVeqp5Q5+X+F+e/10yiPrwWfhX3iRyTMPDPVDVLoxP9OHs2eNIpTRxLnnd6tIuUCZaCGGO5EmO2zw6mMS3Dh9HMJHFhlYvPnnHCjR68w2sxnDZXIPLlsqTOZlI46mpBP92b+UhfOpkGu6eHPRGO+w7GqGlhmeeM+R1ysR7cyoagh6oa23QuGiihlkuh3QsiqZly5Er4aVr4TASjz4KS309PG/LS8TosdiM54Srag4GC7BSceMGKG96E4JT83MYEjaqSvPfuaoYL9WEyssRLtN1XZDM1TwV84rzZIzKybzBmOivJaqV++dNy9U9cyLlRDcZKmsLODEWzYhwmaOMl8S5Kn/+0BacHo2JlfCmdr/Iz0jwM3L4GL04Gj2CD+HBM2fxss2L5T4v9to0HHj99YLY5plkBj8ZmUTw4hD2ei3Yu2snIhkg05sobJcPMcmLxNXa0YH/r39CNF1y9Xv04CnxnYzLyu8MWK2IVjg/fAgYAmE4b1X3WmTGXFDtGuwBG5pCWWgtNiR86gyCIawFgcz5gZ6MqgWreeOU15HMly8762CZOAvb2Uehe5qgLrtJvM1iVRCoawGUNrRu3oFc1pk3dr0XkO7N4HBjGJ7jo7Dbw/B6s4imnfjHV9KYTFqF9/lKbwh2mwX/7c1risJlta8u47l/OZrEkTIJ/uL3qhdioogh1abA1uYQ0jjFisuCSKaOQx9JIevIQfMr0IJ5kklGI7DaHbA5nDPGWM+QiNmwIS8RY6guKz6+LIUvL16E993vFrpkXr+/rKo08z0yvFmpVPpKqC6rhSeTMFWYlwallJMZ0iguYb4c4TJeZK7SCXoLzImUAj2ZTe0+hJPZ/Oq9gmyN32XDnuWzY9AMefE8cL+Yf6G3wXNAsj07No7jjR24pbkBuYkBDPT3izxVoKEBz4TieGM8iJVjg1Atdty+a4P4rF3NCkl9Prh8YOVAMR6DaObsjWC/lsFTR8+iQ02j09+OnNVVSA4HbBaEc5pYTTEcZqx8k2XbDLltWLUNwd4cGtscaFZ1PHc+hq230mhMQo/ONrY2qyIq8KoGmyaFncon/qsRlRRVY2oKSjIE6/gpWII9ollSbTZIoXA7khTUFNzueuE1pgYicNbpWHZzByZD1JQKYWDgDYzmchiNNKDV44QWC8PvacSpkXyCnz05eShCZ02G1ebaz4V4MkzwPx6KYzybK5vgL2w/qyF7MiTk+R07G5G9MAG7TPyXUVzm+daGkphojGPlVAiN+RrmYzwBLkBmHlfm1GmkXnxhlkSM/A6jwRfCly+9JDydYuHLUqrSFHIl6cxVKm2Emfi/snDZw2U0gAyPFSsn11rAsprvIOkxWUzPil235VZHHBM8Gknj7nXNIgyWybFqaX7aaCRVhrEYSuADxu+iZ0FVA0r1n+lchd1uO+ynjoJ7zPp1a6AO3x4JYWJyEttDo1i/fCWG00m0B/J9EVZFx8RkEAcPDWLlihVitThtHBS0xOO4wxVBor4Op9CA58YmcfBAH9p1C7b4XUg6WWAQRTY6ijWrVhRIhg+7VDVY27ENkz0pLFvmhWcyjZTPhmSrG51dfgz2B0sa0Wmp/3lAsYjqMl1n130GypTMTDkITyY5CsvoYegOL7Lr3wH7yR9Aa95QvOGCjD/B/bVPaMi12FDf2CBebncXAoGbcbq/F84zUQwFY3BqaSRzMWxt94o8j9NJwspXTAqdNbGfOSiKfUk9GZngdzLB3xyAu4LMjRbJIHsiBMVvF/1Iim1m20ApT4YqzL64E3pOQ7gxWXgfP5MIB+Guq4c+FerMS8S8hszJE4I02OdSseOf+ZrHHodz166S7y0GbQLLZWXJLBc6UjJfFnGw6lGSjiyKuVY8GVVVxSJ3KXIylxOXteOfoRaGoUopJ19OkpEjm1mKy4QY8xOVMJmg0QMaPHY4rJZ8CbO9ulAczwPlvlkiyDJoVqpxBceVG70E57LlOGZxYbOahnrkKNq7ukTzWr8G7BucgDc4jhvTcWzdvg0jSQUNnhyc9nxl3vHj+YKALdt2oLHOP+M7+wf6kZu4gPV37caq7i6xYnw93o9lu3Zg32QMh0MxjI1MIJXJQUMDei7qaAsPw400xvvOYnVHCxr0dkSGMljd6oZtPAXHujoMWXU0ZlJiH6QhmnXexdCyheQibGJ4GGX8mR+pcFZhS6fgOvkUdJsX2R0fhnX4ELS65YDd6InKXIuBZMIZWDI6MvUzk/p2uwMbl3fhQzccxrff8CGZc2BlvRtvXQa8/vrrsNsz8PomYLONoKGhviBXM6eY5zw8GSb4fzwZwyqXHXdXSPCL8NhgArnzUdhW+mHtmpaOL0UyxnwSf26c9EJbYweyU7I7U55MPBJG0/KVIjcDNlA+/kRZiRjjvsieOI5QtjY2wLFzJxYChs74LPIliziM6sWs0GRIjfd8LcVvL1VOJh7Pe8lmTmaB4I3BVXo55WQSAI3tUs7bKPUdxUTAG5SeFX/P/EQ15YPMx7T6naIznTF6WcI8143OHhWGsbhC27Fjh/guGnsSHB+o+k1b8WQsjdXhceiDF0UosaWtDc++/BrGJ2PYnIxgvceJDbt2igfsyOikyAuRvElcHZ2daI+qcDink6c8LpYt08OxNa9EZ3u7+L1Yges6NnidqI+F0BUbxLmWZkT9dbAqFvhgQWhoFMd6+uH2tGDiAFDnGMMGnwPDag7+bU1obnFjuDco8k5ym+U8mfxkzOoh7gAKO4rQVhKwlS95tU/2wz8yjIg/AF/rTYDNCcv4SaidN8yuWpOJ+qmCgtzFOLKNFrqBhveJgxHEdsvyMDojXrg6V2H9uhVw2/PXeWKC+a4DIu/FvKLPP4pE4hRaWtaWDesUjq2K+/sUE/zhBG7xu7GzQoJfVzXkzkSghTJwbGuEpW72SIgCyRTySMbZMSq8CSfUVisw1SRO74ZkI8Nl4f6LSL+xH/ry1SUlYkp5MhlWHgZD8LEfZgmeZ26Dxpcv2R/FohZ6OXyeeI8z/2gcgVzLsuZaVJclEvl8qunJLBBcsVNptFx/iEzw1bLmvbgCjJ7D4cOHRU6IJcPVfi/zMTTuBD0Z6nbNNUpAlifLUcV8aPhgUF6GifxwoAE/Gw+je6QfgUwSu269FZrLje+MhnExo2LXUC/2bFgvHjD50F4Yi8CRGMeIH0L2gQ+gve+88KwIPnzMo/C4tm7fgTf2DRY+y/0l6fHvXCHu2rgRNwbq8JWhSTzU5MdjJ07jRDqF7i3r0TKqY3uDDfXxHCbq7DjltWDk7DhSJzVcmEhga6cfxwYjyKXUkv0wgmTmk5OZKg/Or4oryMBoKqy9z8AzdArp9Q8jEXkOmrUOSmIcSioErbF4Rnx+3o30ZCgfowXTsKR0EZKcfhvnRzLXwibLJOzJMDYsb4FrKhfCc1dfX4dsthmdnTeJysgLF04jlYrO6IA3hnWq9WT491eiSRyOp3F/Q+UEv5bIIXs8CMVugWN3ExTHHM2YJXIynjELQnUhaLaZZKym0lDVHOzpDJLPPQur3wfP/feJ0ciVIJ7fiQmkz56F98EHFyV8Wc0IZL64UGOIm78j6XAhx2tCopHXYSlKpY2ohZ2Kx+OiGm8x8lm43qvLKp08Y0d+rUhGejK8QehVcfXP8Nh8O1rpydywIr+yZsVYKMmZJ7ayJMMVFt17WZ5MSRd6L7yh6NmdUYGfDY2ju/881jQEsHXPrejPavjpcBC+SBBbRvqwZfv2gu5QPvw1gMNnLuA9uzqwc+OawjmzTyXZGVZgaJLkTrHMZJYJXh36eBJ6m0f8nTkoblNqp1ETjcRz4sB+bLVacE/TahzrjyPoBn5qy2LVJj+2tvhwn8cJfls4mcPfP3MObQGXqJ47fTGCaDSFC5YB8bs2v1N4fEwjzDcnIzvM2aFvnHpZQDoK++kfCaKJrLsFPhLK5GNQvV5Yx05Ca1wtPJoZEEZWdujHoA7lwxO2pIgUGd9YeF82FQesTXB6feWqE0TRRX1DC3y+brhda/Id8JOTYgHB+8xYMVUppMoE/xOhOEazOby3OYCmCgl+dSyJ7OkIbJ0eWFeWGw89k2SKp2DachZ4ghb0dMdneDl8XzYahz2dRfInP4Ft5SqgtWVOghHfR0P52utw3X8frAsQd1wI5FA/Rkik9JBU9S4ulZaks5BSaSNqYadisdiSjV6+nLiiSpiNkOEFhswWMk2yGsiu/Ndee00YU4bH5uuasi9mIp4pJNsdhnBZsQHhzU9yYaiQZMbVFHthSDpSG+z1SAKPXRzCmuE+3LJ2NbqXL8fLkSTeCMewcnIEy7QsUsuWFeK09E5IUKORpCga2LVp3YwhYSSZ/oFBpCYHRd5Lll+zGsqX0pA8Oo6e0ThGg2MirMMCBwlKg0z09cFKocxcCzJ9KdzlsMPZ6EZmbQAnM1m8FEngyWAMGzxOtGqKIJG3bW4VD0Zfk4ozF9Lo7AgIb+/AxRDGYxmkshoGQgmsaXKhxe8Q585TYtVdDCGlLqrGiqRlIv2wnXoEesNq5Fa9Cdr4d0XC3ZLLQrU5YB86jtyqe8psNU8emcSImF+fv1D6FAFNfW+BZJyiP8RTN/vBF4rPhhkrFiVfxmwM65DAiyumGBKh185RC8YVdlTVRIOlQ1Hw8xUS/EzCM/eijiRh31AHa3NlT6GUJyMl/72TdiS9KaSduRlFAZqmIn34GKzne+F+z88hnstAjUYqfo/YrqpCeeUV6B3tsBdVndUSxYn/4mmUxlJpPnsyPC2Jnx7PfL0HPutLbafiS9DtfyXgsleXVfp7rZP/JBjOZ+DshIXK1rAvxmWzwOfMG0mR+C/RjEkyYHiMx0Vvhd/NAV68mfnfXEk9NxnFUz0XsDM2ibv37II9EMD3xiMIJpLYPNKPZQGKEm4pdNbLhk16J60N3Vg+zoT4zJ6b8bFRDKRyuOeGfPhMgjmRuvEwjtiCqAu0ipAddYuMo5xpBJe3r0Zi0g57PIl2txWujQ2ic9+lKLjJZceNfrdISB+Pp/DtvgmkNBWvR5PY5KXHYoHfoWBTh1+8xPeqGs6OxfCt19g0msO58YTwgLxOqyCoNr9D/NvicwjCLmDqsPLhssR0P8zQftj6XkRu5V3Q2rYb+jcUWNQc9FwUipqGXr9y1rWT5MEwWHYibzRFDoOKMDOc0LyEP1/ZVA6eqQXFTPAD0/tL4tJL9MrIiil3QyP8KzWkB/uFB0mjR09aVEfVN2C/049NDXV4S1OgfII/TTWFEHRVz6tZF/UjlfxMiZyMLFv2jdvR25rvQyqQjKqhYX8PUv1WtN11L+yrV0M/dbz8CGoDOJwM2Sz03bsv6Wp8riT8UpVKX4pwmdf0ZGqLakcwzxd80OTwLa5a5iNbXYyRaEqEguRDJDyZIlkZGRcmGdBjoTFhs5kMlxE/GRrHiz0X8Ca7jjtuuwUjuoKfDIdQn4pjw/BFrDF0/nO7JAESEz0PPghPnhxD+1ReyNhzY9E1rF2/cQbB8Jz2HjuNeKQfu9atw7LVa6A68/tPj45d/0yibly1HT3nY7CqWbS1+ODc0jhLlp/70+m0i1d6MI5Eqwt9qQxeCscRSGYR0BTs1nXYps6PzWpBe8CNFr8T927Ih08o0TIWS2M0mhEez+HBKBJpVUjwtPodaHHGoaQ1QVAWiwfZbBRQM7CdewxKZBDZLe+F7i8KceoqbFlVeDlq4zrG2UrdCYwFIXcoDlXPkwyrsdAzBEWdHVYjmGdyNcwOrQivwCAaWUlahmXI35uIinNyl80mVtEs6uB1PTAexKNjIayKTMIx0IPXPZ4ZK+yCengojeyJMCyNDtjX1kExFCpUQqnqMv5bF3GDMc+YLy1ihfydJZOD8uTzcExGkdtwI3wrVhorISp+T7anB5mTp5C58cZLnlOYbwnzQkulL0V1mdf0ZBaHctVHErXwZOhBMLlPI8yRAYtt+Bw1JP2lJ5M1VJcxDEKlAM50ISnQC+FNTe+FNyul+r/dcxGHLg7gfW0N2LF+HV6PpfBaJIE10SAawpPYNKWCLOfOsHqMD4Rx7sxwJIVbVjXOIDWGz1YorhkGVjZmpi4m4OxYi47WNijs65l6D/WL6Pavat6CyOEoVnqBXLcXrk2tc65e6dWROLobPWIi4wt9KbyqWzE6MIENXhe2eJ1otdsKfTLSdDOPtazeLV4S8XQOo7GMOL8D4RRC4QSO9muYcMfRofRhbfRr8Pn8sG/7BSjOmSFOseVMHBaGzEJ90La9tfQOs9R3LA1Lzg7NloYl4IASsEPncc6Y5zVV6qtpyCRzcPpLlU/niwiMJKOq0VnvikwRjN9qEUPkjM/BvlgaB1ULfmFNN1a5HILwpbFjPo3/XReoQ6sWgD9ih2dTE6ztsw3efEgmFLPj9d4oYhcDiK2nbzNFPOEIlj17Eli+Bxf3roP3fE70yOS3MbuL3wg1FELy2efgvvsu0RtzOXpWFvOd1ZZKS9IRIzdqlJPxXeU9Mld0Tqbc4LLFQA494w1CI88+GN48iwGT/hvbpr0Eh00R2mW86WQynatUhuXoPbEaTDZFZlQV/3T0NPqDIXxiwyq0tbbihxMRTKbS2DoxhAYF2Lhrp7jpKSzIG5weBsN7JBBJMAzPBeNZseonAfHF/Avfc/LIsEj888Hj73nMy5o64Pa14KRDA+wW6FkVExNhsa36ukY4443IHg9jWYsb6kY/Tlu1OQkmls4hlsoJr46gXM1ujx31ehrLm+twPJESlXEBqwXrnHZkaeA1XRBOKXidNqziq8mDeLwR4+N+NOsK1rqTcJ19AWc878LB+E5YX59Eiz8mQmytvnyoTVSDsRAgC+iKBj1QuvFPDK0bSsACDzRkYOma8kgtgDJVmDDdQ2JBKh4DdDtsU6HRucJl2ez4jHeEpwhmpdOOG3wufG00LBYZ1Ap9NBjHiOjg96PJnn8seX15DfkSOlaRGKKHRpCaiOKwNwyc60NjMG/oiiX0qyGZMyNZ7DvViFN6EOmQB2MtGro8GlzjUViefxKJ9jro99wG9dRPhZSM3ekyeG1lQngcsfz4E3Bs2gj7qlXQjh275InrpWzGLFcqzWfbqCrN55P5taUkm0QiUVZh5GrCFU0yS+XJ8KFg7oIv49CzxW4/mVGFjIzRk6EKczyZ95YIxnyZWORNz7yHdH8jiSS+cOiEIJrf2bMVSbsL/zIcRIOawcaBC2hvbhLeDz8ne4oYG6b3wu0Z8z0MMbntCi72nBGrH4b/5ArIbrUgGmfvT484D+zHsfWkEO7QgTArpRT09/bjbKofalqB0ueGOx5H04Z6uDc3oJ7VWpOxOc/FcDglhCKNeZS8cdGxzGUXr7vrfTibTONIJIH90ST+cyyK7QEXVrsdFZWDxVhjAN2JY1jLKq21G3HzpnfjBk0XQpU8foba3ugLYyKeRZczhFa1H2tCCUTb16I+p6NU5a8ey0CnFweXKPvFlE6pzkMonN5puZhEKAinmx5luopw2Uz9smBOxffHo1jrduCOgFtslR+ZVHU8rduwQtNEB7+nXII/noP9VApNjU2w37oaK6x5sVPeGwy/0tgZ8wjMN5QytEaSee18GtmcBStdFvR4Mjg94kBzPIiVR88gd8+DGPf3Q+NbExmhvFwgizK9a9x26oUXoDidQr9M/u5SejJiDDYLRGpYkSrPMZ9PqSrN6AAjFrwWPPfyPf5FlEovlQLzdU0ylyJcxpuABp8X7KabbiqIPy7F9hnKqXPbC/0SRHBiDMMjw2hcu0KsdJgXIdHQe5EP2+D4BP7x2BnU+7z41LatOJ7K4ZWxEDZmEnAP92PtmjXCW6EXR3KhB8Ybmjkd41RRiQujYaSCw1DrfMJDkx4O3xMOTqJvYhT3busWOSAaq1QwAm1LPRCK4tyFHqTiSWzYsg2nfnIKjctVtNzeAVtnvoIqkIUQyWQ5s6XCwzIcSaO9zlnx+josCjZ7XdjkcaLPN4FGqwUvRJJ4OpzABo8Dmz1OtEyt4mcgl0bdxQNQEjZkb/8VJCM/ESEbFhZwzDRfWzqmq/3OX/TBOpKEJRHHgbQbZ1++KK4TPT3p8TR57SKvAZ01YVZYA94p8vAK4lUKJCP334JEOASXt75MrqWousyQk5nMqvj+RBTr3Q7cHnDnFzhT7/tRUgWzSexFknmrYqjDCWTPRWHr9sLanb8ufKcswZX3uSzRZTMo72tjSIehWnk9pNHLqGxu1YQDlnVnRO4l0DeAoVvWomP9CmCoP5+3iWfgMWjtlfNksidOINc/AO/D764okFlLyMXXpSI2eo+s2JQLWEYd5JRQEg4hxT3nWypthssuAWTX/0LBC83wmJTCL05AztUwOZ8mTFmePDY+Lr5vdHxSPPhs6mS4Qz6cx3vO41/7hrG2ox3vW7scjwXjYgrlrsg4HPEoNk01UUrdMrrL9F7Ye1Fqv7mSfe3oGaxf1oytWzcWHmhZ2hwJxbBs+UqsWpWXu09diMLe6UE0nUDfhQu4qcWLNmsnEvtCCPg01L+pA/bG6dWTn3pXXFWpGvxT0xFLYSicwtbOQFWLCFFSarWgLTyJbfVexD0+nE6r+M5YFPVUpqaKgdsh3qPER+E5/hNEFQX9rfdima8LiPB8p2C1zg4l0HMLuKxoxRAsTe3YvaEdd9R3i3wRr9dAKIX9fWHYo1lsUBOIZNLwurLIudzIsdHSPuXJTDWLGvefJONpayqjsDzT8MopmhPZHL4/EcNmjwO3+vOGnqBqNtFhUXCjRS1JMKway52NQJtMwbG5HpYGZ0Vjx4UJX4XplJOToriF9xHvH2rPMa8j750N7QrOTVhwPpdDciiDddYIEnctR8Zvmy4KoM+VyMAthDGnj3VW4ntsDKlXXoHn7W+HxZCsvtSezKUmGQkZJlvKUunEEiowMxfNfeE9MB/SZw8f7x2j/ZkvrslwGW9sJtyZA6EeGFfwpU7sYj0Z5mO6G9zCoDMxy5uquakJsbOD8G/oFuQiVy4ky5cPH8FPYxnsWbMaN7c24d9Hw6jTNWwZ6oPXYcfGnTvFPsnOf3pANBrF+84HiATGpjLmWCy+RuzasKpg1PlZEh49n40bOpCa4ml2tKsTKYRWKXhj/zHUeesRiDYgF8ui88YWnL94YWoKpOEccbyx1YJIBZJhboXVYXN5MhI8X4OD/biYA4IjmjB8rQ0NWNPYiKAzgHOpDF6MJLAz0YMdw8/D1roWEbcV6gWObLZNGfBkSZKRsAV7kW3fBlVPCemX5Y1u8SK4T9E3xjEUswE5C3ocwORgBi/0n0fAa0VTJAuHRUdjMgsf81ZToEBk89oOaNrMXIvcJhWdp6+RC+NZDa+MR7Hd68JNftd0gj+awoF43stZbVNgyZYgmGQOmeMhUTXm2N0MpWQeqDSKe3NkfpDGjvcNqwe5EGp1jGJb6yi6LsYQtvfjpvtuxHn1rMhpFebKaKogGW9dw4zeHArAFv6bwpePPwHn7t2wFTUyXy5P5nLkgYpDdJVKpXt6egSJyBAnPZ3iECejL6XGiswH/M7f/M3fxFe+8hWxfywY+uIXvzhjKmYpPPPMM/j0pz8tiJFE9773vU8MPVtI0+plD5ctdQkzDRbLDsnAN954Y8XZ9Ishmfy45RTW1Ss4ePCEOPlyBbl8WSfaOrowOdQnts8VxHMHD+FVuw93b1yPRpcT3x8PYytycPadR3tHuyhtlp3/XDXI3ply301y4apkzcatOHBkQjQ10oCQoPh9srQ50hdCdGrAVOZCBKNqCGdPDWJ1+xo07YvC1qChrtsL35YmKAOlScHPuTI5FXCWXnWNRdOwUVrFPbu8uXh7fMjYH+RxurFt+0a017lEpR89sonxcURCZ9HltGN3rgeW1CB+tvLtGHG60JHpg0OXEjjuvLxLmUWgks3AGh1GeueHoWmnZ5+/aBaORA5uhwUN9S6sv3UZxkOdyMKHSM6HoXMjuBjK4uDrg3BYc1jpiqMvNIxkNI3lznpomYslvpXGbfp+HldteDTZjjvrrLg5kL+O2akO/mHRwe8XVYSJ5OzzzYVA9lQYVgqPrvZX1ZNSCdK48MXzTMUHMV/o2CFse+VZNLSsx+tbOa00ikwuI547STK5ZEr0ynj99SW9NtFN//QzsDY1wrFjx+xzfRk8GRlSvpSopoTZVlQqTe9ChjiZz+E2aK+kTeJzzDD5YvCnf/qn+O53vyueOQ5a/Iu/+As8/PDDIoxPm1MKHLv8tre9DZ/97Gfxu7/7u+K4/umf/km0TVD9/br2ZJgIZYkwk2UMj81VbbMYkokksxidCGJE74fH5RTkwj4WVqCceG2gMB2T4YqDF/pwqmUZ7u7uRIy9ENEkbk5HkRsZEp4WyYIXkL0zsvO/0uRQvpcPEwsJzk+mReNiJJQPr3E1ZCxtlrIyqVACFw/1YLQ9hU3NWzD5WhBRvw1tb1mG7Ol8ZZnswSkG58rQk6mUj6EoZrlksASVppmg5jFfcCrQmFugp+T1ipeo3kmGkT34HSRSIZxz70HjQBDeQAwDdh2nrc2IjobRkW3EtlwS5Xrb7aFB6DYHlPrV0CYPzxJZzfXHC8bS2uEREvhWqxtOm4rOZh9cTQ6stivounU5xiIRDA3ZMDwexFjaht4jYbQ6RmAfHxONoyT3Vp8zX+Y8lfgfzuTww8kMtjlC2DMV7YipmlBQJl+wg5+hQI/FghHDhFChntwbQ24wAfu6gCCZpQa/g4sYbziM9Sf7cWr3ejTuuQ/2/meEseuP9It7py3dhqSWRAIhwOOA1dCoLKX+icyBA6Jk2cc8TInrfzk8mctRMr2QYgOn0zmjVJqeC6/Bf/zHf+Bzn/tcQaGdXtC99947pxp8qf36whe+gE996lMibE/8/u//Pv7u7/5OkAYJqBT+4A/+AHfeeaf4V+JXf/VXsVBc0SRD5ifbzwVeDIaOaGTlpMhqbuxqJ2MWQ4S+Dp2AmmAznFVsRwpSEqywSmVzwqt648JF9HWvxa6mBvSmsqizALsmh4B0XnlZNOAdOCBuUNk7Uw4sg6anw9WOFM4bCE4iF+UsmsSM4gBj3040nsDxJ87A5XejW12B9IEw7FsbMGFXYfXYRQm0UZK9GJyQyf6OchiKpGY0ghrPrygT1rRC9Y1U3baPXOQCeQaUyABcpx6Bs3kFXDd9BI1WuyDV4eFX0Dx5At0pH8bPnsRBj4oXo1Fsbw5jq9+NFU77jKIE5+RFqIEOWBz1s+bPUERSG5eJex229jwLGOX+RQmzmhfyZLGAGneiRVWg+VeiecNy9A3tR9LuEDmeI4NRxNMqWt2jaHHFcTwVwutqFnc3+dCFlMjLjGZy+NFkDN1OO+6p9xTyLx6LAunI6BkV2ZNh0cXv2NkEi3cO4clEAsnHH4f7zW+ekQOZC+z10U6cQPLkSSRu2oygP4RNPo+4p9Y1rkNyPCk8y5yWE/fb8fGDCOeS4vniYoiLAVHtx3Hg/QNIHzyYF74sE7O/HJ7M5SAZYjEVbYqiiMUxX7/zO7+DT3ziE3jzm98snpW//du/xUc+8hHcf//9eOSRR6reJlMGXOTu3bu38DueGy7A6a2UAonuueeew//7f/9PXDuSHiMii1koXNHhsmo8DdmhzjDMDTfcUKi2qQayYXI+4wTyc1qOo3ckAeZhmeBj3HvGja2pOHDoCLIWC/o6V2BFXQA9qQx22BW4LvTA4/Ni3Y4dwugy7MWSallWXYlE+X66vNwHGgAeM5P+m9pmFwdIhCYncPbUOez1tgFRF3KxNDoeWIF+Poz94XzpLntC1OlBVKU8mdFkrmL5cnHSv3AqNA1vvPGGWCzccssthUSmHMGcz2UAluEDsF14AbkVd0Br31kIx5B0mdtyOkNIxK24b+M6DA2fx1BkEqfHUjjgCcDt8WBHYwC7G+vRrCdhj01Abd8s9MsUxTpj/oxa8GIAxWeD4sw/AhwhkM1O5s83bUVBITN/PhKRMBo7u+FyeOB36tjYSRHKfDKcqtsD42GcDrnx2HgUzSkNr19MYNyTRig4gjN2DbfXe3BXnXsGGdKbSVINIQlk9k8ISRv75nrhWc2lCZZ84gnkRkagxWJVkww/52LlmdWKugcfRDp3ivHLgnYZwfuY12hZ8zJMuCfQqTuQyCTFvUajxWdGnxxFvb8O7t598N52W0Xhy+vBk5E2aim/1+v1iiKBX/zFX8QHPvABEeakDZgPSDBE8SgV/jdzQqUgw3ZSkYQRGj67H/3oR/FXf/VXC9Jnu6I9mblIhjkMhsdoiMjW8z0B4oGZqquv5kFgQp0rciKu2XDnttVYtWqmC8uboffcGbhb6tHrdqNesSCharhTySJ+7jzaly0TK0ISo+xbqVQLb5w5I0cnk2R4AyWSKeguP/bu3gSn01Fy8FrPuX402pqhRe2wdrmw/J4u2BxWaMPRvM6ZMGiKmIRYrtqukifDBkyu5KmwXAzuM/NEPM8kGKM2HL0ElhtDzcLW+ySU8EVkN7+nTOPktKwLV1U22xp0qnHc7t4tqvlOToZw5Ow4nrA6cGO2B1tQh2XOFgREbD4/hpl3Or0FlgNLWALTSZ1ZnkxBJTqv68XKsmWbtov35c9vulB4QHFP3ePAyUQnfmVFK7Z4HKJ/6tG+ZTiUAVamVZwemcQ5JZgPrwmNNid0G5COqHAPaLDe5IW1s7ru/dQrr0LPZPLji6usvtTTaSSefBLWYAjOj/wirK2t0AZPlJyQaVRo1tMZ+NuaCl43Q9InX5pA5KWXcDpQD0s0isZz58r25lxqT6YW8i7VfGct8kBxg6wMbYZx/Pl8RYaLbUo5r0vef1/+8pdF8p+RIdqqu+66S3i75UJs11wJs5yqSePLJCZfC1ktGccJVLpB+HCR+cnuBIeBuVRgVUfjjH2SopKWtg5c8HipzYE2i45bk0FEx8ZEMp7hHxKjzN9U+l7pNZFYSDA00kwGMrnPkMbGXTej4XRYTOQ0goad38F/13dvwhMDI/Dc2oLW7dPli+x7odwVk8qKTRGz4Mt5MixjpipwKTJmqKypWMxyanQ1b04eH0NkxZ+z8ctTITiOPikk+LPbPwQ4yq/IjZ+X3fRc6XUvWyZe92oaRoMhRN54GvttHfjWmBetrx/CBpeGbZZRtLW0QR0wEEyjC4qD25TFBIbpmEJWZrqEWaWacy4nynjpGVGuxlhC3ZfO4qdhBbe449jqdYoE/yvpDMYcTfhkO7CivlN4bTMaR88HkR1IoM+WxXO2HDZSHy6UEvk1Y99VMTKnTyN79gx87343Eo8/Dj07N8lo4TASP/sZLIEAIjfdCMtUWNeoXVYMsfjiALRECjZvPukv5fEbwxHYW1vR9cEPIxiJzOjNoSGiMZQ9IdeDJ1MLcUx9KkezmKmYjLIQLJ8ufjalZmIxmA/m+WN4jgRDbN26Fe9///vxk5/85OojmYVUl5F0eEPzxmaIaL7sboS8Gfkd5erVpVoy2Z/7S4mYrNUF68AgGj2OGRVtJADn1p3oPx+BomnYiixWDF1Apq5OEAyTeHwvt8Hu/UqQ/Q2SjAh+nuQqRfrCWYvo0zGeR640ub8BfwAtgVXoG4jDuyqAth0zXWbaUPk5ETKbIplyngzHJSc1HZ4iIUbRhGnIx/DhINGScHmT8udS17ku2YfmwWegrdoBbdXdZQQsZ0ISYKmZMryWHW5gecCCensUdzT4sV9pwivBVjxzbhzdp/Zh75gLHU4PXG43PN1e6IycFYa2TU/HZJ/MtECmjmwqDY+/DpYpQ2J874VUFj8JxnCHT8NyPTOd4Afw7roUAlZHITwoG0c3BXLIhjVEVzhxSAvCk8xhMp7FqZG48IBk4yi9nS0dfvFZQh0fR+rFF+G+915BGGKeyxyeTG54GMnHHoN97To4b7kZ+vPPz5L6L0Uy4neJDCtHYDFUFWbPnUN2cBD+N78ZTo8H7XxN9ebIxDXvXS6EGFngQof3JL31SyGUebnCZbX4zsQi+2RYKMBn8PHHH8c73vEO8TsuTp999llROSbBa8ZjYD6XdoVN67RlRvC/F6o+cMV7MkaS4YFyhc7cA5NXi2kQInhjlDOsBOPQJA+CDxI9Ju7T8aGIUBFmuIn7RKPudnugbNmJN9hTYLdijaaibuAiHJThb20VJYOseWdRQqVVj+zx4eqDHcQkUd5sDH3xXLDAgP9N401RTKOBl9VbK7pXQQv6kEqp2HRTG44eyXtgRmgaZ7NMGX+7VXgy5RL/dosi5pkwZFYse8J8zLauQGEBIMvHGR4jaGyKDhDKxZexfPhJTCy/Bw2rbqjuATXwVKmZMoRl/AS0htXQo6fQWteEB5qW4dbAKYzoDTg53IofZ2Pwp1JoD02i7eQg6rwTcDrCaGwMzAqXSU9GkEw6BY+hT0RKxpxPZYTm2L31HnSqKvrjNrw0FsEypw1vqvciFnHMatzkzJrsmQhsXR40LPfCeyKMFR4H9mxqEX9PZlXROPpiTxAXJpJY3eyBz2nL96I89jicO3bAPrXogM0GXc1V9Hoo8+K65RY4pkpPS0r9F41hNpKMzdswvRij8OVzz8OxeROsRStsY+JaanzJ54dxfi46uLAyyufXwsO5XCRTCxmb+BLIyvz3//7f8fGPf1xEQhhR+LM/+zNx7j/2sY8V3vPrv/7rwuZQHJf4H//jf+C9730vbr75ZtEG8vzzz+Nb3/oWvvrVr17bJMMEuaztJjsv1Q1aKu8jV+MyPEbDbpSjYRMmV5nSqHOfLja141AshS6nDWssWZzrG0R3XUCs5PiQsb68Us8OQS+HTZ38DGPgDDfws7wBSHJMxHF/mevgwyTFOfkzP8f93bB6K0IXNASa7Fi+tQHJHBvr8mKUBVKZsqHyPxV75XAZQWFLljG3G35H6X2pQk3i279/v1i9cvgb/2XScMb2cilYTv1EjEPuX/Ee2PzzmZQ4vW8lRzAz9j9+Emr3XijB70N35I2gzeZClxbH2ogNSZsfZwMaTq1vw4taGnWxdrSeOoeGXC+aGryw24NoaIhNdfxPbXbKk2lqqZ9BMueSaTyXiuMtDV6scztweAJ4PFaPu1qd2OPLl3NbFCdyamR6uFhPVJCMfWMdrE1TQ+6YYzGcIs4muhhMIp1T8dCO9jzBaBoSTz0Na3MTHLt2TZ+RMp4Mz1P69deROXYcnre+FbapsEklqf9iSDkZS8AJq8JFSBbJxx4XBGMRbt7cRTtcIMnKS0YlZE8IvXGCoTcZWmPocylwrYTLVFUVz/liSebDH2avmIZ/+Id/EPliEg09GaM9kwtZCfbIfPOb3xTNl5/5zGeEfeN/v+c977k2SUaGouiCy/LXpf4OI8mI8uSp4V2MhzK0VXwDcfVen5vAyeQEtu/YgYNWN47HUkJZt3l0AIcnJuHxBRCN9ooLyLDeXAPRaJCloisJhvvEnAbd22KC4kPEFW84m0W9E1OTPXNY370D4+eS6Fpfj5YV+RG8dn2aEKyGkBTVf6fDZdOeTDmvjqrKxcn/sVhG5GLURAQvHzok3HOGBeVDPoO04mOwHv8BdHcDVDZJ9sTE4LRqMVMXzA1NpzwKx1znQzBKbBhKNgnNw/CpAkzJ/9PrSU8Mi8IGFxRsczqxZ00zJjmK4KIHw22bMZJT0JaOoSGSw/Arz8Gp+tEadwuD6HaryKaS8NRPezK9OT+eT2l4sM2L1S47Xosm8VLUirs9cezxT/e2CGWCbAp6amq4GHtUdjVBMQwXcytA2pAne/r0BIbCabx7R7sImxHpN96AHo3A89BDMxdXdrsw/kbolIh55hmoY+PwvutBWA3VlsVFLoWufpnkN1SZqWTZREYUI1igIskwm9sN5403Aq+9XPUiTxp9kgjj/XwJxYVoVBg9LoyYW+WCSno5JJ+FGu1aGPzLES6LxfKitIvJyUgwv8JXObBcuRjvfOc7xWspcEXnZJgP4U1DlmV4bKlWO+VIRiYwCRpLxiiLEYnFcehUD9683CZW7E8mchhIZXAfV/NUQVZVuJ129I7FsGt5l1gxzEUwrFpjWIllzEzWkVDpSZGg2ERV/Hmet8mkBqc7h/2vvYqG+ga0ulYjeDGFtXta4J9aJctmTIINmcaGfdE8ViCZfHVZRU+mREMm9crsmRgOHDgj9lMmGo37KYza2AlYzvwMWteN0JffJvIgVkscOWrczwPTngzpgmFOlibnD4pejNa0Dko2nm/EVPKGhv0x2YlQYRu2ZXmBScrp73FOoLXVgwHVhRMJL17Vb0Z353p4B4KoH8/gxPETyKRGoMcjmIxEYXG50a9Y8XwigHv9Maxw2fFYKI7BdA4P1mVQp8/0KoQyQSyOzOlxWJpcsK8NzOreZxsne2VYaff4yXFEUzm8e0ebGHUgrltvLzJHj8H70Ltm9aIoVpsgFWPvTOJnj0GxWsT7LWUUI2aNXy6RmxGLjUQGitcJ37nzYryz9z0PC+HLSlL/pa5Z8XMuhFcDAfGScitS9oZhYT73xiFhXHjNl9QuJWo1FZMwVZhrCIaJpME3drDXimT4MMjvI3mUIgb2pjz96kH4PX7cc/sN0NlTkI7jfoeOgRPHCs2dfk8AHV0tCATyYa1ykBMuSTIkNa5aGPZiopT9MOWKGvg94wkVyXAfunevQXbCgzRUbNzbDodrNiFR8oXTOo1g+Kxg71jNFM8XNpTbX0rLcOKlcd9fO3YGSEZwwz2l+5MUXUNL5DCUM6egbXgAetOaGUKW6Uy1JDPTUOXLRWXILCC0tizjp5Bb93Yo6Qh0G0l2ikCjVmiZZKHAwdI2rV9GUMl5ld0hhoRtSU9g1KnhJacLp5od2LFpAxonziDb68FEMIhnLvTjhMuPez0DQtvsP7SwuBY/1xKAlqAGm2Wm1zCsIjsShG2VXwwXKwXuTUwDfnR0VHzmXdvbCtVlIgfy9DNw33XnDI+kAEO4TJ2YEBVktvYOuO68Ix9KKz6Lhdk4ygwPplTYLJtMCjkZG0cRH+mF+xc+DYtsFK6y5F96TtXIrXBBx5eQqUkmC6E12ZsjCWeuuTnXSuI/Ho+LRfWl9spqgSsuXMYLxtwLy+wYy2VSfTFKyXNBhoj40DBZXYrMjBVT9R2rsN3mK/TYrI1OFpqk+FmSQ9buw/mjI2LbDPeVAvMuJBSu4piUkzkNEg1JtdyDJMupJ5Mqblm/Dol+B+pa7eje3AhLmRG8UlpmxnaYk5liGRpfrULiv9iT4UqT+zoQzOAjb9qDhoYSuaZMDI6T34c3PYbcjj+AxTuTMNknk5vPZTWsvgXJiAqzfKKePTbib3XLYel7ERAkk4c+rEFT8u+z0ospMKs8TkNzpM2BLa406hw6MnEVk1DwbFJBrnEv2jpWIdKSw/utGqKTQ/jmSAaBzEnc5XViItkEpysOu21qH7MasqdC0JMalE5rWYIR365q2DeawT1tCt62qVWQr9hGJlPIgdhXry79WbtN9L9k+/qQfPIpOHdsFzmbSk29+VM5tZ9T52CG4vLU39VYQoRR/S8dQXjLStgMkibiGlShp7YQsUqh0F2kZMxFl8zlyJCyUcnYaOCvlcR/LBYTlWWXWuizFriiwmVkb1aPSekDMrm44Zd4BHO5cFkpgiEZMCfEfWO1xcv9SbR5HIIcGEvmzU/Q62BBAslhMp4RnoNM0heD8WgSKcMF9GBIUgyRURaGlWjlbixuK19OncNExArrmIKO7XVo7q4cTqDhEo2PBogVpgyXUc6/QgmzUSSTDzwJxhVoQEt7C7qbS5RiRwZhPfEDqJ4OXGi6C+vds1fhLEKYT07GuN9iHouhwszKUFnzBqEdpqTC0G1TEjKRDEBPxpbm2BihU2bY0tS/M/tvRIWZ4kSDDmzxuNCSGsTzLk2UJlsUC57VLMh61+Jm/3nc1bStMJY3HD4Jmy2DZncnmiad8LYE4NzWAUxkyzb7slz52FAKDrcV921uLRRmiNX8s89C8XryOZAyoLdCWZfs2bNw33kn7HOIKRaTTLEHI//lcWajCdQPhZBdvhLJjSuKNzRn4t/4fYsx+rI3hy85JEx6OSQcPofGuTmXoxmzFsQWj8eviamYV5Qnw5JdJrq5eqE3IC/aUk3HLIdK25eCm/QuGEIjCY1EQljutwqpFN7wBPfXSA4OadRLGG0eJ70iVopxu5x3w1Jsei+Vck58qLgvTU3NSIXrkEmcxZ5bO1BnyL/Mh2TUGdVleZKxOCt7MuPhCF48cQ6b1q5Fxt2MFi08swmTxnToICznn4G28g6ozdugDzxd8iFkMyZDdgtJ/BOFcJmWg4Uhrc35yhclTUHHfLgsdzEOi8h6qLB02ovkWiqRjEuUMXOey7F4DhPeOnyk2Y9zqSwOxFJi3MDRtB22jI5NLW3Y3tWFUCiJ2MAQLOc0DLjGMTF+Af6MGy53ED7fBAKBmXM8JuIZ/OjIKDpcFtgbHDMq/zKHD4v5LN53Tw//KgYrzlKvvy5+9tx/P2xVSMKXIxmR5Df8N6vJrCfPwZZTEbmJhRwzV+nVKmTUQna/eG6O7M1hKFv2ZPE5Ygia5HMpenNq4cnEp8qXTU9mCSDLb5mDYSVX8fyExQ4uWyjJyJJprp6YnOTFTmVVDI6HMZDqhdOq5EUF162bRQ7S8Gr6NMnIMBcfBibJSWAk1blUl6USKxOiq1esQ2bUib5oHC3NOQQaq+sTYriMzZRG5Dv+p8NlwpNxlU78i96d06cRGgth1dbtWNXRhufOjAuZ/gLULCznHocSPA9163uBum4oZUKFMlxWQdh55vcLafnpfTE2ZFqC56Hb3dB9+eJq4ckEnNCSKvSJLBQ4BEFZ2mcbymKQZFRD1/9roSgOwYN3+cdwNJFGfzqH97cEUA8Vp0b7EbTchJ9OxmCHgmVjKlYn7Fh111p01TuF18lrPTqWxcGDr8FqnR6NzHDqY6eC2LHMj1g8jjfEjOM8hOjkG2/A+8ADZRP3UiKGsNY3VEUwlUimOPHvHgnD2jeGsTtWoM1ugdUwVlq+v9qcTP681sazKNWbw6gD7QVD28yxynktjDTw51rsS60S/94lGlh2XXsyvDivvvqquBkZHivlHtbakylWYubPdMMZvjIqCohhU0dOIxkeh7POJkJjpQaKSaPO/+WQD/XR4yFh8WeSEpOZ/Nxcqst8Px8UhmM2rdmOifNZNLQ74ajT0RCt/mG322Z7MmzGdLDTX7zBIpK83FKx58WcEr0tJmO3rFoHx1Qp9XA4je3LpkJlqTAsJ34AWGxQd36kUD5cMGYlwmJMuBcT37w8GcWFXC6Urypr3oiJRBbffaMfqQvN2GJL4LZUAoow/wqsPj90e6YquZpcNj/24IhTwelgBHuVGA7mmuDPaSLB77NakMu50KjEsDXgwq02O86cmMQhBTjU1IlVuTQ2JYC1Lqfwyq3W5di8aTeSSYdYcb92ohcvXkxid5cHjbkWJLMZJOk58TxFo0g+9SRce/fCWqKyUbwnEkHi0Z8JaRj33ttFB361KJeTMRYA2BIZNO87i9HWOmTb6/JViMVqDFVWl13qAWK0FfR0uPjj4o25Qz47PO/saeOxFI+kXgrwOZ2rgvRyNGJeKbisJEMDT6PLC15uhbGQwWXzgZHEpK4YHwpjyTRJgr0opydyaPbaccMNeyreoKI/xapA41z4RELkUVjKzM/Qa6PxYblypVWVcV/WdmzD2Nkkujc1iPzLc69cQL0jL6FfzcqMHfulEv/y2ReyMjwX2sxwGZOPzL9wRcVw4WQwjkhOEz03YhJmwAUEz8N68hHoLZugrX7TDHmYcsaFYTKR+K/WlTGA+8fRBN86CLxzTQSWYB/Gmm/CB792EKORFKC3Af3A/94Uxh2N+Z4qW0Og4KEYtiT3svAbi9UFLZ3C0RxwzmHBrakkXvK1oVsJ4+EmvziP4n0WlzDQubEI1LNprGtzo7NJQUJLYsThECG1Z8MJrHM50KIFUK+n0NDQhrGsA0N2O37xXj8aLClhAOM0gs4ADh48hJYDB+BfsRyODRsqSsTY1qyF69ZbkLtwYUYJs5x+yrCg4pq9si723gr9MdKTUXNo39eDSJMfOacXiiM/vKz4Os7Hk7mc+RGGoYt7c4yjkI29OSSmhRJFrcJlHjMnszRgLqNS9dilyMnw+xnDpatd3FDIsNbhw4fFzzZ/E/auWVbVCoghMzHUbGhISDPQYPNlnDtTDjQ+JJi21na4s60IDaWx/qZWeOudwsBOJrLodpT2EKrNycwIl1Ek02qBRZsO7/F88LgZhuBCQPQ2TDVkUuBRhAvHX4e1/1Voa94MvW3rrO8t58n85OgIzozGMBxOYnmDE60BF5q9Dvic1tLGq8ioUWTSarHDE70A3d2MR3tVoaEmH/OcruDfBuKCZCiEaXP7SsjQlCAZxYXXE3acUhVs11Q8n85gW4MFezzBAsHkj8sGPagiHR6De303rC0uIJzXddvlc2Gn14mxrIrjiTSeTLZi33gGvuw4xofieGhLqxjZTfBes544CV8asJ45hlA4jFOqCt++fYVOeKlsnDlzJi8Rc9PNcGzJS8QozDfkstNCnoMJoSpgX18Hq2v2PSrJoZx2mffgGSR1DaEVLbBHpr0ba+HMyjNXfU7mcoxBLkVsxt4cejnG3hxqBDK8yXMtQ2vz7c2pVU7mWsAVk/gvh1rnZOT0SuZgtmzZIlY9EmyQlNIyDJ2dOjCWX73PAVF2OTmBIX0SbqdTlGPPR7eM37t21Xokhx1QbRo23tYO+9SMd3oQbNRzpedDMqU8mSJDwR6SqRwSE6jMH1F91TiNz2+1iKmPI9EIbow/A+twBur2DwC+0jmBcg/pWza1oKPOiZ8eGUIklcP5iRCCyawomGj22QXhSDHJaYXpacNIQums88A2eQHqituRG8zPpBHvooFRdaGETNi6vbCo04KWhrM9a7uvJew4kXKg06LjsN2CG4Ih7FodQM5wHGK42IkQlIwNtq1uWGVeSqz4bYXjbnXYxGtjNoKXJhx4IWRHa6cXr6s5xBNprHE5BHFx075IGO5cDqs//jGsY3XiVPUUc3aaqqJ1dAz+4SE03H8/HFPKuPmDyzdjskAhdyYsJGtEL1Bz6Xu02AMxapf5+ifh6p1Ez+1roI8lAe9UCI89SEU5mRlucJWejJC6Ye9HjQ1ntdVlxt4cGTmQ553VntyGsTenkk5irfpkfCbJXBrUMlzGMBhdZzlQS3oY/L6XXnpJ/MzVDY1tLK0ildPEPJBK4LaYf8kwJ+CxIRVLiUT/XMPUSKRy+NrmNTsw3pNBY5cLXRvYB6DMGnVsiZUvNy7lycxuxmReZPq/RcgsowsPhsfPcu1ipWh6Mhcmh+A++p9w+xuh7vowYK/QA1K0YpZgsyFX820BF+7d0CweUOZnWPo9Hsu/jg1FMRHPCjLscE+gzhHDhVgj1kXS6A+lsMavwDY4CrVpPe7xOfCF5y+IuTbMNSmKjgfbPbD4HVACdljCs7XOjPkJ/vxCJInjKSt8ShKDGvCWdBbuaBQuXxfiySmPLJwRBGOpd8De1gjdkSta3c9OkJ8dzUKNTeCPt+0UjbKnkxm8HkvhGYbT3A7YJ4Jw9w9Cu/12MR+Gd5esntKyWYR+9jNEx0Yxum0bzly8CM/ExPRqm/ue0ZA5NJH3Rn12WBudUMr0S80iGVmyHIqi9cAFTN6+BzmvButwHKh3FEJqxSQzX09GYyL+wGuITU5gx9uWRqqk0ncuxOAX9+ZQ5FWMo54qAKrUm2OGy65wT2YppmMuBDTmDEmxxJE3jjGExUQ3Qc0wmfgfjSbQ5HUUmuVKQQx0Onkyv7LRVdicbjS1tc1JMFy1MG9j0e3o9G7A6Nk0lm9pRFPX7OqSkUhKGOfJofI6Y6U8mXS6fJ8MkdVzCI5OIFeHsgPgmkJnseLED9GrrMe2HfdVJBiJclI1tqLEf37UsVO8JEgw4WQOw+NDCMbDGE0qePT4GF7ti+LBpgvwaU4ERzmDxY4vvn8b/v2NQUSH4tjTchDvaLoT1u58M5tI6E8JVRaDu/ZCNIkj8TQcFhty0HGfIwtPOgnF4YbNYQfYkNkfR643CtvqAKwdblgmDKrN+S3NKE5g3unJU+OIJxTsXe1Bozd/Pnf7XNjldWKE4bRgBC8MjiO7bDledHjRkVPht1mnxys/9hicioL6j34UK9xusRCRq20aPutIFMsGbLBuiCCwthnWUwlYt5S/10qRjJJTUffSMQyvboXS0QCkJmBd3y6KOJBNCEKZRTJV9smIqbNqDqdeehbJSASeOQRir5SeFX6eRMIXlde5GJWhNZ53FsMU9+bUIlzWVWbmy9WGy04ylzpcZhx4xgoxbp+rdyOYN+GNZrxZR6bUhsttk2E1hpkIEtfyZV1or3chO3S64v4wVEdSq3O0QYkFEI3lY+yxyZTwYPxNTjHJUn6PqOrqqkOoQnd+6ZzMzHLivEBm/mc+POfOnYHL50Rda91sgqEx6n0ODYMH8b3We5CItOLeQHWVOeVIRo5frgSSIMNltgYnWrw+pFp0bNjSjIym4E77OIaVDoxHIzg5khWNjXtcTqxutEKpG8GkqiHgsSBAMrW6oWVHirZOyRPgmUgKR+P5yrMulxNbtBGkYsuQTSfhZy4nx2maKaipOBzbG2EJyPkwRSQjvAKpE6cJMqSI6T1rOmBFZNY5abMq8L/6IjoSk/he02YxFO5ro2F0O+1Yn02hnVVm7a2iyVJKxNCrZw6TIR6SXjI6jKjtCQw7Qxh8bgBOuwP23rBYGJUSmSwmGfba0IPJuZyY3NSJBoOsjLxmpTwZVNnxn4xGEDp9HMt23wBvQ5P4vquxMZLPA8PdfBl7c5g75TPP39Hj4XcvVW9O3AyXXZ0kYywJ3rNnj1iFyJnWRpSqMqGs/oa22fFkfpY3GvMuBLfJRHm8J4yclinrhfHGZN7j3LkeNFlXAHGXsFHb7ulEOp5DeCyFoXNhnD+UhbfOiUCLC4rfhngmJ1b7ZyooJhfDVk5WZmoQGgl38/KVSGcySBQTQiYOy8lHgGwcyq4Po/9sAss9ekWPrjpPRhHjoyeDQTQ2TM8smQsjkQzW+DJozYSR7l6LVS0eOJ2dSFOV+uURJFw6QpqOY1YVvW8Mie9p88bQaJ/AWDYmqgMb6FXoGl5Kt2BYSQsP5Ga/Gzf5XRjOOJFCGpl0HG5XI7IngmIp5tjdXKjCMzZuqlMFFPleHovopfrx0VFxzh/a3o5MahKp9MxFDJF+dR/0TBr6rl3YaFPQ6HXiBr8bx8734cXjp5BZvw2b1qzAJg1oN5CDmFZ5OiLUDLx72qD3WdCxcQtSsQkkVlgxkQrNEJmUBQSsECwmGd+5YbjHY5h4+zZAic+YK2OsPCvlyRSHBosRHhnG+X0vwtPShpW7b8SZV15Afft0vrNWqLWsTKneHIbWSSzszWHIu3hujmUB+8Mc0VIoMF8JuOyeTDXTMfnALBYyJMWbgeW4sjy5mnAc4/ycm3Lnupn6W6xIkXL8BMmFqx0eE6vL+OtSqzeSJiu3wqEofImVyNkdqG91Yf2eFrFCZOw+r6Rcj0wqh8hYCpHxFE4cG0cqksDAsSAyEUX8DdNjIcqioEBggKpSnLMH3kwQN9xwA7whKwbP90M3zhaJDsF64ofQ/Z3QtjwMxeqAlozA762+v6AUyfC/B/r7hPd35OiwGAMt8wx8lQrVybtkJJrBWv0CtIZVUJy5Qq7FFs4iAAX+gAOK04Gtu7uhWL1i5PFoKINQOIWTIzFMxDLIsprQnUPE4oYnm8HbGnzY6clPGBXlyXoa1ogGR8AJS6MGq987g2DEMSguPB/Tkc1G8TCldXQNqayOH58aQYPHhjdvbMmXabMsumhwGWVgOFDM9+6HOGkObhIu+5ZOHMfm117D7tvvxMSKFTiRSOM/J6JiUNwmtwPrFSucpyJiXLZjVxNgyZ/X3GAMtjonmlc0oRltBSMle0S4mOF9T6NFIywWbcztHLuIgdvWwUlRVRaSGMqVJbmUTPyLPpny13ys9xwuHj2Mtg1bEExl8o3MkTA86zeh1rjUUv9Sw5CEw/NLWyVDmqxW1TRtRgFBtb05rEQ1S5gvEZYiJ0Mvgxe8WLKm2u3TUPG5kuOWi6dm8ubiFEvjDeS0WRClt6DP7GfhzUOys8EJT7wbr0TiOKZG4Ija8CZrDg/t7JiR6CfhsDeGrwEfsCXshtNtRTpoxakXxzHRnkWg2YVAixveOkfJMEZxCTMfBHpQKwIK3nznrWK/s/GYGDlcSIgPHYKl5yloK/ZC77qxUE2UTeTgbrUtmGR4LtjsOjwyKirXbrx5JbRMvmeEDXOMefN8SsIpLj7geIE7lPPQVt4Ji9IDVU3my3cvxqe/028X50GOPK5zNmLMouC2znaomob/HItimCOBEyrWZxWc6w3j8KkJBFx2dDoysPeH4YtZxcgAa4cCJT7TyKY1DT+L+TCajuJDrfm8WTydw6sXJ9Fevwp3rWss5LtYFm0kGY5Q5mwWMUK5rg56fz/cuobBU6eROXusIBHDmr4Ohw13BDzoSWVwbCyOF4bi6A44sW1FHVazEz//DdAG4rBvaS2ZyGY/Fu9v5gul1/7Ck0+i7eAh9C7zI+l3oEkm6Ekowr+driormfgv48lwQdV//AgmLl7A+lvvQELVEOrrQy6dRiaVgmuOkeNLgcstkMkqNN7XfBl7c0ZGRoSHycWt9C4r9eYslSfDSlVOtuQz/ta3vlV8b7X49re/LVIJH/3oRxdFeJfdk6lluIw3HGvgGRKiZA0rdkptfy6SGYmmRFUZjb+Mv7LUmKDkDBN0xR4ZvQcmth2GG1+SXaO3A3rQhxOJJF7LpOByWES453uHBtEacOL2taUl/pkX2tzlR2dHAI2jCtasqocDPkTGkji3f4y1qPALwnEJ4pGy/8YSZhobQXJ2F3Zs31wgRlH6qlEKLAvLmUehTJyFuuVhoH5aHJFElU3l4JgapjVfkmECld/N8733tltx7KUBUeVWZ5gtwveQcPjieebn6+rG4PNpSKmNQHwUdf4U1IY1sEaGhCejR7LQolMd/Qpg8XP/ioec5ZBVM3ginMHFnIr1ASdudI9ifXf+nkhkVExMJhE9ZcV4IorTsGAsmMb4oVG0uKLwRoPY3O6D4rDgkckYYpod7/CMotFuxVg0jX0XJtHVuBy3rGssUhFwFnI3YoTy40/Aud0wQjmThefIMcQb2uB96CHRyW8EhZ3XjGexoj+L1OpGnPFZ8EoshacjCax3O7DM3oAuEkFT+RJbKZXP/YqGw9gUDiOycjmGWrNIDQ0jZA3B6rKizlsHh3u6qsxhceQT/1PEI1Eq/KmyguyNV5GKRbHxjjeJUuXY8LD4zkQkDIfbA5u9cmXm1UgyoriBIdMS3lOp3pxQKCTu7bl6c6QK82LwxS9+Eb/zO7+Dt7/97SL3yzHLjz76KG666aY5P/v9739fkAsjQBzFfFWTTDXhsoV4MlytM6FOo8XwWLma80rTII35GPbHcD+4TTlQiL0z5W4Eh416YVTOyq90GK89f75XDBdTQ3ZhA91r/FAPRtFh4VAtHWfVOgyGZqs2E+pUyO5NG/J1/aIwwQY0tXlFFRrH+yYiGRFWG78YQ9+RSbj8dgSa3Ui7FNHEKWf0sOBhyGuDfaqSSZKMNRNDS//jAGPNuygPM9Pg8ft9Dhty87hrJMlwRUf1AD5wJHxe17x+2UyDxVCZXAnyuvBzQ0PPY3z8IkLpRtSF9mPUWwd7PAlF5EWSyA1MezHWTveU8meR8KWu4KeTIfRmbFjrduAevxWTY9PX3RnLobUnDndLI+JKD1ahE1tX1mN8ZQY9IxYc7A8DbiuOQoVdUfDuRjsQT2AglMJPj41iV6sLGzrqZt3PJBldS4t+lyRHKDc2wLF7V0FGxv78c3A3tyC3ZcssguEgueypMPR4Fo4djXD57KAmMyewDmVyotnzxw2dqK+3YFsshY0eJ7wVcmW8Dq5z52BzOtH+rgfQ2vPNfEgsoyEaj2IyOIn0SFqsxu0uOxr8DbDarXMm/jOpJM6++iKsNrsgGNtUuFMa/BSnehrG/V5rJENU25vT3NxcmO5rnJsje3P4fNJesIR6MZ4MF2i/9Vu/JcYuf+xjHxO/+4Vf+AVBHAzxVwIX5f/lv/wX/M//+T/x6U9/GovFZSeZWoTLWG7I8mRWesw1+rgqTyaSxpY2d6F3hnkXGupKN5bIg0wZUKH9lUihLr0aqkrD7xITLBM9k8IeBiNhwNch7GKjr7SXQNVelv3K5sRiWX4+9FQE4KtjbR1yGVUQDl9D56M4eXYQjWfT2LxzIzrbOqCPDU8PLePnk/0IjD2C0c4OaGywJIMVgT06XXUuRIuKCCpBTPGcygtQeZrnTRrivNx/+W3x/OY73rvQ2urCa33APa5xTNpvwiC9MdsFBFxZdI52wuVyw2q1wMay74mZ2+HVfTbdjXEtjVvr/bjZz5JmapTlCVC9EENuICEmVzrcjcgNHITTuxZWDofM5jAezWHVqjrs13PCiL+nyQ+XlsbpaBQvD4/i9jWNaLFPD0qbeQxu4Q2k9r86Y4RybngEyccfg9bUDDclYhQLMvR8py6Klsgheywo5GEcu2YWHfDznU472lM6dg3HMLRdwZl0Di9Hk1jutGOTx4lVLjtsRYSnDgzA0dsL92/8BnL2qYo1XmcX0OxqhtaoicUKCYev4cFhZJBBc6wZzZnmQlOiCJdNHWsiHBIEE2hpxfIde2Y8E7IZMxEJwR2oPclUOyRtKSGfwYXkgdxut4iC8CV7c6ju/s1vflP073384x/HQw89JMJcd9xxx7wmA9MT4bX68Ic/XPjdb/zGb+D2228XJMPev1KgLSQZ/dEf/ZGILCwFrqlwGW8wrgjoijL3wmRctX045fSYGCLqHZ5AQzQMj10R5c00fHOBif9oPCny8mpGhyfWDYvNiuWbG9CyIr9CuWVVA872AC+ftolE8j3rG3Dn2vwqpxjDkZQIpcl9rDRgjGDZc2OnF/4WB85Hz0LzpLFjzyZkYwqOPTeIzGgMQdjQpCsIJI7A1vsCUp4bMNnUidUlCEbsQziFFfVu9FVJ+tw/XjvGhem9GNUDxD5a56NfpkBJcVyyG803vQVrdErfeBA6+wZCwRCy2TGgxQHnUBw6MoVzk9N1/HgyhkHVh/ubVGyTpdf8O5UBjgahp1ThKVh8dlhiLuQyUXj9XoyEUnhNC8Le3IEzFh0Bq0VomLEp9fiAip7RSbxpfRPWtHgxPlG6C15I0ISiSJ84hMA7f16MUGbiP/ncc3DdfLMoQnDTK1WAhKbBYbFCHU8JD8bW6YF1ZXl5E3pwDncWG6w6tjb7heTPiWQGL0YSeCqkY4Pbgc0eJ1rsVuixGNTnX0B661ZYm5qQyU1NC+UYa0rHWKzI6Tk47U54fV64vW547V5MxibhtrsL+TJGBKIT4whHIkgnE+g98Bra125E+7oNs/ZTNmMmw2EEVs/MGV1ur2KpQNthlOpZbG/Ob//2bwsPhN4OSYGEQO/jAx/4AP7iL/6i6u0x78keH2MRDZvC5d/KkcxnPvMZsR+/9mu/JkJr1wTJLFW4THbM04thtdRcDZAScgVSqiqFhuqpl15HNBSHtyFflVbtiiU0MY4zPb1or7NATzXC4rRi0952eKb6LGRC/qMdvXjnii5oHZvRyg71MueD3lSboVGxmjCfFLh0OD1YtmIZlm9uy3sPWQ1HnjoP5NKIPPvviCdHkF75DnBEfS5RLL8yfS7oydzZ6ceJRKJQulsO3Dfp+lMLrphgCH6+OiVmHTkVCCQvwtGWH05Go1znaRGSL83LusT1jywHxiJjiMWHMDT4KuoaO7Df5UfK6cIDgRhWOqYT8MzhqINJwJ+v1JKzZhTdDjUbQ2/agfqJJPT1LehVY+iw5QmGzZIHLoaxvy+B29rdWNU05XmK1f1s46aFw8j1XITjlg/D0tCAFOcQHT4Cz1veAlt3N3DkiLiWHosFcZXyLom8V7U+AGtL+UokPZWDNpGG4uNFyy/CSH6iFNvnwmAmhxOJDL47EUVAAVa+8Ro6V6yA2toya3ZMjjN5ZCUZCwkMf+NquLuzGxvqNxQGhh06dhAHXngOqclxdG3dAc2br6oqXmnLhVsqGr0knszlIhl+31JqtGWzWfF63/veJ0hHSPLMs8KWYWaShRHyv+kxlcJTTz2Fr3zlKyIKtJS47CQzF6oJZ9GY8sSQtameXElnqBjyhizVtcsHaiicxNquFtx+e2nmL1ds0NNzEW57E1LDUaBDx443L4OtqAxWSOSH+9By430sS6m4XXoRxoKAuUjGKHC5YtUa7H+hV3hlXLFyP5yuGNZpryCwvh6J7l9HJKgjfLoXkydVHMUg6qYq1nyNTlg5ejmVE82FK+pcUBIJRHMa6qdm0RfDmOCn4SmXtxKeTJVy/2NRFcu0QdjaHij8Th+dHq3saPWhc20D2rV2DAy+ArdrIx4fjaF/dAw7EyEkA/0YjNnQ3t4FV1hB9nwQlmYH7BvqZxiIdCKLSC6HE0kLWrpdmFR01Fk1Uabssyh4qSeIUyMxPLBtGbJRh0jqW0SCPD+kzgg9mxUjlO0rumDpbEby6aehDo/A+64HYZ2q8pEeF+kkfCqE5jTg2NkIi7dycQWJiMl+JWyZpcTM4+ly2sXrTs2DYy+/ihM2B15YuRGuaBANyQxaGAsUb85rl5Fkinti5M8km0JTYmsrmsS+6+i+4z1IqlpB1ZjJYZnEprfP+1NlL5CmwenzX5MkU4uS6VgsJv6Vz40cxDbfUByJxgj53+WS+J/61KeE/WRVGUHPlfjqV7+KO++8UyiBXNMkUy6cxRucFVtSLXi+N5jRkykGH5j2VRsNIo2VQeOan72SQpO2CulEGDaPjo13Nc8mGN48I0ehN6wqzF8pBzb4hZJZISczF8nwPLHIgA2iUuBSGjJWmLlYczB+GhsGvw91217oW+6FW7GAE5L102402xJipAAr1i4eDyKbVuFrcGLMSlKxCe+Lo5gjqlqSZHgjM67MVRNDZC+//HLZsB7PSJYd9VMhh0rXLj42BofdBs3bXkiK64MaNFtGTL6kEObUGRDb4rV7qKEtP+YglcLAQBDR8Diihw7Bk7UD61RYbbkZRoJFCK+cGMWE3Yvsch9CSaBO0fBWXxhei4KnT09gIJzCu3e0o95jx1CclWP0/PJ9MkZPpjBC2eOGo2sVEs8+DleWFWTvgqXoIbekdDiHE0g67TO8qnLgsavDSTi2NiB3kUrM5cPJSs85rDl7CjsefhjnYnG8HAWejyQQy6Ywojaj25qFVefsHVlybcj9TP1O/qtmszi/f5/4ef2tdyLQmu/JkZVTsgteSq9wsZeLReB3OC+J4ZfhuUup/FwrcUxiMdVltIXMyxgLIZgXlX8rhfe85z0iEkRpLILKKAQXzTLUds2STKnBQDx5XD2xioJ5kuKJmtVCGrdS3hL/xoqqje1zr8LogjI05fME4I52gyanrsUNf0OstMfB0MTIESGTPxcYKqtz2+GZkpcpRzJyMiDLJFmmKHNH+fk2FmRzOSi9L8EyeADnmu5C14rbROipcLw2BYquo67VLV7dJLh4VjSDvn5sBJlYFkefGUSmDhjIWtDVZRNejtF7IskyYciJovKBL9WMKW5+hWEwrXDu5bUoJhwmzvXxfiS9ywvegjqUgIUZa8IHIYQp3z11NHBPVVrZ3G4013UgEBxBYPV6xDuAsdB5jI2zTPp5QYiB+kbsH1cwFMlisGU7HBagARbc58mI/fzZ8TGho0aC8Tlthq5/YxjDOEL5CNTRUbjvvhvB178AV2AVPLe/syARI2GLaLD3p+Bb04hMh2dOghHHPpKExWPLH/OUEnPJ901OTvfk+P3wx+PYalGxq7UOJ2Iqvh6y4kCmHlbNi7VWBU49ViAZI9nQk8kkEyLBb3PkPW5nkQGUkjd8SekVGqfRgT6E4nEkX3650B9SSvLmWuiRWepZMtZFbPeBBx7AH/zBH4i8yv333y9+941vfEMUGVDthGAJ9Ze//GVRWMAc9p/+6Z/O2AY/+8gjj+Bzn/tcyfaPayonU0wyPDk0Zlw9MU+y2HryciE5ehDUxDLmQkqBSVEm0zqalyM95BKVXptvbcNrBwbE4LKSJBPsFUSjN66ec/+Y9C/WTSsmGZZDMkTFY+E5KQ4ZupGB/cT3oChJqDs/iMk3aFBmfo/isEKJFykme+3ipYyFcdvuVnTa7Tg6EEJvXwTOExH46p2iNyeUHMXFoV5s3z6zH6mYZEQ115Rn6rDZYLHZRTc6j0WEVwzXQerH6bkM7KEhqA2b8xVEmo5cX5wzL2HheOWuUqtXg1jlWAramTT0BhXOjc1wKQr8jcD4+ArU19+EgZEx/ODwEC5qKnJWBW4o8OSSeFD3Q1Fz2NcfQUJR8dCONqEgPb1/0/plxnBZboAjlF+Hc+dOJBgu27oC9nXbZhCMKIo4H4NnBMhtcsLX4RFd/3NBzo2xrcgXBHCbDMvNel8mg+QTT8CxdWuhJ8c4T6bdrmCzPQhFiaMvA4xqbRjOdWNF2ga3lkSLdXqbmUgMJ48dRaCtHcu378b+R3gfWeaUXmEJbtxhx9att8DZ3Cq8HCl50zIwiED3MjTdcIMwqEvhfVwOkqnl6GVlEeeEngd7ZD70oQ/hk5/8pBgF/rWvfQ3f+c53ZjSGs1SZ5EOSqRUuO8lU0rci5MpWGh+65My/sPqC81+W4gKXIxl6MewCdxs8iOIbjLpf7D/pquf8F0XYtp1vWSZW+KwwU9XSJCO8GA76mkMDimDCfWXTzBCLsYRZnhN6c7y5Zj1osRFsHf4+cqtWQ9v5IcDmhKpFZ6gwi22KMcyzv589NuPxDDrr3Qi47Vhp18SEzC1uN8KjCRw7cAbBkRiWda9GatSOSS0uyrRZ4Wa8ttKDkcaAY6FZDS0XD/J4eC1kkxsXEvHBC4gpHjh9+Ti/NpLMzyrgebR7oNcZz6+B0EhG56Ni5e9Y2YKcfcLw4OYNrmpxYN+EDerKbqxpsGPk2GE4FRW7ov3ovxjHkfEL8DYm8ZadgRkEI77bKJI5FS7TYjEkn3wSlvp6pA8eFAKXessodH26/0nPamJkAGfTRLqBpjqbSPxPlCCLYjDZT+G5wswY2+xwmQjVPfccFK8XzqlVq/x9YTtTHf1WRUebJY4tLhcuaiFkrctwKtOE87odKxQV7slRDJ56Axu33oK2tevzVXniZplzV8W1yibi8DU0oM7QHxI9cQKhQ4cRXLUKPa+9JhYZ0svha6ETKi+XJ1OLcJlnCaZifv7zn8e9996Lp59+WoTNGcZmCNuYt2EFG9VKSoEtB/z7YhfxVwTJVFvGzFwDy2F5UiiXsVSx13L5DVHRVUZ5masxGvZsNodGbQ2S44qQddlwa167jMiTzDRBFpCJi456bc9H59w3GgbuB8udi/eZf5MClzwnzEsVQxk+AkvPkwjWb0Fo5T1osDkLMvolSSanlyRbj90K/5SCAKuY+tNZwKqiZ/g4XO063v62m6AmFdGXM9wTQe+hCXjqHUiMAcmmLLQWTRg2aQh4joqbMeXDKv+VhJQd6UPM2wY3zyMrbXrD/GM+DNhQB92ghlwgtIyK3MmIGOjl2NkExZqDFjRWzulIZjX85NAQRrw2LGtxQ08lkXDY8SZvCIpnBYIjjWjwjKDVFcYbr+0TD6WUu8nPFDGGy3QhXknPRUulxLn13P8O2NrbYIlGkM1O5o8pmhWim5z94tjcBO3ogDgO9t9U48moA3FYOz2FZsi8JzOldjCFDKv6Rkbgffjh/BC3UkPEijr5+bNbyWGVIwV/dhhJSwBj43acCTVB2bAZDR0rUKfpcMqR3VUsjqgCkEulZlSWsQFVf3Uf2h94J7rXrBHPBsO7speKFYkLnVBZC4N/uRL/3kV6MhL33XefeJUCv+Pv//7vy36WC9ZKf7/mSIbJRIaEOMq4uDSvVp4MZVy66mdXdfChYGiqPtAAy3gbNIsFyzbWo21VYFZDZqlwmTJ6DHqgCyLbPgeY8M9pGpp9M8mONyClIlhsIBWlZ4BlqeeegjJxCuqmhxDssSM7tRvCoxAkg1kko5QgGXpS7XXTPTrsFxmLJ/Dy8YOFBL940DwQwp4ctCaEPcdT6OsHLh6JINSrw9foELkeqhBw0udccv8iXJZLITXSh3HXJrxl40Z40nZk03GxoNZtgO7zIJOJwunMFw+I40upyB4Iwtrghn1dnRjiZclycNk0GVEK5tXBNAbrbNja4hX6w8FkEvc5dSRVC14fjuDO+pXYua4dmt+PQOCOQmKboVHeL3V1Q/AH3HA6twth0cz+A1DGg7DWN8Dz9rfBMqXVJcmIHlX2bEQUKchZN5IUPUIks3JJuhajfE4W9s3T940gmWSi8N9s8kzv2ycIzlIkxmgsnjGKYYp9nJGL0VHfO4DGYBKda+qxua1BDFtjwcBK9tzYnNhm8BjLIR2PwWq3wz41ClpXVSSeeBL2tWtgX7NG/I73jSRuJqRlFzzPM3vepCSO9HQqyehfS56M7xqZinlVhMtYrURDyhwDy+tKKfTWgmSkB7G7eyahyUl53e2rEb9oE3pmG///9r4CvK30zPqI2cx2HE4chpnAzGSYmWfKsOW/7d/dbXe3u+3utl36F9ql4nbLNIWZdpg5MGFGJ44dM8myLMb/OZ/8KdeyJMu2ZCuJzvPocWILru797ndePO8V1aLTPpm0TBgJngxv9J7DiDZekdGx8RgqrQaxISu9KOqg8X15TsYpu/pHoD7+pAjhhNd+ADAWQ6ftjotkylOtFOI8Hy5LRjI+1Mkxw8xV2Qdxou0cbm5owKLRBP+4727UCrmb4kY1iorUqCotgWcoiIFzbpw74oC5SAen3w91KWVXks8nETpx+15Ci9eEpWtiGnH+/QPQaLQxq3yOBTq9RVR4yTxPqMOBUK8H6kYTtPXnZV5i0jJs0gyhwxHEEycGYLctwHW1RRimfH4kiuv8Dnh0FhzrV2FOEfXNrKPehWpcYpvWZnePB0P2Npxr2w6bfz9KTmpRXLcWZXeN3eApLhTsdSA07IRuWYmYXpkIKi17wrGO9VQWbLjDDU21aawitO58Tibi9cL76iswbNggPKhk51NJMrI3Rpwf6ZlwjRzvAcJaGNYsgE/lwjKTDteaizAUCuPoiAevGovQPejGcmtUqAuUpypld4+I0mX5mSQ/RMIwbt6MTLvgqbUnZVfo5UgZfSmeOmYI20WU+LdMM0SVT8gLkkkFqbVF64UVS7kgmFQkw3HL7Auhiq9cwCQXlkw3VjTB1RGTO0/a/zIKhsuC0YQxyc5OqIJuRCoyS7T1JITsZBUbNz1W6YwjGMc5aE48hWj5oljl2mj3vl4hkkkvhhgXLtNroE5w6GKD0ny4rLEkPgOHj6qGRaiel1pRQSb4uSlzg2A5JDeHioYK1BWVIuhWIXrUi87TThweCAkPSIh7VhgEQTE8yko5W8chOEpX4rJqsxh/HHEG4wRpbCxCwF2EUMgFnVoL//EhRFw+qKuMiFYaRBnt+Ua52HVs7rXj12e8iJQX4RpTmyAYyv/cV27Fnj39OBoqwYJKM+qKYkrDYlRD4nli4YDNBrV6PspKQ7CGluHcm48hWLsGJ6oqEd29O74RllpLED7jQzjkiZUnm7RJN356MmxwDUQZkkpCuIGwUALgXJsxx8L8RSgsjtP72mvQVFeLZH+qaxInGYwlGWHo+YLwnIgN3jOsmQc1vQb/eQIq1Wqw2WqA3tWPimIzTgUjeKzfiQqdBsvMeiHYaVRs8n7K1Y/2xwRb2xA4cRKW++8bV2GXCrx2XON8sFpRyuhL8VRC6eVcbIn/iwV5STK8cKzVJsmsWbNG5GIyHdA1FSQrYeaYY45bJlGwmo35Fz6nUrMYrp4oTFYdlm2pSRs3ZbjMnxAuU/ceQrRqRVJtsGTgcawb9aY4f4WqBpSLUDZtCXAD6dwDddtWRBbcgGjtmjHvwxJmht0IGaFKHAWv0mmE3L8SLNv1hyIoM2vEps+b/IpNm3DOFRTTHJmfSYQywc/cGR+8cRjeI0kzhySGaBmtKC6zYv6CcrgHA7B3uNFxdAgaA9DraEexzY8KqxZh4zywajjUfv77ahusotyXUy/DI70InR6C1qyB4fJqOPv00Gp1sZDa6LGI8zccwVND3TBX1+D+Kg3e6TfBEo3innIb2vpcONrShSvvWAO19xyi0UBMAjnMhH7yaywT/zqtFpbly1Cx5F406euE982NsPdMJwZbz0JV6kW00QFbwI0i41jrW0KvUgkFBHc4CkOSfZIVZeoSvShdjv0iKMYjyxJm/969iDJhfPPNaYl/zLwYZV/PiA840gVd/XygxizCXPHvqcy/jA67m2vUY4FVC39xRITSqC7w9rAXC4w6IWUzx6AVSX9DZZXIw3jffAOmq6+GZhqh7kQZfRpcyhER/Du/H3s9Ypp3uSecQrjsAiQZbujMd3ARMRTEKgtawdOdKZMOtEQSSUyOW5Zim2Wl5Qh2FiGoBuoWFwsRyokgEv9KTybkg6r/pCghzgQMb/W7Aqi26UXpJ5P8JF16Bxw1EH/fcADq5hehcnYgvOpRoGj8BEIm2RM9mcTNSMMquoScDENlpSYN9u3ZI/6/efNm0X1c5HUIrSx2lSfzYJQJfkJOE6RHSg+Dm0P7iS60nG2FzdkqrNHKuZUohh6H9pyAyVCMStcgjtuLoaqKYLjTC12vN96Xo20YtfTsKgTO9UEzxwRtoxWRiCdWEKDTCU9Ekt22FjtedtagpMqER2uL8OZQN0JRFe4useBklxPvnOjE4morVsytwvGTFIEcFizMSrDUJBOT8ddUVUAToocRKw9mGMfi1iIIE6JXGTGkH0Bv3zYcPLgfKtXYHIOyrNisUcErrmmCvFEkKvqCtE2jG3TQA92JJxE1FCGoW45QdxfCA/2w3HsvVGm8fSXJyDkx/G7RQRfc7Q6grgTWJXPRP3I2rgCQSDJyqJ08Iwa1GqssRvEYDIZxwuvHK45YM+EIdNhisohqO938BSIXky3ERkAUiweNLobUWRTE+5WGmHJYGM/1ZDvmMwXX+mQURjIBZ8kUcjJZhlz43HiUpbjSDc3G4LJ0SPb+DBGVwI09e9owr2ERnGfVIm+wdFO1kFrJBIbRcFm82bD/BKKWCsCSmVhg/0hANAU2HzskFh43eLn44iXMHjvUx/8A6Myx/Is+uZtNTyYwmpORyXZlnic+U2Z0cJm8Ji09Q7B3nMGKReVCQUBekyJKzYTGErPc0BMJZtyx6HSil2Zx0ARbhQ/rGgyijp+bBI0MY4kRdfU21Pe044SpCfU2Nfxdbgx0MJGshq7OgqAjAKPdh7A9BFWtBrpR0dHzub14GRReOTMkRDKXVZThvhoV3vKwwVKFm4z92N82hMNdI7iqWgW1vlyks8WAhqhfFAzAz3BZcltsbJ8MX8nwWhSh005EBn2iI19dYoAhwsKCSqxatRFu9yjBtrcL65vniR6eGDI2OiEzEaJkm5L7JXqoPHZoTzyBqKUGoYW3IHqyWTzHuGVLXKomFRL7lYhQxyDQOgDbuvUY1tvPV/iNDiyT/z7/wviXH/f+zM1cpTPjCpsJZ5xu/DoKvDToxjFjMdatXY8llJfJkXfBUDrvDa49tjbQy+d5TiV5k60QVy5CdK4szJLJJ+SFJ8MFTxkUxvpJLpxgma3BZVMhGXbGHz7djnXFHlyz8XL47Wo4VcNYc2O96P3IFLHqsvOejKrnEKI1Y8NY6dDa54Cz9xww3yoaLJWVNVzYRlc7NAe3IVK9CtH516btueHgMp9fzm0ffY8kzZjib/RmdCpxg75z6CSuW9GA1aubxpAGpWVGRs+ZlFiX5zBTwUBRwhyFuOlpgdLDWb481nA53NmMrpbjeCu8GHcWDcCoCqN8rgVBfwRuqxb21zpjCs4LTYj4XEKZwCBCSee9NIYHHz/WhzfcXty6oAyXq5rxynAIVpMWd1TocfA05fq9oovf1XIMUZtNrLNIRIdI1IeImhVRrA9MjlgxAZ8fFEUWCEYRPGYXJ1jI8xtja8XujuCM3QKX2omFVVVx65s5BubX+JPe+zmdBXqbBUVVMQuc11sUM3R6oK03Q+3sgO7kUwhXr0G48SpRreXdulV8hj6FVEhKTyYSQrC5G+GBIWBFLYyVZcCwPR5C488QQmO0y2LvcT6Hkwok8OqAGxt627G4rxfOh9+FY/4w3nINY6FJh2WmWDgtMSc4VXBS6fe6HdgQDKN4dO0xZ8YHJW+4rri+lJI3zPNIb3I6PSm5SvxXjPYUXQzIC5KRDY1KKZRsDC6bCsmwhPLNd/YhEAjj5muugJmJ9VKgZuHkVWQZLqOxL0jG1QuV145IZVNGr6V1u3XvESxqqMJll60ae1NHIzD37oG2dxsi138U0QzeMzaCObYBM8GcTONJLaqEoggHQmg7145Tp1tgLqvGhhWLxz23WKtGnyc0poOfmIx2FD2pYDgsSoK5AVA9W45bbgy1YEh7LWrt9Sjz20Wewz3cBoOVeRQLileWQje3FMODw+jt8+L4tg7ojUZYy0MIaILw+kL4+fFe7AsF8d7FldhQYsYvzpXCqA7gjlIr3j7RAq8njIfW1wvJnmOuEZQ3zhuN8ztBlSImx0PBIKLh2PpLlLthuEyEm6J+RHxBBNscMJTWQrsoVjYtx0W/dmoAHX3FMA9Q+0yDaxaXi+shhoPpdKK/iZvKcFc//CMjIhTKc8JzUakvQYlLBwN6oDv+GkLzrxdGBeHbsSPWr5JhIl32yXQNd+C1V36FIpUF2jVzEFQHz1fhKUqZlUUB598k9mOia+zu6YGxuwfmex/AgpoKrGWkIshBawG87HALKmsy60V1GgsKpgoKdH6/xzGqLpTcq+A5TpS8YW6R9xilb5QjkScreZMrkrEWSpizC1ob7C5NVT3Gi0jrI1fgwpR5AobrQoZyrFtWESOYaUCMYB4Nl6lZtlyxVHTbpwNvAm4yDB0ZSmuwYeX5IV8CQS/UJ5+BwXkOrTU3oTZD0oqRjEz8j++RiTelqoFjh47C7h/GvOVr0dPuiTdhKsGE/3AoFM+/TCRwmfS7hkMiJ1NbqxYGRjxuzuMbOIEu4wYsqCpF+bAV4UoNzEUN8Pi86Le4cbS3AyZnrDnSVKrBnFUlCLiNcPT1Y6jfj98dbMa5Ui3e21iGlRodnuh3wqTW4TrzEF4+3ge/L4gNc4oFwYgO+ZFhuAJBtLYcwdKlKxHFILRqHdSjA7qSyd3EPDY9Ap12hHs80DRYoW0YOx3zZK8bw94w5pYBGl0Upwc8Qgsvsf+K71VVVASXxYpNyxaJsCHXo+9AP/qdO+HqOw3fgttgU9eglN5WSwuCLS0w334bPC+9lNn5plfk96Ftx2ERRrSuW4yh0DDAkdvSg0lCMomeTLJy87HXNYzhbVuhrqyEZrQfhijXaXF1sRZXFZnQ5g+KYoFf9A2jWq/FcpNBTCw1TPDeSrjCEfxwlGA+XFOC3rN20bOWDlLyhg+Su3IkspS8Ye+X7N2ZSPImF+EyTyEnk32wDDddOIwkQw8jV+AiYT0+q7c4++SM14TiSSz2iRL/0ZAfqv5jCK94MO3zuZHJmTgr1l6Gk0ccY3XTXL3QHH8SUUsl3Esfhr8tVsaZCRguC8nEfyQ2yyURJNqIJoqw24crrr4Ch3s8Y5SflaDs/XAwpqDM6zPZ7mRaa8ePHaX7hA0bLh9jDapGOkUxw9loNSoDo9Y0BRxLLbAtq0etRRdX/aU12j8whI7O11FethBaixavaqrQv8CM/1NWBM1QCN/f3y36UK61jGCPqwMBSxNuXlYOryumSusdcWJwYADD5i6sW7cOxcUmdHWFodZRKEYtKtVoAEmPTcrdUE0g3B9BwD0IdbUB2prxXdokdOp06vU1UKk5GkDIfSbNkzDx3zfaMUvCrS0uB8JvQj8/ioE5H+e0ZxFWDvT1ofrwYZhuukmUGUeDMY9yomvgHXag7/hhVDTNA+ZUQ63VIjL6eYnCmEl7aGTIbQJNGf+evfB53IgsXZb0mBgmm2/Uiwc9kZPeAA55fHhz2BMLp7E6Ta9N+31YdPLjXk43Bf6opgRWjRrdU9jwlSORhbHh9QrCkQoEE0neFPpkLpBw2UTIZeKf78vGRm56tKZpxWzf0xEvG54O2JvC9IZhpA3RIgtgG1/1lShwyZuE+ZcOZwilFp0gKjkWQH3mFUTmbEa0YRPUAwOTKutO9GQSb2CSrFCRVkexfEmTCOV0Dw+hoXQ8yfBmZBSbOQ8fVLBOkmDE4KuDB1FVUQNfyDou3MACiUj5EnT3BNHkVQEGiMox45zYwC1C2RzZ3bMCWs0SnOnT47un+lFmjOAOVTfU0SC2V1iwpLEUG4JqbD3QDAvcmOcJozs0BLXVB6vRh8P7dsMbCuPGjRuFhSvyDiysUHEWCju6NeIYlWMhQu4AgseHoY7ogCV6RLxRITGkUoXiXg6xoMKMc0NedLurRahpfoVZNNeO+b6j54/6ZfHEf9ALzc5fQ2UMIrT+/SjRW8AVuWjuXAz95jfwrF+PQaMRZw8eREVHO7THjqG8ujpluMfe2Y6eIwdQPn8RapYvg6r1jPBQEivIlGGz5In/5BNAJYLnzsF/9AhCcxqgsVgn3PSplL3WahSPgWBs0NqLQy5oocKVRSYsNY/3/O3BMH7eN5Zgkim1Txaiwo8FGGazKLvPRPImV7IyNlvu5+9cUiSTremYU3FL5XAtLh4STEiWDafQLJsM9FqNkHIxD59CdPEtKW9Oei48Dm6YTHzzxux1ulBDL4KbXMvrMU+o6R5gVLU5k8mYiZ6MsoRZ6ajJmTxMRg90tonEf2wSpg+Xzz1PtsoEP1tIzFoNXJEIrAklt+nAngb2QNFjhKUcrSf6xz6B33fgJIbm3gHfsB8VZgv8Jk3aIV5ajRldwTB+NKzF6nm1uNt8EGrtHPyuzwGVawA1AR9+7jJjSY0eqxssqCqvxWC3EwMDIbz15CF4OOGxcQkCTjVC+rAo7mC+Jar2iWqxccPIhoMIH3NAV2mEobgSGmMUal9Mz0tW18lrNKfEIEY0D7iDYrYNRzWz6lB5TiVEdRkNAe8QtEefQNClRfTa90A9WjEoZ9QYmDu49VY0qNUI+f0YOHZczPdhfiEx3MMoQU/zCfScPomKJctQVKVQyB4dvZxsQNkYTyZhTk6q+5XioL433oBq/TqoO85CpUs96TUZKkbDaQyb/W7AOVp2MBb9wRB+1eeMh8gkweQidJWJ5I0Mt5EU0kneZAqZMypUl80wclFdxjALJ0fW1dUJgmEPCjHgCohNoChJHmKyoKyMPjgMvbcf0eoVSZ/DUlZuuomin+z0X1YGqA8/JhrvRHmyqSSpCvPkPZlY0l1W9bHZlf03lZWV6NvZKnpDqJlG9eXKUcUDZYOl/Pzi0TLmmgyEGPh65pnYqc2QFK1BSuYkTsZUOdoQUWmwc8iM8FAfQkYzNFUmQJX6ux5z6vGLXjeuqq7Du+Zo0dajx3adBavmFmGdVovHd7dhXlkQleEBtJw5ht6eA7BYRzAS7EPNSi1UfWUwmCvR1zqCtsN2IXQatVCNxyXyCzI8FJOtcSPUOgLdomJoa83Q2c2AKgC1Ri1CXCQnpYo0f9ZYtai16VJW3cU9GY0a2pFO6M69iaBqPoJzL4e+5Hwpa+BwbEaN5cEH48KXGr0eZosZVfPnY4nNJgwnboJ8NJ86hUBfN3SREJquuhZOr2+cXhmLG5INKEuV+E9FMlJxQDt3LnxVlTAOD4IB7slu+n2BEJ4aHMGWIjNWJHgx3YEQfts/SjDVxbApCIbIdcd/Mskb5nAZCWEeNZ3kzWRQyMlc4OEypTQK6+lJMhy2JTdP2YSZDQVUJv6rvKcxYqwTfSzJVA2YB1q/fr1YmOf/FoV3oA3zR3YBNYsRWXgzs8ZJVZin4smIPploVISsaIWx/0a65xF1FJFAWBBAlc0ArSb2OckS/EViQubERCdzTawQY0hSWmnJxi9H+o7jJ/1L8dSeVkS8YQyHInj/ch2KqG+SBM+dG8Lve824qzKIexdWwuGz4wVvDRZbNVip0uKZw324YkkN1s/hmIAFaDvXjGDAgo6Ok9BoWLjggre3FcuvXoC6eRVgNbJz0IfeHh3aT/bC6HDDaR5BuNIF45AfKk8IhjXlUBfpz5cxh32jdeHnCwLkNVaeu6SzchTX0GY/hRVnn4Z7+S2IdjZA23A+ZBLq6YF/zx6Y77wTakVjoVinisFlMtxTW12N5p3bMFJkhaVxAVo7u4SXw43SFI3lQJUNl3K9xz0ZnCegMUgRLuOxRf0BGK+6CkOnT8JcVIxB11hSmwgMl/1hcASXW41YZx0bpm3zBfHkYGx88AdJMEkq0mZSVkZK3vD7cWgioy3pJG8Mk2jYLHgyF3C4TOph0QJRlksrZWWEvP8EQ8oyhV4dRa2vBYMl52c4EOxOjo0JCIr8y5g6/WgUIy070dT3PAzX3YdI3bqkN/Xkw2XnmzF9Pj/az7Vh2XzD+AFnWpXQyWLpbU2xMWUHP0FLUvbKpIKU5KGhwHOurCAcp8IcDuBASxfe6F0tiK7cqMExnx9vnQnj7hUJIatoFD8704/Xe0fwR/V6LC9yiWqj3w/6UKEJYElYjedO9OLqReVYXmuLE0LAH0BX11nMndsIi0WHSGAuDp06irMdnTjV2hbTV6uoQGldMSwLijC8V4+gSoWRd3oxEIgg2miFrceLolBUjDLgewaDg0nl7+WGp8zlKL0cXn+uSUri49wOGLt2o3nubWjULEKRygt1ueG88OUrr8KwcSO0SSbAqjhTRlF96XO5cHrnVhhtRbjs1rugIQlFo2Lt87PFRjjQAfWgGg6dQxCP0kMVx64goDHnfXQOjxKh9nYxXsByX0yXzOMchrWsAtERb8abPtUCfj84InIzl9nGVnWe9gbwnD0mKfSB6mIUpyh5nmntMump8voqJW/4Oykt1DkqecP8jSSciSRvSDLZyMnQqKABzeb2THUfmQ/i/jRO1f1Cz8nMhCfDC8e8RzI1Z+X7UytsSXV2GqF0w2dZFoUhdcU4gUsuNHowYxKV4SDUp1+E/9wpDCy4D6r69Snfeyokw5vCPuTA3n37YDRasHHjhnGLPaqJDdXqHvZjw9zilAQju/5bfalLy3mj8Zzz5ko2TE3Ok5GzbVT2FrhUNvhCagyHgyi2UINMjWHfWC/GH47g26d6cXLIg79YVoMGgx/9rkG81T+MGr0KVc4uvHyiHzc3VYociER7eyf6+uxY2rQApSVajLj6YdBq0LhwIZZfc80YfTWXqwVGwyDKA1GYB02opLhlgxWuoYAYYXD2wKDI1xhKfdBZHVDreS3SG0tKL4cEw14YenMVg7ugdnXAs/RBuD0GODpdKKmNzVERYahXX4Wmtgb6FclDrkpPxjU4gNO7tqOicR7ql62Mh9X4XlznDOPUVdXhTMsZ1GpqYe+PVegdsR9Br7YXdrVdEJ+ybHnM+kjIUYkhba+/DuOVV0FTGhtB4HM6UTVvISIdXRl5MlR3JsGsMhuxMYFgjnn8eGUoJlPzvqpilKTpqZlpkpH3X2Lin8eQKHljt9vFYyLJGz6X53+6JMORyXxwv+N7/u3f/i2+8IUvpHz+U089hb//+78XPYv8Pjz2b3zjG7j//vsxXVwSJEM2Z/6FOQ8m8BIXonx/jltmLoJhomyA0y8HbE1QjVrrygQ7H2NuQO9QTB5Ga8TR2gdgM6VfZJMlGW7oIyMu7NjZgtr6RkQC5uQ3pBAEDWIwwnxMbCxyqlwCu/6d4eRhLG5c/K7UKmMfVLLX85gIhsxYiafuP46a+gYMt4SFMjIXZ0itwvxyHmckXrr69WPdGHYH8Der6lFnM6LPZcRTThNWVOhQ5nCgze7FXSur0VA6OsckGhU9EAxNNjYuRHmZBeGIV+QfPMNDMBfHwh5KfbWBAR+crT0IhB3oigyiuWM/KnyxUte6pnLodKXwOoMY6BmAY2gYfp8Lnq4+FFeUiKmgHP2gTlQgTfBk1ZEgrrR2QB3yI7j2/dDoLDAP2+HxhRAp14rnBfbuRYSJ4DTCl2KmTCiEwfY2tB3chzkrV6Ny3nidMOXQMv4sKylDRTRmAC0yL8JQ15DYCDscHTAMGWDX2IWXnbh5x0U2RR7mdWgbG6FfGlMVp1fmc7vEoDLl56WCgwQzMCJUnDfZxobI9rt8eHs4NivnvVVFKEsxUkAiF5Ve6aBUuEgHvV4vZJT4kGMikkne0ABl8Q8xncQ/Ryx/7WtfwwsvvIDrrrsOL774Iu666y6R97377ruTvua1117Dt7/9bTGbivj617+ORx99VBjElJO6ZMJlmfQCJEs2MynHE0VXNhlkuEyOWzZPQjomJfwjUA21wll0A4zeAVH5w4oUKXCpBC14NlhGqlYKeZiuPV3YXJ1e0E/G8zM5J6LBs+U0+vv7sOyWlYhyHkjbUPLnamJ5KXOpAZbR8cmp3t+mVWMkFBmn7ssiChYUMOdFVz0VpHaaCJlFPAjbW3Cgeg2Wl7jgiHhRXWHGlYvKcGXDWfG8Ll8A3zjeA0Mggq+snYMSk06Qzh+GNKhVj8Bs9+NQjxNX1NriBCPzQbyxGa4bcdljemOjoSCPYwjFNWNLy6OhCFRdUZhCERiLi1CzeB5gWir01ZRzTUg4xRUmaKxqhII2WPXFcNkjonggHIrCVmYYHV9gHJW8iZWq88YtMaqwSn0CKm0JwisegEZrEDV6lpEIApVm6Ix6BFtbETp6FMa770aYo6IDgTGNoOevmQbdp09i0OfGwg1XoLi6JuU6UF5L5b+50bEqbXnNcti77KjV1qKnt0cYaFu3bo1b3XrFsDOh/Eytudtvi7+P1zkMncEgChImIhnnKMEsNulxpc00Zg3tGvFh50isN+7dVUWikXMizLQnkynJKJFO8uZzn/ucyNMSP/7xj3HnnXeKEQeTxXe+8x1BJiQY4tZbb8VNN90U/30y/Md//MeY/3/+85/HV77yFbzxxhsXB8lMBGmdTKYOnheP3gtDIMrEdqr35wJlqCwbpcuyryVaMhcRuxX+kV5hRSsFLgV405/bDnXnbkQW3YJo1XJR0TXkDqJmguOIj9GdgGR4zmQeal7jHJSUlYsKumS6UYK0NED7gAsV1Mqa4ObhhMxANAJ/NArjaLUbbxJ6MbSIkkkEJfVkwlH47adwKFoEg9eMyzU+LNvUiLUbY0aBY/gsmt0a/KSlC41RDT6zrgFmvRbDoTB+1+/EfJMJ+oHTaPavw50rqxBwacd4DDw/nKhKi9LjMSIcZq7AwOwC3MMO1DWdD0NFPCEEjtqh1hqgnqtH5FwIiKjEBszHokWxbnwSDh+trW3Q62PWqLHeg/qmamg0pfC5gnAO+OHo9aLzhAN6kxZacxjnes5gbg2wLHQYkYomhBdcH9eci/hCMLlCCC61QO31Irh1KyzXXw9ddXXcyEqs8OMa6ujvhs9rRtPd948ZdZzs+iqNBmVpsgRzMcLLKS1DZSTWl7S2YW08v2Dv7kKwvxuWHTtgpTr5I49ApSjd9Y04YbKdnxCbam2OhMJ4YnAE8406bCkaSzBvO7044IoJjz5aWYTKDAhmtsJlU2lETiV589Zbb+Hxxx/Hpz71KRG+YniLSig//OEPcfXVVyNT7N69G1/96lfH/G7Lli34z//8z4zfg4YiDSJW0k0XeUMy6aZjSpJhrDITkqHVSmuRLmeisGS696fyckPZ1MXy4qAF13MIrtrN6D3SgSodsGnTprEJ9pAP6pPPClXd8Jr3xJWZSXRWoxYWDlBJg/jwqTQ3FjdDngd+P56H07tj0zFFDiRB0UAm+BvmNeKl5i7g9HHsDnSJsmY+kslrUFGXD3ozGlVYkDo3dn7XTKTV+X7aUeWAzpb9iJYswbUDIfw0EEbD6CgFYdU61XiqO4TVeh0+tKpWVO0xzPI4Ccaoha/DjWAgiPtWl8KkZX5GFc/B0eOgRyWvsVrNJDdJRo8IE+9+xDdmDgULnBiCttYCQ101/MypqTXnB/CMgt+NIq58BIML0XK2WZSdNjefxuHDx+PFAxXVFaieX4lwKIL2M704sq8ZdWoPSo7txbm6K6Ct2oAidxgGS2zjD3d6YCnSw6OKwvPSK0L00rhkSdLiAZ6XgM+Llt3vIBAMYMGSjdCZLWnXQ6JBMqbJMuF3yr/xHPLBMOJgVwdOvv0mwlu3oaW+HieOHo33kvB7M+lvKmYlX2ohTRZokGDmGHS4tvj8uuK6fNXhwXFPLAT7cGWRkJzJFLPhyWTz89RqtfBuaMy8/PLLYg2//vrrwrCZTLKf+5+yWpXgeqShkEnkg9/rYx/7mAiv0Zu6aEgmHWROIJO8jMx78GLx4mRiZfAGJr91D3txGUWmpgtHG9wjDmx39qGkqBQhj38sObr7oDn2JKLmMoTXvV/kYVJNwkwFZZls0kNwOMQmy8XFTZbPZxlzIBxLtCvTBVImhWiYPwdB6wju2LQMmqhLWOsMfZEgJeHwJjhfxqxGr8uN5hNHRZUSPYbJdF2HEMXzXR242deDmrJb0BMIQWfTobzUJIQ8n+kbxqtdQVxvGMF9q+tEiG0oGMbjA04sMOjgbBtBMKzC5bUlMOsorwJ4fT40794trLDENaDWSHn+Ivg9HpiKaqCmNt7ZEYQ6XNAtLYG2yoRIwCuep1KbhSeTClqtGWZT7LF2zRZ4PDEvh94c4+2iZNhkgn1wEJubAqjxnYan4SE4wg0YGfCh67QTOr0GRaUGWLvcsKwqQtfxk4wlwnjF+BHdMlRGKZwz72wVBDlnUZM4L4mNoNJrUXq9Y94rDcmkSvxrqP7d24OGyy7DwuuujVdRydEFgc421CxcDIsz1s+SuAm7RQXgCOr0OtygIJhQNIoXh9w44w2I/z9UYUPtJAhmtkgml1MxrVZryvBWKsjjSdR6pPGXiTo61wgJhgYjPatsTCO+IEgmkzJmmdzlYmfderpcQCJ48qmC7/aH4uOWpwoex+DhV9A+pEPTFcvFmICzxwfOa3z1HYP69EuINGxEdM4V48qTmQ9JFE9MdcypSEZO0OQGq0y6i+mYwpM57znKEIyMnw/7QmD7YX2RBbqi0ri8htQJk2WwtJSEHLkvgp3tbdhSU4ElS5ZMKnTAmPwxXwCPuDswv3IB3H0q9PpDqGuwiRDcY51D2NMxjLttHmyot4qNlJIiTwwMY75Oh/6WYZj0Gty7qhaDfbTivejvHxJhnYUL7xHHngiN2gR/cFiU4ga8HpgtJQgcGUKU/S/rK+LKAhq1URQHaNUmII1twyFkwiuKcHNUiw2CD4Y5eKOzWqenuwt1riNw9Xfi0MI7UawuQkWVHlXzbIiEIxix++E+6YBjOICObWfRFvLAveZKaD1hGK3jNwbnQB/O7NyOynkLUL98lVAB4LXTG8Y3gsaOMUY0ss/p/LEr/p3QjJnqOgaOHgMCQRiv3iIq15RVVPScdz/ZiZBKJdYJwfAp1wm9nKBaI/pgqnQa3FhynmACkSieG3Lh3GilIkdhKwmGo5s1lbw2lrwLl+VqloxqiiE47pPc+2hsK8H/J45QSQTXzCc+8Qk8++yzwoPi/ZwNXBDhsokqzMjSbCzkIh+X98jws0fCGpQYNXGtsKmAx3f80F7Y2g5g4S1fQFF1PU6O9CMUUSESCkLVvhXqviOINN2D6Kg8jBJCymXYh8saJ9ZNk1aqkmRkoQOT02vXrhVeR7LpmGz2ZLQsmeVLT6rSrIN6tHFTnnvpxfAzaL2ScNjQ2heMwqA3CItHWmGZ3CAkGHojHHS13nsWIXOsl6hfB9jKTfiftkGc7RrBe+tK0FRqZTZeSMU/MeDEfK0O3WccqLAZcFNTBbQ8drUR59pPo719EA31tUkJRhkuo8sTGPagxGkBGqMwXFYhxjkrn8fnRNUBzkZI+13EhMzoebl8eS14fob6e3BNaS9M5aVwzHkEkZGAIEGWLzNPyHNKwi7VqaFZZ4Z76040r9wAt0+D3nf6odGpRPFAcQXHGBjh6D4nKsgaV60VJKOsLkvXCErC430idNco7inKxsevdenBJPNkQp2dCJw4AX1T05g8TPw8IAqzyYh1l2+E1+/Hzp07hQfM9bj/2HHst5WjzmbF5tryeLG3LxLBU4Mu4cES95bbYOz2odPrQf2yYgT274d/335YHrifJVcXvSfjyYICs6wo+/KXvxz/3fPPPx8vBCCkJpsc5c718MlPfhJPPvmkqDSjvFW2kDckM1VpGZb9MSzEG5Z5h6kK5DmDKjSm0cfKdGx00fBJNCzfCE31PPF7o5g26YP26G+hosLx2vcDptLkx+ALwR+KZOxNKcuYZaMpz0cqotVxUmc4Eu/4VxKMRM+wH9VWveiVSVcdw6ojblwr5y9GVyCE4WGH2FRJNpKQ2BGd7KaXCfsFJh2K9V6oPXYEQrHyzlPaKAadLoRdIXx4XjlWNxTD4QAGQ2q82u/EPI0G7aftmF9uEXNZWMAgijZ6HBge9mLN6qvg8ztSnzONSXgo4SEfVL1hGBYXQ7+ybBwxqlSj45tV/rThstj5MwLhWLOg3HxYgeYd6sFV5jboTCUIL70bNq0RtkrEeydk8UDviQ4UD6ihHdgN3erVUNcUY/6cclG17Xb4RV9O1+lh9L+0E35PJxZefgUspQ3n4+skmYTwiLIRlJ/F45FaXI6QI5bbCZ1XcQhHRqe3jp6HcWrSHo8oV9avWQN1irJ17/AwDBTF1GqhGg3P0JvmULFf9wyhwePGSrcDB/afE8diLivHboMNXr0RGo0ad5VZUeYOo7VlBAsuK4d/xzvwHzkM46ZNE079VHrjM4VciWOapzFEjfjLv/xLkRflzwceeAC//OUvhUf9i1/8Iv6c7373u6J6jPsW8dnPfhY/+9nP8Ktf/UoYBjRWCTnc7ZINl3HQGW+epH0nk4QzqEaFeWoLhiWIrGKqKC/HSl0YaLg8LuZu9fdizcjriOi2ILLq3nHyMErQiyDBMKw1GZKRCs4ywZ8qjspeFFavhcR5TD64jMewliSXgmTkBkoyY/6lR6NDv9OLtUvmib/xXNDLoaVOEpJhNT64eGU+hWWr1xSb8banBR7UwRrVYZ8xgh1dHiwzqPChphosropZrgMhFZ5xUpGXBOPAyroibJxXEpt+OUquXm8QTcuWwWqzwu9PN7XRiGD/MPx2B7wGL6zLa1LqidGbCalGZItOmutgBIcCEPzO9Ko1nn5s1p+BumwxwgtvihUQKK+FXi8kjfjwhvowMrgbzpJidJlNONvahu2959BQGTtv1Qss8DlPwFbuwqKNN8Dv1eP07n4x14VejtGlhTmafBQGCYZrg/cPh8LxJz1OrhWtJnb7C+8mHJsNxPEFYrMe/T7xhtDXX4emvg7a+fOgOn0q6WexfFkWUUjy8kei+MOgCyUGPe6sLYNG1Sj+1j5ox297h+AYGIQ3FMK1Rg0i7hKcOgPMXV4G9f7t8Dc3Q9swB/rVq9NfAEW+6UJO/GdrYBnbJF599VXRjEnPhGXQDH+xIVqC5KEsj96+fbtYiyxdVoLeTbomzguKZCYiCGW4jIuUzMywQ7Kw0GTBBUpPpsw0eZKhThETnoxfNpZooT7mRpjDyWhldh9AbdsLOGRchjXzboI+DcHIyjKhvJwhuMC52TMXpVRwTgWGy/yjs0dEiCnhnPtDYdg9AdSU2ZJ6Mqxc4QZKSIkYWyCEkVG5Gl4j5WwOWmUkHCmtAVsx9lpKsa68BFezbJWDrDyn4VBfhkOGEJ71B1AcAj62qg5zRvtcKJj4zLARjVEX2s84sGleCdY0xDYyObaY1YNLl66GWh0Z7X9Jvpb4ncInvAj73AjVRqGO6qBJU3kodMlgT5uTkc9jGbL0ZkvCA1ipPSNybuH6jWml8SOuIEKnO2HAMBa85z1YZDLhQFsvbFE/hoYG0XzyBLwdbWLjWX71daisrh4tuY/C44ipDwzYVfAMqFFs7RU9OSQeinz6/LHjYQhz1apVYxox+TDoDKNFL1HGumJGx+i4cOnlaNQa+PbtQ5QW9s03w9ffm/JeZTGCyXa+KpADxCh2aVSrcEeZNT7DaCgcxathLaoqq2Aui+BqkwbFzmGc3tUPf2gE6j88jWJ+HntzmPvJwHhM5pXnGvk8FfOqq64SuZVUYO6FDwlWoeYKeUMyE0GSjNzoaKHRas+GJPaQJyg2iSJD5p6QJDp6U1LgUt38AqKVMWtB3fw8VENn4Vz8AM45OkVF10Sgbtqq+qJJHQM9BhIcE80TTfBjRRmJhCMIEkuYY58fgM2ghYXTIjk3WgHmYeitiYa95cvjNxery7yRiEje6hXvqWw6o5fZ7fbgF+19qPOMQHP8HLad1qHWEkV42IFXSyvhDQagGfHj3mVVcYLpDYTw+wEnGiJueHt7ce3idVhaHbsBSWDcQGmR8Xjc7kPwB0aTnUlOQ2QkGOt/sZqgqTHBO2KHYSJVBY2Rrg9Gx6qkfh410QJB0Z8wR9WLheoORJbcHpuEOgH8h9oR7jsN6yM3QT06ibXYoEdJSSmqykpwsq8T0XnzoK+pR/OZFhw9fiJeIk3jqm5JMcqDRnhb+hButIqKtf59A+L+6HN0oLKhBMuWjJf0EcetihkaojeGCtBarciviUpE6qFRebhvBL7m/TDedRfC9Jxp6KUiGacDJaONrf5wGO9ozWhSqcS4a+3oa2g0MPlfqtNgIBjGDSVmLDXpca4jhDn1WlT3tcOj14FjhM7V1eH4nj3xkI0cXZBsnc8GyeQiB+S+yGT+LziS4cbCuD83unG6X9MAN/dSo1qEBTKBbPKTRCdiqGL65QlEltwB9aFfAmotwms/CDhVCEe7JpSAYdUXFQcy8WRoJVJBgKEZEgwryNI9Nz4DRh1r+RBS/0luVNmMqtKpRbWVBPMGDElxXG1iWJJWqk4UToRRrk5+PTgD5OlhH7bUVWGTba44HobVnIefxjv+CpwZcGGp3gV9mRkLymKhvp5AUHSDV/oj8Pf2iWIISTAsmWWJpfJ4REI/nDxkFOr1IHhqWAw+40PdpYfPNQiDNf3NzAozMWp4gsnfbk8QHe3tWKIuRoN2COHlDyNaNHETW9jhgm/PCZi2LINWoQJByf++/gEMHtqFqvmLUL9sRbwwRqmvRiOHG1Klyw2baxiVtSaU11tEUnfPjmZU1VXBpi3H0bd6YLLpRr0cgxgCl0oMkyE0IfjIYo5QBHX722C8+SPQVlTEiwaYv+FPZYk07x3fyAhMRUVCEuhFp1/ky5hn4RwdotMfxDN2FxoMOrT7gyJc2mQ2oL/NhZEeF+qH9kBj1sNaWoqSdesxf9PGMaMLZCm9JBzuA8pG7XTqFBeSJ2O7iAaWXVAkQw+GcxvkpprNxcSy4XJzrOt/ItCip2vJ5jQl0akGTlLMadSbWY4IO7nVGhi0XoRxXuU5FdiFz8q2YlP6SyJzEDwOklu6BSnJRW4g9GBG/CFoE4aWSXQ7/ZhbZhKlqXIsLzt/mQSkt0DtpUQIj2VU8r88SeSpPxCrCKN0+8aiWEJT5FKsNgx4+mDXrMaasB7r11jxm0N9OHu0F60dNuy3lKEeBtiHI7hxfhnKRvNl9BwZemN8mTFkCY3GLBL6YvyknP8SiSLY4kS41wv98lJoyo3xMmafpw1FVYvTnmtRYca3CqVeayyAOH3yDJoig2go8yO0/H2AceLqQG7K7me3QlNphWHdWOHL8GA/ms8245ZVa4XQpfJcK/XVaGSIUcEHD6KjtRXH3nxTrAeSzMKmheI5RCgQFuoD9HJaD9jhCrigChbDq4oCATXUxvGjltVRFar3tCJYVQLL6tXxSkYm6DkpNLFE2u92iTOvMVtEOXIwEsGVUX+cYFp9ATw/5MYSkx7N3gCuKjKJeTEuux9dR/tRM7AXxoZyUcQQCYVguDymoZU4qVJKsJBgaeRJL4cezkx6MblK/Lvd7pTyVxcq8oZkUk7bG5UqoSwKwwPyxskmaMFXWCYeJyAbPXkMTJqN6TPoORQ73gU3IFp9XuuHxMEK2IkILJM5NlL3ijkIVpDt2bMn5fsqJUhkeXJscFkUBk3yjn96dJvmlULljwipfZ53fmeSKa3GVCgaHV427juNhrsut5nEQ+K014/tZ49h1UAApeEqLFpRibK6cjS5jFi5shy/7uiDptOO010d2FyjgmuwC5pgEQYGmkUOjHm4xIoXpSfDvALHFQSODSEaisb6X5TkHdUiFBwRlVATkoyo9U4e6mRPVsuJQ1iv7YbKYEBo1bsB3VgV4VTw7tyNyLAa1ofWj5FU6Tx2GO7OPtSvWI2KxvTeENcBib88EIDXMYyBuXOFtU9FAv6kx8N7RowuqLWirM4c+4y+Xhw5EoS7NwxVZwVgAjgdmkUpMjSoPnwCOrcfQ1tiBBM7H2qRo+HnMh+nLJF2O4agtVjxNIsqANzMIo3R153yUknZgzVWA464/dhsM2GVxYiAN4Sz73SgtGM/ilbUQVtXC++bb8H6wP1QJdm8E3N+0svh9ySxSg9fejm5Jh1+92xMw1SiEC6bYchZJFzE7N6ezJCuTMEwFb2ItRZdSpJRCm2mbPQ0lyO86GbAOvZvlEAJRSeeYslRx+lCZXJEMz9byuanUmJONQMmNriMzZiauDhl/P09QVHazOq2cMCDjtZ22Ov8ohSSVmI6JJsr0+0Pitj7JpsJ60cJhse1x+XDtiEXVjQfQml0HhrLLNBWmMT315u1eNbhRXnIAIO1Enc9sAJRvxudnT1CIywYNIjNg5sLw0RK6RqNhv0vfkSjIUR8Yfj2DkBTrId+VTFUCdV6QV8EGgOgGc07pM3J8DQlnGI5UbTv7BFcaWiFunweeksaYcuQYCh8GTjYAv2KK6AZHUVA9eKz+3bBM+zA0jUb4dJlrqHHPhnHQL9Yn7IQRqmvxsmn9LjlBq2zqIAaD+oazDhR1g+114awLwpVRzHOOBzQ2w0Y6e1A97rFMCWpVFSqCEhLnuoJe4urYYwC95RaMNjTLZ53YNiNbS6fkPDf5/Zjg80kZsZEwlG0vN0G/ZkDqLpiPvRNS+F6/HGYtmyBumjivCTfWza+MmxKsmHVIz195im5/pW5nExkjiYLfka239czurYvJuSNJ5OqLJgLhLIobOhiiCjbkGEqjltORjJctIz/87PTCW0y0ZsMfG8KMQaC6cdHsz9lWU3y95bNe6KCrbFxTC9DIslI7yWZRL/0ZFiEwDzKmM/nJMwiAwJ+Hw4fOYTyiF6UKGdiqRVpNSIsJtFFghkYwZXF3FBG1ZCjUbw25MZJlw/GThdWOdtQXHM3Wk1mhNXAyUE3WtQR1A1GYQ1Gcfe6Wlj0WgT1saotg9GA1asvE9V0DJvG8xGjPTk2W+zGDPQOIdzlgbbRAm1D8sZQWtA6oyZt1deYnIxiWfC8Mlzn6zyGzYY2aOddiXDDBpQGuic8T+I8DA/D/drr0M3dBP2SKnF8Qcrg7Nwq+nKWXXMjToai6PPE5FUmglC97uqCo7cX69/73rjHqdRX4zHzfiLhsBKx39OPbnW3mB8TjAZgLg3BaA4iWjGAxhI9Bn7SAs+i6xHqdCA0pEdHaEhUrFFVWuQtE3toqDnmCiBkK8bDtWWw9/QIYgsuWILmES+utBrxjtODNWY9Vo/eZ+1vtyBw5CiW3LwUhhXL4XnmGejmzYdu0eRVhwmeR3pXNMBk7orfVymnLwlnoqFhsx0us2ahuiyfkDckowwZMAzBTUS5qWZzBLMSfaNhKo3GM27DplXB8BSTjen6T9JBzElRq8SsmlTwMGbuC46byKmUylm3bl1MxkUB5fheZYJf/i1xgz3vyTAKNJ5krOogdu3ahZryKtREi+J9FBOBFWYto2XMTO4+OTCCLcVmrB4doesNR/CMfQRD3hA0HW5s1vSgSmeBT18FfbUB7d4A3ux2oqnUjCqzCnetrRbDxGT/j96gwpIFS+KDnqREurTUeY2YSyhVDQHBdqjnGaGbk/pGDbpJMryWGTRZCpI5L7/Dykb9wDFcZuqBaukdiFQtF+9iMjZOeJ7YMOl5+WXoGpdCbSqDpsokBCWbd7wNW3kl5q27XOiomSMBeDLID8r10dfbi6U1tSlDmlwLcoOl6GG7vR2nj5+Gw+5A+0A7HHoHAsUBeDUeqJt3IFrrRcOdi3Co5XnoAlR5ADqOORAMhBEODAERv1CZNli0Ig/z4pALA34/PrygGANdXTh56hT8i5ejQ2vAzUUmvDnswRqbCRsthpiqxY6T6N/ZjCW3L4Vu+RL49siRAeO12qZS6aXMXcm1Ikcj09vhPSJHF/AxmdHIF0KfTL4hb0hGXjRa7FIqXjkCNFckI3IhNgM0gbHvz82LGwqTy7wxp7qYtBrGsdUIhMJpc0KlZj0MiqFM0oNiRV2qDn4ZLktM8Keqsolpl40KZCaEy5o7+mDz9mDL6kWY0zAH3re6OU2MLDnhd5ThsnZfEE8NOnFtiQUrLTGCodYYf6cORBA5N4INc0qw5sw2hMyLoa01w64O4dXuYURdQSxvNOL2FVXiOKWSA72UmpoF484/PSw57jbsDWJ4bxd6/HvQoe+Bv7MNfc598TLfxHCf3+WHpZYNlMgs8R9Vi7HNBw7sR7nzMBZbPcCKRxEtTi5dkwrebdtEYltbuQTqIgOcA704s3sHqhctQd3S5fFrZtGo4Rkl7YnK15mLWHf55Qh1dGY8b0nOjlnZuBLtre1QB9UIe8PAvpNo6evF4c0mNPb1IxQNQlcWwZwFpeK9/Z4Qzu4fwGB7ECd29EKrV+NQiRrDxiiuGOqGy1mNto4OuBcvRx8JptSCV4fcWGE14qqimFbZwI6D6N7RioW3r4J5WSMCHR3wHTwA0+jMHPUUy4LTlRNzrTDMzIeURSLhsIiExizvLVkWzoKeTIuKsl1dFh3NMxVIJkeg1co+A4LjkRNjnYwpJ5OVmS6Y7F5cZUVoKCa/wQvN0BwTiHS9JxKVy9Sb8SrCSYmgF6GcH8NzsXfvXmFhkWBSeVBy2Fq6EcnjtctiUv/yafy+J0+3oLm9B5+9ZTka62IVZCqNSjQvqjIgGYbLKN/+h0EnbiyxYPkowZB06MFUhoCBNieuXliB5aUaYGszQlUPobvSgLdbnHD1e7Gy3II7V1YL8pNCnCxPZv+PY3hbys8ODwdE/4u1ohjlJQtQoy/CiIszpMvjSsjcVGVYzWo2I+AJosRoytiTCQVD2LtjBxoDhzG33IjwCkoDTU6tm5pfFHq03Hkvgse9GDY50L7rEOatvQzlc+aOea5JrYJXMZZ63HcOx8YqMIwoQpqRCJw0MHh/TCIRTX0yrpmSohIUhwNQOyKoe9eD0A28ir6ePpwbPAe/2Y8z0TPxDbi0xgydvhjz19fi2Y5h9A/7cHnLELpP2OEY6cHQqoUIhrW4vdIiVJWp7ECCIUbe2YOzW7sw99a1qFw3FyGPB76tW2HcvFmUSCvVwGXeR5n/yUbPCt9PObpAjkaWZfE8BuXognTRi1zJylgLnkzuQOsqldeQC0/GHwxjyBMQ45Z7nTFtNG5uXHC8edNVVE0GDP1QkyydJ7Ow0jomwc+qoaamprQ3jhx/kAnBKHMyNJLZJyOt4ZOddiyd34i5owQj3pu9MimkZZL1wRBM7kqCOeL24U2HG3PDKnS0OXFzUxUWVloQ3r8T0JWjraoSTztdiAz40GDS48alFYJgWD1G65J5uGQl0xLCe+vyiBJl3YIiaOrM0NhNCIc9YmOorZkbV0KWCtLM8QVdI4gO+2DzARZr+u+n0RjhD/jh6mnHkrJ2VDfUIrzsPkA3OW2p8MAAvNu2w3zrLYjYo7B7e9B3+hyWXHkNbOVjQ6CyT4Yq0Z5IFNaEEc78PlwfvNaUiaGVLnXLKJKZTLgyFeR60fhDsG0/gvYVDVg7dy5Kw6VYv2A9WppbYAlbRAiHpexcY1qfW8wQes3hwqBRhQ83VOHk0AmgXA37uvlw+oDVZ7z4absLC2xGrKrWI2SOILBrJ1r29KPiyrWoWdcY84zeegu66hqY16yJ5xeVw9mU93uyiaDZaIxMHI1MD1o5uoA5WEk6/LfyHiuEyy6wcBmtTaW2TqYCmdPNx9hGxy1zkXJxyUFn2awaYfI/VbhMlg5fubA8LlFDomUuKh14vAwDscqJm6i01NNVgsVzMuIGDglvif+uWdAEW+Kp1alT6pcp0eIN4Hl7rCCD8uy0vrcNe3DU48dCP3CuawR3rawW45CjZLeWQ2g3z8cfOPul049N1bZ4ZR29RxY5sGSalUFjoVA4Zv/LqWGE7T7oV5VBUxLzAjVqMwKhnrShkjMH96M7wpLXZvT0HEFRUWk8rJYoTDg46MBA2zmsiPSiuP4KhFfeJZpsM0HroAeHOp2oNqnRtOsVGNauhaq8Cr1v7cNwybBI8BtTWKzsjjeqYyEzq6IyTsrW8BpTJiZuRbNXi5vfJO8P0RMTjaJsTzOC5TYML6wY8zchqFlUjtXzVot1wtBc8/49eMPpxcDRE7jdrMHxtjC6OjvQu/ZyVFTbRGXZ0/0jWBWKYp1Hhd7TTpx5ci/U3hGY16zF3M1zYn01hw8jPDAI60MPjimRTqUiPdGsnGx03/P9lKML2JsnvRySDv+u9HJyES5zF3Iys4dceDKyN4U3D6th+BnU5Mq2C0yS8afYsFk6zI15oKMF3QqJmlRQJvh5IzBnpBySpay4Sowv05Nh0t/l8eF4+ymsbigRHsOzR/sxr3zsBpuJJ8N+lxftLtxSasVhtx+DwbAYm8s8zDx3BJ39Hty/pgaVowUN4fZ+OH0deKLiCkQGfbhnYYUo8eZ4g4FzzfC6R4QHOa6Ek6Gg0a/B8mSGx7ipGi+rhMqgGZNDiY1WTh7iEJtSOIR5i1aCroxOvwgBf6U4fyQ4bt6yr4Qhy7aDb2F5ZBD6ytsQmns7NBkSzKsn+/Envz2K0Gh/zYOVlfjysia0vLgNeuqs3XQ9tBMUkZhJMorkP2P1NAqkjI5yQxUb7qjc/2RAIilp7oHO6UX/LespQRD/m5T6jzdnqtXis3uq5yBi8+P/Ni3EuWNHMej2YIe+CLahIWy0dePXI1YsKC7CrbU2QXqes+/AX+JB5JbrUFxfIuRr6Nn5du6C5Y47oE5hzCWWSEvPJtWsnFx4FQxXy7wfP096OQynMwJAsNKRx8gQ13QbxH0+n/icQrgsR5joAk00tGwqoAehDYxg9+5WYelKddpsQ5BMCk+m0+6Gx96DAVVI5F/S1cgnm/FO65teDx+yA5yEIyqu1Oo44dDyoifj9XrQ3NWHDYuqsWrVsvh52Dx/rOfA2SpsZEyFZo8fLw25cFuZDQtNetFk9+awGw16HaocQfQN+/Hg2lqUmHXnqwZPHsAJXSW0fgMeXlstVJZb+50IOnoQtmjjopupEB7yiwZLTaURukXFsfJiBdgrE454xHyXVPAMD6GutgnuMGDQG1FTHTt39JKVYTXLSAsWBk8i1PRhhF3LoJtgpowSX3nmZGycwige79fi8uffwOXRSlRdu2JCgiEsGlU8+S+LILjZLV68OPm9MgWSQe8ASo93YfiWLYiw6ESxRJWSMxI7nF6cDgM3q/zoPXMa7ijQs3Q15hw7gJvnz8HTnjA0bJxtPYkDDDMdPSpCTKX33y2aVQmG9tyvvArDurWi+TJTpPNy+JNGAcHrqPR0sgWRuyopEQ82YpMQduzYEa9AlWMUpJczFckrt9stfhZIZpYQU56NLa5sLB6+16HT57BQ78QNm9aJ/9OazQWYkwmExgtgcYG+uecQSg06EaJL15OivKlSJUNlBzgfMrzBTZM5Drr+tNR7erxYWF+JxsZY2GLQHRA9LOUWfcbhspMednC7cHuZDQtMetHZ3+bnQKwoDL1eDPvCeJB9LobzN9qxDifah45CE1qE+y+vx5wys/j+nacO44YFVqxcuTIlwTM/Ee73ItBjF+TCqrRkkIPGUiX02fDodTphLa2Exz52E+CmQDLmmORK1zEsMjvRX3U3ulx6GM40Q+UxoWRBjLBJ7OnW4LCXQ6XHQmWsQVlFPbSVmTVsmtRquCNRQXyscmSSOp1GHT0ZTNCLFX8uBxP4gwjv3gr7igaYyosRDvvi5zrZGOadTo/Is92sDqCv7SzUcxbibOMi1GlUKPYOY4+1HMvLdLitdAk8LC1/4gk4wmGcqquDZV+s0o8P/f4DUJtNMKxbh6ki0cthKIseBdfQdIoHJuvl8LOYN+W/5RAwRkRYJs2QmyQdS4aD/Jj053FO1Px8oSFvcjITTcdUCuFNVxiTFSU79hyA3enB/3lgM4ptVuEB5KJEmjBo1fB6xm7YXJC0TgOaItxyWVPGBJNJgp/g82RfCa1fLnzqbBn1OnT29OOYxgOjtxqDETOqrPpxJc0iXMaZ1Ak47vaLpO+d5TbMM+rjHk2pRo3jvS7MVetw/9oaGBXl2MdcXjx74gzuH+5H8bXvQVWZWdyU9Bhonacb28z5JuF2F6JuwLCmHOqi1F4APZl04LwTrcEAnZE6VwzTjE3iHjl0EOZzr2NplRGqNR9Ho7kCzIy5dD0YVnvQNzQUF2mMy7UkGcx25YJSbDtjF3JCYlqkXoON5bXQ1WceUmEZc9egHSMtJ0WOjooX6RALl02g5KlYT1V7W6Gp3YzheYBFIZCZqGHG49094hVh0HtLzDjyzik41Tr0N8zHMosJS70O/LykBot1WtxWZqW4H6KvvorqxUsw77prERrNdTIkeez5F2A6fgy6u+9GRW+v2ICnO0OeBEPjkH1kvBbK/M1kiwcmAxlN4L6kvNc4pI1elRT1PHv2rLi3JeHwGFPtX7Lbf7pGNK8lx6/T26JCSSZ9QFN5zQVJMukgSYZWynRIRgpcOmHGmqXzBcEQMq6bC+h1GjgU1WUywb9g0RIcbw2jrtiUVYJRgueLi4euOL2ltgMDCIZCqCmD0IN7+1QLLEYdTppHxMYpNZ8YLuOsEyWOun14w+HG3eU2zDHosMvpERvQtVYT3j41iKAGuGd19Ziha0dcPvzseC/u7TyFhrJFMC6pFmTHY+INma7AIeINIXB0CFFVBLrFJWkJJu7JECnOkccxBEtxbNgZRTLl8xhmPLR3Jyp730JjQz2w6iFAfz5sycbN6mIr6hsWiGshw2r8DrwuysFs3DT/371N+PNf7cbBPj/qSs346o1LUNwXECMGMoVnyI6Tbe340MqVYlbQhNBlHi6LHDkGvdML/aNbgPYnBaGER0XLEknmbMCI4IgXdxcbcfbgfozoDDhROxc32sxCHuYn7e2wmYy4o9xGywnu556DbsFCGK+8YlQvTyM860qTCSM7dyH88CMYMptEXoOGD/OGkrAnm9fge1CVXamtlyysJgknm16OkmQSQU9Eqi2Ew2FhUJFwKE3FTZzHyu9LUlJ6xfRkpksyJLW77rpLfB7PJ+/xxx57DDfeeGNWX3NRkoxcPNMhArrUrIVn2EGrKoNZsfHLcFwuQKs+GIotcjlsjTeGV22GxdAHq1GbMsGvHCs72cUntd9oSTHfwZ8kgEBYjdqaSiyoWIAj/nYsK1UhHHKK8m1+Hm+AKk0JrD69VIXHYbcPbzs8uLeiCDV6LV4ecuOcP4hbbWbsOD6A+iI9/NaxmmgHRzz40ekBXNnlxwZ9J9RLr8e59nPCG1g5webJyrHAcYfoijdWN0Ctnrg0l6OViVRniSKO5pJSxTRLlThHh3e+iTmOHahdtAbRprvGTS+l9pnMT3GdyDyXbOwj4dCiZjLYZrUg1NeNT9RpsOJdN8BotcF/1A5VjVkQdyagBllfWwdq5i3IjGBkuCwDkgn19iK09wB6Ny6ARvQKxQhFkov8yTBZV9gGT8CAT1fq0XpgP7zWIhyvs2GdOoR1VhOeHHQi4vXgjiITby64XngBhlWrYVi/bsxajYbD8DAP07QUptWrwLIWGhjp9NVI3Onyo3w+zxPvI4amJlM8MJUS6UQolTXSQaPI1RCJowtolPD7kjAZYZmubtn73vc+QW4MsfJ8/sVf/AUeeeQRcb5I6Nl6zUUZLptOhZkUNORJY+knLasjB7pEE+Z033syfTKsDqIrLRP8e9ocqE4iipkswT9ZglF2zCv7bZj8Z9c/m/wodTPsC2HZvEaY9bGZ8Xyd2DTbOqDrCiGk6sBAcTlO6kx4sKYMZVqNUFYORaO4yWTEa0f6saTKgs0LSvGtriHRlMnmzL1OD37c3I91PuB2vQdatwcnoUNva6tQc0i1MQhr85xLPHSLi6GtMUOPtRl9Z7XKkFaPjEn/2urlsedqzPB6fTi1+1ks8h5A+aobEJ1/XfLXs1clSeJf2djHZLBzaAiHXn8ZXr8fnvIq7N5/AFXFFSjv0qHkmgxkZ0aFWGmEXLZyNfaFJmFhM1w22i+TChGfT2z2msvWwG8+eb5yjCQzqgIqfx5xB9AaLsGdOhfaDuyHp6Qcp8uqsXmgA/NUwNODTkHmm139MGkr4N72HIybN8GwYuzYAsK3a5cYg2HctGnM79PpqzGHyNCSJB1ZXs5zRO+FpM4+ocnMXklVPJDsfpvIy5HVbJO9L81JRhfQ+P3sZz8bC2cbjfjWt76FO+64Q1SPTgY8bxyjzNHLMtpDwvjGN74hxjC///3vz8prLmiSmQhT6ZWRDZbcPKXAJRdVn9OPLQvLc1ZYoIQ6GobLw+oXkzgGmX8R45aLx5JMokTMVFx5LlqGImTH/HiRzIggGSoNFI32CSX2CUSqG+Ha24ttxWFsGxrBGudZHO0y4aC1HPNKinCFxYqXjvTh8sZirJtTHNOLEvIyERx3+fCT5n5s0OnwaJER2v5d6NGWYXDEKTyqVIlNTuMMnHQgOhKEYW0F1LbJyajHwmDcjJJMTmQFktMJy2hYJRgoR9f+19EUHULxhgcRrU1NZEL9IE0zLeEesqNl5zbUz1+AuWvWxwezDR/uQad7CIfeOTcmrJYY85bCmwzFsYzbrTPAM+DM/LuzKTPNvcHj8b7xBjTlZVCtWAacVZAMxnoyPWErdjh9WBLthP1MGNaG63C6uFzkXNQDEbwW1qKIastlVhxsbQUCZ2G66SboFy0a97nBc+0IHDsek+9PE+ZO1FeTApeyNJ9rhueN1j69ABLMdKqwMvVylP04ynsxG93+GsXoAnrB//qv/4qf//zn+MMf/oA//dM/FffvG2+8kbYpWQkalQSNOAmG5Fg0wr8lI4ypvOaiJpnJljHL8kK6pEqBy1hvClCmqKjKZmGBEtw0Os61icFldO3lQhVCgU4/1s4pTkowU/Fe+DqGEOixpQpHCU8mEhta1j08Vs5m7BPVOKIGWk02fKaxHp5AEL/r6EOt14XgoWP47qAKWxaWYo7JHL/hOFfmDbsLb7U7cK3JiIcXlMO3owuBnv2wL94UD9klQ8TD/ItdyNgY2P/C6rYpIBYyUyVN+qu1WujNFvT29KD7nW1oMrlg3fhhREsnmFEk5lan9rCHujvRsnen0B+rWbQ0ft3KS8pg0QWhv3ExPJqA2DDppZBM6P0otdWY3+F6JcHQmmXjqo+bHXXmMlgHE4XLAoePxJsf3dEYeSnVvKVR0+wNoyVchof0Kvy8uwXmmqtxprgc95TbUG/Q4SdhDQJqNe6tsMG7ZzfC586h+MOfgH7ueE8t4nbD8/rrMG25CppxzbXpIWX8aSTRUJThJdlmwH+nIuypYLKNoMzjZbNiTT1KsiyCeemll0QYlgSTdKxICtCoEfdhQoiL54h/y9ZrLmiSmWhTnUxIi4tSVi8lyrOwL6TSNraiSmnRZAt06Tn4a059LU44+8Ycg8sfhjcYFpI22SAYaQlLKy9VLDXW9R8bWkaSW1CRvBx4j8+PveowHim1oTcQxtvDXtw9pxZRZwBvjVjwyFI9LGGX+H60LnmDDOuLscMRxp2lRbivqRKuI30YPHcQpRY1ll13J9QpVJ3DAz4ETjhEabJ2wVjpjsmCCX3OlUkE57SYi0vQ3taKkb2/xbIyDYwbP4mopXLC9xRFEEnCZUKt4UwzOk8cwYL1G1FaN1bnLtzjhcqshbpYD5vKILxo2Uku+5loFPDakXxpwUsSNquZMYr1yti0mmmFy0J9fSJkZbnrTtH8GPUMj2u4ZOK/P2xGz0gYC0Ln0H1ED0/JQvTb6vDpiiJU6bV43u4SvTG3a0KI7t8Px66dKLrssqQEI8QeX38d2oZ66JYswXTAe5MbHu8P6hryJ70cSdg8r5JwJiNwmQqZNIJyzcsccbZKpN1udzwnw+909913T+r1NKJ5PCRAZdUejZdUVXxTec0FTTLZCJeJ+RrnzgkXO1HgctgbxOP7urC9ZVBs7jc2VQr1Y0IuzGzkZbgImeCnyivd0CFfBMEjvWOeQymVCoteiFammwGTCbhAmLTjuaG3kE4SRygxCyKNCsUDltsmgj0R+z0+3BvR4diID6fCIdxbbkNvnwf7zg3j7tU1qC+JhbxoefHmONHWjdNHutCk9aNeY8HJw/2I7nWgVteLkvVbOCEsef6lzYVQuwu6pSXQVk2/P4CeTCQaSBrOco44oNn9JhbX10B/2XsBQ4bhliQ5Gc5VaTu0H47uTiy96jpYS8vGf7dON7TzxpMmLW8qNXBTZJ5OhilZisswJ8MV9HC0Eb3QL8sk6yA0y5LcG1Hmh15+RYwz1o6GXWRojCQmf/aHjWgOV+CBqBPbe8/C0XQLRkZ8uMcWRrVeixfsLjhCYdykCkB94jQCviBUl62H1ZR8rfk5bHBkBLZbbpnWps/zqDSeZKiVZELC5mYviwekvpokHNGAnIXJlYleDgsWGC2Q0jKJz5sq4XimqcBMr48g+coJwjx/3Ifk37Lxmks6XMYFwJuUFiIXpFL/iifuf95uxTstduFFMFT0vbdb8ec3LxZWfbZm1sgNnwuRITom+bz9QwiEYtVi8oYTQ8JshqSu+GTADV50qFssoldgojgxZ65zGugIGwbZhGk9b63w/+zqZtPdveVF2NrmgdsbwCP1JTjaNoxTfW7ct7YGldbz4QkeryOowb4hPd51xUosqzKK5LXjeB+KImqE/c1oCS5Did0+ZiSuyL8cdyDqCcGwrgJqa3bG2NKTCWFsLoPnt/XoHjSEjmPemiugXf0AoMncSkvMyYSDQSHRH/B5sezaG2Ewj68Iigz6hcYa1QnSjdLmZklpHzkbiNeT67e7uxvdnjC2d7VidWX56GC21F4ew2VM7I/zJt54E+rSEhjWrDn/+9GGS+nJ9IX0OBGqQGOgDU7eH5Ur4LAU4f+WN2FZUQNe5ryYYBgPllnQ/vxBhGlxv/9D6DlyAMVFJUkr2Px798Fyz91QTcMa5vEzV0EvRoYRE0Frm4TNh2xAJuEwnMZcrCwX5iMbPSgkNeYquLfQ81SK1E63RNo1WsI8VXC/IUk99dRT+NznPid+x6Q+19PNN98cfx5nRvG80EDM9DUXDclMJ1ymHNWcbFQAu6ADnYsAAF8aSURBVLBP9IyITTUc9Ysxwyd7XCI/Izfa6ZIMNwhuHLS2mOCXuR3OiaE+GXMhDFcR1Otqqo7lM4ipWD/M97AkmzdYSrmRpDNlgAF3UJCclJLnDbLN6cFxjx+3llnxqsMtjvVBkxk7zwyhe9iHB9bVosQ0lgzO9Lvx8ol+XLe4AkurLSL809/Th5XWObCaewB1NXq1pWgfLY8W5b/WMti6VdBY9TCsr5hy/iVtr8woeNMff+cV1PS/iYbrH4F2zb2AQiolI7DvZ9ST8XvcaH5nK/RGE5ZdfQM0KSzlUIcL2nrLOOkbuZlwnTBnJjeqxGFbtCrPdttRGvDAPWyPW+myp2RciW+ScFngyBGE+/tgffC8COUYT0algj1ixGmvEWuinXAM9EC9/A4s16nwcGURrJoSsQ56AiE8UGJG9JWXEXaNwHr1NdAUFcE74kTtkmXjPSeWK2+4HNoMy6/TGYws2KHBmIlgrbIpkhsoiVwWD8gmWkk4JInJJu65x9Dz5AZN/TilsKc85uk0grrd7iTCsJmDBu1XvvIV/NVf/ZX4bnyvv/7rv8ZDDz0kSFriAx/4AK677jp897vfzfg1Fw3JTIRUJMDmId60clRz0gYpnVps9g5PAHXFRjh9IVgNWpgU81KmQzIyB8TObOXGQRjZKEdp82B4dJOPoNfpxTULY42BU7Gu6N4yH8LPmszMG5GTiUTQP3I+6c9je3vYg1PeALYUW0QH/wKjHhs1euw+NYgBkxoPrB0rE0Mc6x7B26cHceuyKswtM4qwBm/qdXNWiPJnnbcN2qaNWDEvJvfB62Q/3YOhg+04a/RCP8+Gyh6PuOmzJaXBrn8ZBmLu4+TWp1DZtx3DtZfBsOa+CUcuJ4NKqxLKA64hO06/s1XkXhpXrYUqley8K4jISBD6FeNnztDSpiXMJlSGe9Jd+yKDDmZLGVbPbRgjEyRLfOWgLRJPYrgs1N8fE6G88w6oE86tJJlzvgBOhqpxhzYE61AJ+lb8EWC24oPlRWKmzesON9r9QTxYZITqhRdioqSXXy7eLxQIIOD1wFQ0tnDF89ZbUJeUwLB69aTPc/z4IhFRCEEyJsFMNbHPNcVSYVkuTKOM65PrlB6JDEvyHE5EYqkIRonpNoK63W5xrNPB5z//ebEf/PrXvxZr5NOf/rQoj1aCIXWS8GRec0nnZLjZ0qVmcxebLFPdtCSYd11ejx/vOIe+kQCKTFo8cnl9vHx3OiRDK5M5mFRDzvQ6LdSqGMlwo+5zekV1F6vbplJBxnAUVQMYHlNOD80EJDnqLva7A6Kyje/H8bhnvAGssRrx2pALVxaZ0WTQYXdPLyImLe6/ok70+iiPYV/7sMjP3LWqBtVWrSBYLtANl28ADjoRjQSg13UjXHlr/HUWuxoGXzH0t86D3xwRGybLrXnu0qlHTwYm0wLodOXwuN1oefNXqA+0QLXiHmB4Yk85JSgsOuTA2W1HUb9sJaoXpPcaQx1uaKpN4zw0bnD0POl1ZrKZWIQSc3SclS5LfJXnr9g+hDKnExXDw7AZDGLMs+Gy9dDWjhehrDJVQWtowNODI3iPdREiHW3oWrQCFrNZVJHpVSq8NezBWV8QD1q0UD37DFQ2G8zsAD+wN1aQ4HRAbzKPEfvkYLZQdzdsDz885XMtNAUPHRJeCAkmW8nnxCZaZViSxhrXnyRsrj/l5p8JwWSjEdTj8YwbNzEVPProo+KRCj/96U8n/ZqLhmQyUWLmRkbwQtGi42a7du1asTgmwjWLK4SkPfMhHLk8N0HeXo4zzhR8LhcoF2riuOjE9+V+4/YHUGzSotvhFfNTJhsiE/paR46I8kZaI1OJ32o1KvhCYTi9DJfphbXa6gtirlGH3U6vEL2sUqvxh4O9mKvXYENjyTiC2d4yhBO9LpGfsWmjYqIpNwPhXg8GEPCHoYu2A+WVgKVCjAwIHB9C1BeGYX0l1GYteOaZWJSDxWRYQyrayhue53QyYQ0qMHvdWnS+8V3U6z0ovfGzaDvbA3PJ1DY9UUHWehqezi7Mv3MTyurSa4hFA2GE+7wwXD52PXKN0BiaaBhb4vAyio+mK/GVM+z79u7FyO492Ld3L6yHDqPIbEJxbS0qksgwDYY18Jg2YqFzEJG+HnQsXIZysxl3lNmgVQFbnR40ewN4wKiC+pmnoampgenaa6ES1yGWV2TPkdKLCQ8Nwbd9hxjMlug5ZQo58ZNehhzIlgskhiWlejnXII0lQvY0MQ/GHCsLMzIlmKmWSJ86dUrsZRcb8opkMvVkuCi4GGRyfTKbbWOZWTxSvX+mnow8BpKeTPCnAhcmb17OcYkWm9DnCgqSmQz4Ofw8LtSJJPHTgWXbbn9Y5Fa2u32CYIq1arT5gni4qgi6YBRP7O9GXYkRl5VboQqer6piXum1kwPocviEyrIm5MOuXfvFDUkvTgyjao/JlesN5xCtbBKho8ARO1RWXSz/kkRahZuJcm4HE70kHBI4zzPfX4Y1JvreA71dGHzr+6gvs6F4y+cAQxE8juOonDe57mnxfVlBdnAfnN3dmDdnOYqqJ5amD3V6oC7VCyJVerr0PtesWSO+Q6bgTBl3BkYPz19VbS2Kqqswt6ISwyYTnJs3i854GiXKsJpDrcWTA07MGeoD7ANonb8UDRazyMOxH2fHsAcnPH7crwlD8/Tz0C1cFNchI1jMwLCZ1+mIkwxzQfSc9CtXQjfFceW877i++ZP9ZLkimGRQqpfLsK6UuqHHw7/z/pajkXNRIv3d735X5DMnGlZ4IeKCIxlutpzjICdYZrNxMlOS4cKj+8xjUCb4k0FqkFnMRuzZdwCehjKc6tfhllWZ34z0XBjH52aROLBqsmDJtFA80AIBbwAGVsdEo3hXVTHcniAeP9yLZdVWXLGgVIR9It5YOTBVAl463g+nNyQIxucaxv5Dh4QnQmtQVNnYfYJUVCofNOiBL3ojgvsHoG20ikemYQZl5zdvbKU2GC1KGfZIlNzvaT0J1zs/Rn3jIlg3vg/QGkSpseiRGdUsyxShYABndu1AKOBH03U3ILxrGKB+WRqnihtwqMsN/bLScRIoShHHTGFWzJSZCMzJhO1DiOzahYq770ZNXS0YdWcIhuePj13NZ7DHXIIV0SA0QZ8gmMU2C64vsYgCEAqeUqPuvogXuhdegmH1mvE6ZApPpmr+QvE77zvvcKeG8fLzXeOTAe85rm+eL56nbN7TkwW/G68T8zmUeWFxBtciSYckMBl9tUwQjUbxox/9CF/72tfw8ssv4+qrr8bFhguKZHjDMPnJhClzMNmWf8mEZLjY6D4z95JOop5QJv6WLlqIucUaGEMj6DzWjdOHeuBoLxKLmBtmKm+MmwNLMeU8kel+Z77aropiWKeCLRpFjUGHm0ut6B324bmjfdgwt0TIxIjnjs6U4cC1Z4/0ie9DGf/Bvh7hZZDw6H1IhEa9GIO1C6GhUgTbAP3yUmjKpzbKmt+V4QplE6PcMFktxGSt9HBGupqBQ79B3fKrYF59LxtmxHv4XC4xY8ZkzVzjyu92xSrIzFY0bbleVJB51U6R/E939sO9Xqj0auHJ8FzxHMly+qn0PzBc5s5wWBrJlOCcFuUwMBIxDQFzbT3e6R3Ciq422DxevKUzo7H1DKpKi9Dnr0CbwYID3iDu8Y/A8NqrMF6xGYbly5N8TsyT8Yx6MsGWswiePg3bQw+NhtMmB0YmSDA0LjIpwZ8JcJ3RiJTl5VyHk9FXyxTRaBQ/+9nPRGXX008/fVESTN6RTMqZIgqLkBYGE6e5wEQqzxMl+NNJ9HOmjEZngKnIiuWLDLhxbWV8w2QohQuUGyZJR3br8/O4mXKhT0ZeIhUY7tru8sKtAixmLdZbTbiiyIQzAx68cqIf1y4ux7Ka85sxScbvC+G5Az2wGDS4ZVkV2lvPiuuQWHQQcQbE5EqBzqMIWpfAcFkF1KbsLTFWGSkl1GXX/Kkdz6LGvguhhs0YqbkS+kgUMipHUUwT5f0z7VUYHEDzru0oq5+DxpVrzr+O8c40XoWoHmLZcoM1PpuDHijzVFOtnGPiPxCNIMjS9ySl0MrP9m3fLv5NzyMRA8EQftfnQGlvJ4oRwdmFy/BQiRWLIzGpm5fOtuNgMIo7h/sROHkClttuS0owo58mqsooe6OHCu633oTpmmugnoRQpQRDoSQYegcMJeYrwWSqr8Z7WIZ2lT1hqa4Zq7m+8IUv4Pe//70oKb5YkVckk24eCj0YXlhucLlCKrl/meDv6ekZ1+SZDMk6+DmCmXIulLRh6bByw5TaTHTP5dhk3nhMgMphTNMFQ2Ls2j4XZiJYhfvqS7HKZsLRrhFsPRMrQ56fIDEzwkKDtmGUrS7F9YvLcfLE8XhjXKJlHmx3iZ+qkBOayABUV74PqiQjDLIFKaE+cvx1lIzsh2njuzGorsSZlhYcHs1D8GYP9PfCkmGobLDjHFoP7EHDslWoXrh4fENmGq8i4gggGogA5XqRW+C143maTnWUUa2CGip4KTsz6pklQ+D4cREqS/qdgiH8tncI5q522BBFS918XFtqw2orvUsz2rRGuMzl+GR3K/S7T2Jo5Qo093TDNOyIe4ljmmgptOl0wmCxwvf6G9DNXwD9wljYbLIEw82c9wGHZOU7wWSqr0bSYeSBRlA6QdQnnnhClAn/5je/yVrTY75Cm+/hMWnpsMGSlsNkVZinGy5TJviZf0nnEsv8i3LWhFyoeo1ayP1z3DEr3JTg96Onwgc/h9+ZRQ08Hv5bJm35cyoJUcryP28fEWXK7L24rq4YK61GMWpgf3usDLm+ZGxIi8f5/PF+XKbXYsOCYhw8sF98LxYdjFMQ9oQQ7o91mxtKu6GqbYLKOL25GBMhGPDj3Gs/gtnVioqbPwdd+VzQr6KXK/MQLO89t3c3SuoaELIWp+ya5zXrPnUcPc0nsfDyzSipqUvekDk6UyYZ6MVEK/XYdyBWHUdjZLq5BeZJTBoV3KPjE5IhPDAQq+q6+SYhHwOuvdHPHQqG8ZueIeg721Ci06C1qhE3l9vQZI5dPyo7bBt24/auVpQdPQzL+96LudXV4h6Tg9mUM4b44AjrkM8Hvd2OqFoP05VXTPp7kYC5mfNe4uiNbI9Gng7BcH1kQjCJUN7DyjlDHR0dIpdI4qIByRAzz+0nP/lJ/OIXv8Cdd96Jix15RTLKC5tM4JIbb65mvhDcHEgqiZ3ZtNozSfArJfoTmyzpyXgCYeHJbJ6f3LLm5sjvzPAKNykej1ysTDqyA5pejczjZNIFTYJ5dnAEZ30BlGm1uN5ixFMqNlHacbrfjfvX1gr1AyXY3f/skV6srbehLuDCvt17YbSYUsbMRZioxgTdomLoDr+EcNXYuSHZhtc1jK5XvguLOoDK2/8MGsv5kQ3KPAQrdaJdrahYshQjbrcYDMVrKAsHeC55jdoO7IGzvw9NV18vRDSTIebJRFIrSPd7cFTbDlOxRShgZ8syNyt6ZRIRDQTgfuUVGNathW60KomVXpSYoc7Yr3sGoWlvQ5nJgPbKOtxZXoQFJn18jPabDjduO3sS5WfPwHLPPdCMhj95jrjG+FDOGOL56zp+HBrnMOZ4/MBHPhYntMlu5ryneJ7ygWAk6ZFgeEzTznsmzBkKjOqr/dd//Re+9KUviXPA8BjJnJVsqWYrXSzIK5JJFLgkuSib1nI5WEy+P4mMkDXz/PzJJPhT6Y/pNSq0ufwIR6IoV4wYkEg18165WNmgxpCabMDjjSoJJ1lpJWP5z9pH0OoLYK5BjzvKrXAJzTKgze4VVWLFCTIxrYMevHisD1cuKMMcSwRd2zpRtq4US1Y1Jf1eoi+kxxsrT/YPAj4HomWTD59kCtdgN/pf+zasRWWovO4zUBtSe0x+t1sk6ucuXhLvgZLl0ez69nu9iA70wGI2YfUNt8Cs6PsYB5GTSb7Zu84MosXRjqKmkngpd7Zg0ZBkxpOb6K5/+22orVaR7I8rGYRCcIbCeKxrEJH2VlTarOgqq8J9lcVoMMSu9UmPH68NjeDGU0dQ2dcD6333pcypKGcMsdjm0PAA+t54A8Hlq7Dr5EkYWlvjXjZJOx1pyKZG2XOSLwSzZ8+eKXswmUA/qq/24Q9/GC+++KJI9NOY/du//Vu8+93vFpL+V111FS5W5BXJcBOga8lNNFnuQ+ZM5Gaeq8Q/LTaSHG8EysRMJsGfapHSk+kfCaC22DBmxICyUY/kkq4TnB6ObGCU1hE3TNbzM4QlCwd4E4ehEh3d5/wBrLYYcV2JRWinPXM4pgb9wLoaWPRjLz+9rOeO9AolAvvQEI7sP4NNxTWonRMrUU4GljmLvhCrDurW44iWLUoqPpmNYXCOjpNwvP19WBtWooI5nxSjAyRE0t9WHF8rysTt3Pp6HHnzVQSsNqgqqvHOrt1jyqMTq/2UI5iVGLY70Ln/DKxry7FwWXpjZMqeTBIPKnD8BEKdnbGqrtHvRw/G6Q/g13YPgu1nUVVSgv6ySjxUUSyUlInTXj9eHnDihmMHUO9yCg9GnWFVlFjrzc0wVZRjzQMPQWc0xsNqzJvyHlDmIZT5KBpIJBje09NpasyFB0MDTYqU5gpvvvkm3vOe9+Db3/42PvjBD4rv/y//8i/i3s20OfdCRV6RjCQS9r8kq8iR4So5+jTb4HvSfaW1O9UEfyowJ0NQaSCxao6e22Qb9ZTqs1KXiV6OkPwHcLSkCm6TFbfWlGN9kVnkg0gwgRCnYmIcwRBUI7hqYRnOtHdj55kuFFfWoqUXeOudDoSLdCgy6WAzalFk0ApZHptOjaK2EVjWlIkyYXX/cYQX3jTmPalt9rv93WLUM6vXHl5fJ6RtJovB5t1w7fw5bMtvQvm6uzLSIPM4hpL2x4wMDuD0zm2onjsPc1asFpu0nDefWB7NhwhnJJH75zk//dYR1NdXo371UuQCMiejRHhwEL4dO2C+7dYxBOHW6/FU7xC8g/2orqiAs6RcCF2W62LXusUbwAt9Dlx7aC8aIyFY7r4LqknognHwmWdoCLplTTCM9igppVpkaFf2NNEDl3kw/l+OAs8ngqExkeuw3bZt2/DII4+IkcaSYCSkvP7FjLwiGV5ohhtkXiMRyumV2e4I5qLjZk83dsuWLWnLTtMl+FOBngxRPdrpz9cyx0JSS1atNVVdJl84jMfaezE87MLy/k44Opqxo6gMBxx6NFSW4I6V1XjhWF/S92GZtcnTizJ/Dz5+a6x50Le/H6FKE9xFOiEqOuILip8tAx6ouz3QDwXQfMiP6ugAVg7Z0W6xoGh4SJCR3R3At99sRYDlrho1fruvW4Tn7lw5uXLsvgMvwnfkWRRveBQlSzMPK7iHHShNSOIPtreh9cBezFm5Jt5MmDhvXlkeTdIm6kNlgmzK5piEsUNv+8jhI1hpmoOKNbnr0mYZc5ei2EXkYahLtmY1dAovm0T0hK0cwdazqGAeoKQUj1QUoXi0YIAh0+d67NhyYJfIy5hvvD0mqpkhhODmrl3QsldLqx233hPzEJK0WZHJEn157/K8svJvNkNlM0kwO3fuFIrG//RP/4SPfexjeUGwlzTJJI6CTfY3LohsV5jJBD+JiwtvIoJJl+CfmGT0IvEnN69NmzZlTQTQH4ngyUEXwgYTPrW8ChXaxegccOA3u9pgjjhg7O/GYacNTpcRHs/Y5jGpi8ZzwQoy+TeVTgMjVLAWGVCtGNXMpjyfuw+6dTW4qliP4MkT8NpWYMRsgNMfQrfTh11nHehweFFs1AoCY4/FiR5XxiTDBsOe7b9CsH0vSq/7FGwNY2Xl076WOQvHEOqbVsT/33XyGHpPn8KijVeiuDp1iIIbojLxLbzbI93o6erGoeFmcW5YpLG8ehGKPSaoy6c//jd9Q2bkfPnw21uhtlhgWL8+/hyG037W1o1ofy9K1qwFSsvwUAWl+tVxteVnOgdw5f53sKS8NKZDNomNlcTmefVVkftRt58WwpgTgaRNI4VeIQswSCxSAVlKBWVzfHI+Egw/54EHHhBS+lQ2vhQJJi9JZrqDyyYLaa3yRpDTCTPJv0x25Co32rnlJqiCPuw6cCCu6JrN/oCnBkfET0rEMGHMHMuLp5y4YlmjqGgjuR052wVvX68YTCSVjxkWpLXJ75PY28GGTApcJoIikKy40lSaYIlGoPWeRbjpHtQWnw9PUZ/tZJ8LZr1ahMhc/hAqEyrZUiES9KP79f9BaLgHlbf8Kczlk9PECni9YrgYu9Ij4TBa9+/ByGA/mq65IX2CP4XMiKVRi0hpNQzaWFmqxWxB76Fz6Cs3wNbimnCo2HQ8GfbJiO9ELbeODtgePp+H8UUi+OnZTtg7O1BZUQmDtQgPVBbBOPr3Tn8QT7X3YuO+nVg2pw7GK87rkGUK79atIizHKja0n4YpyaCyRNBY4SbLnCY9Gxla4z0kpYKU45OlJ54NbbB8IBjuKffeey+++MUv4o//+I8vWYK5IEkmWxVmXOxM8JNUmPRjboOufCoV5kwT/KlAif8rarWikiWTWSJTwWVWE+YYdKI7/JzdK8JiG+eWCEl/aV1W19SicUSPa9dVi+/LogMmH/mdWNnGuLqySoiClsqpkPHu9nZ2t8cmDaqG2oSMS7RoLBFQoubmpkq8eXpQSNOsri/C3asn9mJCnmH0vvJN8GNrbv8zGKyT0/ySSX+jzSYIhvmXaDSC5dfeJJLVU4IG6O7oQo9xMObpRQzwBvswslCNfvvAuPLobIWEqF9GT0bkYaTK8aiXSc/1x2fa0d/Vher6Blh8ftyjDccJpssfxO/PduLy/buweuliQRKTXXOBU80InmuH7aEHBbHVL1+Fsrr0hM+SZ0YG5DqfSCpIhialNpisVpusAnemvTm5JhiGwe+++25BLn/+539+SRNMXpLMdKZjTraKjYlyWu5SuDDVe0+XYAgmQ2XFmlLvK5uQPRDNfW68ejI2rbKpxjpOWoaJf4YGSToMBXEzYPiCNzpvEH5X2QBaojZC7R8bvozY/cK70dTENjv1wHFEKpeNS8aziu4TV8/FbSuqRMEBm1Bl2DAVAkNd6Hv1WwhZqtFw48eh1U+NFBgq02h1OP7Wq6L3Zf76jdBMsTmS1/1MawtCw25suHqD2Kj8R+3Q19tQ21CM2oa6ceXRk1WPTlddFgyF4Xj5DVhWr4qrHJNgftR8Dj29vahumIPakiJcHfZAF46FknsCQTxx+hzWH9yLdWtXwbA881CjRNjhgHfr22KODEulibqESZipBghKrb2JwFCZcnyyUoF7soPF8qH5k8d91113iWZLTphUXeIEk5ckM5XBZVOZ0c33SKxiSySZqST4EyHnRDABypkzk1XinSwOdzqxvcWO25ZXjVMWiB1PrJOciWsSinKAFm9kVv/I5jtWvkX7/KhQF8FsO98AKrwYOVo4HIBq8DTCq96d9Hh4vpIdRzJ4u45j8K3/Rbh2HRq3vBvqaVixbocDLvsAahc3Cet7qje7nHES9vrQ1DhPbFQRXwiRQT8MGyvTqkfTiGExSaJ69GRGUxhUQORcG3wWM8oui6kcByJR/PDkWXQODKK6oQELSoqEyKmXJBoMoS8Qwu9OnMWawwdw+ebLpiT7Eg2FxBhl/dIm6ObOzeg1cuonPRSW2U8WybTBlIPFGEqThJPpYLuZJBhGRUgwHG9MVeUCwVygJDOdnIyME3OBcsNP7OBXkoyywZKYyphkEhk3KFbaMMGfrRHDycDj3d3mwKFOJ+5ZXYPa4uRWX5jWon0QR50D4qZLHPaW2Hznah/C8PFedPX0iAbQUn0Rah1W2OY2QEuvyN4iZrbAMvHQuHRwn9kB+87HgMW3ovHy26d9g2p1Osxbe/mU5sgkSgoRq9esRrQlps8W5syYcgPUKbTZlCEhWWmVTD1aCimmnbJ56hT0Q0OIXn21CFexUu9/j59Bu8OBmvoGrCorxrXFsXJiVn31B0N48sgprDp5FJuvuQK6KY7zZSUZYdy0MaPn0wMhwWQ69XOyg8VIFjKsRuImWUjCSSW5z9fQq5oJgmHImQTz8MMP45//+Z/zotE0X3DJhMtkgp8WVqoxAVwYyuqxqST4lc1n3KAYDmBILpdDmHisb522o2UgJhOTTFEgHvZpacGQ3Y7L77g8rvacDiabGdqiUtRvaBI3rX3nOTiMIzi+b7fYLBf7D8JauxikzynRAvsrDj0D+5FXoV/3btQu34xsYMHl05O2Ya6AGxS/IwUcVZ4w/OGoyE+Fut3QrxorZZMOfA85a17qgsmeJkKGJrlZKg2fsN0O77btKLnqeviMRkEw3z/WjFanC7X1DdhYVozNRab4WrZrdXj8XA9Whny46oZroJ2icnfw3DnR7Gl98AFBXBOBmz+/C72PiZqXpwqGG5WD7eg1SfVjKbkvSYfGHA0EXj/+O9cEQ9KjBhlJ5t///d8LBJPvJJPtcBk3YCYUWT3FhF+6fIi0hrhA5bjUqVjUjEuTYFgCyxsvlwucMjWU6e93BYRMTJExOZnxnFHs0DHsxsIF8zMimMTqMm1IjeKoBdXXzsdiLWDv7YBm59PYH2lEqO+tMUnvjBK2kRCGd/4Cg2ePwLblE6icO/m8QS7A8mRuULICUEjSaKJCIJMSOiqzFuqiqRkNibpg3CxZ2ksPhyXk8c2ypAShl1+BftVK2CrKMRwM439OnkGLx4f6unpcV1GM9bbznrE9EMLvB0awzDOC6+68BZopKndH3G54Xnsdpqu3QJNBaJfHTm+dYVbmVWYCvB5cY3wow2pSbomeC+/hmagi6+rqEgRz00034Zvf/GaBYC61cBktHuYdeCMoE/ypIEmFhESph6nkT5h7YQye3hIt11zGZWnZvnC0D95gGA+srYVZn3xjl8rOPHfLlq/AWfvo3JdMSYYWPKc+trugqTJBZdCIAZHVqiGoFqxCyao74pZl4shkPpJ5cdGAG463/1dYweU3fg6l1VMb25ttsLqOBMPrr9SQEwKZPAedLmjnZZYPmAh8D5IKHwwzyc2Sa6jj8SdgjkZgXb8OYZ8Xv2sdQCAYFBv5bZUlWGE5Hw51+AN47J39WOx14YoSy5QJRkwRfe01EWLTL+FczfTgsZJgWJ05m9IoUnKfhQY0EBgS57nltdy6deuYSZbZnLrJ60SCoe7Y9773vbwYV5CPuGjDZTLBz+cywZ+uMkUm+PmgvAstIrr/PBZpdU4k/sfXMi5LrylZriPbILFQJkanUeH+NbUpq7aYh+J5kJpRx3vcUKkyJxk5/SvqDorJjxxEJqEaOIFo5bIxliU35sSkN8laCnkyfBFxD8D+xncxGNCh7pY/hq3k/PCz2QTzCvRAuVmNm0JKgUyxEQOayimWQWe4Wdb5/XDZbPBeew16Rlx4o9sh/m6zWnGtQYVlo1WEhNPnx6+27cECrwvXLJiDqDs2nXQq8B84iIjLBcutt074XKF4cOSI8BSyMVAvG6BxQ9JjLowhThlVkLkwevJcozI8OZ0cKd+TZcpUJv/hD39YIJgLiWQmAkmGlnkm1iiT19zw01kYiQl+uQCV5ZTKsl5ulvypfE9ZEi0HenGR5xIjvhCeOtQjhCxvXlYBbQryY+xfNprKvhwOL0sU6EwHVpCxVybY6oK61AC1ZdQr8TuhcnYhvPTutElv5qZk0pvx83KNG1V9b8Nhno8Ft34YpkmOrM0V5OwUkmTSqac8Dyqcr6rLEcJDQ/Bu3QbbzTfDWluDJw4chcGgR0lxCTaHPfA2n8Qbx4/Gcg9WG14+1Yp5iOCWm69DsLkZoeHhKX1uqKcH/n37YLn3HqgmKLemBc97ght5ro2pyQ5Bkzk0aRBKT5HXVc4ZUk6yVOrTZeqd0vsmwTBUx/HJ2fSOLkZccGeHF5RhhVSgBU1rJl2CP5VEjNJTSSynZFkv35uhNFpw/L1UPCbBkGiSDfTKNqgHRoJhWfA1i8tFOXKqWDH7NagFp4yV87tOdo9U6VQID/pgWHs+2a3uP4FoyRxAn74cl9YiSY4PX9teDO54Fr22VXCY5sG+d29cOXqicbW5hDxX6axyriNtoxXa2tyRImfBeF5+GfqVKxCpqcb/7DuMDpUOdbU1gthubJiLaLRJGFHn2s7ht9v3ojjgRcXyxTjX3Y3SYBAaxTykTBHx+US5snHDBmgnIA2eK4ZEJyvomkvIJH8iwSRCzhnig6+RCtKyglAZVktVqMOwMDv5+R6PPfZYTgt6LhZccCSTrmEy0wT/ZBssE8t6SXIkHH4e/82FxjLLVJpr2UKP0ydCZKvqirBxXvLSVxm2Ywf62rVrxQ2TWCgw6XyCTg2NXgN18XkLVygu18X6NiZENIrg6Tdg3/csRubegjUbbxG/Tpy+KK3KbMfO04HniaGUZOcqEbr5mRVLTBXebdug0huAVavwP3sPoVNnQk1VFa4rtWK/KzbnSJQqhyPY3taNlY31uPGy1fHzeI7Kxz09sDQ1ZWydCz20t96CuqwU+tWr0j6XMjBMrJNgJjpXM00wrD5LRzCJ4D2rnGQpw2q8d2QBhoxqSB0/Gpr33XefMIp++9vfZk1z8GLHRZGTUSb46U1MNGlOyKKMVqhNtoJM1u8zZMcNiqEVLkIuUDZjSXlzLsTJNN1NhDa7RyT5qUG2piH59+N5oEXOTYejCpKF7ThkUTNJktGUGwXBxM+TZwDw2hEtXzzxiyNhBA7/Ad3HdyLQ9ACWrjmvnaWUiZc3ufQUZac3H7nwDvmZJBfqkLFnaranEwr5ltY2aO64Hf974Ci6DRbUVlXhAzWloOmyzekRx+zuH8Bj7+xDVWUF7ty0Dhq1WqwzFpl4i0tgf+MNDPn9cetcWfGXjLgDx48j3NsLK+fSpFkXVKzg+mYOYqIRGLNBMCS+qXrCUp+ODxZgyBAv9xN6bV/+8pfF+/OckpR+//vfT1l9IBPQW/rJT34icqmf+tSnRI/dRGAI8wc/+IHIgzI0SMXnTCtILzmSmWwJs6yc4gabaYKfzxVhoymWKHNjokWnDEXJQWJcnPRyuIGRfGThwHTEE0/1uvDaqQHcsKQCS6qtaZOe/Mm8UKrzEBseNrnP180dS1YiVMbpl9oJNv+gF769v0RH21no1n8AixYnnzyYeJNLT1GGZnizyMKBbBA3zwHJmLF1nqtsGgNTzsO8/TZw5ZX44amz6LMUo76qEu+vLkGRViMkZDhGe6izG7/ffQAVcxpw1/qVgmCU0BoNsJmMqF+5Ml4eLY0fNoQmSrSwD8e3453YXJo0SXAaU1R/WL8+Nv7hYiKYCUO8Pp+QiGEHPw0hqg985CMfET0xDJtle+386le/wuc//3ncf//9gmhuu+22CUmGxELjmgbALbfcgl//+tf43//9XzFmIB+I5oIuYWZsmsk+LvzJJvinQjB8D96w3Px4QXnTKsEFz2Y0PkiEtIS4WVIUk+65JJyJuryVYAf/jhY7bl9RhbllyfMBvBFItLT46cGkCzWFp+DJjIEcTjb/2vTP8w7Bs/MnaOt3ofjKj6FuTuYyI7xxGX7kg0aETNbSy5HJWp7HTKVFlOD1Z3iORJaOjGcKIg/zyisIzZ+Pnw84MFBUhoaKcuHBUOaf0DNENjyMX586heoFC3DP6qVJryEbJykHk1geTcs2UaLFRgNo/wEUr1wBbZr+FlkxSYKZbW9vJggm2T3/hz/8QRg/zz33nCgYeOaZZ/Bv//ZvuP7667NOMps2bRLrnN+NUzQzwVe/+lVRQs7j4h740Y9+VBTd/Pd//ze+9KUvYbZxwYbLZMc0NyIpJT6VBH+mIGkwjCM3p4kWFzd6Xng+uLElDsGSGyVj28mOh8e6q9WBw11O3LumRsjmJwPjxCQYOXVwou8mBTKnCpWrGwj5EC1NLdeicnbCuf1HaPVaUHvdx1FRWTXlzyNxymFivAY8j7z23GSktAjPYyaqx3y9mBwaCgkyzoeYunf7DniCITymMcBeUo7GchJMCQyK78KqMX1zCyqXLsY9yxamNBLEELIUjcrKXhJ63H3PPoeRSBhng0Hotm6Nh9WUpfpyamuq0OvFTjA0cN773vcKY/all14SxiE9Bj7o2eQCC0ZVqyfTcP7ss8/iM5/5TNzI5rWip/X0008XSGYqg8u4qHiTcLOg9zJRE5jSg5lqeIyeAuOxJA4usMlWlMjNkA+G2BjG4EYpGxeVpdHSC9nXPiy0yBZWmtE/EoAvGIHFoIHVoIVRG/sesttaKt5m8t0iEYYJp84yqv7jsVyMOoV90n8CQ+/8AmdVc7Hghoezav3y3MhkbTLVY5mo5c/EayT7pvgeyXTrZgOB5mYMHz2C3yxaBkdFNeaVl+G9VSXQK66P/9Bh+PbsxoM33oyyxgZo013jUU8mFhJN87z2dtgcQ6j70IewxGweU6rPzY3nT0q3kGCmM7U1m+A15jXktZ1Mkn8q4Hqh0CXv01deeSVvwoSJYE8ae5YSFa/5f5JMPmD277RJgN4Icx0y/5JJgn+6Ev30FEgw9DhIENNd2IlhDFpJXMi0GpWl0TXWYmyaVwKXP4xWuwcuX1gM/fKHItCqVQh6XRix92HxvAaYosVwdY2IkceJRJQ08T9VkolGoB44hfCSO5L8LQq078TgvqfQZlmLpqvuGjN5M9tILDGX8+UZ2uFmyfMr8zhcB7R+aeHlWmZkMjL6Ay+8gMcbF8LZMA8LykvxnqriuJfCY/bv2QP/0WOw3HUXiqsm9gaFzhgvMMPJKUg0MjIiqsk4HVM96p3I0l2pwM18I38SNISkgZTL6zkRSH7SSKAHk8vuepLZH/3RH4lc1GuvvTYuLJ5P8Hq94mdiZIVrXf5ttnHBkIxM8Mt8zETuu/RepkMw3Py58Uvp8mxLxCjnoitLo1kq6nQeFyS6gHmcxqp4dzKHfx050Yyz7f3YeNlSqPVmQUQD7gQi0qgE2VhHSceij/10eIMZT6ccd7yOc0IGM1qcoLIbCQPNL6Ln6DZ0V1yNFZtumNGRuonz5Xlz8TzSwuMmKRtE6fHlg/w6vY2eJ57AH8qqMbx0OZaUleLhyqJ4zxPlXVjOHGprg/XeezKWiRHhstH3TyZsGQ2H4X71VegWLoQ+YZiYskqJnjuNOG7kMh/GXORUmxezQTA0EmaCYPhZn/jEJ8S6ef311/OmFygVZEERvU4lWGGaLzm0CyJcJift0TqltcWLT7JJlcuY7gwYOTWT3gUtX1rEMwFlwlvKw3Oz5A0uZ2mwwsXjduP2azamzAtR08ztD2FklHT4b0lEnkA4pUrzRIgNJ1tKGYDzv2R+5sgTaG85haGG27Fq/eZZD0WRkGkUMMQh1w2xa9cuQX7KBtDZIJ2Op5/B02E1HJdtxPLyMtxfcb7ykETg4foeGID13nvj3kZGkJtvini+b89exoFguuKKpGueG6vU+ZNGjVI9WuYVE8ujU0ntX2gEwz3j05/+tPi8N954I2/kctKBhSs0UOm9KyElf/IBeUkyStAaZd6B3oRylCsXX2LcPRsJftlrwhuK8ejZKgFUysPTfaeFyVAh/80NgBVuqSqs9Bo19GY9SrMZ3QgHoRpsRmTFI+d/5xtG+MBjONs1CP/Ce7Bq1dq8CEUp5ed5A7IUleDaSJTZn4mNUom2t9/Gs519GL7+RqypLMedZedn2rPSzP3iS4j6/TGCmaS2FufNQKsR75OIYEcHAkeOwHr/fXGPR4L3i5RF4ppPpumVmA+TfU00gKQm2HQnWM4mwfA7fe5zn8P27duFETtTitJTwRNPPCGOkdVjxLvf/W5RsvwXf/EX4how3PnCCy/gu9/9LvIBeUsyXPj0JPhITPAna8jMRoKfG7isPmKCf7bLW5XHxSofWuTMC8lELW9AnguZe5hIxHM6UA21ADoLotaYdaca6UZg369wagjQr3wQyxefVyyebUhtrcRR1zxXygbQxJkkMh82nXHJqSCUGA4dwvOHTsB5xRasr63CrWW2MdIu7uefF2Eu6913TagflgoqrS5exhx/b69XqCsbGQJLyC9I3T2SBgkmkzXPNZa2PNpmi59neuBTWRczTTBf+MIXRP6Fm7c0SmYDhw8fxte//vW4ofyd73xHEAbLpT/4wQ+K3/G8sJ9GkgzJ5a233hKqFbyGVJ5mn8373/9+5ANU0VxroUwBDBXRe+Fmyvr8RG+Ci0HZpZ2NBD/F85jzkTMoZjvkI8Gbn8fFzVIpPU/ICita5nxIaRZZGp3NG1Nz/ElEzRWIzL1KeDS+/b/FUU85KtbchsYpjNrNFWRnOquPMo2ni0760Y2S55FFBFxbkrynm/AW/VUnTuCF7XvgXtyEjauW4cbS8xVbVD52P/cc1MXFMN94Y0aDwlLB+fNfwHzjDdCOkqv4biQvnQ7mm24at34YVmGFEu+nbOTRWJXFkBvPJb1JRhuSlUdnkuTnc7lx5ppg/vIv/1J08TNERs93NtHZ2YmXX3553O9Z3MI8GcFzQ2/lXe96V/zvvM7btm0Txiify+uZL8hLktm9e7e46UkwyRb+m2++KYiAG2k2EvzcqOnB0EVm01W+WOSy8EAZ8kkFKc0iCYeWubI0elpCfiEftLu+g9DaD0A9dBauI8/jSGge5l5+S97EraXny5uMjbLTKTlVjktmeI2GhyTvySo3yFzHkV17cbp6Dq7cuB7XKjwYVpm5n30O2vo6mK65Jhbymgacj/0apiuvhK4xVpzhP3gI/iNHYHvoQagU95JsSqVxxQ0pFz1DNPyk180H/y/nDKVakzNNMF/5ylfwi1/8QhAMN+cCLhGSYaKfnkSqBUbG5sbLxSrDZkI4cArkIBV4ucCSSrzPErhZSrHPyRYe8JLKmS58kLBlzJzvNVmLVdV7GJqufYgU1WPo1A4cVS/Hso035I2GFb8vLTvm77hhZrOvg6FK2QDKn5MJT8pQlPvECVQ5nMB992F+xflwVai/H+7nnod+6RIYN23KinEz8rvHYVy/HroF82Pv/+RTsNx1J7SKcDPvGUYKaIjwfM2EkjCvkSwz54Prk4aAsjx6JgmGx/OP//iP+P73vy9CZBy8VkBukB8xoQRwwaUbTMbFx5t/OgTDRcZNnDpkmSjwztaGOZUyxMSZLnKOBnMVfG+pBcZHJqEgyshEPQPo7e5Es/FyrN54dd406ElxVHpxzKNNZxBVMnADVio3yPCkcsaQtMyVIVY+lxu5b2AAi4eGUHTjTdApCaarG+4XX4Bh3ToY167N2vEyLCYaMgMBIVdjWL9+HMHIvONMEUyqMnMZVpPl0TwmelT0RHNNMJSFYWKcofcCwVyCngxvhFSyCjxcaoHxJ0NIqWRZJnp/hqFoWXFBz7ZAovK4pK4WjysXzW9SC4wbpQwFScJJmqQNuKHZ+W20OVXoLNmANZdtypuCCLlhMg/A8zWTvTlcf/S45bkkkdNblBI39I5Dfj+WdnTCwFDYVVfFXxtsbYXn1ddgvPIKGJYty+pxuZ59Drr58xDu7kHE64Hlzjvj15Tni54Cj53nK1/yjgxPUoOQ97wMe0vyznZukd/9v/7rv/Cv//qvQiqGifICcosLimRkgp+uNhNkvLn5/8kku7nJss6fC5lVK/mgX6WUPZGhgpmwMOkNShFPhoJ4LngumWeRzXYBjxMt236PkeKlWLtu5izfTCVG5HWc7eMiycjwJL0qrsN5djuKAgGUPfww1KPHFzh1Ct633ob5hhtESCvbYAl0xO1CZMQl8jDqUQNKhqJ4Tbm+8oVgEkNksrFQhtV4v2Zr7AP3D3ovf/d3fycqtjZv3pzV71LABU4yyRL80ppkaIk3NzfqZDpgEvRcSDBy3n2+9HTQc+GNxo2drvtsHJeyh4Q3N88vb26GhxjimEjleiYhVacZGsun4+L6Y3mpqKjy++F+/Q30rF4F/egmWdbfD+2Jk7Dcdit09fU5OQaWKnM2jeWOO+LJf0nIPE+5znVMds3xfHGtJQuRKav+aAyRvKdaHs334ryVv/7rvxaCklu2bMnRtyrggiAZEglvjGQd/KnyLzLZLQmHMV/Z98AFSeuIoSgKx+WLvAjBTZzEx6bLidSkZ/L801Nk/4g835K8Z3JqZTJw0+HGRALMhpZcNomPx8WNb3ljI9y//73QB9PMnSs2SPubb8JHFe/LLkPZ4sXxQWLZ3vB9bDINBMQoZYL3EUNR9ABY1p1PBEPiIzLNwSjLo/lTet4TFWFwb/jZz36GP/uzPxOikdddd13Wv08BFyjJJM6AmUyCX1ZXkXT4b4JzXliVli8hMibiWX3E/pd8qmxj6IxJaxIy5VmkiCdvbiV556JpMR3kWAOWmvM65gMhy1CZlK9ZtnQp3E89BW1VFUxbtsR0yLZuRejcOZjuuAMjFKoezeNwjcuSXj6yHfLjpkyCoceXa9XiXBNMsveQY6f54B6RrDyae8hjjz0muvk5F+amm27K+vcp4AIlGd4gssGSm8lUJWJkpRarg7hJ8cHSSeYduCBnI4nNU061YA6EmkzT4EyAXdskPuXUTyVI2MqmRZ5LWTiQy3PJDUXOD0qUNZ9N8HyQYLieaCz4tu9AqLsb1vvupVUkwlfhQTusd94xRodMWWauLOmVnvd0q+SYyyDB0LPKF+XpbBFMuiIMPujtsqFRVlj+wz/8A37zm9/gjjuSqIdnEdQ7/MY3viGqVrlGSWxLlixJ+xqWT//yl78UBiev/UMPPYTbb78dFxPykmTYI8KqJ1rJ05GIoTVOsmIcWt60DGtID4cxXuYbuEHwAme7/DUV8UkhQt5k+TIMSikKminxyXPJB8OR/C6ScLJZsSebUtnLRG80XyCFW+mFMtQZYtXY66/D+uCDUJvNcR0yyx23T6hDJufK88EQqhRETVn1l0G1FnN8+ZR7zAXBpDqX7H9hDoYbPqtQ3/e+9+Gee+4R4p+5OB9yWi5zPezEp9fESZq7d+9OqSLAMckcivblL39ZVLlxv2Jz6De/+U18/OMfx8WCvCQZavTwIt1666247777xNzqyZTzcpEpE8OpcgiynJeEI29sSTi5KGtmMYNsguOCzJdSYC4B5l9oTfG4piIKSjJXlkbz3EurfCpjkiWYG6I3OpNq2JmApMo1Jj0rzmkZefxxESLT1tfD/cILQh7Gcuutk9YhS6z6k9IsUjk63SbJtU+CkcUt+RJSJMEw90gji0oeuc4NcYPnPsIRxlyLTz31lEj4U0Lmz//8z7P+eX/8x38sPpOl6/xuvKf4PdeuXYsf/ehHSV/z4IMPCoOAxyVB0uF9+Oqrr+JiQV6SDBciLYDf/e53QlOIJ/3mm28WhHPbbbeltf5583MxMzxGVzVTq4U3tiQc3tgT9o9McbqmnOo32yW3ifpVtMp5U2SjN0c5JpmbJUk+001SCRlSZIlyPg2OkgrPMpdGeX7XU09BU14uuu0pE6MuKREaYtPRIUuVe1AqRysNKBIMe8jkgL18JJiZ6M9hqIybNb0ZKhQr73EaeLloJCahc29iuEyCXsn3v/99YSglA//OUBnPDe87HhvLqimGqXyfCx15STJKcGHyIjz++ONC4pobD5N39957r4ixKocn0RqnQCJvflZrTWeTlFY5N0l6HJJwpmKVM3dBqzdb0zWzBak6zU2AN38ukvi8fsrSaC43STipqquEoGRzs8gPTdWzyhX4PVilqFR49u7YgVBnJ8zXXw/3Cy9C21AP09VXT1uHLJU+nVybNFxkDwk3Th4X/82w4qVKMNQ1fPjhh4UHQxXimToPJIm///u/x5/+6Z/Gf0f5/Y997GPCy0+l08aS6h/+8Idiz+Kaf/TRR0WjaL4UJ10SJKMED5VyHvRwSDgMo5D1GWul58MkGoXushlW4U0iQxe8ublY+P7KhsVMKrVYpZVPpdOy14QEOlOlrVJeX+ZxSHKys5sPbkByng+JiZ5VvqgxKIsiGIKVa0x077/2mihX9m7dBn3TUhg3bpyR6ywnqdL7piHDMmUaV7kK9+Y7wVDiniEoegEf/ehHZ/Re43ej9P6nPvWp+O/opbz3ve8VRR3Jrgc9rkceeUQMSqMkEg0+HvuPf/xjYURfLLigSCZZHoHlif/5n/8ZH7hE6+Xuu+8WN1q2FxlvFhkGkg2LknCShYHoJjPJnzjXZLbBRU+Cmc1ek1QingwZ8DxTVytfclbKEQIM3UmdO5mH0S1YgODp00InLJs6ZJmA55A5GK5BejNSOZp5COkxTicndqEQzM6dO0U4naKX/+f//J8Z/74818zLfOlLX4r/jqTzZ3/2ZyKMmex4mGdkoYByuBhnw3BWDIufLhbkh7bEFMCLxuTmiy++KMIDJBp6MbQePv/5z4vZC7QG6OWwFDcbi46bsbS6lWKJDFPIMBBvdh4XcwncmHiD5VM+QSasZ7v5M1HEk2EgnkdZus5/yxDlTFT9ZZIbomclRwgwD+NmclatRrD5NExXXQl9U9OMHhe9FxIMq6fk1FjmiJQ5MVa/cd3KIgyuxVwbFVJTbqYIhufggQcewFe/+tVZIRiCCX6eayX4/9WrV6c8Hl6fxHJ8/p+/536SL1GPS9aTIX7+85+LygzGNOVGxK/DzZ05HBYN7NixQ3g4JBw+eENm++Ipw0AMXXCj5GdwNg1LbvOly1qWAvO4ppOzyjbovZD4GIempyCLMHi8supPWRo9UzcfrytHXlOpO3F4nvedd+A/cBDQqGGhwnIOdMjSgaTMTUwqWKSCNIZk4YAyRDntOUNJoFR55jnLNcHws+6880588YtfFF7DbG3MnEnD/AvHN5NwGMpnufT/+3//TxAf8eSTTwrvhsKcJHp6Xmwb4FRLGlts6mVFLfcLGswXCy5okiHSMT7/xvp1kg1zOG+//bbYxHhxSTi0/rK5KHkDM0RAkqHFSGtyIj21mQ735FspsCy5TaXbJglHSokw7yAJJ5Oc2HRHLpDouFkqK5KCbW1wP/9CbNpkDnXIJvJGuX6Z65vKTBcZoqTXLb2c6YYnZ5pgmJ9l4yKbHtlrMtuWP4/jf/7nf0R4nHlF9sv87//+b3xNs//ls5/9rFjTPDe8J1mkwPuyqalJhP/pibJxlIbgxYILnmQyBb8mby7239DLYZEA8xGScKZbkaPszZGJ9In01GZqWBStcRm6m87EyFzmEzI5/9zElDkxGQbiI9PRvpmeMyb46QEwN6QM18k8DKCC5c47oK2sxEyCx8R1lg1vlOtRnkvZTCvzOJP1GGeaYJjrJMGwafFrX/varBOMMg/LhmaSf+I0246ODnHcN3LEtuJ4GYqlMcz7IF/0C7OJS4ZklOBX5s1K95WE88orr4iLy/zN/fffP+kuaampNdFmSctREg43WDl/hI9clQ/LSi0STL4MGlNa41OtulPmxPiQ/SOZjnxI975y7j03S6V1L/IwTz+DCKuF7rwDmhmeDJrYn5NNJIpP0mNU9jaluz4zTTC0/Ekw7OJnOCpfWgIKSI5LkmSSxbepzkrCYSEB8yj0cPhgeC3dIuZNySQ1SWoy+R7l/BGpp5ZNDTA5YpelyvmkLqA8Z9nKDSXrH1GWRmfqMSqHoHGzTCT+sN0Oz5tvwnLTTWN0yGYC3Ph5PRlWSaYpl8uxD4SyAVRJ4EqC4TrLtXdOq59Nj7JUuUAw+Y8CySSAMWvKQ5Bwnn/+ebFZSQ+HBQTKRc0wFBN3zCXQi5kqEjXA5HhkvudUKqvkADRuBvkw0CtZrwnPGVUZsg05g0SeT3okmeQduEkyn8bXz9TQuEzBjZ4EMxul8LKoRRI4izRkyFdOAGWOYSYIhmW9TIwz0c/8RoFgLgwUSGYCb4MT9Fg08Mwzz4iYNQmHD1aT0KqiyF028xwkCLlB0pqcrJ4aj5kEw2PNJ/VduUlQsFDZa5JryLyDnFgpCZyEI8+nJGVukjy2fKkGJGSJPK/ldAyZbEA5REx64GIC6Lx5wmDIxbhwCeYsqGHIfMb3vve9vFrXBaRHgWQm4W2wQ5eVHyQWhgkYTvvIRz6Cq666KidWnFJPTSk6KRvvEkNzMjckddvyJYGoLAWmxcuqsNmAJHCeU6lPR2uc/+f5zKeZKwQ1+1hBpVQYyAcwb8UQGe8JelZcm3zwfMo8Do2cbK0/ngfmYKjrxXaFfDICCpgYBZKZZFyclWjcNP/kT/5EkA6r1fh/uvAMqV177bU5SeIznMPPJ+HIxKz0cGidy3krsqw1nwiGFTXcyBNLgWcTPJ+0julZcdNUlkZPlOieCfDYeN7ybd6QJBiZt5LGFQ0iWfnHnyQC6TFOp/KP70eNQnqYnG45m1NZC5gaCiQzCfzVX/2V2JR+8pOfxHMl3KzYf/Pb3/5WEA7DMyQckhFd+1wk3GUpryQcboi8yVmllU8lkLJSi3kubkiz3bmfbIwzN3AWIMhKNSkXpBTxnGnvRo43mMmw4nQIJtnzlOeT61XZAJopUXCN817i9aF8VD7lyQrIHAWSmQS4kdNCS7Xp8GZixy8FPEk4TJiyEoZhNY4qyHbMml4C80J80FpkuEzqqWW7d2SyIPkqq47ySVWWpEeCSTbGmRukVG9QdsjPVDOtbJpl8UE+yRFlSjDpplbynDJnKEv3STr0IJOBJEUNQpZq837Kp/VTwORQIJkc3pS7du2KEw7jykxc0sOZaCbOZDrS6c1IOXy5QcpeHKWsPi3imSIcmUjnhkxrPJ9CHJnKsSg75GXhAL1U5QaZ7Y2PhRHMXeVb0yzXlSyHZ3PqdDwKWbpP0pGFGMoGUIKkxOIarlmqdeS6/F563Fy3DE9mel15nAxpMjw920UZ+YwCycwAuIi56cqZOCx7TjUTJxPQY1I2DCYLQyXK6tOjUFrkuUqeciPiJs4NgwnrfEqkM2/FMmV6L4nd2JlAWRpN8pG9TdwkpxsKVIpwzlZhRK4JJtU0VT6Yg+LUyk2bNsWLV1jRmesQK6Vc6DGRMBhp4DVmcc8111yT9pwwdP5f//VfoqycXhfDevx/AeNRIJkZBjd/EoScicNFfsMNNwjC4UKlpZyOcKQ+muznyMTqkiELuUHKZkW5QWbL05B5jnybzKhsAM1WM6PsbeL7Kkd3ywFikwFlSOjFJIpwXswEkwh6iWwLoEQMPRzeB/RmGGrOVW6TYO8bK+So/kGDiPpjzP/wmqRqGfibv/kbMRSN0lQ0pAhqllHipoDxKJDMLEKGvOjh8EHyufrqq8WNReuKG5Zyo87GoDHlHBeG1WQISG6QU91IuDHw2BhDz6fiA2UpcK56TaQki6ysynSSqrK0m5v4dEOo2SYYkjLXR64JhuDncIAX1zg9GJIbw8x88P9s3s02+BkM527btg1XXnllfK1Q8YNEQ/HKRNBYIymRDDlSpICJUSCZPIHccGRIjR4BZ+KQcGjRseKIA5Go1UQPJlthKOV0RdkdLwknVVI2laYWyWUyqsAzWak1U6XAylkuJB5ZyitLo+V14/VmpSLDRNzE86W0ezYIhioC7373u4VHSBl8ZbhQql7lwmhhleiHPvQhQWzKtc5QKnXROAAtETw+qg4wF8PwGkmJ6z6fijTyDfmTkb3EwZuIuQJOxvvzP/9zET6RM3E4J4NgM9pE4bTJgiEBJsD54KZCspE9GrzZZS9OqnAFn08vYSY0tSYL5r4Y9mAineQ5E2DokeeMD27WUgNMOdiODxIRQ20M1+TDqOTZIhh6gR/4wAfEOaJQbWI+KpceMUmNRJFoTNEY4d9SySIR3/rWt4QxyOtMmSSGyv7jP/4jrzz4fEGBZPIQXKj0CP70T/9UWFX0ahg+44ZO956ejBzCls2ZOEyysuqKD6WeGvNGUo6FD1mKzTAP/8a4NDfOfIEs7ZZ5jtlKpNNr4YbFhxTx5DVkWJTFG8xdMfzCvFo+9IAoCWYyZcpTBfOLH/7wh4Ux8Nprr824N8DzTi8qcSYVK+BS5TrlOeF1ZLEGjYo9e/YI1Q85/r2AsSiQTB6DE/PozjM+TJLhzcBNX87EYVyY1S0kG4bVsiklQ8+FBMeHUk+NIR5a3rwJuWnOpJeQCXiO2GdCi5M3fb6EoXhdSHYM33GjIjGTYLhR0RPMpHdkJgiGGyw9mFz3pTCsSOufoUxOgZwNVQMaciR7hryk8CjPA/+fKuwrxyWTHGXBDNcZDb+tW7cWSCYJCjmZPAZvAN6EJJJkmylDMcqZOAy3kXAob8PqrlyUD5NwaInzswl6Nen01GYSPCdUBWYoihtlLgUbJwtuXgyrkJh5bMrwY+LYB5KR7B2Zie8w0wTDdc2RxOwjI8HMtLK0BM811y3DXJ/4xCfE71599VXRXsCiAFk59s477wgS5P1F74vHy3yNrCYjYTKiQIOQhl8BFwjJ8MLRjWayNJ+kNfIRMhQjZ+IwOckqL+nhTDQTZ7IbJftv5LwVWVXFn/y/JJx0VVW5AI+NHgE3jsRNfLahHITGY0vnqTB8I7vjpeikDFPmgsTlsbEAZCYIhp/3f//v/xVeOkuAszFPaDr4h3/4B/zzP/+zePD8sriG+oPUSVN6L7yPSEbED37wA3zxi18UhMLjZwEBvwtbC7I9TO5iQF6SDMNBH/vYx0RlDhNwrK766U9/mlfaV/kMNgo+++yzgnA4qoBWmCScxJk4UxmCRoJJ3CgTRyPLqioSTq4FJ/nZylzCbISbcuEl0GpWkrhSxHOyDbz5QjBf+MIXxGBAbsoy9DTb+PnPfy7uFXrpVOWgl6XMR73rXe/Cli1b8JnPfCb+O95fJCIaDow0/PEf/3HeFb7kC/KOZJiwZaiHlgUbo1jpxBp2bpL/+Z//OduHd8GBG4icicMbgx4GSZvnk9VqmfTayAZQIpOBXsqqKj6k4CQJJ9t6alIjjUQzE4OzpkLM9E6STdqc7HspS6One05ng2DY0U8DkiEylv0WcGkg70jmb//2b0X3LMlFWmr/9m//JlxT3mT5tIlcaKClz/EEJJynnnpKWMYsKGAOh9UxyTr/uUGyum2qDaBKwUlW5GRTT43kp5wAmk8aaXIsMY8x25VaynPKh1Q5zlQyaDYIhvf1r371K+HBLF26NKefV0B+Ie9I5q677hI3CRPaElQ25ibIGyMXnb+XIhga4A1PeRt5ruVMHOo2ceNhjmPnzp3YuHGjCAlM1wNRlvFyc+QGLAlnsnpqPH6SH4lyquoHuQI3fXp+3FzpXeWS/JJJBsnxyMkUHGaaYHh8zHswj8Ey5cL9e+kh70iGXe4Ml3ECngQrrNjsRzebSbkCsguGnJiI5UwcEg43KgoVsiTz0UcfFWHKbOdUpMKxJJzJ6KnxuXv37s3LEdM8l/SueL4YWpxJ70qOR5aEIxUclKrRNBx43pmbmwmC+dd//Vd885vfFARDY6CASw95RzLU7mKNOpNx6TSGCsidFU7xPyZoZQKdStHM4eRiJo5yc5SEw38rrXHlZsgEOgkmH0U4leE7Esxse1cMj8pKNYbXJOHx2HI9SoDXlKrEJBmGaOk1FXBpIu9I5j3veY/YbFivLsFqFM5gYQf3bJc8XuxgGTR1pP793/8dH/nIR0S4TM7E4XVh9Q2r1Hg9ctXomCipL61xlvOyUosy8NlsPM0WwTB8R0LMt/Adb3EaaiQaXjMppyIr1egRZvNc8vO+853v4O///u/FvUuvuIBLF3lHMt///vfx2c9+VmxoUg6EdfXPPfec6DYvILdgyIwb5AMPPDAuls9NVAp4coIjJdhJOPR0ctUXQ2ucZMNCEIZ/pBIBq6rypRdG5odkcUQ+he/kaAmStezRYUhPWRrNvI1SxHM615Gfx/zLX//1X4t7lrnUAi5t5B3JcFOhO8+QGQcD0XKlhhf7ZGhhF5A/GxcJiYRDGRc5E4eFG/Q8skk4tLyZSCe50FPg5sjf0QKXAp6z1d3P6juG7+gh5Ft+iNdJ2aCarH+IxoMU6+R5JaZa/cfPY+8IBV3pEV933XVZ/T4FXJjIO5IhqDv11a9+VQjPUdOJ8g0PPfRQzj6PISG690xOfupTnxL1/AVkBi4fKjbLIWzc1FidRg+HhJM4E2eyoKXNUA/DY8puajlVkR6v7IyXhDNTemVyCijJjlVTFxrBJHuNsjSaIUBlaXS6Iga+liXKbEpk8Qi93AIKyFuSmUmwUZGT7j75yU+KWS0ME/FnAVOfiSMJh0lw2UjLBlBqPk2GcKRiMcun0+lbyc54Pp9WOZUhcpVvUBIMjSB6bTy+fMoPTYVgkr0HQ2yScFhwkaoYg+A1Z6c8vdvbb78dMwFecxaqTHYQHdcIPWEOJyuoiOQelzzJcJHKJC3DHbS+CyQzfXCTovacnIlDkUEmgKXaAAs40m3McqbNZMcIyKFhJBylnlq2pFhkSJcEk48VbpJg2I/EMuVsSeywGEOG1KSIJ68pB3hRlPSjH/2o8GR4fXMNFgAxdE4vkt4j2xv42fR2JwLJheF4vgf7xAohvdwjf/z7WUI+VQFdTODGS20qjqh9++23haQ9e26ef/55QRy8uVnBxqFiic40n0uCYdn6ZOfUyKFhTMCzp4obD0Nr9Kp4HHxfhtem6sDToifB8LguFYIhGI7k9WRjLnW8GMam90JP6b3vfa8gF0rFzERghGOReTy8jnzQYGETMQ3GifBHf/RHojKygJnDJU8yBeQe3IiZT2GVIK1HVqaxPJrNtZRcYQXSv/zLv4im2y9/+cv49Kc/LX4/XfVtKdJJD5WEw7AWE93M8bD5lIrS9Hr4u0yteRIMSYzSKPlGMHKUQLYJJhGsoiOh8Frx35TJZ/iQn8vzwn62XIHeC0cE/N3f/Z0IdfF7UlGA3/3NN99M+1o2hfJ6c/psATOH/BF7KuCSADdm9rkwB8bNiZYoe3BoFXPjIDhfncO9WBadrWS6ckolwysywU3LnxawFJukhZ7Mu2X5NKvIGMfnBpuPBMPvlGuCkaCBQA+GBTOcBsnzQRJmXwxnq+QKu3fvFtdnw4YN8d+x6IKl1zQAWOWYDNSRY98Oi3yy6W1x7TLvl1jowM9gQ3lTU9OYY70UUfBkCpg1cGOit8Ipg9y4ST60SrlZskKNoRgKK0odsGyBhEMy4QZAhQnqizF3w1AarWF6OszpyPALE+DcwOiN5TPBTDXJP1kw7CjlhiTBEAxhsXAml0PImGdLpjpN44F/SwaSH+X6v/GNb6SceDlVcI2wT4wkpsS3vvUt0e9XPcmihIsRBU+mgFnHd7/7XVHlxzCLnDHC5LKciUOVAYa9GPdn7J2babY8HG6QtIL5WLx4cbyiio2/rGxjgpshKG5O+SZPn0gwM9GcymT/I488IkZxML8x04TL685qwkQw75Yqv8oBY/RAmU/idaWXTPAn2yWmQ4oPPvigIFySLQ0RGivsG2NIjuu6sbERlzou+eoyJQrVZZi1hkZulKmsPlqiLBiQM3G48ZNw2IvDirVcFG9wA+esd27iLCbgxpauhPdSIBhuojzv7GFjfm02PDqGoLihswBDlh/Tm2DIijppzOcl4oMf/OCYPJGcusshY1Qe52iR6YBGCItZWPHGscz0jklcNJAKKJCMKMtkuIRgiIQLl7kAJp45cyVXmyq12dhTwhAMq10K9fqTm4nDG5hd5dxc5Uwc9uRkS/WYpa6sSONcd1qjqfTU+JjpSZwkGJYNM5/FHMxMEAxDltyQqcJB8dTZChmyaIReJcviWQpP8F666aabxowC4fBDGiMMiyaC1Yvz58/Pagkzj4H3MT0b5qt4LAzhFVAgGRHrZ09GIrhxTLZ8NtPFSAUD5gOYIN2xY4ewmF955RXxuwIyB0MkPJ8kHHaZc+NjnxM9HDkTZyrg5s1NNVFlIFFPjQ9asdzMJOHk2liYDYLhhsm8A7v5v/SlL816Toqj2Vlg8L3vfU/orlGlg70vbASV4AbP4hIm+2eCZAh6d//93/8t1uJM9AtdKCiEy2YY3LwYFpJxYG4aXOhMmlJQsICpgSEQJu3lTBx6iyQcWrusOMrU25AyNiT8TGa283Mk4Ug9NZINr3G29dRmg2D4eSQYVgIyTDbbBCONCzZM8zrTSOTxsZxaSfDMvzB89Sd/8ifjXs9cDEvaf/GLX2RVIZoVkuzh4TrI9SiFCwkFkskDUCuN+Qb2iRQwfTBGz4FrckQBw1sMZdDDYVgl1ebP0CkJZiIZm1SQemokHPZj0HCQhMN/T1fdeKYJhglsnrcPfOAD+Kd/+qe80mbLRxRIJjkKJDPLYEKZrj5zQFSwLSC7oKXLiihJOCQBOROHkihSTJPhSm7kVArIRtmp1FOTcvokBSngOVk9NSlCSuKaKYKhEgM1yChM+/Wvf71AMBmgQDLJUSCZWQZFBR977DHRySzLdwvIHeGwoVLOxOno6BCeDZP4v/71r8UmkaqZb7qelRTwlPNbJOFMpKemJBhWkc1EgQgrr+jBMNzIHEPBg8kMBZJJjgLJzCJYqUOpi5deegmbN2+ezUO5JAmHs4rY7MkqQpZBc7w0E7a5mImjJByGvEg49Kqk9I0cGKbc0GeDYJivoIdH8mWfR4FgMgcNGCogkJgL1aLnUSCZWQInB7Jjmk2ILL0tYObxwx/+EJ/73OeEV8NmPVqiLI2l1AwTwywaYHk0K5VyQTgkOiaJSThyYJgkHJLcqVOnhOfDENlMbFqscqQHw/XI6ZYF8dgCsoECycwCaD1TgZgNhoXxtLMDehTs+2DRBQlF6T2wK1wSDntleI3kTBxK3+SCcPi5JBxZqcYiAn4OBSdZhJDrDZ+fySot5gc5hTZb/UYFFFAgmRnGj3/8Y6HVJbvVJdjTwTHTBcwcuLFPlA9hT4WciUNxRV4zEg4f7KHJNuHIEBk3ffZpMbRGwsl0QuVUwHAcCZd9QZzLwpxRAQVkCwWSmWEw/8Ixz4lgHwf7EHJltbNyjX0BTOpy/gZ1p6iiW0Dmmz/zFQytkXQoU8KKQBoLJBwWbUyXcPgZLGNnrkaGyBInVLIRlPI2LBwg4UyXEOg9MSTINcEeo9mWyyng4kOBZC4BUBGW1jElL9hgyKZFKsTy95zrUsDUdM1YEk3C4fmk7h3JhqRDoc3JEk4ygkkGjhwg2TCPQ6kbyqaQcKaip0a1AoYA+Vp6ajMtj1PApYECyVwCYII5sUqI8Xf2iPzmN7+ZteO6GEByYLiJ3efM49BLZdhJKkZnMj0zU4JJBEUiJeHQ22F1miSciXpp+HweHxtTqQFXqIYqIFcokMwlCHo1THZ/5StfEbpPBWQHJAuqIrMkmh4OhTwprkkPhxs6lXoTyZ6vYRUZyWI6VWScTCkJhx4KRV5lL07ie9IDolfLY6GqNdUIZkr2h9L6VFRgqHEi8LmcgkmSZgk3KwALuPBQIJlLCExUU2uLyWSWUJNkCsgdOBPnmWeeEYTDUnVu+jKkxk2T4JA2FhNs2bIla1pniXpq9Fh5LKxSo6o09bVYTMDqRqoP5BokXvbdcN2xeo3ClFQS+P73v5/yNcwZUvyUZMTvQ2VjTk79/Oc/n/PjLSDLiBZwyaC9vT168uTJ6He+852o2WyO/uAHP5jtQ7pkMDIyEv3Nb34Tfde73hW12WzROXPmRJctWxYtKSmJ7tq1K+p2u3PyGBoaip46dSr68Y9/PKrVasV1r6+vj27fvj0aiURm5Lt/9rOfjS5evDg6PDws/n/gwIGoRqOJPvHEEylf89Of/jQaDAbj///Vr34VValU0ePHj8/IMReQPRQ8mUsUnAnCxDV7QgqYWTCXQoVg6qWx/4WehhzCls2ZOErQc6EHw9DcmjVrxGfLKkNOccwlmCOSYwIkKN/D6rhMc4L0xCjBQ6+QI54LuHBQ6Li6RMHQDDe7AmYWzMH8zd/8jdCqo+IzQ5hyJs773vc+kSehrA1zOJyJk42eFYp1sjeL4TNKn3Bz57VnCI/FBrmE1GtjHkYJDhfj984UHIPB3AwJsoALCwXt7ktEYeDgwYNigyO2b98udKlo2RYws5DXgHmJhQsXirJhVvpRxoWJ7l/+8peiFJmDuTjUjoO3SAbMS0w14c73ogfDQgQ5rZFGBj0CzonJJeiBEJTJUYKl1/JvmShCs+SeRSo8ZwVcWCiQzCWAG2+8Ucw+Z4krQzPcXHjDcib6TFa00XK/1EFP5d/+7d9EAj4R9FqkMCUbP+ndsEqMExc5yZE9TSw3ZkNmpk24VPnmeWd4jJVmMw1Z2caSaSX4/0wq6aiUTeFSSvv8x3/8R86Os4DcoZCTuYTAzYkbj5yhMpMqB5xNwk2UpbYFTA68ZpyJI+VtGH6iUjIr1ZQzcRJ7o2j9v/3228JrYv5lNsDjYAUbJ1nyeCQYDuRaYIVbKsgJlgy1sQepoEZwYaJAMgXkPCa/YcMGERKibluBZKa/ae/ZsydOONLSJ+GQyJkc53NY6ktyJ8HM9pwihmUZCiThMa9CkuQx/cu//IvwtIgdO3aI7yJDuF1dXYJg2MxaIJgLGwWSKSCn+Qda2nyw4Y8VRgWSyR5IJgyFcROmphpzF9dff73IwzA8yd6SfMhhcIwz5yUx5MV+oJ///OciF8UR2VLK5jOf+YzIPbHakcdPmR6S0de+9rUxHgyr7xKLCArIbxSqywrIGf75n/9ZhHqoLv29732vcKZzkN9hcyMfbFQ8duyYkOn/9re/jbfeeisvCIaglhvJ8Ec/+pEQaGUxA6vdlFppJA/mnwiuGZIRwSo8JfidCiRzYaHgyRSQEzCHwBAONwnKgTCZXfBkZk+rroACZguFlVhATsJkbDZkeSz7MA4cOCDi7fw9/80wSAG5Q4FgCsgnFDyZArIOhjukNpcElYpZLbR69WoxjfLRRx8tnPkCCrgEUCCZAmYEhXBZAQVcmiiEywoooIACCsgZCtVlBcwIKGcyE7pTu3fvxpkzZ8b8jpImLKMuoIACZh6FcFkBFxU++tGPCo2uK664Iv47Nv6x47yAAgqYeRQ8mQsc1HNiP8r+/ftRU1MT/z2T6+wKZ3e47D+4VECCeeyxx2b7MAoooIBCTubCB8UTqdb78Y9/PP47NuJRhJENkJcawRCcwPjkk0+K88BRxAUUUMDsoZD4vwh6In7yk5+I2RzUBuOm+v73vx9/8id/IiRGLkXQqyPBUqpk7ty5+NnPfjbbh1RAAZcsCjmZiwTf/OY38eUvfxlXX3012tvbsWvXrktStZaCkAyXGY1G8X96dH/1V38lmkALciQFFDDzKHgyFwk4L4aigs8++6wQILwUCYag9yYJhqAaMcU5Kb5YQGagiCk94cbGRpHn+8AHPjDhBM2pvKaASwMFkrlIQNmWo0ePivAZ1XcLiIHS8py3UtjwMgcH2jGnRXVneoZURqYOnZzqma3XFHCJIFrABY9wOBy97rrrojfddFP0Bz/4QdRsNkebm5ujlxpCoVC0r69vzO927tzJXS76+OOPz9pxXUjo7OyMqlSq6O9+97v47w4ePCjO4RtvvJG11xRw6aBQwnwR4Otf/7qQUj98+DDq6urEMKsPfvCDYkjUpSSWGAwGcc0114gBacy/UFb+v//7v4VFfd9998324V0Q2L59u/A+OLJbgnpzHN3M+S8cJJaN1xRw6eDS2YEuUhw8eFAk/FlNRYIhvv/974uhVSSfSwnMxbDjnyXdHDXAPAHzU3/4wx9mjGwdDofI/7B8mqR3oYETKZnPKykpGfN7EganW2brNQVcOih4MhcwuIm+973vxXve8x489NBD8d8z8fqtb30LH/rQh3DnnXdeUlVVzL+wCGI2wHP+xS9+EevXr4fZbBYjDUhwnKdzoeWxEkGSTpdfmcprCrg0UCCZCxicLMiO/mSVZO9617tw//33Q6stXOKZwFNPPSUaY5955hncfvvt4necVOn1enEhgd6H3+/HyMgIbDZb/PcsnODfsvWaAi4dFMJlFzBoPTJElCoURBLSaDQzflyXIv7xH/9R5H0kwRD0IBctWoQLCZs3bxY/Ge6TOHXqlAh7yb9l4zUFXDooNGMWUMA0QW+FYbpvf/vbQu2ZeTLmxxg2uxBJnh5wS0sLnn76adFjRAUJNviyoVV+nxUrVuCuu+4SunmZvqaASxMFT6aAAqYJTv2MRCJ46aWXRDPoD37wA5EjW7dundhoLzT86Ec/wrJly7B48WKR32MBA8lDSRZut1uEyCbzmgIuTRQ8mQIKmCaY4K+srBTWPavbTCaTKMq48sorsXDhQvz2t7+9IM8xiZOJ+2RE4fF4RL4vMR+Y7jUFXJooeDIFFJCFgWzFxcUiH0OCIZgrY2Xf3r17L9jzy1xfKrJg9VyygpN0ryng0kSBZAooIAtgfoK9SUocP34cc+bMKZzfAi5pFMJlBRSQBZw9exabNm3C3Xffjauuugo7d+4UIxhefPHFQsd7AZc0Cp5MAQVkAfPnzxeVVA0NDaKUt7q6GkeOHCkQTAGXPAqeTAEFFFBAATlDwZMpoIACCiggZyiQTAEFFFBAATlDgWQKKKCAAgrIGQokU0ABBRRQQM5QIJkCCiiggAJyhgLJFFBAAQUUkDMUSKaAAgoooICcoUAyBRRQQAEF5AwFkimggAIKKCBnKJBMAQUUUEABOUOBZAoooIACCsgZCiRTQAEFFFAAcoX/DzApsWUzSt/gAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=12, show_paths=True)" ] @@ -507,3641 +347,15 @@ "metadata": {}, "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "24", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "animation = phase_space.struphy.plot.animation(x=\"e1\", y=\"v1\", step=4)\n", "HTML(animation.to_jshtml())" @@ -4149,18 +363,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "25", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wrote: ['frame_0000.png', 'frame_0001.png', 'frame_0002.png', 'frame_0003.png', 'frame_0004.png']\n" - ] - } - ], + "outputs": [], "source": [ "frames = phase_space.struphy.plot.frames(os.path.join(demo_root, \"frames\"), x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" @@ -4172,25 +378,15 @@ "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "27", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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Cq8ullahQgWZM2eOWXskNDQ0eV8JAADwrXVGUhPrjAAAkP54e/zmRHkAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACs8k/Knfbs2SOHDh2ScuXKSalSpe54+5iYGNm2bZucPn1aihQpIpUqVUrK0wIAAF8PI9HR0dKtWzdZtGiRVK1aVcLDw6Vnz54yfvx48fPzize4tG7dWq5duyb33nuvbN26VYoWLSoLFy6UwoULJ9frAAAAvtBN89FHH8nSpUtly5YtsmbNGnOZMmWKfPPNN/HeZ8iQIZI7d27566+/ZMmSJebnpUuXZOTIkcnRfgAA4EthZNq0adKpUydX10z16tWlRYsWZnt8Ll++bCoi/v7/KcLkyJFDSpcubbYDAAD4J6aLZvv27dK3b1+P7RpIJk+eHO/93n77bRNgRowYYcaKrFu3Tvbv3y/jxo2L9z5RUVHm4qSVFAAA4ONh5MqVK3Lr1i3JkyePx/a8efPKxYsX472fVkUaNmwokyZNMv/euXOndOjQQUqUKBHvfcLCwuStt97ytmkAAMAXumkCAwPNz8jISI/tet35u7h07txZTp48acaKLFu2TPbu3Str166VPn36xHufoUOHSkREhOty5MgRb5sJAAAyahjJnj27FChQQI4ePeqxXYOCjgGJi1ZSli9fLl26dHEFlpw5c8rjjz8uP//8c7zPlSVLFgkODva4AACAjClRA1hbtmwp8+bNE4fDYa7fuHFD5s+fb7a7T+V1Bo3MmTObAKNjRNzt27fPrDcCAACQqHVG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Vppx/vx5R5s2bRw5c+Z01KpVy1GiRAnHzJkzHRmBT1ZG+vXrZ5an1WSZM2dO+fDDD6V79+5y//338w3lvxWjr7/+Wn788UcZP3687T9XmrJ37155//335cEHHzTX9Ztt3bp15Z577pH/+7//E1938OBBmThxolSrVs1c379/v9x3333mMnDgQNvNSzPeeustKVy4MOfbisM//vEP6dmzZ+r/UdI4h8Mh7dq1kxs3bsihQ4ckT548cuXKFZk7d65kBJl8cU3/OXPmyEsvvWSCiHrhhRckR44cMmPGDNvNSxPee+8918EEnvRD0hlEVMWKFaVRo0ayevVqdpWIdOrUyeO9U6ZMGXPA1eCP/1ixYoVMmzZNJk2axC6Jw9mzZ+WPP/4wXRH4n19++UVWrlwpn3zyiQkiSo9hPXr0kIzA58LItm3b5ObNm1KzZk3XNn9/f6levbps2rTJatuQ/uh7afPmzVK2bFnbTUkz9JubhjP98NRqiH57e/755203K80caPXg8cUXX0hISIjt5qRJb7/9tjz77LOm2qhB/8CBA7ablCYsXbpUihYtaqqMu3btku3bt982viY987lumvPnz5ufefPm9diu152/A7w1dOhQMxBxwIAB7LT/0kHhQ4YMMVVI7abR7isdyAqRp556Sp588klzkEXcQUTDmn5B1ODWtm1b6dy5s/z+++8JnvHVFxw/ftwcp5o3by6HDx82Z7PXY9bHH38sXbp0kfTO5/66AQEB5uf169c9tl+7dk0CAwMttQrp0ejRo80HwezZsznYusmfP7+pjOhsLK0aTZgwQd58803xddo1o7NFHn74YbN/9KLfbPXA8ttvv9luXprw9NNPmyCi8uXLJ++8847ZZxpqfV1AQID5P9WmTRvZvXu3Gb+moV+7jvU9lN75XBhxThGL3Yet15leB2/poOdhw4bJ999/b76pIG7afdW+fXtZtGgRu+i/++P11183BxG9XLhwQZYsWWLOM4LbFSxYMM7Pa19UqlQp8fPzk969e7u2afenBtr169dLeudzYaR8+fJmQN0PP/zg2qYjk8PDwzmowCtjx4413TM6ir1ly5bsNTdXr16Nc3ZW7G5RX6TdD86KiPNSqFAhUw1YvHix+Lq43ju6XzJnzmwGivu6Fi1amBk12l3jdPToUVc1Mr3zuTEjmizDwsJMaUv/gDraX0uBderUMdOm8J9BvhcvXjQhzTkYUdWqVSvjLLCTRFOmTJEXX3xRXnvtNQkKCnLtG/13aGio+LpWrVpJ06ZNpUaNGhIdHW26sHQGAAdb3In+X9KZfN26dTMDNdeuXWuu65gjZ4XEl9WpU0c6duxoLjr9WYPJiBEjpF69elK/fn1J73z2rL0LFiyQqVOnmsF2OlVz8ODBEhwcbLtZaYJOe9Z+2thmzZplPiR82fDhw2X58uW3ba9QoYJ89tln4ut0/R6derhmzRoT/HW/9OnThzE18dADyyOPPGKqIxAzUPXzzz83X4S0gt21a1cTbvG/2Xu69pOGex3jqEGkf//+GWIFcZ8NIwAAIG3wuTEjAAAgbSGMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqn1uBFUDa8Mcff5hVWlXOnDnNasgZYfEmAInHomcAUp2eIC5PnjxSuXJlE0ROnjxpTkEwefJk6dSpE38RwMfQTQMg1f3555/m58KFC80S4AcOHDDnhnr22WclJiaGvwjgYwgjAFLdxo0bzbmgSpYsaa7reWyaNGlizm1z/vx5/iKAjyGMALASRqpUqeKxTc/SWrp0acmXLx9/EcDHMIAVgJUwUrFiRdNFc+3aNVm0aJF89913MnfuXP4agA9iACuAVBURESG5c+eWcuXKmZ/Hjx+XEydOyOzZs6Vt27b8NQAfRBgBkKqWL19uxods2bJFqlatKg6HQ3r06CErVqyQgwcPSubMmfmLAD6GMSMAUr2LJkeOHFKpUiXX4NWhQ4fK0aNHZfHixfw1AB9EGAGQ6mGkZs2aHhUQDSa65oiOGwHgewgjAFKVVkJ0TZHY+vTpYxY/A+B7GDMCAACsojICAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAADEpv8HHVmCw+8BK9gAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.plot.equilibrium()" ] @@ -4203,32 +399,15 @@ "## Derived quantities\n", "\n", "`.struphy.analysis` computes without drawing, and every result is an array that plots itself. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "29", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "largest drift of the total energy: 1.586e-06\n" - ] - }, - { - "data": { - "image/png": "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EwIZAAAAALgJgQwAAAAA3IRABgAAAABuQiADAAAAADchkAEAAACAmxDIAAAAAMBNCGQAAAAA4CYEMgAAAABwEwIZAFSirVu3ygMPPCB9+/aV7t27y1VXXSWvvvqqnDp1ivNgZ/r06XLBBRd43DFZvHixXHbZZdKpUye56aabnHrtu+++KxdffLH4Gk89VwDgLQhkAFBJPvroI2nfvr0cPXpUHnnkEXnzzTdl6NCh8sYbb0i/fv388jxMmzZNevfufcb9R44ckY0bN4onyczMlOHDh0urVq3kww8/lEmTJjn1M6WkpMjmzZsrrH2lvW9Fv9YTzxUAeJNgdzcAAPzBqlWrZMKECfL000/Lww8/bLu/a9euppfs0UcfFX+UmpoqmzZtOuP+a665RgYMGCCe5M8//5Rjx47JDTfcIG3btnX6Z6po5Xlfd7UZAEAPGQBUihdffFFiYmLkwQcfPOOxyMhI87jV6tWr5frrrzdhTXvOXnjhBcnOzj5j6NuiRYtk7Nixct5555lQt2bNmiL7PXHihDz22GNmHxdeeKHcf//9kpycXOQ5e/bskXvuuUd69uwp559/vjz55JNy+vTpM95rwYIFMmLECDNUb8qUKdKuXTvZsGFDkX1pG3v06GF6j9S8efPM8/TWpUsXGTVqlHz++edFhrr985//lOPHj9uep21R33//vQmw9lx1XEpztv2//fbbpndM6c+hbf3f//53xj7O9jNZOdK+c50XZ9/3bD/b2V57rnPoDEd/11xxfEr6vZ07d6557K233rINGb799tvNedSfLz093fbaXr16SW5ubpH3/O2338zztm/fXqafHwBKZQEAVLiGDRtarrjiinM+b926dZbw8HDLjTfeaPnll18sn376qSUuLs5y5ZVX2p7z/PPPW4KCgiydO3e2fP3115YlS5ZYRo8ebalZs6blyJEjtueNHDnS0q1bN8vcuXMtS5cutbzyyiuW4cOH2x7fsmWLJSYmxtK3b1/zHL116NDBMmrUqDPeq23btpYvv/zSsnbtWsuxY8csTZs2tdx///1F2v7ZZ5+Z5+7du9d8f+LECfPz6O3333+3vPnmm5bq1atbpkyZYh7Xtj700EPmPuvzdu/ebR579dVXLdHR0RVyXMpy3JOTky1ffPGFRf+3OWvWLPN8PQ7Fne1ncrR9jpwXZ973XD/b2V57rnNY0rkqiTO/a644PiX93h4/ftySlJRkiYiIMG3W/T/++OOWsLAwc16t53P//v2W4OBgy8yZM4v8DNddd52la9euZ/05AaAsCGQAUAn0gvjWW2895/M0MHXv3r3IfXrBqReMegFpvdjU760XzdYL54CAAHMha1W3bl1zAW0vKyvLtn355Zdb6tWrZzl16pTtvm3btpkL2eLvtWHDhiL7efrppy2xsbGW7Oxs232DBg2yDBky5Kw/n14Qt2nTxvb9Sy+9ZC6uiyt+ke/K41ISR/a/atUq8/2mTZvOuq/SfiZH2+fIeXHmfR352Up7rSPn0JFA5szvmiuOT0m/t6dPnzbtnDx5cpG2XX/99UUCmbrqqqssF198se37lJQUE9zeffddh44RADiDoh4AUAkiIiIkIyPjnM9bsWKFDBkypMh9OvQqJCTEPGZVp04dadiwoe37qKgoczt06JDtPh2q9fjjj8sTTzwhCxculKysLAkNDbU9/tNPP8nIkSNN26yaNWsmzZs3l19++cV2X/Xq1aVNmzZF2jRu3Dgz7+jbb7813+/fv98Mb7MfZpiXlyfvvfeeaUe3bt3McK///Oc/smPHDoeOWUUdl/Lsv7wcaZ+j58VR5fnZXHUOHf2ZXHl8iv/e6lBDHcZbfG7iJZdcckZ777jjDpk/f76piqref/99qVKliowZM8apnxsAHEFRDwCoBB07djTzeM5Fy9/bX2iqoKAgCQsLk5MnT9ruCw4u+Z9vHflgpVUcdd6Thia9wNy1a5ep7mitDqgBcebMmSasFZ+fs3v37iJz3Ipr0KCBuZDVi3W9OP7ggw+kZs2atnlWSt9HL2SfeeYZUwRDL6xnzZpVanXCyjou5dl/eTnSPkfPi6PK87O56hw6+jO58vgU/721Li1R/FgU/17pHDP9eXW+2b/+9S/z9cYbbyzxuQBQXgQyAKgEejGnvUq6jpUWDChu9uzZJsy0aNHijCIGO3fuNBfOLVu2dOo9AwICZNiwYeamZsyYYYolXHHFFaZ0u/YeaJGHe++994zXarg6l7/97W9y7bXXysGDB00g06IR9j1wetF89913F1mvS0v/Fw8G5wpLypXHpaL37+jPVJqynpfS3teRn6201zpyDivyZ3LlvrQXTf8mtBiNfc/Z+vXrS3y+foihwVOXA9AewVtvvdWpdgKAoxiyCACVQEul67CvK6+80lQQtF786hCqhx56yNbjoJXjvvjiC/nhhx/M91o5Tu9r2rSpLVg5Qoea6QXrgQMHbPfl5OSYC1Jrz4GW3//666/NumjWSnpNmjQxlen27dt3zvfQBZK1x+SWW24xw8E0oNnT4WdLliyxVfPT4WS65pq9evXqmWOgbTgbVx2Xyti/oz9Tacp6Xkp7X0d+ttJe68g5rMifyZX70iqnV199tVl64vDhw+Y+XT/t9ddfL/H5+gGD/h3pMNz+/fubDzEAoCLQQwYAlUCDkPY2aHnx8ePHm/lc+mm+zr3q3LmzPPfcc7aLQP00/vLLLzdzYPSiU3sxvvzySzPEzFHa45GQkGBKh+fn50tgYKAJZFrSOz4+3jxn9OjR5kJb31Mv0qtWrWoW+dWePO35Ohdtjz7v1VdfNeXu9cLYns410gBat25dM9RLQ6iWTddFiK10YWx9rbZVh0Hq3CZ9XXGuOi6lceX+Hf2ZSlPW81La+zrys5X2WkfOYUX+TK7e1yuvvGJCmf4NaAjVxb51yO0777xj5tTZ0w8utGdbX3Pbbbc51UYAcEaAVvZw6hUAgHLTIKYXg3phaD/Mz0oDm86J0YvNuLi4Io9pMQ29AC0+lE4X9tUL5xo1apzxXkr3o8GsJNqzoL0B2h7755T2Xlbaq7J3716pVauWee/iNAzq40qLNaSlpZnvi4c37bHQ99GfV5+noUHva926dYUdl5Kcbf96vrQnUIcAlnTOiiv+M5WlfaWdF2fe15Gf7WyvPdc5LO1clcbZ37WyHJ9z/d5qz7GGuUaNGpnQ+eyzz5bYo6nr+OmHGHrcigc2AHAVAhkAAPALf/zxh1kcffDgwabXWueP6bbO39QFz+1pqNOQqYu567BiAKgoBDIAAOAXrEMbdV6cDsHUcKbDEl988UXTK2jVr18/UxW1Q4cOZs6nlrwHgIpCIAMAAH5FS+CnpKSY+XIlldrfsmWLGZbauHFj05MGABWJQAYAAAAAbkLZewAAAABwEwIZAAAAALiJ3weyDRs2yDPPPGPGkgMAAABAZfLrOWS6sKQufrlq1SpZunTpGevinI2uR7Nu3TqpXbt2iROCAQAAAPin3Nxc0+HTvn17CQ8PP+tz/TpJPPHEEzJhwoQyrS+iYaxHjx4V0i4AAAAA3m/58uXSvXt37wpkBw8eNOuEtGjRwpScLUl+fr7s3r3bpM24uLgyH5yNGzfKc889V6ZApj1j1v2UtQ15eXnmZ42JiZGgoKAy7QOVi3PmfThn3odz5n04Z96Hc+Z9OGfeRTONdt5YM4NXBLIZM2bIe++9Z4YOnjx5UrZt2ybNmjU743k//vijjB8/3oSyjIwMadWqlUyfPt2sFWL15ptvSmpqaonv88gjj0hOTo7cd9998vnnn5e5vdZhihrGdB2Tsv5hhYWFmRNFIPMOnDPvwznzPpwz78M58z6cM+/DOfNOjkxt8piiHnPnzpUHH3xQ3n333VKfs3fvXrnsssvkb3/7m+zfv98kz2rVqsmoUaNMQLPKysoyc7xKuumUuddff90EIA2AWtBDF4h8++23ZevWrZX00wIAAACAB/WQffDBB+brF198Uepz3nrrLTNMcdKkSeZ73dYhhzou8+eff5YBAwaY+++5556zvpdOrjt27JgJaEpDmoY4/eQBAAAAAPwukDni119/lfPOO6/I3LKuXbtKRESEecwayM5l4MCB5mY1depUueuuu6R169alviYtLc3crLR3TmmIK2uQ09dpzx5B0HtwzrwP58z7cM68D+fM+3DOvA/nzLs4c33vVYFMhywWD10BAQFSt25d81hZ6Xyy2NjYsz7nxRdflCeffPKM+7Uoh84DKwsNYydOnDDbgYEeM3oUZ8E58z6cM+/DOfM+nDPvwznzPpwz76IZwScDmQ4rDAkJOeN+7THTx8rKkSqL999/v0ycOPGMyilaIdGR6ilnS861atWiqIeX4Jx5H86Z9+GceR/OmffhnHkfzpl3cSabeFUgi4qKMgU4itOqjPpYRb93Se+hxUHKUyFRe8bKuw9ULs6Z9+GceR/OmffhnHkfzpn34Zx5D2eu7b1qnFzLli1l+/btRe7T0vfaW6XrlgEAAACAN/GqQDZs2DBZsWKFKXlv9c0335gxtUOHDnVr2wAAAADAawPZn3/+KevXr7cV59CFofX7Q4cO2Z5zww03SJs2bWT06NGmquLXX39tStzfeuut0rx5cze2HgAAAACc5zFzyJ566inT+6Xatm0riYmJZnv8+PHywAMP2Ip36HpjTz/9tLlP1yF7+OGH5c4773Rr2wEAAADAqwPZ+++/79DzatSoYUrQAwAAAIC385ghi94iKSnJrFnWsWNHdzfF42mxFR1W6oqFr125L39y/PhxWbBggTl2qamp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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "total_energy = out.scalars.total_energy\n", "energy_error = total_energy.struphy.analysis.relative_error()\n", @@ -4244,42 +423,17 @@ "metadata": {}, "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "31", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/diagn_tools.py:220: UserWarning: No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.\n", - " ax.legend()\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy8AAANWCAYAAAD3ALM5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAb0FJREFUeJzt3QeYZFWdN+DTPRlmpocchygZlAEESQIiYRUliIKIqyK4yxowgMB+oKDAGEBxdRFRQCWouARBAQUEBQeWLMmRDCNhEJjQk3pSf8+5bDXdMx2qq6vqpvd9nnpmqupW1e3Tt6rPr/7nnNvS2dnZGQAAADKuNe0dAAAAqIbwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAjTc3Llzw5e//OXwjne8I2y++ebhsssuC7fcckvy/7/+9a+Dfr477rgjeez9998/4LZPPPFEsu11113XsNcAAJpDeAEa7rjjjgu//vWvwze/+c1wzTXXhPe+972hvb09/P3vfw/z588f9PPNmTMneey8efMG3LajoyPZdtasWQ17DYqhlkAdj+PLL788HHTQQcljTz755IbuI0DZDU97B4Diu/LKK8MnPvGJsMcee3Td9u53vzv87W9/CxtssEGq+wYVgw3US5YsCRtvvHFyLB922GHhiCOOCC+99JIGBWgg4QVoqNgRnDlzZhg/fnyP28eOHZt8Uw15NWzYsPD888+H0aNHJ5U6ABrPsDGgV4sWLQo//OEPw/777x/e+ta3hr333jucd955ybfN1frBD34Qtt12267/x7Cy7GXZITpDed3p06eHz372s2G77bYLe+65ZzK3pt7i8LPTTjst7L777uFtb3tbOPDAA8PVV1+93HYvv/xy+PSnP91jX2qdf9O9bX70ox8lw+7ia7/vfe9Lnrezs3NQ+3f99dcn+zF16tTwy1/+Muy1115hp512Sob1VZ7rF7/4RdL2cf/jUKiFCxf2+Rxx2/gccdvPfOYzye+hlnbr/pxxeGGsaEyaNCkce+yx4Z///Gev7THQsVLtc/785z9P9j06/PDDu47P3n633cXgAkDzqLwAvc4TiR3CRx99NHz9618P22+/fXjsscfCCSeckExkj2P8qxE7gbvsskvy+Pj/SuewMr8gXu8+RGcor/v666+HnXfeOfk2/Fvf+lZYY401wqWXXhruuuuuuv2GYwUpvkb8lv3b3/52MuTt2muvDR/4wAfC//t//y/Z5+i1115LFicYNWpUEgjivsQOftyXWubfRAsWLAj77bdfEvZOP/30ZD9i5/tXv/pVEizisLxq92/27NnJfpx//vmhpaUlTJ48OWnn+PtYvHhxmDBhQnL/GWeckfwbQ1h8jXPOOadrfyrP8d///d/JY84888zwyiuvJL+rGBjuvffesPLKKw+q3SrPedFFFyWLPMSfMz5nfP1HHnkk3H777YM+Vqp9zhgE47Cx2AbxdxYDVrTWWmsN4YgBoO46AZZx1llndba0tHROmTKlx+2///3v49fynTfddFPVbdbe3p485qtf/WqP26+++urk9jvvvHPQr3vDDTck12+//faubY4//vjOYcOGdT755JM9HnvIIYck215yySWD+j339honnHBCsn8PPvhgj22PO+645PbHHnssuf7FL34x2Zenn366x3aHHXZYTfsSfe1rX0se++c//3m5+xYuXDio/fvFL36RPNcRRxzRY7tjjz22c8KECZ0f//jHe9z+2c9+tnOFFVboXLBgQddtlec4+OCDe2wb2z/+7LENKga7X4cffniP7S6++OLk9nvuuWfQx8pgnrO3Y3Kwx/nHPvaxQT8WgOoZNgYs54orrghbbrll8m15d/vuu29YccUVww033JC51/3d736XVDviBOrujjzyyLrtX3yNOAyu8q18Rax6xOFW8f4oVh7iz7Dhhhv22C5Wn2oVV2vbZpttkmFXyxoxYsSg9q/iQx/6UI/rcdhXrJIccMABy90eV12bNm3acq8dJ6l3F9t/1113Db/97W+7bhvsfsXJ793tuOOOyb+xglLrsVLNcwKQfYaNAct57rnnkvkEW2+9dXI9djArlzh8KM7nyNrrxlWelu0cRxMnTqzb/r344os9VkyrWH/99bvuj+J+Vub6dLfuuuvW/Nr/+Mc/eg0utexfX/sTh4v1d3scDveWt7xlwPaNtz3wwAM179eyz1kZfvbqq6/WfKxU85wAZJ/wAixn3LhxSQf2wgsv7LV1ll05LAuvG1cvi/NeltXbbUPZvxkzZix3e+zUV+6v7Etv2/V2W7Xizz5QR7va/auI84N609ft3RcGqOirzbu/Vr32q/vrD/ZYGczPBEB2CS/Act71rnclqyzFieYrrbRSLl43Lgxw8803J5Oy47ChiltvvbVu+xdfIw5xih3x7vsXXzeKw6WiOJTpj3/8YzLUaoUVVuja7s9//vOQ2iauDBarFGuvvfaQ9q+ebrvttvAv//IvXdfjz3z33Xcnq481cr8acYxWVg6LCxAAkE3mvADL+epXv5rMo4hzIp599tmu2+PqVt/4xjd6rPqUldeNq0zFlaXiKlJxZa5K5ziuelUvJ554YjIk6eijj+46r0d8/rjfsYO+zz77JLd9+ctfTlYUiytXxVWxKp38e+65p9fnjUspx5XE+nPKKacknes4byYOmYriksBXXXVV15yRavevnu6///5w0003Jf+PP2ts//izx32paMR+NeIYrcyXevDBBwf9WACaQ3gBlhOXso3L+sYKxmabbRbWXHPNsNpqqyXn0ojLyW611VaZe90ddtgh6cjHJZjjHI342LiMb2UZ3nqI5we58cYbw5NPPpnMmYjL6MaqQTx3SJygHpcdruzLlVdemXTq29rakn2J57mJAaT7BPuKuMTvQEPCNtpoo/CXv/wlCTCxkx2rL7HiEKsxlbk+1e5fPcVzt8Rzz8QKSPxZY2CMP3tsg8G2W9rH6CabbBI+97nPhS984QvJ81dznpfKOWHiUs1R3L5yjphaz+cDQN9a4pJj/dwPlFz8Nj0OVYqBoJbhOfEjJq7otOqqqyaXivgNfJyEHjuJvZ3or7/XjUPD4spXccL3mDFjlnu9eNbzOOchPi5+4//0008nnf3BzNXp7zWiGDZipSd2xHu7P1q6dGnyHJV9iRWSuJJX7ODHEypG8cSVm266aXKyyWVX7upLbLtYYVhnnXXCyJEje92mv/2LnfsXXnghWQ0tnotm2duX/Z309ruKoenDH/5wMjE/Lk4Qh4TF14sT41tb+/5erJb9ihWm2E59DRHr71ip5Tnj7z4uABGHj8X9jKGsL7EK1v1cRd0N9pgDYGDCC0CTxG/0YzUonoG+0mGOJ4qMZ4WPQ5X66/RnzbLhBQCaIT9/KQFyJM67qJwXJVaD4nla4vCqT37ykz2+6T/kkEOSoW55Ci4AkBarjQGDFofivP3tb+93m/htfPx2vqz7vd566yXnNolDiuKwqxhgjjnmmPDtb3+7x3arr776kF8LAMrCsDFg0OJcjscff7zfbeJ8hspJCMu833EuSFx9K85PWXaifp71NZcEABpJeAEAAHLBIGsAACAXSjHnJQ4Victojhs3riHnOQAAYGji3MA4JDUuM561RUziyY/j0vtZNHLkyF5POVBUpQgvMbjEcw8AAJBtcaXGddddN2QpuGywwdgwffqSkEVrrrlmeOaZZ0oTYEoRXmLFJaqcLA4AgGypnOi20m/LilhxicHlb3/bKIwbl62KUHv70rDFFk8n+yi8FEhlqFgMLsILAEB2ZXWIfwwu48cPS3s3Si9b8REAAKAPwgsAAJALwgsAAJALpZiwDwBA409N0d9ywiNGjAjDhpkzwtAILwAADEkMLXG53hhg+jNhwoRkad+sTson+4QXAACGdHLJl156KamqxKWOezvBZNxm3rx54ZVXXkmur7XWWlqcmggvAADUbPHixUkwWXvttcMKK6zQ53ZjxoxJ/o0BZvXVVzeEjJqYsA8AQM2WLHnjzPMjR44ccNtKuFm0aJEWpybCCwAAQ1bNPBZzXRgq4QUAAMgF4QUAAMgF4QUAAMgF4QUAAMgF4QUAgCGL53KpxzbQH+EFAICaxZNTRgsXLhxw23g+mGjEiBFanJo4SSUAADUbPnx4cv6Wf/7zn0koaW1t7bXiEoNLPEHlhAkTnKCSmgkvAADULJ67Za211grPPPNMeO655/rdNgaXNddcU2tTM+EFAIAhGTlyZNhkk036HToWqzKVIWZQK+EFAIAhi8PFRo8erSVpKBP2AQCAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXHCSSgAAGMDImUvDyCUtmWqnke1LQ9movAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkwPGTAE088Ef70pz+FFVZYIbz3ve8NbW1tPe6fN29e+N3vfhemT58ettlmm7DHHnuktq8AAEBJKy8nn3xymDRpUrj11luTy+677x6efvrprvtfeuml8La3vS2ceeaZ4b777gsf/OAHw5FHHpnqPgMAACWrvPz85z8P55xzTpgyZUrYYYcdusLK4sWLu7Y58cQTw7hx48Kdd94ZRo0aFR555JEkzBx66KHhoIMOSnHvAQCA0lReYnA57LDDuoJLtNZaa4WJEycm/1+yZEm46qqrwsc+9rEkuERbb7112G23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gTJZOGdM6Fg0ImTJwo7ydeXL9xOTO1Ycq52wAgAUifBSEnlcJpnBE1Yaw9Axysa8FyCrWtPeAWBoneruF0hj6BgA+fLoo4+GO++8s+7P29nZmTz3X//617BkyZI+t2tvbw/3339/eP311wf9GsIL5IiwAgDU6tJLLw077LBD2H333cM+++wT6mnq1Klh8803D3vvvXc44IADwgYbbBDuvvvuHtssXbo0nHjiiWGNNdYIn/zkJ8N2220XTj755EG9jvACOaGyov0BYKgB47zzzgtnnXVWv9vFkBErKPGycOHCqiouhx12WNhyyy3Diy++GKZNmxb222+/cOihh4aOjo6u7f7f//t/4eKLLw733HNPeOCBB8IzzzwT1ltvvUH9DMILANDrvBegWM4444yw44479rvNrbfemlRNDjzwwCSQrLPOOuHqq6/u9zH33ntveOihh8Ipp5wSWlvfiBennnpqEmJuuumm5HocInbuuecmt2+11VbJbS0tLeHYY48d1M8gvJALFhyA7DLvBSBds2fP7nHpXu0YjFg1Ofjgg8M555wTnnzyyfDII4+Eiy66KHz0ox9NgkhfYhUlhpZJkyZ13bb++uuH1VdfPbkv+stf/hIWLFgQ3ve+94UXXnghuT3u62AJLyWg4w/1YegeUE+CP/UyceLE0NbW1nWZPHlyTc9zySWXhFVXXTV5vjihf8qUKWGVVVYJ48ePD7fddlufj4tVlQkTJnRVXSriYyuT8mMwivd/97vfTao/H//4x5O5L//5n/85qH20VDIAAOTYtGnTkoBRMWpUbSfIfvzxx8OMGTPC8ccf3+P2jTbaqCuYxHkwcZso3rbLLruEESNGJFWVZc2fPz+MHDky+X/cJs6lefXVV8Nzzz0Xhg8fHm6//faw1157he233z584AMfqGofhZdBnCkdwDlfKBPne4F8GD9+fI/wUqsxY8Yk813uuOOOPreJE+7vuuuurpB0yy23JEPE5s2bF2bOnJlUYKLFixeHV155pWtCfnze6Oijj06CSxRXPYvzX2KIqTa8GDYGwJAZ/gKQf3vuuWdyjpaHH364x+0xiFQqK2effXYSbuIlBpfK42Jl5dprr+16TJyoHwPNu9/97uT6zjvvnASsON+lYtGiRUnAWW211areR5UXAAAoyVLJr776anjqqaeSIVyVCsu2224bxo4dGw45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gCFRhSEtwgtAk5R93ouqCxSP9zXNJrzkhLkbAEAWqcKQuTkvjz76aLjnnnuqftKtt9467LDDDkPZL6hr8GudaXoXADRSWefC0FxV9ehuuOGGcPrpp4e2trYBt50zZ044+uijhRcAgJKpDCMTYmiUqr+O/rd/+7dw9tlnD7hd3Obll18e6n5B6bW2LQhLZ/kGq4jzXha3j0l7NwAaShWGVMPLTjvtFNrb20O9twUAoJhUYUgtvOy+++5VP+FgtgUAoNiEGOrJamM5YKUxKJayL5kMAKmHl+uvvz4ceeSR4Re/+EW9nhLqRgAEgHTEyfsm8JO58PL666+HqVOnhldeeaVeTwmF1NLWkfYukAGqL0AZAovQQr3V7eQXseoSL0D9DRu3ICyxdj4AGSao0AzO3AeQEssmA3knsJD58DJlypTwhz/8oc/7d9lll7DvvvsOdb/4P+ZqAABZI7SQm/Dy/PPPh5tvvrnHbXPnzg2PPfZYmDBhQlhttdWEFzIbBFtnKjaSLaovQF4ILGTBoHtyhx9+eHJZ1lNPPRXe8573hPe///312jcAAFIksFDY1cY23njjcPDBB4frrruuXk8JUApWHgOyxEphZFldx9DMmzcvvPrqqyHvLGULAJSJCguFDS9///vfwwMPPNDjtiVLloSHH344XHDBBf1O5mdwTNaH8jD3BUiD0ELhw8stt9wSvvKVr/R8kuHDw3rrrZeEl3e+85313D+oK5P2ASg7gYVShZf/+I//SC4A1JfqC9AoAgtFYd1YAIACElgoorqFlwcffDDccccdYdKkSWHXXXet19MClIrqCzAUAgtFV7elkuOJKz/72c+Gq6++OmRV6/iOtHcBYECWTgZqXd4Yiq5ulZcvfelL4fOf/3xoba1bHio1K401tm1bZxoxCUC+CSuUUd16cC0tLcmqYwAMneFjQG8EFsqu5rQxa9as8Pjjj4fXX389dHZ2dt2+4YYbhs0226xe+wcAUGoCCwwxvHzve98LJ598cpg/f35ScamEl/j/E044IXzzm9+s5WkB6P4BPW5+WNw+RptACQksUKfw8re//S2ceuqp4YYbbgh33313mD59ejjppJPCRRddFL7//e8n/wf619LWETpnjdJMAPQgtGRXx/zRYfiibP3t7ljcEspm0LPr77rrrvD+978/7LHHHsnk/MWLF4dVV101fPnLX05u+9WvftWYPS0Rk/WBCiuPQXlWChNcoAHh5bXXXgtrrrlm8v9VVlklvPzyy133bbLJJmHatGmDfUpoOgERgDQJLFCbIa1rvMMOO4Q//OEPYcqUKclwsl/+8pdho402GspTArAM1RcoBoEFUpjzsvXWW4eJEyd2/f8Tn/hE2HXXXZPre+65ZzjyyCPrsFsAAPlnKBikHF7233//HtfPOeeccMopp4TZs2eH9ddfv577BsD/sfIY5IvQAo1Rl7NKrrTSSsmFoTMXAwDySWCBjMx5+e1vfxsuu+yyUO9tIU2CInlj7gtkj3kskMHwMnXq1HD//fcnyyIPdHnsscfCAw88EPJ8/g0AgL4ILJCD1ca+853vhBEjRgx4OfHEExu7xwAlpvoC6YYVQ8MgB3NeDjvssGRZ5GpVViMDAMgDoQQKFF5iGBFIGs8cDKAaVh6D2ggokH91WW0M8hwYW2d6GwAUhYACxabXBpBDqi+UlXAC5Sa8AOSUAENRCShAX4QXAKDpBBSgaeHllVdeSc77MnPmzDBu3Liw/fbbhzXXXLOmHeANJusDtVB9IcsEFCD18PLCCy+Eddddt8dtLS0tYa+99gpnn312mDRpUj33DxrOpH2A+hBWgMycpLJipZVWCjfeeGN4/vnnw6uvvhqefPLJcMkllyT37bbbbuGhhx5qxH5C4bS0daS9CwB1NXLcgq4LQCbCyworrBD222+/5Lwvq6yySth4443DRz7ykXDLLbeEQw89NHz3u99tyI4CsLzF7WM0C5kkyACZCC/LWrRoUdf/N9988/DPf/6z6sfOnTs3fOtb3wp777132GmnncJRRx0VHnnkkeW2+93vfpcEpm233TZ89KMfDU8//fRQdxsASCHIqMoAqYaXWInZcMMNw/rrrx/OOuus8IlPfKLqx/7bv/1bmDdvXjjttNPCueeeG5YsWRJ22WWXHuHk+uuvDwcddFDYZ599wn//93+H+fPnh9133z3MmDFjqLsOAKRAkAFSWyr5rrvuCn/84x/DpZdeGt7//veHAw44oOrHXnzxxWHEiBFd19/+9reHyy+/PNx2221ho402Sm6LweaII44Ixx9/fNc2cWWz888/P5x88smhCKw0BkBZda/EmPAP1L3yEpdHvv3227uux2WSTzjhhGTp5OnTp4fJkydX/Vzdg0ulylIJKNGcOXPCvffeG/bff/+ubUaOHBne/e53h1tvvXWwuw59EiAB0qciA9Q9vMQ5LnEI18EHHxz+/Oc/h87OzuT2YcOGhR133DH87//+76Ce74477kjmyqy99trhYx/7WLjuuuvCNttsk9z3j3/8I3n+tdZaq8djYuUl3teXjo6OMHv27B4XACA/zJMB6hJeVltttfDAAw8kAWGPPfZIVhzbeeedk7kqJ510UlfwqFY8L8w111wTrrjiimTYWQwwcfnlKM6BqVRbuhs1alRYvHhxn88Zqz9tbW1dl7gyGgCQX6oyQM0T9rfYYotkiFc8p8sXvvCFrmWTTz311HD66acP6rlWXHHFpPISzxHz05/+NKy88srh+9//fnJffM4onk+mu3i9cl9v4lyYWbNmdV2mTZtW1b447waQJ5ZJpqwEmYL+Psc6PxANnrAfqyyDrbT0p6WlJamUVIZ5xeFh6667brIoQKzKVNx5553J0LW+xMpMvOSBuRYAUDsT/rPLsthkcqnkWsUlj2OlphJU4tyWn//85+Gee+4JBx54YI/llH/yk5+EJ554Irl+0UUXJcPKjjnmmLR2nYISJAHyzTyZ5rdxfxfI5FLJtYqVkVhl2XTTTZM5LXF4Vxwy9uMf/zg5r0tFnEfz/PPPh6233jrZPs51icPL3vrWt6a16wBADqjK1NZWkGWphZfW1tbk3C3x8sILLyRzXyZMmLDcdsOHDw8XXHBBOPvss8Nrr72WDCNbdollAID+lDXICCX1M2/umNAyPFvHzrzFqQ2iKl946W6dddYZcJvx48cnFwCAsgcZoYSyykR4KStzLAAgWyEgzTAjkMDAhBdYJlC2zvS2IPsskwz5qcoIJVA/emkAADUEGaEEmk94AQAYgKAC2VC+JQoAAIBcEl5SYrI+UUtbh4YAAKiS8AIAAOSC8ALLUBUDAMgm4QUgZyyTDEBZCS8AAEAuCC8pMCwJAAAGT3j5P1Z9AgCAbBNeIMPVsda2N8/uDABQdsILAACQC8ILAACQC8PT3gEAymFh++ge10eOMywSgMERXko6lwLIp7ye42XZ4NLXbQINAP0RXqCfoNk601sEmkmgAaA/emYAND2QDPXxKjQAtXv00UfD7Nmzw8477xzqqbOzMzz22GNh8eLFYeuttw7Dhg1bbpsFCxaEJ598Mrlv8803Dy0tLYN6DeEFgNwxfwZg8C699NJw7rnnhqeffjosXLgwzJkzJ9TL1KlTw4EHHhhmzZoVRowYkdx25ZVXhh133LFrmx/+8IfhpJNOChMnTkzC07hx48LVV18dNt1006pfx2pjABQizCx7AWD5gHHeeeeFs846K/Rn6dKlSXUmXmLIqabicthhh4Utt9wyvPjii2HatGlhv/32C4ceemjo6OhItrnrrrvCpz/96XDhhReGRx55JDz33HNh7733TraJj6+W8NJEJusDZZNmiBBoAHo644wzelRCenPrrbeGDTbYIKmixECyzjrrJNWR/tx7773hoYceCqecckpobX0jXpx66qlJiLnpppu6nne11VZLwkoUh4sde+yx4eGHHw533nlnqJbwAkDDpD03Jb5+9wtAEc2ePbvHpVLtGKxYNTn44IPDOeeck8xLiRWSiy66KHz0ox9NgkhfHnjggSS0TJo0qeu29ddfP6y++urJfdHKK6+c7Fv3oWovvPBC8u99991X9T4KL9AP1TKyZvi4+WnvQqYsG06EFaCMJk6cGNra2roukydPrul5LrnkkrDqqqsmzxerIVOmTAmrrLJKGD9+fLjtttv6fNzrr78eJkyY0FV1qYiPjfdFH/zgB5MAE8PRb3/723D55ZeH4447LowePbprm2qYsA8pa2nrCJ2zRqW9G+QswOTpfC8xUNQ6fEy1BGBg06ZNSwJGxahRtfUrHn/88TBjxoxw/PHH97h9o4026gomcR5M3CaKt+2yyy7JBP24itiy5s+fH0aOHJn8PwaXOLzsO9/5TvjBD36Q7O/FF18c3v3ud4cxY6r/mya8AJBagBFOAIZu/PjxPcJLrWKIiPNd7rjjjj63iYEjTr6vhKRbbrklGSI2b968MHPmzKQCE8Xlkl955ZWw3nrrdT12rbXWCt/+9rd7hKX29vZkWeVqCS9NYvgRUObqSySoAGTbnnvuGc4///xkEv0222zTdXsMIvESh3idffbZvT4uVl+uvfba8K//+q/JbXGifgw0sbLSvRLTvcoSl05ee+21e2wzEOEFIKfyGGAASHep5FdffTU89dRTyXLIlQrLtttuG8aOHRsOOeSQZInj/fffP1k5LA4Xi4/5yU9+Eq655pqw8cYb9/q8cZ5MHGr2+c9/Pgk5sSJz4oknhqOOOipsttlmXdsddNBByeUtb3lL+N3vfhcuuOCCcN1113UNLauG8AIA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RJ1hQVjamb19KptMh5fXX389+XeVVVbpcXu8XrmvN5MnT06Gmy1r4sSJDdhLAADqpb29PbS1tWWmQUeOHJnMuX755TNCFq255prJPpZFpsPLiBEjuibtdzd//vx+f0knn3xy+OIXv9h1febMmWH99dcPzz//fKbeDFkUq1Mx5MXV3azqpp0cU95/WeWzSls5ror3HowVlxhc1l577ZAlo0ePDs8880xYuHBhyKKRI0cm+1gWmQ4v8U3R2toaXnjhhR63x+vrrbden48bNWpUcllWDC465NWJ7aSttFM9Oaa0VSM4rrSV46pY78Gsfskcw0GZAkKWZXqp5BVWWCHssssu4dprr+26be7cueHmm28O++yzT6r7BgAANFemKy/RGWeckQSVOBRs5513Ts7xsvrqq4dPfepTae8aAADQRJmuvER77LFHuPXWW8Nzzz0Xvve974Wtttoq3HHHHWHs2LFVP0ccQvbVr36116FkaKtaOKa0VSM4rrSV4ypd3oPaiuxr6czqenQAAAB5qrwAAABEwgsAAJALwgsAAJALmV9tbKhee+218PTTTyfnjIlnIGV58QRTcUGE7oYPHx7e8Y53aK4QwtKlS8N9992XrO++zTbb9NomHR0d4dFHHw0rrrhi2GyzzUrbbosWLUraauWVVw6bbrppj/vmzJkTHnzwweUe87a3vS2MGzculM0//vGP8Prrr4eNN944OW56E0+w++STTyafXeuuu24oozgt89lnnw3z5s1L2mrZ8yy89NJL4amnnlrucbvttlsoo/hZPmvWrLDhhhv2+b6K50qL7Rbbc6WVVgplPa7icRNPgr3RRhslp2bo7u9//3v45z//udz5R/r6G1AGlffaJptsEtZYY43l7n/88ceTz/m4sJIFkmiozgL7yle+0jlq1KjOLbfcMvn3k5/8ZOeSJUvS3q3M+frXv945bty4zl133bXrsv/++3eW3cKFCzsnT57cudFGG3W2tbV17rHHHr1ud/3113eussoqyXYrrbRS5/bbb9/54osvdpbJ7NmzO0855ZTOddddt3Ps2LGdH/nIR5bb5p577omLg3S+4x3v6HGsPfroo51lcu2113ZuvfXWneuss07nNtts07niiismn1XL+va3v905evTozi222KJzzJgxnYcddlhnR0dHZ5n87Gc/69x44407N9xww6Qd4vvwBz/4QY9tvv/97yft0/2YipeyufHGG5PjKrZX/De2yfHHH9+5dOnSHp9pRxxxRNdxFf/9xje+0Vk2l19+efJ5vfnmm3duttlmyd+/b37zmz22ie+3NdZYo8cxdeyxx3aW1dy5c5O+VPwM//GPf9zjvpdeeqlzxx137JwwYULSriuvvHLnb3/729T2leIrbHi55pprOkeMGNH5l7/8Jbk+derU5A/f9773vbR3LZPhZaeddkp7NzJn5syZnSeeeGLnU089lQTf3sLLP//5z+QP39e+9rXk+vz585PO+b/8y790lklso9gGMbTtt99+/YaXGTNmdJZZ/Ax65JFHuq7/6U9/Sj6rfvWrX3Xddtttt3W2tLR0/v73v0+uP/fcc52rr7565+mnn95ZJmeddVbnM88803X9iiuuSNrljjvu6BFeYge07C688MLkfVhx1113JW119dVXd912xhlnJMfRs88+m1z/wx/+kGxzyy23dJZJDMDdv2C68sork8+m+BnVPbzEz33e8IlPfKLzS1/6UuewYcOWCy8HHHBA0oeYN29eV58ifok1ffp0zUdDFHbOy0UXXRT23nvvsMsuuyTX41CeD37wg8ntLG/x4sXhr3/9a1Iqj//njSEC3/jGN5IhBX359a9/HZYsWRK+9KUvJdfjkJbjjz8+3HjjjUmJvSxiG5166qlhrbXWGnDbOAwqDh+bO3duKKPPfe5zybCKine+851d56+qiJ9TO+20U9h3332T6+utt17413/919J9fsWTE2+wwQZd1+Nn+CqrrBL+8pe/9Nguvgcffvjh8Le//S0ZulhGRx11VI/Pqh133DEZjhiHiFXE4yceR+uvv35yPZ4AOh5nZTuuPv3pT/f4rNprr72Sf7u3VRSHKt57773JsMUyn1XiF7/4Rbj//vvDmWeeudx906dPD7/73e/CCSecEMaMGZPcFv8exva64oorUthbyqCw4eWBBx4I22+/fY/b4of5I488Uto/bgO11xFHHJEEvji+/pJLLkl7l3LTbltssUWP8dLxOIsf3L3N7yCEQw45JHz4wx9Oxtp//vOfL31YnjFjRjKO/C1vecuAn19xPkPcvqzi/MU4T6h7W0Wx/T70oQ+F/fffP6y66qrh/PPPD2U0e/bsJATfcMMN4cgjj0yCX3yvVe6L7dfbcRWPt7J59dVXk7a67rrrwkc+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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "spectrum: (20, 8)\n" - ] - } - ], + "outputs": [], "source": [ - "omega, kvec, spectrum, _ = out.em_fields.e_field_log.struphy.analysis.dispersion(\n", + "omega, kvec, spectrum, _ = out.em_fields.e_field.struphy.analysis.dispersion(\n", " slice_at=(None, 0, 0),\n", " do_plot=True,\n", ")\n", @@ -4294,27 +448,15 @@ "## Save standard output\n", "\n", "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `out.save_report()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/report/`." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "33", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wrote:\n", - " post_processing/report/scalars.csv\n", - " post_processing/report/scalars.png\n", - " post_processing/report/electric_energy.png\n", - " post_processing/report/kinetic_energy.png\n", - " post_processing/report/total_energy.png\n" - ] - } - ], + "outputs": [], "source": [ "written = out.save_report()\n", "print(\"Wrote:\")\n", @@ -4330,43 +472,15 @@ "## Comparing runs\n", "\n", "Time series accept arrays of other simulations, so comparing runs needs nothing special. Series are labelled by the run they come from, and the runs may have different time grids." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "35", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", - "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 20/20 [00:04<00:00, 4.05step/s]\n" - ] - }, - { - "data": { - "image/png": "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Q1lPNox1ZB9YT2YJScna2fBoqz4khszU0ZUrV7QiRYpotWrV0tauXavt2rVLa9++vVa5cuWUff/880/1/zUyMjLDr5H/xYUKFdIaN26srVq1Sjtw4ID20UcfaZMmTdJiY2O1GTNmqGP++++/2rlz57TLly+r1y1YsECtL126tLZkyRLt9OnT2s2bN++K4UHHv5+MxG15/woVKmjLli3Tjhw5or388suak5OTijUrx0p/LnPmzNFcXFy0Tz75RDt06JD23XffaXnz5lX77tmzR9WTlFFAQID2wQcfpDmHyZMna0WLFn3g/62vv/5a8/DwSLNPdHS0eo/PP/88zb5vvvmm5u/vryUmJqrn48aN04KDg1O2S7xSP/KQ858+fbrm6uqqLV++PGWfsmXLat27d9d2796t7d+/X/vss8+0kSNHpmz/448/NIPBoL311lvawYMH1fn7+Pio98pMbA+TkVgHDx6s6fV6rUuXLtrOnTu1bdu2aRUrVtSKFy+ufkqdHz58WHv22We1fPnyaTdu3LjrtXXr1tUWL16sbdmyRWvVqpVWrFgxdQ4WK1eu1JydndUxpExWrFihlSpVShs7duxdx2rdurW2efNmFXNCQoI2atQoVXdfffWV+mxImbi5uWnr1q3TMqpnz55aYGCgOu99+/alvJd8DlPLyHt5e3urc5k6daoql3nz5qnfhaCgIK1bt26qDLdu3aqVL19e69GjR5rj9+nTR/2OLlq0SP1+Tpw4UX0ONmzYkGa/H374QXN3d9fi4+O17HT+/PlHyguYkDlAQnZqSjUt/nVfLfSNktrRN2to299qrq16r7u2eNpQbf7nL2mzvp2mzfjpR236wuXaV8t3aF+vO6XN3nZO+21PiPb3oSvahhNh2u5zEdq/l29poTfjNGOyydqn5JB4EWn7WEf2gfVkJglRicnLrfqQGDJTR3KRKBfD165dS1l3+/ZtdTE6e/Zs9Tx9MpSR18gFeeHChbWYmJi74hByQSvHjIqKSrPdksRIkpVa+hgedvx7yUjclvffvn17yj4mk0krUKCASqCycqz055L+4lxIspI6IZPzeOWVV7SSJUtqycnmJFsu2CVBmTJlivYgo0eP1ipVqnTXerkwr169eprzkuNPmDAhZV36hOx+x+/QoYNalkTkXueYuh7k4r1///5ptktCLhfnFy9ezHBsWZE6Vsv7+Pn5aXFxcWkSAjmHNWvWpKy7fv26Wvf333+nea0k0hcuXEipI0kCvby8tI8//jhlP0nmGzZsmCYOSUIkkZEkwXIs+fzcunUrZR9JIuU9U3/OxJgxY7T69etn6Hwl8ZFjpE6qpCyrVKmSJiHL6Ht5e3trAwcOTLPPk08+qX4f5PNo8e2332p58uRJeS6JtxxfEtzUJAFt0KBBmnWbNm1S+8qXH7aUkDlZu4WOHp2PKRKuOiMKIgIFtQhAWuXlkXDv/ZM0AyLgheuat3qEwxtHLcuaNyJ1PtDy+MPZuwA8fQJQ0McDhbzdUdjHDQXlp7cb/DxdodfrWH1ERJRh0rWtadOmCAgISFmXN29e1T1JunDJoPusvGbLli1q3JeHh0ea1xoMhgzFJcd6kKwcPzPnWqNGjZRlnU6HggULpumOmJljpT6XqKgonD179q4Z76Rb5Ouvv55m3YgRI/Dhhx+qCTqky+Pvv/+OyMhIDBky5KETc/j4+Ny1ftCgQaq73f79+9X5Sfcx6W4n6+9HuoB+/vnnWL16Na5evaq6od26dQuFCxdW2/PkyaO6Bj777LOq25qUiXRJlK5olvOVLpTpz61169aqy+KhQ4dQrFixLMWW2VgtpEuiJT5RoEAB9VO6XFrkz59ffZbSd8OVrndFihRJ6Urq7e2tuszu27dPPb9+/TqOHTuGzz77LM3rmjRpoo63a9culChRQq2T7oypx0TKZ0r06tXrrrKS7pjS5dHJ6cFpgsQh+8j7pf78yucrdffNzLxX9VTlYikvid3FxSXNupiYGPWQz4SlPKQbZPrjv/HGG6q7pMQlLJ9V+dzaEiZkDiC0zbc4EXoWebRoGOKuwykuHC7xEXBLiIB70g14Jt2Ai/ZfduasS0ZBRKKgLvL+B00EEA4Yw/S4kZK8eeEMvLFT88YN+CDR3Q96zwA4eRWEh29B+OQviAI+niji444yAZ5wd8nYP0IiIso8+Vu7/oVmVo8hM2QsyMaNG9W4m9TkwrJx48ZZfo1cDLu7Zy6W1B722qwcPzPneq/ETi4is3Ks1HFaxvK4urqm2Sf9cyGJikxBLxfIkpDJTxlvkz7BSE8Sw9Rj7SwkUZJkRMYtSdIjP+vWravGAD1o0oWQkBA1HX6ZMmXUxfYnn3yixrhZyBgwudiXREjG9EkiNXXqVIwcOVKVk0idAKV+bimPrMSWlVgflLQ/rM5F6iQkdd1ZzsNyvjJ2SsbJpSZJjoyLs0j/+bW8VsZlpZaQkKCS18uXL6ckc/cjx5BEKv25pC//zLyX4R7lcr8ytJSXlIfs4+zsfFccSUlJqiws2yyfVfnc2hImZA6gYt026tsTf3//e39o5QObGA1EhwEx4Xd+hgHR4eqnKUoe19SyLiYcBuN/0wI76UwIwE0E6NIOmFWSAETeeVwAkjUdbiAvwrR8WK4FIiRPZSQVDEb+klVRsUg+VCrsDW+PtL8sRESUNS5OepT0y2NXxScD/+XiVQbop3e/hCcjr5HB/zt37kzzTXhqlv+N6S94M+phx89q3BmV1WNJi5q0CEjLkLQsWRw8ePCe+48ePVpNMCGTikir4Nq1ax8am0wQIa1p9yItWdLq9tprr2HRokV3teSkJhfO0jo3d+5cdO7cOWV9+kksZPpySRwt9y+TSSDGjh2rZnmU6yBfX1/VYiKTZ1hIS5iQJCwrsWU11kclrX2pb9UgicWRI0fUuYpChQqp850wYYKatCO9fPnyPfAzJZ9lmXHwXi2c0jL3MNKCJ8nW8ePH05StpcUqO9/rQeS9LS2g0oKYut7l85k6UZMkWj5D6b/csDbOspgbyD8P17xA/tJA8XpAxU5A7SFA85eBDp9C//Q8OA1bA6fxh2D4XyjwyhXguQPA4NVA73lqHzR7BaZaQxAf1AHRBWojxrMEEg1pp7c16DT4626jkv4Ceho2YkL8dEw+Pwi91zVG8uzOmP3eELzw7jQ8/+M6fL7mFNYcvYbQW3FZ/gdJRET2RWZgk5Ye6cIkLTJyUSTdtWS2tfSzzGXmNTLTnswY+PLLL6d8Gy8XhZZZBuV1Qi4cs+Jhx8+uc82JY0myIl3rLEnY6dOnVYvKvUi3TLnIltYfmQlPZsR7GNlHun9J8pCedKWUrn1y02i5IL9X0mAhF83SFU3uZyaJh2VWvtRd3yThkbKQFhUhF+HSzU9aOyytfhMnTsTXX3+Nbdu2qedyPyyZZVASTUkMshJbVmLNDhLfpEmTVAIo5ypdMWV2QZlh0/JFw6uvvqo+h1IOllkfpdVq+vTpqgvl/UhdS0uhfBakJUleJ58t6X4p5few7opCusJWq1ZNzf5pea85c+ao1svsfq8Hka6rtWrVUrMzSquxkJlE582bpz4PqcmMptIt8l6JoTWxhYzu5pIH8C1pfqTL3t3u+poo7k6r23+tbVHXziDpwm7kjTgI1+QY5NXFobHhCBrjCJC0RLWmnTlXCPu1IHxpCsJZ1wpwKVwJFYv4olJhL/UIzJ+HY9SIiBxM7dq11UWrXGTKRaWMaZHugJIAdO3aNcuvke5mso+0FEiLiewj34zPmDFDbZcLsGeeeQaNGjVSF9JyYZ6+a9mDPOz42XWuOXEsuWCXrmtywSpjkCT5kPFX0jp0ry5xo0aNUknclClTMtQaKMeVJG7x4sVq+vPUZCycdH+UKfklAXrYfd1kev6BAweqVh9JciQplHFHe/fuTWlJ8fPzQ/369dX4ISkDmape3tvSCioxSIIoCZgkaTKuTGL47rvvHim2zMaaHaS1R5Iy+bxK4if1t2DBAhQvXjxlH/lMSpLTs2dPNeZPkhtJUCVJkha0+5G6tXymgoKCVEurdP2TqefffPPNDMUnLU0LFy5USa3Ui3TblHKQ55L4ZOd7PSwOaaWVz7Wcs9wPT+pEPvupx0DKuqVLl2LMmDGwNTqZ2cPaQdgj+cZFsntp+ixatKhVY5FvTR7YZdFaTMlA+Ang0i7En9uJ5Is7kef2mXvuGq254ZCpFPZpQdhnCsIJp3IoVrQYGgf5o3GQn+ruaLDzSURstp4oBevIPrCeHKOO5EJZumNZJjmwkAs1uXmrXHTKhVZGXpOaXJTKe97rAltatyQu2S5jo+SiXp6n7770oBgedPz7uV/c93v/K1euqIvWe3U5y+yxUm+Xi3t5nVy8yn3DpDVBxvGkrqdvv/1WXeRLK9SDurylJq+RLn/Sipi+vKTlRMpMWvTSj9uRxEneP33iIK09kkxJAiKvl3E/6bu1SUuRJCKSBNyLtCpJeUhLSPrJWDISW0Y9KFaJUeKQ5O9hny25cbnEYbm5tiQS0pop3UalJUlIOaUv39SkPqX1TmJJ7V5xpCbJnpSVvP+9kvSMsPxeSYJ6vzp72HtdvHhRfeZS14Wck/w9Sf15l8+/lLuMO0v/pYG8r+Vznv5vj5SldGWVltbsbiGT+pPfvazmBUzIsogJWRbFRQKX9wIhu2G8uBO4tAdOSVH33PWcqYBK0PabgnDapQICytREo7IF0SjID4UzOZDcFvAi0vaxjuwD68n2sY5sx/nz51XXRmnNkQRGum3KZB0y7kbGTqVOnCXZk0kuZLyZdHPMKLnIllZI6RrYr1+/HD2f3CJ1QsYvc7OHtJBLQiaTwWS3R03I2GWRHi/3fECZVuqhPnwmE3D9pGpFQ8gumC7thi78BHTQUFJ/DSVxDd0NWwANiD3pikMnSmGJKQiheavAq0x9BFcqi7ol8yOPKz/KRERE6UmrikwEIa0j0p1NWheke1v6Wfmk6+OaNWtUl7PMdiOT48okIJKYEdkik8mkJmGxjCe1NWwhyyK2kOWg+Fuq5Uwe2qVdMF3cDUPivQemXjAF4ACCcCNfNeQp3QAVqtdHpaL5bXL8Gb8xtn2sI/vAerJ9rCPbvCCV7l/SGmbp5pW6nqQLmKyXrl4ZnUnSEUn3wAfdl65du3ZqIoqcZulmKHVjjRayWbNmqSn97+fLL79Ex44dH1s8to4tZOR43LyBMi3VQ/4lGKQVLeK0akUzXtiJhPM74XHzpGpFK6EPQwmEAbe2Avu+RtxeFxzQlUaEb3W4l6qHoJrNUaDwg++jQURE5Ohk7FHqG0un96AJIHITSXw2bNhw3+33G7OW3WSMlSVptgbp2ic3eH5QOVH2YT8vsn0ygNW/rHo41ehn/tDG31Zj0aLPbkf06e3Ie/0A8iTfhrsuETVxDLghjwXAHuCkU1mElemFcq0Gwt+Pf0CIiIjo3qQVytbuUWUNMmlNZmeepKxjQkb2yc0LKN0cnvJobb75tRZxGqH/bsKtE1vhEb4fRRPPqXujlTWeRNnj7yDu2FRsztMUydWfQa0m7eDpxptUExEREZF1MSEjx6DTQecXhMJN5TFYrYqPvol/d62Eaf88VLy9RbWeNY5dDWxbjXNbC2GNfyfkb9Af9apVhLOB90gnIiIiosePCRk5LDdPH1Rt0Rto0RsxkVdxYs1M+J78BUWSLqKkLhQlr8+Acen32LwsGJdK9kTFJl1RM/C/wc5ERERERDmNCRnlCnnyFUSVnq8C2iu4eWorwjb8gGJX/oG7Lh7NsRs4txvXzr6Huc4tkVilD5rWr4cyAeabMxIRERER5RQmZJS76HTwKdtIPZAQhfCdvyBp92wUjjqMArqbeMa4CNi/CNv3VMRS77bwrdUD7WuWQoCXm7UjJyIiIiIHxISMci/XvPBvMhRoMhRa2DGEb/oBeY7/jjzGm6hvOIr60Udxe/03WLq2IU4U7oJmTVujRfkAm7zHGRERERHZJ85kQCQNZwEVENDjY+R56RSSe8xGRKEmMEEHL10snjGsxjvXRqPgL23w5QcvYuGmQ4hNNLLciIhywMGDBzFw4EDExsbm6hjsjdwsedGiRZl6zfz58zFlypQci4koIzdMf+6553D8+HFYExMyotScXGCo3AX5h/8J/fOHkdj4JcS6F1abKuvPY1zid+i8tgXWv9cZCxbOxdWb/GdNRJSdQkJCMHv2bCQmJmbbMfft26cSrISEBKvF4MjkYvbFF19ErVq1MvW6Xbt24c8//8yxuIgycsN0b29vPP/887AmJmRE9+NTDC4tX4bHpH+BZ5YgslQnJOmc4aZLQntswdPHRiPx02r466sJOHbiBMuRiMhGXbx4USVYSUlJGdq/evXq+PHHH+Hh4ZHjsTmCt99+G126dEGJEiWsHQpRpkkL2bp167B9+3ZYC8eQET2MXq9uQp2vdHMg9gZu7ZyHhF0/IiDuDIrrwlD8+kwkz5+FvW61YQjujyrNe8Pg7MJyJSK6TxehJUuWYNOmTaoFqnHjxujdu7f6pvpRXiP7LFu2TO0jy61atUKHDh1U98MvvvhC7TNixAg4OTmhePHieOutt7Bz50588803+Pjjj/HDDz/gzJkz6Ny5M4oWLYoNGzagV69ecHFxeeDxH+RhcVve/7PPPsPcuXNx4sQJ+Pv7Y/DgwShSpEiWjpX+XNq3b69asH7++WdERESgUqVK6Nixo+oq+MYbb8Dd3R2bN2/GnDlzMH36dLi5/TeJ1a1bt1TLwciRI1GnTp17nmN4eDh+//33NC1dGzduVO8n3RhdXV1T1t++fRvjxo1T9VC3bt27jvXXX3/ht99+U8sSV6lSpdCvXz8UKlQoZR8594ULF2Lv3r2qbpo0aXJXPVy+fFm9/7lz5+Dn54fu3bsjODj4kWLLSqyS1F+7dg1PPvkk/v77b9XyKuU/dOhQ1R121qxZOHXqlPo8Shnny5fvrte2bdsWCxYsQGRkJGrUqIFBgwalfCZTxz5v3jwcOnRIfYnQrVs3NGzY8K5jderUSe139epVvPbaawgMDFSfDSlPKTOJQ45fuLC5Z1BGyedWyuLSpUsoV66cOoa0Olls2bJF/e7I5ykoKEi1Vku9WEyZMkXVlfD19UW1atXQt29f9btqMWrUKFWP0dHRKnmSMpg2bRquXLmifnfOnz+v4pZ9KlSokCa+h52j/M5JHcnvT/369WENbCEjygwPX3g3H4uAF/cibsBqnCjSAzFwh0GnIThhF6pvG4Nb75bBkZ/GIfbKMZYtEeUcYyIQcca6D4khE+Ri+oknnlAJh5eXF0qWLImXX35ZXXw9ymvi4+PRunVrdaErF8eyz4wZM9SFXv78+VG5cmW1nyQxzZo1S+laJxeB0nJWu3ZtdZEqF+JywZa+y+KDjv8ocVveX5IdWS5fvrxKGKSFTpKnrBwr/blIoiYX8kePHlXlIM8bNWqk9r1x44Z6bcWKFdV4LkuCYSH7SLIlcd3PmjVrkJycnCYBKFu2rHqtXISn9ssvv6iHbL8XiVfqRx5VqlTB7t271QW+xG4hyca7776rkma5qJ45cyYmTJiQsl2SEkl65KJdzjcsLEyV708//fRIsWUl1q1bt2Lq1Kkqsbd8EfD666+jXbt2qFevnkomJMbFixerOjEajWleKwmH7CvJuLQ+vvPOOypB0zQtZT9JvOUYkmCWLl1aJSryZYEk5emPJYm4fH7l9yBv3rwqMZXYjxw5on5K115JhuSYGSVJpbz/4cOHVblLUtagQQMVs5AvCJo3b65+h+Rz9scff6j9LAmYCA4OTilLSWglVkm0LccQ8vns37+/+vKiTJky6vMt71W1alXVsiXxS/nJ74R8mWKR0XOU95akOXXZPlYaZUlISIjUmPppbUajUQsNDVU/yQrlHxelHVr+tfbvuw007Q2vNI+LHzXWbmyZpWkJ0awnO8DfJfvAerrj+um7/uY89ofEkIk6+uCDDzS9Xq/t3bs3Zd3Zs2c1Z2dnbd26der5n3/+qf6/RkZGZvg1b7/9tubu7q6dO3cuzftdunRJ/Vy8eLE6ZlRUVJrtCxYsUOt/+OGHNOvTx/Cw499LRuK2vP+vv/6ask9MTIyWN29e7dtvv83SsdKfS8OGDbWOHTumWffss8+qfffs2ZNSTwMGDNDq16+fZr8KFSpow4cP1x5kwoQJWtmyZe9a3759e+3JJ59Ms65u3bra008/nfJ83LhxWnBw8AOP361bN61v375qOTo6WsW9Zs2aNPukvhZr2rSp1qRJE81kMqWsmzx5subj45NSnxmJLStSxyoGDx6seXh4aJcvX05ZN23aNHUOP/30U8q6U6dOqXXr169P81pZt23btpQ6OnHihGYwGFRdW7Rq1UorUaKEKhuLH3/8UfP19dVu3ryZciz5rMj7WFy9elVzc3PTRowYkeYcpGx69eqVofOV83JxcdGmTJly13opf6kXeY/PPvssZVtiYqL6XHXv3v2+x71586b6HZDfWwtvb2+tTp06aep19uzZql5Tk+PLuWX2HOUzJeV9+vS9/6Y9zPnz5x8pL2ALGdEjMrh5okr7kaj4ylac6LEea32fRrhmbqovFnUQ+VY/j/gPyiBy4Sg4XbfuLD5ERNa0dOlS9e18zZo1U9ZJa4+0Dsi2rL5GustJS4J0wUotfbe/+5GuXA+SleNn5lylu5SFdDmT95Fxb1k5VupzkUlMpPXgqaeeSrPPvVokx4wZo/aVLp5CWhmOHTumutI9iHSFk1bI9KRb2KpVq1Q3MSHHktY5aeV7EOk+Ka1Iw4YNU13bzp49m9LqlCdPHtU68sEHH2D9+vWIi4tT66W1zNKSKN3jnnnmGeh0/92iZsCAAbh586ZqHXmU2DITq4W0HqXuHmdpgWvTpk3KOmnZkq6n0jKbmpxr6q6i8lpp3V27dq16HhUVpVoChwwZosrGok+fPqp7oMRnId345HgWK1euVK1W0k0zNWmFktbDjLQUyTGkzKU7YWpyvlL+0jIn7yHlb+Hs7Iynn35axW0RHx+vusxOmjRJ1Y3EJC2K6ctSuqamrlcpW+mu+eqrr6p9pUVNjl+gQIFMn6OlC6W0LlsDx5ARZaNylWuiXOVvcfVGFBatmA+/kwvRSNsHN1Ms3E7+AtPJhbhRuhv8Or8LeP3Xz5yIKNO8iwFjzReYVo0hE2S8kZCL19Rk/Ienp2eWXyPjazI7w19qqcfu3EtWjp+Zc5VuZKnJxbl0A8zKsVKfi3R7lIvU9AnTvRIoOT+5+JdxNN9++23KeBrp3vUgcgF8r8lSpHucvI90D3zllVdU1zZJNFu0aHHfY7300kvqvZ999ln1vpJkSGIh3eEsJFGUbnAyEYOMv5JEVbrySRe269evq3JLPT5JWJ5L8pjV2LIS6/3qNv16STLkkbrO71dPci7SDVNIl1Op39WrV+P06dN31YskiPf7jFs+U++9916asZhSRpLEhIaGPnQsmfxeGAyGe8ZpOZYkVqnHk1nOQV4ryZx8dhrc6eIoXTtljJl0u5SusJJspZb+HOQLCklOpftwy5Yt1bGkS+v777+vYsrMOVo+w1Ju1sCEjCgHFPTNi+59hiM2cTAWb92HG9tmo13iChTVXYffmUWI//QvxNQei/ytJwLOaf9YExFliJMLkL+0XRWWjKGRizAZr5GaPL/fxV9GXiP7nDx58r7vm/pb9azG/aDjZ9e5ZvexChYsqC5uJXFLLfX4nfStZNLaMXHiRDWuScZnPUyxYsXUxXN6cmErk1zIhBLS8iEtIHLs+9WFXJB/9dVX+Pzzz9O0VMmYuvStkp988olalgRMYpVxVZKkyPnKpCSpExFheS6tilmJLauxPqoLFy7c1VIldSfJh5CWIJmYRMb4pZ+MQj4blolM7veZkvOVVtb0k4TIZDEyxuxh5BiSREr5SitfelLeMq5LWv5kXwvZX+pR3leSySNHjqi6tCRcUr4ZnYbeMvbMMn5QJvWQOpUkOzPnaEnWLa2tjxu7LBLlIA8XJ/RoXgdDXv4KR7qswddO/XFbc4ebFo/8u6bh5tSqiNw5XwZzsh6IyOHJpBgyI5tcIElrj+UhLTM+Pj5Zfo10c5JvymXmOwuZxc7SLcrSQpJ6oozMeNjxs+tcs/tY0irQo0cP1dols9NZujHKxAj3Ii0U0nIj3R5l8pCePXs+NDaZzEMmV7Bc0KYvN2m5kQlIpLUifQvfvVgmGhHS2iSz41nIe/zzzz8pz6VemzZtqs5NWjjkfKV7pkxwYTmOJAzSxVG6+6Xu8pmV2DITa3aQ7nOWyUiEdE+VrnmWLqiSfEpiKZ8NSTBSfzZk5kBJUO9HuuDKBBrSlTP16+QzIN1mM5KQSSIs7yGthanv2SeTY0hSZZmkQ1owLYmlfFbknKRbpZCEyWQyqRYzC5nUQ1obH0ZmHE09OYdM8CEtbJZ6ycw57tmzRyWQmf2yJLuwhYzoMdDrdWhdtThCC7yCNacHQdvwProkr4JPUhjwz0hc3jwd3l0+gmcZ60y3SkT0OMiFpLTWyDf3MhugdCuSCyr5Fl1aK7L6Grm4k4tS6a4kSYpcqMu35TIbn5DXyoWaTAEvF+XSPU2mvc+ohx0/u841J44ls/xJVzyZ4U5iP3DggJqiXUh3s9SktUVafCSBeeGFF9JMgX8/khBJPDJeR8bmpCZjfCRe6WIoY6ZSt5KkJ8nUm2++qboQSqIrsW3btk0dw3KBLfFJy5SMCZIZ82QMmYyTknqwdAG0zEwoM/lJVzhJYOSCXG4ZkLqVJDOxZSXW7CD1JPUnMwXK+cm4ucmTJ6vzspBWOuk2KS2Vsl66CEpyKK1m0p3zfqSLpSROkshJgiSxSznJFPEPGzeY+hgyvlKSfsv4NvmcSpImyZokPNItVN5DbgIuCY8kUdLFU8beCZm9tGnTpqrLqczGKK2C8sVJRhIjSeak+6ll5lFJsKU1zvLFSWbOccWKFap1zVp0MrOH1d7djkmGLx9+qXhrNW9ayLc/8u2OfBuS/o8r2Y7U9ZRkApatWoNiu99BfRxK2edkQFsU7/0h3PLz5prWriP+Ltku1pP915FctMoFbExMjLqQk2neLd3F5P+rdIGTJCj1BfSDXpO6RUEmppD3lC5c8v4WMmZELgallUXGtEgrkHT/km5mMulA6mPdL4YHHf9+HhT3/d5fJhyQLl3pu5xl5ViWVjG5mJdWCEnMpAxkGn05TzmX1PUkU9/LBax00Uw9CcSDSCvTjh071HukJxNpSPIqF+uW2w9YyEW6dDVMfR8xeV9JGiX5kgRDxvnIhbYkwxaSjMo+kjDKcS2TOFjIpa3EI2UiyaJMoZ5+LNfDYsuIh8UqdSXjoFJP2nK/z5YkXXIMS5nLRB1yrOXLl6vWWalzqW+pv3uRboByPtIdU5JVS9J9vzgspHVK6kHKSspRvrDIbAuutE7KBB7y+yGJoMSZmkw+IomzJEOWxC19fW3ZskVdU0uLlrS6SoKU+ndApr2X2NLfgkH+1sg96SzxSxmm7574sHOUBE3KTL50SV1umSGJpHzRk9W8gAlZFjEho+y4QLkVk4hVS39G8ImPUUp3Ra2LhwtOl3kW5bv/D07uXizox4gX+vaB9WT7WEe2QxIeeVgSDula1qVLF3UPLLmQTf9/SVor5GL1frNe3osketICKYlk6tYbyjpLQibJGL8ozFkyK6e0ssmN0bPqURMydlkksiLvPC7o2WcIrt7ojWW/T0OTyz/ARxeDyqdn4PrU33AleBKqtBsOnZ4tn0RElHnSYiKtZjJmRloH5AbG0qLz66+/ptnv7bffVt3v9u/fr6aAzwyZjEG6rj3q5ClEj5u0nkmX1dSttNbAhIzIRmZl7DTsLZy9OBT7Fr2OxjeXwg834LfnZZw+MBPxLd9F5fp3dzUgIiJ6WLIk3bVk0gL5Fl/GhkmXLenWlXqadVknrVwy7iij929LLf0sf/ZKWvvGjx9/3+0y7mz06NE5HodMOpJ+2vfHSe7TJt0E70fGYMm4L3un1+tVC5m1MSEjsiGlihdDqfE/4ujhPYhb/gqCE3aijPE0sLI3dm9uDJ9OHyCofOb7uRMRUe4l3RHl4vlBF9Ay4QmZJw5Jf3uB1NLfHDynWLp+pr832eMi3e4eVA4BAQGPNR5Hx4SMyAZVrFILWuWVOLhxMbw2vYmSpguoHbsZCQuaYk3+nqjU+y0UKsA/hkRERNlJunZmZQp8RyOTh9xvAhHKfrwPGZGNkr741Zp1Q4lX9uJA1ddxE15w1RnR6sYCOH9dC1sXfoRko9HaYRIRERHRI2BCRmTj9E7OqN5tIjxeOITDJQYgCQb46W6h4bG3cfH9Wji/+7+bZBIRERGRfWFCRmQnXDzzocqzX+D2oK046Gm+2WPJ5HMI/OspnPysI+KvnrR2iERERESUSUzIiOxM/uIVUO2F5djfYi5O6kqqdWVvboLh2/oI+WU8EHfT2iESERERUQYxISOyUzWadETRyTuxvOSrCNN84Awjih2fheiPqiB68zdAMseXEREREdk6JmREdszDzRUdBryI6wO34Vf3XkjQnOGZfBuea1/CrU/rQDu1xtohEhEREdEDMCEjcgAVSxZBtxdmYEmjpfhbM9+c0zv6DHTzuiNmVlcg/IS1QyQiIiKie2BCRuQgnAx69G7dENXGL8Z7BT/HAVNptT7PxXUwTa+P5OUvALE3rB0mEREREaXChIzIwRTxccfLwwfgSo8/8brhOYRqvtAjGYY938P4WTVg+9eAMdHaYRIREREREzIixyQ3lW5XtQgmvvA6ZlRZiE+TuiNWc4VT4m1g5ctInl4XOLnS2mESERER5XpsISNyYN7uznizRx00HvoRBuf9BouSG6n1hsizwPxewF8vAMYEa4dJRERElGsxISPKBWoF+mL2811xpdln6G58F/tNZcwbdn+P5JlPAJEXrB0iERERUa7EhIwol3Bx0mNsyyBMGzcQUwt/hhnG9mq9IXQ/jN82Bk6ssHaIRERERLkOEzKiXKaUvyfmDmuEpBZTMDxpIm5rHnBKuAUs6A1t1Ru8oTQRERHRY8SEjCgXMuh1GNMiCMOGjcFg149x2BSo1uu2fYbEHzsAUVetHSIRERFRrsCEjCgXCy7hix/G98QPQTMw19hSrXO5tB2J0xsAZzdaOzwiIiIih8eEjCiXk5kYP+tXF86dP8MLpjFqenyX+AiYfu4C4/qpgMlk7RCJiIiIHBYTMiJS9y3rXbs4Rox5Cc97fYJTpiLQwwSnje8i7qduQEwES4mIiIgoBzAhI6IUZQLy4ovnnsZvNWZjcXJDtc794nrEftUACNnNkiIiIiLKZkzIiCgNN2cDXulaG3mfmoV3dMOQoDnBI+6qul9ZwpbpgKaxxIiIiIiyCRMyIrqnVpUKYsjzb+MN/09x0eQPA5LhuuYV3Jr9NBB/i6VGRERElA2YkBHRfRX0dsO7o57Bika/YpWpllrnff4f3Pq8IUxXDrHkiIiIiB4REzIieug9y4a1qYn8g37Dl04DYdT08I4LgfH7lojaNotdGImIiIgeARMyIsqQ4EBf9J/4ET4v/hlCNV+4aInIu2o8rs0ZBCTGshSJiIiIsoAJGRFl6p5lEwb1x87Wi7FVq6rWFTj7ByK/aAwt/CRLkoiIiCiTcnVCFhYWhrlz52L69On4888/rR0Okd3cs6xLo+ooMPJP/OTyNEyaDvmiTyPxmyYwHvrd2uERERER2ZVcm5Bt3rwZZcuWxa+//opjx44hJCTE2iER2ZUyBX3QbcKX+CjgfURoeeFqioPTH4MRv3Q8YEywdnhEREREdsEJuZDJZMKIESMwbdo0DB061NrhENktLzdnTBgxHF8uqYBGByehtv4k3PbPQvzlvXB7eg6Qr4S1QyQiIiKyaTbXQhYVFYVLly4hOTn5gftFRkYiNjZrEwmcOnUKFy9eRJs2bTBr1iysWbMGGm92S5QlTgY9xndvhrPtfsH3yR3UOrewg0j6pjFwYgVLlYiIiMgeWshWrFihkqO//vpLJVqSNJUpU+au/bZv344hQ4aoLoZGoxFNmzZVrytUqFDKPvPmzVMJ272MHDlSJXweHh7o1asXKlWqpI5ZoUIF/PHHHzl6jkSOrHe90tgR8AXGz6mAKabp8Eq8BSzoDa3h89C1eA0w2MyfGyIiIiKbYTMtZDNmzECPHj3w3Xff3Xefq1evom3btqplSxKu0NBQREdHo1u3bmn2O3fuHI4fP37Ph7SE+fj4qAk9FixYoJK5Xbt2qVaykyc5SxzRo6hXKj/Gj5mA0Xk/wxFToFqn2/oZTLM7AlFXWbhERERE6djMV9aLFy9WP3///fcHJm3i/fffh8FggLe3N6ZOnYoGDRpg06ZNaNKkidr+v//974HvJZN55MmTB15eXuq5u7s7XFxckJiYmI1nRJQ7Fc/vga/HdMOE+UXQ7Own6Ou0FvqL22D6phH0PWYCpZpaO0QiIiIim2EzCVlGbNy4EXXr1oWbm1vKOnkuCZVssyRkD5M3b17VdbFDhw7o3r071q5di/Lly6NixYr3fc3t27fVw0Ja54SMdXvYeLecJu8vE5VYOw56sNxUTx7Oekx/ph4+XPk2xm0rh/edZ8IjNhzanC7Qmr4ErdEEQGczDfS5so7sGevJ9rGO7APryfaxjuzDo1432FVCduHCBTRv3jzNOr1er8aPybbM+PDDDzF//nwcOHAAXbp0wcCBA9Wx7ueTTz7BlClT7lofEREBV1dXWJNcQN66dUstP+gcyLpyYz0NrZUff7r3Rbe1JfGl02cI0l+GbsN7SDizGTdbTIXm7gtbkhvryB6xnmwf68g+sJ5sH+vIPty4cSP3JGTx8fGqa2F6khDJtsyQi61+/fqpR0ZMmDBBTSaSuoWsTp06yJ8/P/z9/WELWbmfn5/qykm2KbfW06Bm/qgSWAAD5xbAC8YZ6GrYCteQzQhY3AOm7rOAorVhK3JrHdkb1pPtYx3ZB9aT7WMd2YeszvxulwmZp6fnPU84JiZGbctJMt7MMuYsNblos4ULN0kwbSUWur/cWk/1Svvjl7GtMeQnH+yJWILXnX6G6+3L0M9uD12bd4C6IwCdDrYgt9aRvWE92T7WkX1gPdk+1pHte9RrBrvqkxMUFIQzZ86kWSctY1euXFHbiMh2FfP1wKLRDXGtbB90T3wTF03+0JmMwIqXgN8GAIkx1g6RiIiI6LGzq4RMpryXKerDw8NT1q1cuVLdj0y2EZFt83R1woxnaqFhk1bokPguViUHmzccXQrM7QEkRFs7RCIiIqLcmZBJkiU3bLYMipN7jslzywB7MWjQIAQGBqJv3744fPgwNmzYgHHjxuGZZ5554AyJRGQ7DHodXm5bAa/3bIgxphcwLamXecPFbcDc7kBClLVDJCIiInpsbGYM2ahRo7B9+3a1XKRIETz11FNqecSIESn3FZPp7SUJe/XVV9V09TL9/bPPPouXX37ZqrETUeb1CC6Kkn4eGPazM+LjXfCa81wgZAcwpxvQbxHgdveYTSIiIiJHYzMJ2W+//Zah/WSK+1mzZuV4PESU84JL+GLxqIbo84MBybf1eNP5Z+DSLmCuJSnzZjUQERGRQ7OZLotElDsVz++B30bUx2bf7ngtaaB55aXdwJyuQNxNa4dHRERElKOYkBGR1RXydsfC4fWxN6AH/pf0rHnl5b3AnC5AXKS1wyMiIiLKMUzIiMgm+Hm6YsGwejhapCdeThpsXnllP7SfOwOx5sl+iIiIiBwNEzIishne7s6YM7guLpbshclJQ2HSdNCFHoT2cycmZUREROSQmJARkU3J4+qEmQNqI6Jsb0w23knKrh6GNrsjEBNh7fCIiIiIshUTMiKyOW7OBnzTLxjxlftgUtJwc1J27Qi02R2AmOvWDo+IiIgo2zAhy6Rp06YhICAA1apVy75aIKK7OBv0+Kx3dTgH98WEpJFIlqQs7ChMP7UHosNYYkREROQQmJBl0qRJkxAWFoaDBw/mTI0QUQqDXof3u1WBX4NnMD5plErK9OHHYfqxPRB1jSVFREREdo8JGRHZNJ1Oh1fbV0DJ5gMxLmkMjJoe+oiTSFZJ2VVrh0dERET0SJiQEZFdJGXjW5dFtScH4bk7SZnhxikYZ7UDbodaOzwiIiKiLGNCRkR2Y2iTUmjUeSjGGp9DkmaAU+SZO0nZFWuHRkRERJQlTMiIyK70qVscT/YchueM48xJ2c2zSJrZFrh1ydqhEREREWUaEzIisjudqxdB1z7DMTZ5PBI1A5xvnUfizHbAzRBrh0ZERESUKUzIiMgutalUEP0GjMQ400QkaE5wuX0BiT+0BSIvWDs0IiIiogxjQkZEdqtRkB+GDBmJ8boXkKA5wyU6BInSfTHyvLVDIyIiIsoQJmREZNeCS/hi1NBRGK+fdCcpuwyjJGU3zlo7NCIiIqKHYkJGRHavchFvDB80HGO0SYjXnOEUfQXJs9oDEWesHRoRERHRAzEhIyKHUK2YD54dMBgjTC8iTnOBQZIyuXk0kzIiIiKyYUzIiMhhNCjth75PD8BQ46Q7SVkoTD+2Ba6fsnZoRERERPfEhIyIHErrigXQvUcfDEycjFjNFfroa9CkpSz8hLVDIyIiIroLEzIicjhdaxRF+049MCBxMmI0V+hirkH7qQMQdtzaoRERERGlwYSMiBxS//qBaNKqk0rKojU36GLCoP3UHrh21NqhEREREaVgQpZJ06ZNQ0BAAKpVq5bZlxLRYzamRRlUb9gW/RNfQpTmDl3sdWizOwBXj7AuiIiIyCYwIcukSZMmISwsDAcPHsyZGiGibKPT6fBq+woICm6pkrLbKimLAGZ3BEIPsaSJiIjI6piQEZHDJ2XvdauCQpUb45nEl3Fb8wDibgA/dwJC+cUKERERWRcTMiJyeAa9Dp/2rg6vMvXQN/EV3FJJWSQwuxNwZb+1wyMiIqJcjAkZEeUKrk4GzHgmGC7Fg9En8VXc1PIA8TeBnzsDl/daOzwiIiLKpZiQEVGu4eHihFkDa8NUsBr6Jr6KSM0TiL8F/NwVuLTH2uERERFRLsSEjIhyFW93Z/w8qA5i81dSLWWRWl4g4RYwR5KyXdYOj4iIiHIZJmRElOv453XFnMF1cNOrHJ5OfBU3VFJ2G/p5PeB8dZ+1wyMiIqJchAkZEeVKRfN5YM7gugjzKIOnEv+HG/CCLjEa+f4azJYyIiIiemyYkBFRrlUmwBOzn62DKy4l0Svhf4iAD/RJsdD/2h+4ddna4REREVEuwISMiHK1KkW9MXNALYQYiqNPwkuIhRt0MWHAwn5AUry1wyMiIiIHx4SMiHK9uqXy49t+wTijK4HxiSPM5XFlH7B8PKBpub58iIiIKOcwISMiAtC8fACm9aiClaY6+MLYxVwmB+cDO2ewfIiIiCjHMCEjIrqjU7XCGFS3ED419sCa5BrmlStfAc5tYhkRERFRjmBCRkSUypB6hfBk5UIYnzQaZ0yFAC0Z+HUAEHmB5URERETZjgkZEVHqP4o6HaZ1r4oSRQpiaNJERGnuQNwNYGFfIDGWZUVERETZigkZEVE67i4GfN+/FqI9S2Jc0miYoAOuHgaWjeEkH0RERJStmJBl0rRp0xAQEIBq1aplb00QkU0p5O2O7/rXwlZ9LXyS1MO88sgiYNsX1g6NiIiIHAgTskyaNGkSwsLCcPDgwZypESKyGdWL+WBaz2qYntwZ/yTXVuu0NW8Cp9dYOzQiIiJyEEzIiIgeMvPi2JblMDFpJI6bikGnmYDfBwERZ1huRERE9MiYkBERPcTzLYPQrEoghiVNwC3NA4i/BfzSF0iIYtkRERHRI2FCRkT0sD+Ueh0+7lkdXoWDMDZpLJI1HRB+DFg8AjCZWH5ERESUZUzIiIgyMfPi8Tx18KHxKfPK48uBzR+z/IiIiCjLmJAREWVi5kVJymbrOmFpcgO1Tlv/LnDiH5YhERERZQkTMiKiTKhWzAcf9ayOyUlD8a+pBHTQoC0aCoSfZDkSERFRpjEhIyLKpI7VCmNYy8oYljgBEVpe6BKjgF+eNk/2QURERJQJTMiIiLI482K1KlUwJuk5GDU9EHEakJYyTvJBREREmcCEjIjoEWZejC7UAO8Y+5lXnloJbHiP5UlEREQZxoSMiOgRZ178270TfjM2Ma/cNA04upRlSkRERBnihEw4evQo1q1bl5mXoFKlSmjevHmmXkNEZC8Kervh+wG10W/GEASZLqG6/iy0xSOhy18GKFDJ2uERERGRIyVk27Ztw8SJE+Hu7p6h/RMTE9GnTx8mZETk8DMvvtezNkYsGI8/Xf8H/6Rb0H7pA93Q9YCHr7XDIyIiIkfqsvjMM8/g5s2bGXp88cUXORM1EZENzrzYu2U9jEh8HomaAbrI88Dvg4Bko7VDIyIiIkdJyAIDA1GjRo0c25+IyJ6NaxmEgpWb4Q3jQPOKs+uBtW9aOywiIiJylISsVatWGD16dI7tT0Rk7zMvftSzGg4GdMU8Y0vzym1fAod+s3ZoREREZKM4yyIRUTbPvDi9b01MMwzGblNZtU5bNga4coDlTERERDmfkGmaBqPRCBNvjkpEuVRJvzx4u1sNjEp8Hle1fNAZ44GF/YCY69YOjYiIiBw9IZs5cyacnZ0xbNiw7D40EZFdTfLRpm5VDE8cjwTNGbgVAvw2EEhOsnZoRERE5MgJWbVq1TB58mS0a9cuuw9NRGRXXutQEYkFa+JV4yDzivObgZWvWjssIiIistf7kGVE7dq11cNRTZs2TT2Sk5OtHQoR2Tg3ZwOm96mBjl/GoJLxPJ51WgnsmgEUqgrU6Gft8IiIiMgGcFKPTJo0aRLCwsJw8ODBnKkRInIopfw98V63KnjX2BfbkyuaVy4fD1zaY+3QiIiIyAYwISMiymGdqxdBzzolMTrpOVzS/IDkRPMkH1HXWPZERES5XJYTsh9++AE6ne6+jyFDhmRvpEREduyNjpUQULAIhidOQDxcgKhQ4NdnAGOCtUMjIiIiexxD1qJFCyxYsCDNutjYWCxfvhx79uzB8OHDsyM+IiLHGU/WtyY6fhmLFxOH4QuXr4CQncA/LwIdP7d2eERERGRvCVmpUqXUI71Bgwahffv2uHr16qPGRkTkUEr7e+LdrpUxfmEyKhrPY4TTcmDvT0ChakCtOzMxEhERUa6SI2PIWrduje3bt+fEoYmI7FrXGkXRu1YxTDU+hU2mKuaVf78IXODfTCIiotwoRxKykydPQtO0nDg0EZHde7NTJQQV8MbYxLEIQUHAlAT82h+4ddnaoREREZG9dFk8cOCAGi+Wmtyb6+jRo1i0aBE2bNiQHfERETkcdxcZTyb3J4vF4ITxWOb2BtxiwswzLz77D+DsZu0QiYiIyNYTslOnTuGnn35Ks85gMKB48eL47bff0KhRo+yIj4jIIZUJyIt3ulTGxN+SMS5hBGa4fAZc2We+R1mXrwGdztohEhERkS0nZD179lQPIiLKmu7BRbH9bAR+3wt8aeyCsU5LgIPzzZN81BvBYiUiIsoFeGNoIiIreqtzJQQFeOITYw9s0gWbV658BTi3ifVCRESUC2R7QiaTeRiNRphMpuw+NBGRw/FwcVL3J3N1dsLouJG44lQU0JKBXwcAkResHR4RERHZW0I2c+ZMODs7Y9iwYdl9aCIih1S2QF683bkyouCBfjHPI8GQB4i7AfzSF0iMtXZ4REREZE8JWbVq1TB58mS0a9cuuw9NROSwetYqhm41i+CsVhij40dCgw64dtg8yQcRERE5rCxP6nE/tWvXVg8iIsocaSU7GHITa8JrYoZrb4ww/QIc+gWo2Bkozy+5iIiIHBEn9SAishF5XJ3wdd9guDnrMTW2A065VDRvWP48EHvD2uERERGRrbWQyY2g5V5kO3fuRHh4uJrQw6J169YYPXp0dsRIRJRrlCuYF1M6VcLkRYcxPGoQVrm9Aqfoa+aZF7t+a+3wiIiIyJZayLp164bXXnsNu3fvxqFDh3Dz5k2sWLEC27ZtUxN7EBFR5vWqVQxda5jHk32U1MO88uAC4ORKFicREZGDyXJCduDAAWzduhVHjhxRLWHNmzfHhg0bcPDgQeh0OtSsWTN7IyUiyiXkb+jbXSqjiI87vjO2w3FDWfOGP8cBcTetHR4RERHZQkJ24sQJtGjRAr6+vjAYDEhISFDry5Urh+HDh2PZsmXZGScRUa7i6eqEaT2rwgQ9xsQOgVHnDESFAqtetXZoREREZAsJWVRUFLy8vNSyv78/Ll++nLLNx8cHN25wADoR0aNoUNoPA+qXwGmtKD5N6mZeuX8ucGoNC5aIiMhBZMssi9I9USb2+OOPP7Bv3z58//33KFv2ThcbIiLKssltyyMwvwe+NXbACX1p88o/nwPib7FUiYiIcnNCVqZMGdSvX18tFy5cWN0Munv37ggODoaHhwcGDRqUnXESEeVKHi5O+KhnNZh0BoyNG4ZknRNw+zKw6jVrh0ZERETWTMiaNWuGwYMHpzx//fXXcfXqVTXJx65du+Dp6QlHNG3aNAQEBKBatWrWDoWIcolagb4Y0qgkTmrF8FlSV/PKfbOBM+utHRoRERHZ0o2hCxQogEqVKqlJPhzVpEmTEBYWpmaTJCJ6XCa2KYfS/nnwjbEjTupLmVcuGwskRLESiIiIcktCFhoaqmZXzKn9iYjo3tycDfi4V3WYdE4YFzcUyTAAt0KA1W+wyIiIiHJLQvbXX3+pLns5tT8REd1f9WI+GNmsNI5pJfClsbN55Z6ZwNmNLDYiIiI75ZTZF8gsim+++WaG95XxVkRElD2eaxmEtcfCMP1qF7R33ocg7by56+LIbYCrY47dJSIicmSZSsjk/mKxsbH45ZdfMvwaTn5BRJR9XJ2k62I1dP5qK56PH4plrq/DcPMCsHYK0I49EoiIiBw6IevRo4d6EBGR9VQq7I2xLYLw6RoN040d8ZzTEmDXd0DFzkBgI1YNERFRbp1lkYiIHo9RzUujShFvfGXsijO6YuaVS8cAibGsAiIiIjvChIyIyA45G/Sq6yIMrng+frh51sXIc8C6t60dGhEREWUCEzIiIjtVtkBeTGhTFoe1UphhbG9eueMb4OIOa4dGREREGcSEjIjIjg1tXAo1i/vgc2M3nEVRABqwZBSQFGft0IiIiCgDmJAREdkxg16Hj3pWg87ZDRMThsIkf9ZvnAHWvWPt0IiIiCgnE7Lw8HAkJSVl9eVERJRNSvl74sUnymO/FoTvjW3NK3d8DYTsZhkTERE5akK2dOlSFCtWDP/73/9w8eLF7I2KiIgyZWCDQNQt6YtPjD1xHoUAzQQsla6L8SxJIiIiR0zIOnXqhGHDhuHHH39EyZIl1fN//vkHJpMpeyMkIqKH0ut1mNajGgwu7piQMBwm6IDrJ4EN77P0iIiIHDEhCwgIwFtvvYULFy7g119/RWxsLNq3b48yZcrgww8/VF0aiYjo8Sme3wOvtKuAfVpZzDI+aV657Qvg8l5WAxERkaNO6uHk5ITu3btjzZo1OH78OCpVqoSXXnoJRYsWxYABA3D69OnsiZSIiB6qb93iaBzkh4+MvXABBc1dF2XWRWMCS4+IiMhRZ1k0Go1YtGgRRo0aheXLl6NWrVp4/fXXcfjwYVSvXh179/LbWSKix0Gn0+HD7lXh7JoHLyQMM3ddDD8ObJzKCiAiInK0hOzSpUt44403ULx4cfTt2xeFChXCjh07sHv3brz66qvYt28fnnrqKcydOzf7IiYiogcq7OOO1zpWxG6tPGYb25hXbvkUuLKfJUdEROQoCdnixYsRGBiIn376CWPGjEFISAjmzJmDunXrptmvcePGnB6fiOgx6xlcFC3LB2CqsTcuagUALRlYMhowJrIuiIiIHCEh8/f3x2+//YazZ8/ilVdeUc/vRcaRffXVV48SIxERZaHr4vvdqsDFPS9eTBpqXhn2L7D5I5YlERGRIyRkjRo1QteuXWEwGLI3IiIiyhYBXm54tX0F7DBVxM/G1uaVmz8GQg+xhImIiOw9IdM0TU3mca9HcnJy9kZJRERZ0qNmUdQJ9MUHxqcRCn/AZDTfMDo5iSVKRERkzwnZzJkz4ezsfM+HTIXv6emJNm3aYM+ePdkbMRERZeqG0e90rYxEvTsmJt7punj1sHmSDyIiIrLfhKxVq1Zo1qyZmtjjnXfeURN6fPrpp6hTp466B9nUqeYpltu2bYuIiIjsjJmIiDKhbIG8GNK4FLaZKmNBckvzSpkG/9q/LEciIiJ7TciSkpJw5swZNbW9THHfr18/PP/889i+fTuCgoKQL18+rFq1ChUrVsSCBQuyN2oiIsqU51qWQREfd7yb9DTC9dJ1MQlYMpJdF4mIiOw1Idu1axfq16+vEq80B9Tr0aFDB5WYCVmWxI2IiKzHw8UJb3WuhGh4YGL8IPPK0IPA1s9ZLURERPaYkLm6uuLYsWMwmUx3bTty5IjaLqKjo1GgQIFHi5KIiB5ZywoF8ESlAthkqobFaG5eufFDIOwYS5eIiMgex5BdvnwZvXv3xrZt29TywYMHMXnyZMyePRs9evRQsy0uX74cHTt2zN6oiYgoS97oWAkeLga8Ed8HN538geREYInMumhkiRIREdlTQubj44N//vkHJ0+eRMOGDdVEHtWrV1eTe8ybNw9169ZVk3l89tlnqFSpEhzFtGnTEBAQgGrVqlk7FCKiTCvs447xrcriNvLg+diB5pVX9gHbv2JpEhER2VNCFhISAjc3N+zfv191XVy5cqVaPnfuHJ566im1jyQujRs3hiOZNGkSwsLCVGsgEZE9GtgwEOUL5sUGUw2sdGphXrn+PSD8pLVDIyIiynWynJCtXbsWX3zxhZrEo3z58uqeY9JCZhk7RkREtsnZoMe7Xauo5UnRTyHGxQ9ITjDfMNqUbO3wiIiIcpUsJ2RFihTB+fPnszcaIiJ6LIJL5MPTdYrhNjwxMe5Z88pLu4EdX7MGiIiI7CEha9q0qRoj9vPPP2dvRERE9FhMfrI8fPO4YEVSDWzLc+eG0eveAa6fZg0QERHZekK2bNkyXLlyBQMGDIC3tzfKlSunui5aHq+99lr2RkpERNnKx8MFr7aroJZHRfRCgqsfYIwHlo5m10UiIqLHxCmrLyxZsiSGDh163+3BwcFZPTQRET0m3WoWwa97QrDzHPBa8iBMxVQgZAew6zug3kjWAxERka0mZJJwMekiIrJvOp0O73atjLafb8av0dXRv1BrVI5cDayZAgS1AfKXtnaIREREDi3LXRYtoqOj1X3HpkyZgoULF6pxZSdOnMie6IiIKMeVCciLYU1KqeUBV7vD6JYfMMYBy8YCJhNrgIiIyFYTsiVLlqiuiyNHjsSMGTOwevVq9W1ru3btVKJGRET2YUzzIBTzdUeE5oWPXYabV17YCuz+wdqhERERObQsJ2Ryc+R+/frhlVdeUa1ib731llrv6+uLli1bYv78+dkZJxER5SB3FwPe6lRZLX8TVhkXCrQ2b1jzJnDjHMueiIjI1hKyDRs2oEGDBhg/fjycnZ3TbKtYsSIOHTqUHfEREdFj0rx8ANpWLqiW+1/rBZObL5AUw66LREREtpiQGY1GuLq63nNbbGzsfbcREZHter1jReRxMeBCfB7M9R1jXnl+M7D3R2uHRkRE5JCynJDVrFkTa9aswZ49e9Ks1zRN3aOsXr162REfERE9RoW83TGhTTm1/PrZcogodqfr4urXgZsXWRdERES2kpDJzZ+7dOmiui327dsXK1aswLFjx9ChQwfEx8erbUREZH8G1C+BioW8ZFJ8DIvoC83NB0iMBpY9J9+6WTs8IiIih/JIsyzOnj0bkyZNwrp167Bo0SIcPHgQ+fLlU7Mtph9XRkRE9sHJoFf3JtPpgL03XLC6xATzhrPrgRN/Wzs8IiIih/JICZmLiwveffddhIaGIiEhQU11P3fuXPj7+2dfhERE9NjVKJ4PfeoUV8tj/g1CfOG6/3VdTE5ijRAREdnKjaFTJ2dEROQ4XnyiPPw8XZBo1PCesZ95ZcRpYM8sa4dGRETkMJwe9QAyvf2JEydUC1lqZcqU4cQeRER2zNvDGf9rXxHPLzyAny/mx/CgDigSshzY8AFQtTfg7mPtEImIiHJ3QiY3hp43bx50Oh30+rSNbYMHD2ZCRkRk5zpXL4xf94Rg25kIjAxtj6WG1dDF3QA2fwS0ecfa4REREeXeLovbt29X09uvX78eiYmJ6r5kqR8zZszI3kiJiOixky/c3u5SGU56HQ5Fe2N3wafMG3bOACLPs0aIiIislZCdO3cObdu2RbNmzeDk9Mg9H4mIyEaV9vdE//qBannkxaZIds8PJCcCa6ZYOzQiIqLcm5AFBgYiMjIye6MhIiKbNK5lEHw8nBGR5IY/vPqbV/77BxCy29qhERER5c6ErE6dOrhx4waWLFmSvREREZFNTvAhSZl46UJNxHuXNm9Y+QpvFk1ERGSNhEwSsZCQEHTt2hUFChRA+fLl0zxee+21R4mLiIhsTL96JVDKPw+SYcAnumfMKy/tAo7yizkiIqKsyvLgr5IlS2Lo0KH33R4cHJzVQxMRkQ1yNujxarsKGDx7D767GoRhxerBL3wHsPoNoFw7wMnV2iESERHlnoRMEi4mXUREuUuL8gFoVMYPW05fxwu3e+FH7ITu5gVg13dAg7HWDo+IiCj3dFm0iI6OVvcimzJlChYuXIiIiAh1o2giInLMafD/16EC9Dpgw62COF6wg3nDpmlA7A1rh0dERJS7EjIZRyZdF0eOHKnuO7Z69Wr1z7pdu3YqUSMiIsdTvqAXetcuppZHh7aH5uQBxN8CNn5o7dCIiIhyT0IWFhaGfv364ZVXXlGtYm+99ZZa7+vri5YtW2L+/PnZGScREdmQCa3LwdPVCWcTvLDOt5d55e4fgOunrR0aERFR7kjINmzYgAYNGmD8+PFwdnZOs61ixYo4dOhQdsRHREQ2yD+vK0Y1N099Py6kCYweAYDJCKx5w9qhERER5Y6EzGg0wtX13jNqxcbG3ncbERE5hkENS6JoPndEa26Y5drXvPL4cuD8FmuHRkRE5PgJWc2aNbFmzRrs2bMnzXpN07Bs2TLUq1cvO+IjIiIb5eZswEtty6vlD0KDEeVdzrxh5auAyWTd4IiIiBw9IZObP3fp0kV1W+zbty9WrFiBY8eOoUOHDoiPj1fbiIjIsbWvUgjBJfLBBD3eSuxjXhl6ADj8m7VDIyIicvxZFmfPno1JkyZh3bp1WLRoEQ4ePIh8+fKp2RbTjysjIiLHIzPrvtaholr+LTIIl/I3NG9Y+xaQFGfd4IiIiBw9IXNxccG7776L0NBQJCQkqKnu586dC39//+yLkIiIbFr1Yj7oUr2wWn4uojs0nR64fQnYPt3aoRERETn+jaFTJ2dERJQ7vfhkebg567EvviD25u9kXrnlUyA6zNqhERER5Y6EjIiIcq/CPu4Y1riUWh595QmYnPMAidHAhvetHRoREZFNY0KWSdOmTUNAQACqVauWMzVCRGSnhjctjYC8rrhm8sZSz97mlXt/AsKOWzs0IiIim8WELJNkEpOwsDA1gQkREf0nj6sTXnjCPPX9y6GNkeBRENBMwOrXWExERET3wYSMiIiyTY+aRVGpsBfi4Yov8LR55alVwJn1LGUiIqLsTsi2bt16142hL126pKbAJyKi3Eev1+F/7c3T4H99Ixg3vMzLWPU/wJRs3eCIiIgcKSG7efMmnn32WQQGBqZZX7hwYTXOau/evdkRHxER2Zn6pfOjTcUC0KDHSzFPmVdeOwIcmG/t0IiIiBwnIdu8eTMqVKgAPz+/tAfU69G5c2csXbo0O+IjIiI79Eq7CnA26LAqpgxO5WtiXrnuHSAh2tqhERER2ZQsJ2QxMTFITEy85za5SfStW7ceJS4iIrJjgX55MKC+uQfF6PAu0PROQPRVYNuX1g6NiIjIMRKy4OBgbNy4EcePp53OOCoqCnPmzFHbiYgo9xrbMgj5PJxx0lgQG73u3Cx62xfA7VBrh0ZERGT/CVlQUBA6deqEunXrYsKECfjmm2/wxhtvoEqVKnB1dUWvXr2yN1IiIrIr3u7OGN+6rFoef7UNjC55gaRYc9dFIiIievRZFmfPno3hw4dj3rx5GDVqFD755BM0bNgQ69evh5ub26McmoiIHMDTdYqjtH8eRMILc5zvfFF3YB4QesjaoREREdl/QiYtYVOnTsW1a9cQHx+vuitKclagQIHsi5CIiOyWs0GfMg3++xFNEONRFIBmngZf06wdHhERkePcGFqSMyIiovSalfNH4yA/JMIZ7yf2Nq88t9F8w2giIqJcLlMJ2YIFC1C0aFG8+OKLKcv3e8g+REREOp35ZtF6HTA3uiZCvaqaC2XVa0CykQVERES5mlNmdq5UqRLGjBmDatWqoUiRImr5fmQfIiIiUa5gXjxVpzjm77yISbd7YS4OAddPAPt+AmoPYSEREVGulamErGrVquohQkJC0K5du5TnREREDzKuZRAW7b2ELfGlcLxwS5S/sRZY/z5QpRfg5sXCIyKiXCnLY8jWrl2LL774InujISIih1XAyw0DG5hvFj02vDM0gwsQex3Y8qm1QyMiIrK/hEy6LJ4/fz57oyEiIoc2omlpeLo64VSSH7bn725eueNr4GaItUMjIiKyr4SsadOmiIiIwM8//5y9ERERkcPKl8cFQxqXVMtjL7dAsls+wBgPrH3L2qERERHZV0K2bNkyXLlyBQMGDIC3tzfKlSuH8uXLpzxee+217I2UiIgcwuBGJZHPwxkRyXnwp88z5pWHfwUu77V2aERERLY9qUdqJUuWxNChQ++7PTg4OKuHJiIiB5bXzRmjmpXBu38fw+QLtdAuoCRcbp0DVv4PePZvQKezdohERES2n5BJwsWki4iIsuKZ+iXww5azuHYb+NFjEIbfeg24uA04vhyo0JGFSkREuUaWuyxu2LABM2fOzPQ2IiIiN2cDnmsZpAri/XOlEFOwrrlQVr8OGBNZQERElGtkOSE7ffo0tm/fft9tO3fufJS4iIjIwfWqVQzFfT0A6PAR7owlu3EW2MMv9IiIKPfIckL2IMePH4efn19OHJqIiByEs0GPCa3LquUfz/vieqku5g0bPwTiIq0bHBERka2OIVu4cCEmT56M6OhoJCQkYM2aNWm2x8bG4vr169i4cWN2xklERA6oY7XC+GbDGZy4FoXXorrja6cV0Ekytukj4Il3rR0eERGR7SVkMr39kCFDsHfvXoSEhKBLlzvfaN7h5eWFxo0bo0aNGtkZJxEROSCDXocJbcpi+Jy9+CfEgIvBz6LEv98AO2cAtQcDvqWsHSIREZFtJWTVq1dXj0OHDuHy5cto27ZtzkRGRES5QpuKBVCtqDcOXrqFF0Kb49c8v0MXEw6seRPo9bO1wyMiIrLNMWRVq1ZVrWF79uxJs/7SpUtYtGhRdsRGRES5gE6nw6Qnyqvl3VeM+LfsaPOGo0uBizusGxwREZGtJmQ3b97Es88+i8DAwDTrCxcujGnTpqkujURERBnRsEx+1C+VXy1POF0Nmr85QcPKVwFNYyESEZHDynJCtnnzZlSoUOGu2RT1ej06d+6MpUuXZkd8RESUS1rJXniinFo+GR6HbSXHmTdc3gMcYa8LIiJyXFlOyGJiYpCYeO+bd8rsi7du3XqUuIiIKJcJLpEPrSoEqOXJhwrAVLKZecOaKUBSvHWDIyIisrWELDg4WE1tL/ccSy0qKgpz5sxR24mIiDJjYhtzK9mlm/H4u+AoddNo3LoI7JrBgiQiIoeU5YQsKCgInTp1Qt26dTFhwgR88803eOONN1ClShW4urqiV69e2RspERE5vAqFvNCpWmG1PGWPAcZqfcwbNn0MxERYNzgiIiJbSsjE7NmzMXz4cMybNw+jRo3CJ598goYNG2L9+vVwc3PLviiJiCjXGN+6rLo/WXhUAubn6Q84ewAJt4CNH1g7NCIiIttKyKQlbOrUqbh27Rri4+NVd0VJzgoUKJB9ERIRUa5S0i8PegYXVcsfb7+N+LpjzBv2zAKun7JucERERLaUkIno6GiVhH3wwQdYuHAhIiIicOLEieyJjoiIcqXnWgbBxaDHrbgkfGfsAOQtBJiMwOrXrR0aERGR7SRkS5YsQcmSJTFy5EjMmDEDq1evVlMXt2vXTiVqREREWVHYxx396pVQyzO2hSK6wUvmDSf+Bs5vYaESEZHDcMrqC8PCwtCvXz+8/fbbGDNmjBpPtmPHDvj6+qJly5aYP38+hg0blr3REhFRrjGqeWn8svsiYhKT8fn1YLxaoApw7TD0q18DOv9i7fCIiFLEJhpxOTIOl27G4crNOLV8+WYcbsQkqsYKvQ7Qq5+plvXmezBa1hl0urT7SrOJBiQmxCO/9w345XWFbx5X5M/jAt87j/yeLvBwyfLlPNmILNfghg0b0KBBA4wfP/6ubRUrVsShQ4ceNTYiIsrF/DxdMbhRSXy57jRm77yEEb1fR/5FPaG7ehBuJ5cBAUOtHSIR5QKapqnE6nKqRCv1siRgkbFJ2f6+LkhCHsQhjy4B8ZoLbiAvTPfo3ObmrIevhwt8PV3uTthSLcvf1CL53OFseOQRS2QrCZnRaFSTetxLbGzsfbcRERFl1JDGpfDz9gtqLNlHp4ri/aAngFMrkXfXJ0DdvoAhLwuTiLLN7fgkbD11HVvPXMeFiNiUhCs+yfTQ1+phQh7Eo1geE0p6mVDC04QA1yS4JsfCOTkWLskxd36al11NcXBNjoGLKVbt42qKhZssm+LUTycY0xw/GXpEwBthJm+Ea/LwQTi8EW7yQXiUD8JveyMEPtineSMa7ub7OKYjM9gW9/VAKb88KOUvD0+1XNI/D/w9XVULHdlRQlazZk0MHjwYe/bsQa1atdJ8i7Bs2TJMnDgxu2IkIqJcytvdGSOalsaHK47j1z0hGDPwFRQ+vQaGmGsw7ZkJNHre2iESkR2T69bTYdFYfyIM646HYc/5SLiZYlBffxRFdLdRFnHwRDzyOMXDU5Z18cjvnIh8TgnIq09Q69y0OLgYY2BIjjMfNBlA5J1HNjLAhABEIkD/8AMnwBU3dD4I07xxNdkb1yR5kyQOPgi/4Y3wCB8cPe6D6/BG0p10IK+r011JWik/TzXzrbuLIXtPhrInIStfvjy6dOmiui327NkTCQkJCA0NRYcOHdQU+LKNiIjoUQ1oUAIzt5zD9egETN2n4dNqT0F3YB50Wz8Hag0C3LxYyESUqfFe289EqCRs/fFw1QomXQNb6vfha8MONHU+BFfdA7ogSmNZYiYKXGcAXD0Bl7x3fnqm+pn3/s9d8yLZyQORMYnI5+kKQ+x1IPoaEB2W7uc1IOoakJyQ8pauSEAh7RoK4RqqPSSXitQ8zcmatLRd80b4VXPytlbzwS+SwGneMHgVQn6/AigZkBflCuZFnUBflAnwZItaNnmkUYAykUepUqUwa9YsXL16FXny5FGJ2E8//QRnZ+fsipGIiHIxGbA+tkUZvLHsXyw7eAVjBoxCmUO/Qhd3A9jxDdBssrVDJCIbd/56jDkBOxGOHWcjkGg0wQPxKgl7zXknmhsOwBWpkjCdHvDwS5soPSh5etBzZ3eZvSNrgScnwxgeDvj7A4YHZFaaBiTcvjtJu1cCFxNuni3kjny6aPUoi8v3P34CkHTJgOuXvHFRC8Dfpoo44lwFziXqoWbpgqgd6ItKhb3gxPFpWaLTpK02GyQmJsLFxQW5xaVLl1CsWDGEhISgaFHzDUytJTk5GeHh4fD394fhQb+sZFWsJ9vHOrJdCcZktPhoo/omu1WFAHye5yfkOTIXcPUCxh0EPHytHSKlwt8l++DI9SR/M3adu6FawDacCMPZ6zFqvTvi0UJ/AO0NO9BSJWGJaZOwkk2ASl2B8h2BPPnhkHWUbARiI+5uZUuTuF2FFn0NuoSoBx4qQXPGPlMQtpsq4oChEgzFa6NmyYKoXdIX1Yv5wM3ZsT5X93PhwgUEBgZmOS/Itnkyc1MyRkREj5erkwHPtwrCpN8PYc2xMOzt/AwaH18EnXwjvO0LoNWbrBKiXC4+KRl/HQrFP0euYuvp64hLksFcgBsS0FZ/AF2kJUy/Hy5aQtokLLDRf0mYpz8cnsEJyFvA/HgA1aaXGAvEhP2XrEVdhSn0EJLObITr7Quqa2d9w1H1AH5HXIgL9l4IwuZ1lfCZrhJQuAZqlCqAOiV9EVwiH7zc2IPukROyBQsWYNKkSRnat0+fPpg6dWpmDk9ERHRfXWsUwbcbz+BMeAy+2JeIRrWHQLf9S2DnDKDuyIdeXBCRYwqPSsDcHRcwb+cFXI9OTEnCntQfxNN59qB+8h64mOLNO6t+Ybo7SVgXoEInwDPAqvHbNBcPwCUQyBeYskomzVdzqd+6BJzbDO38ZhjPbIJzVAjcdYloZPhXPUTsNVfsCS2L7Zsr4UutAhIDqqNWKX+VoDUs7QdvDyZomU7IKlWqpG4CnRHVqlXLVH0TERE9iIxNmNimHEbN24c9IVHY3fQZ1HX5CUiMArZ8CrT9gAVIlIscvXIbs7aew7IDV5CYbFLdD58wHMSz3vsRnLATzjLrYcqwMB1QouF/SRi/wHl03kWB6k9DV/1pqLQq8gJwfrNK0pLPboIh+go8dAloYjisHiI60g27d5fD9p0V8T0qwTOwJlpXLoLWFQugkLdM1Z87ZSohq1q1qnoQERFZw5OVCqJCwbw4djUKn2y9gYX1RwEbPwRkCvwGY8wXCETksJJNmpqeftaWc9h+NkIlYU31h9DZdSdaG/are3kh1rK3Dihe39wdsaIkYQWtG7yjy1fC/KjRDwaZoiLynErOJEkznd0Efcw1dduA5oaD6iFuX3LHrovl8cPyiojwr4OgKvXQpnKRXDeD4yOPIYuOjsbSpUtx+vRpNRV+q1atcP36dZQrVy57IiQiIrpDr9dhbIvSGDX/AHaeu4E9TZ9GLbcZQPxNYONUoNMXLCsiBxSdYMTve0Lw47bzCI24hSb6Q/jUeQfaGPapKesVy72bi9X7LwnzKmzNsHMvSaZ8S5kfwQOglwQt4gxwfpNK0kznt0AfEwYvXRxaGfarB27Ow61NHti5oQJWuteAe9mmqF6rAWoUz6/+9juyR0rIlixZgqFDh6p7kHl6eqJdu3Zo3bq1+nnw4EG1joiIKDu1rlAApf3cceZ6HD7dcg3z5ObQa94E9s8FGo4D8pdmgRM5iJAbsfh5+3ks2n0W1RP34znDDrR23asu5NMoWudOEtYZ8C5irXDpQQmaXxnzo9Ygc4J2/SRwbpPq3ph8bjNcEiLhrYtFG8NetEncCxz5ATcOe2KDvhKiCzVAwWqtUK1mPbg6Z9uchDYjy2cUFhaGfv364e2331bjyuSeZDt27ICvry9atmyJ+fPnY9iwYdkbLRER5XryTemgOoXw6t9nsfV0BPY17Ymaeb42zwQm3Re7fZfry4jInskdmfZeiMTsLScRc2wN2ul3YoN+D7xcUvoimhWtnSoJY3dlu0vQ/Muph6HOUBhMJiD8OEznNuHm0XVwu7wdHsm34auLRgttJ3BFHp8i4m8v/Ju3BpxKN0Wp2k/Cs0jFrN/jzRESsg0bNqBBgwYYP378XdsqVqyIQ4cOPWpsRERE99Q8yAdBAZ44FRaNTzddxpzGE4EVk4FDvwKNxgMBFVhyRHYmKdmEfw5exL4NS1Dxxjq8Y9gNb+d0SViR4P+SMJ/i1gqVspteDxSoCH2BivCtNwIwmaBdO4Kww2sQfXw9CkTuhacWg/y628gfvRE4KI+3cFPvi6s1n0f5DuNyZ0JmNBrh6qomvbxLbGzsfbcRERE9Kr1OhzHNS2PcwoPYfOo69jXvgppeX8oIcWD9e0DvOSxkIjthTDbh7/UboG37Ck2Td6CTLibNFaqpUA3oK99JwlJNv04OTK+HrlBVFJBHmwmAKRnXTu3GhT0r4XRxC4LiDyOvLg4+phs4HK9HeSB3JmQ1a9bE4MGDsWfPHtSqVStNM/OyZcswceJEOKJp06aph9w5nYiIrKdt5YL4cv0ZnA6LxucbLmJ200nAn+OAY8uAKweAwtVZPUQ2bvuJyzj3xxT0iP8dLrrkO3cjBqLyVYJnzZ7QVeoCvW9Ja4dJ1qY3oEC5euohbkTFYu2ujbj571o0qN8R9k6nSQaVRU8//TQWLVqEnj17qok9QkND4ePjg8uXL2P37t1wdnbcm71dunQJxYoVQ0hICIoWtW6/ZUkOw8PD4e/vD4PBYNVY6P5YT7aPdWR/9bT88FWM++WAWr9kZB1UX9LaPNVymdZAv9+tHWquxd8l+2DNeroQEYNFv89D18sfoaT+mloX6eSH5OAh8Kvb2zw7H/F3yU5cuHABgYGBWc4L5GbbWSYTeUyaNAnr1q1TiZnMrJgvXz6sXr3aoZMxIiKyDR2qFkYp/zxq+fN154Dmr5g3nF4NXNxh3eCI6J7T13++bAf2ff4UJoROUsmYCTpcrTAQ+SYdgF/bl5mMUa7zSAmZi4sL3n33XdUyJi1kck+yuXPnqm9aiIiIcppB3ZesjFpefyIch3xaAv53JvRY+7b0o2clENkAk0nDb7sv4uOpr+OZvd3RVb9JrY/0Kg9t8FoU7P054JrX2mES2V9Clj45IyIietw6Vi2Mkn7mVrIv1p/9r5Xswhbg7AZWCJGV7Tl/A6O+/BVFlvXGG8nT1VTmiXo3xDefgnzjtsJQLNjaIRI5RkJGRERkDU4GPUY3N7eSrTkWhiNeTYBCdyb0WMdWMiJruXIzDhPm7cSm7yfh8xuj0MBwVK2PLdESLmN3wa3p84DB8W7yS5RZTMiIiMjudaleGCXye6jlz9edBlq8Zt5weS9wcoV1gyPKZeISk/HZmpOY9PE3GHniWUxw/h2uOiMS3fyBHj/CY+AiIF8Ja4dJZDOYkBERkUO1kq0+eg3/etQCipmnR8a6d9RNRokoZ6lbHx28gi4fLUeBDS9inmEKgvSX1TZTzYFwGbcHqNwN0N2Z256IFCZkRETkELrWKIJivu5q+ct1Z4CWd1rJrh0Bji62bnBEDu7wpVvo+c02rFk4HXMTxuBpp/VqfbJfeWDQKug7fQ64+1g7TCKbxISMiIgcgrO0kjUzt5Kt+PcqjrlWBUo1N29c/z6QbLRugEQO6FZcEib/fgijpy/C2NCX8IXLV/DX3YbJ4Kq6DhtGbAaK17V2mEQ2jQkZERE5jG41i6KIj6WV7NR/Y8kiTgGHFlo3OCIHs/fCDXT8bD3y7Z+OlS6T0dRwSK3XSjWDftR2oMkLgBNn4SZ6GCZkRETkMFyc/htL9vfhqzjhVBYo1868ceMHgDHRugESOYBkk4bP15zCe9/NwYy4CXjJ+Re46xKheeQHun4H3TNLgPylrR0mkd1gQkZERA6lR3BRFPZ2+6+VrPmr5g03LwL7f7ZucER27vLNOAz6di3ybXgZvzm9gQr6EPOGGv2gG7MHqNabk3YQZRITMiIicrhWspF3Wsn+OhyKU7oSQKVu5o2bPgKS4qwbIJGd+ufQFXz82VRMvTYY/Z1WQ6/TYPItAwz8C+g8HfDwtXaIRHaJCRkRETmcXrWKopC3GzRNWslOA81fAXR6ICoU2P2DtcMjsiuxiUZ88MtqOP3WF5/gExTQ3USy3hlo+hL0o7YBgY2sHSKRXWNCRkREDsfVyYCRzcxjWP48dAWnTYWAan3MG7d8CiREWTdAIjvx76UI/PTxJIw91hetDfvUuvgi9WEYuQ1o/jLg5GrtEInsHhMyIiJySL1qFUMBL1fVSjZ9/Wmg6YuAfKsfGwHs+Nba4RHZ/E2el/z9F7TvW2JUwizk0SUg3skLyR2/hNuQfwD/stYOkchhMCEjIiKH5OZswIim5laypQcu46wxPxA8wLxx25dAXKR1AySyUddvRGDVJ4PQcWdfVNadM68r1Rluz++DIbg/J+0gymZMyIiIyGE9Xac4/PO6wqQBX0krWWO5L5IbkHAL2PqFtcMjsjlH1v0C4xd18ETUHzDoNIQ7F0JMr9/h1/9nwNPf2uEROSQmZERElEtaya7gfKIXUGeoeePOb4HocOsGSGQjEiMv49jnXVB503AUxHUkaQYcLT0EfpP2IU/F1tYOj8ihMSEjIiKH1qdOcfh5uqqb2aqxZA3HAy6eQFIssOUTa4dHZF2mZNzYMB3GL4JRIXK9WnXUUB6hT61ExWc+hs7FgzVElMOYkBERkUNzdzFgeJNSavmP/ZdxMd4dqDfKvHH3TODWZesGSGQlWtgx6Bf0gP/m1+ChxeG25o6/ik9C6Ze2oHiF2qwXoseECRkRETm8vvWKI38el/9ayeqPBtx8gOQEYNM0a4dH9NglnlwL4/etEBB1VD1frauPw13XoP2g/8HV2Zk1QvQYMSEjIiKH5+HihGF3WskW7buEkDgXoOFz5o375wA3zDPJEeUGMXsXQj+/F9xMcQjXvPCR39uoPmEpGlavbO3QiHIlJmRERJQr9KtXAr55XGA0afh6wxmg7gggjz9gMgIbPrB2eESPReT6L+H+53A4wYgLpgDMLz8d44aPUrOREpF1MCEjIqJcIY+rE4Y0LqmWf98bgsuxeqDxRPPGQwuBsOPWDZAoJ2kari1+Bfk2/g96aDhiCsTelr+gd/M60Ot1LHsiK2JCRkREuUb/+oHw8XBGUrKGr2UsWfCzgFcRuVoFNrxn7fCIckayEZd/HoICB6erpzu0yojs9Qc6N6rOEieyAUzIiIgo1/CUVrJG5layX/eE4EqMBjSZZN54dCkQetC6ARJlt8RYhHzbHUXO/a6ertHVh9eQxWhc2Xx/PiKyPiZkRESUqwxoEAhvd3Mr2bcbzwA1+gH5As0b171r7fCIso0pJhIhXzyBYuEb1POlLu1QYezvqFgsgKVMZEOYkBERUa6S180Zg++0kv2yKwRXo5OBZi+bN55aCYTssm6ARNkgPuIirn7eDMWiD6nnv3oNQPPxs1HE15PlS2RjmJAREVGuM7BhILzcnJCYbMI3G04DVXoC/uXNG9e+Ze3wiB7JzYtHEDW9BQonnkeypsOvhV5El+c+g5e7C0uWyAYxISMiolzHy80ZQxqb70u2QFrJopKA5q+YN57fDJzdaN0AibIo9N9N0M16Ev6mcMRrzlheYRp6DnsFLk685COyVfztJCKiXOmuVrIKnYCCVc0b172tpgknsient/4Bn9+6wxtRuKXlwbYGP6DzU0Oh03FaeyJbxoSMiIhypbtayW4nAC1eM2+8tBs4udK6ARJlwqG/vkXgqsFwRyKuaflwuv2vaPFEF5YhkR1gQkZERLnWXa1kQa2BYnXNG9e/A5hM1g6R6KF2znsTVXdPhpPOhPMogtt9/0ZwnUYsOSI7wYSMiIhyrQe2kl09DBxbat0AiR4gOTkZ274ZgbqnPlXPjxvKwm3EKgSVrchyI7IjTMiIiChXu6uVrGRjoGRT88b17wGmZGuHSHSXuLh47P60FxpcW6CeH3SrhSLjVqNgwaIsLSI7w4SMiIhytbtayW7FAy1fN2+8fhI4tNC6ARKlcyPyBo592g71oteo53u926DihL+R18uHZUVkh5iQERFRrndXK1nRWkDZtuZy2fABYEzM9WVEtiEyPBRhXz2Bmol71fO9RZ5BzXG/wNnF1dqhEVEWMSEjIqJc756tZJb7kt28AOyfk+vLiKzv1pUziPmmJconn1TPD1aYiOChX0GnN1g7NCJ6BEzIiIiI7jWWrFBVoFJXc9lsmgYkxbGcyGpunT+ApO9boajpMpI0A3ZXfx/Vet/pWktEdo0JGRER0Z2xZIMbpWsla/YKoNMDUaHAnlksJ7KKqBMbYZjdDn7aDcRqrthZdzpqdxnF2iByEEzIiIiI7tdK5l8WqPqUeePmT4CEaJYVPVYxB5fAdUF3eGoxuKF5YnujH9Go3dOsBSIHwoSMiIjoDm/3e7WSTQb0zkDsdWDnNywremxit8+E2+Jn4YIkXNb8sLP5fLRs3Z41QORgmJARERE9qJUsXyBQs79549YvgbhIlhflLE1D3Jr34bFyAgww4bipGPa1Woi2ze7cH4+IHAoTMiIiooe1kjWZBDi5AQm3gG1fsbwo55iSkbBsIty3fKCe7jKVw7Enf0HHxrVY6kQOigkZERHRw1rJvAoBtYeYN+74BogOZ5lR9jMmIGnhQLjun6merkoOxsV2c9G1QWWWNpEDY0JGRET0oFay3XdayRqNB1w8gaQYYOtnLDPKXvG3Yfy5G5xPLDN/7ozNEdn+B/SoV5YlTeTgmJARERE9qJXMaMK3G88AefyAeiPNG3d9D9y+wnKj7BF1Dcmz2sHp4hb19AtjF6Dj5+hdz/ylABE5NiZkRERED2klm7/rormVrP4YwM0bSE4w3yya6FFFnIFpZhsYwg7DpOnwetIA+HV8G0/XLcGyJcolmJARERFltJXM3Qdo8Jx5476fgcjzLDvKuisHYJr5BPQ3zyNBc8LYpLEo12ki+tQtzlIlykWYkBEREWWmlazuCMDDDzAZgQ0fsuwoa85ugPZTO+hjwxGluWNg0mTU7zQEfdkyRpTrMCEjIiLKTCuZqyfQeIJ546FfgPATLD/KnCN/QJvbA7rEGIRrXngq8X9o16k3+tVjN0Wi3IgJGRERUWZbyWoNBvIWBjQTsOF9lh9l3K7vof0+CDpTEi6YAtA9cQqe6tQBzzAZI8q1mJARERFltpXM2Q1oOsm88d/FwJX9LEN6uN0zgb9fgA4ajpgCVTI2pFMLPFM/kKVHlIsxISMiIspKK1n1fkC+OxfSS8cCxkSWI93fod+g/TVRLe42lVXdFJ/r3AD9mYwR5XpMyIiIiLLSSubkAnS4c4Poa4eBzR+xHOneTvwDbfHwlJaxQYkvYlKn2kzGiEhhQkZERJTVVrLSzc3jycSmj9h1ke52bjO0XwdApyXjtKkw+ie+hBFP1MCABuymSERmTMiIiIiy2komWr8F+JQAtGRg8UjAmMDyJLPLe4EFT0GXnIBLmh/6Jb6M9vWqYFSz0iwhIkrBhIyIiOhRWslkGvwuX5t3CD/GWRfJ7NpRYG53IDEa4Zq3SsaqV6qENztVgk6nYykRUQomZERERI/aShbYyHzDaLH1c+DSHpZpbnbjLDCnKxAXiVuaB55JfBn+JSris6eqw6BnMkZEaTEhIyIietRWMtHyDcC3lPneZEtGAklxLNfc6PYV4OcuQPRVxGiuGJg4GUb/ivi+fy24ORusHR0R2SAmZERERNnRSubiAXT5BoAOuH4SWPcOyzW3iYkwt4zdvIBEOGFo0kSE5q2C2YPqwMfDxdrREZGNYkJGRESUXa1kxesB9Uebl7dPBy7uYNnmFvG3gXndgfDjMEKPMYljcdilOn4aVBtFfNytHR0R2TAmZERERI/YSvbp6pP/bWjxP8CvLADN3HUxMYbl6+ike+qCp1JuezApcTg26Oriu2dqoXxBL2tHR0Q2jgkZERFRFlvJRjUvo5Z/3RuCf6/cMm9wdjd3XdTpzZM7rH2L5evIjInAr/2BC1vV09eTBmCJ1hif9K6G+qXzWzs6IrIDTMiIiIiy6NmGgSjm6w5NA95ZfgyaLIiitYCGz5uXd36rbg5MDsgk954bDpxapZ5OS+qFn5OfwGvtK6JD1cLWjo6I7AQTMiIioixydTLg5bYV1PL2sxFYffTafxubvQQEVDQvLx0FJESznB2JJN9/TQD+/UM9/dbYAdOTO2N4k1IY1KiktaMjIjvChIyIiOgRtK1cEHUCfdXye38fU2PKFCfXO10XDcDNi8Dq11nOjpSMSX3u/Uk9nW9sjg+MT6NL9SKY/GR5a0dHRHaGCRkREdEj0Ol0+F8HcyvZ+YhY/Lz9/H8bC1cHmrxgXt4zEziznmXtCDZ/DGz7Qi3+mVwP/zMORqMy/pjaoxr0vPEzEWUSEzIiIqJHVLWoD7rVLKKWP197CjdiEv/b2PgFoGAV8/LSMebp0cl+7foeWPe2WtxgqoEJSaNQvpAPvulXEy5OvKwioszjXw4iIqJs8OIT5eHubEBUvBGfrUk1Db6Ti7nrot4ZuH0JWPkKy9teHVwI/G1u8dyDChiR+BwK5Mur7jWW183Z2tERkZ1iQkZERJQNCnq7YUTT0mp53s6LOHUtKtXGKkDTyebl/XOAU6tZ5vbm+F/m+8oBOKYrjYHxE+Hu4YnZg+ogIK+btaMjIjvGhIyIiCibDGtSCoW83ZBs0vDu38fSbmw0HihU3by8bCwQF8lytxdnNwC/DQS0ZFzQF0OfuEkwOnti5sDaKO3vae3oiMjOMSEjIiLKJu4uBrz4ZDm1vOFEODaeDP9vo8EJ6PotYHABokKBFS+z3O1ByG5gQR8gORHX9AXQK3Yybum88NXTNVGzeD5rR0dEDoAJGRERUTbqXK0IqhX1VsvvLD8KY/KdafBFQAWg+avm5YMLgON/s+xt2dUjwLzuQFIMbhl80TPuJVyDL97tWgWtKhawdnRE5CCYkBEREWXnP1a9Dq91MN8Q+lRYNBbsDkm7Q4OxQNHa5uU/xwGxN1j+tijiDDCnKxB/C3EGL/SMnYyLWgE81zIIT9cpbu3oiMiBMCEjIiLKZrUCfdG+aiG1/MmqE7gVl5TqP6/BPOuikxsQEwb8PYnlb2tuXQZ+7qLqJ8nggadiX8BJrRi61SiC8a2CrB0dETkYJmREREQ54KUny6v7UkXGJuGrdafSbvQLAlq+bl4+8jtwdCnrwFbEXAfmdAFuXYRJ74KB8RNwUCuDOiV98X73KupG4ERE2YkJGRERUQ4o5uuBwY1KquWftp3H+esxaXeoOwIoXt+8vHw8EJ1qAhCyjvhb5m6K109C0xnwXPLz2JpcEaX88uC7Z4Lh6mRgzRBRtmNCRkRElENGNSsNP09XJCVreP+fdNPgS9fFztMBZw8gNgL4awKgaawLa0mMBeb3Bq4eggYdpjg9h+UJ1ZHPwxmzBtaGj4cL64aIcgQTMiIiohyS180ZL7Qpq5ZX/nsN289EpN0hf2mg1RTz8rFlwJFFrAtrMCYCvz4DXNyunn7rOQo/RdWGi0GP7/rXQqBfHtYLEeUYJmREREQ5qGetYqhQyEstv/PXUXXT6DRqDwECG5uX/34BiLrG+nicTMnAH0OB02vU08X5h+DD6w3V8rSeVVE70Jf1QUQ5igkZERFRDjLINPjtK6jlf6/cxqJ9l9L9J9abuy66eAJxkcDy59l18XGRLqJy64GjS9TT7YWewfjLLdTyhNZl0bl6kccWChHlXrkyITt37hzKly+f5lG9enVrh0VERA6qQRk/tKpgvpHwtJUnEJNgTLtDvhJAm3fMyyf+Bg4ttEKUuTAZW/U/YP8c9fRU8V54+tyTarlbzSIY26KMlQMkotwiVyZkRYoUwZIlS1Iebdu2RYUK5m8viYiIcsIr7crDSa9DeFQCvtlw5u4dggcCpZqbl/9+Ebh9hRWRkzZNA7Z/pRbDAjuh7enOAHSoW9IXH3SryuntieixyZUJmYuLS5rWsZUrV2Lw4MHWDouIiBxYKX9P9K8fqJa/33wWl2/Gpd1B7m/V+SvA1QtIuAUse45dF3PKjm+B9e+qxegSrfDEuadgNOnU9PYznglW948jInpcbOovzpUrV/DJJ5/gpZdeQkREupmoUlm/fj3efvttTJs2DcePH3+k99y6dSvi4uLQsmXLRzoOERHRw4xrGQQfD2ckGE348J97/P/yLgo8+b55+fRqYP9cFmp2OzAfWDFZLSYWa4iOV4cgMgFqevsfn+X09kSUixOyzp07o169elixYgU+/PBDREZG3nO/0aNHo3v37oiKisKJEyfU2K/58+en2addu3Z3jRGzPBITE9Ps+8MPP2DQoEHsmkBERDnO28MZz7cMUsvLDl7Bvov3+F9XvS8Q1Ma8vOJl4GYIaya7HF0GLB2tFk2FaqJ/7Hicu2VS09t/378WSuTn9PZE9Pg5wUaMGjUKrVq1wuLFi7F69ep77rN27Vp8/fXX2LRpExo3Nk8RXLx4cYwcOVKNA8uXL59aN336dCQkJNzzGM7OzinLktQtWrQI//77b46cExERUXp965XAnB0XcCY8Bm8vP4o/RjZI+6WgLHf8Avi6LhAvXRfHAM8sMa+nrDuzDlg0GNBM0Pwr4EW317DjXFzK9Pa1OL09EeX2FrInnngCBoPhgfvMmzdPtXJZkjExbNgw3L59G3/++WfKupIlS963hSz1Pz1pWWvYsCGKFSuWQ2dFRESUlrNBj/+1r6iW91+8qVrK7uJVCGg7zbx8dgOwZxaL8VFc3An80hdITgTyBeKrolPx+zFzMjaR09sTkZXZTAtZRhw4cABVq1ZNs65gwYLw9/fHwYMHM3086a44ebK5H/nDSNInD4vQ0FD1Mzk5WT2sSd7fZDJZPQ56MNaT7WMd2QdHqKfGZXzRuEx+bD4dgQ/+OY5W5f3h5pzuS8lK3aE/uhS6E39BW/UaTCWbm6fHtwM2VUen10D/xxDokmKh5S2EpVWm4+NVt9SmbjUKY2TTkrYRZ26vJ7on1pF9eNTfIbtKyK5fv466devetd7X11dty6y5c+eidOnSGdpXJhuZMmXKXetl8hFXV1dYk/wxvXXL/M9FLzcYJZvEerJ9rCP74Cj1NKJeAWw9E4HQW/H4YtW/eLZOobv20dd9BX4XtkIffxPGP4YjsuNsQGf752wTdWQywnPPl/Dc9635qZsPNlT7DBNXm+OqWdQT4xsVyNL1g6OwiXqiB2Id2YcbN27knoRMaHIjx3usS9P/PoPKlSuX4X0nTJiAIUOGpGkhq1OnDvLnz69a6GwhK/fz83tot0+yHtaT7WMd2QdHqSf51/FU7SjM3xWCOXuuYUDjsijg5ZZ+L6DdR8AfQ+B6ZRcCzi+FVmcYbJ3V6yjqKvSLh0N3YYt6qhWojLPNvsJzv4QhWYOa3v77AXXg4+GC3Mzq9UQPxTqyD7GxsbknIZPE517T4cu6nE6KvLy81CM9+QNmC3/E5JstW4mF7o/1ZPtYR/bBUeppYpty+PNgKKISjPhkzWl81LPa3TtV7QkcXw4cXQL92ilA2TZA/oz17siVdXRuE/D7YCAmzPw8eCDCG76JAd/tR3SCEb55XNT09vnzuj/euGyUo/wuOTLWke171N8fu2qfDg4OVuPIUgsJCVEJWY0aNawWFxERUVbk93TFmBZl1PLvey9hxRHz+OS7tP8Y8PADjHHAklGAiWN+7mIyARunAT93Nidjzh5A1+8Q+8THGDr/X3Ujbrnh83fPBHN6eyKyKXaVkPXv3x+nT5/GypUrU9bJFPcyhqxjx45WjY2IiCgrnm1YEtWKeqvlF347hLPh0XfvlMcP6PiZeTlkB7DjGxZ2ajERwLwewPp31LT28CsHDF2PuAo9MPinPTh4yTxOSlogOb09Edkam+my+N133+Hs2bPqZs/io48+go+PD5o0aaJu9CwaNWqEl19+GT169ECfPn1Uy9g///yjpq/Pmzevlc+AiIgo86TV5ut+wejwxWZExiZh5Nx9WDy6ATxc0v2LrtARqNITOPwbsPYt882j/cuyyGVK+9+fBW5fNpdF1d5Ah08Rr3PDsJ/3YPtZ81CHV9tVQKdqhVleRGRzbKaFzNPTUyVgMovi+++/j8DAQPXc3T1tH+/33nsPGzduRKVKldCmTRuVwHXu3NlqcRMRET2qIj7u+OypGurezyeuReHVxUfuOYkV2k4FPAsAyQnAkhFAsjH3Fr6Uz7YvgZ/amZMxgyvQ8XOg6wwk6t0xat4+bD5lnkFx0hPlMLRJKWtHTERk2y1k0uKVUTVr1lQPIiIiR9G0rD+eb1kWn645icX7LyO4RD70q5fuvmMevuakY8FTwOW9wPYvgUbjkevERQJLRgMn/jI/9y0F9JwNFKqKpGQTxszfh3XHzZN6jGsZhNHNzeP0iIhskc20kBEREeV2Y1uUQbNy5lmD3/rzKA6E3Lx7p3JtgWp3vsRc/x5w7ShylSv7gRlN/0vGKnQChm1QyZgx2YTnfzmAVUevqU2jmpXG862CrBsvEdFDMCEjIiKyEXq9Dp/2qq66MCYmmzB63j7ciEm8e8cn3wfyFgaSE4ElI4HkJOSKLoq7vgdmtgFuXgD0zuYunL1+Bty8kWzSMPG3g/jrsHmmyiGNSqquilm5TykR0ePEhIyIiMiG5Mvjgm/61YSLQa+mah/3y36VbKTh7gN0+tK8HHoA2PIpHFpCFLBoMPD3C+Yk1LsYMGgFUHc4ZOCdyaRh8qJDWHrgitp9QP0SeLV9BSZjRGQXmJARERHZmKpFffBmp0pqWSam+GLtqbt3CmoF1BxgXt74IRB6CA7p2r/Ad82AI4vMz4OeAIZvAorWUk8lGXt1yWF1HzfRp25xVXZsGSMie8GEjIiIyAY9XacYutcsqpa/WHcK60+YJ6lIo8075tYik9HcddF4j+6N9mz/XOD7lkDEaUBnAFpNAZ7+xTy5ierFqOHNP//Fgl0h6nnP4KJ4p3NlJmNEZFeYkBEREdkgaeF5p0tllC+YVw2fGr/wAEJuxKbdyc0L6PyVefnaEWDTNDiExFhgyShg6WjAGAd4FgQG/Ak0el4G2qUkY+/8dQw/b7+gnnetUQQfdK+qxuEREdkTJmSZNG3aNAQEBKBatWo5UyNERER3uLsY8G2/YOR1dcLN2CR1b634pOS05VOqGVB7iHl588fA7plAjPlmyHbp+ingh5bAgXn/nd+ILUBgw5RdJBn7cMUJzNxyTj1vX7UQpvWoCgOTMSKyQ0zIMmnSpEkICwvDwYMHc6ZGiIiIUgn0y4OPe5m/BDx8+RbeWn6Pae6lK1++QEBLBv6aAHwUBMzuaJ6V8LZ51kG7cPh383ixMDlHHdDsZaDfH4Cn+VYAFp+uOYVvN55Ry09UKoDPeleHk4GXNERkn/jXi4iIyMa1qVQQI5qWVsvzd15MmcAihasn0Oc384QXBhdzYnZuk3lWwk/KAz+0BrZ9CUSeh00yJgDLJ5hnUkyMBjz8gGf+AJq9BOgNaXb9at2plElOWpYPwJdP14QzkzEismNO1g6AiIiIHu6FNmVxICQSO87ewKuLD6NiIS9ULOz13w7+ZYG+vwLxt4FTq4CjS4HTa4CkWODSLvNj1f+AglWBip3MN1T2L2f9or9xDvhtoHn6flG8PtBjFuBV+K5dZ2w8g49WnVTLTcr6Y3rfmnBx4nfLRGTf+FeMiIjIDkiXPGkNCsjrigSjCSPn7cWtuHvcEFom+qjSA+g9B5h0Bug9F6jaG3D1Nm+/eghY9w4wvQ7wVR1g7dtA6EHzjZcft2PLgRlN/0vGGj4PDFh+z2Rs1pZzeP+f42q5Qen8+O6ZYLg5p209IyKyR0zIiIiI7IR/Xld83bcmnPQ6XIiIxQu/HVQTXNyXiwdQoSPQ7Ttg0mmg7yKgZn/AI795+/UTwOaPgBlNgM+rAStfBUJ2yc29cvZEkpPM77WwL5BwC3DzAZ5eCLSeAhju7rwzZ8eFlLFzdQJ98cOAWkzGiMhhMCEjIiKyI7UCffFyuwpqefXRa5ix6WzGXujkYr6ZdKcvgYknzS1RdYYDee+0Rt28AGz/CpjZGvi0IvDXC8DZjUCyMXtP4NYl4Md25vcShWuab/Rc7sl77r5w90W8tuSIWq5Z3Aeznq0NDxeOuCAix8G/aERERHZmUMNA7LsQib8Oh2LqiuOoVtQH9UvfafXKCGmFKtnY/HjyA+DyXuDYMvNDJv6ICgV2f29+SGtauXbmMWelmgJOrlkP/NQa4I+hQNwN83NJCOXm1pIspiMtf3LD51eXHFbPqxX1xk+D6sDTlZcuRORY+FeNiIjIDm8a/WGPqjh29TbOhsdg7IJ9+Ou5xijg5Zb5g8mNlovVNj9av2W+wfTRO8lZ+HEgNgLYP8f8cPUCyj5hTs7KtARc8mTsPaSVbcP75vukQQNc8gKdvwQqdb3n7hHRCfjfkiP458hV9bxSYS/8PKguvNycM39+REQ2jgkZERGRHZKWohn9gtF5+lZcj07E6Hn7sGBYvUebAl6nAwpWMT9avAqEn7zTcvaneeKNhNvA4d/MDyd3cxdISc4kSXO7M2lIelHXzNPZn99sfl6gCtBrNpDfPI1/etIN8+U/DqlzEo3K+OGLp2vA24PJGBE5JiZkREREdiqoQF68360Kxv1yAHsuROKDf47jtQ4Vs+8NZCp9/xeAJi8AkReA48vNrWchOwFjnDlRk4fc+6xUM/MEIuXamyfpEOe3AH8MAWLCzM9rDgDafgg4u9/1VlHxSXh7+VH8usd8jzU3Zz1eaVcB/eqWgF6vy75zIiKyMUzIiIiI7Fjn6kWw/+JN/L+9e4GKslobOP6goIBCioKRoOQtywteMTUvmaUpal5Weiz9XJV0MUNXUWp+eTodv9bK8mh9pWWmpa5V+WV4K/OWKWpZmnjB1AwLVATBu4EC861n43DGRA8MwvuO/H9rzYIZmZntu9fM+z57P/vZ87cclrkJyVLT30diuja88ftz1awv0nFMwe1sWkFwpsFY8iaRvIsFe5/pzStWKtXvLIH+YVJp32cijnwRH3+R6H+JRA4r8qW3Hso0FSOPnPrT3G8VXkOmPxwpDYKr39j/AwDYEAEZAAAeTmeSdqWekh1/nDIbJ8fvPCr/6N9MOjWqXTZvGHCrSPsnCm4XskT2f12Q2nhovQnOvA5vEn/n39a+oyBFMaSgMqSr7Et5Mu2b/SaQVFrOf1zPxvJUt4Zm3zUAqAgIyAAA8HA6G/bRqPbyP1/tMyl/v6afk+Ef/iD9Im+TyX3vdK/YR3H5B4m0fqTgln3GzJI5kpaK47eNIk0flEp93hSpevVM1+7U0zL+852mrapJneoy/eFW0rzuNdaiAcBNioAMAICbQA3/KvLGkEgZ2j5c/jt+ryQdOyPLE4/K+n3HZfz9TeS/OkWUruBHcfgGirQYIvl3DZSMjAwJDg4WqVz5ij+5lJcv7317SN5Zf1By8x2mjkhMlwamjb4+V/4tAFQE5AOU0LRp0yQkJEQiIyPLpkcAACiFtvWDZNmzneXV/s0kwNdbzl/Mk3+u3CfRbyfID79lWnpsdTZs8Kwt8q+1B0wwFh7kJ5/FdDQbXROMAaioCMhKKC4uTtLT0yUxMbFsegQAgFLS9Vc6I7b++e4yqE1d89j+42dl6Affy/jPdkr62exyPcb5+Q75KCFZ+r69SXalnjaP/S2qnnwd21Wibg8q17YAgN2QsggAwE0qOKCqWZc1rH09eWXpHvkl7ax8+fMRWZtUkMY4smP9Mi+ekXrygsQt3iVbL8/OaZveGNxS7m0aUqbvCwCeghkyAABucjoLtWLsPWaPMt1Q+mxOrvxjRZJEv5MgPx3OKpP3dDgc8n87UqX3jE2FwVjfFqGyelxXgjEAcMEMGQAAFYDOhD1+z+3Sr2WoqcaopfF1xmzI7K0yuE2YTOzTVGpXr+r262sJ+9STf0rKyQvyx4nzsnrPEdmcXJCeGOjrLa891Fz6R94mXlrFAwBQiIAMAIAKJCTQV2YMay3DogrSGA8cPydf7EiV1UlpEtfrDnmkQ32pXOnqoCk3L1+Onc42AVdqVkHglZJ1QVI0CMu6IOlnc4p8v65Ngk2K4q23lGHpfQDwYARkAABUQHc3qCUrn+si8zcflhlrD8jZ7Fx5Zele+ezHFLO2LONsjqQ4A6+TF+ToqWzJy3cU67VDAqrKrQHeMjQqQoZ3qM+sGABcBwEZAAAVlO5LNrprA7OB9D9XJsmKXcdk79Ez8tIXu6/7vFv8fEzJ+vCa/hIe5C/hNf0kzPz0l7CafuJTSQr3ISNFEQCuj4AMAIAKTtMJ/3d4G/lb1Amzviz5xHkTWIVpwFXTzwRd5ncNwoL8JdDX57qvl5eXV25tBwBPR0AGAACMzo1qmzRGAED5oew9AAAAAFiEgAwAAAAALEJABgAAAAAWISADAAAAAIsQkAEAAACARQjIAAAAAMAiBGQAAAAAYBECMgAAAACwCAFZCU2bNk1CQkIkMjKybHoEAAAAQIVBQFZCcXFxkp6eLomJiWXTIwAAAAAqDAIyAAAAALAIARkAAAAAWISADAAAAAAsQkAGAAAAABYhIAMAAAAAixCQAQAAAIBFCMgAAAAAwCLeVr2xp8vNzTU/jx07ZnVTJC8vTzIzMyUnJ0cqV65sdXNwDfST/dFHnoF+sj/6yDPQT/ZHH3kGZzzgjA9KioDMTRkZGeZnVFSUuy8BAAAA4CaRnJwsERERJX6el8PhcJRJi25y2dnZsnv3bgkODhZvb2/Lo3INDLdt2yahoaGWtgXXRj/ZH33kGegn+6OPPAP9ZH/0kWdISUmRTp06ycGDB6VRo0Ylfj4zZG7y9fWV9u3bi51oMBYWFmZ1M/Af0E/2Rx95BvrJ/ugjz0A/2R995DnxgTso6gEAAAAAFiEgAwAAAACLEJDdBAIDA2XKlCnmJ+yLfrI/+sgz0E/2Rx95BvrJ/uijitFPFPUAAAAAAIswQwYAAAAAFiEgAwAAAACLEJABAAAAgEUIyAAAAADAImwM7eF+//13WbNmjeTl5Un37t3ljjvusLpJKEJCQoL88MMP0q5dO+nWrRvHyGZyc3Nl06ZNsn//fgkODjZ9VLt2baubBRdLly6VgwcPmt/9/PykYcOGct9994mPjw/Hyaa2bNlibh07dpTOnTtb3Rxc9v3335tz0l899NBD0qhRI46TzezcudNcP9SoUUN69+4tt9xyi9VNwmXz58+XEydOSFFGjBghderUkeJihsyDLVq0SJo2bWoCss2bN0vr1q1l5syZVjcLLuLj402QPGHCBJk8ebIsX76c42Mzq1atkgYNGkhMTIwkJibKu+++ay72FyxYYHXT4OLkyZOSlpZmbrt375Znn33WfLYOHTrEcbKhjIwMGTRokLz44ovy9ddfW90cuFi7dq1MnTq18PPkvOXk5HCcbET7Y+jQofLAAw+YgGz16tXSqVMn2bNnj9VNw2WZmZlXfY7mzJljrvlKihkyD6WdrheQr776qjnhqZ49e8pjjz0mvXr1MoEarFezZk1ZsWKFNG7cmBkXG48WDxw4UN566y3x9i74SnzppZdk9OjR5kRYkhEulJ1Ro0ZddbGig1Bjx46Vr776ikNvM3p+GjBggBk4hP3UqlVL3nzzTaubget47rnnzOzYvn37TH+p48ePy7lz5zhuNvH8889flW3zySefyIMPPljiawdmyDzU4sWL5eLFi/L0008XPvbII4+YDekWLlxoadvwb5r6psEY7Es/Nzqz7AzGlI7s6wW/ngxhT1WrVjWfr127dlndFPzFxx9/LD/99JNMmzaNYwO4ITU1VebOnSt///vfC4MxpRf5msEBe1q5cqXJDtDJkZJihsxDbd++3XwoAwICCh+rXLmytGzZ0vwbgOIpKmBOSkoyP+vXr89htCmHwyE7duyQVq1aWd0UuEhJSZHY2FgzMKgDhLCnCxcuyIcffij5+fkm9bdLly5SqRJj9HZKK9XaAD169DBLHfRzpan1er9KlSpWNw/XWVMWEhIi0dHRUlIEZB4qPT29yBQ4HUnRQh8A3JOVlSVTpkyRu+++m9Rfmzlz5ox88MEH5mJSL1h8fX1l1qxZVjcLLkGyppbqxYg7FyQoPxp8bdy40fTZpEmTzODT559/zuyLTRw+fNhkbQwfPlyys7OlefPmMn36dBOM6brnevXqWd1EFLGeTGfINI3enWJTBGQezMvLq8jH9AsWQMlpmuLgwYPNBT9FPexHR4x1/ez58+dNcKYXlZraEx4ebnXTICLvvPOOKbiiF/awL13bFxcXZ9J+nYNQUVFRMnLkSFMgDNbTmUtdj6RVfz/99FPzmH7vtWjRQsaPHy9ffPGF1U3EX+h62UuXLrmVrqgIyDzUtWbC9IvVNd8YQPHoyU8rWmka3Pr16yn/bNMiOa6FCPQCUi8uNZ3HeXEJ62hhnDZt2si8efMKH9MLFC2co/02ZswYs2UBrKUX9a6CgoLkySefNAXCtJqpfs5gLWcGlJZOd6pWrZoZMHT9fMFea2c7dOggzZo1c+v5JAx7KF038euvv8qff/5Z+JjOjO3du1ciIyMtbRvgiaORmmqlaXBaortt27ZWNwnF0K9fP7OAWtN7YD2tTKqpVa4loPW8pCP7zt9hT86iRjoLDetpBVlnbQBXep8+sh/dikAHc92dHVMEZB5qyJAh5uSm5TWdli1bZtaWac4xgOLTkfslS5aYLQp0nxfYi37XFbX3zjfffGMKG5GyaA+616LOhLnedM2LbuCtv/v7+1vdRIiY/RZd6cCuFiPQmbOi1qaj/Ol5SIt4fPnll1dkceg5SqvLwl501lK/34YNG+b2a5Cy6KF0Aa6WFB43bpypCKfpOrNnz5aJEycWjqzAevv37y/cDFpPeloK2nmRonuMwHozZswwn50+ffqY/tGbk+4l4m76AW5sQKYjj6GhoaaSrN7XggR6YamV4rjQB4pPrxu0II6ml+r2OboeST9TOigFe9CZME2B0/OSZgHo954WjNDZZj1nwT40UNb1Y5pOWprKsl4Ocgg8mpa4101RdQpbRyG1dC3sQ/exKmpfOA2gp06dakmbcKX4+HhJSEgo8rDoaFe7du04ZDaxbt062bZtm/m+i4iIkP79+1Na3eZefvllc17q3bu31U2Biw0bNsjWrVvN702aNDHpv5RTt5+jR4+afWe1gp9u0aIX/QxA2YuuYda9THW9X2mWDBGQAQAAAIBFWEMGAAAAABYhIAMAAAAAixCQAQAAAIBFCMgAAAAAwCIEZAAAAABgEQIyAAAAALAIARkAAAAAWMTbqjcGAAAAgOL67bffZOHChWbT7EmTJkm9evWkPK1Zs0YSEhLk/Pnz0r17d4mOjr4hr8sMGQAAJeBwOOTUqVOFt5JwfZ6+DgCgeAYMGCD333+/7NmzR95//31JT0+X8jRq1CgZOnSo5OXlSWBgoMTExMjIkSNvyGt7OTgjAABg5OTkyMWLFyUgIOCaRyQtLU1CQ0OlWrVq4u3tXaKgrEaNGpKbm2tGV1NSUiQsLIwjDwDFsG3bNmnfvr2sW7fOBGY//vijtGvXTsrDsmXLTED43XffSdeuXc1j+/btk+bNm0t8fLz069evVK/PDBkAAJc988wzMmjQoGIdjxUrVrg1Q6bPAwCUTFRUlHh5eRXrb7ds2SKvvPKKxMbGyrx58+TSpUulOtybN282A3XOYEzdeeed0qBBA1m0aJGUFgEZAKDCc6Yh6km8devWpU4r1Jk2AED5i42NlW7duklGRobJZpg+fbrce++9JjvBXT4+Piao++s5QTMqEhMTS91mAjIAQIWXlJRkFof/8ssvMmvWLImIiJAmTZqU6ASen58vL7zwgtSqVcukJtatW1emTJlCcAYA5WTlypXy9ttvm1kx/S6fMGGCmd06cOCAecxdPXv2lOzsbFmwYEHhY6tXr5Y//vhDMjMzS91uqiwCACq8Zs2amRO5pqNoUKbBVEnNnz9fPvroI9mwYYO0bNnSjM7qwnNdZ9CqVasKf4wBoKwtWbJEQkJC5NFHHy18TAtwaCq6/tvo0aPNY+PGjTMB1vU8/PDD0qNHD/O7VlScPHmyPPHEE7J48WIzY6Zr2Dp37izJycmlbjcBGQAAIrJjxw6pU6eOW8GY0jLMtWvXlrvuusvcDw4ONidwAED5yMjIMOvMnnrqqSse3759u5w5c6bwfosWLf7jujL9Dnf12muvmUqLeq7QjIg5c+aYAO1G1EckIAMA4PIJu02bNm4fCy1/rLNkjRs3NhW3unTpIr169TKjswCAsle3bl1TAfevWQl6v3r16oX3H3/8cbdev2HDhuamdIZNqy6OHz++lK1mDRkAAIaOerZt29bto6Fr0DQ9Udcp6EzZG2+8YSpwaVoLAKDsjRo1So4dO2YG13SWzHnTlMXSbjOi5faddFZs4sSJUqVKFRkzZkyp280MGQCgwtORTl07phUWS0PXFehaA71pyWUdlZ07d67ZOwcA4L7Zs2fLzp075ciRI+b+66+/btIK77nnnsI1Yx06dJD33nvPZCfoAJsGYbrn4+HDh021xdLQgh4ahGla+s8//yznzp2TVatWSVBQUKm7lYAMAFDhOdcAaCWurKwsM+rpmt5SHHFxceY50dHR5iJAT9h6ERATE1Phjy8AlNbtt99ufupAV9++fQsfDw8Pv2qWbODAgbJx40Y5ceKEeZ4Gan5+fqV6f01J14BQy9xrAKil9XUQ7kbwctyIlWgAAHg4LZE8Y8YMSU9Plz59+lxzs8+0tDSzt823335rZsKcTp8+LTNnzpSlS5eaAh96kTBixAgZO3bsFc/XKoy6J46O2pY2hQYA4PkIyAAAKAFnQLZ8+XKTKqN7jhWXbjadkJBgin4QkAEACMgAACghnUHTTaNdg6zicg3edDPq2267jeMPABUcM2QAAAAAYJFKVr0xAAAAAFR0BGQAAAAAYBECMgAAAACwCAEZAAAAAFiEgAwAAAAALEJABgAAAAAWISADAAAAAIsQkAEAAACARQjIAAAAAMAiBGQAAAAAYBECMgAAAACwCAEZAAAAAIg1/h/gQuT3bpLuYQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "sim_coarse = Simulation(\n", " model=build_model(),\n", @@ -4393,7 +507,8 @@ "## Other models\n", "\n", "The interface is the same for every model; only the products differ. Two more short runs show the two product types the Vlasov–Ampère demo does not have: SPH densities, and vector fields on a mapped domain." - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -4403,81 +518,15 @@ "### SPH densities\n", "\n", "A standing sound wave discretized with SPH markers. `KernelDensityPlot` reconstructs the density on a grid, which appears under `out.densities`, while `BinningPlot` produces the binned quantities under `out.distributions`." - ] + ], + "outputs": [] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "38", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 80/80 [00:01<00:00, 40.45step/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "No post-processed data in /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave, processing with default options (call out.process(...) to choose them)\n", - "\n", - "Post-processing path /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave\n", - "\n", - "No feec fields found in hdf5 file, skipping post-processing of fields.\n", - "Evaluation of 3 marker orbits for euler_fluid\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "100%|██████████| 81/81 [00:00<00:00, 1507.65it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of distribution functions for euler_fluid\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "0 starting post-processing of distribution functions for /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave/post_processing/kinetic_data/euler_fluid ...\n", - "100%|██████████| 1/1 [00:00<00:00, 450.90it/s]\n", - " 0%| | 0/1 [00:00 Date: Wed, 16 Sep 2026 17:22:14 +0200 Subject: [PATCH 034/193] Added ProductCatalog --- src/struphy/post_processing/output.py | 45 +- .../tests/test_output_accessors.py | 13 + tutorials/tutorial_post_processing.ipynb | 4085 +---------------- 3 files changed, 113 insertions(+), 4030 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 6f9912551..e4f5b6f19 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -38,11 +38,43 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self._loaders) + def __contains__(self, key: object) -> bool: + """Check the catalog without loading a product.""" + return key in self._loaders + def clear_cache(self): """Drop loaded arrays while keeping product discovery information.""" self._cache.clear() +class ProductCatalog(Mapping[str, xr.DataArray]): + """A relative, lazy view of a :class:`ProductMapping` subtree.""" + + def __init__(self, mapping: ProductMapping, prefix: str = ""): + self._mapping, self._prefix = mapping, prefix + + def _full_key(self, key: str) -> str: + return f"{self._prefix}/{key}" if self._prefix else key + + def __getitem__(self, key: str) -> xr.DataArray: + return self._mapping[self._full_key(key)] + + def __iter__(self) -> Iterator[str]: + prefix = f"{self._prefix}/" if self._prefix else "" + return iter(sorted(key[len(prefix) :] for key in self._mapping if key.startswith(prefix))) + + def __len__(self) -> int: + prefix = f"{self._prefix}/" if self._prefix else "" + return sum(key.startswith(prefix) for key in self._mapping) + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and self._full_key(key) in self._mapping + + def clear_cache(self): + """Drop cached arrays in the underlying catalog.""" + self._mapping.clear_cache() + + class ProductNamespace: """Hierarchical, discoverable attribute view over product names. @@ -55,6 +87,11 @@ class ProductNamespace: def __init__(self, mapping, prefix=""): self._mapping, self._prefix = mapping, prefix + def __repr__(self): + location = self._prefix or "products" + products = tuple(self.catalog) + return f"{type(self).__name__}({location!r}, products={products!r})" + def __getattr__(self, name): key = f"{self._prefix}/{name}" if self._prefix else name if key in self._mapping: @@ -62,10 +99,12 @@ def __getattr__(self, name): prefix = key + "/" if any(product.startswith(prefix) for product in self._mapping): return type(self)(self._mapping, key) - raise AttributeError(f"{name!r}; available products: {tuple(self._mapping)}") + location = self._prefix or "products" + raise AttributeError(f"{name!r}; available names under {location!r}: {tuple(self)}") def __getitem__(self, key): if "/" in key: + key = f"{self._prefix}/{key}" if self._prefix else key return self._mapping[key] return getattr(self, key) @@ -82,8 +121,8 @@ def __dir__(self): @property def catalog(self): - """Flat lazy catalog for algorithms that do not know product names.""" - return self._mapping + """Flat lazy catalog below this namespace, with names relative to it.""" + return ProductCatalog(self._mapping, self._prefix) class FieldProducts(ProductNamespace): diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 5ca3f84f0..43ca238f3 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -141,6 +141,19 @@ def test_products_of_one_species_sit_on_the_output(run): run.electrons +def test_product_namespaces_expose_a_scoped_lazy_catalog(run): + products = run.kinetic_ions + assert tuple(products.catalog) == ("e1_v1_density/f_binned", "orbits", "view_0/n_sph") + assert "e1_v1_density/f_binned" in products.catalog + assert "em_fields/E" not in products.catalog + assert "e1_v1_density/f_binned" in repr(products) + assert run.distribution_catalog._cache == {} + assert products["e1_v1_density/f_binned"].dims == ("t", "e1", "v1") + assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f_binned"] is products.catalog[ + "e1_v1_density/f_binned" + ] + + def test_arrays_plot_themselves(run): phase_space = run.kinetic_ions.e1_v1_density.f_binned assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index a189c3e27..650749bb6 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "1", "metadata": {}, "outputs": [], @@ -58,21 +58,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "3", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" - ] - } - ], + "outputs": [], "source": [ "def build_model():\n", " model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", @@ -110,26 +99,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "4", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 40/40 [00:09<00:00, 4.26step/s]\n", - "Raw output: /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo\n" - ] - } - ], + "outputs": [], "source": [ "demo_tmp = tempfile.TemporaryDirectory(prefix=\"struphy_postprocessing_\")\n", "demo_root = demo_tmp.name\n", @@ -168,80 +141,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "6", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "Post-processing path /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo\n", - "\n", - "Reading hdf5 data of following species:\n", - "em_fields:\n", - " e_field: \n", - " phi: \n", - "Creation of Struphy Fields done.\n", - "\n", - "Evaluating fields ...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "100%|██████████| 41/41 [00:00<00:00, 142.83it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of 12 marker orbits for kinetic_ions\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "100%|██████████| 41/41 [00:00<00:00, 1353.10it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of distribution functions for kinetic_ions\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "0 starting post-processing of distribution functions for /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/vlasov_ampere_demo/post_processing/kinetic_data/kinetic_ions ...\n", - "100%|██████████| 1/1 [00:00<00:00, 546.99it/s]\n", - " 0%| | 0/1 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.plot.scalars()" ] @@ -333,26 +226,7 @@ "execution_count": null, "id": "14", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "growth rate: -202872114.0880568\n", - "growth rate: -202872114.0880568\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2QAAAIgCAYAAADqVqoGAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAsTZJREFUeJzs3Qd0VMXbBvAnvffeCBB6CxA6goj0jgVQAVEUQeyK9bP3P1ZUioqioCKCFEFsNAHpJaEFEkJCQkJ67+0774QNSQiQhCS7m31+59yTzd3N3dl7Q9hnZ+Ydo9LS0lIQERERERFRozNu/KckIiIiIiIiwUBGRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGRERERERERawkBGRERERESkJQxkREREREREWsJARkR1tnXrVqxevdogz2BsbCzeeustbTeD6llISAgWL158Q8f46aefcPLkSTQFBw8exPr167XdDL06p7pyzohIfzCQEVGdMZDpXiBjUNRuIDt37hzmzZsHf39/NAXyOmbPno2kpCSttUHfzqkunDMi0i+m2m4AETUNMTEx+P777/Hiiy/ecMjbv38/3N3dcfvtt8PBwaFOjy8pKcEbb7xxxeNfeeUVGBvr92dRKSkpWLBgAR577DE4OzvX+Tj//fcf/vrrr6ve36dPH4wYMaLGxzt69Ki6FrNmzbrmvoZw5swZ/Pjjj+jbty+GDx9e6fm3b9+OJ554Ao3ho48+wt133w0bGxv1vbwp//zzz8vvHzNmDHr06HHFz50/fx6bNm1CUVERbrvtNvj4+NTo+Wp6/Lr+vJubmzqfX3zxBV599VVoQ9VzKhISErB582YkJycjMDAQt956a42PZwjnjIj0i36/KyEinQpk77zzzg0d45lnnsFdd92FCxcuYNmyZejatSvi4+Pr9HgJZK+//nqT/JRaApm8Nvlalbe3N/7v//6v1sfMyspSx7x48WKd2yXh58svv7zuvoYKZNL+qVOnIj09vdLzf/LJJ2gMEqZ++OEHTJo0qdr7pR0ynK0q+ZmOHTvi33//VcFs9OjRCA0NrfXzX+34N/rzd9xxh/r3pQ3VndMVK1agX79+2LZtG6KjozFt2jSMHz9e/ZuvraZ4zohI/7CHjIhqLC8vD7/++isSExNx0003le/PycnB119/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G651/KqSk5PVsDwZaqchr10KY0jAqOlru1pb4uLiVEiRUCbtktciwxhr0oN0reer7pzI8aWgiMzjkjmAN3IsDelllF5LCwsL+Pr6qoIgcoxffvnlisf26dNHBb2Kc/WIiKh6DGRERER0TbJItPSsacKq9JjJUEwJY7fffnulx/7zzz8YOnToNecIEhHRZSzqQURERNfUsmVLFbykx1B636S37JNPPqkUxqRHTuabnT17Fs8++yzDGBFRDbGHjIiIiGokMTERhYWFarhqVfn5+SqUSVEVKSJCREQ1w0BGRERERESkJayySEREREREpCUMZERERERERFrCQEZERERERKQlrLJYR1Jl6tixY2rysqkpTyMRERERkSEqKipSRY86d+4MS0vLWv88k0QdSRjr1atXXX+ciIiIiIiakP3796Nnz561/jkGsjqSnjHNiffy8oI2ZWRkIC4uTpUZNjbmKFRdJev2pKWlwdHRkddJR/Ea6QdeJ93Ha6QfeJ10H6/R1eXk5MDe3h42NjbQtpiYGPTt27c8H9QWA1kdaYYpShjz9fWFNqWnp6O0tFS1hYFMt/+oWllZwdnZmddJR/Ea6QdeJ93Ha6QfeJ10H6/R1WVnZ8PBwQG2trbQtuLiYvW1rtOY2J1CRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGRERERERERawkBGRERERESkJQxkREREREREWsJARkREREREpCUMZERERERERFrCQEZERERERKQlDGREREREpNe2bNmCs2fPGnwbSD8xkBERERGRXnvsscewYcOGej3mP//8g4iICK22gQwDAxkRERERURVz587Fpk2banxebr31VrRq1YrnkWrNtPY/QkRERETUeEpLS3H8+HHExcXB19cXHTp0qJefkcecOnUKsbGxaN26Nfz9/cuHQObn5yM0NLQ8lPXv3x+Ojo74888/0a5dO3U7JCQERUVFuOWWWzB+/Hg0b968Rsevabt9fHzg4eFR6X7N87u6uqpjSzs7d+4Me3v7Wp+Dq70WERMTo47v5OSErl274sCBA7C1tVXPVVxcrH5Wbvv5+VU6ZnBwMDIzM3HTTTdd97VSGQayJmDuzydwITUL9tbJsDIzgZWZMazNy75qvrdS31e8bVzlsWW3bS1MYWJspO2XRERERPWssLgEsen55d+XlpQgLb0AWciFkXHjDJrydrCAmUntnuvMmTO47777EB8fj44dO+LEiRPo3r07li1bBmtr6zr/jIStBx54QIWVTp06ITo6GkFBQfjyyy+xfv165OXl4fDhw0hLS1OPb9u2rQous2bNQs+ePVVYkR6xFi1aqBAjQxbleE8++eR1j29kZFTjdsvPrlixQoUhIc/fq1cv9VgJgHL8lJQU/Pzzz+r4tTkHV3st8+fPV5sEsYKCAhXscnJyVMj6+OOPYWJigk8//VQFTHk9GhLU7rnnHtx2220MZLXAQNYERCTnIDa9EEgtvOFjmRgBLrbmcLc1h5t8tZPN4vLtS/ttLUyu+seEiIiIdI+EsfFfHtZqG9bP6g5/Z6saP17CwOTJk1Vv0KFDh+Dg4KDCx7Bhw/DZZ5/hueeeq9PPSNi68847Vcj6448/ysPOqlWrVPhYsGCB6gG6++678eijj17xHNKbtHXrVtXrVJ3rHb+691DVtTspKQlDhw7F559/jueff778sadPn8bff/+tes9KSkowadIkvPnmm1i3bl2tz1vV1yIh9N1338W3336rev3E4sWL8cILL1QKWQ8++CAeeeQR9VgXFxe1T86Z9AZKEKSaYyBrAu7v64uouGSYWlojr6gUuYXFyC0sQU5Bcflt9bWgBHmFxcgpLEF+UUm1xyouBRIyC9R2LdKbJgGtLKhZVApwHnYWaOFiBTtL/noRERHRjRXWiIyMVCFCQoVwdnbGnDlz8N5771UbyGryM/IYGZK3evXq8rAkJNjUxJQpU64axjRtqO3xr9buGTNmXBHIJChqhjIaGxur+WvSY1WX81b1tUholGGMmjCmCV8ffvhhpfbK/S+99JLqvXv88cfVvm+++QaDBw9WPW1Uc3zH3ATc2c0LMW7F8PLyUv8oa6K4pFSFs/KwdinApeQUIjGrLJDJpm5n5SMxswCZ+cXlPy+Pj0rJU9vVeNlboLWbNVq726CNuzVau9mgmbMVTDkkkoiIqNHJcEHpoao8ZDEdjg4OjTpksTbOnTunvl68eLFSgY3k5GTVe5Senl4eOGrzM1FRUWrYnczrqotrhTFRl+NX127p/ZKeraqvVcJVRRYWFsjNzb3msa523qq+Fml71UAlr6Xq/DczMzNMnz5dDYGU4Zryc9LTJgGNaoeBzEDJPDEbC1PY1OLvYm5BMRKyCi4HtktBTfZpwptshdLNBiAuI19t/55NLT+GuYkRWrqWhTMJaa0ufXWxMW+Il0lERESXyNytisMF5c2+LXLh7GxV4w90G5udnZ0a3rd27dor7hs1apQaGlg1kNXkZ6QAhsx3ysjIUPPC6ltdjl9du2V4Y2FhIUaOHFnta63Nsa533jQk7Em4qkpCXFUyNPGTTz5RRVB27twJb29vDB8+vEZtpMsYyKjGpBiI/CG/1thv+cMhoSw8KQfhCTk4k5iNsIQcNc9NglpBcSlC47PVVpGztRlaX+pFk161Nu42KrhZmOrmfxBERETU8KTAhKmpqSpB369fv0r3SbGKqhUIa/ozgwYNUo/58ccf8fDDD5ffn5qaqgKUhBkbGxtVwbAuanL8mrRb00Mmoay611qf502jb9+++PXXXys97uTJk2rh7aqVEyWAScCTOWZHjx7F7NmzVW8a1Q4DGdUr+QOj5pTZWaBfC6dKlZ1keGN4YjbOJOYgLCEbYYnZuJhRNldNhkrui0xXm4aZiRG6eNuhp78DejZzQGdvO5gzoBERERkMKan+zjvvqKFxsrVv31711Pz333/Izs5WlQXr8jPyGCmC8fLLLyM8PBy9e/dWVRBlzpc8Tt7PSMXCNWvWqKF6UpVQU/a+pu2+3vFr0m6p8Lhjxw4VyKp7rfV53irOKfvqq6/UHDEJWFIg5IsvvlBz4aprt8wvGzt2rBrCOHXq1Bq3kS5jIKNGGybRyk2GKFpjRIX9GXlFZSEtIUcFtLBLX2WOmvSoHYrOUNtiRMPS1BiBvnYqnElI6+BpW+vSuURERKRfpHR8nz59VJjZvHkz3NzcMG7cOLVdbVHmmvyMhI0ePXqo0CXVAdu0aYPffvutfPjm//73P1XSXe6T+VmasvdSqbDqemPVteF6x6/Ja5UqiTIE8K677ip/THXPL6FR9tf2vFV3LOlZk3bKa5cwKK956dKlqnBHxQIlGtJrJj1p0rPm6el51ddGV2dUKmPMqNakco58+iCfdlxvYmdDk088pD21Keqhy0pKS3EhLQ/BFzJxICod+6PS1Vy0qqzNjdHN1768B62dh61Or6GmGXYgY7ObwnVqiniN9AOvk+7jNdIPvE66T1vXqGqxFKnYKL18S5YswYQJEyo9Vsrm33zzzdi4caPqRWws2dnZqo3VhcTGJnPuJNjWNRewh4x0jrGREfycrNQ2ppO72icBTcLZgfNlmxQRySkowe6INLUJWRstyM8ePS71oMk8NDkWEREREdXcvHnzVO+cLAydkJCg5ojJ7dGjR5c/RsLHwYMHVbl9mTPXmGGsqWEgI73g42iptgmBHqpwyPnUPBXMDl4KacnZhcjKL8aO8FS1CQdLUxXOBgQ44ZY2znCwMtP2yyAiIiLSeRKyli9fju3bt8PS0lKtgSbDJisW7JDS+lL8Y8CAAXjyySe12l59x0BGekcmlGqqPd7R1VMFtIjkXNWDdlBC2vl0pOUWIT2vCFvOJKvtrT+N0MvfAUPbuapw5shwRkRERFQtKysrzJo165pnZ+DAgWqjG8dARk0ioAW4WqttSpCXmoMWnpijAtqeyDTsPZeGopJS/HcuTW1vM5wRERERkY5gIKMmR+aNyfwx2e7p6a0qOe4IS8HfoUkqoEn1Rk04e+sPoFdzRwxt54LBbVzYc0ZEREREjYqBjJo8e0tTjO3srrZMCWfhZeFMApmEsz3n0tT29h9n0dP/cjhzsuacMyIiIiJqWAxkZFDsLE1V5UbZJJz9G56CvyqEs70yxDEyDe/8yXBGRERERA2PgYwMOpyN7uSuNk04+zs0GbvPpVYbzm7r6oFbWjtzMWoiIiIiqjcMZERVwllWvmZYYzL+i0hFQYVw5mZrhomBnrg90AMe9hY8d0RERER0QxjIiKqwtTDF6I7uatOEsw0hCdgXlY7ErEJ8uTsaS/+Lxs2tnTGpu5cqp88FqImIiIioLhjIiGoYzs4l5+CXIxex4ViCWoR665kUtfk7W+LObl4Y19ldFRAhIiKi+hUbG4uDBw8iLS0NQ4cORXx8PKKiojB+/Pgmf6rPnDmDuLg4ODk5ISgoCA4ODjX+2dDQUISEhKifkQWcra2tr3iMHPvIkSNqXddOnTrB39+/2mMlJCTg8OHDyMnJQevWrdG5c+dK9+/evRtnz56t9mf79OmDNm3aoDGcPXsW+/fvV6/1lltugb29/RWPKSoqwn///Yfo6Gh4enqib9++1Z6bxmKstWcm0jMtXKzx7JCW+HtuT7wyMgDtPGzU/qiUPHyw5RyGfX4Ar/0ehpMXs7TdVCIioibjwIED6NmzJ1auXKlCWUZGBjZu3Ii33367/DEXLlzA999/j/z8fDQVEpIGDx6MqVOnqtf73nvvITAwUJ2HmnjxxRdVeP3jjz/Uz8o5PHXqVPn9mZmZmDFjBrp164Zly5bhhx9+UMHpySefRHFxcaVjffvtt+jatSsWLlyI3377DWPGjMEdd9xR6XxHRkaq61NxW758OR5//HFERESgMbz77rsqKK5ZswYffPABWrZsqYJi1cDWvn173Hfffdi0aZNqX6tWrbB3715oCz/OJ6olK3MT3BboiYldPHA8Lgs/H47DX6eSkFdUgnUhCWrr5GWLO7t5Ynh7V1iamfAcExER1dGqVatUaPjxxx/L98n3RkZG5d9L0JA31qNHj4aFhUWTCaI+Pj4qDEmvlbGxsQocjz76qOrRuVpPlpDQtHjxYvz111/o0aOH6v266667MGvWLOzcuVM9JjU1VfWebd26FR06dCh/zpEjR6pQc//996t90hv57LPP4plnnsFzzz1XHmr69++vnkPOu7jnnnvUVtGkSZPg7u6OIUOG3NC5KCgoUEF03LhxcHR0rPYx0uMlIXTDhg0YO3as2jdz5kz1usPCwsp/L5544gmYmpqqwGtpaYmSkhIMHz5cvd6TJ09CG9hDRlRH8h9BZ287vDWmDf6c2xNP3OIPX0dLdZ8EtVd/D8ewLw7io63ncD41l+eZiIioln766Sc1TE56xaQH7JdfflH7JajI8DrNkDsJFZrwJo9bv359+TF+/vln7Nmz55rPI/e//PLLqpenqi+++EKFosY2cOBA1XNlZ2dXvm/y5MlquN2+ffuu+bPSMyWhTcKY5j3Lww8/jOPHj6sgImQonwQ3TRgT0otWtVdJhkzKc1YMVQEBAepxJ06cuGobJMjJdZFQJgGoIgmIMqxQQtZff/2FvLy8a76e3NxcFfxk6OrVLF26VA2L1IQx8fTTT6thiX///Xf5PgmhMpRRwpiQoDtixAg1vLOwsBDawB4yonogi0jP6O2L6b181CLTqw7H4d/wVGTkFeH7/bFq69PcAWPb2mKEkxPPORERUQ1IGEtJSVFD6GQInISIO++8Uw3hk54QmUMmYU1Cgzh69KjqCfHw8CifXya9JtJzJgHlauQYv/76qworcmwNGcb2yiuvqNBQHellklBzPfL8Li4utbrm7dq1U703FWkCowTSa5HzIEGoIhlyKOQ1Sg+j9DRV7W2S4HPx4kXVY1QxfEmgkl5ImcMm0tPTERMTo4YtXo0EYbluMuSyIhleKvvk5/v166d622T45Lp169CiRQvUlfx+aAKohoRNKysrdZ8Ms9Tsk2Ba0bFjx9S8ODMzM2gDAxlRPZJqi/1bOqntQloe1gTHY21wPFJzCrE3Ml1tPwSnYfZNzTAgwKnScAsiIqLGEJVSNmqjtKQEaekFyEIujIwvD5pytDKFg9XlN6by/1lRSelVj2djbgJXW/Py7+Mz85FXWDlICH9nq1q3df78+WoomcxVWrBgQbWPadu2LWbPno0tW7bgnXfeuSL4SK9S1QIUVUkAef7551V4k94b+f9ZwoQM1Zs2bVp5EKkqOztbvdm/nkGDBtU6kFUl5+CNN95QQxWrBo+KpP1JSUlXPJ+EWQkciYmJV/1ZmWsmr+m2224r3+ft7Y0PP/xQDZeUuWCurq6qp1J6meS8X6t3U3rc5PpU9MADD6hAJ72ScqySkhLce++96lxrekDrQoKktKkqeQ7prdP47LPPVJC8/fbb1XWRgLpr1y41h05bGMiIGoiPoyUeu9kfs/v7YcuZZPxwIFYNZTx5MRuPrT6FDp62mH2TH4MZERE1qvFfHr7m/XMHNsOD/fzKv5/98wlEp159SNnYTu54c0zr8u/f/OMsdp1NveJxR5/vD22QkFYTEtqysrJUD46vry+++eYb1Yuzdu3aq/6MPO5qQbGmpIeo4hBB6Rnr1avXFSHrscceU8PqZDjmtebJScDRhMqqZHhe1YIdGhKGJKy88MILqget6s/JED8Z7ifBTnoG5XzJUMbqSMiRtn7yySeV9oeHh6teR9kvQUlz7MceewzDhg1TQxIlAEqPpfSYaWiGNErvpSYAS0CdOHGiKsihed01ec1yW3pQpVdVXpPML9MEQ21hICNqYOamxhjZwQ3D2jrjn2Mx+CEkDSGxEsyyLgUzG/aYERERaZkMWZM39PJGXYa5SZCTeWXX6tmqjyGL0ptVsZdNnrtqIJOeOxlSKfPjevfufc3nMjExUfPOpBeqIgk1EmKkfH5Vf/75J+bOnauKYMybN6/Sfdu2bVOFRGR+nlRtFBKYbr75ZjVH66uvvqq2d8zGxqZST5s4f/58+Vd5LRpZWWUVqs+dO6cCmRTxqHhO5HshQw0lJAuZ73XrrbeWP8bZ2Vkti1CV7JP7ND8jvaEyfFWqT2oCnLxmmUcmPYA32pNZFwxkRI1E/tEH+dpgSGdf7D+ficW7ziP4QmaFHjMGMyIianjrZ3WvMGQxHY4ODlcMWaxo8eSO1x2yWNHLIwKqHbKo6yRASBiQQCa9MzI0UMrCX0t9DFmUgHWtkPXxxx+rghVff/21qoBYEzJPSnqoKtJ8X7GIh5DhevI6p0yZgv/9739XHEt676QnSRPGhPSSyWuqbm6dhCcpOy8VESsWJBGaOWvSU1Z16OS0adPK1wyTHquKPY8SLmVOmgwr1bRfzn3FNdmkx65qkRHp7ZSf1QxZlbXrZHv//fcr9abJ8EUpky89ezdaEbIuGMiIGpn8AejbwlEV+ZA5ZQxmRETUmDRzuWSIli1y4exspYZ1XWsIfm142DV+2XnNG39ZtLhq8JE38s2aNbtmUQ8NqdInYULemG/evPma56W+hixeiwwhlNLyUulxwoQJ1T5GqkxKFUHpLdIU+5CeqVdffVXd5+XlVd5rJYsgSyENjUOHDqmy8FKZUIYRVjfkT4Kp9K5JUNUs7ixDIqUQhpzXqqTnSQqxVC2BL7p06QI/Pz81J0+GKVYduhkQEIC6krmCskko69ixo9r33XffwdbWFqNGjSq/XjKPTq6vPLbieRA3UlTkRjCQEelAMNsXlY5FO9ljRkREVBey0K/0vkgIkXLxMiyvNlUWNSRwLFmyRFUBlIIU2iRzxV577TXVbhlqWHGInwQaTeg4ffq0Kgkvc8A0gUx6vKSXT0Kc3JawI6XwpXy/ppKgFMGQipXW1tbqOVasWFF+fJljpam0KI+Rn5NjSfEN6cWSIY6yZpeE16ok+ElJfFmnrCqp1ig9fXJ+pRKkFCcpKChQAUnaKL11dSWFOuSaS/iSIZZSyEMCrYRZzTBN6emToagyT0566GSunARNGXb5yCOP3FAgvBEMZEQ6EMz6NHdEb/+yYCY9ZkdjKg9lfKh/MwxsxaqMRERkeCRgVV0fqurC0BISpGdG5lnJG303N7fyQFaTKosazZs3V3O4JNhpmwQnCS4SxmRYZMXXKz09mkAmPWAy3K9iKXxzc3MV6KR3MDg4WIXV7du3q+CqIUFIUwpeszaZhhTK0AQyCTEyNFGWGZBeMZk3JwU4JLhKcKtIinxIe6qW3K9I5sfJOmpyrSRM2traqtCn6cWqjrweeY3VzX/TkPMjAVGz7pxcR1kEu2qwlgWuZVjipk2b1DBO+V2R1zdgwABoi1Gp9DlSrcmEQulylcXm5B+FNsnY2C1Hz8LG3gm9mjuylLqOkqEh0oUvE0uvNQRC/klWDGYa7T3K5pgxmGn/GpF28TrpPl4j/cDrdCWZoyRrn11t3bHGxmt0dZo5ZBLotE3mpUmYr2suYA9ZE/Ht4RQci49DZ29b3N/HFze3dlZrYpF+95jtl6GMl4LZqfhsPL7mlApmT9zSHL2bV17MkYiIiG6M9CbJPCeixsSPgJuAzLwiZBeUdXQei83Ck7+G4o6lR7DxeAIKi/WvyhFdDmYSur69pzOWTOmIrr5lE5YlmD208gSeWRuqFuskIiKi+iHV/SqWUidqDOwhawLsLE2xYLQXzuVZ4du9sTgck4GIpFz838YwfPHvedzb2wcTurjD0qxyWVrSr2DW69Ics0+2RSI0Phv/nE7GzrOpuK+PD2b09uH1JSIiukFS+IKosbGHrAm9ae/f0gnfTO2Mb6d2xsCAskmPcRn5eO/vCIQl5mi7iVRPQxl/uDcQ/zciQK0Tk19UgsW7ojHxqyPYcjpZzT8jIiIiIv3BHrImqJuvPbrd2QFnErLx7d4YpOYUobP35YX5ZJibpZkxXGzMtdpOqhsTYyPc0dUTQ9u6YOHO8/jlyEUVvJ9eG6rmnT07tCUCXK15eomIiIj0AHvImrA27jZ4d1xbfD6p8orsH249h1GLDuGdv85yDpIec7AywwvDArDyvq4I8itb2V6GNE5aegTz/4lQcwuJiIiISLcxkBkAU+PL1RbTcwtV5T4Z6rbq8EWMW3IIL/12BuEc0qjXwfvruzvh/fFt4GFnjuJS4IeDcRj/5WGsC45HCYcxEhEREeksBjID7FX5fU4PzB3YDE5WpurN+6YTiaoq4xOrTyHkwuV1r0i/5pcNb++GdQ92xwP9fGFmYoSUnEK8tjkc074PwbFYXlciIiIiXcRAZoDsLU3xYD8//P5wDzw3pAU87cvmkm0PT8H05SE4GpOh7SZSHVmZm+CRgf749YFuGNTKWe07EZelQtkrm8KQnF3Ac0tERESkQxjIDJiVmQnu6uGN3x4KwhujW6OFixXautsg0OdyAZCs/CIUl7Byn77xc7LCJ3e0xxeTOsDf2VLt23AsQQ1jXL7/AtenIyIiItIRDGQEMxNjjOvsjjUPdMNnd7ZXw980PtxyDrd9XTYXiYtM6x9ZCmH1zG548pbmsDY3RlZ+MT7cGonJ3xzF3sg0bTePiIiIyOAxkFE5YyMjuNtZlH8vc5B+O56IqJQ8NRdpzOJDWHEgFrkFxTxreha4ZXHw9bOCMKaTm9oXkZyL2StP4PXN4aoXlIiIiIi0g4GMrsrZ2kwtMn1L67K5SPGZBfhgyzmMXHQQS3adVxUbSX+42ZrjrTFtsGxqZ7TzsFH71gbH486lR7GfvWVEREREWsFARtckC0p/fHt7NZxRelekhH5abhEW7YpWwSz0YhbPoJ7p6muP5dO74OEBzdT1lEWlZ608gff+imDvJxEREVEjYyCjGglwtVa9Kxse6o4p3b1gYWoMJysztHIv62kRpVzvSq+GMc7q74cV07ugtZu12rfycBwmfXuUVTaJiIiIGhEDGdWKt4Mlnh/WEpvnBOG98W0rLTr92Y4oPLsuFKHx7DXTF+08bfHDvYGY2dcXcimjU/Nw34pj+HhbpFo8nIiIiIgaFgMZ1YmzjbkazqiRkVekelj+Ck3GlG+DMXfVCRyOTufZ1QPmpsZ49GZ/fDetC5o7W0EWOfhu3wXctewoTsRxQWkiIiKihsRARvVChjA+Mag5fBzKqjTujkjD/T8cx4wVIfg3PIXDGfWABOyV9wViak9vSL9nRFIupn8fgoX/RnHJAyIiIqIGwkBG9RbIJnX3wvqHgvD22NZodWle0tGYTDy2+hQmfXMUkck5PNs6ztLMBM/c2gJf391JheviUuDL/2Iw9bsQhCVka7t5RERERE0OAxnVK5lTNrqjO1bd3xUL7miPQJ+yYY1JWQXwsL+8xhnptqBmDvhlZjfc2c1TfX86IRt3LQvG0j0xKCqRQY1EREREVB9M6+UoRNUsMj2wlTMGBDjhcHQGkrILYWVmUn7/6iMXkZlfpN7w21rw11AXWZub4KXhARjcxgWv/R6m1qGTwi3bziTjzTGt0cKlrBeUiIiIiOqOPWTUoIyMjFRvy/D2ruX78gqLsXDneXy6PQojFx7E5/9GISWHi0zrqr4tHLF6ZjeM7+yuvj8el6UKtyzffwElXOqAiIiI6IYwkFGjyysqwc2tndXwxsz8Ynz9XwxGLTyI9/+OQFx6Pq+IDrKzNMXro1vj09vbw8XGTJXE/3BrJB744Thi0vK03TwiIiIivcVARo3O0coMr45shU1zgjCtpzeszIxVSPvpUBzGLjmElzeGISGTwUwXSZBe80A3jLjU43k4JgNTvj2KHWEp2m4aERERkV4y2EBWWlqKzz77DIMGDUK3bt0wd+5cbTfJ4HjYWeDpW1tg88M9MPsmPzhYmqqCEf+cTlJrY5HuBmpZFPx/E9rC3tIUWfnFeHzNKTW/rJgFP4iIiIhqxWCrKbz++uv45Zdf8O6776JZs2awt7fXdpMM+g3+7JuaYXovH6w5ehGFxaVqn8aus6kwNzFCT38HNSeNdMOwdq7o6GmLp9eGIjQ+W1VgPB6biXfHtVELhxMRERHR9RlkIMvMzMQHH3yAvXv3olOnTtpuDlWo6jetl0+l8yE9Lu//E4Ho1Dx08rLF/X19Mai1s6riSNrn42iJ76Z1wXt/R2BtcDz2RaVjyrJgfDChHbpcWvKAiIiIiK5Op8aFJSUlYeHChXjttdeQknL1OSl79uzB/Pnz1ZDDiIiIWj/P6dOn4ejoiP/++w/9+vXDlClTcPz48RtsPTUEWb/MxtykvLrfU7+G4o6lR/DbsQQUFpfwpOvIouAyJ/C1ka1UT2ZCZgHu/+EYVh6KU0ODiYiIiEgPAtnkyZPRpUsXrFq1Sg0nvFoge/rppzFixAhER0dj37596NixI1avXl3pMRMnTkTXrl2r3QoKCpCRkYHk5GQcPnxY9ZS1a9cOw4YNQ05OTiO9WqopWUz6pxmBWDipA4L8yoaVRiTl4uVNYRi35DB+OhiL3MJinlAdMCHQA99P6wIfBws1F1B6zV747QxyC3h9iIiIiHR+yOI999yD5cuXY8OGDdixY0e1j5H9H330EbZs2YLBgwerfS1atMCDDz6IoUOHwsHBQe2TeWF5edWX4jYzM4OPj4+6/5NPPoGlpaXqJfvqq69w6tQpBAUFNeCrpLqQeWP9WjqpLfhCBr7ZE4Md4amIy8hXpdcHtXaBlcPlRadJe9p52uKn+7ripd/OYOfZVPxxMglhCTn4cGJbNOdC0kRERES620M2btw4mJtfuxCABLY2bdqUhzExZ84cpKWlYePGjeX7pMfraj1k8uZejiHbrl271ONDQ0ORmpqqghrptkAfe3x6Rwf8cn9XjOzgirGd3ODlYFF+/5mEbDXMkbRHKi9+ekd7PDKwGYyNgLNJObjnuxD8E5rEy0JERESkqz1kNSFDDAMDAyvt8/b2hqurK44cOaJ62WpCQtnnn3+Ou+66C87Ozrh48SL+97//wdPT86o/I8McZdOIi4tTX4uLi9WmTfL8JSUlajMUAa5WeHtMazVHSfO65farm8JUABjX2R3Te3nD19ESukJzjQzlOt3fxwcdPW3wwm9hSMstwjPrTmNazwwV1MxMdOazIIO+RvqK10n38RrpB14n3cdrdO1zowvvw8WNtkGvApkU/ejVq9cV+yVUJSYm1upYMsRR5qFFRkbCz88PNjY213y8DJWUuW1VyVw0C4vLPTTakJ2djdzcXNXLZ8hl4WMzChCVkouC4lKsPhqPX4PjMailHaYEuqCls3avkSYwyrWSa2Qo16m1PbBwfDO8sSUWoYl5WH4gDkej0/B/g73hYq17f34M8RrpI14n3cdrpB94nXQfr9HV5efno6ioSL0H1rZrFSOsCd17R3SdX8r6fJMk88dkeGNNPPXUU3jggQcq9ZBJOHRxcYGbmxu0SYZ6SkESJycnGBvrZs9DY3B2BjbPccWqIxfx46E4pOYUYevZTLUNCHBSPTaBWizFLp/kyO+woV0nuS7fTXdT8/1WHYnHsYu5mLv+vFqvTFOoRVcY6jXSN7xOuo/XSD/wOuk+XqOrkw9QZR1hW1tbaNuNFgbUq0AmwUd6pKpLpTJssSHJBa9u8WgTExO1aZM8v7x51GyGzMHaHA/2b4apvXywPiQB3+27oIp/SIEJ2X57qDv8nKy01j5DvU4WxsZ4cXgrBPra480/ziIpuxCzV57A44OaY1ovb53qjTLUa6RveJ10H6+RfuB10n28Rlc/L7rwPlzcaBv06h1H9+7dERwcXGlfbGysGsoo9xFpWJmZYEqQFzY81B1vjm6Nli5WGNTKuVIYS8kuUAtPU+MZ3dEdK6Z3gb+zJYpLgY+2RWLeutPIyi/iZSAiIiKDpFeBbOrUqThz5gy2bt1avm/RokVqkecxY8ZotW2km6R4xNjO7lj9QDe8PrpVpfte/C0ME786jF+DL6KgiEUcGksrNxv8cG8gbm3jor7/53Qy7vkuWM3/IyIiIjI0OjNkcdmyZarAxsmTJ9X3CxYsUMU6ZI0wWbRZDBo0CE8++aRa+HnGjBlq+OKaNWvw/fffl69BRlQdYyMjOFiZlX8fmZyDvZFp6vYbm89i8a5oTOvpjdu7esLaXPtd302drYUpPpjYFsv3x+LT7ZGISsnDvctD8Mnt7dHVV7fmlREREREZVA9Zhw4d8Oqrr6owdrVqh5s3b1ZrhklRjRMnTuDOO+9s9HaSfpNFilfc20X10sjspYTMAlV0YuTCg1i08zzScgu13cQmT+aNTe/tg8VTOsLWwkSVxn9o5QlsPX3lPFEiIiKipsqoVEqKUa3FxMSocvlSOt/X11erZzA9PV21x8vLi4UI6iAiKQfL9l3A7ycSUXRpTpmVmTHWPNAN3g6W9VopSQrQyIcNLBhRWXhiDh755QQuZhSogPzc0JZqDmBj4zXSD7xOuo/XSD/wOuk+XqNrV1mUEXK6UGUxKioKzZs3r3Mu0LkeMqLG1tLVGm+Mbo3fHgrCXUFesDQ1RjsP20phrISfWzSoVm7W+G5aF7Rxs4ZE4vf+jsDH2yJ53omIiKjJ05k5ZETa5uVgoXpmHuzvh7ScykMWX/rtjOo9m9nHF+08tf9JTFPkYWeBb6Z2xtNrQ7EvMl0tWRCfka/CsrkpPzsiIiKiponvcoiqcLY2U71mGudTc/HnqST8HZqMKcuC8fDPJ3DwfLpaQJjqv9jH53d2wJhOZYut/3EqCXN+PoGMPJbFJyIioqaJgYyoBj03Lw4LgK9j2RDG/86l4YEfj+PeFcewIyyFwayeyVIFsnbcg/3KxmAfis7AjBUhiEvP5+8qERERNTkMZETXYWFqjDu6eWLdrO54d1wbNc9JhFzIxONrTuHOb44iIZNhob4rMM4d6I//GxEAYyMpvJKL6cuDERqfxd9XIiIialIYyGpp/vz5cHd3R2BgYMNcEdJZpsZGGNnBDT/f3xUL7pD1suzKC3642ppru3lN0h1dPdXaZJZmxkjMKsTMH45jz7my9eOIiIiImgIGslqaN28eEhISEBwc3DBXhPSi92ZgK2csm9oF397TGc8PaakWntZYuPM8vt0bg6x8znuqD3Kuv767E5yszZBdUIxHfzmJ344l1MuxiYiIiLSNgYzoBnTzs0ev5o7l3ydlFWDZ3hh8uj0KIxYexGc7opCSXcBzfIM6ednh+2md0czJUlW7fHlTGL7aHc35e0RERKT3GMiI6pHUXRzTyR1mJkbIyi/G0j0xGLXoEN77KwKxLEpxQ/ycrNRaZV18yoaKfrHzPN7842z5Yt5ERERE+oiBjKgeudma45WRrbBxdhCm9/KGlZkx8opKsPJwHCZ8dQTv74hDWm7lNc6o5mTY4pdTOmJwG2f1/a/B8XhizSnkFBTzNBIREZFeYiAjaqBS+U8NboE/Hu6BOTf5wdHKVPXkHI3NgY25Cc/5DbA0M8H8Ce0wpbuX+n7X2VS1DEEyh4YSERGRHmIgI2pADlZmeOimZtg8pweeHtwc9wW5qnW2NKQ4xd7INM6FqiUTYyM8N7QFnrylufr+5MUsTP8+BFEpufV7AYmIiIgaGAMZUSOwMjfBPT28MKyNQ/k+qcL4/j8RmL3yBKZ+H4Itp5NVCX2qebXLe3v74L1xbdScvQvp+ZixPARhCdk8hURERKQ3GMiItCQxqwB+jpbq9om4LDy9NhS3f30E60PiUVhcwutSQyM6uGHR5I6wtTBBam4RZv10HGcYyoiIiEhPMJARaUkLF2v8OCMQCyd3QI9m9mrfueRcvPp7OMYuOYQfDsQir5DFKmqiRzMHhjIiIiLSSwxkRFoedtevhRO+vrszvp/WBYNalVUPvJhRgCW7o1HMku411tnbrjyUpbGnjIiIiPQEAxmRjpD1tT65oz1Wz+yK0R3d1JwzGwvT8vsPnU9Xwxzp2qFs8ZTKoex0POeUERERke5iICPSMa3cbPD22DaqOqNGQVEJnt9wBqMXHcRbf4QjJi1Pq23UZZ28Koeyh1YylBEREZHuYiAj0gNhidnILSxGQXEpVh+Nx7glh/D8htMsXlHDUMaeMiIiItJVDGS1NH/+fLi7uyMwMLBhrghRNTp62eH3OT3w6M3+cLI2g0wt++NkEiZ9cxSP/nISR2IyeN6qCWVLpnSEnYUJ0vPKQllofBbPExEREekUBrJamjdvHhISEhAcHNwwV4ToKuwtTTGzry82zwnC80NbwsveQu3feTZVrWWWnlvIc1dNkF1cIZQ99NMJhF5kKCMiIiLdwUBGpGcszUwwJcgLGx7qjrfGtEZLVytM7OIBByuz8sfEpuexQmOFULbkrk6XQ9lKhjIiIiLSHQxkRHrKzMQYYzq5Y/XMbnh8kH/5/pLSUsxddRITvjqMNUcvqoIghq6Dp60KZdLLqIYvMpQRERGRjmAgI9JzxkZGsDI3Kf/+aEyGWmA6OjUPb/5xFqMXH8L3+y4gp8CwF5lWoWxKRxXKMi6FslMcvkhERERaxkBG1MR093PAD/d2wZC2LjAC1NplH22LxIiFB7Fw53mkGfBcs/ZVQpkMX2QoIyIiIm1iICNqovOmPpjYDr8+2A3jO7vD1NhIBZAvd0djzOJDyMwrgqFiKCMiIiJdwkBG1IS1cLHG66NbY+PsINzdwwuWZsYYEOAEO0vT8scUFpcYZCj7ckpHOGiGL/50HCc5fJGIiIi0gIGMyAB42lvg2SEtsXlODzwxqHn5/tLSUjz403E8szbU4Ibutbs0fFFCWWZ+MR5iKCMiIiItYCAjMiCyqLTHpfXLxOHoDByNycQ/p5Nx17JgzPn5BA5EpaugZjCh7K7LoWzOzydxOjFP280iIiIiA8JARmTAOnvb4f9GBMDP0VJ9v+dcmuoxu3f5MWwPS1Yl9Ju6dh6VQ9lzm6M5fJGIiIgaDQMZkQEzNzXGHV09sXZWd7w3rg3auNuo/SGxmXhiTSjuXHoU2flFBhHKvryrkwplWQUleHT1KZxPzdV2s4iIiMgAMJARkarCOKKDG36+LxCf3dke3Xzt1VnxsDOHjcXlAiBNWVsPGyya3AHWZsZIzSnC3J9PIiW7QNvNIiIioiaOgYyIyhkZGWFAgDO+ndoZ397TGY8N8q90dl7dFIale2KabNn8dh42eG2Itwqo0Wl5eOSXUwa/oDYRERE1LAYyIqpWNz97NZRP40xCNtYfS8BnO6IwctFBLNgR1SR7kLr72OCNUa3UbSmFLxUoDXFpACIiImocDGREVCP2lqa4vasHzEyMkJVfjG/2xGDkokN496+zuJDWtCoTjujgiqduKVse4L9zaXh9c7jBVJ4kIiKixsVARkQ1Xsvs5RGtsGl2EKb38oa1uTHyi0rw8+GLGLfkEP7vtzPILSxuMmdzem8fTO3prW5vPJ6Iz/49r+0mERERURPEQFZL8+fPh7u7OwIDAxvmihDpOHc7Czw1uAX+eLgnHh7QDI5WpiguBc6l5MLStGn9SXlqcHOMaO+qbkuP4MpDcdpuEhERETUxTevdUyOYN28eEhISEBwcrO2mEGl9COOs/n7YPKcHnh3SAnMHNFNFQTSW7YvBf+dS9Xqon7GREd4Y3Ro9mzmo79//OwL/hCZpu1lERETUhBhGPWsiajBW5ia4u0fZ0D4NmVP22fYo1XPWwdMG9/fxxeC2Lirg6ONabR/d1g4zfziGM4k5ePG3M3C2MUN3v7KQRkRERHQj2ENGRPUuI68IbS9VaDx5MRvPrDuN2746gnUh8XpZsdDO0hSfT+oAL3sLFBSX4vHVpxCemKPtZhEREVETwEBGRPWuvactfri3CxZP6Yhe/mU9SZEpuXjt93CMWXwIPxyI1btgJnPnFk7uAAdLU2TmF2PuqhO4mJGv7WYRERGRIQ1ZzMjIUPOnasPBwQFubm61bRcR6TmZT9anuaPajsVmqqIY28JSEJ9ZgJ8Px2FKkBf0TQsXa3x6R3s8tPKEeh1zV51Ui2jLfDoiIiKiuqjVu4hVq1bhwQcfrNUTzJw5E19//XVt20VETUhnbzt8fHt7nE3Kwbd7Y1SRDBPjy/PJtoclo4OnreqF0nVdfe3x/vg2eOrXUPV6nlxzCgsnd4RFE6swSURERI2j1h/rjhs3Di+88EKNHrthw4Za96gRUdMV4GqNt8a0qbQvNacQz284g+KSUozt5I4ZfXzQzMkKumxQaxe8ODwAb/1xFoeiM/DSb2fw/vi2lUImERERUb0HMlNTU3h7e6NPnz41enxoaChSU1Nr8xREZGDOJGTD1NgIeYUl+DU4XhX+GNrOFff18UG7S4VBdNEdXT2RkFmAL3dH45/TyZj/zzk8N7RFpdL/RERERNdTqzE2M2bMwKJFixrs8URkeHo3d1RrmT0+yB8uNmYoKQX+PJWEKd8Gqzlah6PToavm3OSHiYEe6vbKw3H4du8FbTeJiIiI9AwnPRCRTpSVv6+PLzbNDsKLw1rC26FsLtnuiFQ8u+40Cop0syKj9Ia9NDwAAwOc1PcLdkTht2Mcpk1EREQ1x0BGRDrD0swEk7p7YcNDQXh7TGs15+yent5qcWYNKaRRJN1oOkKGW8r8sc7eZcMrX98croIkERERkVYC2ebNmzFkyBB89NFH9X1oIjIQEnJGd3LHLzO7YmpP7/L9uQXFmPnDMUz88jBWH7mIfB3pObMyN8GCOzrA39lShcVn1obiRFymtptFREREhhjI8vLykJSUhKysrPo+NBEZGGMjI5iZXP4zJT1PablFiE7Lw1t/nsWYxQexbF8MsvOLoG1O1mZYOKkjXG3MkFtYgkd/OYXY9DxtN4uIiIgMLZBNnDgRR48exSuvvFLfhyYiAzeknSt+nBGIoe1cILUME7MK8cm2KIxYeBBf/BulSuhrk4+jJT6/swOszY2RklOo1irLLSzWapuIiIhIt3EOGRHpFVlAev6Edlg3qzsmdnFXwxsz84vx1X8xuHPpERQWa3cYYztPW7wztmyttdD4bLz5x1mUlurOnDciIiLSLQxkRKSX/J2t8Oqo1qoyo8wzszQzxrjO7pWGOMqcM20tHD37Jj91+/cTiVhxIFYr7SAiIqImtjB0RX/++Sc+/vjjq94/YsQIPPHEE3U9PBFRjXjYW+CZW1vggX6+as6ZhvSU3bH0CNp72uL+vr6qZ60xzervp3rItoel4ONtkWjjbqPWXCMiIiKqlx4yKysreHp6Vtrs7e0RHByMkJAQODg41PXQRES15mhlBnvLy58x/XM6GRfS89XXu5cFY/bKEzgQldZowwclHL41pjVauFipxa6fW38aF9JY5IOIiIjqqYds4MCBaqsqIyMD/fv3R/fu3dEUzZ8/X23FxZyoT6TLbm3jgldGBuDbvRcQnZqHvZFpapP1wu7v44ubWztX6lFrCLYWpvj4tnaY+n2Iqg755K+h+G5aZ1iZmTTo8xIREZEBzyGTXrKpU6di06ZNaIrmzZuHhIQE1RNIRLpLFpO+LdAT6x7srhZubutuo/Yfi81SweieZcGNssB0cxdrVeRDot+ZhGy8/ns4i3wQERFRwxb1KCwsRGJiYkMcmoioVkyMjTC8vStW3heILyZ1QJCfvdrfwctWVWhsDANbOePhAc3U7T9OJeH7/SzyQURERDc4ZFEWf46MjKy0T4bxnTx5Ep988gk+/fTTuh6aiKjeGRkZoX9LJ7UdicmAh515+X0yr2zOzycR5GeHoc0t4NwA539mP1+cis/C1jMp+HR7WZGPvi1Y5IOIiMjQ1TmQrVu3Dg8++OAV+01MTPDAAw/grrvuutG2ERE1iG6+Zb1kGjvPppbPMft2rzEmdc/BtF4+cLG5HNpulMxXe3N0a0SmhCAiKVcV+ZBFrn0dLevtOYiIiMiAhixOmjQJYWFhlbaIiAjk5ORg8eLFMDbmEmdEpB8CXK1xZzdPmJsYIaewBMv2xWLkwoN4+8+z9VoZ0UYV+WgPWwsTZOQV4ck1p7S2VhoRERHpBuMbKd7RqlWrSluLFi1gbl5/nygTETUGH0dLvDQ8AL891B2TuzjDxtwEBcWl+OXIRYxbcgj/99sZFNdTARBZ0Pq9cW1VkY+wxBy8+nsYi3wQEREZMHZjERFd4mZrjgd7uWHT7O6YO7AZnKxMUVwK5BWVqOIg9eWmACc8crO/uv1XaDKW7bvAa0BERGSg6j2Qbd68GUOGDMFHH31U34cmImoUssD0g/388PvDPfDskBZ4oJ9vpfs/3HIO/0Wk3lDP1v19fDCkrYu6vWB7FHZHpN5wu4mIiEj/1Hsgy8vLUxUYs7Ky6vvQRESNShZwvruHN9p52JbvC76QgeUHYvHwqpO4e1kw/gpNqtNwRqn6+Mbo1mjlZg356efXn8b51Nx6fgVERERkcIFs4sSJOHr0KF555ZX6PjQRkdZJp1hHr7KAdio+G8+uO43bvj6MdcHxKCwuqdWxrM1N8PFt7WBnYYLM/GI8uSYUOSzyQUREZFA4h4yIqBa6+tpjxfQuWDylI3r7O6h9USl5eG1zOMYsPoQVB2JRUouhjH5OVnh/fFvIFLWzSTl4ZROLfBARERmSOq9DpiGLQx8+fBiJiYmV5lO0b98eN998840enohI58hwwz7NHdV2LDYT3+6NUQs+x2cWYEdYCqb29K7V8fq1dMKjN/vj0+1R+Od0MpbuicED/fwarP1ERETURALZhx9+iOeff169OZGtsLBQhTIzMzPMnj2bgYyImrzO3nb46Lb2iEjKwbd7L2BMJ7dK968PiUefFo7wsLO45nFm9PZBaHw2/jyVhC/+PY+2HjYYEODcwK0nIiIivR2yGB0dreaJ/f3331i4cCGmTZuG7OxstSi0g4MDXnjhhfptKRGRDmvpao03x7RG7+aO5fskpL36e9lQxtc3hyMq5epFO+RDrddGtkKbS0U+Xthw5pqPJyIiIgMPZEeOHFE9YIMGDVJvJIqLi2FlZYWHHnpIFfb44Ycf6relRER65lxyLmwtTFBYXIq1wfGY+NVhPLsuFKEXq69Ca2Vugo9ubw8HS1NkSZGPX08hO7+o0dtNREREehDIZM6Yl5eXuu3o6Ijk5OTy+zp16oRz587VTwuJiPTUrW1d8MfDPfD4IH+42JhBquPLQtBTlgVj7qoTOHQ+/Yq1zHwdLfH+hLIiHxFJuaqH7UbWOyMiIqImGsjkDYL0jIk2bdpg9+7dKqSVlJSoYYxubpXnURARGSJbC1Pc18cXv8/pgZeGt4SPQ9lcst0RaXj/n+o/uJJiIY8Paq5uS5GPNcHxjdpmIiIi0oNAZmlpCTs7O3W7c+fOCAoKQosWLeDj44Nt27Zh+vTp9dlOIiK9ZmFqjDu7eWH9Q0F4Z2wbtHazxsy+PuUfbAmp2Fh0aZHp6b28MSDASd2e/885hCfmaK3tREREpINVFqdOnao2jQ0bNuDnn39GUlISxo8fj5YtW9ZXG4mImgxTYyOM6uiGkR1cVfEOjfiMfNy34hg87S1UxcWxnd3x+ujWmLT0CJKyC/HChtNYcW+gCnZERETUdNzwOmQVe8zuvffe+jocEVGTppYLqfD9zrOpqncsJi0Pb/15Fot3ncc9Pb3xfyMC8OSaUIQl5uDjrZF4fhg/7CIiImpK+FErEZEOuKObJ1bOCMSwdi4qqEmvmCwU/fLGMHTxKRsevvJwHLaHXS6gRERERAYWyJYtW4Y5c+Y02OP1wfz58+Hu7o7AwEBtN4WImph2nrb434R2WDerOyYGeqjhjZn5xQi+kFk+VPHVTeGIz8zXdlOJiIhIG0MWi4qKEBsbi71799bo8WfOnEFhYSGaknnz5qktJiYGfn5+2m4OETVB/s5WeHVkK8y+yQ8r9sdi9dGLal7Z9/tjkZ5XhJd+O4P5E9rBydpM200lIiKixp5DJsU7ZKupmTNn1vYpiIgIgIedBZ6+tQUe6OcLSzMT+DlZ4cXfzuDg+QwM/+IAbgpwwv19fdHJq2xIIxERETXxQDZlyhQMGTKkVk+gKY1PRER142BV1hMm1Rn/O5eKjccTUVBciq1nUtTW299BBbNe/g6VyugTERFREwtktra2aiMiIu14YWhLHI3JQExaPkyMjVBcUop9Uelq6+Rli/v7+GJQG2cYM5gRERHpBVZZJCLSIzYWpqrwh+mlMBboY4d2HjbqvuNxWXhqbSge/PE4SksrrnJGREREuoqBjIhIz3TwtMVjN/ur21KBcXJ3Tyyc1AFBfvZqX58WjpWGLjKcERER6S4GMiIiPTS1lzf6tnBUt9//5xy8HSyw9J7O+G5aZ0zu7lX+uLzCYkz65ii++i8aGXlFWmwxERERVYeBjIhID8kcsTdHt4aztRnyCkvw3PozKCgqQaCPPewtL08P3nAsAWGJOfji3/MYufAgPtkWiaSsAq22nYiIiC5jICMi0lOutuZ4c0xrdft0QjYW7Ii64jH9WzphUndPmJsYIbugGMv2XcCoRQfx9p9nEZOWp4VWExERUb0EsuXLl+Oee+7Brl276noIIiK6QRK4pvX0VrdXHIjFzrMple73cbTEi8MC8PucHrivjw9szE1UyfxfjlzEuCWH8OYf4bwGRERE+hjIOnbsiLCwMAwYMACdO3fGwoULkZGRUb+tIyKi63r0Zn+0v1Rp8ZVN4UisZkii9KY9Pqg5Nj/cA48MbAYnazOUlALWZiY8w0RERPoYyLp37479+/fj0KFD6NOnD+bNmwcfHx/Mnj0bwcHB9dtKIiK6KnNTY7w3vi2szIyRmlOIlzeGoeQqZe9lftkD/fzw+5wgPD+0pSoOoiFl9F/87Qx2R6SyMiMREZG+zCGTYPbVV18hNjYW77zzDnbu3ImuXbuiX79++Omnn1BSUlI/LSUioqvyd7bCC8MC1O29kWn4fv+Fa54tKzMTTAnygoedRfm+f04n4fcTiZi76iTuWhaMP08lqZBGREREelDUo6ioCPn5+WozMzODiYkJ7r33XvTo0QMJCQn19TRERHQVYzu5YUQHV3X78x3ncTwus1bnSio2dvKyVbdD47Px3PrTmPjVYfwafFFVcCQiIiIdDGR79uzB9OnT1XDFjz/+GNOmTUNUVJTqKYuIiICVlZWaX0ZERA1LFoN+aVgAfBwsUFRSihfWn0F2fs3XHuvp74jl07vgyykd0bu5g9p3PjUPb2w+izFLDuGng7EN2HoiIiLDVOdAduDAgfKhiZGRkfj+++9VEHv11Vfh5VW2KKmvr68KaxcuXHvoDBER1Q87S1M1n8zU2AjRaXl496+IWoe6Xs0dsWRKJ6y4twtubeMCIwAJmQU4FZ/Ny0RERFTPLq8eWktnz55F7969Vfl7qbJ4NRMmTMCtt95a16chIqJa6uxthzkDmuGzHVHYeCIRfVo4Ykwn91qfx05edvjwtnY4l5yDb/deUGXzNUpLS/H9/liMaO8KD/vL89CIiIiokQLZlClT1HY9Hh4eaiMiosYzo7cP9kWmYX9UOt756ywCfezg52RVp2O1cLHGG6PLFqDW2BuZjo+3RarQJ3PXZvTxVYVFiIiISEtFPYiISHeYGBvhrTGt4WRlipyCErz2e/hVS+HXRVJWAewsTNRctbUhCZjw5WHMWxeK0ItZ9fYcREREhqDOPWSbN2/Ghx9+eNU5CA4ODmp+2axZs2BrW1a1i4iIGo+7nQVeHB6AeetO41B0BlYdvqhK3deHsZ3dcUsbZ6w+ehEr9sciKbsQf4cmq61fC0fM7OuLoGZlhUGIiIioAXrI7OzskJSUhK1bt6pS956enjA1NcWuXbtw4sQJFBYW4s0338Qtt9yC4uLiuj4NERHdgKHtXDGkrYu6/en2SFxIy6u382lrYYoZvX2xaU4P/N/wAPg6Wqr9/527/jpoREREdIOBrEOHDkhNTVUBTErcr1ixAn/88QfCw8NhbW2NF198EWFhYWoNslWrVtX1aYiI6Aa9OKwlHK1MkVtYgtc3h6uCHPXJwtQYd3TzxLpZ3fHuuDZo42aN+/v6VnrMnnNpangjERER1VMg2759e3nZ+4qk1P3MmTOxZs0auLq64r777kNISEhdn4aIiG6Qs405nh/aUt2WIh9rjsY3yDmVUvsjO7jh5/u7ItDHvnz/yYtZmPPzCYxfcgirDschr5CjJoiIiG44kOXk5KitOtnZ2eX3yfwxmVNGRETaM7y9K25p7axuf7TtHGLT62/oYlVV/+ZL75i4kJ6Pd/6KwOjFh/DNnhhk5tV80WoiIqKmqs6BrE+fPqqX7Kuvvqo0R2zbtm34/PPPcdNNN6nv5TE333xz/bSWiIjqHJJeGh4Ae8uyqotvbD5b70MXr0YKfKy8L1CFQmMjIDm7EAt2RGHkooPqa0p2QaO0g4iIqEkFslatWuGDDz7AI488ooYmyuLQXl5eGDx4MCZOnIjJkycjLi4OPXr0wPDhw+u31UREVGuutuZ4bkgLdXtvZBrWhSQ02lls52GL98e3VfPMbgv0gJmJEbLyi1VP2QsbzjRaO4iIiJpMIJNPVh999FGEhobi1VdfxdChQ/H4449j3759+Pbbb9WnsRLQXnvtNTQl8+fPh7u7OwIDA7XdFCKiWhvV0Q0DA5zU7Q+3nkN8Rn6jnsVmTlZ4ZWQrbJodhOm9vGFlZox7enpXekxCZuO2iYiISJuMSus4ZmXp0qXYs2cPvv76axiimJgY+Pn5ITo6WhUy0ab09HTVHgnAxsZc61tXlZSUICUlBc7OzrxOOspQrlF8Zj5u//qI6qHq39IJn9/ZXmtzfTPyitQC05rnP5+ai4lfHsaAVs5qqGNnbzuDvU76jNdIP/A66T5eo6uTmhWy7rEurHccFRWF5s2b1zkX1Pl/MvmPUP5DJCIi/eJhZ4Fnbi0burg7IhW/HU/UWltkTlvFMLj6yEUUlwLbw1Iw7fsQzPrpuBpe2Vjz3YiIiBpbnQPZwIEDceTIEURERNRvi4iIqMGN7+yO/i0d1e35/0TozDDBR2/2x2sjW8Hf2bK8TP/slSdwz3ch2HI6GSUMZkRE1MSY1vUHZdFnFxcXdOnSBaNGjVLD5Sp+ytm3b19V2IOIiHSP/L1+eUQrNXQxM78Yb/95Fp/crr2hixpmJsaYEOiBsZ3dsfVMsir6cSo+W61l9vTaUFW6/8OJbbXaRiIiIp0IZPHxZQuLtmvXTvWSVe0pk8qLRESkuzztLfDU4OZ484+z2BGeit9PJmJ0R3foAhNjIwxt54ohbV2wNzJdBbMD59Nxa1uXSo8rLilVjyUiIjK4QDZ+/Hi1ERGR/pIS9H+FJmFfZDr+9/c59PZ3VOXxdYX02PVt4ai2Y7GZaOdhU35fUlYBpq84htsDPTE5yEvNRyMiItI39VKeKikpSZW7P3HiRH0cjoiIGjHwvDqyFazNjZGeV4R3/mq8BaNrSyouypBGjZ8OxeFiRgG+2HkeIxYewMfbIpGYxUWmiYjIgALZuXPn1CLQbm5u6NOnDz7++GM1t6x37946+x86ERFV5u1giScGNVe3t55JUT1m+uD2rh6Y3N0TFqbGyCkowXf7LmD0ooN4649wRKfmart5REREDRvI8vPz1WLQBQUF2LZtG95//321v3Xr1vD29sbGjRvremgiImpkd3TzRM9mDur2e39FICWnUC+C5AvDAvD7nCDc39cXthYmKCguxeqj8Rj/5WF8uOWctptIRETUcIFsy5YtsLKywoYNGzBo0CC1LplGr169sH379roemoiIGpmxDF0c1QqWZsZIzS3Ce3+d1Ztr4GJjjsdu9sfmOT3UV2drM5SUlhUtISIiarKBTOaNtW/fHiYmJlfcZ2FhoXrQiIhIf/g6WuLxm/3V7b9Ck/GPngxd1LCzNFU9ZdJj9tLwlqpgiUZWfhEe/eUkdp1N5ZB6IiJqGoGsZcuW2LlzJ1JTU6+4Twp8SFgjIiL9ItUKu/vaq9vv/BWBVD0YuliVpZkJ7uzmBSvzyx8Yrj56ETvPpuKRX05iyrfB+PNUoiqZT0REpLeBrF+/fmqtsf79+2P58uWIiYlBZmYmFixYgK1bt2LKlCn121IiImq8oYumxmoe2f/+qbzGpL5q626Lzt626vbphGw8t/4MJnx5WAW1gqISbTePiIgMWJ0XbTE2Nsb69esxadIkTJ8+vXz/rl27sGbNGri4VF68k4iI9IO/sxXmDmyGD7dGYvPJJAxr54pb2uj333RZx6xPcwccPJ+OpXsuYG9kGqLT8vDWH2exZNd5PNS/mSpsQkRE1NhuaBVNGbZ44MABhISEICoqCk5OTqrkvbm57iwqSkREtXd3D2/8fToZIRcy8fafZ9Hdzx4OVmZ6v+ZaT39HtZ2Iy8S3ey9gy+lkJGYVIoHrlxERkb4uDC3/wQUGBmLcuHEYMGAAwxgRURNgYmyEN0a1grmJEZKyCzG/iZWQ7+hlhw8mtsOvD3ZTxT/uCvIqv6+wuASf/xuFixksTkVERHoQyITMHZOqixW3rKys+jg0ERFpSXMXazw8sJm6vfF4Iv4NT2ly16KFizVeGdkKTtaXe//ktX79XwzGLD6EVzeFITI5R6ttJCKipu2GAtmyZcvUItD29vZwc3OrtD3xxBP110oiItKKaT190MmrrBjGW3+eRXZ+UZO/EtJDZmdhgqKSUqw/loCJXx3BM2tDcfIiP2gkIiIdmkN29uxZzJo1C0899ZQaqihrj1Xk4+NTH+0jIiItD118fXRrTP7mKBIyC7B4VzSevrVFk74mk7p7YXRHN6w+Go8VBy6oOWb/nE5WW5/mjniwny+Cmjlou5lERGTogezgwYMYPHgw3nvvvfptERER6ZQAV2tM7+2Db/bE4MeDsRjX2R2t3W3QlNlYmOLe3j5qbtmG4wn4bu8FVZVRqjM2d7ZiICMiIu0PWZSKira2ZcNYiIioaXuwry+87C1QXFq2YHRpqWEsqmxuaow7unpi3azueH98G3TwtMX03t7l95eUluKf0CQ1zJGIiKhRA1mvXr1w9OhRXLhwgWeeiKiJszI3wbNDyoYqHonJwG/HE2FoQzeHt3fDjzMC4e1gWb5/+5kUPLPuNMZ/eRg/H45DXmGxVttJREQGNGQxNDRU9ZJJyfvRo0er2xX17dsXkydPro82EhGRDhjU2hkDApyw82wqPt4Wqb63t7yh5Sz1Xkhspvoam56Pd/+KwJJd0Zja0xt3dvOEnYGfGyIiqpk6/28RHx+vhqw0b94cJ06cuOJ+V1fXuh6aiIh0kKw7+dzQltgfdQSpOYX4bEcUXhoeAEP2xC3NMaqjG77ZG4O/TiUhJacQC3ZEqe+lOMjUHl5wtjHXdjOJiKgpBrLx48erjYiIDIevoyVm9vXFwp3nsfrIRUzo4q4WWTZkbdxt8N64tnh4QDN8t+8CNhxLQFZ+sSqCkpCRj7fGttF2E4mIqKkvDC0LQe/bt6/anjIiImpapPqgn5MlpKyHFPgoLjGMAh/X08zJCi+PaIXf5/RQ58jG3ERVp9SQUSXRqblabSMRETWxQHbu3DlMnDhRLQTdp08ffPzxxwgLC0Pv3r0NpgIXEZGhsTA1xgvDWqrbJ+Ky8GvwRW03Sae42ZrjyVua459HeqreM41D0RkYu+QwnlhzCiEXyuaeERER1TmQ5efnY+jQoSgoKMC2bdvw/vvvq/2tW7eGt7c3Nm7cyLNLRNRE9WvhhCFtXdTtz3acR0p2gbabpJOVKStaGxyvvm4PS8H05SF44Mdj+O9cKj/AJCIycHUOZFu2bIGVlRU2bNiAQYMGwdnZuVJJ/O3bt9dXG4mISAfNu7UFrMyMkZFXhE+2R2m7OTrvtVGt8PqoVmjhYqW+P3g+Aw//fBL3fBeMv0OTOPSTiMhAGd/IvLH27dvDxKTyJ4DCwsJC9aAREVHT5WFvgdk3NVO3pZCFrE9GV2dmYozxXTyw5oFu+HBiO7XItDh5MRvz1p3GW3+e5ekjIjJAdQ5kLVu2xM6dO5GamnrFfVLgQ8IaERE1bXf38EKAq7W6/c6fZ1HEAh/XZWxkhFvbuuCHe7tg8ZSO6OXvoPaP7uhW6XEFRSUNc9GIiKhpBLJ+/fqptcb69++P5cuXIyYmBpmZmViwYAG2bt2KKVOm1G9LiYhIJ3t9XrxU4CMsMQc/HYzVdpP0al23Ps0d8eVdnbDq/q4I8rMvv+9MQjaGf3EAS3adR3puoVbbSUREOroOmbGxMdavX49JkyZh+vTp5ft37dqFNWvWwMWlbLI3ERE1bUHNHDCmoxs2nkjEol3nMay9KzzsLLTdLL1SsRqjWLbvAlJzi7BoVzS+238Bd3T1xNSe3nDneSUianLqHMg0wxYPHDiAkJAQREVFwcnJSZW8Nzc3r78WEhGRzntycHNsD09RCyJ/tDUS749vq+0m6bW5A5rBzsIE60ISkFNQgu/3x+KnQ3EY28kdM/r4qDXPiIioaTCujyEXgYGBGDduHAYMGMAwRkRkgFxszPHIQH91+89TSdgXmabtJuk1H0dLvDAsAL/PCcLMvr6wtTBBYXEpfg2Ox4QvD2PxrvPabiIREelKIDM08+fPh7u7uwqhRER02Z3dPNHeo2zo3bt/RbAoRT0F3Udv9sfmOT3w+CB/uNiYQeqmtHGrPMSRiIj0FwNZLc2bNw8JCQkIDg5umCtCRKSnTIyN8OLwABgBiEzJxff7L2i7SU2GnaUp7uvji02zg/Dm6NYY1Oby2p8X0vIw84dj+Dc8hYtMExHpIQYyIiKqN5297XBbVw91++v/YlRYoPpjaWaCsZ3dVel8jeX7Y3EoOgOPrT6Fyd8cxeaTiVx+gIhIjzCQERFRvZIhdo5WpsgrKsH8f87x7DawmwKc0MXHTt0+k5iDFzacUfPMVh+5iHyuZUZE1LQDmSwKLcP3KpK1yGJjuQ4NEZGhcrQywxODmqvbUnlRhtJRwway76Z2xtd3d0K/Fo5qX0xaHt768yxGLzqIDccq/z9NRERNJJAVFxdjxIgRiI6OvmL/0KFDcfHixfpoHxER6aFxXdzLe23e+zsCuYXF2m5SkyYVj3s0c8DCyR3x04xADG3noubyJWUXoliqgBARUdMLZLIAtLW1NYKCgirtd3R0xNixY7FixYr6aB8REekhmeP00rAAGBsBsen5+GZPjLabZDDae9pi/oR2WDerO+7p4YXRHd3K70vNKcSHW87hYka+VttIRET1EMjOnz8PV1fXau+T/ZGRkXU9NBERNQFtPWwwJchL3V627wKiUnK13SSD4u9shXlDWsLc9PJ/9bK49PIDsRiz+BBe3RSGyOQcrbaRiIhuIJAFBARg9+7das5YVZs3b0aLFi14fomIDNycm5rB1cZMLWosa5OVlnL4nDbZWZjA3tJUVWFcfywBE786gmfWhuLkxSyttouIyJDVOZD16dMHLi4uGDlyJP766y+cO3cOe/bswT333IP9+/fj7rvvrt+WEhGRXq6f9fStZR/Q7Y1Mw9+nk7XdJIM2rZcP/ni4B54e3BxutuaQePzP6WTcvSwYD608jiPRGdpuIhGRwalzIDM2NsbatWtVD9nw4cPRsmVL9OvXD9u2bcP69evh5VU2TIWIiAzbiPau6NnMQd3+YMs5ZOcXabtJBs3a3EQFM1lk+pWRAfBzslT790WmIziWgYyISK/K3rdq1QpHjx7FoUOHsG7dOuzcuVPNHRs8eHD9tZCIiPS+AuALw1rC1NgICZkF+PI/FvjQBTK37LZAT6x7sDveH98WPZrZ446unuX35xYUY9PxBBQWl2i1nURETZ1xffxH2717d4wfPx433XQTzM3N66dlRETUZLR0tca0Xt7q9o8HY3EhLU/bTaJLTIyNMLy9K76+uzNsLUzLz8uvwfF4aWMYxi05jJWH4pDHpQuIiBrE5b+8NSx1v2zZMgwYMEAV9ZDbVyOPuffee+ujjURE1ATM7OuLdSEJqvT6p9sj8b8J7bTdJLqGc5cqMMZl5Ku15JbsjlZl9Cd191KFQYiISAs9ZOnp6QgPD0dCQkL57att8hgiIiIN6X15eICfuv1XaDKOxnC+ki77vxGtsOr+rhjZwVWtJydB+vN/z2PUooMqUCdnF2i7iURETUKtPuIaPXq02oSULpYKi1Lcg4iIqCYmBnqqtbAiknLx4dZz+H5aFzX0nXRTG3cbvDuuLeYO9Md3+y5gfUg8svKL8e3eC+q6PXazv7abSESk9+qcpr755hvMmjWrfltDRERNmhT2eHpwWRn8Y7FZ+PNUkrabRDXg62iJl4YHYNOcHpjR2wfO1mZq+KKGFP4IT+Qi00REjRrInJ2dkZKSUtcfJyIiA9W/pRP6tnBUtz/dHsViEXpE1i574pbm+HNuD7jYXC7iJcH6jqVH8Pjqkwi+wKGoRESNEsgGDhyII0eOICIioq6HICIiA/XULc3VvCQpGPHjwThtN4dqycyk8tuHzScT1dcd4am4d/kxPPjTCRyMyVbTG4iI6NrqXCYpLCwMLi4u6NKlC0aNGqUWgq44D6Bv376YPHlyXQ9PRERNWGt3G0wM9MCao/FYuicGE7q4w7lCjwvpl09ub4/NJ5OwbG8MIpJzcSg6Q23LjqTi/r6+uLWNiyqvT0RE9RjI4uPj1dd27dqpXrKqPWWurq51PTQRERmAhwc0Uz0r2QXFWLQrWs1RIv3tMRvX2R1jOrlh+5kULN0TjRMXsxEan41n151WoYwFQIiI6jmQyULQshEREdWFzEG6v4+vKqW+5uhFTO7uhVZu1jyZeszYyAiD27rg5laO2HI8BqtPZuLg+XTVA6ohwxjzCktgZW6i1bYSEen9HLKzZ89i7969tb6PiIhIY2pPb3jam6OkFPh4WyRPTBMhUxi6+9hg8eQO2PBQEJo5WZXft/NsKkYsPIhFO88jLbdQq+0kItLrQLZt2zZ8/fXXtb6PiIhIw9LMBI9eWstqd0Qq/otI5clpgiXzK5L1zNLzirBkdzRGLjyID7ecQ3xmvtbaR0SkbQ2yqnNaWhrs7e0b4tBERNTEjOzgho5etur2R9siUSzdZdRkvTmmNe4K8oKlqTFyC0uw/EAsRi86hNc3hyMqJVfbzSMi0v05ZLt378by5csRGhqqCnvMnj270v05OTn4/fff8cknn9RnO4mIqAnPO3pmcAvc98MxtbjwupB43N7VU9vNogbi7WCJ54a2xIP9/fDjwVisPBSHrPxirA2Ox7rgeDw1uDmm9fLh+Scig1HrQJaamorjx4+rMJaZmaluVyQ9Y8888wymTJlSn+0kIqImrJufPYa0dcE/p5Pxxb/nMaK9K2ws6lx3ivSAs7UZHhnojxm9ffDLkYv44UAskrILEdTMQdtNIyJqVLX+327MmDFq27RpE06ePIl58+Y1TMuIiMigPD7IH9vDUpCSU4hv9l4on1tGTZuthSnu6+OLu3t4Y8+5VHTwLBu+KoIvZOCjrZGY2dcXAwKcKq13SkQEQ59DNnr0aDzwwANISEiotF96zWJjY+ujbUREZED8nKzU3CKx4kAs4tJZ6MGQWJgaY1Brl0r7vtkTg+ALmXhs9SlM+uYofj+RiCLOMSSiJqbOgay4uBgjRoxAdHT0FfuHDh2Kixcv1kf7iIjIgMi8IgdLU+QXleCzf6O03RzSMplLGOhjp26HJebgxd/OYMKXh/HLkTj1O0JEZNCBbNeuXbC2tkZQUFCl/Y6Ojhg7dixWrFhRH+0jIiIDYm9pitk3+anb0htyLDZT200iLRrYyhnLpnbG0rs7oX9LR7UvJi0Pb/8ZgdGLDuKv0CReHyIy3EB2/vx5uLq6Vnuf7I+M5AKfRERUe3d084S/c9naVTJ/qLSUZfANmcwbk0IfX0zqiJUzAjGsnQuMjaAKgNix8AsRGXIgCwgIUCXwZc5YVZs3b0aLFi1utG1ERGSAzEyM8eQtzdXtIzEZ2HI6WdtNIh3RztMW/5vQDmsf7I6HBzRDn+aXKzJGJOXg/b8jOPeQiAwnkPXp0wcuLi4YOXIk/vrrL5w7dw579uzBPffcg/379+Puu++u35YSEZHBuLmVM3peKn/+yfYoFHC+EFXg72yFWf39KlVdXLbvAn46FIexSw7h5Y1hKqAREemDOi/yYmxsjLVr1+L222/H8OHDy/d7eXlh/fr16isREVFdyBvtpwc3x13LgtWcoZ8Px3GxYLpuSJOCMOl5RfjteAI2Hk/ALW2ccX9fX3TyKisMQtSQcguLEZ+Rj/jMAly89DUttxDGRkZqmK1Rla/SK1J+28gIRlW+yn6Rl5MDF8ciOFmbwVE2K1M4WpmpObcmmgeRXruhVTdbtWqFo0eP4siRI6raovSY9erVC+bm5vXXQiIiMtjhaWM7u2PDsQQs2R2NMZ3c1RsSourIWmV3B3lhTXA8vt9/AQmZBdh6JkVtvf0dMHegP7pcqthIVFt5ErYyC5CQmY+LGQW4mJmvfsdU8LoUvuTDgIZTeZkpIVFMQpmDBDQJapaXvlrJvsvBTfO9s7Wp+hvK9fyaWCATclGbNWuGwsJC2NraMowREVG9mTuwmaqkl5VfjC93R+O5oS15dumqrMxNMLWnNyZ398Sm44n4dl8MolLysC8qHbdl5DGQ0XVl5BVhf2QaDkVnIDY9TwUtCVypubULW3YWJvC0tyj/EKmktBRSn0iW0bt8u1R9X1rxa6XHlqKkBCgsLkZeUalqW8Vl+OSmhEDZzqfm1ahdVmbGas3HZk6W8Lu0NXOyUl/dbM1VzxzpWSCTeWNPPfUU1q1bp76fOXMmnnvuOUydOhV79+5lAiciohviYWeBe3v5qB6yX45cVG+05Y0E0fUKw0wI9FA9rFvPJOPPU0kY0vZyZej4zHzsPZeGUR3d1GPJcBUWl6jFx/dGpqnfiZMXsyqFnurYWpiov02e9uZwl6925vCwtyjfJ1+tzU3qpX0lJSVISUmBs7Oz9IIgM68IabmyFSI9twipOWVf02S/5nZuoXpM+qWvFRdTzy0swZmEbLVVZWlqDF8JaY6Xg1pZcLOChz3Dmk4Gsvz8fLUAdNu2bbFt2zZVyOPMmTNo3bo1vL29sXHjRrUeGRER0Y2Y0dsHvwZfRGJWIT7ZFoWPbmvLE0o1IvNrhrZzVVtFK/bHYvmBWCzeFY3pvbxVeLMyq5830KTbpBfqbFJuWQCTnrDz6SqkVCTTsjp42qKVm/WlkCVhSxO6zGGrpeUWpPdKhh7K5g+rGr/e7IJiFcySswsQk5qnetOi0/IQnZqL6NQ8dZ/IKypBeGKO2qoyNzGC76Wg1tLVGp297dSi7S42nKZUH+r8G7VlyxZYWVlhw4YNMDExQXh4ePl9Mo9s+/btDGRERFQvw9Bk/s9rv4dje3gK9keloxWnAtENSMouUF/jMvLx/j/n8OV/Mbi7hxcmd/dSc3KoaUnKKsC+yDTsiUzDvsh0JGaVXf+KJGzIMgp9mjuiV3PHJvN7IFOLJEDKJq8x0Mf+isfIUMjzl8KZbBVvp+QUqscUFJciIjlXbTvCU8t/Vo4pczO7eNuhq48dWrnbwJSFRmqtzr9tSUlJaN++vQpjVVlYWKgeNCIiovowtpM7fjoYh9MJ2fh4WyQWjPHliaU6e3dcW1UE5Nu9F/DHyUQ17OuLf89j2d4LuLObp5qH5mrLT/71udrhkeiM8mGIZ6rp8ZE5Xr2bO6oA1qeFowoWhkrCp1Qira4aqQyRlEq3qlctNVd9DY3PRnhithraKffJ9vuJxPI5ah29bNHFx14FNOlJYzGmBgxkLVu2xM6dO5GamgonJ6dK9+3btw8DBw6s66GJiIiuGHomZfBnrTyB0wk5+Cc8A3e5uvAsUZ21crPB22PbqAWmv9t/AeuC49XQLlnPzNvBApO6c/kefVJcUopdZ1Ox+uhFFcQKiytPBJNeGxliJ+FLQpgMSWTJ+OuzszRFe09btVWUnV+E43FZCLmQieDYTPVVetpk+OfB8xlq05B5aNKLJudfeugCXK157usrkPXr1w+urq7o378/XnjhBcTExCAzMxMLFizA1q1bsXDhwroemoiI6AoyjOjmVk5quMw3BxMxoXsz2FiyIAPdGB9HS7w4LACz+vnhh4OxqgjI+C4e5ffLm0wpbd7G3YanWkeHI64Njsea4IuqHH1FLV2t0PdSD1iQn0O9FdogwMbCVPUwyiakImRUSq4qkKJC2oVMtTi7xGLpVZNt4/GyXjQbcxN08rJVf9MHBDihtZu1wRcCvKGFoWUB6EmTJmH69Onl+3ft2oU1a9aoNcmIiIjq05O3NMfuiDQk5xTjx0NxeLB/M55gqhcyRPHxQc3xyED/Sp/ey6LkMpxR3jjKItPdfK+cg0ONSwpVHIhKx6ojF7E9LKVSFUHphZnYxQN9WzqqghzUeAVHWrhYq23CpQ80ZLjjsbjLAe1YbKZawkR6omUpCtk+2xGlKlPe1NIJAwKc0cvfQc0bNjQ3NGNRhi0eOHAAISEhiIqKUkMXe/fuzbXIiIioQTRX/9m7Y/XReHy3LxZ3dvdSC58S1ZeKYUze+MsbfrHzbKrauvvaY2Y/X/Rr4Wjwn+o3NumtlIXiVx+5iMiU3PL91ubGGN3RXc3/Y0+mbg137NfCSW2aXrSIpFyExGbgcHQG/otIU0VDLmYUqL/pskk1xx7NHNQHIANaORvM3L56WRg6MDBQbURERA3twb6++O1YArIKilVRBuk1I2oI8h5n2dTO+ONkEr7dG6MqzB2OycDhVSfRzsMG9/XxxZC2LpwP04AkFMtcJVmHUNaTyy+6XKK+jZs17uzuiVEd3NQQOtL9XjRZSkC22wI9VUA7EZel5v7Jhx2yBpxUc/zvXJrapAJqc2ersnAW4IRufvZNdt3AWv32ynDEZcuW1eixAwYMwL333lvXdhEREVXLzc4cEzs5YWVwClYeisPdQV5qfSCihiBvAGWB6dGd3FRv2Td7YlRAkEpzz60/jXm3tsA9Pb158utZbkExNp9KxC+HL+JU/OVFjKUHZVg7VxXEpNS6hGbS34AmVRg7e9thzoBmaj7grohUFdD2nEtTQxulJ1Q2WTdQ5p71beGohjfeFOAEKyMDDWTp6emV1hu7FlkwmoiIqCFM7uKMTaHpyMwvxpLd0XhlZCueaGrwN4+D27jgltbOav7S0r0xOB6bpcKaRkFRiZrPxOIRdXc2KUeFsI0nEtR8Iw1ZkPjOrp4Y18Wdw5Sb8DzOCV081FZYXIKjMRnlQ4XPJeeqgPbP6WS1ibbuVrilrTsmBvmjraed4QSy0aNHq42IiEibZA2h+/r4YMGO81gfEo/pvbzV/DKihiY9MlIdTjZZYLjiAsLrQuJVAZApQV64qwfnN9ZmWKIU65FhoYeiL5dLNzECbm7tjDu7eaF3cwcVislweqZ7+juq7anBLdRaZ2VDG1PUByIytPF0Qi5OJ0TB1srCsALZtRaJPnv2LGxtbdGxY8f6OCQREdE1Te7uiZ8OxSExqxBf7DyP+RPa8YxRo3KrsHi0hAopm5+eV6R6bb/ffwG3d/XEtJ7eHFJ7DVJ575NtkZWCmLudOW4L9MDEQA9WSiRFinvIBx2yyXDW/VHp2Ho6AXujsnBLu8u91PrqhmbGnTt3DhMnToSbmxv69OmDjz/+GGFhYarSovxhaormz58Pd3d3FjEhItIyKzMTPHSp7P3fock4EZep7SaRgfecLZ7cCXf38IKlqbFaIHfFgViMXnwIr/8eptZoossik3Pw9K+hmPZ9SHkY6+xti48mtsPvc3pg9k3NGMaoWlIWX3pOn73FD/881hsdvOwNN5Dl5+dj6NChKCgowLZt2/D++++r/a1bt4a3tzc2btyIpmjevHlISEhAcHCwtptCRGTwxndxV3NLxGc7zhv8+SDt8nKwwLNDWuL3h3vgwX6+amitzClbG5KACV8eViXbDV18Zj7e2ByO278+gi1nyuYCSSU9CWLfT+uCwW1dYFph6QGi630Q0hQKu9Q5kG3ZsgVWVlbYsGEDBg0aBGdn5/L7evXqhe3bt9dXG4mIiK46z+CRgWW9ZHsj07AvMo1nirTO2doMcwf6Y/PDPfDELf5wtTFTpfF7+zuUP0ZGEjXV0URXW0NswfZIjF9yGL8Gx6O4tGzI5ysjA7D6gW4qiDWFN9ZEjTqHTOaNtW/fHiYmV66mbWFhoXrQiIiIGtrQdq5qPTIpQ75gRxRW+DvwjR3pBFsLU8zo7Yu7grzVXKmKyzP8FZqEHw7E4f6+PhjYyrnJFqyQdcNkeYqle2JUKBPSc3h/X181H0iGHhMZujr3kLVs2RI7d+5EamrqFfft27dPhTUiIqKGJm9kH7vZX92WRUa3nknhSSedYmFqjB7NKveOLd1zASGxmXhiTSgmfXMUm44nqOGNTUVxSamqOjluySF8vC1ShTFZQ+ze3j7YODtILarNMEZ0g4GsX79+cHV1Rf/+/bF8+XLExMQgMzMTCxYswNatWzFlypS6HpqIiKhWZLHQHs3KJnZ//m9Uk3pjS02TDLXt6ltWqjs8MQcvbQxT4eXnw3HIK7y8/pa+kbC5PSxZhczXfg9HfGYBZErYhC7u2PBQEJ68pTkcrMy03UyipjFk0djYGOvXr8ekSZMwffr08v27du3CmjVr4OLiUl9tJCIiuiaZe/LYzc0xfXmIWkB047EETAj04Fkjnf19lWGKsh2JzlCLTMsaS7Hp+Xj3rwh8uTsar41qhQEBl+fn6wN5LZ/uiMTRmMsVTwe1dsajN/sjwJXrBBI1yDpkMmzxwIEDCAkJQVRUFJycnFTJe3Pzy+tyEBERNYYuPna4pbUztoWlYNGu8xjZ0U0NFSPSZd387PG5Xwecjs9WCyPL3LKU7EL4OJRVD9UH0sP32Y5I7Ai/PI2lm689Hh/kj66++l+SnEjnF4aWT3kCAwO5LhcREWmdVLbbEZ6ihkmtOhyHab18tN0kohpp62GD98a3xcMDm6lqoS0r9Cjtj0xTJeJl/pW3DgW1gqISfPlfNL7dE6OqJopWbtZqTueAACcW1yFqrEBGRESkK+TN4JhO7mq9J6nqNjHQQ1W6I9IXzZys1FbRV//F4MD5dKw+chEjO7jhvr6+Wh8CGHoxCy9vCkNYYo763tPeHA8P8Mfojm6qxD8R1RzHchARUZMy+yY/mJkYIS23CN/vj9V2c4huuFphT38HOFqZql6ojScS1aLKT6w5pUrpN7bC4hIs3nUeU78PKQ9jU7p7Ye0D3TGuszvDGFEdMJAREVGTIkO6JnXzVLeX77+AlOwCbTeJqM6kt2lWfz9sntMD825tAQ+7snn628NSMO37EMz66ThOXsxqlDN8JiFbPefiXdGqkqm3gwW+uqsjnh/WElbmXE+MqK4YyIiIqMmZ2c8P1ubGyC0sUcO9iPSdBJ57enqrNbykAmNz57Jhjfuj0lWvVUOS8PXVf9G4e1mwWoBd3NnNE7/c3xU9/R0b9LmJDAEH1hMRUZPjbG2G6b181Cf5vxy5iKk9veHjqDvFEIjqyszEGBO6eGBsJ3dsO5Os5pYF+thX6sU6dTELozq6qcfeqLNJOXh5Y1h5L5zMFXt1ZGu19h8R1Q/2kBERUZM0rac3nKxM1af7UgafqKkNZRzSzhUvDAuotF/WMHv193CMXXIIPx6MRW5BcZ3nrkkZ/infHi0PY7K48y/3d2MYI6pnDGRERNQk2ViY4oF+fur2puOJCE8sG2pF1FRJiCq9VH7+YkYB/vfPOYxadFCFtIy8ohofJzI5B/etOIZPt0ehsLgUbrbm+OzO9nhtVGvYWXJwFVF9YyAjIqImS+a5eNlbQN6jfr6DvWTU9HvNPrytHVbP7IYxUn7eCEjNLcLCnecxYuEBfLT1HBKzCq4Z6KQQzuRvgxFyqYLjmE5uWPNANwwIcG7EV0JkWBjIiIioyTI3NcacAc3U7e3hKTgak6HtJhE1ynp8b41tgw0PBWFSd09YmBojp6BELQMh88uqcz41Fw/8eBwfbo1EflEJXGzM8Mnt7fDWmDawZ68YUYNiICMioiZNFqpt6VpWkW7B9iiUasZ0ETVxUsjmxWEB+H1OEO7v44PuvvYYEOBUfn9cRj7CkvKw8lAcJi09iiOXPrAY0cFV9YoNau2ixdYTGQ4OBCYioiY/jOvRgf548tdQHI7JwO6INNxU4U0pUVPnYmOOxwY1Vx9GGBkZle//bEcU/jiVXP69k7UZXhrWUhULIaLGwx4yIiJq8ga1dkYXbzt1e8GOKJSwl4wMUMUwtvNsCv4KvRzGhK+jBSzMjNmLTNTIGMiIiMgg3og+Nsi/fJ2mP08labtJRFohvWRSuOPx1adQUgpYmBjBw85c3XcsNguP/nJKFfX442SiKvJBRA2PgYyIiAxCj2YO6N+ybDHbL/49j8LiEm03iahRSbGOlzeFqcIdkrVauFhhyW3NsWl2d1XAQ9OLLB9aPL/hDP45XbkHjYgaBgMZEREZDJlLJmLS8rA2OF7bzSFqNPGZ+Zj5wzFsPJ6ovr+5lRO+m9oJvg7mMDYyUgU8vpvWGV/d1Ukt/OztYIHBbS6Xus/MK0J2fs3XMiOimmNRDyIiMhjtPG0xor0r/jiVpBbLHdvJHVbmJtpuFlGDCrmQiafXnkJiVqH6/oF+vnhYloMoLUVKduWhvT39HdQmC0mbmVz+3P7bvTFYfTQeU7p74q4e3qoACBHVD/aQERGRQXl4YDOYGhshKbsQPxyM1XZziBrU+pB4zPzxmApjlmbGeH98Wzwy0F/1il1LxbXHZHiv9ChLSPvyvxiMXHQQ//snAhcz8nn1iOoBAxkRERmUZk5WmBjooW4v23cBabllvQZETUlRSakKTa/+Ho7C4lJ42Vtg2dTOGN6+9iXtpads5X1dcU8PLxXq8gpL8OPBOIxZfAivbgpDZHJOg7wGIkPBQEZERAbnwX6+sDQ1RlZ+Mb7ZE6Pt5hDVK/mQYe7PJ1RoEkF+9vhhRiDaedjW+Zge9haYN6QlNs/pgVn9/VQPmoS+9ccSMPGrI/g3PKUeXwGRYWEgIyIig+NuZ4F7enqr2ysPxSEunUOvqGkIT8zG1O9CsC8qXX0/qbsnFk/pCOd6mvMlc8dk/tnmOUF46pbmcLM1g4OVKXo2cyh/jJTLl/L6RFQzDGRERGSQZvT2gYOlKQqKS7Fo13ltN4fohm09k4zpy0NUFVGZJ/l/IwLw4rCASsU56ouNhSmm9/bBptk9sHhyx0rFcZYfuIB7lx/D9rBkLsJOVAMMZEREZJDsLE0xs6+vur3xeALCEzkPhvRTSWkpluw6j6d+DUVOQYnqxZLy9Xd09Wzw5zY3NVbVSyuudbZifyxCYjPxxJpQ3Ln0qPr3xXX/iK6OgYyIiAzW5CAveNqbq0VyP98Rpe3mENVaTkEx5q09jUW7otX37T1s8OO9gejmZ6+Vs2luYoTXRrVGd9+y5z+blIP/2xiG8V8eVsOD8wqLtdIuIl3GQEZERAbLwtQYc25qpm5vD0/BkegMbTeJqMYupOXh3uUh2HImWX0/ooMrvpnaGV4OFlo7i7KW2U0BTqod307tjAEBTmp/bHo+3vs7AqMWHcLh6LL5bURUhoGMiIgM2phO7ghwtVa3P90RyWIEpBf2R6bhnu+CEZaYA1lR7PFB/nh3bBtYmenOQufdfO3x2Z0dsOr+riosGhsBOYXFaOFS9u+NiMowkBERkUEzMTbCYzf7q9tHYzLxb3iqtptEdE3rQuIx5+cTSMstgq2FCRbc2R739fFVvVO6qI27Dd4b1xbrZ3XHG6NbqzluGjK/7J2/zqrePiJDdXkZdiIiIgM1sJUTuvraqUC2YEeUGnIlQY1I16w4EIsPtpxTt/2dLfHp7e3RXE96nPycrNRWsTz+l7ujcT41D2uOXMSIDm4qWLZy04/XQ1Rf2ENGREQGT3oWHh/UvLwIgXxqT6RLZF2vhTvPl4exTl62+H5aF70JY9WRhaXHdnKHk5UpikuBTScSccfSI3hi9SmEXMjUdvOIGg0DGRER0aX5LoNaOatzIeuSSfluIl0pa/+/f86p3iTRy98BS6Z0hINV/Sz2rM2iOg/298Pvc3rg2SEtVMVTTYEdWU/tgR+PqYWuiZo6BjIiIqJLHrnZXxUeuJhRgFWH43heSCd6kV7ZFIafDpX9Pg5q7awKZcjCzE2FLCp9dw9v/PZQEF4f1QotXMqGNUrVU10qUkLUUJrOv2YiIqIbJHNXpOrihmMJ+Pq/GEzo4qEWkCbSBumlfX79aWwLS1Hfj+nohtdGt4ZpE53faGZijPFdPDC2szu2nUlBRHIOfBwty+/fG5mGi+n5GN3JTT2WqKngbzMREVEFsi6ZLG6bnleEZfsu8NyQ1hZ8fuyXk+VhbEp3L7wxpumGsYqMjYxwa1sXPNjPr9Icus92ROG1zeEYs/iQKm6SW8BFpqlpYCAjIiKqQBbVnRLkpW7/cCAWiVkFPD/UqNJzCzF75QnsiypbQHlWP188N7SFCiqGKrewBO62ZXPM4jMLVHGTkYsOYsmu8+p8EekzBjIiIqIq7u/rq9Z3yisqUW/4iBqLfAAw88fjCIktqzL49ODmeHigv86uMdZYrM1N8PHt7bHmgW4Y08kNJkZQ67At2hWtgtlHW88hiR+ekJ5iICMiIqrC0cpMrYck1gbHIyoll+eIGpwsjnz/CqksmKOKy7w6shWm9fLhma8gwNUab41pg99mB6lhnFKpMaegBN/vj0VKDnvKSD8xkBEREVXj7h5ecLM1U+sjff5vFM8RNShZ/27GimOITstT88TeH98WEwM9eNavwtvBEs8Pa4nNc4Iws68vRnV0Qxt3m/L7wxKycTqeJfNJP7B0FBERUTWk3PZD/ZvhrT/P4u/QZByPy0QnLzueK6p3J+IyMXfVSTUEz9LUGB/d1g79WjrxTNeAs405Hr3Z/4r9n2yPxO6INNwU4ISZfXzRzc+e55N0FnvIiIiIrmJ8F3f4O5eV3V6wPUpVeiOqTwfPp2PWTydUGJN5i4smd2QYu0GZeUWITC4bZrzrbCru++EYZqwIwb/hKfw3TDqJgYyIiOgqZK2jRwaWffq+PyodeyLTeK6o3khAkJ6x7IJiOFmb4eu7O7Enpx7I2oHrHwrC22Nbq7UFxdGYTDy2+hQmf3MUm08mqgW3iXQFAxkREdE1DGnrgo5etur2p9uiUMJeMqoHEgqe+jVULf7saW+Ob+/phHYeZb9ndONkHt7oju5YdX9XLLijPQJ9yoYbn0nMwQsbzuDYhbIqlkS6gIGMiIjoGqTc+BODynrJTidk489TSTxfdEN+ORKHFzecUb00MiT223u6oLlLWU8O1S9Zu21gK2csm9oZS+/uhH4tHFU46+prV2mpgez8Ip560hoW9SAiIrqOnv6O6o3cf+fS8MW/51WvmQxnJKqtb/fG4NPtZVU727rbYNHkDqowBTX8BytBzRzUlltYXGldtw+3nsPus6lqQfi7enjD2dqMl4MaFf83ISIiqoHHLlVyi0nLw5qj8TxnVGvL9l0OY9JD89XdnRjGtFRBVSM9txDbz6QgM78YX/0Xg1ELD+L9vyNwMSNfG00jA8VARkREVAPtPG0xsoOruv3l7mjkFBTzvFGNrTwUh0+2lYWxXv4OqpqivSUHKmmbg5UZ1s/qjqk9vWFpZoy8ohL8dCgOYxYfwiubwnAuOUfbTSQDwEBGRERUQ3MH+qtiASk5hVi+/wLPG9XIupB4vPd3RHnP2Ce3t6/US0Pa5WFvgWdubYE/Hu6Bh/r7qaAs8/s2HEvAbV8dQQgLgFADYyAjIiKqIV9HS9zR1VPd/m7/BaRkF/Dc0TX9cTIRr/8erm538LTFZ3d0gLU5w5gucrQyw5wBzVQwe3pwc7jZmqOFixU6eV+ufllQVMK1zKjeMZARERHVwqz+vrA2N0ZOQYmac0J0NVvPJOOl385AVrxq7WaNhZM7qDWySLdJYJ7WywebZgfh49vbq0qNGh9tjcS070PUteUSGFRfGMiIiIhqQSriTevpo27/cuQiLqTl8fzRFf6LSMVz60+juBRo7myFxVM6qh4Y0h/mpsbwd7Yq/z41pxBrQ+JxPC5LrSF3x9Ij+O1YAgqLS7TaTtJ/DGRERES1NL2XN5yszdQ8ky92nuf5o0oORKXjyV9DUVhcCh8HCyyZ0hEuLG2v9xytTPHxbe0Q5Gevvo9IysXLm8IwbslhVbRFyukT1QUDGRERUS3ZWJhiVj9fdXvziUSExmfxHJIiBSAeW30S+UUl8LAzx5d3dVJFI0j/ydpl/Vo6Yek9ndVC0wMDnNT+uIx8VbRl1KJDOB2fre1mkh5iICMiIqqDO7p5qt4PmR/07l8RnE9COHUxCw+vOoHcwhK42JipMObjaMkz0wR19bXHgjs74Jf7u6rlMIyNADMTI7R0vTzEkaimGMiIiIjqwMzEGPOGtFC3gy9kqhLZZLjCE3Mw5+cTyMovhoOlqRqmWHH+ETVNrd1t8O64ttjwUBDeHN1a/V3QWLonBm//eVYtJk90LQxkREREdTSotQtublU2bOnTbZFIzy3kuTRAUSm5mL3yONJyi2BrYYJFUzqilZuNtptFjbwkRu/mjuXfZ+cX4bt9F1Thn/FLDuHF384gPJHDGal6DGREREQ34NkhLWFpaozU3CJ8tiOK59LAxKbn4aGVx5GUXQgrM2N8fmcHtd4YGTaZbzZNiv9YmapKm7+fSMQdS4/i8dUnEXwhQ9vNIx3DQEZERHQDZI7QzEsFPtYcjcex2EyeTwMRn5mPWT+dwMWMAliYGuPT29uruUVEspbZg/388PvDPfDckBbwulTYZUd4Ku5dfgwzfziG6NRcnihSGMiIiIhu0L29fODvbKkKfMickeISuUVNWUp2AWavPKHmB5kaG+HDie3Qq8KQNSJhZWaCu3p4Y8ND3dUcs5YuZfMKzyRkq6UziAQDGRERUT0sIPvCsAB1OzQ+W80boaZL5grO/vkkziXnwsQIeH98W9x0qQQ6UXWk2MfYzu5Y/UA3fHRbOzw5uDlsLUzL75chjWuD47nItIFiICMiIqoHfZo7Ynh7V3X783+jkJRVwPPaBGXlF+HhVSdVD4cRgDfHtMGtbV203SzSE8ZGRhjcxgW3BXqW7yssLsGn2yPx+uZwjF58CMv3X0BOAReZNiQMZERERPXk6cHNYWNuokqff7wtkue1icktKMZjv5zCibiyhcBfGdkKozq6abtZpOcy84rQ0ausEExCZgE+3BqJkQsPYvGu86zcaiAYyIiIiOqJu50F5gxopm5vOpGIA1HpPLdNRH5RCZ749RQOx5RVyJNCDRMDPbTdLGoCnG3M8dFt7fHrA90wtpO7mpOYnleExbuiMWrxYSzem4DUHC6p0ZQxkBEREdWjKUFeaONmrW6/+/dZzglpAopKSvHc+tPYF1kWsB8f5K8KNRDVp5au1nhzTGtVAOSuIC+1nEZuYQnWn0pjoaAmjoGMiIioHsmn2y8MLyvwEZGUixUHYnl+9VhpaSne2ByO7WEp6vtZ/XxxX5+yZQ6IGoK3gyWeG9pSlcyf2dcHt3dygqutefn9h86nI/Ri2bBZahoYyIiIiOpZN197TOjirm4v2R2NuPR8nmM9JXMBNxxLULendPcqH5JK1NCcrc0wd0AzPNDz8jzFktJSvP3XWUxZFoy5q06ocCYfGpB+YyAjIiJqAI8Pag4HS1PkFZbggy3neI710Ld7Y/D9/rIezhEdXPHs0BYwMpLaikTaEZ9RgIKisgC2OyINM388jhkrjmFHWAqDmR5jICMiImoAsujro4P81e0tZ5Kx82zZkDfSD7Im1Kfbo9Ttfi0c1aK+UrKcSJu8HCywblZ3vDO2DVpfmqsafCETj685hTu/OYpNJxLUnEfSLwxkREREDeS2QA90ulTO+v2/zyGvkGsL6YOtp5Px5h/h6nYXbzt8OLGdWtiXSFfmqcpyC6vu74oFd7RHoI+d2h+emIOXN4YhNj1P202kWuJfFyIiogYiPSovDQ+AsREQk5aHb/de4LnWcQei0vDchtOQToYAV2t8dmd7WJmbaLtZRFeQ4bMDWzlj2dTOWHp3J/Rv6Yhh7VzRzMmq/DFRKblqMXPSbababgAREVFT1t7TFpO6eWHl4Tg1J0k+2fZ3vvyGiXTHyYtZeGJNKAqLS+Flb4GFkzvAwcpM280ium4wC2rmoLbC4pJK9736e5jqOZPlOO7u4a0KhZDuMcgesvDwcLi6ulba/P3LxvkTERHVt4cHNoOLjRkKikvx/t8RnHyvg6QnYe6qk8guKFbz/xZP6QgPOwttN4uoVioOrY1OzcWJuCxk5Rfj6/9iMGrhQfX3h1VfdY9BBrIWLVogNDS0fHvggQcwePBgbTeLiIiaKHtLUzw1uLm6/d+5NGw5naztJlEF8Rn5mL3yBFJzCmFjboJFkzqwF5P0np+TFTbODsL0Xt6wMjNGXlEJfjoUh7FLDqm5ZhFJOdpuIhlyIDMxMSnvGXNxccEvv/yCmTNnartZRETUhI3q4IYezezV7f9tOYdszuvQCWm5hZiz6gTiMvJhbmKET25vj3aeZYVYiPSd9PI+NbgF/ni4B+bc5KeW4pAqjL8dT8DtXx9RvWikfToVyEJCQvDss89ixowZSEgoW4SxKln87qeffsLcuXPx9NNPY/fu3Tf0nFu3boWpqSluuummGzoOERHR9eZ5vDAsQFVIS8gsUAtGk3blFBTj0V9OIiIpVxVeeX98W/T0d+BloSZH5kI+dFMzFcyeubUF3O3M0bu5o+pF05APibjItIEHsj59+mDatGmIjIzEd999h4yMjGofN2nSJBXaAgICYGVlhSFDhuCzzz6r9Ji+ffteMUdMs+Xn51d67Ndff83eMSIiahRStW9aL291+4cDsQhPzOaZ1xIpfvD02lAci81S378yshVuaePC60FNmlQMndrTG5tmB+GN0a0q3ffib2cw9fsQtexDSSnXMjPIKotLlixBYGAgVq9erYYQVmfDhg3q/iNHjqBr165qn4Ss5557DpMnT4a7u7vat3nzZhQVVV/i08Li8gTdlJQUbNy4ER9//HGDvCYiIqKqZvXzw+aTibiYUYB3/ozA0ns6qd4zajzFJaX4v41h2HMuTX3/5C3NMaGLBy8BGVTxD/cKRWsik3OwIzxV3X5qbShaulhhRh9fjOzgyjX4DKmHTMLY9axatQpdunQpD2NCetXy8vJUWNNwdHS8ag9ZRcuXL1c9bJ6envX8aoiIiK7+CfWzt7ZUtw/HZGDj8USeqkYkQ7Le/ycCf55KUt/P6O2De3v78BqQQZOlOGSZB80814jkXLyyKQzjlhzGTwdjkctF7Q2jh6wmjh07hg4dOlTaJ0U5pGfs+PHjtT7e0qVL8c4779TosTKEsuIwyri4OPW1uLhYbdokz19SUqI20l2aa8TrpLt4jfRDU7hON7dyxE0tHbErIg0fbTuHAQGOqhJjU6HL12jRrmisOnxR3Z7Q2R2PDvTTyXYa+nWixr9Gffwd1BYSm6kWsZceMyl28/4/5/Dlf9FYPq0LvB10ZymIkpISnXgfLm60DXr111+GGErvV1WyT+6rS0EPCXQ18dFHH+H111+/Yn9ycnKlYZDakJ2djdzcXKSmpnLYi45/KivXSoYmcXiSbuI10g9N5TrN6uGE/VHpSM0pwkd/h+Gx/k1nyJyuXqO1J1Lx1Z6yomH9/W0xp6ej+r/TUOnqdSLtXiNfS+DlQe6Y2sUBP4ekYOvZDLhZm8CiKAspKdnl7dL270x+fr6aoiTvgbWtLjlEbwOZsbFxtZ8QyD4pZV9bVYcwXstTTz2l1iur2EPWq1cvFejc3NygTebm5sjJyYGTk5M6R6Sb5PdU/oDxOukuXiP90FSuk7MzcH+fQizeHYPfTqVhUg8/dPBqGuXWdfEayby9Ly6FsR5+9vjg9vawMNWNtmmLLl4n0p1rJH+jglp54UJaHtLziuDicvnv0zt/RaivssaZr6OlVi6bBFV7e3vY2mr/76a8DzeYQCZzvRITrxxrL/s8PBr2k0W54LJVJUGwLmGwPsnzyz9SzUa6i9dJ9/Ea6Yemcp3u6+uHTSeTEJ2ah7f/isCyaV2aTEjQpWu062wqXv39rLrd3sMGn9zRHlbmevUWyCCuE+nmNfJztoZfhe/j0vOxLiRBrWf2a3A8hrd3xf19fNHa3aZR22VsbKwT78PFjbZBr/719ezZEwcPHqy0Lzw8HGlpaeo+IiIifSLh64VhZQU+TsVn480/wrkOUD3bF5mGZ9aGqjeP/s6W+GJSB9haMIwR1ZWdpQnmDGgGJ2szlJRK73MS7vzmKB775SSOxFS/bBU1oUA2c+ZMxMTEYOXKleX7PvjgA3h5eWHUqFFabRsREVFd9GvhhJl9fdVtqbi4bN8Fnsh68m94ilr4Oa+oRC2Eu2hyRzjbmPP8Et0A+UBD/mZtnhOE54e2hJd9WS2Ff8+m4r4Vx3D/imNIyirgOa4FnfmI6L333kNoaCiioqLU97K2mJ2dHUaOHKnWGBPdunVTxTXuv/9+/PTTT6qgxqlTp7B27Vq1SDQREZE+mjuwGSKScrAtLAULtkeh5f+3dx/QVVX5Hsd/CQFCrwmEDqG30HFAioiCUhQsWJHnCFYEnkYFfYOOw3NGlEEdFbGACvOsiBRFEEEFREQk0puUIIQEIoQWSHvrvzGZoKgpwDk3+X7Wuiv3XLnJ9ux17zn/vf/7vyuVVLcGFb1uVkCbv3G/xsza7GbG7IZx8vXNVK2cN2tdgIIotGgRXdc2Qle1qqJ56/dryvLdrlx+wtGTbvYMARiQ2d5imfuB3XLLLVmvR0ZGnvbvRo4cqYEDB2r58uUKDQ1V9+7dz7i2CwCAQBEcFKRx/RpqyJvfa3PCMY2evUmv39TyvK/JKChmr4nX2I+2uHSqWhVC9dJ1zRXho3LdQEHbZLpfi3D1aR6mxVsSVSQoSEWC/1OB8Y2vf1Sp4kXUr3m4ihWQNbIFNiDr3bt3jv9trVq13AMAgIKiZLEirtjETa9/r8RjKRrx/gZNuyVKFRlpzpV3Vu3NqgBXP6ykJg1qpsqlSVMEzsfAUo+Gp28ndfB4il5cskvHU9I1aUmsbm5fTVe3ruq+7/AfhKkAAPiEpdRNGNhYRYsEac+hE7p/xkalpLFpb07ZSHxmMNa0aim9ckNzgjHAQ8dPpqtzvQqy+bKEIyc1YdEO9X5hpV74cpcL1nAKARkAAD7SqkZZ/U/v+u75qt1JGvfJNiov/gHbp2nSkl3uZs9EVS/j0hTLl2AdC+AlSxV+akBjzRjaWv1bhCskOEhJyamavDTWBWbjP/3BHRd2BGQAAPiM3bjc0rG6e277/Uz7Zo/XTfJ1MDZx8U6XDmU61i7n0hTLhPpmVQZQ6NWtVFJ/7dNAs29vq+vbRig0JFjJKemat2F/gdl7MT/4tgIAwIfu7VZb2/cfc6Wk/7loh7uhuTCygtfN8pX0jAz9fcEPemdVnDvuElnBjcZzgwf4d8bswUvqaWjnmnpr5R63DUXxbAHZZ5sOuH/TpGppFSaEpAAA+JBVKfvf/g0VWbmkqxb40KxNrjQ+TrFy9mPnbs0Kxi5pXMmtvyMYA/zPihXd1bW2K5uf6XhKmh6ft1XXT43RnW+v08pdhwpNujYBWS6NHz9e4eHhioqKOjc9AgBAtg1Yn7m6icqXCNGRE2ka8d4GFsJLrtDJ6FmbNHttvDtPVk77if6NXPltAIHpx4PJ7jvPfLX9oG7791rdMm2NPt+S6GbDCzK+uXIpOjpa8fHxiomJOTc9AgBANjXKh+rpAY3dYvjYg8m6/4PCXXnxRGq67puxUQs2HnDH17Suqsf61HfnB0Dgqh9WSh8Ma6O/92+ohmEl3Wvf/3jYbQFy7WurNXdtvJsZL4gIyAAA8Lm2tcppTK967vnKXUn6x4IfCk0qT3bHT6bp3nfXu3V1ZnCHahpzaT23/xGAwBcSHKTeTcP09q2t9Nw1TdS6Rln3+taEYxo3f5uOnUxTQURRDwAAAsDAqKralnBM01fu1Xur97nR5OzrLwq6w8mpGv7eeq3efdgd3965pu64sKaCCMaAAicoKEhdIiu6x3exSXp1+W7Vr1xSZbNVT92ccEyNi5dS6QJQ/4OADACAADGqR11tP3Bcy7YfdPv31K5YQn+qW14FnW0ge9fb67Q+7qg7HnlRbQ3pWMPrZgE4D1rXLKt/1Wx62joyS118+KMdurTpMT16ZeB/B5KyCABAAKXz/OOKRqpbqYTSMqQHZm7UzsTjKsj2HznpFvdnBmOjL61HMAYUQsHZZsM3xB3RT8dTdWP7U/s1BjoCMgAAAohteDzxqiYudefwiTTd+956JSWnqiCKSzqhW6evcetHrGaHbSw7qE3hSdMEcGYtqpXRh7c2U0S5UBUEBGQAAAQYS1V86spGKhIk7UxMVvTMjQWq+pgVLJmzNl7XTVmtXT8lu5lBK2vfv0W4100D4BOlihVRQUFABgBAAOpQp7wevORU5cWvdxzSU59uV0HZi+iud9brkTlbdPB4qkKLBruy/72aVPa6aQBwTlDUAwCAAHVtmwht239Mb6+K01ur9ioyrISuaR2YKX1p6Rn698o9ev7LXUpOObXPWqe65fVwr0hVL18w0pIA4EwIyAAACGD3X1xXOw4c19c7D+kfC7araHCw+rcMD6i9uTbtO6q/ztuqdXuPuOPyJUIUfXFdXd4sjLL2AAo8UhYBAAhgRYsE68krG6lmhVC3juzRj7fqxqkxWrnrkPwuOSVNz36+Uze+HpMVjPVpFqYZQ9uoT/NwgjEAhQIBGQAAAa5ciaKafF1zdY2s4I437DvqSsWPen+Db8viW8B47Wur9dpXu10gGVG2uJ6/tqnG9WuoiiWLet08ADhvSFkEAKAAiChXXM9e01TLdxzUhIXbtTnhmBZtSdSX237SoDZVNaxzTRe4ec1K9E9ctEMzYva5Y0usvKFdhO7uWlslC1DVNADIKWbIcmn8+PEKDw9XVFRUbt8KAMA5d0Gd8vq//2qlRy+rr8qlirrZp+kr96rfS6s07Zs9Skk7VTDDCws3HdDAl1dlBWMNwkrqjcEtFd2zHsEYgEKLgCyXoqOjFR8fr5iYmHPTIwAA5FOR4CBdGVVFs25vq2Gdaig0JNjNTD21cLsGvvKdPtt8wO31db7sO3zCpU/e98FG7T+aomJFgnRP11r695Aot8ErABRmpCwCAFBAWQrgXV1ra2CrqvrX5zs1Z12CYn9K1n/P2Ki2NcvqvovrqmnV0ufs76dnZGjGd3GauHiHjpxIc6/Z3/2f3pGqU6nkOfu7ABBICMgAACjgqpYtrr/1a6jr20Vowmc79G1sknvcMDVGfZuHaXjX2qpStvhZ/ZuxB08qet46fbf7sDsuXbyIRl5URwOjqgRUSX4AONcIyAAAKCSaRZTRKzc016LNifrnoh2KPZisOWsT9OnGAxrcoZqGXFAjx2u5jp9M096kE6ceh0784nmy4g+fVPrPWZE9GlbUQ5fUU3iZsxv0AUBBQEAGAEAhEhQUpB6NKqlL/Qp6e1WcXlqyS4dPpGnyst2u2MY9XWurX4twHUpOVdzPAdaeQ8ku2Mo8tucHj6f+4d+yoiKjL43UxY0qnZf/NwAIRARkAAAU0g2lb2pfzaUsTl4aq3dWxbmCG7ax9N8+2eaqM+aU7RtmZfdtLzF7VC1bTGWCU3RR0+oq44NS+wDgZwRkAAAUYuVLFNUDPetpUJsItz+Y7V2WPRgLCQ5SeJlip4KtzKDL/QxVtXLFVaVMMYUWPT3NMT09XYmJiSpVnH3FAOCPEJABAADVrlhC/7yqidbtPewqMVqRj2pli6ty6WKujD4A4NwgIAMAAKcV/rAHAOD8YGNoAAAAAPAIARkAAAAAeISADAAAAAA8QkAGAAAAAB4hIAMAAAAAjxCQAQAAAIBHCMgAAAAAwCMEZLk0fvx4hYeHKyoq6tz0CAAAAIBCg4Asl6KjoxUfH6+YmJhz0yMAAAAACg0CMgAAAADwCAEZAAAAAHiEgAwAAAAAPEJABgAAAAAeISADAAAAAI8QkAEAAACAR0K8+sOBLjU11f3cu3ev101RUlKS9uzZo6NHjyooKMjr5uA3ZGRk6PDhw0pMTKSffIo+Cgz0k//RR4GBfvI/+uj3z82RI0dUqlQpeS0zHsiMD3KLgCyPEhIS3M8OHTrk9VcAAAAAKCC2b9+uOnXq5Pp9QRkWXiLXkpOTtWbNGoWFhSkkJMTzqNwCwxUrVigiIsLTtuC30U/+Rx8FBvrJ/+ijwEA/+R99FBhiY2PVqVMnbdmyRfXr18/1+5khy6PQ0FC1b99efmLBWI0aNbxuBv4A/eR/9FFgoJ/8jz4KDPST/9FHgRMf5AVFPQAAAADAIwRkAAAAAOARArICoGzZsho7dqz7Cf+in/yPPgoM9JP/0UeBgX7yP/qocPQTRT0AAAAAwCPMkAEAAACARwjIAAAAAMAjBGQAAAAA4BECMgAAAADwCBtDB7idO3dqwYIFSktLU/fu3dWoUSOvm4QzWLJkib7++mu1a9dO3bp14xz5TGpqqr788ktt2rRJYWFhro8qV67sdbOQzYcffqgtW7a45yVKlFBkZKQuvvhiFS1alPPkU8uWLXOPP/3pT+rcubPXzcHPli9f7q5Jv3TllVeqfv36nCefWb16tbt/KF++vHr37q1y5cp53ST8bOrUqdq/f7/O5Oabb1aVKlWUU8yQBbDp06ercePGLiBbunSpWrdurWeeecbrZiGbmTNnuiD5oYce0iOPPKLZs2dzfnxm3rx5qlevnoYNG6aYmBg9//zz7mb/zTff9LppyOann35SXFyce6xZs0b33HOP+2xt27aN8+RDCQkJGjhwoB544AF9/PHHXjcH2Xz66acaN25c1ucp83HixAnOk49YfwwaNEiXXnqpC8jmz5+vTp06ae3atV43DT87cODArz5HL7/8srvnyy1myAKUdbrdQD722GPugmd69uypW2+9Vb169XKBGrxXoUIFzZkzRw0aNGDGxcejxQMGDNDTTz+tkJBTX4kPPvighg4d6i6EuRnhwrkzZMiQX92s2CDU8OHD9dFHH3HqfcauT1dccYUbOIT/VKpUSU899ZTXzcDvuPfee93s2IYNG1x/mX379unIkSOcN5+47777fpVt88Ybb+iyyy7L9b0DM2QB6t1339XJkyd15513Zr124403ug3ppk2b5mnb8B+W+mbBGPzLPjc2s5wZjBkb2bcbfrsYwp+KFy/uPl/ff/+9103BL7z++utauXKlxo8fz7kB8mD37t169dVX9eijj2YFY8Zu8i2DA/40d+5clx1gkyO5xQxZgPr222/dh7JMmTJZrxUpUkQtW7Z0/w1AzpwpYF6/fr37Wbt2bU6jT2VkZGjVqlVq1aqV101BNrGxsRoxYoQbGLQBQvjTsWPH9Morryg9Pd2l/nbp0kXBwYzR+ymt1GoD9OjRwy11sM+VpdbbcbFixbxuHn5nTVl4eLj69u2r3CIgC1Dx8fFnTIGzkRQr9AEgbxITEzV27FhdcMEFpP76TFJSkiZPnuxuJu2GJTQ0VC+++KLXzUK2INlSS+1mJC83JDh/LPj64osvXJ+NGTPGDT698847zL74xI4dO1zWxg033KDk5GQ1b95cEyZMcMGYrXuuVauW103EGdaT2QyZpdHnpdgUAVkACwoKOuNr9gULIPcsTfGqq65yN/wU9fAfGzG29bNHjx51wZndVFpqT82aNb1uGiQ999xzruCK3djDv2xtX3R0tEv7zRyE6tChgwYPHuwKhMF7NnNp65Gs6u9bb73lXrPvvRYtWmjUqFF6//33vW4ifsHWy6akpOQpXdEQkAWo35oJsy/W7PnGAHLGLn5W0crS4D777DPKP/u0SE72QgR2A2k3l5bOk3lzCe9YYZw2bdpoypQpWa/ZDYoVzrF+u/vuu92WBfCW3dRnV7FiRd1+++2uQJhVM7XPGbyVmQFlpdMzlSpVyg0YZv98wV9rZzt27KhmzZrl6f0kDAcoWzexdetWHT9+POs1mxlbt26doqKiPG0bEIijkZZqZWlwVqK7bdu2XjcJOdCvXz+3gNrSe+A9q0xqqVXZS0DbdclG9jOfw58yixrZLDS8ZxVkM2sDZGfH9JH/2FYENpib19kxQ0AWoK6++mp3cbPymplmzZrl1pZZzjGAnLOR+xkzZrgtCmyfF/iLfdedae+dTz75xBU2ImXRH2yvRZsJy/6wNS+2gbc9L1mypNdNhOT2W8zOBnatGIHNnJ1pbTrOP7sOWRGPDz744LQsDrtGWXVZ+IvNWtr323XXXZfn30HKYoCyBbhWUnjkyJGuIpyl60yaNEmjR4/OGlmB9zZt2pS1GbRd9KwUdOZNiu0xAu9NnDjRfXYuv/xy1z/2yGR7ieQ1/QBnNyCzkceIiAhXSdaOrSCB3VhapThu9IGcs/sGK4hj6aW2fY6tR7LPlA1KwR9sJsxS4Oy6ZFkA9r1nBSNsttmuWfAPC5Rt/Zilk+ansmxQBjkEAc1K3NumqDaFbaOQVroW/mH7WJ1pXzgLoMeNG+dJm3C6mTNnasmSJWc8LTba1a5dO06ZTyxcuFArVqxw33d16tRR//79Ka3ucw8//LC7LvXu3dvrpiCbxYsX66uvvnLPGzZs6NJ/KafuP3v27HH7zloFP9uixW76GYDyF1vDbHuZ2nq//CwZIiADAAAAAI+whgwAAAAAPEJABgAAAAAeISADAAAAAI8QkAEAAACARwjIAAAAAMAjBGQAAAAA4BECMgAAAADwSIhXfxgAAAAAcuqHH37QtGnT3KbZY8aMUa1atXQ+LViwQEuWLNHRo0fVvXt39e3b96z8XmbIAADIhYyMDB08eDDrkRvZ32e/BwCQM1dccYUuueQSrV27Vi+99JLi4+N1Pg0ZMkSDBg1SWlqaypYtq2HDhmnw4MFn5XcHZXBFAADAOXHihE6ePKkyZcr85hmJi4tTRESESpUqpZCQkFwFZeXLl1dqaqobXY2NjVWNGjU48wCQAytWrFD79u21cOFCF5h98803ateu3Xk5d7NmzXIB4eeff66uXbu61zZs2KDmzZtr5syZ6tevX75+PzNkAAD87K677tLAgQNzdD7mzJmTpxkyex8AIHc6dOigoKCgHP3bZcuW6S9/+YtGjBihKVOmKCUlJV+ne+nSpW6gLjMYM02aNFG9evU0ffp05RcBGQCg0MtMQ7SLeOvWrfOdVmgzbQCA82/EiBHq1q2bEhISXDbDhAkTdNFFF7nshLwqWrSoC+p+eU2wjIqYmJh8t5mADABQ6K1fv94tDt+4caNefPFF1alTRw0bNszVBTw9PV3333+/KlWq5FITq1evrrFjxxKcAcB5MnfuXD377LNuVsy+yx966CE3u7V582b3Wl717NlTycnJevPNN7Nemz9/vnbt2qUDBw7ku91UWQQAFHrNmjVzF3JLR7GgzIKp3Jo6dapee+01LV68WC1btnSjs7bw3NYZtGrVqtCfYwA412bMmKHw8HDddNNNWa9ZAQ5LRbf/NnToUPfayJEjXYD1e6699lr16NHDPbeKio888ohuu+02vfvuu27GzNawde7cWdu3b893uwnIAACQtGrVKlWpUiVPwZixMsyVK1dW06ZN3XFYWJi7gAMAzo+EhAS3zuyOO+447fVvv/1WSUlJWcctWrT4w3Vl9h2e3eOPP+4qLdq1wjIiXn75ZRegnY36iARkAAD8fMFu06ZNns+FlT+2WbIGDRq4iltdunRRr1693OgsAODcq169uquA+8usBDsuXbp01vGf//znPP3+yMhI9zA2w2ZVF0eNGpXPVrOGDAAAx0Y927Ztm+ezYWvQLD3R1inYTNmTTz7pKnBZWgsA4NwbMmSI9u7d6wbXbJYs82Epi/ndZsTK7WeyWbHRo0erWLFiuvvuu/PdbmbIAACFno102toxq7CYH7auwNYa2MNKLtuo7Kuvvur2zgEA5N2kSZO0evVq/fjjj+74iSeecGmFF154YdaasY4dO+qFF15w2Qk2wGZBmO35uGPHDldtMT+soIcFYZaW/t133+nIkSOaN2+eKlasmO9uJSADABR6mWsArBJXYmKiG/XMnt6SE9HR0e49ffv2dTcBdsG2m4Bhw4YV+vMLAPlVt25d99MGuvr06ZP1es2aNX81SzZgwAB98cUX2r9/v3ufBWolSpTI19+3lHQLCK3MvQWAVlrfBuHOhqCMs7ESDQCAAGclkidOnKj4+Hhdfvnlv7nZZ1xcnNvbZtGiRW4mLNOhQ4f0zDPP6MMPP3QFPuwm4eabb9bw4cNPe79VYbQ9cWzUNr8pNACAwEdABgBALmQGZLNnz3apMrbnWE7ZZtNLlixxRT8IyAAABGQAAOSSzaDZptHZg6ycyh682WbU1apV4/wDQCHHDBkAAAAAeCTYqz8MAAAAAIUdARkAAAAAeISADAAAAAA8QkAGAAAAAB4hIAMAAAAAjxCQAQAAAIBHCMgAAAAAwCMEZAAAAADgEQIyAAAAAPAIARkAAAAAeISADAAAAAA8QkAGAAAAAPLG/wPUW+StjQ/8JgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "t_fit = 2.0 # Struphy time units, like every time coordinate of this run\n", "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", @@ -380,21 +254,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "16", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzwAAAIgCAYAAABAlWy6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAVcdJREFUeJzt3Qd8FNX2wPGT0IuhBJFeDCpKiUoREAsgiqIoWEBBwaeCBQtgVwQUfT4LYm+APLEhKiqoWJAiYgQBg0oVpCO9d5L5f871bf67qbvJ3cxk9vf9fOaT7GTn7rTdzNlz75k4x3EcAQAAAAAfind7BQAAAAAgWgh4AAAAAPgWAQ8AAAAA3yLgAQAAAOBbBDwAAAAAfIuABwAAAIBvEfAAAAAA8C0CHgAAAAC+RcADAAAAwLcIeADk2/fffy8fffRRTO7BDRs2yPDhw91eDVi2cOFCee211wrUxvvvvy+LFi0SP/jll1/ks88+c3s1itQ+9co+A/D/CHgA5BsBj/cCHgIxdwOev/76S+655x6pW7eu+IFux8033yxbt251bR2K2j71wj4DEKp4pscAkC/r1q2Tt99+Wx588MECB1Fz5syRqlWryuWXXy4VKlTI1/PT09Pl0UcfzfL8Rx55ROLji/Z3Pdu3b5cXXnhB7rjjDqlcuXK+25k9e7Z88803Of69VatW0qlTp7Db+/XXX82x6Nu3b67zomHZsmXy3nvvSevWreWCCy4Ief3p06fLXXfdJYVhxIgRcs0110i5cuXMY73ofemllzL+fvHFF0vz5s2zLLdmzRr54osv5OjRo9KtWzepWbNmWK8Xbvv5Xf7YY481+/Pll1+WIUOGiBsy71O1efNm+eqrr2Tbtm2SnJwsHTp0CLu9WNhnAEIV7f/6ADwV8DzxxBMFauPuu++Wq6++WtavXy9jx46VU089VTZt2pSv52vAM2zYMF9+y6oBj26b/sysRo0a8vDDD0fc5t69e02bf//9d77XS4OLN954I8950Qp4dP179eolu3btCnn9kSNHSmHQYOXdd9+Vq666Ktu/63pod6fMdJlGjRrJzJkzTeDTuXNnWbJkScSvn1P7BV3+iiuuMO8vN2S3T9955x1p06aNTJs2TdauXSvXXnutXHrppeY9Hyk/7jMAWZHhARC2gwcPyieffCJbtmyRtm3bZszfv3+/jBo1Sg4fPixDhw418/Rb6qZNm4bd9tKlS803uT/99JOcccYZkpaWZi5q/v3vf2d7wRru8/v37y8NGzaM+Cjv3Lkzox39Zlm/RT7//PNzXebAgQMyceJEs390Xfbs2WOCEr34UbqO+i2+Xpxrt5dLLrlESpcuLba7tI0ZMyZL0KNZjgULFpiMkL6u/tR11ElpoPPss8+arjgaOO7evdvsX9233333nVln7VakF6Cff/65CXBPPvlkufDCC03GTC/UP/30U/P6gXNAL9wzz9NMjwZlui/0G3q9sK9du7a5YA3si/nz55tJs0vffvutmXf99dfnue2a3dNjrYH3f/7znxyfl9trF4Sus75HdP8FVKlSJWPbs7sAXrVqldxwww3mfXXRRReZeRqwaQAajrzat7G8niO6ntq1rH79+nm2GTh++n75+uuvTSCi55wedxv79JRTTjFdD8uWLZvxHm/QoIH5LDjzzDOL5D4DEF0EPADCcujQIRPk6MVLu3bt5KabbpJ9+/bJaaedluMyGgDllfXRLE358uVN16p69eqZC2xVrFgx6d69e47ZgXCfr4OdtX29ENYLSn1epDRrdOutt5rtfvPNN7N9jl6U6UWOtn/uuefKLbfcYoKG008/3QQ8ui90eb2Y1e4uWuxBu7v8+OOPUqlSpYj2VThjeAIBj+M45vV///130/VGX0+7Hc6aNSvXCzFdd82YaADXrFkzqVOnjtkP7du3l+OOO87sdw1yn3vuOXNRGwkNBLULUlxcnJx99tkm+NB11gtW3T69yNX1f/755+W8886TJk2ahN22Bm3nnHOO2f96fkT62tntz9wyVBoo3X///eb3xYsXm/1UvHj4/1o1e6HnZlJSkjz99NOSmJhoAsVwu7QVBg2O9RzVogHhBjx6/J555hkTtK5YsUIeeughmTFjhslkFXSf6nsqWMWKFc3x1EC2qO4zANFFwAMgLHpxqxeLv/32m5QsWdIEQHrxovSb1htvvFE++OCDjG8+lV7Eh0u7ptSqVStknj7W+fl5vmYdAv3n9QJLB6Lr+B296EpISMhzffQiKnhbNEjQCxe9ENOL08w0ENJ9ot8868WZbntg/wT+rlkRzSqUKVPGXJzpGJknn3wy12xEQem4Ku3Wpcct8I24jmfRbfvvf/+b5/IDBgyQPn36mN9vv/128y29diUK7N8TTjjBvIZmYC67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AACAbxHwAAAAAPAtAh4AAAAAvkXAAwAAAMC3CHgAAAAA+BYBDwAAAADfIuABAAAA4FsEPAAitmHDBmncuHG2019//SUzZ840v+/Zs8f63rXZ9pQpU0xbaWlpVtYNkTnvvPNk7NixGY+jed5k135hHf9obxcAIHcEPAAidvjwYfnjjz+kV69e8sEHH4RMNWvWlN27d5u/R+NC0mbbO3fuNG05jmNl3RCZJUuWyNatW/N9bL///nsTSBw4cCCs52du3/bxz2l9ovl+AADkrXgYzwGAbNWqVctc4GV2zjnnyG+//SYJCQnsOYQt0vMm0kAi2udlTuvD+wEA3EWGB4B1CxYskB49esi+fftCug6lpqZKnz59pHXr1tKlSxeZNm1ayHJ6sRjoGnfqqafKJZdcIm+88Yakp6dH9PqB1/vll19MFuqMM86QSy+9VGbPnp3t8/V1C7pehw4dkqeeesp009J2+vXrZ7r3Bdu2bZs8+OCD0rZtW2nZsqUMGjRIduzYkeu25NVuuNsa7r7VdXz44Yfl7LPPNus5dOjQkIxFfrZBLVq0yJwTp59+unTu3DnLPs7uvMlt27Wb2J133ml+1/XQ7briiitC9sn8+fPlyiuvNK85evToLO0Hv25u++7999+Xs846K8v6nnvuufLOO+/kuT7Zve7KlSvN9rRo0ULOPPNMGTx4sAmYAsJ9zwAAwuAAQIT++usv7QPkjBs3Ltu/T5o0yfx9x44d5vH7779vHp944onOe++956SkpDh33HGHU7JkSWfZsmUZyx04cMD57bffzLRgwQLTfu3atZ377rsvx7azE3i94447znnrrbecWbNmOTfffLN5PW038/NsrNddd93lJCUlOZ9++qkzZ84cZ9SoUU7r1q0z/r5582anfv36TuPGjZ3PPvvM+f7775127do5zZo1c9LS0nLclrzaDXdbw9mGv//+26lbt67TvHlz55NPPjFtDR061Bk0aFCBtmH9+vVOhQoVnCuvvNKZMWOG8+GHHzp16tRxihUr5jz99NM5Htvctn337t3O888/b56vf9Pt+vPPP0P2Sb169Zx33nnH+fXXX50tW7bkeF7mte9efPFFJzExMct26XLPPfdcnuuT+XV1P2t7F110kdmHui9POukkp2XLlhn7MdxzEwCQNwIeAPkOeGrVquU0atQoY9ILMpXTheXMmTMz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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.struphy.plot.slice(\n", " x=\"e1\",\n", @@ -415,21 +278,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "18", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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fsBXL3oka80qSj9dA0F2IZ7KdpT1unXd6HxWGWDvvfdO+3f9c07BWf2uAuzKc1T20NjDQfWaUdLx1bAjyrMif0fXoO6DW221lS0PlUZFPGYAAAAoAx4AAABCb7fddlO/pt7GjRujpn/11Vd2+lFHHVU0bauttvL23ntv+9F3kZ+zzz47avlDDz202Dz6dOvWzVu3bl3RfAsXLvSaN28ed95HH320aL7x48d7DRo0iDvfww8/7LSvrr+l/dxrr728Hj16FJvvsMMO8woLC1PeTxk3blyJ+zB27NhA99V1HTfccIOd9u233xZbxxlnnOFVqVLFW7BgQdG0ESNGeNWqVSu2zho1angffvhh1PKdOnXydtlll6hpmvfMM88s9lvPPPOM/W7MmDHeypUr4263PjqGJa07lW1L5VzOlJdeesnLyckpti8XX3yx/ffHH3/slFbXXXedTS+Xz9VXX130+1OnTrXrOfroo4tt2/vvv2+/i5w/nlTXcc4559hp/fv397p27erts88+3vXXX1+0L6WRyrUa7/x68cUX4x6Piy66qOh4pHMupnt8EuXfseLtT7r5TMOGDb22bdtGTbvggguKjmXken744QfviSeeKJZu+tSrV89ug8ycObPE8zjRd76PPvrIq1WrVrHvlX+9+uqrUduqaYcffrjXoUOHovl69uzppUPpoI+uwyZNmth03rx5c9H3xx13nF3/n3/+mda2Kq379esXNU3XiOv5cs8990Qtq/tao0aNku7XrbfeWuyc9s2dO9d+17Rp06TnXWkl2o7yWPf06dPtPIMGDSrVb+nY6ji2a9eu2HEfMGBAuR/fSC1btvR23HFHLygV5ZgBAAAgeHRPDQAAkKWWLFlibr31Vvvv7bbbLuo7td5Qq7DnnnvOtpZRK0K1ZnzqqadsN4c+tay78MILbUsztT5TCyyNsffdd9/ZFnm+d99917Z2U6tmtSLTvGp9eOSRR5r169fbeTQepFrkLF++3LZOU6tQtdB64oknbCsZtd7R2JvJuPyW74svvrDzPvvss+brr7+2+6cWd9oPLZPqfqo1mfbh77//tq0qR4wYYdNv2LBhZvfddw90X1NZh9+NZWwrO7Xqe/31121LJL+loVoinnnmmbZlslogaxxErVut7NT95amnnlosHdOhFpHq7lZd4KqFVGQXuGoBH0862+Z6LmeC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Wvlj5rrwz20/T9D1outCQXSNx+nzW1lrHOVEglhHZJAt2UsXQeVJsfwXSvTSSWwPBDpGpTkX0z0+pRFEPuMiMi3US0OytEh0Hpf0nb8vat3qj5MdK9nLDUHRNiqorxeRFDwui231x7x1ETvmbbt27eyLF6Wxww472LJC48aNi5UZ/Osk2UsY2RR8XLNmjU230lIvCQoOR7YE1rH8448/osoEZXl8Nea4WsBrfHb1uqLztaIJ8pgBAAAgWASNAQAAKqkVK1bY/6tr40MPPbSoNdnjjz9ugz+RXV6re1RVoqobXr8ljipWFWD67rvv7N/qxlUtaFTJ2a1bN9t1tk8tGLVedTetQJACECUFB11+y6eAsVqKPfXUU7YFz/Lly23X0qoMP/DAA1PeT+2DWlH++9//tsETte5Vha1anqm1jyqNFRBKZV9Lks461EX1K6+8Yj9qcaxtVJfAkfbbbz8b9NM+qLXnnnvuaad/+OGHNoiqAELv3r1NIkoTtWJWoE1dD/stn7RNsRSw//XXX23LO+1PIkFsWyoSnWdBULehOj/UhfWOO+5oj6MMGzbMflJJq9tuu83861//cvpddePu03mvVoPvvfeeufbaa+161CpNLX7Vta7OZ78785Kksg4FTxXEPP/884uCa7o+HnnkEXtO6pr1z5lUpXKtJjoegwYNskFuP3j76quvmhdeeKFU52K6x6c0gshnXOjYq8viq666ynb5778goHxG+Z5eAogMrOs8njFjhg3ix7aALOm7I444wl4nOp+UX0e+ZKH5df7o3CsvBx10kNlnn33MfffdV3TdBrmtjz32mL0/uWjUqFHU3zoX9RvqLSPdc0kvGem46qWPZ5991t47dG3ohSN/25VnVQT+CzolvViRCvVOcfHFF9thP/TCioLFellN/DJFWR1fvWyhIRV03ulaHDx4sKmogjxmAAAACFgZjJMMAACAgO22225quuVt3Lgx6bxbbbWV17t372LTP/74Y7uOl156yf79008/eVWrVrXTmjVr5m277bZerVq1vIYNG3odO3b02rZtW7TswIED7Xw5OTleixYtvG222cbLzc210/r161c03/3332+n6ZOfn++1a9fOy8vLK5r27bffJt1+19/SfhYUFNj1V6tWzc6n/2u+5s2be0uWLEl5P+W2224r2l7Np/X6y48ZMybQfU11HZs2bbL70LJlS/v9oEGD4q73/PPPL1pH06ZNvcaNGxf9feWVV0bN26lTJ2+XXXaJmvboo48WHYNWrVp59evXt3/vtddexdLhzDPPtNPq1atnj4fW99dff5W47lS2zfVcziR///XR9uo46t89e/a0/9e2uqRVaUyaNMmrUaOGXXeDBg3sOetv0/XXXx81r84Z/e748ePTWsfMmTOjztk2bdp4W2yxRdG0++67L2q9L774ov29F154Iel+pHqtxju/TjvttKjj4Z+7u+66q/3/6NGj0zoXg8i/tb3xPq+88kqJ+5NuPqM0i02vCy64wC6zcuXKYvMfeeSRRevccssti9JNnyFDhkTN26dPn6LzxD+PXb67+uqri9ap7dO+1K5du2javHnziuatUqWK179//7j79sADD9j1vv/++54LpUNsWsg333xT9Nv6/Pnnn2ltq76PvDeVlp+/vf7668W+e+ONN4rOmyZNmtj5WrduXTRN16fvhx9+sNeP5tHxVBpUr17d/q17699//512mqayHWW5brnkkku8mjVrRu1POr+rc27nnXe2aaS8UL+pafr9Dh06eGvXrvXK8vi+++67drp+s6S8Yv369RX6mAEAACDzCBoDAABU0qCxPP/8816jRo2KKsL9YJKCg5GV7FOnTvXOOOMMr27dulEB1XPOOadYpd9rr73mde7cOaoyXoGlO+64w1u1alXS7Xf9LX8/FcDceuuti+bt3r27XUck1/30PfHEEzZY6s+vys0LL7zQW7ZsWaD7ms46Lr/88qL5YitiI4PLN9xwgw1O+vMqiKOAeGFhYdS88QJVOs8U4PQrzOvUqePddddd3jPPPFMsaDx79mxvhx12iNr+hQsXlrjuVLYtG4LGqsQ/99xzi15YULDhmmuusedcbNA4UVqVloKhCqz661VgUQHi2DT1A9eRxzCVdaxbt8679957ve222y5qPxQgHD58eLF1al59/9RTTzntRyrXarzzS9s3YMCAqONx7bXXenfeeaf9+7vvvkvrXAwi/y7po5c0StqfdPOZVIPGSreLL744KjCqwP0jjzxSbF4FI7XuyO1x+c7PW/UygP+dXkzRuTN06FBvw4YNTkHjK664wi773nvveaUJGstRRx0VN2icyrYGHTTWuacg9eGHH17sO21TonNp8uTJUfN/+eWX3k477VT0vdL1kEMOsXlR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RzrzHXzwwUW/v/3229vAdYcOHQJ7/nbdDr3w2bdvXxs079Gjh3nxxRdtXq+gC4Dsc//995t777233H/3wQcftM+/eslPLyLq+fzII48MbNnevXsXy7s137vvvmuaNGlSNN/bb79tG8F89dVX5uuvvzZffPFFoPupvFF55Msvv2z22GMPU1hYaPPO9957L2q+Tz/91Jaf9aKS9k0vlqucq20CgIzwgBCpXbu29+ijj5brb/bs2dM76KCDiv7esGGDt+2223pnnHFGYMsGPV867rvvPu+5554r1/0BEE6nnnqq17t373L9zfnz53srVqyw/9Zvp/L7P/74o1e1alXv9ddfj5q+aNGitOY799xzvebNm3vr1q0rmvbQQw8p8mG3s7Tuvfder1atWt7ixYuLpl1++eVekyZNon6za9eu3j777BO17HnnnWfz08LCwlJvB4DyNWLECJuPrFy5MhTb8dVXX5XZsvHm27Rpk1dQUOCddNJJUfOuWbOmKP8vjRkzZnhVqlTxhg0bVjTtiy++sNsxevToomm33nqrV7NmTW/hwoVF0z788EMvJyfH++GHH0q9HQDKn679Z555plx/c6+99vLOOeectJbt0qWLd/jhhxf9rfJfq1atvPPPPz+t+WIpT61fv7536aWXprV96WzvTz/9ZPPRp556qmja6tWrvWbNmnk33nhjINsBoHwdddRRXr9+/cr1N7/++msvNzfXe//99xM+swe5rPJMPZ+fdtppUdPnzZvnrVq1yv57l112CTwtbr75Zi8/P99bunRp0TTdV1q2bOlt3ry5aFr79u29vn37Ri178sknezvuuGOg2wMArmhpjNBQiwW1zFArMbXS0EctWcuS3szVW7UnnXRS0TS1CDvuuOPMG2+8kbCFhuuyQc8X2dpXLSbUukJdBupNtES+/PLLoi76ymN/AITT448/bltqTJkypSivVZfRZU1dLKXbhd7QoUPNtttua4466qio6erOOZ351O1e69ato7oWVGsN/7tIanFx9913254wLrroIpuXJqP0POCAA6LeYlaeqS6x1TOD6H73/fffm/333z9qWf2trqoT5dcAwkdd0fstNZRfKG+9+uqrM7ItaoUmJXXLF8Sy8eZ7//33bQuJ66+/PmpedVsam/9v2rTJtlBTl6vqUlBdom7evDnhb44YMcJUrVrVHH300UXT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bT/xfPzxx95xxx3nLVq0qGja119/7eXk5Hhvv/120bT777/fq1atmjd58uSU59NvFxQUeCtWrCia9vjjj3sqFn3xxRdF00466SRvv/3281avXl00bfPmzTavXbZsWYn7N3LkSLsdn332mf1706ZN3v777+9169Ytar4JEyZ4c+bMKfr7p59+8ho2bOjdcccdJa4bQHh99dVXNh/59ddfy+03P/jgA2/+/PlR09588027HcOHDy+apjxLeavyp1SXdZ1v/fr1XuvWrb0zzjjDKywstNOmTZvm1ahRI6qM+Prrr9u8burUqVHrnDhxojd27NgS91X7oOUuu+yyomlDhw71qlevHpXmyrPff//9or9XrVpl8/1evXrZPBxA9mnevLl31113ldvvKU/573//GzVtxIgRtnwXux033nijd/3110dNU37bsWNHm//IzJkzvVq1atlyaarz/fLLL0VlSt/vv//ubb311t4RRxxRNG3JkiVegwYNvIcffjhqXk1/+eWXE+6v6/a+88473oYNG4r+vueee+x806dPT7h+AOF03nnn2fJReVLeqjLkJ598ElXfqvKi8h6fyqzKmyKfu12Xjay7bNy4cVS+VZJddtnF69evX9zvDj/8cK9v377e2rVri6bpGf/ZZ5/11q1bV+I6X3rpJa9KlSre+PHj7d/ajt12283be++9o+ZTPUZkWVtl4rp163oPPvhg0u0GgLJA0BihctNNN3l16tSxN2oVDubNm1fmv6lga8+ePb0WLVrY361fv753zDHHeBs3boyaT5VNO+64Y1rLBj2fChAdOnTw2rVr5x122GG20KGHxkceeaRUQeOgtxNAOH377bdezZo1vT333NPmta+99lqZ/6YegvRb+jRt2tR+/L8jA8SPPvqofRCMfejTA5O2ed9997V5nh6inn766WK/4zKfKt+23357+wCphz89IOpBM/alpZUrV3rHHnusrYA78MADvYMOOshr2bKld+KJJ0Y9MMZz3XXX2e3o06ePrYRr06aNN2PGjKh5FMju3Lmz3VYFp2vXrm2X84MtALKLgqadOnWy5TOVi2JfNiwLH374of3NHj162DKZyqp6kU9BjMi8ZOHChTZvvfvuu1Ne1nU+/2UYlQ+VtynPVB6sCrvYMuJ9993n1atXz+bTKstq/Xqx5ocffki4v3qxSOVOlUOVdypY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119/bebMmWM6duyYVef5999/b8/Xxo0bm7333ttsscUWTsvp/Pjmm2/M7NmzTYMGDcyuu+5q6tWrZ7LFxIkT7adbt25mp512SulYjx071u73NttsY3r16mVyc3PLPf1Lu2wmlSYNFy1aZL777juzceNGs8MOO5i2bduabLF+/Xp7P/zzzz9N586dUzrvfPPmzTMffvih2Xrrrc0BBxxgssGKFSvMmDFjzJo1a0yPHj1SPmbKm7/99lvTsGFDe0/KlvN88eLF5vPPP7d55V577WWaNm2a0j7/8MMP9t877rhjVp3n2m+do3Xr1jVHHHFESsv+8ssv9j5cu3Zts++++9p1pEt5hdI/JyfHpn+TJk3KZdlMSzcN165da6+z+fPn23xZ12pp7m3lbdKkSbbcrWOl+2H16tVTWl7X6QsvvGCfW04//XRTpUoVkw330q+++sqWPdu0aWPvpTpnU8mbVd7XvUnl9VTyqLDcS7fffnubR7patmyZLbsuWbLEptkuu+ySUppler8/+OADu9/HH3+8qVOnjvOyy5cvt/dhXecqr+saz0T6l2bZTCtNGv74449m6tSpthyjvDWbnpcWLlxo74fKE3U/1DNHqpRuKtfonqTrLhvMmDHDPm/oHqp6gVSut82bN9vnHJXZdZ7rky0mTJhgpk2bZu8HupdWq1Yt5bqQRo0a2WsklbqUTFP5R+Vu3RNSOV7ab9V7/fbbb/bc7tmzZ6nuKemmf2mXzaTSpOGCBQvsdap16H5SmntbeVu3bp3NG1UeUZ2GPqnS9fbJJ5/Y/e7du7fJBiqHab+1/zrWrVu3Tjlv1rmu8r7KrqmW9zNF5+oXX3xh76W6PpVPprLPKkdoWdUzp5pmmd7vjz76yO7voYcemtKy06dPt9e3yky6D+vZFshKXgU2d+5cb7vttvM6derknXjiiV6DBg28/v37e5s3by7TZTPtm2++8Zo0aeL17NnTO/roo71atWp5gwYNSrrcxIkTvW7dutl9PfbYY71evXp59erV855++mkvG7z66qte7dq1vYMOOsjr06eP3e9hw4altI5ff/3Vy8vL83RpPPHEE142uP32272aNWt6Rx11lLf77rt7DRs29L788kunZX/55Rdvhx128LbddlvvpJNO8nbbbTe7nrDTdXjKKad49evX90444QSvc+fOdh9mz56ddNk///zT69Kli9e8eXPv+OOP97p3727XM3LkSC/s3nzzTXu8tP05OTnerbfe6rzsihUrvD322MPbeuutbV6m/+vvlStXlmv6l2bZTEs3DdesWWPPtS222MLmT4cffri9Zk8//XRvw4YNXtjp2OgY6VjpmOnY6Rimcj/UvEornbfKn7OB8lHlp8pXlS/qmCm/daHjetppp3n5+fl2WR3zHXfc0Zs1a5YXdi+99JK9fx588MHeIYccYv+taS4GDhxoz/PDDjvMfvTviy++2As73fu1vVtttZXXokULe6xSccstt9h0UplL99HGjRt748aNS2tbVG7RunSdKL9QuUblm7JeNtPSTcMHHnjAa9Sokb03Kp9t2bKlPX7Tp0/3wk75ou4lKnfrWUPPHHr2+O2331Jaz5AhQ2zeqrLr2rVrvbBbvny5fcZo1aqV3X9dc3vvvbe3atUqp+Wfe+45+3yy77772uXbt2/vDR8+3As75f9t2rSx56rupbo/nHHGGV5hYWHSZd966y07f48ePex53qxZM/vM9tdff3lhp3uAytwqQ+gcTaW899lnn9nrY8899/SOPPJIex++8847yz39S7NspqWbhmPHjrX7q7z4uOOOs+ee1uNaHsi0Z5991u7roYce6h144IH2fvjGG2+ktI6pU6fa+5LO22zZ7+uuu85u8zHHHOPtuuuuXtOmTb3vvvvOeX87duzodejQwdYL7LLLLt6pp57qhd2mTZvsOaoyu+6lBQUFdh9+//33pMsuXLjQ23777e19SHnrzjvvbNfz4YcfemH3yiuv2HJD165d7Tl69913Oy+7bNkye360bt3a3kdV/tU9dfXq1eWa/qVZNtPSTUPVJ6isq2ckPWfp+aNGjRreOeecY9Mj7GbOnGn3WWVtXTMqjw0YMCCl+6H2U/mLyq7ZUPcoY8aMsXUge+21l3fEEUfY+8u9997rtOy6devsOaLldez79etn6/MUcwi7p556yu5r3759vQMOOMCrU6eOLZO60Hmhc1vppWfT6tWre9dcc40XdtOmTbP1IHqmVPlVz6WpuOqqq2yZQzEVlZ20jkmTJpXZ9gJlqUIHjffff397I/cr5VWBpEzrkUceKdNlM2njxo32oVY3Jd9HH31kC5KjR49OuOyjjz5qCy0qAEVW4lWpUsUWDsJMBX49IEVW6KsSTYUx18o3Vd7pgfrcc8/N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hJn2O16LEpf91oOQAh8K4mSb0hxvVVaOHz/ePP/887YloIwcOdIGUvfee297gw+j0l6ffiWt3nRTixVVNuphOswtBUoryDytMuetQW27WvWrolcPTGF/Iac0+638RS019FZttr2QUdq8VW+fq7WuAlhaRg+JeqtWlW96YKqI+63vVVHg31tE/1YAXd9V1Mq3IMtdpVlXkNtR3oLadt3PVUmha+3WW281FTVv9YMaesmvR48eJpuUZr+Vn8iXX35pWyepJZheTlFgQ+VY9Zaj/KailiFUVlWPV6po1wtJCuz4aVIRBZ23VtayaxBpqPWceuqptvJbL8GFuReH0u63yuZa9vrrrzfZprRl16+++sq2MNZ9VF566SVz4okn2noBtZILo9Jen/pe9xL13KBegdRCVC3pqBco+/TP9rw1qG3XS2933323fdE6toViRdrvL774wjzwwANm7NixUS1XK0Peqhf+lM+oNzKtR4FFPasoiP7www+bilovoN721Lpc9QJKB61PdUL6f2xPJhVFNj+PA/GEu8RfCmptKwoGRr79q791EaswWBbLZpq2PV43bdp2f79c6O1avWH52muv2Tf5w0779ttvv6W838q477jjDnPSSSfZApvoLTm/UkoFGj0kh5X2TfuYzvFWCx51g6N99ylQrDez1d1fWIPGehtPN93Y/fb/TrbfRx55pH0JQl2S+5UeQ4cOtQ/IqoTUG/QVTWSelk6aBZX+pT12FSENdW3179/fnHHGGVnRQihyvyPfHNbfOpaJ3o5V3qquidSFlz6iNyzVKufJJ5+0b/RvueWWpiLmrf6LV/4Dkd7I1RvG7777rgkz7dsff/wRd7+bNWuWcNkhQ4bYSg8FNfyWgupCVoENvV17xRVXmIqoNOdKKutK1mKhNMtWhDTUixkHHHCAvb9/8MEHoQ0e+vQsoWeKdO4pevFIvaSoVZjyUtFLcPLss8/arr3D+kJSae6lft6q8poq30RpqLz2lVdesWW4sAbRI/c7srcq/a2yqHr+KYnun4cffrh58MEHzXnnnWenqcXGXnvtZY4++mgb6KmIgs5b003/0ixbUdJQ551at6tVVNiDGsn2O1GvQKoD0DOh8hS17vd7hRC9AKnKYAVRs2m/9WLNihUrnMqu2j8/YCx6Iefcc881n3zySWiDxn69XLrPZ6rvUPlW9QL+y4133nmnfYlegeSwl58yXS9QmvQv7bGrCGmoVu2nnXaa7UnkhhtuMGEXud+RPY3pb/V6o+e9ktx+++32mVBDlekjfg+PKsvqWgxrb55B1Aucc845RcFE5Tn9+vWz9UJhVppnS9VzPfroo7ahTsuWLe00/Vs9leja98uzFU28NFPMQeWLMOdpQEkq5usd/xsLTVQAjKS/1Qd/5Hh9QS6badp2dR/ljzkUue3+fiWjt9zuv/9+2wJVlRTZQPum7oQi397RQ5IqWhLtt27cKqjpoV8Vbvr443GoxYI/jlVYad9iz1O1tNGLA8mOtwptetMr9k0oVbjqjbCw0jZrTKp416ck2m+1ztGbbnoQjHxLXsEcVcDpzceKSN3jqiAfL800Xd+XR/qXZtmKkIY6v1QZo0rexx57LO5biGGT6H6oFywSdYuqCiZVLvp5qz4KGKubcv07zONQar9V4F+wYEHK91LlrRLbQ0fY81bRvum+GdnCQv9WxVmy/VbrI3VrGNm1rK53TdN3FVW8+7BaqqiLtlTzND/9I8e50pvZKt8kW1dpls32NFSFuMa5VflXveOEtdIpkoLa6mY4nfuhuopTS2O9oOHnrf74eWoplWhc4EzTOPEqf8Xbb+WRsWOvxu63Wi3Ey1slzPlronupgliJWm4qKKznG7306NNLrSrLqothlV8rIqWZKpRju/NL5Zk2cl3+sqmmf2mWrQhpqBe+nn76afPqq6/aYSiygfZN49RG0nWiF9kS7beChnqpU/dOP2/1ux7VsmEeY7Wke6m68Ne+u5Rd4/Usp2e3MOetemlDL1Clcy9VOV+96ikvjewNR/UC2mf/ZayKRi/0an/jpZnKJqrvLI/0L82yFSEN1UWx6gRUntNLYdkg0f1QaZKo9ej+++9v85nIegGV8/Vijl9HEOb91n3Uf4moMtULxB5rbbPqyV3qBbp3714UMPbXp/qjylYvoHoUlePDnKcBJfIqsE6dOnnHHHNM0d/Lly/3GjVq5F133XVR87355pveqFGj0lo2bFasWOHl5+d7N9xwQ9G0GTNmeLm5ud4LL7wQNe8zzzzjjR07NmrakCFDvJycHPtdNpkyZYrd7ldeeaVo2ttvv62aU2/ixIlRx/GJJ57wpk2bVuK6/vzzT7uc5gu7V1991e639t932223eXXq1PH+/vvvomlz5syx+7No0aKiaZ9++qndz3HjxhVN++mnn+y58uyzz3phNmjQIK9Bgwbe0qVLi6adcMIJXocOHaLmmzRpkt3v9evXF01r1qyZ179//6j53nrrLZsWSpNsUaVKFe/WW2+N+93o0aO9F198MWrakUce6e20007e5s2b7d/6v/4+6qijyjX9XZcNI9c0jJf+yofq1atn7yubNm3yskn79u3tMfLp2NWvX9+78cYbo+Z74403vA8++CDhunbbbTevT58+Xtgp/1Q+qvw09j7z2muvRc2rc/ybb74p+nvdunVe06ZNvauuuqpomo65yhWHH364F2Y//PCD3UcdS5/+rWk//vhj0bRly5bZ/f7555+Lpl144YW2nKRySOQ9t2HDhva7bHHcccd5O+64Y9zvZs+ebff7jz/+KJqma133zchyxc033+zl5eXZ/U/FhAkT7L1I5RefyjVav+7PkeentkPlu1SXDSPXNIyX/qtXr/Z23313r2XLlvb7bHLttdfaayZyH3WP6Ny5c9R8Orba740bN5a4rvvuu88e/7Vr13phd9hhh3ldu3Ytupcqf9Q+69qL9Mknn3gvv/xy1LSzzjrLlhci00LPZrVr17b5UlgVFhZ62267rXfyyScXTVuyZIktF8SW5XSP+fDDD6PSQcf2/fffj5rv6KOP9lq0aOFli5deesnuR7zrVOmjc3z8+PFRz2O1atXy7rzzzqJp33//vV3HiBEjyi39U1k2bFzTMF76yy233GKfNyKfr7OB9kP7+N577xVNGz58uL3PTJ8+Peo4ar9/+eWXEtel81Xr0vkbdk8//bRXtWpVb9asWUXTrrnmGvvcpXulT/ur/db++959991iZb1vv/3W7vvrr7/uhdmVV15py92R5c8jjjiiWFnuu+++s/sd+Rym55kzzzwzaj7dd7TfX3/9tZcNdD/U9t59991xv//4449tnVGkQw45xNtll13ste+vo2PHjsXqSMo6/V2XDSPXNIyX/npu1HOm5vXLQtlA27rNNtt4p59+etR9pm7dut4dd9wRNa/2WfueiMqC6dRFlbfFixd7NWvW9O69995iz10jR44smqZzW+e4znXfqlWr7LNwZL3Jhg0bbLni+OOP98JMsQLtY2SZ6LnnnrPlgsj7ps4B7Xfkvee0006zZdQ1a9ZEzafz/uqrr/ayheqtVH8Vj57Dtd+R9e6PP/64V61aNVv/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QGLTpk0za9euNQcffLDNX31vvfWW/U5Wr15tX86R2rVr2xcjAQDJy6516tSxLzgq75TffvvNvPTSS0XzqDcyvy1H27ZtTU5ODskKAEnyVr2E88ADD9jecUQvPvbs2dPmqV26dLF1sKtWrbLfNWvWzObFAAA3RKmAgK1fv962zrjsssuighVqxRHZCu6II44wBx10kH0jzg9wAAASPxzus88+RQFjP28VP39VyzjlrTvttBPdVgNAChVv1157bdT0yLLruHHjbN662267mQEDBpCuAJCEghUzZ840V1xxRVHAODZvFfXucOCBB9p6AdUlAACSl10POeSQooCxn7eKn7/edttttuy6ww47mHfffZckBYAUEDQGAjZ58mSzceNGG9iI9NNPP5nOnTsX/f3DDz+Yn3/+mfQHgBSDxrF5q4YAUMsMufPOO+148hrTCADglrfuuOOOpkGDBlHBDrWG88uuylOVtyqPBQAk9/3339uexZLVC6hOQHUDAAA3kyZNipu35ufnm6222sr+/eCDD9qya69evUhWAEgRQWMgYBMmTLDdnmy33XbFKuTURQoAIHV///23mTt3rtl5552L5a3qnrpqVUbcAIB0y66xeasf7KDsCgDp560NGzY0rVq1KhbsIG8FgPSoTuCvv/6KWy9A3goAwSBoDARMBRV1ixo5FlFJwQ4AgHvFm8R7OCRvBYD0aBxNtXCLl7c2bdrUbLnlliQtAKQhXgBjwYIFdpxNyq4AkH7eqvpW1bvGTidvBYBgEDQGAhavoKJpwltvAJB+3qrghYIYsa3hyFsBID0zZswwK1eupLUGAJRTvYCCHRoSAACQXt6qMeDr1q1bNG3Tpk12qEDqBQAgGASNgYD17t3bHHvsscUKNVtvvbXtngoAkDp17XfddddFTfv111/N0qVLeaMYANKkSraBAweaHXbYIWo6rTUAoHQOOuggc+SRRyYNdgAA3G277bbmiiuuiJo2depUs27dOuoFACAgDAAIBGzw4MHFpsUbt+j33383q1evtv+ePXu2adSokWnZsiXHAwDiOP744+PmrRrLWGMa+5YtW2bHOFL++ueff5pffvnFBpyrVatGugJAjE6dOpn7778/atr69evNtGnTzKBBg4qmbdy40Q618scff5g1a9bYvFVl1/z8fNIUAOK4++67neoF5s2bZ1+C9F+IzMvLMy1atCBNASCOU089NW7eWqtWLdO+ffuoYQL1Wbt2rVm8eLEtu26zzTamSpUqpCsAJEFLY6AcxGutcdFFF5k+ffqYtm3bmmOOOcacc845HAsASDFv7dixo6lRo0bRtFdeecW27NDLOE899ZT9tyrjAABufvzxR9sCObLsqnxU+enjjz9ufvvtN/tv5bcAgNLVC6geQC2SVS9w2GGH2XoCAEBqeau6/c/N/f8wx3PPPWfLqwoYP/jgg/bfeqkcAJAcLY2BMrZ582bTuXNns99++0VNHzFiBGkPAKWgrv1OOeWUYhVvvIQDAOlbtWqVfaFRrTF8bdq0sS00AADpUS8OChjvu+++UdNHjRpFkgJAKdSvX9/0798/atqll15qPwCA1OV4nuelsRwAAAAAAAAAAAAAoAKge2oAAAAAAAAAAAAAqMQIGgMAAAAAAAAAAABAJUbQGAAAAAAAAAAAAAAqMYLGAAAAAAAAAAAAAFCJETQGAAAAAAAAAAAAgEqMoDEAAAAAAAAAAAAAVGIEjQEAAAAAAAAAAACgEiNoDAAAAAAAAAAAAACVGEFjAAAAAAAAAAAAAKjECBoDAAAAAAAAAAAAQCVG0BgAAAAAAAAAAAAATOX1f40M0PY+D9nBAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.struphy.plot.panels(\n", " x=\"e1\",\n", @@ -452,21 +304,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "20", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAt0AAAIWCAYAAACLGDGFAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAWKlJREFUeJzt3Qd8FNX2wPGTUKQmoXcR6SAdQVBUFHgqUkSwoaIoduWJiqhPBSwgCIr6twsoolhBURFBRUCqEBWN9CJIJ5DQS3b+n3Pf281uenZ2srOb39fPuOzszN3ZmU1y9uy598ZYlmUJAAAAAMfEOtc0AAAAAIJuAAAAoACQ6QYAAAAcRtANAAAAOIygGwAAAHAYQTcAAADgMIJuAAAAwGEE3QAAAIDDCLoBAAAAhxF0AxHglVdekX/++UcKo2+//VZ++umncB8GQuyDDz6QVatWBb3/oUOH5NVXX5VoMWnSJNmzZ09YjyHSzqkbzhmQHwTdQAT4z3/+I5s2bZLCaNq0aTJz5kxxGz4M2PPSSy/J0qVLg97/+eefl7Vr10q02LZtm4wcOTKsxxBp59QN5wzIj6L52hpA2H3zzTdStmxZ6dSpU9BtHDlyxASymiVq3769nH322UFvv3LlSvnhhx8Ctm/ZsqV06dJFIt2PP/4oycnJcuWVV2Z6LCUlRU6dOpWndt566y2zfXZuvfVWSUhIyPNxvf/++9KqVStp2rRpjuuc8Nlnn5kPgLfddpvExcUFPH+jRo2kbdu24jR9P06YMEF++eUX37rvv/9eEhMTzb9jY2NlyJAhWe67ZMkS856tV6+edOvWLc/Pmdf2g93/jjvukDPOOEOefPJJqVixohS0rM6p0m8j9JyVKVPG/ExXqlQpz21G+zkD8otMNxCBX8t//fXXQe+/b98+E5xpVmvFihXyr3/9S4YPHx709vPnz5eXX35Zdu7c6VtyCjAjyRdffGEC5qxcffXVcvnll+epHf2w4j03GrS++OKLAecrLS0tX8el+y9fvjzXdU7Q8/HQQw/JqFGjMj3/vHnzpCBMnz7dBM1169b1rTt48KA5l4sWLZKhQ4dm2ufEiRPSt29f6devnwksX3vtNbnxxhvz/Jy5tW93fw1mO3bsKFOnTpVwyOqc9uzZU2644QbzvnrvvfekTp068t133+W5zWg/Z0B+kekGXGjLli0my1OuXLmAjLH+8fnrr79k+/btJgguUaKE3HPPPflqW4MlzVppW8WKFTP10hdddJEJQM4888ygtq9fv745nmBoe/pHPSYmRqpVqyadO3c2tznRTKtmofX8XHzxxSYguOKKK6RGjRrm8d27d5vHtUa1Q4cO0qRJE3GivKRkyZJywQUX+Nbp82nWf8eOHdKiRQs555xzzPpHH300oFRIg1Pv+VqwYIEps9AMtf5bM3aXXHKJeY16X8+LPsfpp59utv/qq69k165d5vn37t0r1atXNxnnjOuuu+4637nQc6wfhPRc+GfC9bzptxaHDx82H6guvPBCadiwYa6vXd+TmhXVTGPt2rWz3S6n57Zj7ty5Jtjy17t3b7Po+fnyyy8z7TN69Gj5888/5Y8//pD4+HizTl9zXuXWfij2P/fcc83P/eDBg/PUpvf66fn9/fffTdCsP5uhOqd33XWXeS963X333ea9nNdvCNx4zoBwIugGXEaDxV69eknXrl2lVKlSJqt8/PhxX1B39OhRKVq0qMkAadCnli1bZjLO2dFAqkePHubfmiW/5ZZbTACtNKCrWbOmCdj0j2xGedleM7naAUuDP/0jqBmx/GazLMuShQsXyr333iuzZ8/OtkxB/8BqgK1/+L3nR+tQtaRFg24NVPX86XFoAHv//febbbxfTefnXOVWa67te4Nu/RpeM9+6v2YL9cOKBgyaAc6Jnt/PP/9cihcvLueff75pTzN3GtDqcWgWXM+z94OFBlgnT540t3reTjvtNHPuMq5Teh6vv/56026FChXkkUceMedCgyc1fvx4E9TreWzXrp0pHcoL3U7b0wAsuyxjbs+dkb6fNCDOjgbtek2VBs+agc1vhl4/8CxevNh8oNEPAHpsbtKgQQNznfNKr5/S660f8vT86s/F5MmTQ3JO/QNupR/mjh07JpF8zoBwIugGXOa+++6Thx9+WB577DFfcHfttdeaf+sf1NatW5ugVzN3Xpqp1GArO5UrV/b9+++///ZlTb30vq7PSm7bt2nTxnRoWr9+vVkGDRok48aNyzKAz4oGqv5lGmPGjDGdo7LLbGlGS4Mn79fNn3zyiVx11VW+xzWou/32233lDxr4XXrppaa0QI87P+cqP/Q5Nfv/1FNPmfuacdaAQNfpNcvJ/v37Zd26daauW49Pr6+OWDNgwADz+P/93/+Z9rt37y79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AQAAAIcRdAMAAAAOI+gGAAAAHEbQDQAAADiMoBsAAABwGEE3AAAA4DCCbgAAAMBhBN0AAACAwwi6AQAAAIcRdAMAAAAOI+gGAAAAHEbQDQAAADiMoBsAAABwGEE3AAAA4DCCbgAAAMBhBN0AAACAwwi6AQAAAIcRdAMIqbS0NPF4PJxVAAD8EHQDsO3gwYNyzTXXSPny5aVEiRIyYcKEoNrRYD2vAXuog/vRo0dLrVq1pFixYtK0aVOZPn16nvbbsmWL9OzZU0qVKiVlypSRfv36yc6dOzNt98MPP0jr1q1N+1WqVJFHHnlETp06lWm7cePGSe3atc12jRs3lo8++kgKip7P4cOHS/Xq1c3zt2jRQmbNmpVpuz179sh1110nZcuWlZIlS8pll10mGzduLLDjBIBIRNANwLbHH39c/vrrL1m7dq2cPHlS7r///qDaGThwoHTr1i1P25577rly3333SSg8//zz8swzz8iUKVPkyJEjcs8995jgef78+Tnud/z4cXO8+gHg77//lvXr18vu3bule/fuAR8I/vjjDxOY9u7dWw4dOiTffPONTJw4UYYNGxbQ3iuvvGLO5dtvv22O46GHHjLB7Zw5c2y/xrx8oBk5cqS89NJL8umnn8rhw4flhhtukF69esny5ct921iWZT5kbN68WdasWSP//POPnHbaadKlSxc5evSo7eMEgKhlAUCQPB6PdfLkSatGjRrWqFGjzL/T0tKy3PbUqVPmcb3Niu534403WhdddJHZLre22rdvb911112+bfVYgqH7li9f3nr00UcD1nfu3Nnq1q1bjvu+++67VmxsrLVt2zbfuj///NPSX61ffvmlb90NN9xgNWnSJGDfCRMmWMWLF7f279/ve/1VqlSxhgwZErDdpZdeap1//vmWXQ888IDVq1evbB8/fPiwVbp0aevZZ58NWH/22Wdbffr08d2fPXu2eX0rV670rdu1a5dVtGhR680337R9nAAQrch0AwjapEmTTDmJZjv/85//mH+PHTs2y20bNmxoHtesaLly5UxmNDEx0ff4nXfeaTLNP/74o9lOl6effjrLti6++GJZunSpvPbaa75tP/nkE/OYZnO1bCOnRTPTXitXrpTk5GRzPP66du0q8+bNy7IExGvu3LnSqFEjqVGjhm9dkyZNTHmGPua/XVbtnzhxQhYsWODLhu/a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7wsvvFAqVapkeiDnRj8UTZgwwQyQr0ujRo3MZEdTpkzhchSAMWPGmGvVuXNnqVKlitx+++0moMuPjz76SBo0aGCGeILzJk+ebK5Vp06dpHr16tKvX788f2DS8W+vuuoq83N6/vnnm2mrp02b5vgxF3azZs2S008/Xc455xxzzrt06WJmn8yL0aNHmwlPzj77bNPGWWedJQsWLHD8mAszTRpp3KCzJzds2NCc+/xYvny5+VvWsmVLady4sbRp00ZWr17t2PGiAFhR7Pbbb7dOP/10a+vWreb+9OnTrZiYGGvWrFmO7ovgjRo1yoqLi7NWrVpl7i9ZssQqUaKE9frrr+e436ZNm6ySJUtab731lpWWlmbWTZgwQdNv1tdff80lcdCHH35oFS1a1Pr+++/N/XXr1lmVKlWyhg0bluc2tm/fblWoUMG68MILrdNOO83Bo4X66aefrNjYWGvq1Knm/o4dO6z69etb119/fa4n6MiRI1bTpk2tXr16WcnJyWbdnj17rGeffZaT6yD9udLfhc8884y5n5qaanXs2NHq1KlTrvt+/PHH5neh3qrjx49b1157rVWuXDnfNUTorV+/3rrjjjuspUuXWg8//LAVHx+f5333799vVaxY0ex/6tQpc82uvPJKq27duubfiExRG3TrG7ZYsWLWyy+/HLD+oosusrp27erYvgieBstVqlSxHnjggYD1AwcONAFBTjRomDdvXqb1Z5xxhjVgwAAui4POPvts88fA38iRI62yZctax44dy1Mbl156qfnj8uSTTxJ0F4ArrrjCat++fcC6iRMnmkBcf5Zy8txzz1kJCQlWSkqKw0cJf/fff79VvXp1X1JB/fDDDyaY1qAuJ7fddptVp06dgHW//PKL2XfOnDmc6AKQ36Bbk0bFixe3Dhw44Fu3ceNGc80++ugjh44STova8pIlS5aYkgP96tOf3v/5559zrEG0sy+Cp7W8u3btyvK8ay2cPpYdbxmQP71O+vVeqVKluCwO1t6vWLEiy2um/SF+/fXXXNt44403ZNWqVfLcc89xnQrI/Pnzs7xmWt6lv+Ny8vHHH8u//vUvM5nH9u3bJTk52eGjhfeanXvuuRIbm/5n23s/tzKRChUqSGpqakDJl7cmXB+DO693ixYtJD4+3reuTp06UrNmTcqCIljUBt3auUfVqFEjYL3WLmqgkFMnFDv7wplr5v94Xr333num80m3bt24LA7RDj4aqAV7zTZu3CgPPvigCbyZEbZgaH8V/R0W7DVLSkoyM79pXbEGBbVr1zb1phokwDl6XTJes+LFi0vFihVzvWb33HOPCd5uueUWWbZsmXz77bdmRr8BAwZIq1atuGwRcr29P6f5/VsI94jaoNs76oX+UvJ32mmnBTwe6n0RvFCed82w3n333dKnTx/p3bs3l8WF10yDdf2jr9fnsssu4xpFwDXTb480aNdOmP3795c9e/aYTLd2FOvZs2eeO/UhuOuW8Zp5r1tuvxs1UBs2bJh8/vnncsUVV8g111xj2vr3v//NpYjC6w33itqgu2zZsuY2Y2987/2csmp29kXwQnXetRTlkksuMRkcRi9x7zX74IMPZOXKlXLHHXeYHvm66FCPGtjpvzWgQ+iVKVNGYmJigrpmup/ur19z33vvvWZdsWLFzOg1Ouzg119/zSVz8Gctq9FldF1uvxtHjhxpvlH68ccfzbdT+kGpb9++5tsKLe1CdF1vuFfUBt06PI9av359wHq9r8Nk5fSmtbMvnLlmWrdYt27dXNvQr910GC39ylsDAOq5naXDMmqpQVbXTOkQgDllunV//cpbs9266LBz2p9C/80HJmdokKxBczDXTOkQZtWqVQtYp0MHFi1aNMd+F7D/+zHjNdPgef/+/bleMy210wx327ZtzX39ffrYY4+ZWx2qE5FxvTUhoSV5uV1vuFfUBt06HqaOG/zFF18E/JHXQKx79+4B2+rkEP41UvnZF6Gjf8g1O+1/3tWXX35pxuwuXbq0b51ma/SXjz/9g68Bd/ny5U3NIh+OnFekSBHzrYJeo4zXTD8kaYDmP7bzmjVrfPdvvPFGX4bbu2jtqX6lqv/W8drhDP09pmM++0/6pddM637PO++8gKBOr0VaWppvnZaR/PXXX6Z/i385l3bSa9q0KZfMwWu2cOHCgI6res30Z1A7tnppB2a9Zv4lCDo+d8YJw/SbCZ3Qqly5clyzMNNrpddMr53/9dbBBfzH5f7pp5/MdSMOiWBWFHvnnXfMkDtvvvmmtXLlSuuWW24xQ11t2LAhYLsWLVpYF198cVD7IrS+++47q0iRImbM38TEROuxxx4z12Hx4sUB2+kYwTpeqZeOWavXUYccXLBggfXXX3/5li1btnCZHPT7779bpUqVsgYPHmyumQ51peN2f/rppwHb6eN6bXPCkIEFY9u2bWYs9euuu85asWKFNXnyZHMNX3rppYDtxo4da4Yo8x9GUIcw0+HnLr/8cuvnn382cxfouN0dOnQw4wnDGYcOHTJDp+qwtTp/gc4doddwyJAhAdt98skn5pr5/87U4SB13eOPP24tX77cmjt3rhkTX8fG//vvv7lkDlqzZo35OzRo0CAzjKr375L+zfLSa6LXR+c88NKhIc877zyrVatWZlx9/dtYr149q0+fPlyvCFZUotjAgQNNtlNHRnjllVdMFmbRokVy5plnBmynX7Vm/LSf130RWl27dpXvvvtOxo8fb0oN9HzPmzfP1B7602GTtEOX/xS7mi3QjM6tt94asK128tKvV+GMZs2amQzcs88+azpGaqetGTNmZMrGaGmWf+Y7KzoSg7fMCM7RURH095nW+urvOv126M033zSdI/3pcHJ6PbR0xEuz4brvU0895RsVQ2ezfOihh0zWFc7Qb/p0aMARI0bInXfeaUrnHn/8cdNh3J/+3dJrpmVfXjfffLMZVvWdd97xld3pyDMTJ040JV5wzrXXXiuHDx82/9bfjd6O/fo3ztuBXK+VXjP/b2e19Oebb76Rp59+2nzrpz9b+u2g/pwhcsVo5B3ugwAAAACiWdTWdAMAAABuQdANAAAAOIygGwAAAHAYQTcAAADgMIJuAAAAwGEE3QAAAIDDCLoBAAAAhxF0AwAAAA4j6AYAAAAcRtANAAAAOIygGwAAAHBYUaefAACixd69e+XTTz+Va6+9VhYsWCBz586V8uXLy0033SSnn356uA8PAOBiMZZlWeE+CACIBB9//LEJuHv16iUpKSlyxhlnyMyZM6VUqVKyceNGiY3ly0MAQNbIdANAHq1cuVI8Ho/069fPBN9q+vTp0qdPHxN016tXj3MJAMgSaRkAyEfQ3a1bN1/ArcqVK2du4+LizO2VV14pFStWNMsPP/zAuQUAGATdAJCPoLt79+4B69asWWMC78qVK5v7kyZNktWrV0vNmjXlxIkTnFsAgEF5CQDkwZYtW2Tfvn3SunXrTIF4q1atfPe9Ge+iRfn1CgBIR6YbAPJAg2vtKNmyZcuA9YmJiQFBNwAAWSHoBoA8Bt3169eXMmXK+NadOnVKVq1alSn7DQBARgTdAJAHR48eld69ewesS0pKkmPHjpHpBgDkiqJDAMiD559/Psvst47R3bBhQ84hACBHZLoBIEgadLdo0SJgUpxhw4aZ4QJ/++03ufrqq82/Dx8+zDkGgEKOGSkBIEg9e/aURo0ayZgxY3zrNMDWUhR/GngDAAo3gm4AAADAYZSXAAAAAA4j6AYAAAAcRtANAAAAOIygGwAAAHAYQTcAAADgMIJuAAAAwGEE3QAAAIDDCLoBAAAAhxF0AwAAAA4r6vQTwN0sy5KUlBTZvn27HDx4UNLS0qRIkSJStmxZqV69usTHx0tMTEy4DxMAAEShlStXyj///CMNGzaUBg0aSDRjGvhCLDk5WdavXy9HjhwxgXZCQoK51cD7wIED5rZUqVJSr149KV++fLgPFwAARIkDBw7I5ZdfLps3b5aWLVvKggULpFevXjJp0iQTi0QjMt1RZu/evXLOOefIe++9Jx07dsx2u507d8rq1aulRIkSUr9+falSpYoULZr+djh16pTs2rVLtm7dKr///rs0atRIqlatWkCvAgAARLPBgwfL7t27JSkpSeLi4kwSUIPvVq1ayf333y/RiJruKKPB8oYNG0z2OqcMtwbcWjrSpk0bqVGjRkDArfS+rtfHdTvdXvcDAACwIzU1VaZNmyb33HOPCbiVfqver18/efPNN6P25BJ0R5H9+/dLhw4dzL8HDBhg3sC6nDhxIqCGWz9Naob7rLPOkmLFiuXYpj6u2+n2up/uDwAAEKzExEQTm7Rr1y5gvd7XJJ/2NYtGlJdEEc1If/jhhybwHjVqlK+8xD+w1jeyZsG1pCS3gNtLt6tVq5asW7dOfv311zzvBwAACgdN0OXVjh07zK2Wtvrz3tcSWI1pog1BdxSJjY2VM844w/xbRx7RLHdGOkqJdlDI+EbPjW6/ceNGOXr0KEE3AACwVQqrMnaY9N73Ph5tKC8pZHRYQB2lJGMNd250e91PRzQBAAAIVrly5cxtxjIS733v49GGoLuQ8Y7DHQzdj5puAAAQilKUv/76K2C93teAW7+tj0YE3VHGG1BnFxx7x+EOhu7HRDkAAMCO2rVrm9HRpk6d6lt38uRJ+eSTT6Rv375Re3IJuqNMxYoVzUgja9euzfJxnWlSB6TPb72Ubq/7ReuA9QAAoOC8/PLLMmfOHLntttvk/fffl549e8qxY8dk+PDhUXsZCLqjjGaiR44cKQ8++KD5JJlxyED9ykYz1jrxTX7o9rpfyZIlHThqAABQmHTo0MEMHaijlMyaNctM7Kf3o7W0RDENfJTSQFuH3NHbunXr+spCtOxk+fLl4vF4zFc7eRn+T7/yWbFihRkd5eyzz6bEBAAAIJ8IugshnVlSp3bXT5e5TZCjAfcff/xhehQ3b95cypcvX6DHCgAAEA0IugspzYLrrE9a/60T3+g43P7DCGoNt5aUbN261dRYNWrUSKpWrRrWYwYAAIhUBN2FPOOtU7vrDJXaQVLH4faObqKdJvW2VKlSpi6cDDcAAEDwCLoLOa3x1tIRnany0KFDJsOtGe8yZcqYzgxagsIwgQAAAPYQdAMAAAAOY8hAAAAAwGEE3QAAAIDDCLoBAAAAhxF0AwAAAA4j6AYAAAAcRtANAAAAOIygGwAAAHAYQTcAAADgMIJuAAAAQJz1/xU04Wy6LBVaAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_viewer = phase_space.struphy.plot.viewer(x=\"e1\", y=\"v1\")\n", "phase_viewer" @@ -482,21 +323,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "22", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=12, show_paths=True)" ] @@ -511,3637 +341,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "24", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "animation = phase_space.struphy.plot.animation(x=\"e1\", y=\"v1\", step=4)\n", "HTML(animation.to_jshtml())" @@ -4149,18 +352,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "25", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wrote: ['frame_0000.png', 'frame_0001.png', 'frame_0002.png', 'frame_0003.png', 'frame_0004.png']\n" - ] - } - ], + "outputs": [], "source": [ "frames = phase_space.struphy.plot.frames(os.path.join(demo_root, \"frames\"), x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" @@ -4176,21 +371,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "27", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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Cq8ullahQgWZM2eOWXskNDQ0eV8JAADwrXVGUhPrjAAAkP54e/zmRHkAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACs8k/Knfbs2SOHDh2ScuXKSalSpe54+5iYGNm2bZucPn1aihQpIpUqVUrK0wIAAF8PI9HR0dKtWzdZtGiRVK1aVcLDw6Vnz54yfvx48fPzize4tG7dWq5duyb33nuvbN26VYoWLSoLFy6UwoULJ9frAAAAvtBN89FHH8nSpUtly5YtsmbNGnOZMmWKfPPNN/HeZ8iQIZI7d27566+/ZMmSJebnpUuXZOTIkcnRfgAA4EthZNq0adKpUydX10z16tWlRYsWZnt8Ll++bCoi/v7/KcLkyJFDSpcubbYDAAD4J6aLZvv27dK3b1+P7RpIJk+eHO/93n77bRNgRowYYcaKrFu3Tvbv3y/jxo2L9z5RUVHm4qSVFAAA4ONh5MqVK3Lr1i3JkyePx/a8efPKxYsX472fVkUaNmwokyZNMv/euXOndOjQQUqUKBHvfcLCwuStt97ytmkAAMAXumkCAwPNz8jISI/tet35u7h07txZTp48acaKLFu2TPbu3Str166VPn36xHufoUOHSkREhOty5MgRb5sJAAAyahjJnj27FChQQI4ePeqxXYOCjgGJi1ZSli9fLl26dHEFlpw5c8rjjz8uP//8c7zPlSVLFgkODva4AACAjClRA1hbtmwp8+bNE4fDYa7fuHFD5s+fb7a7T+V1Bo3MmTObAKNjRNzt27fPrDcCAACQqHVG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Vppx/vx5R5s2bRw5c+Z01KpVy1GiRAnHzJkzHRmBT1ZG+vXrZ5an1WSZM2dO+fDDD6V79+5y//338w3lvxWjr7/+Wn788UcZP3687T9XmrJ37155//335cEHHzTX9Ztt3bp15Z577pH/+7//E1938OBBmThxolSrVs1c379/v9x3333mMnDgQNvNSzPeeustKVy4MOfbisM//vEP6dmzZ+r/UdI4h8Mh7dq1kxs3bsihQ4ckT548cuXKFZk7d65kBJl8cU3/OXPmyEsvvWSCiHrhhRckR44cMmPGDNvNSxPee+8918EEnvRD0hlEVMWKFaVRo0ayevVqdpWIdOrUyeO9U6ZMGXPA1eCP/1ixYoVMmzZNJk2axC6Jw9mzZ+WPP/4wXRH4n19++UVWrlwpn3zyiQkiSo9hPXr0kIzA58LItm3b5ObNm1KzZk3XNn9/f6levbps2rTJatuQ/uh7afPmzVK2bFnbTUkz9JubhjP98NRqiH57e/755203K80caPXg8cUXX0hISIjt5qRJb7/9tjz77LOm2qhB/8CBA7ablCYsXbpUihYtaqqMu3btku3bt982viY987lumvPnz5ufefPm9diu152/A7w1dOhQMxBxwIAB7LT/0kHhQ4YMMVVI7abR7isdyAqRp556Sp588klzkEXcQUTDmn5B1ODWtm1b6dy5s/z+++8JnvHVFxw/ftwcp5o3by6HDx82Z7PXY9bHH38sXbp0kfTO5/66AQEB5uf169c9tl+7dk0CAwMttQrp0ejRo80HwezZsznYusmfP7+pjOhsLK0aTZgwQd58803xddo1o7NFHn74YbN/9KLfbPXA8ttvv9luXprw9NNPmyCi8uXLJ++8847ZZxpqfV1AQID5P9WmTRvZvXu3Gb+moV+7jvU9lN75XBhxThGL3Yet15leB2/poOdhw4bJ999/b76pIG7afdW+fXtZtGgRu+i/++P11183BxG9XLhwQZYsWWLOM4LbFSxYMM7Pa19UqlQp8fPzk969e7u2afenBtr169dLeudzYaR8+fJmQN0PP/zg2qYjk8PDwzmowCtjx4413TM6ir1ly5bsNTdXr16Nc3ZW7G5RX6TdD86KiPNSqFAhUw1YvHix+Lq43ju6XzJnzmwGivu6Fi1amBk12l3jdPToUVc1Mr3zuTEjmizDwsJMaUv/gDraX0uBderUMdOm8J9BvhcvXjQhzTkYUdWqVSvjLLCTRFOmTJEXX3xRXnvtNQkKCnLtG/13aGio+LpWrVpJ06ZNpUaNGhIdHW26sHQGAAdb3In+X9KZfN26dTMDNdeuXWuu65gjZ4XEl9WpU0c6duxoLjr9WYPJiBEjpF69elK/fn1J73z2rL0LFiyQqVOnmsF2OlVz8ODBEhwcbLtZaYJOe9Z+2thmzZplPiR82fDhw2X58uW3ba9QoYJ89tln4ut0/R6derhmzRoT/HW/9OnThzE18dADyyOPPGKqIxAzUPXzzz83X4S0gt21a1cTbvG/2Xu69pOGex3jqEGkf//+GWIFcZ8NIwAAIG3wuTEjAAAgbSGMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqn1uBFUDa8Mcff5hVWlXOnDnNasgZYfEmAInHomcAUp2eIC5PnjxSuXJlE0ROnjxpTkEwefJk6dSpE38RwMfQTQMg1f3555/m58KFC80S4AcOHDDnhnr22WclJiaGvwjgYwgjAFLdxo0bzbmgSpYsaa7reWyaNGlizm1z/vx5/iKAjyGMALASRqpUqeKxTc/SWrp0acmXLx9/EcDHMIAVgJUwUrFiRdNFc+3aNVm0aJF89913MnfuXP4agA9iACuAVBURESG5c+eWcuXKmZ/Hjx+XEydOyOzZs6Vt27b8NQAfRBgBkKqWL19uxods2bJFqlatKg6HQ3r06CErVqyQgwcPSubMmfmLAD6GMSMAUr2LJkeOHFKpUiXX4NWhQ4fK0aNHZfHixfw1AB9EGAGQ6mGkZs2aHhUQDSa65oiOGwHgewgjAFKVVkJ0TZHY+vTpYxY/A+B7GDMCAACsojICAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAADEpv8HHVmCw+8BK9gAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.plot.equilibrium()" ] @@ -4207,28 +391,10 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "29", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "largest drift of the total energy: 1.586e-06\n" - ] - }, - { - "data": { - "image/png": "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EwIZAAAAALgJgQwAAAAA3IRABgAAAABuQiADAAAAADchkAEAAACAmxDIAAAAAMBNCGQAAAAA4CYEMgAAAABwEwIZAFSirVu3ygMPPCB9+/aV7t27y1VXXSWvvvqqnDp1ivNgZ/r06XLBBRd43DFZvHixXHbZZdKpUye56aabnHrtu+++KxdffLH4Gk89VwDgLQhkAFBJPvroI2nfvr0cPXpUHnnkEXnzzTdl6NCh8sYbb0i/fv388jxMmzZNevfufcb9R44ckY0bN4onyczMlOHDh0urVq3kww8/lEmTJjn1M6WkpMjmzZsrrH2lvW9Fv9YTzxUAeJNgdzcAAPzBqlWrZMKECfL000/Lww8/bLu/a9euppfs0UcfFX+UmpoqmzZtOuP+a665RgYMGCCe5M8//5Rjx47JDTfcIG3btnX6Z6po5Xlfd7UZAEAPGQBUihdffFFiYmLkwQcfPOOxyMhI87jV6tWr5frrrzdhTXvOXnjhBcnOzj5j6NuiRYtk7Nixct5555lQt2bNmiL7PXHihDz22GNmHxdeeKHcf//9kpycXOQ5e/bskXvuuUd69uwp559/vjz55JNy+vTpM95rwYIFMmLECDNUb8qUKdKuXTvZsGFDkX1pG3v06GF6j9S8efPM8/TWpUsXGTVqlHz++edFhrr985//lOPHj9uep21R33//vQmw9lx1XEpztv2//fbbpndM6c+hbf3f//53xj7O9jNZOdK+c50XZ9/3bD/b2V57rnPoDEd/11xxfEr6vZ07d6557K233rINGb799tvNedSfLz093fbaXr16SW5ubpH3/O2338zztm/fXqafHwBKZQEAVLiGDRtarrjiinM+b926dZbw8HDLjTfeaPnll18sn376qSUuLs5y5ZVX2p7z/PPPW4KCgiydO3e2fP3115YlS5ZYRo8ebalZs6blyJEjtueNHDnS0q1bN8vcuXMtS5cutbzyyiuW4cOH2x7fsmWLJSYmxtK3b1/zHL116NDBMmrUqDPeq23btpYvv/zSsnbtWsuxY8csTZs2tdx///1F2v7ZZ5+Z5+7du9d8f+LECfPz6O3333+3vPnmm5bq1atbpkyZYh7Xtj700EPmPuvzdu/ebR579dVXLdHR0RVyXMpy3JOTky1ffPGFRf+3OWvWLPN8PQ7Fne1ncrR9jpwXZ973XD/b2V57rnNY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pM24uLgyH5yNGzfKc889V6ZApj1j1v2UtQ15eXnmZ42JiZGgoKAy7QOVi3PmfThn3odz5n04Z96Hc+Z9OGfeRTONdt5YM4NXBLIZM2bIe++9Z4YOnjx5UrZt2ybNmjU743k//vijjB8/3oSyjIwMadWqlUyfPt2sFWL15ptvSmpqaonv88gjj0hOTo7cd9998vnnn5e5vdZhihrGdB2Tsv5hhYWFmRNFIPMOnDPvwznzPpwz78M58z6cM+/DOfNOjkxt8piiHnPnzpUHH3xQ3n333VKfs3fvXrnsssvkb3/7m+zfv98kz2rVqsmoUaNMQLPKysoyc7xKuumUuddff90EIA2AWtBDF4h8++23ZevWrZX00wIAAACAB/WQffDBB+brF198Uepz3nrrLTNMcdKkSeZ73dYhhzou8+eff5YBAwaY+++5556zvpdOrjt27JgJaEpDmoY4/eQBAAAAAPwukDni119/lfPOO6/I3LKuXbtKRESEecwayM5l4MCB5mY1depUueuuu6R169alviYtLc3crLR3TmmIK2uQ09dpzx5B0HtwzrwP58z7cM68D+fM+3DOvA/nzLs4c33vVYFMhywWD10BAQFSt25d81hZ6Xyy2NjYsz7nxRdflCeffPKM+7Uoh84DKwsNYydOnDDbgYEeM3oUZ8E58z6cM+/DOfM+nDPvwznzPpwz76IZwScDmQ4rDAkJOeN+7THTx8rKkSqL999/v0ycOPGMyilaIdGR6ilnS861atWiqIeX4Jx5H86Z9+GceR/OmffhnHkfzpl3cSabeFUgi4qKMgU4itOqjPpYRb93Se+hxUHKUyFRe8bKuw9ULs6Z9+GceR/OmffhnHkfzpn34Zx5D2eu7b1qnFzLli1l+/btRe7T0vfaW6XrlgEAAACAN/GqQDZs2DBZsWKFKXlv9c0335gxtUOHDnVr2wAAAADAawPZn3/+KevXr7cV59CFofX7Q4cO2Z5zww03SJs2bWT06NGmquLXX39tStzfeuut0rx5cze2HgAAAACc5zFzyJ566inT+6Xatm0riYmJZnv8+PHywAMP2Ip36HpjTz/9tLlP1yF7+OGH5c4773Rr2wEAAADAqwPZ+++/79DzatSoYUrQAwAAAIC385ghi94iKSnJrFnWsWNHdzfF42mxFR1W6oqFr125L39y/PhxWbBggTl2qamp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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "total_energy = out.scalars.total_energy\n", "energy_error = total_energy.struphy.analysis.relative_error()\n", @@ -4248,36 +414,10 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "31", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/diagn_tools.py:220: UserWarning: No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.\n", - " ax.legend()\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy8AAANWCAYAAAD3ALM5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAb0FJREFUeJzt3QeYZFWdN+DTPRlmpocchygZlAEESQIiYRUliIKIqyK4yxowgMB+oKDAGEBxdRFRQCWouARBAQUEBQeWLMmRDCNhEJjQk3pSf8+5bDXdMx2qq6vqpvd9nnpmqupW1e3Tt6rPr/7nnNvS2dnZGQAAADKuNe0dAAAAqIbwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAgAA5ILwAjTc3Llzw5e//OXwjne8I2y++ebhsssuC7fcckvy/7/+9a+Dfr477rgjeez9998/4LZPPPFEsu11113XsNcAAJpDeAEa7rjjjgu//vWvwze/+c1wzTXXhPe+972hvb09/P3vfw/z588f9PPNmTMneey8efMG3LajoyPZdtasWQ17DYqhlkAdj+PLL788HHTQQcljTz755IbuI0DZDU97B4Diu/LKK8MnPvGJsMcee3Td9u53vzv87W9/CxtssEGq+wYVgw3US5YsCRtvvHFyLB922GHhiCOOCC+99JIGBWgg4QVoqNgRnDlzZhg/fnyP28eOHZt8Uw15NWzYsPD888+H0aNHJ5U6ABrPsDGgV4sWLQo//OEPw/777x/e+ta3hr333jucd955ybfN1frBD34Qtt12267/x7Cy7GXZITpDed3p06eHz372s2G77bYLe+65ZzK3pt7i8LPTTjst7L777uFtb3tbOPDAA8PVV1+93HYvv/xy+PSnP91jX2qdf9O9bX70ox8lw+7ia7/vfe9Lnrezs3NQ+3f99dcn+zF16tTwy1/+Muy1115hp512Sob1VZ7rF7/4RdL2cf/jUKiFCxf2+Rxx2/gccdvPfOYzye+hlnbr/pxxeGGsaEyaNCkce+yx4Z///Gev7THQsVLtc/785z9P9j06/PDDu47P3n633cXgAkDzqLwAvc4TiR3CRx99NHz9618P22+/fXjsscfCCSeckExkj2P8qxE7gbvsskvy+Pj/SuewMr8gXu8+RGcor/v666+HnXfeOfk2/Fvf+lZYY401wqWXXhruuuuuuv2GYwUpvkb8lv3b3/52MuTt2muvDR/4wAfC//t//y/Z5+i1115LFicYNWpUEgjivsQOftyXWubfRAsWLAj77bdfEvZOP/30ZD9i5/tXv/pVEizisLxq92/27NnJfpx//vmhpaUlTJ48OWnn+PtYvHhxmDBhQnL/GWeckfwbQ1h8jXPOOadrfyrP8d///d/JY84888zwyiuvJL+rGBjuvffesPLKKw+q3SrPedFFFyWLPMSfMz5nfP1HHnkk3H777YM+Vqp9zhgE47Cx2AbxdxYDVrTWWmsN4YgBoO46AZZx1llndba0tHROmTKlx+2///3v49fynTfddFPVbdbe3p485qtf/WqP26+++urk9jvvvHPQr3vDDTck12+//faubY4//vjOYcOGdT755JM9HnvIIYck215yySWD+j339honnHBCsn8PPvhgj22PO+645PbHHnssuf7FL34x2Zenn366x3aHHXZYTfsSfe1rX0se++c//3m5+xYuXDio/fvFL36RPNcRRxzRY7tjjz22c8KECZ0f//jHe9z+2c9+tnOFFVboXLBgQddtlec4+OCDe2wb2z/+7LENKga7X4cffniP7S6++OLk9nvuuWfQx8pgnrO3Y3Kwx/nHPvaxQT8WgOoZNgYs54orrghbbrll8m15d/vuu29YccUVww033JC51/3d736XVDviBOrujjzyyLrtX3yNOAyu8q18Rax6xOFW8f4oVh7iz7Dhhhv22C5Wn2oVV2vbZpttkmFXyxoxYsSg9q/iQx/6UI/rcdhXrJIccMABy90eV12bNm3acq8dJ6l3F9t/1113Db/97W+7bhvsfsXJ793tuOOOyb+xglLrsVLNcwKQfYaNAct57rnnkvkEW2+9dXI9djArlzh8KM7nyNrrxlWelu0cRxMnTqzb/r344os9VkyrWH/99bvuj+J+Vub6dLfuuuvW/Nr/+Mc/eg0utexfX/sTh4v1d3scDveWt7xlwPaNtz3wwAM179eyz1kZfvbqq6/WfKxU85wAZJ/wAixn3LhxSQf2wgsv7LV1ll05LAuvG1cvi/NeltXbbUPZvxkzZix3e+zUV+6v7Etv2/V2W7Xizz5QR7va/auI84N609ft3RcGqOirzbu/Vr32q/vrD/ZYGczPBEB2CS/Act71rnclqyzFieYrrbRSLl43Lgxw8803J5Oy47ChiltvvbVu+xdfIw5xih3x7vsXXzeKw6WiOJTpj3/8YzLUaoUVVuja7s9//vOQ2iauDBarFGuvvfaQ9q+ebrvttvAv//IvXdfjz3z33Xcnq481cr8acYxWVg6LCxAAkE3mvADL+epXv5rMo4hzIp599tmu2+PqVt/4xjd6rPqUldeNq0zFlaXiKlJxZa5K5ziuelUvJ554YjIk6eijj+46r0d8/rjfsYO+zz77JLd9+ctfTlYUiytXxVWxKp38e+65p9fnjUspx5XE+nPKKacknes4byYOmYriksBXXXVV15yRavevnu6///5w0003Jf+PP2ts//izx32paMR+NeIYrcyXevDBBwf9WACaQ3gBlhOXso3L+sYKxmabbRbWXHPNsNpqqyXn0ojLyW611VaZe90ddtgh6cjHJZjjHI342LiMb2UZ3nqI5we58cYbw5NPPpnMmYjL6MaqQTx3SJygHpcdruzLlVdemXTq29rakn2J57mJAaT7BPuKuMTvQEPCNtpoo/CXv/wlCTCxkx2rL7HiEKsxlbk+1e5fPcVzt8Rzz8QKSPxZY2CMP3tsg8G2W9rH6CabbBI+97nPhS984QvJ81dznpfKOWHiUs1R3L5yjphaz+cDQN9a4pJj/dwPlFz8Nj0OVYqBoJbhOfEjJq7otOqqqyaXivgNfJyEHjuJvZ3or7/XjUPD4spXccL3mDFjlnu9eNbzOOchPi5+4//0008nnf3BzNXp7zWiGDZipSd2xHu7P1q6dGnyHJV9iRWSuJJX7ODHEypG8cSVm266aXKyyWVX7upLbLtYYVhnnXXCyJEje92mv/2LnfsXXnghWQ0tnotm2duX/Z309ruKoenDH/5wMjE/Lk4Qh4TF14sT41tb+/5erJb9ihWm2E59DRHr71ip5Tnj7z4uABGHj8X9jKGsL7EK1v1cRd0N9pgDYGDCC0CTxG/0YzUonoG+0mGOJ4qMZ4WPQ5X66/RnzbLhBQCaIT9/KQFyJM67qJwXJVaD4nla4vCqT37ykz2+6T/kkEOSoW55Ci4AkBarjQGDFofivP3tb+93m/htfPx2vqz7vd566yXnNolDiuKwqxhgjjnmmPDtb3+7x3arr776kF8LAMrCsDFg0OJcjscff7zfbeJ8hspJCMu833EuSFx9K85PWXaifp71NZcEABpJeAEAAHLBIGsAACAXSjHnJQ4Victojhs3riHnOQAAYGji3MA4JDUuM561RUziyY/j0vtZNHLkyF5POVBUpQgvMbjEcw8AAJBtcaXGddddN2QpuGywwdgwffqSkEVrrrlmeOaZZ0oTYEoRXmLFJaqcLA4AgGypnOi20m/LilhxicHlb3/bKIwbl62KUHv70rDFFk8n+yi8FEhlqFgMLsILAEB2ZXWIfwwu48cPS3s3Si9b8REAAKAPwgsAAJALwgsAAJALpZiwDwBA409N0d9ywiNGjAjDhpkzwtAILwAADEkMLXG53hhg+jNhwoRkad+sTson+4QXAACGdHLJl156KamqxKWOezvBZNxm3rx54ZVXXkmur7XWWlqcmggvAADUbPHixUkwWXvttcMKK6zQ53ZjxoxJ/o0BZvXVVzeEjJqYsA8AQM2WLHnjzPMjR44ccNtKuFm0aJEWpybCCwAAQ1bNPBZzXRgq4QUAAMgF4QUAAMgF4QUAAMgF4QUAAMgF4QUAgCGL53KpxzbQH+EFAICaxZNTRgsXLhxw23g+mGjEiBFanJo4SSUAADUbPnx4cv6Wf/7zn0koaW1t7bXiEoNLPEHlhAkTnKCSmgkvAADULJ67Za211grPPPNMeO655/rdNgaXNddcU2tTM+EFAIAhGTlyZNhkk036HToWqzKVIWZQK+EFAIAhi8PFRo8erSVpKBP2AQCAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXBBeAACAXHCSSgAAGMDImUvDyCUtmWqnke1LQ9movAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkgvAAAALkwPGTAE088Ef70pz+FFVZYIbz3ve8NbW1tPe6fN29e+N3vfhemT58ettlmm7DHHnuktq8AAEBJKy8nn3xymDRpUrj11luTy+677x6efvrprvtfeuml8La3vS2ceeaZ4b777gsf/OAHw5FHHpnqPgMAACWrvPz85z8P55xzTpgyZUrYYYcdusLK4sWLu7Y58cQTw7hx48Kdd94ZRo0aFR555JEkzBx66KHhoIMOSnHvAQCA0lReYnA57LDDuoJLtNZaa4WJEycm/1+yZEm46qqrwsc+9rEkuERbb7112G23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gTJZOGdM6Fg0ImTJwo7ydeXL9xOTO1Ycq52wAgAUifBSEnlcJpnBE1Yaw9Axysa8FyCrWtPeAWBoneruF0hj6BgA+fLoo4+GO++8s+7P29nZmTz3X//617BkyZI+t2tvbw/3339/eP311wf9GsIL5IiwAgDU6tJLLw077LBD2H333cM+++wT6mnq1Klh8803D3vvvXc44IADwgYbbBDuvvvuHtssXbo0nHjiiWGNNdYIn/zkJ8N2220XTj755EG9jvACOaGyov0BYKgB47zzzgtnnXVWv9vFkBErKPGycOHCqiouhx12WNhyyy3Diy++GKZNmxb222+/cOihh4aOjo6u7f7f//t/4eKLLw733HNPeOCBB8IzzzwT1ltvvUH9DMILANDrvBegWM4444yw44479rvNrbfemlRNDjzwwCSQrLPOOuHqq6/u9zH33ntveOihh8Ipp5wSWlvfiBennnpqEmJuuumm5HocInbuuecmt2+11VbJbS0tLeHYY48d1M8gvJALFhyA7DLvBSBds2fP7nHpXu0YjFg1Ofjgg8M555wTnnzyyfDII4+Eiy66KHz0ox9NgkhfYhUlhpZJkyZ13bb++uuH1VdfPbkv+stf/hIWLFgQ3ve+94UXXnghuT3u62AJLyWg4w/1YegeUE+CP/UyceLE0NbW1nWZPHlyTc9zySWXhFVXXTV5vjihf8qUKWGVVVYJ48ePD7fddlufj4tVlQkTJnRVXSriYyuT8mMwivd/97vfTao/H//4x5O5L//5n/85qH20VDIAAOTYtGnTkoBRMWpUbSfIfvzxx8OMGTPC8ccf3+P2jTbaqCuYxHkwcZso3rbLLruEESNGJFWVZc2fPz+MHDky+X/cJs6lefXVV8Nzzz0Xhg8fHm6//faw1157he233z584AMfqGofhZdBnCkdwDlfKBPne4F8GD9+fI/wUqsxY8Yk813uuOOOPreJE+7vuuuurpB0yy23JEPE5s2bF2bOnJlUYKLFixeHV155pWtCfnze6Oijj06CSxRXPYvzX2KIqTa8GDYGwJAZ/gKQf3vuuWdyjpaHH364x+0xiFQqK2effXYSbuIlBpfK42Jl5dprr+16TJyoHwPNu9/97uT6zjvvnASsON+lYtGiRUnAWW211areR5UXAAAoyVLJr776anjqqaeSIVyVCsu2224bxo4dGw45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gCFRhSEtwgtAk5R93ouqCxSP9zXNJrzkhLkbAEAWqcKQuTkvjz76aLjnnnuqftKtt9467LDDDkPZL6hr8GudaXoXADRSWefC0FxV9ehuuOGGcPrpp4e2trYBt50zZ044+uijhRcAgJKpDCMTYmiUqr+O/rd/+7dw9tlnD7hd3Obll18e6n5B6bW2LQhLZ/kGq4jzXha3j0l7NwAaShWGVMPLTjvtFNrb20O9twUAoJhUYUgtvOy+++5VP+FgtgUAoNiEGOrJamM5YKUxKJayL5kMAKmHl+uvvz4ceeSR4Re/+EW9nhLqRgAEgHTEyfsm8JO58PL666+HqVOnhldeeaVeTwmF1NLWkfYukAGqL0AZAovQQr3V7eQXseoSL0D9DRu3ICyxdj4AGSao0AzO3AeQEssmA3knsJD58DJlypTwhz/8oc/7d9lll7DvvvsOdb/4P+ZqAABZI7SQm/Dy/PPPh5tvvrnHbXPnzg2PPfZYmDBhQlhttdWEFzIbBFtnKjaSLaovQF4ILGTBoHtyhx9+eHJZ1lNPPRXe8573hPe///312jcAAFIksFDY1cY23njjcPDBB4frrruuXk8JUApWHgOyxEphZFldx9DMmzcvvPrqqyHvLGULAJSJCguFDS9///vfwwMPPNDjtiVLloSHH344XHDBBf1O5mdwTNaH8jD3BUiD0ELhw8stt9wSvvKVr/R8kuHDw3rrrZeEl3e+85313D+oK5P2ASg7gYVShZf/+I//SC4A1JfqC9AoAgtFYd1YAIACElgoorqFlwcffDDccccdYdKkSWHXXXet19MClIrqCzAUAgtFV7elkuOJKz/72c+Gq6++OmRV6/iOtHcBYECWTgZqXd4Yiq5ulZcvfelL4fOf/3xoba1bHio1K401tm1bZxoxCUC+CSuUUd16cC0tLcmqYwAMneFjQG8EFsqu5rQxa9as8Pjjj4fXX389dHZ2dt2+4YYbhs0226xe+wcAUGoCCwwxvHzve98LJ598cpg/f35ScamEl/j/E044IXzzm9+s5WkB6P4BPW5+WNw+RptACQksUKfw8re//S2ceuqp4YYbbgh33313mD59ejjppJPCRRddFL7//e8n/wf619LWETpnjdJMAPQgtGRXx/zRYfiibP3t7ljcEspm0LPr77rrrvD+978/7LHHHsnk/MWLF4dVV101fPnL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7rnnhqeffjosXLgwzJkzJ9TL1KlTw4EHHhhmzZoVRowYkdx25ZVXhh133LFrmx/+8IfhpJNOChMnTkzC07hx48LVV18dNt1006pfx2pjABQizCx7AWD5gHHeeeeFs846K/Rn6dKlSXUmXmLIqabicthhh4Utt9wyvPjii2HatGlhv/32C4ceemjo6OhItrnrrrvCpz/96XDhhReGRx55JDz33HNh7733TraJj6+W8NJEJusDZZNmiBBoAHo644wzelRCenPrrbeGDTbYIKmixECyzjrrJNWR/tx7773hoYceCqecckpobX0jXpx66qlJiLnpppu6nne11VZLwkoUh4sde+yx4eGHHw533nlnqJbwAkDDpD03Jb5+9wtAEc2ePbvHpVLtGKxYNTn44IPDOeeck8xLiRWSiy66KHz0ox9NgkhfHnjggSS0TJo0qeu29ddfP6y++urJfdHKK6+c7Fv3oWovvPBC8u99991X9T4KL9AP1TKyZvi4+WnvQqYsG06EFaCMJk6cGNra2roukydPrul5LrnkkrDqqqsmzxerIVOmTAmrrLJKGD9+fLjtttv6fNzrr78eJkyY0FV1qYiPjfdFH/zgB5MAE8PRb3/723D55ZeH4447LowePbprm2qYsA8pa2nrCJ2zRqW9G+QswOTpfC8xUNQ6fEy1BGBg06ZNSwJGxahRtfUrHn/88TBjxoxw/PHH97h9o4026gomcR5M3CaKt+2yyy7JBP24itiy5s+fH0aOHJn8PwaXOLzsO9/5TvjBD36Q7O/FF18c3v3ud4cxY6r/mya8AJBagBFOAIZu/PjxPcJLrWKIiPNd7rjjjj63iYEjTr6vhKRbbrklGSI2b968MHPmzKQCE8Xlkl955ZWw3nrrdT12rbXWCt/+9rd7hKX29vZkWeVqCS9NYvgRUObqSySoAGTbnnvuGc4///xkEv0222zTdXsMIvESh3idffbZvT4uVl+uvfba8K//+q/JbXGifgw0sbLSvRLTvcoSl05ee+21e2wzEOEFIKfyGGAASHep5FdffTU89dRTyXLIlQrLtttuG8aOHRsOOeSQZInj/fffP1k5LA4Xi4/5yU9+Eq655pqw8cYb9/q8cZ5MHGr2+c9/Pgk5sSJz4oknhqOOOipsttlmXdsddNBByeUtb3lL+N3vfhcuuOCCcN1113UNLauG8AIA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RJ1hQVjamb19KptMh5fXX389+XeVVVbpcXu8XrmvN5MnT06Gmy1r4sSJDdhLAADqpb29PbS1tWWmQUeOHJnMuX755TNCFq255prJPpZFpsPLiBEjuibtdzd//vx+f0knn3xy+OIXv9h1febMmWH99dcPzz//fKbeDFkUq1Mx5MXV3azqpp0cU95/WeWzSls5ror3HowVlxhc1l577ZAlo0ePDs8880xYuHBhyKKRI0cm+1gWmQ4v8U3R2toaXnjhhR63x+vrrbden48bNWpUcllWDC465NWJ7aSttFM9Oaa0VSM4rrSV46pY78Gsfskcw0GZAkKWZXqp5BVWWCHssssu4dprr+26be7cueHmm28O++yzT6r7BgAANFemKy/RGWeckQSVOBRs5513Ts7xsvrqq4dPfepTae8aAADQRJmuvER77LFHuPXWW8Nzzz0Xvve974Wtttoq3HHHHWHs2LFVP0ccQvbVr36116FkaKtaOKa0VSM4rrSV4ypd3oPaiuxr6czqenQAAAB5qrwAAABEwgsAAJALwgsAAJALmV9tbKhee+218PTTTyfnjIlnIGV58QRTcUGE7oYPHx7e8Y53aK4QwtKlS8N9992XrO++zTbb9NomHR0d4dFHHw0rrrhi2GyzzUrbbosWLUraauWVVw6bbrppj/vmzJkTHnzwweUe87a3vS2MGzculM0//vGP8Prrr4eNN944OW56E0+w++STTyafXeuuu24oozgt89lnnw3z5s1L2mrZ8yy89NJL4amnnlrucbvttlsoo/hZPmvWrLDhhhv2+b6K50qL7Rbbc6WVVgplPa7icRNPgr3RRhslp2bo7u9//3v45z//udz5R/r6G1AGlffaJptsEtZYY43l7n/88ceTz/m4sJIFkmiozgL7yle+0jlq1KjOLbfcMvn3k5/8ZOeSJUvS3q3M+frXv945bty4zl133bXrsv/++3eW3cKFCzsnT57cudFGG3W2tbV17rHHHr1ud/3113eussoqyXYrrbRS5/bbb9/54osvdpbJ7NmzO0855ZTOddddt3Ps2LGdH/nIR5bb5p577omLg3S+4x3v6HGsPfroo51lcu2113ZuvfXWneuss07nNtts07niiismn1XL+va3v905evTozi222KJzzJgxnYcddlhnR0dHZ5n87Gc/69x44407N9xww6Qd4vvwBz/4QY9tvv/97yft0/2YipeyufHGG5PjKrZX/De2yfHHH9+5dOnSHp9pRxxxRNdxFf/9xje+0Vk2l19+efJ5vfnmm3duttlmyd+/b37zmz22ie+3NdZYo8cxdeyxx3aW1dy5c5O+VPwM//GPf9zjvpdeeqlzxx137JwwYULSriuvvHLnb3/729T2leIrbHi55pprOkeMGNH5l7/8Jbk+derU5A/f9773vbR3LZPhZaeddkp7NzJn5syZnSeeeGLnU089lQTf3sLLP//5z+QP39e+9rXk+vz585PO+b/8y790lklso9gGMbTtt99+/YaXGTNmdJZZ/Ax65JFHuq7/6U9/Sj6rfvWrX3Xddtttt3W2tLR0/v73v0+uP/fcc52rr7565+mnn95ZJmeddVbnM88803X9iiuuSNrljjvu6BFeYge07C688MLkfVhx1113JW119dVXd912xhlnJMfRs88+m1z/wx/+kGxzyy23dJZJDMDdv2C68sork8+m+BnVPbzEz33e8IlPfKLzS1/6UuewYcOWCy8HHHBA0oeYN29eV58ifok1ffp0zUdDFHbOy0UXXRT23nvvsMsuuyTX41CeD37wg8ntLG/x4sXhr3/9a1Iqj//njSEC3/jGN5IhBX359a9/HZYsWRK+9KUvJdfjkJbjjz8+3HjjjUmJvSxiG5166qlhrbXWGnDbOAwqDh+bO3duKKPPfe5zybCKine+851d56+qiJ9TO+20U9h3332T6+utt17413/919J9fsWTE2+wwQZd1+Nn+CqrrBL+8pe/9Nguvgcffvjh8Le//S0ZulhGRx11VI/Pqh133DEZjhiHiFXE4yceR+uvv35yPZ4AOh5nZTuuPv3pT/f4rNprr72Sf7u3VRSHKt57773JsMUyn1XiF7/4Rbj//vvDmWeeudx906dPD7/73e/CCSecEMaMGZPcFv8exva64oorUthbyqCw4eWBBx4I22+/fY/b4of5I488Uto/bgO11xFHHJEEvji+/pJLLkl7l3LTbltssUWP8dLxOIsf3L3N7yCEQw45JHz4wx9Oxtp//vOfL31YnjFjRjKO/C1vecuAn19xPkPcvqzi/MU4T6h7W0Wx/T70oQ+F/fffP6y66qrh/PPPD2U0e/bsJATfcMMN4cgjj0yCX3yvVe6L7dfbcRWPt7J59dVXk7a67rrrwkc+8pHkS4R4/HR35ZVXhk9+8pNhhx12SI652267LZRNfG/Fz+nLLrus13ks8UvP+Peu+3EVQ0z8QqaMxxXNUdgJ+/EPXPyGrrt4PX5DFz/El72vzN7+9rcnf9Qq38ade+654eMf/3gyKc+k/dqOs8p9vCmGlZtvvjkJyNHdd9+d/D+2V6zalFH8o/+pT30qaZuPfexjVR9XZZxkHb90ip9Lb33rW8P73ve+rtu33nrrMHXq1K4FIi6++OKuKkSlclUWMdyedNJJyTESqwiTJ09OFs/o/nnU23FVxs+q2On+6le/mlQO4gIH3//+93t0zj/wgQ+EH/7wh8l7LR57sWJ68MEHh8cee6yqCnMRxJ87ht/4+dy9Wtyd44o0FLbyMmLEiGQVke7mz5+f/Dty5MiU9iqb9ttvv67gEsVvWWJwiUOi6J/jrHpxZaNKcKl84xu/1fzlL39Z2sMsdoj++Mc/hmuvvTYZpljhuOopfukUK8Oxc37NNdck7VOx55579ljZ7hOf+EQyFOpXv/pVKJu4ElasJsQO9h/+8IdkKM+FF16Y3Fdps97+Lpbxb2L8LIpt9cQTTyTD5mL1JQ737T5EsfIlQWy77373u8lKWt23Kbpvf/vbyZe92267bdJWlaGtcehvHEYWOa5IQ2HDS+yMLzt+NV6fMGFCKZdlHay4DOKy7Uf1x1llngKOs7584QtfSIZi3HTTTcly0dUcV7GjULYl32NwiR3L//3f/w233nprjy9a+uLzKyQBLg6FikPIonjcxMpCb8dV2T+rDjjggGT4b3/BJM5nHD9+fKn+LsZQG4dhxmpe5RLfj3E43TnnnJNsU3k/Oq5opsKGlzgR8frrr0/eaBW/+c1vktvpadmJ03Ft+zhfIw7HYODjLI4Jjt90dj/O4lCN7bbbTvP1c5xFseNexuPsi1/8YvjZz36W/Py9HSfxuIrfnMfzB3U/rmKVoXvVoeji53ecuzFlypRkvkFvi2cse1zFb4pj0CnbcbVsO8S2ixPNK8PEhg0blkxMj1W+inh8xQ57mf4uxqFQy857jRPz4wIrlbaKi9YsXLiwxzax0hCHSJXpuIqLz1QqLpVLPI5OPPHE5IuXKFZlYsDpflzFhTNiRatMxxXNNbzInYOf//zn4fDDD09WV4mrYcQO+Y9+9KO0dy1z4h+0gw46KEyaNCmZxPitb30rrL766uE//uM/QtnFlWbiMIvKuOhK2bxyArw49CCOq49DDE4//fTw4osvJiuyxCEGZepkRnEFqDiHI55YMf4b2yp+0xvnVFVWjoqdgthm8SSol156abjzzjtLNQwj+spXvhK+973vJcdIHLJTOaZiB2DzzTdP/h/fexdccEGyuMG///u/Jx33OF/oT3/6UyiTY445Jlx99dVJW8T3VrxE66yzTnISxijOf4kVhnicxeDyne98J3nvxb8BZRJ//jjXJw4da29vT+b+vPzyy0mFr+JrX/ta2H333ZPb3vWudyV/D+N79DOf+Uwoi7jgRfzMPvroo5NVSOOXdeedd15SWYnHWxQ/6+MXBfF6fE/GL6jOOOOM5Lbu861444TW8W9eHAIbh77G9+Zpp52WHF9lm3NG87TE9ZJDQT3zzDNJRzwu/xvL4vEDe9nhGbzxYf6DH/wg+bYyrpoV5yLEztOyZxwuoxhKelvyuPuytvFbu9hh+vOf/5y0WRziEh9XNvEP+7LLbMcQfNVVV3V9ExxXsYvDWOK3xLFT8NnPfraqYUBFEjtNcYJ5b18ifP3rX++6HodhfPOb3wyPPvpoMkE4tlUcClQmBx54YHjttdeWuz2uLBY7S1HsqMfOZwzPcZhLrGTFzngc4lMmr7zySvI5Hr9wias9xYUNjj322OQ92N0999wT/uu//isJgnGoVBwKtO6664YyiXOnYlvF5bXjcRKDX1w4o/u8s+7bxC8WYmc8LqoRKw9lFj/nY0UmDrXrLn7Ox8/3OC8oBuS4XHJcqhsaodDhBQAAKI7CznkBAACKRXgBAAByQXgBAAByQXgBAAByQXgBAAByQXgBAAByQXgBAAByQXgBaKD9998/OTFnNeKJF3fYYYfk8vjjjzf991J57bjPAJBFwgtAg7z++uvh97//fVhrrbWq2j4GlnjW8/PPPz9MnDix6b+X+LrxrPYPPvhg018bAKoxvKqtABi0Bx54IIwYMSJsueWWVT9mjTXWSKofaYivO3Xq1FReGwCqofIC0CCxghGDy8iRI5Prr7zySvjABz4Q/vM//zMsWbKkqud47bXXuoZz7bzzzuGII44If/7zn/t9zG9+85vw/ve/v8dtf/3rX5PnWLhw4RB+IgBIl8oLQAPDy6RJk5L/33nnneFDH/pQEj7OOOOM0Npa3XdHbW1tyXCuKAaPKVOmhP322y/cdNNNYbfdduv1Mf/85z/DQw891OO29vb2cN9994WlS5cO+ecCgLQILwANDC9HH310+MEPfhC+8pWvhAsuuCAceuihg3qO4cOH9xhGtssuu4Rp06aFH/7wh32GFwAoKuEFoAE6OjqS+SM/+clPwoIFC8Ltt98ettpqq5qe65ZbbgkXX3xxeO6558L8+fPD9OnTw7rrrlv3fQaArDPnBaABHnnkkWRey8orrxzWX3/9sNlmm9X0PHG1sgMOOCBss8024fTTT0+GkMX5LDHEAEDZCC8ADRoy9pa3vCVcc801yTCvz3zmMzU9T3z8QQcdFE488cTwrne9KxlCNmfOnH4fs+KKK4Z58+b1uO2ll16q6fUBIEuEF4AGhZe3ve1tYaWVVgrXXXdduOKKK8K555476OeJlZtHH320q9ISVxr75S9/2WObOJdmr7326rr+1re+Nbz66qvhT3/6U3J95syZ4ZxzzunxmLiAQAxCcSgaAOSF8ALQwPASbbrppuHXv/51OOmkk8L1118/qOf5/Oc/n6xMFue4bLLJJuHwww8P++67b49tXnzxxeScMhVxbk2s1MTt4lLN8cSXlX2pmDVrVrL6mOFnAORJS2dnZ2faOwFQNA8//HBYe+21wyqrrNJ12+OPP57Mg4lhojf7779/EnpiULn88suT0BPF5Y2feeaZsGjRoiTAxKpKPP9L5eSXMbzEc8hsu+22PZ7v9ddfT5ZN3njjjZNFA+ICAttvv31oaWkJs2fPTvZn6623DqNHj062j5WY+Jg45Ozll19uYOsAQG2EF4CMeOKJJ5KKSKV6MmbMmKa+/r333pv8O2LEiOUqNQCQBcILAACQC+a8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAuSC8AAAAIQ/+P9vLYMByZ1pOAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "spectrum: (20, 8)\n" - ] - } - ], + "outputs": [], "source": [ "omega, kvec, spectrum, _ = out.em_fields.e_field_log.struphy.analysis.dispersion(\n", " slice_at=(None, 0, 0),\n", @@ -4298,23 +438,10 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "33", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wrote:\n", - " post_processing/report/scalars.csv\n", - " post_processing/report/scalars.png\n", - " post_processing/report/electric_energy.png\n", - " post_processing/report/kinetic_energy.png\n", - " post_processing/report/total_energy.png\n" - ] - } - ], + "outputs": [], "source": [ "written = out.save_report()\n", "print(\"Wrote:\")\n", @@ -4334,39 +461,10 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "35", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", - "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 20/20 [00:04<00:00, 4.05step/s]\n" - ] - }, - { - "data": { - "image/png": "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Q1lPNox1ZB9YT2YJScna2fBoqz4khszU0ZUrV7QiRYpotWrV0tauXavt2rVLa9++vVa5cuWUff/880/1/zUyMjLDr5H/xYUKFdIaN26srVq1Sjtw4ID20UcfaZMmTdJiY2O1GTNmqGP++++/2rlz57TLly+r1y1YsECtL126tLZkyRLt9OnT2s2bN++K4UHHv5+MxG15/woVKmjLli3Tjhw5or388suak5OTijUrx0p/LnPmzNFcXFy0Tz75RDt06JD23XffaXnz5lX77tmzR9WTlFFAQID2wQcfpDmHyZMna0WLFn3g/62vv/5a8/DwSLNPdHS0eo/PP/88zb5vvvmm5u/vryUmJqrn48aN04KDg1O2S7xSP/KQ858+fbrm6uqqLV++PGWfsmXLat27d9d2796t7d+/X/vss8+0kSNHpmz/448/NIPBoL311lvawYMH1fn7+Pio98pMbA+TkVgHDx6s6fV6rUuXLtrOnTu1bdu2aRUrVtSKFy+ufkqdHz58WHv22We1fPnyaTdu3LjrtXXr1tUWL16sbdmyRWvVqpVWrFgxdQ4WK1eu1JydndUxpExWrFihlSpVShs7duxdx2rdurW2efNmFXNCQoI2atQoVXdfffWV+mxImbi5uWnr1q3TMqpnz55aYGCgOu99+/alvJd8DlPLyHt5e3urc5k6daoql3nz5qnfhaCgIK1bt26qDLdu3aqVL19e69GjR5rj9+nTR/2OLlq0SP1+Tpw4UX0ONmzYkGa/H374QXN3d9fi4+O17HT+/PlHyguYkDlAQnZqSjUt/nVfLfSNktrRN2to299qrq16r7u2eNpQbf7nL2mzvp2mzfjpR236wuXaV8t3aF+vO6XN3nZO+21PiPb3oSvahhNh2u5zEdq/l29poTfjNGOyydqn5JB4EWn7WEf2gfVkJglRicnLrfqQGDJTR3KRKBfD165dS1l3+/ZtdTE6e/Zs9Tx9MpSR18gFeeHChbWYmJi74hByQSvHjIqKSrPdksRIkpVa+hgedvx7yUjclvffvn17yj4mk0krUKCASqCycqz055L+4lxIspI6IZPzeOWVV7SSJUtqycnmJFsu2CVBmTJlivYgo0eP1ipVqnTXerkwr169eprzkuNPmDAhZV36hOx+x+/QoYNalkTkXueYuh7k4r1///5ptktCLhfnFy9ezHBsWZE6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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "sim_coarse = Simulation(\n", " model=build_model(),\n", @@ -4407,77 +505,10 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "38", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 80/80 [00:01<00:00, 40.45step/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "No post-processed data in /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave, processing with default options (call out.process(...) to choose them)\n", - "\n", - "Post-processing path /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave\n", - "\n", - "No feec fields found in hdf5 file, skipping post-processing of fields.\n", - "Evaluation of 3 marker orbits for euler_fluid\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "100%|██████████| 81/81 [00:00<00:00, 1507.65it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of distribution functions for euler_fluid\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "0 starting post-processing of distribution functions for /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_xc2w51vz/sph_soundwave/post_processing/kinetic_data/euler_fluid ...\n", - "100%|██████████| 1/1 [00:00<00:00, 450.90it/s]\n", - " 0%| | 0/1 [00:00 Date: Wed, 16 Sep 2026 17:23:21 +0200 Subject: [PATCH 035/193] Added ProductCatalog --- src/struphy/post_processing/output.py | 45 +++++- .../tests/test_output_accessors.py | 13 ++ tutorials/tutorial_post_processing.ipynb | 133 +++++++++--------- 3 files changed, 122 insertions(+), 69 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 68d646c51..db058cfa4 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -38,11 +38,43 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self._loaders) + def __contains__(self, key: object) -> bool: + """Check the catalog without loading a product.""" + return key in self._loaders + def clear_cache(self): """Drop loaded arrays while keeping product discovery information.""" self._cache.clear() +class ProductCatalog(Mapping[str, xr.DataArray]): + """A relative, lazy view of a :class:`ProductMapping` subtree.""" + + def __init__(self, mapping: ProductMapping, prefix: str = ""): + self._mapping, self._prefix = mapping, prefix + + def _full_key(self, key: str) -> str: + return f"{self._prefix}/{key}" if self._prefix else key + + def __getitem__(self, key: str) -> xr.DataArray: + return self._mapping[self._full_key(key)] + + def __iter__(self) -> Iterator[str]: + prefix = f"{self._prefix}/" if self._prefix else "" + return iter(sorted(key[len(prefix) :] for key in self._mapping if key.startswith(prefix))) + + def __len__(self) -> int: + prefix = f"{self._prefix}/" if self._prefix else "" + return sum(key.startswith(prefix) for key in self._mapping) + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and self._full_key(key) in self._mapping + + def clear_cache(self): + """Drop cached arrays in the underlying catalog.""" + self._mapping.clear_cache() + + class ProductNamespace: """Hierarchical, discoverable attribute view over product names. @@ -55,6 +87,11 @@ class ProductNamespace: def __init__(self, mapping, prefix=""): self._mapping, self._prefix = mapping, prefix + def __repr__(self): + location = self._prefix or "products" + products = tuple(self.catalog) + return f"{type(self).__name__}({location!r}, products={products!r})" + def __getattr__(self, name): key = f"{self._prefix}/{name}" if self._prefix else name if key in self._mapping: @@ -62,10 +99,12 @@ def __getattr__(self, name): prefix = key + "/" if any(product.startswith(prefix) for product in self._mapping): return type(self)(self._mapping, key) - raise AttributeError(f"{name!r}; available products: {tuple(self._mapping)}") + location = self._prefix or "products" + raise AttributeError(f"{name!r}; available names under {location!r}: {tuple(self)}") def __getitem__(self, key): if "/" in key: + key = f"{self._prefix}/{key}" if self._prefix else key return self._mapping[key] return getattr(self, key) @@ -82,8 +121,8 @@ def __dir__(self): @property def catalog(self): - """Flat lazy catalog for algorithms that do not know product names.""" - return self._mapping + """Flat lazy catalog below this namespace, with names relative to it.""" + return ProductCatalog(self._mapping, self._prefix) class FieldProducts(ProductNamespace): diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 6b56a7ca2..5b48be11b 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -141,6 +141,19 @@ def test_products_of_one_species_sit_on_the_output(run): run.electrons +def test_product_namespaces_expose_a_scoped_lazy_catalog(run): + products = run.kinetic_ions + assert tuple(products.catalog) == ("e1_v1_density/f_binned", "orbits", "view_0/n_sph") + assert "e1_v1_density/f_binned" in products.catalog + assert "em_fields/E" not in products.catalog + assert "e1_v1_density/f_binned" in repr(products) + assert run.distribution_catalog._cache == {} + assert products["e1_v1_density/f_binned"].dims == ("t", "e1", "v1") + assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f_binned"] is products.catalog[ + "e1_v1_density/f_binned" + ] + + def test_arrays_plot_themselves(run): phase_space = run.kinetic_ions.e1_v1_density.f assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 2297b5666..fc691198a 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -4,14 +4,14 @@ "cell_type": "markdown", "id": "0", "metadata": {}, + "outputs": [], "source": [ "# Post-processing and standard plots\n", "\n", "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, get its output as an autocomplete-friendly `Output`, and make the plots most commonly used to inspect a simulation.\n", "\n", "For a production run you can skip the simulation setup and open its output folder with `struphy.open_output(\"path/to/sim\")` instead." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -51,12 +51,12 @@ "cell_type": "markdown", "id": "2", "metadata": {}, + "outputs": [], "source": [ "## Create a compact demonstration run\n", "\n", "Post-processing operates on a completed run. The small setup below saves an electric field, a few marker trajectories, scalar diagnostics, and a binned $(\\eta_1,v_1)$ distribution. These are the main output types handled by the plotting interface." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -131,6 +131,7 @@ "cell_type": "markdown", "id": "5", "metadata": {}, + "outputs": [], "source": [ "## Process and load the output\n", "\n", @@ -139,8 +140,7 @@ "To choose options, call `out.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", "\n", "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `struphy.open_output(path, time_units=\"physical\")` makes `t` itself seconds. Arrays are loaded only when accessed. The simulation that produced them is `out.sim`." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -156,66 +156,67 @@ "cell_type": "markdown", "id": "7", "metadata": {}, + "outputs": [], "source": [ "Products sit on the run under the species that produced them, so VS Code and interactive shells complete them as you type: `out.kinetic_ions.e1_v1_density.f`, `out.kinetic_ions.orbits`, `out.em_fields.phi`. The grouped views `out.fields`, `out.distributions`, `out.densities` and `out.orbits` show the same products by kind, and `out[\"kinetic_ions/e1_v1_density/f\"]` looks one up by name, which is handy in scripts and loops. Flat catalogs remain available for code that iterates over arbitrary products." - ], - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": null, + "id": "8", "metadata": {}, + "outputs": [], "source": [ "print(out.info())\n", + "print(out.kinetic_ions)\n", "\n", "phase_space = out.kinetic_ions.e1_v1_density.f\n", "print(phase_space)" - ], - "execution_count": null, - "outputs": [], - "id": "8" + ] }, { "cell_type": "markdown", + "id": "9", "metadata": {}, + "outputs": [], "source": [ "### Products are xarray arrays\n", "\n", "A product is an `xarray.DataArray`, so xarray's own plotting already draws it, with the labels and units Struphy stored:" - ], - "id": "9", - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": null, + "id": "10", "metadata": {}, + "outputs": [], "source": [ "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" - ], - "execution_count": null, - "outputs": [], - "id": "10" + ] }, { "cell_type": "markdown", + "id": "11", "metadata": {}, + "outputs": [], "source": [ "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." - ], - "id": "11", - "outputs": [] + ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, + "outputs": [], "source": [ "## Scalar overview and time series\n", "\n", "Products plot themselves: every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f.struphy.plot.slice(...)` needs no imports and completes as you type. `out[\"kinetic_ions/e1_v1_density/f\"]` looks the same product up by name, which suits scripts and loops. The plots that need a whole run, `out.plot.scalars()` and `out.plot.equilibrium()`, stay on the run.\n", "\n", "`out.plot.scalars()` gives a quick overview of every recorded scalar. `.struphy.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -252,12 +253,12 @@ "cell_type": "markdown", "id": "15", "metadata": {}, + "outputs": [], "source": [ "## Two-dimensional data\n", "\n", "Choose the displayed dimensions with `x` and `y`, and pick one value for every other dimension by naming it: `t=\"last\"` (or `\"first\"`), `t=-1` for a position, and `t=0.35` for the nearest coordinate value. Arrays can also be sliced beforehand with xarray's `.isel()` and `.sel()`. `coords=\"physical\"` draws on the mapped coordinates instead of the logical ones." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -279,10 +280,10 @@ "cell_type": "markdown", "id": "17", "metadata": {}, + "outputs": [], "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -304,12 +305,12 @@ "cell_type": "markdown", "id": "19", "metadata": {}, + "outputs": [], "source": [ "## Interactive plots\n", "\n", "`.struphy.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -326,10 +327,10 @@ "cell_type": "markdown", "id": "21", "metadata": {}, + "outputs": [], "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -345,10 +346,10 @@ "cell_type": "markdown", "id": "23", "metadata": {}, + "outputs": [], "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -376,10 +377,10 @@ "cell_type": "markdown", "id": "26", "metadata": {}, + "outputs": [], "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -395,12 +396,12 @@ "cell_type": "markdown", "id": "28", "metadata": {}, + "outputs": [], "source": [ "## Derived quantities\n", "\n", "`.struphy.analysis` computes without drawing, and every result is an array that plots itself. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -421,10 +422,10 @@ "cell_type": "markdown", "id": "30", "metadata": {}, + "outputs": [], "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -444,12 +445,12 @@ "cell_type": "markdown", "id": "32", "metadata": {}, + "outputs": [], "source": [ "## Save standard output\n", "\n", "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `out.save_report()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/report/`." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -468,12 +469,12 @@ "cell_type": "markdown", "id": "34", "metadata": {}, + "outputs": [], "source": [ "## Comparing runs\n", "\n", "Time series accept arrays of other simulations, so comparing runs needs nothing special. Series are labelled by the run they come from, and the runs may have different time grids." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -503,23 +504,23 @@ "cell_type": "markdown", "id": "36", "metadata": {}, + "outputs": [], "source": [ "## Other models\n", "\n", "The interface is the same for every model; only the products differ. Two more short runs show the two product types the Vlasov–Ampère demo does not have: SPH densities, and vector fields on a mapped domain." - ], - "outputs": [] + ] }, { "cell_type": "markdown", "id": "37", "metadata": {}, + "outputs": [], "source": [ "### SPH densities\n", "\n", "A standing sound wave discretized with SPH markers. `KernelDensityPlot` reconstructs the density on a grid, which appears under `out.densities`, while `BinningPlot` produces the binned quantities under `out.distributions`." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -564,51 +565,51 @@ "cell_type": "markdown", "id": "39", "metadata": {}, + "outputs": [], "source": [ "For a one-dimensional run, the clearest picture is a space-time map: the sweep dimension `t` may be used as a display axis." - ], - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": null, + "id": "40", "metadata": {}, + "outputs": [], "source": [ "density = out_sph.euler_fluid.view_0.n.isel(e2=0, e3=0)\n", "density.plot(x=\"t\", y=\"e1\")" - ], - "execution_count": null, - "outputs": [], - "id": "40" + ] }, { "cell_type": "markdown", "id": "41", "metadata": {}, + "outputs": [], "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" - ], - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": null, + "id": "42", "metadata": {}, + "outputs": [], "source": [ "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" - ], - "execution_count": null, - "outputs": [], - "id": "42" + ] }, { "cell_type": "markdown", "id": "43", "metadata": {}, + "outputs": [], "source": [ "### Vector fields on a mapped domain\n", "\n", "A coaxial waveguide mode of the Maxwell model, on an annulus. With `physical=True` the post-processing also computes the Cartesian field components (`*_xyz`), and `coords=\"physical\"` draws them on the mapped grid, with the plane chosen by `plane`." - ], - "outputs": [] + ] }, { "cell_type": "code", @@ -684,6 +685,7 @@ "cell_type": "markdown", "id": "47", "metadata": {}, + "outputs": [], "source": [ "## Apply the workflow to another run\n", "\n", @@ -697,13 +699,12 @@ "```\n", "\n", "Use `out.scalars`, `out.fields`, `out.distributions`, `out.orbits`, and `out.densities`. Attribute access is the normal interactive API; the corresponding `*_catalog` mappings are intended for generic loops and tooling." - ], - "outputs": [] + ] } ], "metadata": { "kernelspec": { - "display_name": ".venv-1 (3.14.4)", + "display_name": ".venv (3.12.3)", "language": "python", "name": "python3" }, @@ -717,7 +718,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.14.4" + "version": "3.12.3" } }, "nbformat": 4, From f2d6e391ff619df226c3e6e5dfa2c02d5cf061bf Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 15:36:33 +0200 Subject: [PATCH 036/193] Fixes --- .../tests/verification/test_verif_LinearMHD.py | 10 ++++------ .../tests/verification/test_verif_Maxwell.py | 5 ++--- .../post_processing/tests/test_output_accessors.py | 14 ++++++++------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 7e380cffb..25e4032c8 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -87,9 +87,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} - _1, _2, _3, coeffs = run.analysis.dispersion( - "mhd/velocity", - physical=True, + _1, _2, _3, coeffs = run["mhd/velocity"].struphy.analysis.dispersion( + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -108,9 +107,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 # second fft - _1, _2, _3, coeffs = run.analysis.dispersion( - "mhd/pressure", - physical=True, + _1, _2, _3, coeffs = run["mhd/pressure"].struphy.analysis.dispersion( + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index e311a3f85..445ef4061 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -72,9 +72,8 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: # fft - _1, _2, _3, coeffs = run.analysis.dispersion( - "em_fields/e_field", - physical=True, + _1, _2, _3, coeffs = run["em_fields/e_field"].struphy.analysis.dispersion( + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 5b48be11b..055cde715 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -143,14 +143,16 @@ def test_products_of_one_species_sit_on_the_output(run): def test_product_namespaces_expose_a_scoped_lazy_catalog(run): products = run.kinetic_ions - assert tuple(products.catalog) == ("e1_v1_density/f_binned", "orbits", "view_0/n_sph") - assert "e1_v1_density/f_binned" in products.catalog + assert tuple(sorted(products.catalog)) == ( + "e1_v1_density/delta_f", "e1_v1_density/f", "orbits", "view_0/n" + ) + assert "e1_v1_density/f" in products.catalog assert "em_fields/E" not in products.catalog - assert "e1_v1_density/f_binned" in repr(products) + assert "e1_v1_density/f" in repr(products) assert run.distribution_catalog._cache == {} - assert products["e1_v1_density/f_binned"].dims == ("t", "e1", "v1") - assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f_binned"] is products.catalog[ - "e1_v1_density/f_binned" + assert products["e1_v1_density/f"].dims == ("t", "e1", "v1") + assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f"] is products.catalog[ + "e1_v1_density/f" ] From 674d2ee6d5823d8968fde9ef8f6abbf5996e14ca Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 15:48:35 +0200 Subject: [PATCH 037/193] PostProcessor.from_output --- doc/sections/userguide.rst | 15 +++++++ .../post_processing/post_processing_tools.py | 31 ++++++++++--- src/struphy/simulation/sim.py | 15 ++++--- src/struphy/simulation/tests/test_output.py | 43 +++++++++++++++++++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 0939415b9..dbd796ebb 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -499,6 +499,21 @@ After initial conditions are set, launch the run: 11. Post-processing and visualization ------------------------------------- +A serial postprocessor can be reconstructed from a saved output folder, including +one moved to another location: + +.. code-block:: python + + from struphy.post_processing.post_processing_tools import PostProcessor + + processor = PostProcessor.from_output("./runs/my_run") + processor.process(physical=True) + +This reads ``config.json``, falling back to ``run_metadata.json`` when needed. +The original MPI rank count comes from ``run_metadata.json`` or legacy ``meta.yml``. +Under MPI, call this factory on one rank only; use ``Output.process`` for automatic +rank handling. + The output of a simulation is a :class:`~struphy.Output`. ``sim.run()`` returns it, and it stays available as ``sim.output``: diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 6d9f4e44f..270ab22cd 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -39,7 +39,7 @@ def source_fingerprint(path_out: str) -> str: """Fingerprint the raw run files that determine post-processing products.""" digest = hashlib.sha256() - for name in ("config.json", "meta.yml", "data/data_proc0.hdf5"): + for name in ("config.json", "run_metadata.json", "meta.yml", "data/data_proc0.hdf5"): path = os.path.join(path_out, name) if not os.path.exists(path): continue @@ -83,8 +83,8 @@ def is_processed(path_out: str, options: dict | None = None) -> bool: class PostProcessor: """Post-process the raw output of a finished Struphy simulation. - Users do not call this directly; use :meth:`struphy.Output.process`, which also decides - on which MPI ranks processing runs. + Use :meth:`from_output` to reconstruct a serial processor from a saved run. + For automatic MPI rank handling, use :meth:`struphy.Output.process`. Parameters ---------- @@ -134,10 +134,14 @@ def __init__(self, sim: "Simulation", parallel_pproc: bool = False): else: self.derham = Derham(sim.grid, sim.derham_opts, comm=None, domain=sim.domain) self.comm = MockComm() - # get number of MPI ranks used in the simulation from meta.yml - with open(os.path.join(self.path_out, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - self.comm_size = meta["MPI processes"] + # The saved rank count describes the raw files, not the current communicator. + metadata_path = os.path.join(self.path_out, "run_metadata.json") + if os.path.isfile(metadata_path): + with open(metadata_path) as f: + self.comm_size = json.load(f)["mpi_ranks"] + else: + with open(os.path.join(self.path_out, "meta.yml")) as f: + self.comm_size = yaml.safe_load(f)["MPI processes"] self.rank = 0 self.range_ranks = range(int(self.comm_size)) @@ -147,6 +151,19 @@ def __init__(self, sim: "Simulation", parallel_pproc: bool = False): os.makedirs(self.path_pproc, exist_ok=True) self.comm.Barrier() + @classmethod + def from_output(cls, path_out: str | os.PathLike) -> "PostProcessor": + """Create a serial processor from a saved output folder. + + Reads ``config.json`` (or ``run_metadata.json`` when absent), without + executing the parameter file or allocating a simulation. The folder may + have been moved. Existing post-processing products are preserved until + :meth:`process` is called. Under MPI, call this on one rank only. + """ + from struphy.simulation.sim import Simulation + + return cls(Simulation.from_output(path_out)) + def _write_manifest(self, status, *, options=None, error=None): if self.rank != 0: return diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 64c451cb6..af276724e 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1661,8 +1661,8 @@ def to_run_metadata(self, file_path: str = None, **extra_data) -> str: non-reconstructible facts (MPI layout, live particle counts, caller-supplied timestamps, ...), serialized to a JSON string. - This is metadata for humans/logging, not a serialization meant to be fed back - into :meth:`from_dict` — use :meth:`to_dict`/:meth:`export` for that. + The configuration snapshot can also be restored by :meth:`from_output`; + run-specific facts do not restore live simulation state. Parameters ---------- @@ -1708,8 +1708,8 @@ def from_dict(cls, dct) -> "Simulation": time_opts=Time.from_dict(dct["time_opts"]), domain=domains.Cuboid.from_dict(dct["domain"]), equil=FluidEquilibrium.from_dict(dct["equil"]), - grid=grids.TensorProductGrid.from_dict(dct["grid"]), - derham_opts=DerhamOptions.from_dict(dct["derham_opts"]), + grid=grids.TensorProductGrid.from_dict(dct["grid"]) if dct["grid"] is not None else None, + derham_opts=DerhamOptions.from_dict(dct["derham_opts"]) if dct["derham_opts"] is not None else None, profiling_opts=ProfilingOptions( **{ key: value @@ -1752,7 +1752,8 @@ def convert_lists_to_tuples(obj): def from_output(cls, path_out: str) -> "Simulation": """Restore the simulation that wrote the output folder ``path_out``. - The configuration is read from the ``config.json`` written by :meth:`run`; a copied + The configuration is read from the ``config.json`` written by :meth:`run`, + falling back to ``run_metadata.json`` if absent; a copied parameter file is never executed. ``config.json`` holds the options objects and the arguments of the model (and thus its units), which is all that post-processing and plotting need, but not configuration applied to the model after construction, such as @@ -1761,9 +1762,11 @@ def from_output(cls, path_out: str) -> "Simulation": """ path_out = os.path.abspath(path_out) config_path = os.path.join(path_out, "config.json") + if not os.path.exists(config_path): + config_path = os.path.join(path_out, "run_metadata.json") if not os.path.exists(config_path): raise FileNotFoundError( - f"{config_path} does not exist; is {path_out} a Struphy output folder? Outputs of older " + f"Neither config.json nor run_metadata.json exists in {path_out}; is it a Struphy output folder? Outputs of older " "versions can get one with sim.export(os.path.join(path_out, 'config.json')) from their parameter file." ) sim = cls.from_file(config_path) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index dc0e41fd1..e205419c2 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -2,10 +2,12 @@ import os +import h5py import pytest from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time, open_output from struphy.models import Maxwell, VlasovAmpereOneSpecies +from struphy.post_processing.post_processing_tools import PostProcessor, is_processed def make_sim(tmp_path, **kwargs): @@ -73,6 +75,47 @@ def test_from_output_requires_a_configuration(tmp_path): Simulation.from_output(tmp_path) +@pytest.mark.parametrize("metadata_only", [False, True]) +def test_processor_from_moved_output(tmp_path, metadata_only): + sim = make_sim(tmp_path, grid=None, derham_opts=None, time_opts=Time(dt=0.123)) + os.makedirs(os.path.join(sim.env.path_out, "data")) + if metadata_only: + sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json"), mpi_ranks=3) + else: + sim._save_config() + with open(os.path.join(sim.env.path_out, "meta.yml"), "w") as stream: + stream.write("MPI processes: 3\n") + with h5py.File(os.path.join(sim.env.path_out, "data", "data_proc0.hdf5"), "w") as data: + data.create_dataset("time/value", data=[0.0, 0.123]) + moved = tmp_path / "moved" + os.rename(sim.env.path_out, moved) + products = moved / "post_processing" + products.mkdir() + sentinel = products / "existing.txt" + sentinel.write_text("keep until processing") + + processor = PostProcessor.from_output(moved) + + assert processor.path_out == str(moved) + assert processor.model.to_dict() == sim.model.to_dict() + assert processor.domain == sim.domain + assert processor.comm_size == 3 + assert list(processor.range_ranks) == [0, 1, 2] + assert sentinel.read_text() == "keep until processing" + assert open_output(moved).sim.time_opts.dt == 0.123 + assert processor.process(create_vtk=False) + assert is_processed(moved) + + +def test_from_output_prefers_config_over_metadata(tmp_path): + sim = make_sim(tmp_path, time_opts=Time(dt=0.123)) + os.makedirs(sim.env.path_out) + sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json")) + sim.time_opts = Time(dt=0.456) + sim._save_config() + assert Simulation.from_output(sim.env.path_out).time_opts.dt == 0.456 + + def test_deprecated_pproc_delegates_to_the_output(tmp_path, monkeypatch): sim = make_sim(tmp_path) calls = [] From edfce66c25c3422378f2671a880c4faadf1fde93 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 16:02:02 +0200 Subject: [PATCH 038/193] Removed redundant config.json --- doc/sections/userguide.rst | 6 ++--- .../post_processing/post_processing_tools.py | 2 +- src/struphy/simulation/sim.py | 20 +++++++------- src/struphy/simulation/tests/test_output.py | 26 ++++++++++++------- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index dbd796ebb..18eeceb88 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -509,7 +509,7 @@ one moved to another location: processor = PostProcessor.from_output("./runs/my_run") processor.process(physical=True) -This reads ``config.json``, falling back to ``run_metadata.json`` when needed. +This reads ``run_metadata.json``, falling back to legacy ``config.json`` when needed. The original MPI rank count comes from ``run_metadata.json`` or legacy ``meta.yml``. Under MPI, call this factory on one rank only; use ``Output.process`` for automatic rank handling. @@ -535,8 +535,8 @@ along as the coordinate ``t_seconds``. Pass ``time_units="physical"`` to In a separate process, for example a plotting script on a laptop after a cluster run, open the output folder instead. Nothing is allocated and no MPI is needed; -``out.sim`` is restored from the ``config.json`` that ``sim.run()`` writes to the folder; a -copied parameter file is never executed. ``config.json`` holds the options and the model +``out.sim`` is restored from the ``run_metadata.json`` that ``sim.run()`` writes to the folder; a +copied parameter file is never executed. The metadata holds the options and the model arguments (and thus the units), which is all that post-processing and plotting need, but not configuration applied to the model afterwards, such as backgrounds or perturbations: diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 270ab22cd..8ca8cf087 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -155,7 +155,7 @@ def __init__(self, sim: "Simulation", parallel_pproc: bool = False): def from_output(cls, path_out: str | os.PathLike) -> "PostProcessor": """Create a serial processor from a saved output folder. - Reads ``config.json`` (or ``run_metadata.json`` when absent), without + Reads ``run_metadata.json`` (or legacy ``config.json`` when absent), without executing the parameter file or allocating a simulation. The folder may have been moved. Existing post-processing products are preserved until :meth:`process` is called. Under MPI, call this on one rank only. diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index af276724e..1a06ccb67 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -633,7 +633,7 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No self._remove_existing_output_files() self._setup_folders() - self._save_config() + self._copy_parameter_file() self.Barrier() self._output = None @@ -1162,13 +1162,11 @@ def _remove_existing_output_files(self): if n < 10: # print only ten statements in case of many processes logger.info("Removed existing file " + file) - def _save_config(self): - """Save the configuration as ``config.json`` to the output folder, which is what - :meth:`from_output` reads. A parameter file is copied alongside for reference.""" + def _copy_parameter_file(self): + """Copy the parameter file to the output folder for reference.""" if self.rank != 0: return - self.export(os.path.join(self.env.path_out, "config.json")) if self.params_path is not None: try: shutil.copy2( @@ -1752,22 +1750,22 @@ def convert_lists_to_tuples(obj): def from_output(cls, path_out: str) -> "Simulation": """Restore the simulation that wrote the output folder ``path_out``. - The configuration is read from the ``config.json`` written by :meth:`run`, - falling back to ``run_metadata.json`` if absent; a copied - parameter file is never executed. ``config.json`` holds the options objects and the + The configuration is read from the ``run_metadata.json`` written by :meth:`run`, + falling back to legacy ``config.json`` if absent; a copied + parameter file is never executed. The metadata holds the options objects and the arguments of the model (and thus its units), which is all that post-processing and plotting need, but not configuration applied to the model after construction, such as markers, backgrounds, perturbations and propagator options. Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. """ path_out = os.path.abspath(path_out) - config_path = os.path.join(path_out, "config.json") + config_path = os.path.join(path_out, "run_metadata.json") if not os.path.exists(config_path): - config_path = os.path.join(path_out, "run_metadata.json") + config_path = os.path.join(path_out, "config.json") if not os.path.exists(config_path): raise FileNotFoundError( f"Neither config.json nor run_metadata.json exists in {path_out}; is it a Struphy output folder? Outputs of older " - "versions can get one with sim.export(os.path.join(path_out, 'config.json')) from their parameter file." + "versions can get one with sim.to_run_metadata(os.path.join(path_out, 'run_metadata.json')) from their parameter file." ) sim = cls.from_file(config_path) sim.env = dataclasses.replace( diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index e205419c2..2c894344b 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -1,5 +1,6 @@ """Tests for the link between a Simulation and its output.""" +import json import os import h5py @@ -33,11 +34,11 @@ def test_output_is_the_run_of_the_current_output_folder(tmp_path): assert sim.output.path_out.name == "sim_2" -def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): +def test_from_output_restores_metadata_and_follows_a_moved_folder(tmp_path): model = VlasovAmpereOneSpecies(base_units=BaseUnits(x=2.0, B=3.0, n=4.0), mass_number=4.0, with_B0=False) sim = Simulation(model=model, env=EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_1")) os.makedirs(os.path.join(sim.env.path_out, "data")) - sim._save_config() + sim._write_run_metadata() moved = tmp_path / "moved" os.rename(sim.env.path_out, moved) @@ -52,19 +53,24 @@ def test_from_output_restores_config_json_and_follows_a_moved_folder(tmp_path): assert sorted(os.listdir(tmp_path)) == ["moved"] -def test_run_writes_config_json_and_copies_the_parameter_file(tmp_path): +def test_run_writes_only_metadata_and_copies_the_parameter_file(tmp_path): params = tmp_path / "params_maxwell.py" params.write_text("# a parameter file\n") sim = make_sim(tmp_path, params_path=str(params)) os.makedirs(sim.env.path_out) - sim._save_config() - assert sorted(os.listdir(sim.env.path_out)) == ["config.json", "parameters.py"] + sim._write_run_metadata() + sim._copy_parameter_file() + assert sorted(os.listdir(sim.env.path_out)) == ["parameters.py", "run_metadata.json"] + metadata = json.loads((tmp_path / "sim_1" / "run_metadata.json").read_text()) + assert metadata["model"] == sim.model.to_dict() + assert metadata["mpi_ranks"] == sim.comm_size + assert metadata["started_at_epoch_s"] == sim.start_time def test_from_output_never_executes_the_parameter_file(tmp_path): sim = make_sim(tmp_path, time_opts=Time(dt=0.123)) os.makedirs(os.path.join(sim.env.path_out, "data")) - sim._save_config() + sim._write_run_metadata() with open(os.path.join(sim.env.path_out, "parameters.py"), "w") as stream: stream.write("raise RuntimeError('the parameter file was executed')\n") assert Simulation.from_output(sim.env.path_out).time_opts.dt == 0.123 @@ -82,7 +88,7 @@ def test_processor_from_moved_output(tmp_path, metadata_only): if metadata_only: sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json"), mpi_ranks=3) else: - sim._save_config() + sim.export(os.path.join(sim.env.path_out, "config.json")) with open(os.path.join(sim.env.path_out, "meta.yml"), "w") as stream: stream.write("MPI processes: 3\n") with h5py.File(os.path.join(sim.env.path_out, "data", "data_proc0.hdf5"), "w") as data: @@ -107,12 +113,12 @@ def test_processor_from_moved_output(tmp_path, metadata_only): assert is_processed(moved) -def test_from_output_prefers_config_over_metadata(tmp_path): +def test_from_output_prefers_metadata_over_legacy_config(tmp_path): sim = make_sim(tmp_path, time_opts=Time(dt=0.123)) os.makedirs(sim.env.path_out) - sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json")) + sim.export(os.path.join(sim.env.path_out, "config.json")) sim.time_opts = Time(dt=0.456) - sim._save_config() + sim._write_run_metadata() assert Simulation.from_output(sim.env.path_out).time_opts.dt == 0.456 From 0241851fb61847b0288f60089f0f8e6fb75d1cd6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 16:04:49 +0200 Subject: [PATCH 039/193] Updated examples --- .../bump_on/pproc_bump_on.py | 14 +++++++--- .../pproc_strong_Landau_damping.py | 14 +++++++--- .../two_stream/pproc_two_stream.py | 14 +++++++--- .../pproc_weak_Landau_damping.py | 20 +++++++++----- .../pproc_weibel_instability.py | 26 +++++++++++-------- 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 38e40b2a5..f89da5f20 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -1,9 +1,12 @@ -import params_bump_on as params +import argparse + from matplotlib import pyplot as plt +from struphy import open_output + -def main(): - run = params.sim.output +def main(path_out="sim_data"): + run = open_output(path_out) # initial velocity distribution initial = run["kinetic_ions/v1_density/f"].isel(t=0) @@ -19,4 +22,7 @@ def main(): if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="Plot a saved simulation run.") + parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + args = parser.parse_args() + main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index 0c2c67cf5..e7a4e42f4 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,8 +1,11 @@ -import params_strong_Landau_damping as params +import argparse -def main(): - run = params.sim.output +from struphy import open_output + + +def main(path_out="sim_data"): + run = open_output(path_out) # electric field energy run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() @@ -12,4 +15,7 @@ def main(): if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="Plot a saved simulation run.") + parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + args = parser.parse_args() + main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index e7e0dc3be..14cdd5978 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,8 +1,11 @@ -import params_two_stream as params +import argparse -def main(): - run = params.sim.output +from struphy import open_output + + +def main(path_out="sim_data"): + run = open_output(path_out) # table and figures of every scalar: post_processing/report/ run.save_report() @@ -22,4 +25,7 @@ def main(): if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="Plot a saved simulation run.") + parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + args = parser.parse_args() + main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index e3e3bfbc3..d8a74ec40 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -1,10 +1,12 @@ +import argparse + import cunumpy as xp -import params_weak_Landau_damping as params + +from struphy import open_output -def E_exact(t): +def E_exact(t, eps=0.001): """Analytical electric energy of weak Landau damping, t in normalized units.""" - eps = params.perturbation.amps[0] r = 0.3677 omega_r = 1.4156 omega_i = -0.1533 @@ -12,13 +14,13 @@ def E_exact(t): return (4 * eps * r * xp.exp(omega_i * t) * xp.cos(omega_r * t - phi)) ** 2 * xp.pi -def main(): - run = params.sim.output +def main(path_out="sim_data", amplitude=0.001): + run = open_output(path_out) # electric field energy against the analytical damping energy = run.scalars.electric_energy.copy() energy.attrs["label"] = "numerical" - analytical = energy.copy(data=E_exact(energy.t.values)) # t is in Struphy units + analytical = energy.copy(data=E_exact(energy.t.values, eps=amplitude)) # t is in Struphy units analytical.attrs["label"] = "analytical" energy.struphy.plot.timeseries(analytical, title="Electric energy").show() @@ -30,4 +32,8 @@ def main(): if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="Plot a saved simulation run.") + parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + parser.add_argument("--amplitude", type=float, default=0.001, help="Initial perturbation amplitude for the analytical curve") + args = parser.parse_args() + main(args.path_out, amplitude=args.amplitude) diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 8368608a0..75e4c3525 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -1,20 +1,21 @@ +import argparse + import cunumpy as xp -import params_weibel_instability as params from matplotlib import pyplot as plt +from struphy import open_output + -def main(): - run = params.sim.output.with_time_units("normalized") +def main(path_out="sim_data"): + run = open_output(path_out, time_units="normalized") time = run.time - Tend = params.time_opts.Tend - algo = params.time_opts.split_algo - ppc = params.loading_params.ppc - control_variate = params.weights_params.control_variate + Tend = run.sim.time_opts.Tend + algo = run.sim.time_opts.split_algo # ------------------ # Gauss law violation # ------------------ - if params.model.measure_gauss_law: + if run.sim.model.measure_gauss_law: gauss_error = run.scalars.gauss_error fig, ax = plt.subplots(1, figsize=(10, 6)) @@ -83,7 +84,7 @@ def field_energy(field): ax.set_yscale("log") ax.minorticks_on() - fig.suptitle(f"VlasovMaxwellOneSpecies simulation:\n {control_variate=}, {ppc=}, {algo=}") + fig.suptitle(f"VlasovMaxwellOneSpecies simulation:\n {algo=}") plt.tight_layout() plt.show() @@ -116,7 +117,7 @@ def plot_EM_state(time_step: float, n_dim=3): axs[0, 0].set_ylim(-5e-3, 5e-3) axs[1, 0].set_ylim(-5e-3, 5e-3) - fig.suptitle(f"EM-field at time step: {float(electric_field.t):.2f}, {ppc=},{control_variate=}") + fig.suptitle(f"EM-field at time step: {float(electric_field.t):.2f}") plt.show() plt.close() @@ -150,4 +151,7 @@ def current_1D(time_step: float): if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="Plot a saved simulation run.") + parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + args = parser.parse_args() + main(args.path_out) From fec081db0ed10bd439a27de968398f84d16e262e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 16:28:47 +0200 Subject: [PATCH 040/193] Output no longer stores or reconstructs a Simulation --- doc/sections/userguide.rst | 13 +- .../cyclone/pproc_cyclone.py | 4 +- .../itg_cylindre/pproc_drift_kinetic.py | 4 +- .../diocotron_instability/pproc_diocotron.py | 4 +- .../bump_on/pproc_bump_on.py | 4 +- .../pproc_strong_Landau_damping.py | 4 +- .../two_stream/pproc_two_stream.py | 4 +- .../pproc_weak_Landau_damping.py | 4 +- .../pproc_weibel_instability.py | 10 +- src/struphy/post_processing/output.py | 134 +++++++++++++----- .../post_processing/post_processing_tools.py | 70 ++++----- .../post_processing/tests/test_output.py | 118 +++++++++------ .../tests/test_output_accessors.py | 5 +- .../post_processing/tests/test_pproc.py | 11 +- src/struphy/simulation/sim.py | 2 +- src/struphy/simulation/tests/test_output.py | 16 ++- tutorials/tutorial_post_processing.ipynb | 8 +- 17 files changed, 250 insertions(+), 165 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 18eeceb88..8c7ffef85 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -523,7 +523,7 @@ and it stays available as ``sim.output``: out.scalars.total_energy # scalar time series, straight from the raw output out.fields.em_fields.e_field # evaluated FEEC field (post-processed on first access) - out.sim # the Simulation that produced the output + out.domain, out.model.units # reconstructed from saved metadata Every product is an :class:`xarray.DataArray` with named dimensions (``t``, ``component``, ``e1``, ``e2``, ``e3``, ``v1``, ...), coordinates and units. @@ -531,11 +531,12 @@ Arrays are read from disk only when accessed. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate ``t_seconds``. Pass ``time_units="physical"`` to -:func:`~struphy.open_output` to make ``t`` itself seconds. +:class:`~struphy.Output` to make ``t`` itself seconds. In a separate process, for example a plotting script on a laptop after a cluster -run, open the output folder instead. Nothing is allocated and no MPI is needed; -``out.sim`` is restored from the ``run_metadata.json`` that ``sim.run()`` writes to the folder; a +run, open the output folder instead. Nothing is allocated and no MPI is needed. +The domain, model and numerical options are restored directly from the +``run_metadata.json`` that ``sim.run()`` writes to the folder; a copied parameter file is never executed. The metadata holds the options and the model arguments (and thus the units), which is all that post-processing and plotting need, but not configuration applied to the model afterwards, such as backgrounds or perturbations: @@ -544,8 +545,8 @@ not configuration applied to the model afterwards, such as backgrounds or pertur import struphy - out = struphy.open_output("./runs/vm1s_scan_A/sim_1") - out.sim.domain, out.sim.model.units + out = struphy.Output("./runs/vm1s_scan_A/sim_1") + out.domain, out.model.units Choosing post-processing options: ``out.process()`` diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index d12a0a627..f55851dc2 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,7 +1,7 @@ import os import sys -from struphy import open_output +from struphy import Output # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" @@ -19,7 +19,7 @@ def main(path_out): - run = open_output(path_out).process(physical=True) + run = Output(path_out).process(physical=True) # growth rate of the electrostatic potential run[FIT_QUANTITY].struphy.plot.timeseries( diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 4f7062ded..6f10c3afb 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,7 +1,7 @@ import os import sys -from struphy import open_output +from struphy import Output # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" @@ -19,7 +19,7 @@ def main(path_out): - run = open_output(path_out).process(physical=True) + run = Output(path_out).process(physical=True) # growth rate of the electrostatic potential run[FIT_QUANTITY].struphy.plot.timeseries( diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 0d3a770c7..ffb2013e1 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -7,7 +7,7 @@ import os import sys -from struphy import open_output +from struphy import Output FIT_QUANTITY = "en_phi" FIT_WINDOW = (0.0, 42.0) @@ -23,7 +23,7 @@ def main(paths): - runs = [open_output(path).process(physical=True) for path in paths] + runs = [Output(path).process(physical=True) for path in paths] run = runs[0] # growth rate of the electrostatic energy, one curve per run diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index f89da5f20..a8a8da1cb 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -2,11 +2,11 @@ from matplotlib import pyplot as plt -from struphy import open_output +from struphy import Output def main(path_out="sim_data"): - run = open_output(path_out) + run = Output(path_out) # initial velocity distribution initial = run["kinetic_ions/v1_density/f"].isel(t=0) diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index e7a4e42f4..cb7068c65 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,11 +1,11 @@ import argparse -from struphy import open_output +from struphy import Output def main(path_out="sim_data"): - run = open_output(path_out) + run = Output(path_out) # electric field energy run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 14cdd5978..7d6aca069 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,11 +1,11 @@ import argparse -from struphy import open_output +from struphy import Output def main(path_out="sim_data"): - run = open_output(path_out) + run = Output(path_out) # table and figures of every scalar: post_processing/report/ run.save_report() diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index d8a74ec40..f62b5c086 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -2,7 +2,7 @@ import cunumpy as xp -from struphy import open_output +from struphy import Output def E_exact(t, eps=0.001): @@ -15,7 +15,7 @@ def E_exact(t, eps=0.001): def main(path_out="sim_data", amplitude=0.001): - run = open_output(path_out) + run = Output(path_out) # electric field energy against the analytical damping energy = run.scalars.electric_energy.copy() diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 75e4c3525..440db95e9 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -3,19 +3,19 @@ import cunumpy as xp from matplotlib import pyplot as plt -from struphy import open_output +from struphy import Output def main(path_out="sim_data"): - run = open_output(path_out, time_units="normalized") + run = Output(path_out, time_units="normalized") time = run.time - Tend = run.sim.time_opts.Tend - algo = run.sim.time_opts.split_algo + Tend = run.time_opts.Tend + algo = run.time_opts.split_algo # ------------------ # Gauss law violation # ------------------ - if run.sim.model.measure_gauss_law: + if run.model.measure_gauss_law: gauss_error = run.scalars.gauss_error fig, ax = plt.subplots(1, figsize=(10, 6)) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index db058cfa4..76059fc08 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -2,14 +2,17 @@ from __future__ import annotations +import json import logging import warnings from collections.abc import Callable, Iterator, Mapping +from functools import cached_property from pathlib import Path import h5py import numpy as np import xarray as xr +from feectools.ddm.mpi import mpi as MPI from struphy.post_processing import store from struphy.post_processing.arrays import data_array, save_scalars @@ -145,14 +148,14 @@ class Output: """The output of one Struphy simulation, loaded lazily from its output folder. Obtain it from :attr:`Simulation.output` (or the return value of :meth:`Simulation.run`) - or, in a separate process, from :func:`open_output`. Nothing is read at construction. + or construct ``Output(path_out)`` in a separate process. Nothing is read at construction. * :attr:`scalars` are read directly from the raw HDF5 output. * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` need post-processed data. When there is none, the first access processes the run with default options; call :meth:`process` beforehand to choose options. - * :attr:`sim` is the :class:`~struphy.Simulation` that produced the output: the live - object for ``sim.output``, otherwise restored from disk without allocating anything. + * :attr:`model`, :attr:`domain` and numerical options are reconstructed lazily + from saved metadata. No simulation object is created or retained. * Products plot themselves, e.g. ``out["en_phi"].struphy.plot.timeseries(fit=(0, 40))``; :attr:`plot` holds the plots that need the whole run. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. @@ -161,20 +164,20 @@ class Output: ---------- path_out: The simulation output folder, ``sim.env.path_out``. - sim: - The simulation that wrote ``path_out``, if it is at hand. + comm: + Communicator for post-processing; defaults to MPI.COMM_WORLD. time_units: ``"normalized"`` (the default) keeps Struphy time units, in which the analytic results of the models are expressed; every product then also carries seconds as the coordinate ``t_seconds``. ``"physical"`` makes ``t`` itself seconds. """ - def __init__(self, path_out, *, sim=None, time_units: str = "normalized"): + def __init__(self, path_out, *, time_units: str = "normalized", comm=None): if time_units not in {"physical", "normalized"}: raise ValueError("time_units must be 'physical' or 'normalized'") self.path_out = Path(path_out).resolve() self.time_units = time_units - self._sim = sim + self.comm = MPI.COMM_WORLD if comm is None else comm self._reset() def __repr__(self): @@ -182,7 +185,7 @@ def __repr__(self): def with_time_units(self, time_units: str) -> "Output": """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" - return type(self)(self.path_out, sim=self._sim, time_units=time_units) + return type(self)(self.path_out, time_units=time_units, comm=self.comm) def _reset(self): if getattr(self, "_tree", None) is not None: @@ -223,7 +226,7 @@ def seconds_per_time(self) -> float | None: """One Struphy time unit in seconds; None when the configuration is missing.""" if self._seconds is None: try: - self._seconds = float(self.sim.model.units.t) + self._seconds = float(self.model.units.t) except FileNotFoundError: self._seconds = False return self._seconds or None @@ -232,14 +235,79 @@ def seconds_per_time(self) -> float | None: def path_pproc(self) -> Path: return self.path_out / "post_processing" - @property - def sim(self): - """The simulation that produced this output; restored from disk when not given.""" - if self._sim is None: - from struphy.simulation.sim import Simulation + @cached_property + def metadata(self) -> dict: + """Saved run metadata, with legacy ``config.json`` supported as a fallback.""" + for name in ("run_metadata.json", "config.json"): + path = self.path_out / name + if path.is_file(): + with path.open() as stream: + return json.load(stream) + raise FileNotFoundError(f"Neither run_metadata.json nor config.json exists in {self.path_out}") + + def _restore(self, key, cls): + # Constructors expect tuples where JSON encodes sequences as lists. + def tuples(value): + if isinstance(value, dict): + return {name: tuples(item) for name, item in value.items()} + if isinstance(value, list): + return tuple(tuples(item) for item in value) + return value + + value = self.metadata[key] + return cls.from_dict(tuples(value)) if value is not None else None + + @cached_property + def model(self): + """Model reconstructed from its saved constructor arguments.""" + from struphy.models.base import StruphyModel + + return self._restore("model", StruphyModel) + + @cached_property + def domain(self): + """Computational domain of the saved run.""" + from struphy.geometry.base import Domain + + return self._restore("domain", Domain) + + @cached_property + def equil(self): + """Saved equilibrium, if present.""" + from struphy.fields_background.base import FluidEquilibrium + + return self._restore("equil", FluidEquilibrium) + + @cached_property + def grid(self): + """Saved spatial grid, if present.""" + from struphy.topology.grids import TensorProductGrid + + return self._restore("grid", TensorProductGrid) + + @cached_property + def derham_opts(self): + """Saved finite element options, if present.""" + from struphy.io.options import DerhamOptions + + return self._restore("derham_opts", DerhamOptions) + + @cached_property + def time_opts(self): + """Time-stepping options of the saved run.""" + from struphy.io.options import Time + + return self._restore("time_opts", Time) + + @cached_property + def mpi_ranks(self) -> int: + """Number of ranks that wrote the raw output (not the current communicator).""" + if "mpi_ranks" in self.metadata: + return int(self.metadata["mpi_ranks"]) + import yaml - self._sim = Simulation.from_output(self.path_out) - return self._sim + with (self.path_out / "meta.yml").open() as stream: + return int(yaml.safe_load(stream)["MPI processes"]) @property def is_processed(self) -> bool: @@ -263,7 +331,8 @@ def process( """Post-process the raw output; reuses existing products made with the same options. Call this on every MPI rank. Serial processing (the default) runs on rank 0 while - the other ranks wait; ``parallel=True`` needs the allocated simulation that ran. + the other ranks wait. Parallel processing reconstructs the field decomposition + and requires the same number of ranks as the saved run. Parameters ---------- @@ -280,14 +349,14 @@ def process( create_vtk: Also write VTK files of the fields. parallel: - Evaluate fields on all MPI ranks of the simulation's communicator. + Evaluate fields on all ranks of this output's communicator. force: Reprocess even when matching products exist. Returns ------- Output - This run, so that ``run = open_output(path).process(physical=True)`` reads naturally. + This run, so that ``run = Output(path).process(physical=True)`` reads naturally. """ from struphy.post_processing.post_processing_tools import PostProcessor @@ -300,20 +369,19 @@ def process( create_vtk=create_vtk, force=force, ) - sim = self.sim if parallel: - PostProcessor(sim, parallel_pproc=True).process(**options) + PostProcessor(self, parallel_pproc=True).process(**options) else: - if sim.rank == 0: - PostProcessor(sim).process(**options) - sim.Barrier() + if self.comm.Get_rank() == 0: + PostProcessor(self).process(**options) + self.comm.Barrier() self._reset() return self def _ensure_processed(self): if self.is_processed: return - if self.sim.comm_size > 1: + if self.comm.Get_size() > 1: raise RuntimeError(f"{self.path_out} has no post-processed data; call out.process() on all ranks first") logger.warning( "\nNo post-processed data in %s, processing with default options (call out.process(...) to choose them)", @@ -358,9 +426,11 @@ def __getattr__(self, name: str) -> ProductNamespace: products of that species, whatever kind they are; the grouped views :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` show them by kind. """ - if name.startswith("_"): + if name.startswith("_") or name == "sim": raise AttributeError(name) attribute = getattr(type(self), name, None) + if isinstance(attribute, cached_property): + attribute.func(self) if isinstance(attribute, property): attribute.fget(self) # the property raised AttributeError itself; show its own error # the raw output names the species, so an unknown name never starts post-processing @@ -460,7 +530,7 @@ def t_grid(self): @property def time_scale(self) -> float: """Factor from Struphy time units to :attr:`time_units`.""" - return float(self.sim.model.units.t) if self.time_units == "physical" else 1.0 + return float(self.model.units.t) if self.time_units == "physical" else 1.0 @property def time_unit(self) -> str: @@ -538,16 +608,16 @@ def label(self) -> str: """Short description of the numerical parameters, for figure titles.""" if self._label is None: try: - sim = self.sim + self.metadata except FileNotFoundError: # an output folder without its configuration self._label = self.path_out.name return self._label values = [] for holder, attr, name in ( - (sim.time_opts, "dt", "dt"), - (sim.time_opts, "split_algo", "algo"), - (sim.grid, "num_elements", "Nel"), - (sim.derham_opts, "degree", "p"), + (self.time_opts, "dt", "dt"), + (self.time_opts, "split_algo", "algo"), + (self.grid, "num_elements", "Nel"), + (self.derham_opts, "degree", "p"), ): value = getattr(holder, attr, None) if holder is not None else None if value is not None: diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 8ca8cf087..be0e20c4f 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -10,7 +10,6 @@ import cunumpy as xp import h5py import xarray as xr -import yaml from feectools.ddm.mpi import MockComm from feectools.ddm.mpi import mpi as MPI from pyevtk.hl import gridToVTK @@ -25,7 +24,7 @@ from struphy.utils.progress import tqdm if TYPE_CHECKING: - from struphy.simulation.sim import Simulation + from struphy.post_processing.output import Output logger = logging.getLogger("struphy") @@ -88,12 +87,11 @@ class PostProcessor: Parameters ---------- - sim : Simulation - Simulation of the run, either the one that ran or one restored with - :meth:`Simulation.from_output`. Its ``env.path_out`` locates the output. + output : Output + Output folder and lazily reconstructed configuration of the saved run. parallel_pproc : bool, optional - Whether to run post-processing in parallel using MPI. This requires an allocated - ``sim`` and a call on every rank. Default is False (serial post-processing). + Whether to run post-processing in parallel using MPI. This requires the same + number of ranks as the saved run and a call on every rank. Default is False (serial post-processing). Attributes ---------- @@ -111,39 +109,25 @@ class PostProcessor: Number of MPI ranks used to produce the output. """ - def __init__(self, sim: "Simulation", parallel_pproc: bool = False): - self.path_out = sim.env.path_out + def __init__(self, output: "Output", parallel_pproc: bool = False): + self.path_out = str(output.path_out) self.path_pproc = os.path.join(self.path_out, "post_processing") self.parallel_pproc = parallel_pproc - - # struphy objects needed for post-processing - self.domain = sim.domain - self.equil = sim.equil - self.model = sim.model - - if self.parallel_pproc: - assert sim.derham is not None, "Parallel post-processing needs an allocated simulation." - self.derham = sim.derham - self.comm = self.derham.comm - self.comm_size = self.comm.Get_size() - self.rank = self.comm.Get_rank() - self.range_ranks = range(self.rank, self.rank + 1) - else: - if sim.grid is None or sim.derham_opts is None: - self.derham = None - else: - self.derham = Derham(sim.grid, sim.derham_opts, comm=None, domain=sim.domain) - self.comm = MockComm() - # The saved rank count describes the raw files, not the current communicator. - metadata_path = os.path.join(self.path_out, "run_metadata.json") - if os.path.isfile(metadata_path): - with open(metadata_path) as f: - self.comm_size = json.load(f)["mpi_ranks"] - else: - with open(os.path.join(self.path_out, "meta.yml")) as f: - self.comm_size = yaml.safe_load(f)["MPI processes"] - self.rank = 0 - self.range_ranks = range(int(self.comm_size)) + self.domain = output.domain + self.equil = output.equil + self.model = output.model + self.comm_size = output.mpi_ranks + self.comm = output.comm if parallel_pproc else MockComm() + self.rank = self.comm.Get_rank() + if parallel_pproc and self.comm.Get_size() != self.comm_size: + raise ValueError("Parallel post-processing requires the same number of MPI ranks as the saved run.") + self.range_ranks = range(self.rank, self.rank + 1) if parallel_pproc else range(self.comm_size) + self.derham = None + if output.grid is not None and output.derham_opts is not None: + self.derham = Derham( + output.grid, output.derham_opts, + comm=self.comm if parallel_pproc else None, domain=self.domain, + ) # the directory is only cleared in process(), so that constructing a # PostProcessor to inspect a run does not destroy its post-processed data @@ -160,9 +144,9 @@ def from_output(cls, path_out: str | os.PathLike) -> "PostProcessor": have been moved. Existing post-processing products are preserved until :meth:`process` is called. Under MPI, call this on one rank only. """ - from struphy.simulation.sim import Simulation + from struphy.post_processing.output import Output - return cls(Simulation.from_output(path_out)) + return cls(Output(path_out)) def _write_manifest(self, status, *, options=None, error=None): if self.rank != 0: @@ -792,11 +776,9 @@ def _create_vtk( """ for species, vars in point_data.items(): species_path = os.path.join(path, species, "vtk" + physical * "_phy") - try: - os.mkdir(species_path) - except: + if os.path.exists(species_path): shutil.rmtree(species_path) - os.mkdir(species_path) + os.makedirs(species_path) # time loop nt = len(t_grid) - 1 diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 9b8ca5e5c..c30dfcf05 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -8,6 +8,9 @@ import pytest import xarray as xr +from struphy import BaseUnits, Time, domains +from struphy.models import Maxwell + from struphy.post_processing.output import Output, open_output from struphy.post_processing import store from struphy.post_processing.arrays import orbit_quantities @@ -54,6 +57,14 @@ def write_tree(root): file.create_group("feec/em_fields") # the raw output names the species, file.create_group("kinetic/kinetic_ions") # as a real run does file.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) + metadata = { + "model": Maxwell(base_units=BaseUnits(x=2.0)).to_dict(), + "domain": domains.Cuboid().to_dict(), + "equil": None, "grid": None, "derham_opts": None, + "time_opts": Time().to_dict(), "mpi_ranks": 1, + } + with open(os.path.join(root, "run_metadata.json"), "w") as stream: + json.dump(metadata, stream) write_manifest(root) return root @@ -69,31 +80,24 @@ def write_manifest(root, **options): json.dump(manifest, stream) -class FakeUnits: - t = 2.0 - - -class FakeModel: - units = FakeUnits() +class FakeComm: + def __init__(self, rank=0, size=1): + self.rank, self.size = rank, size + self.barriers = 0 + def Get_rank(self): + return self.rank -class FakeSim: - """Just enough of a Simulation for Output: no configuration, a single rank.""" - - time_opts = grid = derham_opts = domain = None - model = FakeModel() - rank, comm_size = 0, 1 - - def __init__(self): - self.processed = [] + def Get_size(self): + return self.size def Barrier(self): - pass + self.barriers += 1 @pytest.fixture def run(tmp_path): - return Output(write_tree(str(tmp_path)), sim=FakeSim(), time_units="normalized") + return Output(write_tree(str(tmp_path)), time_units="normalized") def test_products_are_discovered_without_loading_arrays(run): @@ -150,22 +154,31 @@ def test_open_output_needs_an_output_folder(tmp_path): assert run.path_out == tmp_path.resolve() -def test_sim_is_restored_from_disk_only_on_access(tmp_path, monkeypatch): - from struphy.simulation.sim import Simulation +def test_configuration_is_restored_lazily_without_a_simulation(tmp_path, monkeypatch): + from struphy import Simulation - restored = FakeSim() - calls = [] - monkeypatch.setattr(Simulation, "from_output", classmethod(lambda cls, path: calls.append(path) or restored)) - run = open_output(write_tree(str(tmp_path))) - assert calls == [] - assert run.sim is restored and run.sim is restored - assert calls == [tmp_path.resolve()] + root = write_tree(str(tmp_path)) + def forbidden(*args, **kwargs): + raise AssertionError("Output must not construct a Simulation") + monkeypatch.setattr(Simulation, "__init__", forbidden) + run = Output(root) + assert "metadata" not in vars(run) + assert "model" not in vars(run) + assert not hasattr(run, "sim") + assert "_sim" not in vars(run) + assert run.domain == domains.Cuboid() + assert run.model.to_dict() == run.metadata["model"] + assert run.model is run.model + assert run.time_opts == Time() + assert run.grid is run.derham_opts is run.equil is None + assert run.mpi_ranks == 1 + assert run.with_time_units("physical").model.to_dict() == run.model.to_dict() def test_products_trigger_default_processing_when_missing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = Output(root, sim=FakeSim(), time_units="normalized") + run = Output(root, time_units="normalized") calls = [] def fake_process(self, **options): @@ -184,10 +197,9 @@ def fake_process(self, **options): def test_products_refuse_implicit_processing_on_many_ranks(tmp_path): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - sim = FakeSim() - sim.comm_size = 2 + comm = FakeComm(size=2) with pytest.raises(RuntimeError, match="on all ranks"): - Output(root, sim=sim).fields + Output(root, comm=comm).fields def test_processing_options_are_part_of_the_manifest(tmp_path): @@ -212,16 +224,15 @@ def test_serial_process_runs_on_rank_zero_only(tmp_path, monkeypatch, rank): calls = [] class FakePostProcessor: - def __init__(self, sim, parallel_pproc=False): + def __init__(self, output, parallel_pproc=False): calls.append(("construct", parallel_pproc)) def process(self, **options): calls.append(("process", options)) monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) - sim = FakeSim() - sim.rank = rank - run = Output(write_tree(str(tmp_path)), sim=sim) + comm = FakeComm(rank=rank, size=2) + run = Output(write_tree(str(tmp_path)), comm=comm) assert run.process(physical=True) is run expected = [ ("construct", False), @@ -233,6 +244,7 @@ def process(self, **options): ), ] assert calls == (expected if rank == 0 else []) + assert comm.barriers == 1 def test_parallel_process_runs_on_every_rank(tmp_path, monkeypatch): @@ -241,23 +253,21 @@ def test_parallel_process_runs_on_every_rank(tmp_path, monkeypatch): calls = [] class FakePostProcessor: - def __init__(self, sim, parallel_pproc=False): + def __init__(self, output, parallel_pproc=False): calls.append(parallel_pproc) def process(self, **options): pass monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) - sim = FakeSim() - sim.rank = 3 - Output(write_tree(str(tmp_path)), sim=sim).process(parallel=True) + Output(write_tree(str(tmp_path)), comm=FakeComm(rank=3, size=4)).process(parallel=True) assert calls == [True] def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = Output(root, sim=FakeSim(), time_units="normalized") + run = Output(root, time_units="normalized") calls = [] monkeypatch.setattr(Output, "process", lambda self, **options: calls.append(options)) @@ -280,15 +290,33 @@ def test_info_lists_products_without_loading(run): def test_normalized_time_carries_seconds_as_a_coordinate(run): energy = run.scalars.en_tot assert "units" not in energy.t.attrs, "normalized time has no unit" - np.testing.assert_allclose(energy.t_seconds, energy.t * FakeUnits.t) + np.testing.assert_allclose(energy.t_seconds, energy.t * float(run.model.units.t)) assert energy.t_seconds.attrs["units"] == "s" - seconds = Output(run.path_out, sim=FakeSim(), time_units="physical").scalars.en_tot - np.testing.assert_allclose(seconds.t, energy.t * FakeUnits.t) + seconds = Output(run.path_out, time_units="physical").scalars.en_tot + np.testing.assert_allclose(seconds.t, energy.t * float(run.model.units.t)) assert "t_seconds" not in seconds.coords def test_a_failing_property_reports_its_own_error(tmp_path): - run = Output(write_tree(str(tmp_path))) # no sim, no config.json - with pytest.raises(FileNotFoundError, match="config.json"): - run.sim + run = Output(write_tree(str(tmp_path))) + (run.path_out / "run_metadata.json").unlink() + with pytest.raises(FileNotFoundError, match="run_metadata.json"): + run.domain + + +def test_parallel_processing_rejects_a_different_rank_count(tmp_path): + run = Output(write_tree(str(tmp_path)), comm=FakeComm(size=2)) + with pytest.raises(ValueError, match="same number of MPI ranks"): + run.process(parallel=True) + + +def test_saved_rank_count_does_not_block_serial_implicit_processing(tmp_path, monkeypatch): + root = write_tree(str(tmp_path)) + run = Output(root, comm=FakeComm()) + run.metadata["mpi_ranks"] = 8 + (run.path_pproc / "manifest.json").unlink() + calls = [] + monkeypatch.setattr(Output, "process", lambda self: calls.append(self.path_out)) + run._ensure_processed() + assert calls == [run.path_out] diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 055cde715..33e29095c 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -12,7 +12,7 @@ from matplotlib import pyplot as plt # noqa: E402 from struphy.post_processing.output import Output # noqa: E402 -from struphy.post_processing.tests.test_output import FakeSim, write_manifest, write_tree # noqa: E402 +from struphy.post_processing.tests.test_output import write_manifest, write_tree # noqa: E402 RATE = 2.0 @@ -25,7 +25,7 @@ def make_run(root, name="sim_1"): time = np.asarray(file["time/value"]) file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) write_manifest(path) - return Output(path, sim=FakeSim(), time_units="normalized") + return Output(path, time_units="normalized") @pytest.fixture @@ -110,7 +110,6 @@ def test_analysis_by_name(run): def test_dispersion_rejects_fields_in_seconds(run): physical = run.with_time_units("physical") - physical._sim.model = type("Model", (), {"units": type("Units", (), {"t": 2.0})()})() with pytest.raises(ValueError, match="normalized"): physical.fields.em_fields.E.struphy.analysis.dispersion() diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index 168883992..b7e8a42bb 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -21,11 +21,11 @@ def test_pproc_mpi(show_plot=False): def do_plotting(run: Output, from_parallel=False): - e_field = run.fields.em_fields.e_field_log.isel(t=0, component=0, e2=0, e3=0) - phi = run.fields.em_fields.phi_log.isel(t=0, e2=0, e3=0) + e_field = run.fields.em_fields.e_field.isel(t=0, component=0, e2=0, e3=0) + phi = run.fields.em_fields.phi.isel(t=0, e2=0, e3=0) f = run.distributions.kinetic_ions.e1_v1_density - f_binned = f.f_binned.isel(t=0) - df_binned = f.delta_f_binned.isel(t=0) + f_binned = f.f.isel(t=0) + df_binned = f.delta_f.isel(t=0) if show_plot: extra = " (from parallel pproc)" if from_parallel else "" @@ -46,7 +46,8 @@ def do_plotting(run: Output, from_parallel=False): sim: Simulation = test_mod.test_weak_Landau(do_plot=False, exit_before_run=True) - run = sim.run(one_time_step=True) + sim.run(one_time_step=True) + run = Output(sim.env.path_out) # serial pproc run.process(create_vtk=True) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 1a06ccb67..f841bd3f4 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -892,7 +892,7 @@ def output(self) -> Output: post-processed on first access, or explicitly with ``sim.output.process(...)``. """ if self._output is None or self._output.path_out != Path(self.env.path_out).resolve(): - self._output = Output(self.env.path_out, sim=self) + self._output = Output(self.env.path_out, comm=self.comm) return self._output # ------------------------------------------------------------------ diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 2c894344b..e4978823d 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -2,11 +2,12 @@ import json import os +from pathlib import Path import h5py import pytest -from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time, open_output +from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time from struphy.models import Maxwell, VlasovAmpereOneSpecies from struphy.post_processing.post_processing_tools import PostProcessor, is_processed @@ -26,7 +27,9 @@ def test_output_is_the_run_of_the_current_output_folder(tmp_path): sim = make_sim(tmp_path) run = sim.output assert isinstance(run, Output) - assert run.sim is sim + assert run.path_out == Path(sim.env.path_out).resolve() + assert not hasattr(run, "sim") + assert "_sim" not in vars(run) assert sim.output is run sim.env = EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_2") @@ -42,14 +45,15 @@ def test_from_output_restores_metadata_and_follows_a_moved_folder(tmp_path): moved = tmp_path / "moved" os.rename(sim.env.path_out, moved) - restored = open_output(moved).sim + restored = Output(moved) assert restored.model.to_dict() == model.to_dict() assert restored.model.params["mass_number"] == 4.0 assert float(restored.model.units.t) == float(model.units.t) assert restored.domain == sim.domain - assert restored.env.path_out == str(moved) - assert restored.derham is None + assert restored.path_out == moved.resolve() + assert restored.grid == sim.grid + assert restored.derham_opts == sim.derham_opts assert sorted(os.listdir(tmp_path)) == ["moved"] @@ -108,7 +112,7 @@ def test_processor_from_moved_output(tmp_path, metadata_only): assert processor.comm_size == 3 assert list(processor.range_ranks) == [0, 1, 2] assert sentinel.read_text() == "keep until processing" - assert open_output(moved).sim.time_opts.dt == 0.123 + assert Output(moved).time_opts.dt == 0.123 assert processor.process(create_vtk=False) assert is_processed(moved) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index f30d802da..f2091a430 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -10,7 +10,7 @@ "\n", "This tutorial introduces the standardized post-processing interface. We run a small Vlasov–Ampère example, get its output as an autocomplete-friendly `Output`, and make the plots most commonly used to inspect a simulation.\n", "\n", - "For a production run you can skip the simulation setup and open its output folder with `struphy.open_output(\"path/to/sim\")` instead." + "For a production run you can skip the simulation setup and open its output folder with `struphy.Output(\"path/to/sim\")` instead." ] }, { @@ -139,7 +139,7 @@ "\n", "To choose options, call `out.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", "\n", - "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `struphy.open_output(path, time_units=\"physical\")` makes `t` itself seconds. Arrays are loaded only when accessed. The simulation that produced them is `out.sim`." + "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `struphy.Output(path, time_units=\"physical\")` makes `t` itself seconds. Arrays are loaded only when accessed. The saved configuration is available through `out.domain`, `out.model`, and `out.time_opts`; no simulation object is created." ] }, { @@ -694,8 +694,8 @@ "```python\n", "import struphy\n", "\n", - "out = struphy.open_output(\"/path/to/sim_1\").process(physical=True)\n", - "out.sim.domain, out.sim.model.units # the simulation, restored from disk without allocating\n", + "out = struphy.Output(\"/path/to/sim_1\").process(physical=True)\n", + "out.domain, out.model.units # reconstructed directly from saved metadata\n", "```\n", "\n", "Use `out.scalars`, `out.fields`, `out.distributions`, `out.orbits`, and `out.densities`. Attribute access is the normal interactive API; the corresponding `*_catalog` mappings are intended for generic loops and tooling." From dc4c4702f3f7151432d0ee0b459c5d44ae436f7c Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 16:50:03 +0200 Subject: [PATCH 041/193] Set default output path to Path(__file__) --- .../cyclone/params_cyclone.py | 4 +++- .../cyclone/pproc_cyclone.py | 9 +++++---- .../itg_cylindre/params_drift_kinetic.py | 4 +++- .../itg_cylindre/pproc_drift_kinetic.py | 9 +++++---- .../diocotron_instability/params_diocotron.py | 4 +++- .../diocotron_instability/pproc_diocotron.py | 9 +++++---- .../VlasovAmpereOneSpecies/bump_on/params_bump_on.py | 4 +++- examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py | 7 +++++-- .../params_strong_Landau_damping.py | 4 +++- .../strong_Landau_damping/pproc_strong_Landau_damping.py | 7 +++++-- .../two_stream/params_two_stream.py | 4 +++- .../two_stream/pproc_two_stream.py | 7 +++++-- .../weak_Landau_damping/params_weak_Landau_damping.py | 4 +++- .../weak_Landau_damping/pproc_weak_Landau_damping.py | 7 +++++-- .../weibel_instability/params_weibel_instability.py | 4 +++- .../weibel_instability/pproc_weibel_instability.py | 7 +++++-- 16 files changed, 64 insertions(+), 30 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py index 9d4d681d1..1a55019e1 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py @@ -26,6 +26,8 @@ from struphy import set_logging_level set_logging_level(logging.INFO) +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -76,7 +78,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_1", restart=False) +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_1", restart=False) # Time stepping time_opts = Time(dt=0.001, Tend=0.01, split_algo="LieTrotter") diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index f55851dc2..0a7264b1a 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,8 +1,10 @@ -import os import sys +from pathlib import Path from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" + # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" FIT_WINDOW = (0.0, None) @@ -18,7 +20,7 @@ ] -def main(path_out): +def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).process(physical=True) # growth rate of the electrostatic potential @@ -39,5 +41,4 @@ def main(path_out): if __name__ == "__main__": - sim_name = sys.argv[1] if len(sys.argv) > 1 else "sim_1" - main(os.path.join(os.getcwd(), sim_name)) + main(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_OUTPUT) diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py index 90d1595a3..102fa7cd6 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py @@ -26,6 +26,8 @@ from struphy.propagators import implicit_diffusion +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -78,7 +80,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_1", restart=False) +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_1", restart=False) # Time stepping time_opts = Time(dt=5.0, Tend=500.0, split_algo="LieTrotter") diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 6f10c3afb..6ca770664 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,8 +1,10 @@ -import os import sys +from pathlib import Path from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" + # quantity whose exponential growth rate is fitted FIT_QUANTITY = "phi_integral" FIT_WINDOW = (0.0, None) @@ -18,7 +20,7 @@ ] -def main(path_out): +def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).process(physical=True) # growth rate of the electrostatic potential @@ -39,5 +41,4 @@ def main(path_out): if __name__ == "__main__": - sim_name = sys.argv[1] if len(sys.argv) > 1 else "sim_1" - main(os.path.join(os.getcwd(), sim_name)) + main(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_OUTPUT) diff --git a/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py index 7fa472050..95d1b2010 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py @@ -32,6 +32,8 @@ import cunumpy as xp # For particles: +from pathlib import Path + from struphy import ( BaseUnits, BinningPlot, @@ -74,7 +76,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_1", restart=False) +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_1", restart=False) # Time stepping time_opts = Time(dt=0.01, Tend=51.0, split_algo="LieTrotter") diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index ffb2013e1..55f30a478 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -4,11 +4,13 @@ more than one folder only the growth-rate comparison is shown. """ -import os import sys +from pathlib import Path from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" + FIT_QUANTITY = "en_phi" FIT_WINDOW = (0.0, 42.0) @@ -22,7 +24,7 @@ ] -def main(paths): +def main(paths=(DEFAULT_OUTPUT,)): runs = [Output(path).process(physical=True) for path in paths] run = runs[0] @@ -50,5 +52,4 @@ def main(paths): if __name__ == "__main__": - sim_names = sys.argv[1:] or ["sim_1"] - main([os.path.join(os.getcwd(), name) for name in sim_names]) + main(sys.argv[1:] or [DEFAULT_OUTPUT]) diff --git a/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py index 3143ccbac..0c38059f5 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py @@ -17,6 +17,8 @@ # Import Struphy API # ------------------ +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -64,7 +66,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_data") +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping time_opts = Time(dt = 0.1, Tend = 60.0, split_algo = "LieTrotter") diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index a8a8da1cb..0985ddddb 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -1,11 +1,14 @@ import argparse +from pathlib import Path from matplotlib import pyplot as plt from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" -def main(path_out="sim_data"): + +def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # initial velocity distribution @@ -23,6 +26,6 @@ def main(path_out="sim_data"): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") args = parser.parse_args() main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py index d2fd6f8e2..6d96487f7 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py @@ -17,6 +17,8 @@ # Import Struphy API # ------------------ +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -64,7 +66,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_data") +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping time_opts = Time(dt = 0.05, Tend = 75.0, split_algo = "LieTrotter") diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index cb7068c65..ace216f62 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,10 +1,13 @@ import argparse +from pathlib import Path from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" -def main(path_out="sim_data"): + +def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # electric field energy @@ -16,6 +19,6 @@ def main(path_out="sim_data"): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") args = parser.parse_args() main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py index a9a6a3cf3..53e71d77c 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py @@ -17,6 +17,8 @@ # Import Struphy API # ------------------ +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -64,7 +66,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_data") +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping time_opts = Time(dt = 0.1, Tend = 50.0, split_algo = "LieTrotter") diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 7d6aca069..533baa8d1 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,10 +1,13 @@ import argparse +from pathlib import Path from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" -def main(path_out="sim_data"): + +def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # table and figures of every scalar: post_processing/report/ @@ -26,6 +29,6 @@ def main(path_out="sim_data"): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") args = parser.parse_args() main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py index b06020d0f..afab3db16 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py @@ -16,6 +16,8 @@ # Import Struphy API # ------------------ +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -63,7 +65,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_data") +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping time_opts = Time(dt = 0.05, Tend = 20.0, split_algo = "LieTrotter") diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index f62b5c086..2ce167598 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -1,9 +1,12 @@ import argparse +from pathlib import Path import cunumpy as xp from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" + def E_exact(t, eps=0.001): """Analytical electric energy of weak Landau damping, t in normalized units.""" @@ -14,7 +17,7 @@ def E_exact(t, eps=0.001): return (4 * eps * r * xp.exp(omega_i * t) * xp.cos(omega_r * t - phi)) ** 2 * xp.pi -def main(path_out="sim_data", amplitude=0.001): +def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): run = Output(path_out) # electric field energy against the analytical damping @@ -33,7 +36,7 @@ def main(path_out="sim_data", amplitude=0.001): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") parser.add_argument("--amplitude", type=float, default=0.001, help="Initial perturbation amplitude for the analytical curve") args = parser.parse_args() main(args.path_out, amplitude=args.amplitude) diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py index da8a86a27..6524c26e3 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py @@ -18,6 +18,8 @@ # Import Struphy API # ------------------ +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, @@ -83,7 +85,7 @@ # -------------------------- # Environment options -env = EnvironmentOptions(sim_folder="sim_data") +env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping time_opts = Time(dt = 0.05, Tend = 400, split_algo = "LieTrotter") diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 440db95e9..06750a364 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -1,12 +1,15 @@ import argparse +from pathlib import Path import cunumpy as xp from matplotlib import pyplot as plt from struphy import Output +DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" -def main(path_out="sim_data"): + +def main(path_out=DEFAULT_OUTPUT): run = Output(path_out, time_units="normalized") time = run.time Tend = run.time_opts.Tend @@ -152,6 +155,6 @@ def current_1D(time_step: float): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default="sim_data", help="Simulation output folder") + parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") args = parser.parse_args() main(args.path_out) From 822d929097c8d6fbba363917ef97a8873e317b48 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 16:50:56 +0200 Subject: [PATCH 042/193] formatting --- .../cyclone/params_cyclone.py | 34 +++++++------- .../cyclone/pproc_cyclone.py | 4 +- .../itg_cylindre/params_drift_kinetic.py | 44 ++++++++++--------- .../itg_cylindre/pproc_drift_kinetic.py | 4 +- .../diocotron_instability/params_diocotron.py | 4 +- .../diocotron_instability/pproc_diocotron.py | 8 +++- .../bump_on/params_bump_on.py | 23 +++++----- .../bump_on/pproc_bump_on.py | 17 +++++-- .../params_strong_Landau_damping.py | 23 +++++----- .../pproc_strong_Landau_damping.py | 12 +++-- .../two_stream/params_two_stream.py | 23 +++++----- .../two_stream/pproc_two_stream.py | 12 +++-- .../params_weak_Landau_damping.py | 23 +++++----- .../pproc_weak_Landau_damping.py | 18 ++++++-- .../params_weibel_instability.py | 26 +++++------ .../pproc_weibel_instability.py | 28 +++++++++--- 16 files changed, 176 insertions(+), 127 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py index 1a55019e1..b31813ae3 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py @@ -23,45 +23,44 @@ import logging + from struphy import set_logging_level + set_logging_level(logging.INFO) from pathlib import Path +import cunumpy as xp + +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, ) - -import cunumpy as xp +from struphy.linear_algebra.solver import SolverParameters +from struphy.models import DriftKineticElectrostaticAdiabatic +from struphy.pic.accumulation.filter import FilterParameters # --------------------- # Instance of the model # --------------------- -from struphy.linear_algebra.solver import SolverParameters -from struphy.models import DriftKineticElectrostaticAdiabatic -from struphy.pic.accumulation.filter import FilterParameters base_units = BaseUnits(kBT=0.1916) # provides the correct value for epsilon = 1.4142e-3 = 0.36/(180*sqrt(2)) from the paper model = DriftKineticElectrostaticAdiabatic( @@ -231,6 +230,7 @@ def pert_func(*etas): #background.plot_density_profile("e1", "v2", domain=domain, use_mu=True, equil=equil) from struphy.initial.base import GenericPerturbation + perturbation = GenericPerturbation(pert_func, given_in_basis="0") init = maxwellians.GyroMaxwellian2D(n=(n_init, perturbation), vth_para=(vth_init, None), vth_perp=(vth_init, None),)# B0=equil.absB0) model.kinetic_ions.var.add_initial_condition(init) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 0a7264b1a..3c48a0553 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -35,7 +35,9 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection).show() + run[name].struphy.plot.viewer( + x="e1", y="e2", coords="physical", plane=plane, **selection + ).show() run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py index 102fa7cd6..e0834ac2d 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py @@ -15,49 +15,51 @@ """ import logging -from struphy import set_logging_level -set_logging_level(logging.INFO) - -import cunumpy as xp -# ------------------ -# Import Struphy API -# ------------------ +from struphy import set_logging_level -from struphy.propagators import implicit_diffusion +set_logging_level(logging.INFO) from pathlib import Path +import cunumpy as xp + +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, + maxwellians, perturbations, ) +from struphy.models import DriftKineticElectrostaticAdiabatic +from struphy.propagators import implicit_diffusion + +# ------------------ +# Import Struphy API +# ------------------ + + + -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - maxwellians, - SortingParameters, - SavingParameters, -) # --------------------- # Instance of the model # --------------------- -from struphy.models import DriftKineticElectrostaticAdiabatic restart = False @@ -102,6 +104,7 @@ # Simulation object from feectools.ddm.mpi import mpi as MPI + rank = MPI.COMM_WORLD.Get_rank() sim = Simulation( model=model, @@ -184,6 +187,7 @@ def n0(r): return C_n0 * xp.exp(-kappa_n0*delta_r_n0*xp.tanh((r-rp)/delta_r_n0)) from struphy.initial.base import GenericPerturbation + def n_init(*etas): if len(etas)==1: eta1=etas[0][:,0] diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 6ca770664..37ae22417 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -35,7 +35,9 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection).show() + run[name].struphy.plot.viewer( + x="e1", y="e2", coords="physical", plane=plane, **selection + ).show() run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() diff --git a/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py index 95d1b2010..f0c3eac8a 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py @@ -29,11 +29,11 @@ set_logging_level(logging.INFO) -import cunumpy as xp - # For particles: from pathlib import Path +import cunumpy as xp + from struphy import ( BaseUnits, BinningPlot, diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 55f30a478..3fa20a95b 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -37,7 +37,9 @@ def main(paths=(DEFAULT_OUTPUT,)): ).show() for each, result in zip(runs, plot.fit_results): - print(f"{each.path_out.name}: growth rate = {None if result is None else result.rate}") + print( + f"{each.path_out.name}: growth rate = {None if result is None else result.rate}" + ) if len(runs) > 1: return @@ -46,7 +48,9 @@ def main(paths=(DEFAULT_OUTPUT,)): run.plot.equilibrium() for name in SWEEPS: - run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() + run[name].struphy.plot.viewer( + x="e1", y="e2", coords="physical", plane="XY" + ).show() run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() diff --git a/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py index 0c38059f5..5f7b690cf 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py @@ -19,36 +19,33 @@ from pathlib import Path +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, ) +from struphy.models import VlasovAmpereOneSpecies # --------------------- # Instance of the model # --------------------- -from struphy.models import VlasovAmpereOneSpecies # Units base_units = BaseUnits() diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 0985ddddb..73ffa1d7e 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -14,18 +14,29 @@ def main(path_out=DEFAULT_OUTPUT): # initial velocity distribution initial = run["kinetic_ions/v1_density/f"].isel(t=0) ax = initial.plot()[0].axes - ax.set(xlabel="velocity $v$", ylabel="distribution $f(v)$", title="Initial velocity distribution") + ax.set( + xlabel="velocity $v$", + ylabel="distribution $f(v)$", + title="Initial velocity distribution", + ) plt.show() # electric field energy run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + run.kinetic_ions.e1_v1_density.f.struphy.plot.panels( + x="e1", y="v1", nrows=3, ncols=4, title="full-$f$" + ).show() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") + parser.add_argument( + "path_out", + nargs="?", + default=DEFAULT_OUTPUT, + help="Simulation output folder (default: sim_data beside this script)", + ) args = parser.parse_args() main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py index 6d96487f7..b88e7ed0b 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py @@ -19,36 +19,33 @@ from pathlib import Path +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, ) +from struphy.models import VlasovAmpereOneSpecies # --------------------- # Instance of the model # --------------------- -from struphy.models import VlasovAmpereOneSpecies # Units base_units = BaseUnits() diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index ace216f62..316905b53 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,7 +1,6 @@ import argparse from pathlib import Path - from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" @@ -14,11 +13,18 @@ def main(path_out=DEFAULT_OUTPUT): run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + run.kinetic_ions.e1_v1_density.f.struphy.plot.panels( + x="e1", y="v1", nrows=3, ncols=4, title="full-$f$" + ).show() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") + parser.add_argument( + "path_out", + nargs="?", + default=DEFAULT_OUTPUT, + help="Simulation output folder (default: sim_data beside this script)", + ) args = parser.parse_args() main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py index 53e71d77c..d76d35441 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py @@ -19,36 +19,33 @@ from pathlib import Path +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, ) +from struphy.models import VlasovAmpereOneSpecies # --------------------- # Instance of the model # --------------------- -from struphy.models import VlasovAmpereOneSpecies # Units base_units = BaseUnits() diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 533baa8d1..b928e1420 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,7 +1,6 @@ import argparse from pathlib import Path - from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" @@ -15,7 +14,9 @@ def main(path_out=DEFAULT_OUTPUT): # electric field growth against the analytical rate (0.2845 in units of m/c) energy = run.scalars.electric_energy - analytical = energy.copy(data=10 ** (0.2845 * energy.t - 5.3)) # t is in Struphy units + analytical = energy.copy( + data=10 ** (0.2845 * energy.t - 5.3) + ) # t is in Struphy units analytical.attrs["label"] = "analytical" energy.struphy.plot.timeseries(analytical, title="Electric energy").show() @@ -29,6 +30,11 @@ def main(path_out=DEFAULT_OUTPUT): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") + parser.add_argument( + "path_out", + nargs="?", + default=DEFAULT_OUTPUT, + help="Simulation output folder (default: sim_data beside this script)", + ) args = parser.parse_args() main(args.path_out) diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py index afab3db16..9adbaf169 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py @@ -18,36 +18,33 @@ from pathlib import Path +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, ) +from struphy.models import VlasovAmpereOneSpecies # --------------------- # Instance of the model # --------------------- -from struphy.models import VlasovAmpereOneSpecies # Units base_units = BaseUnits() diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 2ce167598..50a8e173c 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -23,7 +23,9 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): # electric field energy against the analytical damping energy = run.scalars.electric_energy.copy() energy.attrs["label"] = "numerical" - analytical = energy.copy(data=E_exact(energy.t.values, eps=amplitude)) # t is in Struphy units + analytical = energy.copy( + data=E_exact(energy.t.values, eps=amplitude) + ) # t is in Struphy units analytical.attrs["label"] = "analytical" energy.struphy.plot.timeseries(analytical, title="Electric energy").show() @@ -36,7 +38,17 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") - parser.add_argument("--amplitude", type=float, default=0.001, help="Initial perturbation amplitude for the analytical curve") + parser.add_argument( + "path_out", + nargs="?", + default=DEFAULT_OUTPUT, + help="Simulation output folder (default: sim_data beside this script)", + ) + parser.add_argument( + "--amplitude", + type=float, + default=0.001, + help="Initial perturbation amplitude for the analytical curve", + ) args = parser.parse_args() main(args.path_out, amplitude=args.amplitude) diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py index 6524c26e3..591353626 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py @@ -18,35 +18,32 @@ # Import Struphy API # ------------------ +import logging from pathlib import Path +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, + set_logging_level, ) -import logging -from struphy import set_logging_level set_logging_level(logging.WARNING) # --------------------- @@ -69,6 +66,7 @@ # --------------------- import cunumpy as xp + k = 1.25 B_pert_amp = -1e-4 diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 06750a364..949b0cc2d 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -42,7 +42,9 @@ def main(path_out=DEFAULT_OUTPUT): def field_energy(field): """Energy of each component over space, as array of shape (component, t).""" - return (field**2).sum(spatial).transpose("component", "t").values * unit_volume / 2 + return ( + (field**2).sum(spatial).transpose("component", "t").values * unit_volume / 2 + ) electric_energy = field_energy(e_field) magnetic_energy = field_energy(b_field) @@ -95,9 +97,14 @@ def field_energy(field): # Binning distribution evolution # ------------------ distributions = run.distributions.kinetic_ions - for bin_name, x, y in (("e1_v1_density", "e1", "v1"), ("v1_v2_density", "v1", "v2")): + for bin_name, x, y in ( + ("e1_v1_density", "e1", "v1"), + ("v1_v2_density", "v1", "v2"), + ): for quantity in ("f", "delta_f"): - getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels(x=x, y=y, nrows=5, ncols=4).show() + getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels( + x=x, y=y, nrows=5, ncols=4 + ).show() # ------------------ # EM field at selected times @@ -106,7 +113,9 @@ def plot_EM_state(time_step: float, n_dim=3): electric_field = e_field.sel(t=time_step, method="nearest").isel(e2=0, e3=0) magnetic_field = b_field.sel(t=time_step, method="nearest").isel(e2=0, e3=0) - fig, axs = plt.subplots(nrows=2, ncols=3, figsize=(8, 6), sharex=True, sharey=True) + fig, axs = plt.subplots( + nrows=2, ncols=3, figsize=(8, 6), sharex=True, sharey=True + ) for i in range(n_dim): axs[0, i].plot(electric_field.e1, electric_field.isel(component=i)) axs[0, i].set_title(rf"$E_{i + 1}$") @@ -131,7 +140,9 @@ def plot_EM_state(time_step: float, n_dim=3): # Current density evolution # ------------------ def current_1D(time_step: float): - fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True) + fig, ax = plt.subplots( + nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True + ) for i in range(3): for j in range(3): current = getattr(distributions, f"e{i + 1}_current_{j + 1}").f @@ -155,6 +166,11 @@ def current_1D(time_step: float): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") - parser.add_argument("path_out", nargs="?", default=DEFAULT_OUTPUT, help="Simulation output folder (default: sim_data beside this script)") + parser.add_argument( + "path_out", + nargs="?", + default=DEFAULT_OUTPUT, + help="Simulation output folder (default: sim_data beside this script)", + ) args = parser.parse_args() main(args.path_out) From 8347c5c602c241c2d0d728cd7b5bbab5c4018f55 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 17:45:08 +0200 Subject: [PATCH 043/193] Update docs and skill --- .claude/skills/setup-simulation/SKILL.md | 47 +++++++---- doc/sections/quickstart.rst | 101 +++++++++++++++-------- 2 files changed, 101 insertions(+), 47 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 298915355..6ff3cd6a2 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -1,14 +1,15 @@ --- name: setup-simulation -description: Use when creating or editing a Struphy simulation parameter file (params_*.py) - choosing a model, configuring domain/grid/time-stepping, species/backgrounds/perturbations and propagator options, then running the simulation and post-processing results. Triggers on requests like "set up a simulation", "create a params file for ", "configure a run of ", or "how do I run struphy". +description: Use when creating or editing a Struphy simulation parameter file (params_*.py) - choosing a model, configuring domain/grid/time-stepping, species/backgrounds/perturbations and propagator options, then running the simulation and post-processing results. Triggers on requests like "set up a simulation", "create a params file for a model", "configure a model run", or "how do I run struphy". --- # Setting up a Struphy simulation Struphy simulations are configured as plain Python scripts (`params_.py`) that -build a `Simulation` object from the Struphy API, then call `sim.run()`. There is no -YAML/JSON config — the params file _is_ the config, so it can use real Python -(loops, conditionals, computed values) to derive parameters. +build a `Simulation` object from the Struphy API, then call `sim.run()`. The params +file can use real Python (loops, conditionals, computed values) to derive parameters. +Each run writes `run_metadata.json`, containing a configuration snapshot and runtime +facts such as the MPI rank count. Post-processing reads this saved metadata directly. ## Workflow @@ -50,6 +51,8 @@ Based on `examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py` and description = """...""" # 2. Imports from the top-level struphy API +from pathlib import Path + from struphy import ( BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Simulation, Time, domains, equils, grids, perturbations, @@ -63,11 +66,14 @@ from struphy.models import # 3. Model instance (constructor kwargs are model-specific, e.g. alpha/epsilon/with_B0) model = (...) -model..save_data = True # opt in/out of saving each variable +model...save_data = True # opt in/out of saving each variable # 4. Simulation-level config (all optional, sensible defaults exist) base_units = BaseUnits() # x, B, n, kBT -> derived units -env = EnvironmentOptions(sim_folder="sim_data") # output folder, restart, save_step, ... +env = EnvironmentOptions( + out_folders=str(Path(__file__).resolve().parent), + sim_folder="sim_data", +) # output beside the script, independent of the working directory time_opts = Time(dt=0.1, Tend=50.0, split_algo="LieTrotter") domain = domains.Cuboid(r1=31.42) # see src/struphy/geometry/domains.py equil = None # or e.g. equils.HomogenSlab() @@ -108,8 +114,7 @@ perturbation = perturbations.ModesCos(amps=(0.001,), ls=(1,)) init = maxwellians.Maxwellian3D(n=(0.5, perturbation), u1=(3.0, None)) model..var.add_initial_condition(init) -# 9. Run — guard with __main__ so pproc scripts can `import params_ as params` -# without triggering a run. +# 9. Run only when the parameter script is executed directly. if __name__ == "__main__": sim.run() ``` @@ -129,18 +134,32 @@ Key building blocks and where to look them up: ## Post-processing pattern ```python -import params_ as params # importing does NOT re-run the sim (guarded above) +from pathlib import Path +from struphy import Output -out = params.sim.output # or, from anywhere: struphy.open_output() +path_out = Path(__file__).resolve().parent / "sim_data" +out = Output(path_out) out.process(physical=True) # optional; products are otherwise processed with defaults on first access out.scalars. # xarray time series, no post-processing needed out.fields.. # dims (t, [component,] e1, e2, e3) out.distributions...f # dims (t, ) -out.orbits. # dims (t, marker, attribute) -out.sim.model.units # the Simulation, restored without allocating +out.orbits. # dims (t, marker, quantity) +out.model.units # model reconstructed from metadata +out.domain, out.grid, out.time_opts, out.derham_opts # reconstructed lazily with from_dict() ``` +Use the same folder name as the parameter script (`sim_data` or `sim_1` in the +examples). Keep a command-line path override when useful; only the default is +relative to `__file__`. In notebooks, where `__file__` is unavailable, choose an +explicit output path. + +`Output` does not retain or construct a `Simulation`; there is no `out.sim`. Do not +import the parameter module for post-processing. Metadata comes from +`run_metadata.json`, with legacy `config.json` supported only as a fallback. +`sim.pproc()` remains supported for existing callers. Under MPI, call +`out.process()` on every rank; `parallel=True` requires the saved run's rank count. + Products sit under their species and plot themselves, no imports needed: ```python @@ -160,5 +179,5 @@ See `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a compl - For kinetic species, a background is required even if only using perturbations; perturbations attach to distribution moments (via `maxwellians.*`), not directly to the species variable like field perturbations do. -- `sim.run()` must stay behind `if __name__ == "__main__":` so the params file can be - safely imported for post-processing. +- Keep `sim.run()` behind `if __name__ == "__main__":` to avoid starting a run on + import. Post-processing scripts should open the saved output folder directly. diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 29a593b50..fa1765770 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -13,6 +13,10 @@ Solve Poisson In A Few Steps ---------------------------- Make sure that Struphy is installed and compiled (see :ref:`install_modes`). +Save the code below as ``params_poisson.py``. Output is written beside that script, +regardless of the directory from which you launch it. In a notebook, replace +``Path(__file__).resolve().parent`` with an explicit directory such as ``Path.cwd()`` +and omit ``params_path=__file__`` from the simulation constructor. We search for a potential :math:`\phi(x)` satisfying the Poisson equation @@ -26,7 +30,9 @@ for given source term :math:`\rho(x)` on a periodic 1D domain. .. code-block:: python - from struphy import Simulation, domains, grids, perturbations + from pathlib import Path + + from struphy import EnvironmentOptions, Output, Simulation, domains, grids, perturbations from struphy.models import Poisson 2. Create the :class:`~struphy.models.poisson.Poisson` model. @@ -61,15 +67,21 @@ For periodic boundary conditions we will stabilize via ``options``. model.em_fields.source.add_perturbation(fun) -5. Build domain and grid, then instantiate a simulation. +5. Set the output folder, build domain and grid, then instantiate a simulation. .. code-block:: python + script_dir = Path(__file__).resolve().parent + path_out = script_dir / "sim_data" + env = EnvironmentOptions(out_folders=str(script_dir), sim_folder=path_out.name) + domain = domains.Cuboid(l1=0.0, r1=Lx) grid = grids.TensorProductGrid(num_elements=(64, 1, 1)) sim = Simulation( model=model, + params_path=__file__, + env=env, domain=domain, grid=grid, ) @@ -78,16 +90,22 @@ For periodic boundary conditions we will stabilize via ``options``. .. code-block:: python - out = sim.run(one_time_step=True) + sim.run(one_time_step=True) -7. Get the output. Fields are post-processed when first accessed and come as labeled - :class:`xarray.DataArray` objects. +7. Open the output folder directly. Fields are post-processed when first accessed + and come as labeled :class:`xarray.DataArray` objects. .. code-block:: python + out = Output(path_out) phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) -8. Compare to the exact solution, and save the figure. +``Output`` reconstructs the model, domain and numerical options lazily from +``run_metadata.json``, using their ``from_dict()`` methods. Access them as +``out.model``, ``out.domain`` or ``out.time_opts``; there is no ``out.sim``. A separate +post-processing script can use the same path without importing the parameter file. + +8. Compare to the exact solution, and save the figure in the output folder. .. code-block:: python @@ -107,7 +125,7 @@ For periodic boundary conditions we will stabilize via ``options``. plt.legend() plt.grid(alpha=0.3) plt.tight_layout() - plt.savefig("quickstart_poisson_phi.png", dpi=150) + plt.savefig(path_out / "quickstart_poisson_phi.png", dpi=150) plt.show() print(f"max error = {err_max:.3e}") @@ -116,14 +134,16 @@ For periodic boundary conditions we will stabilize via ``options``. :figwidth: 85% :alt: Poisson quickstart comparison of exact and numerical solution - Exact (dashed) and Struphy (markers) solutions from Step 6. + Exact (dashed) and Struphy (markers) solutions from Step 8. -Full copy-paste script: +Full script (save as ``params_poisson.py`` and run with ``python params_poisson.py``): .. code-block:: python + from pathlib import Path + import numpy as np - from struphy import Simulation, domains, grids, perturbations + from struphy import EnvironmentOptions, Output, Simulation, domains, grids, perturbations from struphy.models import Poisson model = Poisson() @@ -143,30 +163,39 @@ Full copy-paste script: model.em_fields.source.add_perturbation(fun) + script_dir = Path(__file__).resolve().parent + path_out = script_dir / "sim_data" + env = EnvironmentOptions(out_folders=str(script_dir), sim_folder=path_out.name) + domain = domains.Cuboid(l1=0.0, r1=Lx) grid = grids.TensorProductGrid(num_elements=(64, 1, 1)) - sim = Simulation(model=model, domain=domain, grid=grid) - out = sim.run(one_time_step=True) - - phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) - x = phi.X.values - phi_num = phi.values - phi_exact = np.cos(k * x) - - import matplotlib.pyplot as plt + sim = Simulation(model=model, params_path=__file__, env=env, domain=domain, grid=grid) + if __name__ == "__main__": + sim.run(one_time_step=True) + + out = Output(path_out) + phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) + x = phi.X.values + phi_num = phi.values + phi_exact = np.cos(k * x) + err_max = np.max(np.abs(phi_num - phi_exact)) + + import matplotlib.pyplot as plt + + plt.figure(figsize=(7, 3.8)) + plt.plot(x, phi_exact, "k--", lw=1.8, label="exact") + plt.plot(x, phi_num, "o", ms=3.5, label="Struphy") + plt.xlabel("x") + plt.ylabel("phi") + plt.title("Struphy quickstart: Poisson solution") + plt.legend() + plt.grid(alpha=0.3) + plt.tight_layout() + plt.savefig(path_out / "quickstart_poisson_phi.png", dpi=150) + plt.show() + print(f"max error = {err_max:.3e}") - plt.figure(figsize=(7, 3.8)) - plt.plot(x, phi_exact, "k--", lw=1.8, label="exact") - plt.plot(x, phi_num, "o", ms=3.5, label="Struphy") - plt.xlabel("x") - plt.ylabel("phi") - plt.title("Struphy quickstart: Poisson solution") - plt.legend() - plt.grid(alpha=0.3) - plt.tight_layout() - plt.savefig("quickstart_poisson_phi.png", dpi=150) - plt.show() Same Workflow For All Models ---------------------------- @@ -175,7 +204,9 @@ The same Simulation API is reused across models. For example, replace :class:`~s .. code-block:: python - from struphy import Simulation, perturbations + from pathlib import Path + + from struphy import EnvironmentOptions, Simulation, perturbations from struphy.models import Maxwell model = Maxwell() @@ -183,8 +214,12 @@ The same Simulation API is reused across models. For example, replace :class:`~s perturbations.ModesCos(ls=(1,), amps=(1e-2,), comp=1) ) - sim = Simulation(model=model) - sim.run() + env = EnvironmentOptions( + out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data" + ) + sim = Simulation(model=model, env=env, params_path=__file__) + if __name__ == "__main__": + sim.run() Check :ref:`models` for more models and their specific options. From 5358055e95aa8ae7f4e58878d876a5c633dc2b8b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 18:55:43 +0200 Subject: [PATCH 044/193] Support legacy plotting --- src/struphy/diagnostics/diagn_tools.py | 32 ++ src/struphy/diagnostics/plotting.py | 375 +++++++++++------- .../diagnostics/tests/test_plotting.py | 98 +++++ .../post_processing/xarray_accessors.py | 267 +++++++++---- 4 files changed, 549 insertions(+), 223 deletions(-) diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index c32e9f769..153e362a7 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -1,5 +1,14 @@ #!/usr/bin/env python3 +"""Spectral diagnostics and deprecated plotting helpers for legacy output. + +Use ``Output(path)`` and array ``.struphy.plot`` accessors for new plotting code. +The legacy distribution/video helpers read the old NPY layout, not output.nc. +``power_spectrum_2d`` remains supported by the analysis accessor. +""" + import logging +import warnings +from functools import wraps import os import shutil import subprocess @@ -17,6 +26,23 @@ logger = logging.getLogger("struphy") +def _legacy_plot(replacement): + def decorate(function): + @wraps(function) + def wrapped(*args, **kwargs): + warnings.warn( + f"diagn_tools.{function.__name__} is deprecated; use {replacement}. " + "Open new output with Output(path); legacy file-based helpers require the old NPY layout.", + DeprecationWarning, + stacklevel=2, + ) + return function(*args, **kwargs) + + return wrapped + + return decorate + + def power_spectrum_2d( field: xr.DataArray, component: int = 0, @@ -230,6 +256,7 @@ def fun(k): return omega, kvec, dispersion, coeffs +@_legacy_plot("out.plot.scalars() or scalar.struphy.plot.timeseries()") def plot_scalars( time, scalar_quantities, @@ -425,6 +452,7 @@ def plot_scalars( plt.show() +@_legacy_plot("array.struphy.plot.slice()") def plot_distr_fun( path, time_idx, @@ -559,6 +587,7 @@ def plot_distr_fun( del delta_f +@_legacy_plot("array.struphy.plot.view(...).animation() or .panels()") def plots_videos_2d( t_grid, grid_slices, @@ -718,6 +747,7 @@ def plots_videos_2d( raise NotImplementedError(f"{output=} is not implemented!") +@_legacy_plot("array.struphy.plot.view(...).animation().save(path)") def video_2d(slc, diagn_path, images_path): """Create a video of all 2D slices of the distribution function over time. @@ -792,6 +822,7 @@ def video_2d(slc, diagn_path, images_path): video.release() +@_legacy_plot("array.struphy.plot.view(...).animation()") def plots_2d_video( t_grid, grid_1_mesh, @@ -873,6 +904,7 @@ def plots_2d_video( plt.close("all") +@_legacy_plot("array.struphy.plot.panels()") def plots_2d_overview( t_grid, grid_1_mesh, diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 1f0e1c23d..91d9088fc 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -16,6 +16,14 @@ import xarray as xr from matplotlib.widgets import Slider +from struphy.diagnostics.analysis import ( + FitResult, + GrowthFit, + drift, + growth_rate, + relative_error, +) # noqa: F401 (backward-compatible imports) + from struphy.post_processing.arrays import ( SCALARS_EXCLUDE, axis_label, @@ -45,22 +53,6 @@ } -@dataclass(frozen=True) -class GrowthFit: - """Configuration for an exponential growth-rate fit.""" - - window: tuple[float | None, float | None] = (None, None) - amplitude_from_quadratic: bool = False - - -@dataclass(frozen=True) -class FitResult: - rate: float - intercept: float - time: np.ndarray - fitted: np.ndarray - - @dataclass(frozen=True) class View: """A reusable selection and rendering recipe for an N-dimensional product.""" @@ -177,49 +169,6 @@ def _select(data: xr.DataArray, view: View, *, keep_sweep=True): return selected -def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: - """Fit ``exp(rate*t + intercept)`` using only finite, positive samples.""" - validate_array(data, required_dims=("t",)) - if data.dims != ("t",): - raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") - fit = fit or GrowthFit() - time, values = np.asarray(data.t), np.asarray(data) - lo = time[0] if fit.window[0] is None else fit.window[0] - hi = time[-1] if fit.window[1] is None else fit.window[1] - lo, hi = sorted((lo, hi)) - valid = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) - if np.count_nonzero(valid) < 2: - return None - selected_time = time[valid] - signal = np.log(np.sqrt(values[valid])) if fit.amplitude_from_quadratic else np.log(values[valid]) - rate, intercept = np.polyfit(selected_time, signal, 1) - scale = 2.0 if fit.amplitude_from_quadratic else 1.0 - fitted = np.exp(scale * (rate * selected_time + intercept)) - return FitResult(float(rate), float(intercept), selected_time, fitted) - - -def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: - """Signed deviation from an explicit reference or the first time sample.""" - validate_array(data, required_dims=("t",)) - reference = data.isel(t=0) if ref is None else ref - out = data - reference - out.attrs = dict(data.attrs) - out.attrs["label"] = f"{_label(data)} drift".strip() - return out - - -def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: - """Absolute relative deviation from an explicit reference or first sample.""" - validate_array(data, required_dims=("t",)) - reference = data.isel(t=0) if ref is None else ref - if np.any(np.asarray(reference) == 0): - raise ValueError("cannot take a relative error against a reference of zero") - out = abs(data - reference) / abs(reference) - out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - out.attrs.update(label=f"relative error of {_label(data)}".strip(), units="") - return out.isel(t=slice(1, None)) if skip_first else out - - def logical_grids(data: xr.DataArray, *, x=None, y=None): """Return 2-D logical coordinate grids and their labels.""" if x is None or y is None: @@ -316,39 +265,117 @@ def label_of(item): return PlotResult(fig, ax, artists, fits) +class _SliceRenderer: + """Shared selection, color limits and mesh rendering for every slice presentation.""" + + def __init__(self, data, view, *, vmin=None, vmax=None, shared_clim=True, cmap=None, equal_aspect=None, title=None): + self.data = _select(data, view) + self.view = View(x=view.x, y=view.y, sweep=view.sweep, coordinates=view.coordinates, plane=view.plane) + self.vmin, self.vmax = vmin, vmax + self.shared_clim = shared_clim + self.cmap = cmap or STRUPHY_STYLE["image.cmap"] + self.equal_aspect = view.coordinates == "physical" if equal_aspect is None else equal_aspect + self.title = _label(data) if title is None else title + self.limits = self._limits(self.data) if shared_clim else None + + def _limits(self, data): + if self.vmin is not None and self.vmax is not None: + return self.vmin, self.vmax + values = np.asarray(data) + finite = values[np.isfinite(values)] + if not finite.size: + raise ValueError("cannot determine color limits from data without finite values; provide vmin and vmax") + return ( + float(finite.min()) if self.vmin is None else self.vmin, + float(finite.max()) if self.vmax is None else self.vmax, + ) + + def draw(self, ax, data): + values, (xg, yg, xlabel, ylabel) = _slice_data(data, self.view) + lo, hi = self.limits if self.shared_clim else self._limits(values) + mesh = ax.pcolormesh(xg, yg, values, shading="auto", vmin=lo, vmax=hi, cmap=self.cmap) + ax.set(xlabel=xlabel, ylabel=ylabel, aspect="equal" if self.equal_aspect else "auto") + ax.grid(False) + return mesh + + def frame_title(self, index): + return f"{self.title} at {self.view.sweep} = {float(self.data[self.view.sweep][index]):.3e}" + + def indices(self, step): + if not isinstance(step, (int, np.integer)) or step < 1: + raise ValueError("step must be a positive integer") + validate_array(self.data, required_dims=(self.view.sweep,)) + if not self.data.sizes[self.view.sweep]: + raise ValueError("cannot render an empty sweep") + return range(0, self.data.sizes[self.view.sweep], step) + + def plot_slice( - data: xr.DataArray, *, view=None, ax=None, vmin=None, vmax=None, equal_aspect=None, title=None, run_label=None + data: xr.DataArray, + *, + view=None, + ax=None, + vmin=None, + vmax=None, + equal_aspect=None, + title=None, + run_label=None, + cmap=None, + shared_clim=True, ): """Render one selected two-dimensional slice.""" - view = view or View() + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + shared_clim=shared_clim, + ) run_label = shared_run_label(data) if run_label is None else run_label - selected, (xgrid, ygrid, xlabel, ylabel) = _slice_data(data, view) own_figure = ax is None with plt.rc_context(STRUPHY_STYLE): fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - mesh = ax.pcolormesh(xgrid, ygrid, np.asarray(selected), shading="auto", vmin=vmin, vmax=vmax) + mesh = renderer.draw(ax, renderer.data) fig.colorbar(mesh, ax=ax, label=value_label(data)) - use_equal_aspect = view.coordinates == "physical" if equal_aspect is None else equal_aspect - if use_equal_aspect: - ax.set_aspect("equal", adjustable="box") - ax.set(xlabel=xlabel, ylabel=ylabel, title=title if title is not None else _label(data)) - ax.grid(False) + ax.set_title(renderer.title) _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) return PlotResult(fig, ax, [mesh]) -def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim=True, title=None, run_label=None): - """Plot snapshots spread across a sweep coordinate.""" - view = view or View() +def plot_panels( + data: xr.DataArray, + *, + view=None, + nrows=3, + ncols=4, + shared_clim=True, + title=None, + run_label=None, + vmin=None, + vmax=None, + cmap=None, + equal_aspect=None, +): + """Plot snapshots with common color limits over the entire selected sweep by default.""" + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + ) + renderer.indices(1) + if nrows < 1 or ncols < 1: + raise ValueError("nrows and ncols must be positive") + sweep = renderer.view.sweep + indices = np.linspace(0, renderer.data.sizes[sweep] - 1, nrows * ncols).astype(int) run_label = shared_run_label(data) if run_label is None else run_label - selected = _select(data, view) - validate_array(selected, required_dims=(view.sweep,)) - count = nrows * ncols - indices = np.linspace(0, selected.sizes[view.sweep] - 1, count).astype(int) - snapshots = [selected.isel({view.sweep: int(index)}) for index in indices] - limits = (None, None) - if shared_clim: - limits = (min(float(item.min()) for item in snapshots), max(float(item.max()) for item in snapshots)) with plt.rc_context(STRUPHY_STYLE): fig, axes = plt.subplots( nrows, @@ -360,115 +387,136 @@ def plot_panels(data: xr.DataArray, *, view=None, nrows=3, ncols=4, shared_clim= layout="constrained", ) meshes = [] - for ax, index, snapshot in zip(axes.ravel(), indices, snapshots): - local_view = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) - values, (xg, yg, xlabel, ylabel) = _slice_data(snapshot, local_view) - mesh = ax.pcolormesh(xg, yg, values, shading="auto", vmin=limits[0], vmax=limits[1]) + for ax, index in zip(axes.ravel(), indices): + mesh = renderer.draw(ax, renderer.data.isel({sweep: int(index)})) meshes.append(mesh) - ax.set_title(f"{view.sweep} = {float(selected[view.sweep][index]):.3e}") - ax.grid(False) + ax.set_title(f"{sweep} = {float(renderer.data[sweep][index]):.3e}") if not shared_clim: - fig.colorbar(mesh, ax=ax) - for ax in axes[-1]: - ax.set_xlabel(xlabel) - for row in axes: - row[0].set_ylabel(ylabel) + fig.colorbar(mesh, ax=ax, label=value_label(data)) if shared_clim: fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) - heading = title if title is not None else _label(data) - fig.suptitle(" — ".join(filter(None, (heading, run_label)))) + fig.suptitle(" — ".join(filter(None, (renderer.title, run_label)))) return PlotResult(fig, axes, meshes) class InteractiveSliceViewer: - """Stateful viewer using one recipe for the sweep and all remaining dimensions.""" - - def __init__(self, data: xr.DataArray, *, view=None, vmin=None, vmax=None, run_label=None): + """Slider view with the same rendering options as static and exported slices.""" + + def __init__( + self, + data: xr.DataArray, + *, + view=None, + vmin=None, + vmax=None, + run_label=None, + shared_clim=True, + cmap=None, + equal_aspect=None, + title=None, + ): self.data = validate_array(data) self.view = view or View() - self.vmin, self.vmax = vmin, vmax + self.options = dict( + vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title + ) self.run_label = shared_run_label(data) if run_label is None else run_label self.result = None self.sliders = {} def show(self): - return self.draw().show() + (self.result or self.draw()).show() + return self def _ipython_display_(self): (self.result or self.draw())._ipython_display_() def draw(self): - base = _select(self.data, self.view) + if self.result is not None: + return self.result + renderer = _SliceRenderer(self.data, self.view, **self.options) + base = renderer.data x, y = self.view.x, self.view.y if x is None or y is None: candidates = [dim for dim in base.dims if dim != self.view.sweep] if len(candidates) < 2: raise ValueError("viewer needs two display dimensions") x, y = candidates[:2] + renderer.view = View(x=x, y=y, coordinates=self.view.coordinates, plane=self.view.plane) controls = [dim for dim in base.dims if dim not in {x, y}] indices = {dim: 0 for dim in controls} - - def frame(): - return base.isel(indices), View(x=x, y=y, coordinates=self.view.coordinates, plane=self.view.plane) - - selected, frame_view = frame() - selected, (xg, yg, xlabel, ylabel) = _slice_data(selected, frame_view) with plt.rc_context(STRUPHY_STYLE): fig, ax = plt.subplots() fig.subplots_adjust(bottom=0.13 + 0.05 * len(controls)) - mesh = ax.pcolormesh(xg, yg, selected, shading="auto", vmin=self.vmin, vmax=self.vmax) + mesh = renderer.draw(ax, base.isel(indices)) colorbar = fig.colorbar(mesh, ax=ax, label=value_label(self.data)) - ax.set(xlabel=xlabel, ylabel=ylabel) - ax.grid(False) - if self.view.coordinates == "physical": - ax.set_aspect("equal", adjustable="box") - state = {"mesh": mesh} + self.result = PlotResult(fig, ax, [mesh]) def update(_=None): for dim, slider in self.sliders.items(): indices[dim] = int(slider.val) - item, item_view = frame() - item, grids = _slice_data(item, item_view) - state["mesh"].remove() - state["mesh"] = ax.pcolormesh(grids[0], grids[1], item, shading="auto", vmin=self.vmin, vmax=self.vmax) - if self.vmin is None and self.vmax is None: - state["mesh"].set_clim(float(item.min()), float(item.max())) - colorbar.update_normal(state["mesh"]) + self.result.artists[0].remove() + mesh = renderer.draw(ax, base.isel(indices)) + self.result.artists[:] = [mesh] + colorbar.update_normal(mesh) values = ", ".join(f"{dim}={float(base[dim][index]):.3e}" for dim, index in indices.items()) - ax.set_title(" at ".join(filter(None, (_label(self.data), values)))) + ax.set_title(" at ".join(filter(None, (renderer.title, values)))) fig.canvas.draw_idle() for row, dim in enumerate(controls): + if base.sizes[dim] == 1: + continue slider_ax = fig.add_axes([0.20, 0.05 + 0.05 * row, 0.60, 0.025]) slider = Slider(slider_ax, dim, 0, base.sizes[dim] - 1, valstep=1) slider.on_changed(update) self.sliders[dim] = slider update() _finish(fig, run_label=self.run_label, tight=False) - self.result = PlotResult(fig, ax, [state["mesh"]]) + # Keep widget callbacks alive even if only the PlotResult is retained. + self.result.data["viewer"] = self return self.result -def animate_slices(data: xr.DataArray, *, view=None, interval=100, step=1, vmin=None, vmax=None): - """Create an animation using the same :class:`View` as static slices.""" +def animate_slices( + data: xr.DataArray, + *, + view=None, + interval=100, + step=1, + vmin=None, + vmax=None, + shared_clim=True, + cmap=None, + equal_aspect=None, + title=None, +): + """Animate slices with fixed color limits over the selected sweep by default.""" from matplotlib.animation import FuncAnimation - view = view or View() - selected = _select(data, view) - frames = range(0, selected.sizes[view.sweep], step) - first = selected.isel({view.sweep: 0}) - local = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) - values, grids = _slice_data(first, local) - fig, ax = plt.subplots() - mesh = ax.pcolormesh(grids[0], grids[1], values, shading="auto", vmin=vmin, vmax=vmax) - fig.colorbar(mesh, ax=ax, label=value_label(data)) - ax.set(xlabel=grids[2], ylabel=grids[3]) + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + ) + frames = renderer.indices(step) + sweep = renderer.view.sweep + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() + mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) + colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) + _finish(fig, run_label=shared_run_label(data)) def update(index): - item = selected.isel({view.sweep: index}) - item, item_grids = _slice_data(item, local) - mesh.set_array(np.asarray(item).ravel()) - ax.set_title(f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}") + nonlocal mesh + mesh.remove() + mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) + colorbar.update_normal(mesh) + ax.set_title(renderer.frame_title(index)) return (mesh,) animation = FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) @@ -476,22 +524,53 @@ def update(index): return animation -def save_frames(data: xr.DataArray, directory, *, view=None, step=1, prefix="frame", dpi=110): - """Write a sweep as PNG frames without retaining figures.""" - view = view or View() - selected = _select(data, view) +def save_frames( + data: xr.DataArray, + directory, + *, + view=None, + step=1, + prefix="frame", + dpi=110, + vmin=None, + vmax=None, + shared_clim=True, + cmap=None, + equal_aspect=None, + title=None, +): + """Export the configured sweep as PNGs, sharing color limits by default.""" + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + ) + frames = renderer.indices(step) directory = Path(directory) directory.mkdir(parents=True, exist_ok=True) paths = [] - for frame, index in enumerate(range(0, selected.sizes[view.sweep], step)): - item = selected.isel({view.sweep: index}) - local = View(x=view.x, y=view.y, coordinates=view.coordinates, plane=view.plane) - result = plot_slice( - item, view=local, title=f"{_label(data)} at {view.sweep} = {float(selected[view.sweep][index]):.3e}" - ) - path = directory / f"{prefix}_{frame:04d}.png" - result.save(path, dpi=dpi, close=True) - paths.append(str(path)) + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() + try: + sweep = renderer.view.sweep + mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) + colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) + _finish(fig, run_label=shared_run_label(data)) + for frame, index in enumerate(frames): + mesh.remove() + mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) + colorbar.update_normal(mesh) + ax.set_title(renderer.frame_title(index)) + path = directory / f"{prefix}_{frame:04d}.png" + fig.savefig(path, dpi=dpi, bbox_inches="tight") + paths.append(str(path)) + finally: + plt.close(fig) return paths diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 76f017eac..2f86f8562 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -183,3 +183,101 @@ def test_slice_can_display_the_sweep_dimension(): assert result.ax.get_xlabel() == "$t$ [s]" with pytest.raises(ValueError, match="display it as x or y"): plot_slice(data, view=View(x="e1", y="v1")) + + +def test_every_presentation_uses_the_full_selected_color_range(tmp_path, monkeypatch): + import struphy.post_processing.xarray_accessors # noqa: F401 + from matplotlib.figure import Figure + + data = phase_space(nt=3).astype(float) + data[1] = data[1] * 100 # extrema in a frame omitted by panels and export + view = data.struphy.plot.view(x="e1", y="v1", cmap="plasma", equal_aspect=True) + limits = (float(data.min()), float(data.max())) + assert plt.get_fignums() == [] + snapshot = view.slice(t="last") + panels = view.panels(nrows=1, ncols=2) + viewer = view.viewer() + result = viewer.draw() + viewer.sliders["t"].set_val(2) + animation = view.animation(step=2) + mesh = animation._func(2)[0] + for artist in [snapshot.artists[0], *panels.artists, result.artists[0], mesh]: + assert artist.get_clim() == limits + assert artist.get_cmap().name == "plasma" + assert artist.axes.get_aspect() == 1.0 + captured = [] + original = Figure.savefig + + def capture(fig, *args, **kwargs): + captured.append(fig.axes[0].collections[0].get_clim()) + return original(fig, *args, **kwargs) + + monkeypatch.setattr(Figure, "savefig", capture) + before = plt.get_fignums() + assert len(view.save_frames(tmp_path, step=2)) == 2 + assert captured == [limits, limits] + assert plt.get_fignums() == before + + +@pytest.mark.parametrize("shared_clim", [True, False]) +def test_explicit_color_limits_work_for_all_renderers(tmp_path, monkeypatch, shared_clim): + import struphy.post_processing.xarray_accessors # noqa: F401 + from matplotlib.figure import Figure + + data = phase_space(nt=2) + options = dict(x="e1", y="v1", vmin=-5, vmax=100, shared_clim=shared_clim, cmap="coolwarm") + panels = data.struphy.plot.panels(nrows=1, ncols=2, **options) + animation = data.struphy.plot.animation(**options) + viewer = data.struphy.plot.viewer(**options) + viewer.draw() + viewer.sliders["t"].set_val(1) + for mesh in [*panels.artists, animation._func(1)[0], viewer.result.artists[0]]: + assert mesh.get_clim() == (-5, 100) + assert mesh.get_cmap().name == "coolwarm" + captured = [] + monkeypatch.setattr( + Figure, "savefig", lambda fig, *args, **kwargs: captured.append(fig.axes[0].collections[0].get_clim()) + ) + data.struphy.plot.frames(tmp_path, **options) + assert captured == [(-5, 100), (-5, 100)] + + +def test_per_frame_scaling_is_explicit_and_supports_a_fixed_lower_limit(): + import struphy.post_processing.xarray_accessors # noqa: F401 + + data = phase_space(nt=2) + view = data.struphy.plot.view(x="e1", y="v1", shared_clim=False, vmin=-1) + panels = view.panels(nrows=1, ncols=2) + animation = view.animation() + for index in range(2): + limits = (-1, float(data.isel(t=index).max())) + assert panels.artists[index].get_clim() == limits + assert animation._func(index)[0].get_clim() == limits + + +def test_viewer_show_retains_controls_and_does_not_redraw(monkeypatch): + viewer = InteractiveSliceViewer(phase_space(), view=View(x="e1", y="v1")) + result = viewer.draw() + monkeypatch.setattr(plt, "show", lambda: None) + assert viewer.show() is viewer + assert viewer.draw() is result + assert len(plt.get_fignums()) == 1 + viewer.sliders["t"].set_val(2) + assert result.artists[0] is result.ax.collections[0] + + +@pytest.mark.parametrize("step", [0, -1]) +def test_sweep_rejects_invalid_step(tmp_path, step): + with pytest.raises(ValueError, match="positive integer"): + animate_slices(phase_space(), view=View(x="e1", y="v1"), step=step) + with pytest.raises(ValueError, match="positive integer"): + save_frames(phase_space(), tmp_path, view=View(x="e1", y="v1"), step=step) + + +def test_legacy_scalar_plot_warns_and_still_renders(monkeypatch): + from struphy.diagnostics import diagn_tools + + monkeypatch.setattr(plt, "show", lambda: None) + with pytest.deprecated_call(match="out.plot.scalars"): + diagn_tools.plot_scalars(np.arange(3), {"en_tot": np.array([1.0, 2.0, 3.0])}) + assert plt.get_fignums() diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index a4f8b3bcd..440ec8b6e 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -98,48 +98,80 @@ def timeseries( growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) return plot_timeseries([self._array, *others], ax=ax, logy=logy, fit=growth, title=title) - def slice( + def view( self, *, x: str | None = None, y: str | None = None, + sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", vmin=None, vmax=None, + shared_clim: bool = True, + cmap: str | None = None, equal_aspect: bool | None = None, title: str | None = None, - ax=None, **selection, - ): - """A two-dimensional color plot of one slice. + ) -> "SliceView": + """Configure a reusable slice view without rendering a figure. - Parameters - ---------- - x, y: - Displayed dimensions, e.g. ``x="e1", y="v1"``; inferred for two-dimensional data. - The sweep dimension ``t`` may be displayed, which gives a space-time map. - coords: - ``"physical"`` draws on the mapped coordinates of ``plane`` instead of logical ones. - **selection: - One value per remaining dimension, e.g. ``t="last", component=2, e3=0``. + Use xarray's ``.sel()``/``.isel()`` for general selection, or pass remaining + dimensions here (integers are positions, floats nearest coordinates, + ``"first"``/``"last"`` select an end). + + ``shared_clim=True`` fixes color limits over all selected data, including + frames omitted by a panel layout or export step. False rescales each frame. + Explicit ``vmin``/``vmax`` override either limit in both modes. ``cmap``, + ``equal_aspect`` and ``title`` apply to every presentation of this view. Examples -------- - >>> out.ions.eta1_v1.f.struphy.plot.slice(x="e1", y="v1", t="last") - >>> out.em_fields.b_field_phy.struphy.plot.slice(x="e1", y="e2", component=2, e3=0, coords="physical") + >>> view = f.struphy.plot.view(x="e1", y="v1", cmap="RdBu_r") + >>> view.slice(t="last") + >>> view.panels(nrows=2, ncols=3) + >>> view.save_frames("frames") """ - from struphy.diagnostics.plotting import plot_slice - - return plot_slice( + self._view(x, y, sweep, coords, plane, selection) # validate selections now + return SliceView( self._array, - view=self._view(x, y, "t", coords, plane, selection), - ax=ax, + dict(x=x, y=y, sweep=sweep, coords=coords, plane=plane), + selection, + dict(vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title), + ) + + def slice( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + ax=None, + **selection, + ): + """Render one 2-D slice; see :meth:`view` for shared options.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, vmin=vmin, vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, equal_aspect=equal_aspect, title=title, - ) + **selection, + ).slice(ax=ax) def panels( self, @@ -149,26 +181,31 @@ def panels( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", - nrows: int = 3, - ncols: int = 4, + vmin=None, + vmax=None, shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, title: str | None = None, + nrows: int = 3, + ncols: int = 4, **selection, ): - """Snapshots evenly spread along ``sweep`` (time by default), one panel each. - - Takes the same arguments as :meth:`slice`, except that ``sweep`` is not selected. - """ - from struphy.diagnostics.plotting import plot_panels - - return plot_panels( - self._array, - view=self._view(x, y, sweep, coords, plane, selection), - nrows=nrows, - ncols=ncols, + """Render evenly spaced snapshots; see :meth:`view` for shared options.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, title=title, - ) + **selection, + ).panels(nrows=nrows, ncols=ncols) def viewer( self, @@ -180,18 +217,27 @@ def viewer( plane: Plane = "XY", vmin=None, vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, **selection, ): - """An interactive viewer with one slider per dimension that is neither displayed nor selected. - - Takes the same arguments as :meth:`slice`. Call ``.show()`` on the result, and keep it - alive so that the sliders stay connected. - """ - from struphy.diagnostics.plotting import InteractiveSliceViewer - - return InteractiveSliceViewer( - self._array, view=self._view(x, y, sweep, coords, plane, selection), vmin=vmin, vmax=vmax - ) + """Create an interactive slider view; retain the returned viewer.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).viewer() def animation( self, @@ -201,23 +247,31 @@ def animation( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", - interval: int = 100, - step: int = 1, vmin=None, vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + interval: int = 100, + step: int = 1, **selection, ): - """A Matplotlib animation along ``sweep``, taking the same arguments as :meth:`slice`.""" - from struphy.diagnostics.plotting import animate_slices - - return animate_slices( - self._array, - view=self._view(x, y, sweep, coords, plane, selection), - interval=interval, - step=step, + """Animate the sweep; retain the returned Matplotlib animation.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, vmin=vmin, vmax=vmax, - ) + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).animation(interval=interval, step=step) def frames( self, @@ -228,25 +282,32 @@ def frames( sweep: str = "t", coords: Coordinates = "logical", plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, step: int = 1, prefix: str = "frame", dpi: int = 110, **selection, - ) -> list[str]: - """Write the slices along ``sweep`` as numbered PNG files; returns their paths. - - Takes the same arguments as :meth:`slice`. - """ - from struphy.diagnostics.plotting import save_frames - - return save_frames( - self._array, - directory, - view=self._view(x, y, sweep, coords, plane, selection), - step=step, - prefix=prefix, - dpi=dpi, - ) + ): + """Export PNGs; equivalent to ``plot.view(...).save_frames(directory)``.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).save_frames(directory, step=step, prefix=prefix, dpi=dpi) def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): """Three-dimensional paths of saved markers; for an orbit product.""" @@ -255,10 +316,66 @@ def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None return plot_marker_trajectories(self._array, ax=ax, max_markers=max_markers, show_paths=show_paths) +class SliceView: + """A configured array view, shared by static, interactive and exported plots. + + Construct with ``array.struphy.plot.view(...)``. Configuration does not create + figures or copy the underlying array. + """ + + def __init__(self, array, coordinates, selection, options): + self._array = array + self._coordinates = dict(coordinates) + self._selection = dict(selection) + self._options = dict(options) + + def _view(self, **selection): + return ArrayPlots(self._array)._view(**self._coordinates, selection={**self._selection, **selection}) + + def slice(self, *, ax=None, **selection): + """Draw a snapshot, e.g. ``view.slice(t="last")``; return a PlotResult.""" + from struphy.diagnostics.plotting import plot_slice + + # Resolve shared limits before selecting a single snapshot, so it uses + # the same scale as panels, animation and export of this configured view. + from struphy.diagnostics.plotting import _SliceRenderer + + options = dict(self._options) + if options["shared_clim"]: + renderer = _SliceRenderer(self._array, self._view(), **options) + options.update(zip(("vmin", "vmax"), renderer.limits)) + return plot_slice(self._array, view=self._view(**selection), ax=ax, **options) + + def panels(self, *, nrows=3, ncols=4): + """Draw snapshots spread along the sweep; return a PlotResult.""" + from struphy.diagnostics.plotting import plot_panels + + return plot_panels(self._array, view=self._view(), nrows=nrows, ncols=ncols, **self._options) + + def viewer(self): + """Create a viewer with sliders for unselected dimensions.""" + from struphy.diagnostics.plotting import InteractiveSliceViewer + + return InteractiveSliceViewer(self._array, view=self._view(), **self._options) + + def animation(self, *, interval=100, step=1): + """Create a Matplotlib animation using this view's rendering options.""" + from struphy.diagnostics.plotting import animate_slices + + return animate_slices(self._array, view=self._view(), interval=interval, step=step, **self._options) + + def save_frames(self, directory, *, step=1, prefix="frame", dpi=110): + """Export PNG frames using this view's rendering options; return paths.""" + from struphy.diagnostics.plotting import save_frames + + return save_frames( + self._array, directory, view=self._view(), step=step, prefix=prefix, dpi=dpi, **self._options + ) + + class ArrayAnalysis(_ArrayAccessor): """Quantitative diagnostics of one array, as ``array.struphy.analysis.(...)``.""" - def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): """Fit ``exp(rate * t + intercept)`` to this time series within ``window``. @@ -266,19 +383,19 @@ def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. """ - from struphy.diagnostics.plotting import GrowthFit, growth_rate + from struphy.diagnostics.analysis import GrowthFit, growth_rate return growth_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) def drift(self, *, ref=None) -> xr.DataArray: """Signed deviation of this time series from ``ref`` or from its first sample.""" - from struphy.diagnostics.plotting import drift + from struphy.diagnostics.analysis import drift return drift(self._array, ref=ref) def relative_error(self, *, ref=None, skip_first: bool = True) -> xr.DataArray: """Absolute relative deviation from ``ref`` or from this series' first sample.""" - from struphy.diagnostics.plotting import relative_error + from struphy.diagnostics.analysis import relative_error return relative_error(self._array, ref=ref, skip_first=skip_first) From c2043dd7ac506c8b822cd830f02d19a10c78b604 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 20:30:40 +0200 Subject: [PATCH 045/193] Improved Output.info() --- src/struphy/diagnostics/analysis.py | 71 +++++++++++++++++++ src/struphy/post_processing/arrays.py | 2 +- src/struphy/post_processing/output.py | 19 +++-- .../post_processing/tests/test_arrays.py | 2 +- .../post_processing/tests/test_output.py | 8 +++ 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 src/struphy/diagnostics/analysis.py diff --git a/src/struphy/diagnostics/analysis.py b/src/struphy/diagnostics/analysis.py new file mode 100644 index 000000000..47ae10da3 --- /dev/null +++ b/src/struphy/diagnostics/analysis.py @@ -0,0 +1,71 @@ +"""Numerical diagnostics returning values and labeled arrays, without rendering.""" + +from dataclasses import dataclass + +import numpy as np +import xarray as xr + +from struphy.post_processing.arrays import validate_array + + +def _label(data): + return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" + + +@dataclass(frozen=True) +class GrowthFit: + """Configuration for an exponential growth-rate fit.""" + + window: tuple[float | None, float | None] = (None, None) + amplitude_from_quadratic: bool = False + + +@dataclass(frozen=True) +class FitResult: + rate: float + intercept: float + time: np.ndarray + fitted: np.ndarray + + +def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: + """Fit ``exp(rate*t + intercept)`` using only finite, positive samples.""" + validate_array(data, required_dims=("t",)) + if data.dims != ("t",): + raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") + fit = fit or GrowthFit() + time, values = np.asarray(data.t), np.asarray(data) + lo = time[0] if fit.window[0] is None else fit.window[0] + hi = time[-1] if fit.window[1] is None else fit.window[1] + lo, hi = sorted((lo, hi)) + valid = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) + if np.count_nonzero(valid) < 2: + return None + selected_time = time[valid] + signal = np.log(np.sqrt(values[valid])) if fit.amplitude_from_quadratic else np.log(values[valid]) + rate, intercept = np.polyfit(selected_time, signal, 1) + scale = 2.0 if fit.amplitude_from_quadratic else 1.0 + fitted = np.exp(scale * (rate * selected_time + intercept)) + return FitResult(float(rate), float(intercept), selected_time, fitted) + + +def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: + """Signed deviation from an explicit reference or the first time sample.""" + validate_array(data, required_dims=("t",)) + reference = data.isel(t=0) if ref is None else ref + out = data - reference + out.attrs = dict(data.attrs) + out.attrs["label"] = f"{_label(data)} drift".strip() + return out + + +def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: + """Absolute relative deviation from an explicit reference or first sample.""" + validate_array(data, required_dims=("t",)) + reference = data.isel(t=0) if ref is None else ref + if np.any(np.asarray(reference) == 0): + raise ValueError("cannot take a relative error against a reference of zero") + out = abs(data - reference) / abs(reference) + out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + out.attrs.update(label=f"relative error of {_label(data)}".strip(), units="") + return out.isel(t=slice(1, None)) if skip_first else out diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 422f25b52..b495c991d 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -28,7 +28,7 @@ "marker": "marker", "quantity": "quantity", } -BINNED_LABELS = {"f_binned": "$f$", "delta_f_binned": r"$\delta f$", "n_sph": "$n$"} +BINNED_LABELS = {"f": "$f$", "delta_f": r"$\delta f$", "n": "$n$"} SCALARS_EXCLUDE = ("time",) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 76059fc08..9c5225ef1 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -15,7 +15,7 @@ from feectools.ddm.mpi import mpi as MPI from struphy.post_processing import store -from struphy.post_processing.arrays import data_array, save_scalars +from struphy.post_processing.arrays import BINNED_LABELS, data_array, save_scalars from struphy.post_processing.output_accessors import OutputPlots logger = logging.getLogger("struphy") @@ -197,7 +197,7 @@ def _reset(self): def __getitem__(self, name: str) -> xr.DataArray: """Any product by name: a scalar (``"en_tot"``), a field (``"em_fields/phi_log"``), a binned - distribution or SPH density (``"kinetic_ions/e1_v1_density/f_binned"``) or orbits (``"kinetic_ions"``). + distribution or SPH density (``"kinetic_ions/e1_v1_density/f"``) or orbits (``"kinetic_ions"``). """ if name in self.scalars.data_vars: return self.scalars[name] @@ -422,7 +422,7 @@ def species_catalog(self) -> ProductMapping: def __getattr__(self, name: str) -> ProductNamespace: """Products of one species or field group, as ``out..``. - ``out.kinetic_ions.e1_v1_density.f_binned`` and ``out.kinetic_ions.orbits`` are the + ``out.kinetic_ions.e1_v1_density.f`` and ``out.kinetic_ions.orbits`` are the products of that species, whatever kind they are; the grouped views :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` show them by kind. """ @@ -628,8 +628,9 @@ def label(self) -> str: def info(self) -> str: """A table of everything this output holds, printed by ``print(out.info())``. - Names are listed as they are reached, e.g. ``out.kinetic_ions.e1_v1_density.f_binned`` - and ``out["kinetic_ions/e1_v1_density/f_binned"]``. Nothing is loaded. + Names are listed as they are reached, e.g. ``out.kinetic_ions.e1_v1_density.f`` + and ``out["kinetic_ions/e1_v1_density/f"]``. Distribution and density products carry + their symbol, e.g. ``f`` ($f$) vs. ``delta_f`` ($\\delta f$). Nothing is loaded. """ lines = [f"Output of {self.path_out}", f" {self.label}", ""] scalars = tuple(self.scalars.data_vars) @@ -646,12 +647,18 @@ def info(self) -> str: ("orbits", self.orbit_catalog), ): lines += ["", kind] - entries = [f" out.{key.replace('/', '.')}" for key in catalog] + entries = [f" out.{key.replace('/', '.')}{self._quantity_hint(key)}" for key in catalog] if kind == "orbits": entries = [f" out.{key}.orbits" for key in catalog] lines += entries or [" (none)"] return "\n".join(lines) + @staticmethod + def _quantity_hint(key: str) -> str: + """Static label for a catalog key, e.g. distinguishing ``f`` from ``delta_f``.""" + label = BINNED_LABELS.get(key.rsplit("/", 1)[-1]) + return f" ({label})" if label else "" + def save_scalars(self, path=None, **kwargs) -> str: """Write the scalar time series as CSV (or NPZ); ``post_processing/scalars.csv`` by default.""" path = Path(path) if path else self.path_pproc / "scalars.csv" diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index c45de7b80..bb1700dd9 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -69,7 +69,7 @@ def test_vector_field_has_named_component_dimension(): def test_binned_wrapper_keeps_memory_mappable_values(): values = np.ones((2, 3, 4)) - data = wrap_binned_data(values, ("e1", "v1"), {"t": [0, 1], "e1": range(3), "v1": range(4)}, name="f_binned") + data = wrap_binned_data(values, ("e1", "v1"), {"t": [0, 1], "e1": range(3), "v1": range(4)}, name="f") assert data.dims == ("t", "e1", "v1") assert data.attrs["label"] == "$f$" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index c30dfcf05..4dd090c23 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -287,6 +287,14 @@ def test_info_lists_products_without_loading(run): assert run.field_catalog._cache == {}, "listing must not load arrays" +def test_info_hints_distribution_and_density_symbols(run): + text = run.info() + assert "out.kinetic_ions.e1_v1_density.f ($f$)" in text + assert "out.kinetic_ions.e1_v1_density.delta_f ($\\delta f$)" in text + assert "out.kinetic_ions.view_0.n ($n$)" in text + assert "out.em_fields.E" in text and "($" not in text.split("out.em_fields.E")[1].split("\n")[0] + + def test_normalized_time_carries_seconds_as_a_coordinate(run): energy = run.scalars.en_tot assert "units" not in energy.t.attrs, "normalized time has no unit" From 420928587008a4f602831871a560220196a983bb Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 23:17:22 +0200 Subject: [PATCH 046/193] update tutorial --- tutorials/tutorial_post_processing.ipynb | 11445 ++++++++++++++++++++- 1 file changed, 11379 insertions(+), 66 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index f2091a430..9a9fbb3a5 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -4,7 +4,6 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "outputs": [], "source": [ "# Post-processing and standard plots\n", "\n", @@ -15,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "1", "metadata": {}, "outputs": [], @@ -51,7 +50,6 @@ "cell_type": "markdown", "id": "2", "metadata": {}, - "outputs": [], "source": [ "## Create a compact demonstration run\n", "\n", @@ -60,10 +58,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", + " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", + " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" + ] + } + ], "source": [ "def build_model():\n", " model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", @@ -101,10 +110,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time stepping: 100%|██████████| 100/100 [00:22<00:00, 4.40step/s]\n", + "Raw output: /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo\n" + ] + } + ], "source": [ "demo_tmp = tempfile.TemporaryDirectory(prefix=\"struphy_postprocessing_\")\n", "demo_root = demo_tmp.name\n", @@ -117,7 +142,7 @@ "sim = Simulation(\n", " model=model,\n", " env=env,\n", - " time_opts=Time(dt=0.05, Tend=2.0),\n", + " time_opts=Time(dt=0.05, Tend=5.0),\n", " domain=domains.Cuboid(r1=2 * 3.141592653589793),\n", " equil=equils.HomogenSlab(),\n", " grid=grids.TensorProductGrid(num_elements=(16, 1, 1)),\n", @@ -131,7 +156,6 @@ "cell_type": "markdown", "id": "5", "metadata": {}, - "outputs": [], "source": [ "## Process and load the output\n", "\n", @@ -144,10 +168,83 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", + " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", + " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", + "\n", + "Post-processing path /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo\n", + "\n", + "Reading hdf5 data of following species:\n", + "em_fields:\n", + " e_field: \n", + " phi: \n", + "Creation of Struphy Fields done.\n", + "\n", + "Evaluating fields ...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100%|██████████| 101/101 [00:00<00:00, 146.08it/s]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluation of 12 marker orbits for kinetic_ions\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100%|██████████| 101/101 [00:00<00:00, 1306.96it/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluation of distribution functions for kinetic_ions\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "0 starting post-processing of distribution functions for /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo/post_processing/kinetic_data/kinetic_ions ...\n", + "100%|██████████| 1/1 [00:00<00:00, 1620.05it/s]\n", + " 0%| | 0/1 [00:00 Size: 827kB\n", + "[103424 values with dtype=float64]\n", + "Coordinates:\n", + " * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.8 4.85 4.9 4.95 5.0\n", + " t_seconds (t) float64 808B 0.0 1.668e-10 3.336e-10 ... 1.651e-08 1.668e-08\n", + " * e1 (e1) float64 256B 0.01562 0.04688 0.07812 ... 0.9531 0.9844\n", + " * v1 (v1) float64 256B -4.844 -4.531 -4.219 ... 4.219 4.531 4.844\n", + "Attributes:\n", + " label: $f$\n", + " long_name: $f$\n", + " run: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)\n", + " run_name: vlasov_ampere_demo\n" + ] + } + ], "source": [ "print(out.info())\n", "print(out.kinetic_ions)\n", @@ -175,11 +314,133 @@ "print(phase_space)" ] }, + { + "cell_type": "markdown", + "id": "8a", + "metadata": {}, + "source": [ + "### Inspecting `out` itself\n", + "\n", + "Most of what `out` exposes is generated on demand (`__getattr__`, cached properties), so `vars(out)` only shows a handful of private cache slots, not the products or methods. Use `dir(out)` for the flat list IPython's own tab-completion relies on, and `out.info()` for the full product tree. The same applies one level down: `out.model` is not a generic stub but the concrete model class of the run (`VlasovAmpereOneSpecies` here), so `dir(out.model)` and `print(out.model)` already show that model's own parameters directly — no separate `OutputVlasovAmpereOneSpecies`-style class is needed." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "vars(out): {'path_out': PosixPath('/private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo'), 'time_units': 'normalized', 'comm': , '_time': None, '_grids_log': None, '_grids_phy': None, '_scalars': Size: 4kB\n", + "Dimensions: (t: 101)\n", + "Coordinates:\n", + " * t (t) float64 808B 0.0 0.05 0.1 0.15 ... 4.85 4.9 4.95 5.0\n", + " t_seconds (t) float64 808B 0.0 1.668e-10 ... 1.651e-08 1.668e-08\n", + "Data variables:\n", + " electric_energy (t) float64 808B 1.662e-06 1.651e-06 ... 1.488e-07\n", + " kinetic_energy (t) float64 808B 0.01887 0.01887 ... 0.01887 0.01887\n", + " total_energy (t) float64 808B 0.01887 0.01887 ... 0.01887 0.01887, '_products': {'fields': , 'distributions': , 'densities': , 'orbits': }, '_label': 'dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)', '_tree': \n", + "Group: /\n", + "│ Attributes:\n", + "│ schema_version: 1\n", + "│ options: {\"step\": 1, \"celldivide\": [1, 1, 1], \"physical\": true, \"...\n", + "├── Group: /em_fields\n", + "│ Dimensions: (t: 101, component: 3, e1: 17, e2: 2, e3: 2)\n", + "│ Coordinates:\n", + "│ * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.85 4.9 4.95 5.0\n", + "│ * component (component) int64 24B 0 1 2\n", + "│ * e1 (e1) float64 136B 0.0 0.0625 0.125 0.1875 ... 0.875 0.9375 1.0\n", + "│ * e2 (e2) float64 16B 0.0 1.0\n", + "│ * e3 (e3) float64 16B 0.0 1.0\n", + "│ X (e1, e2, e3) float64 544B ...\n", + "│ Y (e1, e2, e3) float64 544B ...\n", + "│ Z (e1, e2, e3) float64 544B ...\n", + "│ Data variables:\n", + "│ e_field (t, component, e1, e2, e3) float64 165kB ...\n", + "│ e_field_xyz (t, component, e1, e2, e3) float64 165kB ...\n", + "│ phi (t, e1, e2, e3) float64 55kB ...\n", + "│ phi_xyz (t, e1, e2, e3) float64 55kB ...\n", + "└── Group: /kinetic_ions\n", + " │ Dimensions: (t: 101, marker: 12, quantity: 8)\n", + " │ Coordinates:\n", + " │ * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.8 4.85 4.9 4.95 5.0\n", + " │ * marker (marker) int64 96B 0 1 2 3 4 5 6 7 8 9 10 11\n", + " │ * quantity (quantity) " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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65csWSLNmzfPtzLRQMNeAAB8ZtCgQTJ16lRZv369eayNdr/55huzPuj555+X7t272yoTbb7KxAAAAJFbbrlFfv75Z2nSpInUqFFDfv/9dxk7dqycf/75odOjTS+WLVtmq0y0+SqI0bpBbeSUXR0hOH9ce7GJ9y3nj2sv/xUsWFBeeeUVeeihh+TPP/80471ou5f0brrpJrn00kttlYk2Xw12BwAA4gdtYgAAgCcRxAAAAE8iiAEAAJ4UV0HM5s2b5cYbb5SOHTvKZZddJosWLYpImXikE3k9/fTTct5550nXrl1l4sSJkpaWdsIp2fXcderUyUwANmfOHPGrr7/+Wvr162euo3/961/y119/5bnslClTpHXr1vLUU0+JH/39998ybNgwc+4uueSSPM/D8vHHH0v//v2lc+fOZvK5/BgszGu0SePLL78sF1xwgXTp0kXGjx9/wtFTd+zYIffff7+Z80bf77fffrvpheJHK1askCFDhpj335dffpmnMvq5p593HTp0MGV1oDq4J26CmN27d5spwTUoueuuu8zIsvqhmFtQ4qRMvNJW6I8++qhcc801MnDgQBkzZowMHz48x+3ff/99U6Zx48by73//W5o2bSo9e/aU5557TvxGP/w0kKtbt67ceeed8uuvv8pZZ50lKSkpJyy7bt06M6W9XoO//fab+M2hQ4fk7LPPlpUrV8odd9xhriftrjl7du5DsOtNWG8kbdu2lfvuu8/cuO+55x7xm3vvvVfuvvtuE0APHjxYnn32WfMezklqaqqcc8458tlnn8nQoUNNAKNdZvVzcM+ePeInEyZMMIO2NWjQwAzYFpyWJjfLly+X9u3bS4UKFcz7Vsvoudu2bVtU9hnZsOLEAw88YFWsWNE6cuRIaF337t2tzp0752uZeLRhwwYrEAhYH330UWjd1KlTrYIFC1pbtmzJtkxKSkqWdXfeeadVq1Yty2/atm1r9evXL/T4wIEDVunSpa3x48fnWu7w4cNW06ZNrXfffdf8vPXWWy2/ef75563ixYtbe/fuDa276qqrrGbNmuVYZtmyZeZ6nTlzZob1hw4dsvxk586dVuHCha3XX389tG7u3Lna29RavXp1tmXWrVtnfr9w4cLQuuTkZLPus88+s/xkz549oetGj3/69OknLHPxxRdbnTp1Cj0+duyYVa1aNWvEiBER3Vfk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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" ] @@ -200,7 +482,6 @@ "cell_type": "markdown", "id": "11", "metadata": {}, - "outputs": [], "source": [ "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." ] @@ -209,7 +490,6 @@ "cell_type": "markdown", "id": "12", "metadata": {}, - "outputs": [], "source": [ "## Scalar overview and time series\n", "\n", @@ -220,20 +500,50 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "13", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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X+92snXr1iDPDkkhLAIAgFSndxwJJMwFuj5SD5NyAwCAVKWhT+8+ovP66WTTxYsXN/eFVj///LO5K8nZs2fN3Uv69OljXV/vJ63VxoiICClVqpS5k8ktt9wS0LnonVEmTpwohw4dkqZNm8oTTzwhkZGRMnv2bDMXYcGCBeWHH34wt9174YUXzHH83VYn9tYJtfv27Su33XabeQ+LFi2ScuXKSatWrWT+/Pny7rvvyoYNG+Sll16Szz//3L3vVatWyfjx483dZMINlUUAADIol8sl5+LPpfpDj5OU6OhoczcSvafxiy++KIMGDTLLNfx17NhRbrzxRunWrZuMGTNGRo8ebV3fea4hTm+Jp9v88ccffl+P9evXm/BZvnx56d+/vyxevFgGDhzovi+zBtHt27dLv3795NSpU9KrV6+AttUwOWDAAKlTp455L++8847ce++9Jiw+8MADsnbtWvf7+O2332TlypXu/b///vtewTScUFkEACCDOv/veWkws0GqH2dlz5USlS3K+rreHaRChQpy/PhxE7gcb775pgwZMsTcwk4VK1bM3Iv5mWeeSXT9a665xjyULtd7KX/88cdSv359v85Tq3mPP/64PPXUU+Z5s2bNTNXyjTfeMM9r164t48aNc/+9TJkycuXKFcmSJYvPbStVqmSqjg6tEmrFsWXLlua53udZw7HKmjWr9O7dWz766CNp0KCBnDt3TubMmSPLli2TcERYBAAAIbFr1y53dU5p6NOm59jY2ETX11Cl1boVK1aYyt+RI0dMSPPX5s2bZefOnfLLL7+Yaqg+NAzu3bvXvK4B1VGoUCGJj483x8yTJ4/PbatWrere9syZM3Ls2DGpVauWe5mGTycsKq00NmrUSN566y0zmEcrltWrV5dwRFgEACCDyhWZy1T90uI4vmg/w4Sc+xI7NADqelqJTGx97SOo4ew///mP6VuofQA1xPlLj6f9IrW/oafrrrvOeo7+bqt9Fx1RUVHmuQZfPU/l+T6VhtzKlSvLV199JdOmTfNq8g439FkEACCD0vCjzcOp/UgqZDk0AGqzsScd9KF99S5duuRuutVqm4atxNbXPoHafNu5c2dp3LixbNy4MaDroU3c2ldQm361GVuDn4bNvHnzpui2WbNmNX0qJ02aZJ5fvHjRBMKEdDDPyy+/bJqfe/bsKeGKsAgAAFKdBsPV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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out.plot.scalars()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "14", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "growth rate of the energy: -1.3534170635343759\n", + "growth rate of the amplitude: -0.6767085317671879\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "t_fit = 2.0 # Struphy time units, like every time coordinate of this run\n", "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", @@ -253,7 +563,6 @@ "cell_type": "markdown", "id": "15", "metadata": {}, - "outputs": [], "source": [ "## Two-dimensional data\n", "\n", @@ -262,10 +571,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "16", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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AMC3CHgAAAAA+BYBDwAAAADfIuABAAAA4FsEPAAAAAB8i4AHAAAAgG8R8AAAAADwLQIeABHbunWrNGzYMMNpw4YNMn/+fPP//fv3W9+7NtueNWuWaSs5OdnKuiEy559/vkycODH4OJrnTUbt59Xxj/Z2AQCyRsADIGLHjh2TX3/9Va699lp59913Q6Zq1arJvn37zO+jcSFps+09e/aYthzHsbJuiMyqVatk165dOT62c+bMMYHE4cOHw3p+2vZtH//M1iea7wcAQPaKhPEcAMhQ9erVzQVeWu3atZOff/5ZYmNj2XMIW6TnTaSBRLTPy8zWh/cDALiLDA8A65YvXy59+vSRgwcPhnQdSkxMlP79+0urVq2ke/fu8s0334QspxeLga5xZ511llxyySXyyiuvSEpKSkSvH3i9H374wWShzjnnHOnRo4csWrQow+fr6+Z2vY4ePSpPPvmk6aal7QwaNMh070stKSlJ7r//fmnbtq20aNFCRowYIbt3785yW7JrN9xtDXff6jo++OCDct5555n1fPTRR0MyFjnZBrVy5UpzTjRp0kS6du2abh9ndN5kte3aTWzIkCHm/7oeul1XXnllyD758ccfpWfPnuY1X3vttXTtp37drPbdlClT5Nxzz023vu3bt5fJkydnuz4Zve769evN9jRv3lzatGkjDz30kAmYAsJ9zwAAwuAAQIQ2bNigfYCcSZMmZfj76dOnm9/v3r3bPJ4yZYp5XK9ePeedd95xlixZ4txxxx1OsWLFnDVr1gSXO3z4sPPzzz+bafny5ab9GjVqOPfcc0+mbWck8Honn3yy88YbbzgLFy50Bg8ebF5P2037PBvrNXToUCc+Pt75+OOPnaVLlzoTJkxwWrVqFfz9jh07nNq1azsNGzZ0PvnkE2fOnDlOhw4dnKZNmzrJycmZbkt27Ya7reFsw19//eXUrFnTadasmfPhhx+ath599FFnxIgRudqGLVu2OGXLlnV69uzpzJs3z3n//fedU0891SlcuLDz1FNPZXpss9r2ffv2Of/5z3/M8/V3ul2///57yD6pVauWM3nyZOenn35ydu7cmel5md2+e+6555y4uLh026XLjR07Ntv1Sfu6up+1vYsvvtjsQ92X9evXd1q0aBHcj+GemwCA7BHwAMhxwFO9enXnzDPPDE56QaYyu7CcP39+sA29sKtU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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "phase_space.struphy.plot.slice(\n", " x=\"e1\",\n", @@ -280,17 +600,27 @@ "cell_type": "markdown", "id": "17", "metadata": {}, - "outputs": [], "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "18", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAB44AAAFACAYAAABDU1H7AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAApsBJREFUeJzt3Qd8FMX///EJASEQOkoXBCkKIoJ0FBt2RbH3BvaCvbefvSG2r733CurXig1BpNhQQIoISLUgvQnJ/h/v8Xv5310ut3PJ5rJ3eT0fjxNzt2W2fXZ2Zmcmx/M8zwAAAAAAAAAAAAAAKq0qFZ0AAAAAAAAAAAAAAEDFouIYAAAAAAAAAAAAACo5Ko4BAAAAAAAAAAAAoJKj4hgAAAAAAAAAAAAAKjkqjgEAAAAAAAAAAACgkqPiGAAAAAAAAAAAAAAqOSqOAQAAAAAAAAAAAKCSo+IYAAAAAAAAAAAAACo5Ko4BAAAAAAAAAAAAoJKj4hgAACDL7LXXXmbo0KEVnQxkiEMPPdQcd9xxGbfssPvvf/9rOnfubCZMmFDRScla2XZ+Zdv2VBa77LKLueCCCwJf7vnnn2+GDBkS+HKzxYYNG0zfvn3Ns88+azJRZTy+mX7MAAAAKgsqjgEAADLAiSeeaCuhIp+ddtrJHHDAAea+++6zBXHRZsyYYebOnVthaUVmmT17tpkzZ06p599///3t+Vkey85kK1asMNOmTTNr1qxx2ldltXbtWnPzzTebAQMGmJ49e5pTTz3V/Pjjj+WyjJtuuikmHkV/3n77bZMumXJ+ReJ3QUFBuWzPrbfeWuLxiP888MADpqx0blx00UUp/1ZRdN1p20855ZSEv1966aX2d12zpfHzzz+b+fPnmyBNmjTJPPjgg+bII48s9tsPP/xgrrvuOtOrVy+nl1O+++47c9ZZZ5l+/fqZ3Xff3Vx//fVm+fLlZU5jqukIetk1atQwe+65p7nsssvM6tWrTSYp6fguWrTI3HHHHWbw4MH2Who0aJC9vleuXBnIejlmAAAAcFHVaSoAAABUqF9//dVWQsUXAL7//vvmxRdfNF9++aUtRAXSbdasWWbVqlUJfxs1apSpUoV3VV32VVlomWr1GF3JO3nyZBsbPvjgA7PHHnsEugxVbsTHo4ggKqSyNX57npd0utJeL4sXLy7xeMT7/fffTVlNnz7dbL311in/VpHXnSrktY/0MoTO82gLFiywv23evNmExTXXXGO6dOli9t5775jvzzzzTPPoo4/GfBf9ckq84cOH24rxwsLCou+++OIL+4KH8hCllWo6ymvZw4YNM3feead9IeKqq64ymSLR8dXx6N69e8yxknfeeceMGDHCfPbZZ7YSvbQ4ZgAAAHBFKQ4AAEAGUcHiTz/9ZFurPPLII2arrbaylTtPP/10RScNKGbbbbc1bdq0Yc+UM7USVoWvKh30MsnEiRNthco///xjK8r0b3ks49tvv7XxKPpzyCGHlNNWZr/SXi9XX311seNQr14907Jly2Lfq3vcyqhRo0amdu3a5oorrjBhp5bnn3zyScLeCdQrwI477mhuvPFGe10m8+GHH5qLL77Y/v8555xjK4x1XasFa61atcqUxlTSUZ7LbtiwoW1Rrorm+ArXTDu+6j2madOm9hx98803bT7v1Vdftfvizz//NBdeeGGZ1ssxAwAAgCtaHAMAAGSQTp06mapV/83C9ejRwxaGH3744WbcuHG2K8po69evN7fffrstoFQrNnVtfckllxTNL6oMUsGkKorUIis/P98MHDjQnHfeefb/41tl3X///eb777+33UKqkuOoo44yBx98cMx0S5Yssa1/lCYVVGq6M844w6nVYyrr0ljO22yzjbnnnnvMLbfcYltdb7HFFrZrR6W/WrVqpdrOyDY89NBD5quvvrLb0LFjR7sN6uoz6G11WcZBBx1kNm3aZCsC4o0fP96cfvrptvLomGOOKfpelYBarv7NyckxXbt2teNwbrfddr5pUqumww47zBbcR1MrNa1HLypoGp2DOlbahuiWUGPGjLEF+hqztWbNmrbVajTXtEWOsc4Fv3O5In3++ed2e3777TfTtm1b23Vq/LXot69KS90f63ioIkjnt14mEXVzunTpUvPKK6+Yjz/+2Bx44IGBL2P77bcPvKeDVK9Vl+OhFpfaDlXIPPHEE6Z3796BXCdBK+l68YsRqmzSJ1pubq6Nh9HnmioMX3rpJfuykVo3P/fcc7b1+FtvvWWXqViiF5I03IHip85ZvTzQunVrO7/O3/3228+ez6NHj45Ztlqll/Tb1KlTY1pfa1tUKaZjrdiqY6tzLZqOg5an61zXvmKx0qg0l0bdunXNCSecYG644QbbgjP+vpWIa1qDpuOv1umJXsLQ8YlU+uplD79WraL9p2sgyO7EU0lHeS9b143OZ+UBdtttt1KvN3LOqYJW61Ulu2KO8h4aizio3jNKOr4agkQxK3o9ugZ1H2zevLnND5VFNh4zAAAAlBMPAAAAodevXz/1cept2rQp5vuvvvrKfn/YYYcVfde8eXNvt912sx/9Fv05/fTTY+Y/8MADi02jz8477+xt2LChaLolS5Z4TZs2TTjtww8/XDTdpEmTvAYNGiSc7qGHHnLaVtd1aTsHDBjg9ezZs9h0Bx98sFdYWJjydsrXX39d4jaMHz8+0G11Xca1115rv5s8eXKxZZx66qlebm6ut3jx4qLvRo4c6VWrVq3YMmvUqOF99NFHMfN36tTJ69WrV8x3mva0004rtq6nn37a/vb55597q1evTphufXQMS1p2KmlL5VyuKC+//LKXk5NTbFvOP/98+/+jR4922ldXX3213V8unyuuuKJo/dOnT7fLOfzww4ul7YMPPrC/RU+fSKrLOOOMM+x3xx13nNe9e3dv991396655pqibSmLVK7VROfXSy+9lPB4nHfeeUXHozTnYmmPT7L4HS/R9pQ2zjRs2NBr27ZtzHfnnHNO0bGMXs6UKVO8xx9/vNh+06du3bo2DTJ79uwSz+Nkv0V8/PHHXs2aNYv9rvj12muvxaRV3x1yyCFex44di6br06ePVxraD/roOtxqq63sfi4oKCj6/aijjrLL//PPP0uVVu3rQYMGxXyna8T1fLn77rtj5tV9rVGjRr7bddNNNxU7pyPmz59vf2vcuLHveVdWydKRjmXPnDnTTnPdddeVaV06tjqO7dq1K3bchw4dmvbjG61ly5bejjvu6AUlW44ZAAAAygddVQMAAGSoZcuWmZtuusn+f/v27WN+UysOtQ579tlnbasZtSZUq8Ynn3zSdnkYoRZ25557rm1xplZoaomlMfe++eYb2zIv4r///a9t9abWzWpNpmnVCnHw4MFm48aNdhqND6mWOStXrrSt1NQ6VC21Hn/8cdtaRq14NBanH5d1RYwdO9ZO+8wzz5gJEybY7VPLO22H5kl1O9WqTNvw999/29aVI0eOtPvvhRdeMP379w90W1NZRqRLy/jWdmrd98Ybb9gWSZEWh2qReNppp9kWymqJrHERtWy1tlNXmCeddFKx/Vgaahmprm/VHa5aSkV3h6uW8ImUJm2u53JF0Lib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Rub//PPPA93WVJexefNmuw0tW7a0v1933XUJl3v22WcXLaNx48belltuWfT3ZZddFjNtp06dvF69esV89/DDDxcdg1atWnn169e3fw8YMKDYfjjttNPsd3Xr1rXHQ8v766+/Slx2KmlzPZcrUmT79VF6dRz1/3369LH/Kq0u+6osvv/+e69GjRp22Q0aNLDnbCRN11xzTcy0Ome03kmTJpVqGbNnz445Z9u0aeNVr1696Lt77703ZrkvvfSSXd/zzz/vux2pXquJzq+TTz455nhEzt3evXvbfz/77LNSnYtBxG+lN9Hn1VdfLXF7ShtntM/i99c555xj51m9enWx6QcPHly0zGbNmhXtN32GDx8eM+0BBxxQdJ5EzmOX36644oqiZSp92pZatWoVfbdgwYKiaXNzc73jjjsu4bbdf//9drkffPCB50L7IX5fyMSJE4vWrc+ff/5ZqrTq9+h7U1lF4tsbb7xR7Lc333yz6LzZaqut7HStW7cu+k7XZ8SUKVPs9aNpdDy1D7bYYgv7t+6tf//9d6n3aSrpKM9ly7Bhw7y8vLyY7SnNenXOdevWze4jxUKtU99p/R07dvTWr1/vlefx/e9//2u/1zpLihUbN27M6mMGAACAcKDiGAAAoJJWHMtzzz3nNWrUqKgwPFKhpArC6IL26dOne6eeeqpXu3btmErVM844o1jB3+uvv+517tw5pkBelUu33Xabt2bNGt/0u64rsp2qxNx6662Lpu3Ro4ddRjTX7Yx4/PHHbYVpZHoVcJ577rneihUrAt3W0izj4osvLpouvjA2uoL52muvtRWUkWlVkaNK8cLCwphpE1VW6TxTJWek0Dw/P9+78847vaeffrpYxfHcuXO9Ll26xKR/yZIlJS47lbRlQsWxCvLPPPPMopcWVOFw5ZVX2nMuvuI42b4qK1WIqnI1slxVLqqSOH6fRiqvo49hKsvYsGGDd88993jt27eP2Q5VEr744ovFlqlp9fuTTz7ptB2pXKuJzi+lb+jQoTHH46qrrvLuuOMO+/c333xTqnMxiPhd0kcvapS0PaWNM6lWHGu/nX/++TGVo6q8/89//lNsWlVIatnR6XH5LRJb9UJA5De9nKJz57777vP++ecfp4rjSy+91M773nvveWWpOJbDDjssYcVxKmkNuuJY554qqg855JBivylNyc6ln376KWb6cePGeV27di36Xft1//33t7GoLPs0lXSU57J1DeulD70wEi/V9UbOubfeessuM7K+3Xff3fvtt9+88j6+I0eOTLrd+kRXXmfjMQMAAEA45Og/5dGSGQAAAMGZO3euWbt2bbFxOxOZOXOm7WKxdevWMd9rfi2nZcuWthvLCHUBre5n1T10pMtTTbd58+Zi488p67ho0SLb5WWTJk2KdVMaP4bd33//bZeprnBT5bcudUGsblU/+eQTuw0aM1DbXVK3ralsZ8TixYttN7ytWrWKGfc16G1NZRnqzlfbqq50tf3JaNs0rfahuhVOdLx++eUXu22JugFW19/qqlP7Oi8vz3atqX2oLrIj3YhHj+OoblC1n5UupS/Zsl3Sluq5XJHWrFlj94HGD9W+0b5Tt6eu+yooOmfVVbv2jbqYTvS7zrNE6XJdRvS5uHTpUjvObklj7Q4ePNh8++239lj6dWWe6rWa7PxSN7JKm8ahrlmzpu1q+91337XncPw543IuBhG/S6I0alzxZNuTapyZMWOG3Y7o/aXzTsvYfvvtS4xn6hpc5632vY5/svFVdYx0nitWx9+bkv0mGkNd508ktsTT+LE6Tom6mu/bt68dH/W7775LGpfjx95OFOcj16lst912CY+9X1q1r3WORbpuD4K6CFd38Eq77j8Rihu6L5ZE2xgZFzya4rjOfZ076k68rPs0lXSU57LVDf2xxx5rpkyZUmw86lTXqzh89NFH22EGIjFB19lWW21lgpbo+CpmqQv6ZLSNkWsyG48ZAAAAwoGKYwAAAGSk6IpjAOGjyheN7/nQQw+Z4447Li3rHDNmjK0cHTRokK0E1Fi7Gq9U4xP37t3bfP3112lJB8qHKj91Tmks6iDHQw8bvTyhsaZ79OhhKxkzdZ+W57K1j/RSgsYMfuSRR8q83uiK47Af32w8ZgAAAAiP4F5tBwAAAADgf9Ta9KuvvrItXNNFrWqPOeYY27pNLXnVCk69BqiV3o033sixyXA6rj/99JNtHZzN1Mpz9OjRtiVnJu/T8ly2XkxR61X1npDO9Ybh+GbjMQMAAEB40OIYAAAAGYkWxwDiqXvqYcOGmZEjR9pul0WVFHfddZc57LDD2GEAKrzFMQAAABBmVBwDAAAgI5U0/i0AqIvqyLjnGncaAJJJNq42AAAAUJlQcQwAAAAAAAAAAAAAlVyVik4AAAAAAAAAAAAAAKBiUXEMAAAAAAAAAAAAAJUcFccAAAAAAAAAAAAAUMlRcQwAAAAAAAAAAAAAlRwVxwAAAAAAAAAAAABQyVFxDAAAAAAAAAAAAACVHBXHAAAAAAAAAAAAAFDJUXEMAAAAAAAAAAAAAJVc1YpOAJBu69evN4899pj54YcfTP369c3xxx9vunXrFui8QU9XXjIlnQAy0+bNm817771nRo0aZWrWrGkeeugh53kVb9555x0zf/5807JlS3Psscea9u3bx0wzc+ZMc/311xebd//99zcnnnhizHeFhYXmxRdfNGPGjDFbbLGFGTRokNlnn31Muriuv6LTCSAzzZ0717z22mtm1qxZpnHjxjZ29OrVy3e+1atXm6FDhxb7vmvXruaKK64o9v37779v3n33XbNp0yazxx57mGOOOcbk5OSUerrykinpBJBZ1q1bZ0499VT7/4oVjRo1ss/Fxx13nM23JfP999+bO+64I+FvO+ywg7n66quL/j7zzDPNihUrYqZp2LBhwrz0pEmTzEsvvWRWrlxpunfvboYMGWJq1Khh0sV1/RWdTgCZ55ZbbjE//fRTse+feuopW77g55dffrHTLlq0yHTo0MGcccYZNpaW93TlJVPSCSA70OIYFUYPPbfffnvaH/T69+9vnn32Wftvbm6u6d27t3nrrbcCmzfo6cpLpqQTQNmpAvacc85J665UpW7r1q3tA8uvv/5q3n77bed5L7roIlvhsXTpUtO3b1/z22+/2QK15557Lma6P//807z66qtm1113NYccckjRR9PGO+qoo2yB3E477WQrog8//HD7IJouruuv6HQCKJtvvvnGHH300Wbjxo1p25UPP/ywadeunX3hRjFT6x4wYIC57rrrfOfVtIqj2267bUwc7dOnT7Fpr732Wluxus0225gdd9zRXHrppcVe0klluvKSKekEUDa///67jbcqIE+XatWqFcVJvaDTqlUrc9NNN9nnYz03J6OXeqLjrD4777yzjcGrVq2KmVYvXVavXj1m2kQvEr788stml112sRWwiv962Xv33XdP2z3Idf0VnU4AZac80ueff57WXan1/fHHH8Vip2KxH72sovzdkiVLbLzRsvSMrb/Lc7rykinpBJBFPKCCHHfccd4+++yT1nXecccdXn5+vvfnn38WfXfBBRd4TZo08f75559A5g16utIYO3asd+6556Z1ewCE12233eY1b948retcvny5t3TpUvv/55xzTkrrP/nkk72vvvoq5rvTTjvNq1+/vrdp06aYWKeszIIFC5Iu77333rPTTZ48uei7xx9/3KtWrZo3f/58ryzmzZvnHXXUUd6GDRvKvP7yTCeA9Bg5cqS9jlevXp22XX7ttdd6r776asx3d911l1elShXvt99+Szqv8ndK7+jRo5NON2vWLC83N9d74YUXisXgzz77LOXpSkP5TsXbOXPmhDqdANJj9uzZ9pr9+uuvQ5GORx99NOV5b775ZjvvjBkzYr5v3Lixd++99yadd926dV7Dhg29K664oui7xYsXezVq1PDuv/9+r6z0zP/FF1+Uef3lnU4A6VG9enX7bJpOe+65py0HKI0ePXp4Bx54YNHfGzdu9Nq0aeOdfvrp5Tpdadx9990xedJ0bA8A+KHFMSqEWkaMGzfOTJkyxb4lrM/IkSPLfb1vvPGG2XfffW2XUhHqdlmt2pSeIOYNejrRG3bq1urkk0825513nvnqq6+SpnXevHm22710bg+AcPrvf/9r3/L/+++/i+Lt/fffX+7rrVevnm1ZURr33XefbY0QTS2Qly9fbrcjVYpjHTt2tK06IrQf1C10fEvotWvXmkceecScdtpp5vTTT7ctPpJRmtRSRN2clnX9qaQTQPioG9J77rnH/r/ybLp+E3X3HLRLLrnEHHnkkcVipmKHurAOgvLpVatWtb0gRKgnGrW2U+xKdbrIcAbqtlRdsqrr16efftoUFBSUmAb9pnib7D5QHukEED7qdWbYsGFFvQco3qq76IqgHhvUknbBggUpz6vedJTnVVeiqVIrsmXLltln84imTZuaPffcM2Ec+/DDD835559vTjrpJHuvWrNmje8zRLJ7iOv6U00ngPBRvlbPu+otIFKmsHjxYhNW6rFs8uTJMXFHwwkcccQRMXEn6OkiNO1ll11mW2nfcMMNtvw0mbFjx9rniHRtDwC4YIxjVAiNA6QuODWumroZkWQPSx988IHtKjkZPaw988wzSaeZOnWqrQSNtt122xX9pm48yjpv0NMp8zBw4EA7tqdu/hqjQg9ZTz75ZJkejoNOJ4BwUvelnTp1MgsXLiyKtyoYL4mu65tvvtl3uQ888IDZcsstTXmoU6dOse8+/fRTOy7PVlttVew3jXOs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vEwv9qhQS8uJj6OPPfaYLXiKdH+tilzFwEglRirTqVWeKijit19/Rwq14ltkq4Weut/WdqplXyJBpxNAeKXaUjXIrqqjKY+qfOLcuXOd59FL8ZpHvSy4Uh43Pi6Lxs2MjDkaeVaPzjcq3k6ZMiVhvFWcLalFtlrHaTkllSm4rt91OgDZE3OD6qraJRYmou6tVUYcH2f0t3rlibyAHvR0ird6jld5Qvz+0suikXLr+JfCX3nlFftyUEnxNuh0AoATD6ggF154odetW7e0rnPkyJFeTk6ON27cOPv3pk2bvD322MPr3bt3zHQfffSRd9RRR3l//PFHyvMGPd3RRx/t7bPPPt66deuKvisoKPBeeOEFb/Xq1Qm38/nnn/datWoVyL5wnQ5AeD355JNerVq17PVbEc455xyvefPmCX+bNm2ajbffffdd0XeLFi3yttlmGxtnVq1aVeJy33zzTW/58uVFf2taxaeGDRvGzPfzzz97ubm53mOPPVb03dlnn+01btzYW7NmTdF3N998s9exY0fv999/j1nP6NGjvVmzZiVMw/fff+8pO1VSPE5l/a7TAQivr776ysaEX3/9NW3rVHzo06eP17p1a++3334rcboVK1bYePvuu+/G5HkXLFhQ9Pc///zjnXrqqV7VqlW9mTNnxsyr2HrRRRcVfXffffd5W2yxRcy2uk73xhtv2OmmT58ek0bdC8aPH58w/evXr7f7dvLkyUm3Mch0AgivlStX2pjwzjvvpG2dEydO9KZOnVosL5ifn+9deeWVMd8PHTrU+89//lNsGcrnKr932223JVzHpEmTvClTpsR8d+eddxbb1sLCQq9r167efvvt523evNl+9+mnn9rpPvzww6LpfvzxRy8vL8/G+2jz5s3z3nvvvRK3tW3btt7TTz9d4u+u63edDkC4tWzZ0rv11lvTtj7FKD2HR1MMrFKlSrF03Hjjjd5VV10V891xxx3ndejQoeg5fc6cOTZW33333eU2ncomGjVq5N1///0x8/7999/eyy+/XOK2Dho0yLv44ouT7o+gtwcA/FBxjAozYcIEr0aNGt5uu+1mC7HeeuuttKz38ssv92rWrOkdeOCBtoJg22239X755ZeYaR544AH7IDN37tyU5w16OlV+HHbYYV6DBg1sBbI+LVq08I4//nhvw4YNpa44Lo/tARBOS5YssQ8w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7zJrSP1ArmfXeOty38kGtRcWmOo1C8q879sOm+C0vvUOeUmXc7Buu15ZfRydYrI45c3d9kHBVIfnD4f9vs1lM5zWt7TPSv9phvnfV/M7+T8LVASXvHKQz025DvvB5dqS/NkOEzk8MtSd73YvzPXJX+RuWmWM22kVemu3qWs25yW5DwVYJtX+yb99p3GJ783GBNfEwjUmL0rz85pLzHUrC3FMk0Oe2mVZTnHbGNPqev/1LdrL7Vl6U7/V/hN97D9J3blu8cElXS7PVy4x0jVOBhm7Xe4VLs8Crsvy2w823rqdVpVCkM90Lnko1/tA/iyHZ1vH+8naVoWBPGO5xG3X69XlecEl/+AaJ13X51R25JAm53y+w7JcYmTD593O4/zZ/rFtyyd+d1qWGeKlrSzENf653HdclhP0fQAhkaUtjg877DDz7bfflvj75MmTzc4771yqZVNxDAAAAAAAAAAAACALK46TtTg2Gemll14y69ati/lu0aJF5qabbjK9evUynTp1KvWyqTgGAAAAAAAAAAAAkFU8799Pst8zUfv2xXsa6dq1q6003mGHHczw4cNLvewqZUwbAAAAAAAAAAAAAIRLYY7/J4s0atTI1K9f38ydO7fUy6DFMQAAAAAAAAAAAICsomrhnCStirOr2tjY7qrnzZtnatWqVeplUHEMAAAAAAAAAAAAIAvHOPb5PQNdd9115tdff435bu3atWbMmDGmX79+pmnTpuHsqlp9aP/888/luQoAQAXQTem2225j3wNAOVu/fr05//zz2c8AkAaXXXaZWb58OfsaAMrZiBEjzLRp09jPAIDy5+X4fzLQ6tWrzYoVK2I+1atXN1dccYUZNWpUmZZdri2O33//fdOlSxez3Xbblds6CgoKzA8//GDWrVtndtxxR1OnTp1SL2vpLvVNtdr1Sp5gYF+n5bR/YpnvNAWOacrt3MF3mib3jvedZs6I3k7ra/+Ew0Nyjv+FtGivhk7rqzvPf0+saVffd5pa890ubpdlmXa9nJbVbEyhCcKigY1MUJqP/sthKod9YIxZ2To3sGUtHuD/jsouffwfGGZNcFtfszHBvCaUP3uFCUreyIm+06w/1O3cC4M//vjDvP322+bKK68s1fy15q0yNaoln2bRwIYBxZAkcT3F4918dHDxfemFfQNZn3PMPcT//Mob5X+eyoI3dvCdptEL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+qz9k3+nrQzANZa65pWd4rLDvWnjuhXGfPi+0zpRzirZGMdBKFWTYI37o/GF1bpXfWSfddZZae9CT+MnR7cajtB3fuNkXHTRRbagLvKZNGlSOaYUADLLFVdcYR/yHnroIbP99tubt99+u9TLIt4CQMkOOugg8+GHH5ovvvjCtGvXzjz88MOmoKB0lbjEWwAombpp++abb8wff/xhe3i44YYbzPr160u9y4i5AFByzw0qM9Y48j179jSnn366+euv0r9sQLwFAJRZocMnQy1btszcd9995rzzzrMvbkV/NAxPaZW6L2m9ofv666/bLk01prAKux588MFSF3alqlOnTnanxGc+Zs6caSs6/MaRUwYm8mna1L+lIwBUFhpn6MQTT7Tx9KijjrKfffbZx/z8s38PCPGItwCQnFrAqctqVWJcc801pnv37ubLL78k3gJAwNRT2pgxY8yzzz5rnnnmGVuZrDKN0iCPCwAl23rrrW131Xo58ttvv7VlxirUVu+RxFsAQLqpm2q/TyZasGCBHYZHDcD0jPPdd9+Z//73v3aM48mTJ5uNG0vfA0KZByHeddddbSbg9ttvN//3f/9nH8Y+//xzU972339/2/L51VdfLfpu3rx5thJb3U8BAMomPz/f3HrrrbbCWP+v8RH0tu/KlSvZtQAQIA21cu6555rZs2eb/v37mz333NMcffTR9iEAABCsI444wraG01v4GoJr9913Nz/99BO7GQACpnytCq41RMBtt91myxQ++eQT9jMAIL08h08Guuuuu8x+++1nn23at29vHnvsMbNw4UJzySWX2KHQ9F2FVRzbhVSpYrse0biYqlxQpW55a9KkiW2ZoS5V9daa3mQbNGiQ6dWrlzn++OPLff0AUFlss8025rXXXrOxVm8wqXcJAEDwGjRoYB544AEzcuRIW6h22WWXsZsBoBzUqFHDXHvttbY1nF7SUW87AIDgqcz4tNNOsw19NLSgCrgBAEirLK04njZtmjnppJNs76G63/7zzz+2YcIdd9xhpk6dan7//fdSL7tqaWecMmWK+fHHH+2buZHPokWL7G/q/jkdrrzyStsttSo0ND6RKozVWkM7BwBQOn///bftSUJxPRLnp0+fbjZs2GDjq8aYBwCU3bp162y8jc9Tr1692mb8VZEMACi7TZs2me+//z4m3ur/NfyVqKcHAEAw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8XJycryTTjrJC7NvvvnGa9y4sde7d2/viCOO8GrWrOldffXVTvNu3rzZO+uss7w6dep4hx56qD3OnTt39qZPn+6F1bhx47yGDRt6/fv3t+nVOXnrrbc6zXvfffd5NWrUsOey9lXt2rW9gw8+2Pvnn3+8MNI1quPTpEkTG3MUmpcsWeI8/yeffGKv9913390eX+2rESNGeOm0Zs0ab8CAAfaaVKxs1aqV17dvX2/lypXlOm9FWbFihb0WFXOV5ubNm3t77LGHt3bt2qTzrVq1yjv88MO96tWre/vvv789L3WunnHGGfY6Das///zT69q1q7ftttva7VUsOuCAA7wNGzaktBxdy4q3itthtmjRIm/77bf3OnbsaPMAjRo1ssdt06ZNTvPffffd9p607777esccc4zXoUMH76OPPvLCau7cufbY6r6g9NavX9878cQTnfI87777rp2+R48eNu/RtGlTb6eddrLnTFjdcccd3tZbb23PacXb0aNHO887c+ZMG6O0jdpe3VcVv9N9zzj11FO9evXq2eOlfEybNm1sfq48560oio3K5ylPoOsxcm0uXLjQd95zzz3X3hN32WUXew1ru/fbbz8bw8NKeVPdG7baaisbb/U8ouP0+++/p7Sc6667zsZb3W/CbN26dd7AgQO9Zs2a2e3V+ah48vfffzvN/+abb9rnNB3j448/3ttuu+28Rx55xAsr5QOUVsUgba/+1d+rV6/2nffHH3+08UfnhK5f/au/9X1YvfTSS/aa1X1f8fbee+91nlfngM4FnRPaVzpHdK7onEn3PUNxZPDgwd6uu+5qz7cvv/yy3OetKIodes5UzOzTp4+NRZMmTfKdT8dWcTpSFqF8ve6Vv/zyixdml1xyic2zHXnkkfZ803k2ZcqUlJbx+uuv23irc1xlMmEWKRPQMdLxUQxR3saFzoNIeZPirea/6KKLvLBSPlb5WeVTFTOVz1V+V/leP8rHavuUr9W+0rmh5SjfG1Zjxoyxz9C6N6j847TTTnOeV884uub1zKO8lvJZit2LFy/20um1117zatWqZZ+h9KypWPTCCy+U+7wV5ZlnnrH3iAMPPNDbZ599bPqVr3GJObp2dX/U+anjpWtTZWhh9vDDD9vtHTRokC2r03n6/vvvp7SM7777zttiiy1svA3z9RgpE9D2qoxO5V2KIZ9//rnTvPPm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kCqylDcpT1qPzqdk12N5zFtRdL2p0DKoNCs/oBcinnvuOZOp8TaZzz//3Dz66KO2IiO6YC6syrK9qjhUjFKeVpU2u+yyi61AVsHaM888YwYNGmQycXuTFZA99thj5sgjjzTHHXec6dWrl5k4caLN22fK/TXofZWOF0CUDt3LS/NMUpZ5w/RcoYJbPU+lmubJkyebl19+2ZYrhJW2KX57VWaiwnq/7VW80bbpvqLn9LDTixirVq0q9f1FeVyVH+hFgp49e9p8n+LvbbfdZi688EITNqqA0D2itNt7880323irl+9VjqAXe1SBo3xveef10i1SflbR+cOylAGVZd6KojQnegmhNGnWSxz33HOPOeigg+zLSWHdXlUcl2Z7FW9OOukkc91119kXsOJfosu27VW8jTQcUkMnvQRx/PHHmyFDhpgHH3zQhFGia1Bl8ipH8Nves88+25ad7LnnnnY7VeeiZzWVI6VSrpYpEu0r3Vfy8/NDG69QeWVtxbEKmSS+8jBSiJKsBU1Z5q0oQab5mmuusW84vf766wkzX2HZ3kQVw9peZXr1u260iag1ggqUMqVQzW97/Y6tXnbQ/CosVksV7RdVBOihTw95r776qsmm7RVtoyo0dIyVcdb26oFe+yLblHVfBZUGId6mHm/VQuquu+4yN954Y7G3DsOiLMdXBfiXX365LWTJhEK1sm5vJMboDVm1llLmX6211ULjhBNOSNp6LlPzD7rn6tjq/qLW52pxrbeLszneVmT+kHhb+n2uSja95KCCYF2PYVSW46sKgVNOOcVcffXV9s39yhJv1epYBYhbb721/fuMM84wJ598spkzZ45tGZZN8Vbz68Uc9X4h+le9zoTx2bSswvI8XpZ8dhjy6KkKKs3qTWDw4MG2VZzyuGEUKTMozfZqPlVi6CX7ML6kkso1FcnXuJQpfPrpp2bMmDFm1113td/pZW29iL/77rsX6xkt02OItldlYcrTT5gwwVbMqWFFZLnZJEzxVr0blDbelnbeTI+3qlRVHNILL4888ogJq7Jsr8otlae75JJLTKYoaxmuXj7XC4NDhw6133388cf2RZYBAwaYI444wmTb9qo8QeW2el7TM43KczOlvD5VmZg/ROWVnVehMUUFBQo80fS3LlC9NVse81aUZGlWwFXhtYvhw4fbVkJ6s+ewww4zYaXtVUWoPvHbqwx9SZXGkbdHVUGj7prVKkwftcJRBYf+P4xd3Gl7dfPUA2789voVikVaiunttMh+0U1ZrRtHjx5twkjbFH8uu27vu+++awuGH3/8cfsChCrG1b3Se++9Z1tdZ5uy7Ksg0xBZZ3waon8Pet6KElSadU6qcFtvWF577bUmrMqyvbqnqFB72bJlRfFWLTRUwaj/V8F+Nm1vJN6qNVjkvqueItRVtwr41QVTJm2vHl6SVfirK3kVVOj+ooJEvQihynG1ElWBcbbRvtJ9WHmG+H2lN8E1xEJ5U08rkRfA4tMQSWN5zFtRInnusqZZFYsq4O/fv39oWxuXNf6o1ZuuPcWhSLxV/kcFEPp/tf7Lxni73377FVUaiyqONX8Yu3ory/aq96i9997btkhWq3kNhaB/1fJGBYn6PZuEJX9Ylnx2GPLoqSopzbrvuaZZcUgvzCkf9OGHH9r7YxhFynXit1cv/GmIkWTbq5a2X375pX0ROxJvVakqevleBfxhozyKWjTFb6+OrfI2LmUKeikpUmkskQoNvcATNsq/Kh9b2hiifKzOZeVrlb9VPve0006zQ++EcSilsiDeVtx+L+s9QuWhehbTi7sq2wtzN+ql3V61wFSM7devn3n66aft/6snL1GsVczNtuOreBspQ4hQHnCbbbbJyjLcc889126XKstVfvvBBx/YOonTTz/d/n+2SbSv9Lyml0DCmj9E5ZW1Fcdq7SLxrXv0t7p60NtY5TFvRUmWZmXwXd7U0Q1Yb3Cpi5Owd+kb2d7IG/fR2xv5rSQqOFShi94cjXzUdZMeDPT/YSx40Tapq6X4cS1ctlcPtBL/0K6/tcz4yugw0DbpRhpfyeKyvXqo04Oi3nyOUKHAHnvsYX/LNtof2k/xGQ+XfRUUjcOj8ylR/FGGN1m3i2WZt6IopqryJVGaVSjjMm66xr09/PDD7Vjrels/zJLdX3RtNW/evMR5e/fubbscio63qkRWnNX/K+6GTWTMqETbq24xk1Wkal/pfpso3oq6Ms6k46sKiehxt+KNHz/eviEcXUms61Zd+qm7qfiXuzJdsn2Vri71I3ExURqi0xj0vBVFld3qGr4saVb3kqrE0LSjRo0KdReTKvBTF3al2d4OHTqYY445xva2Eom36s5Z+Tz9v1oAho0KwFS5lGh7FUuTjd2r/aThVjIp3uoYKaYm2l59r99LouFH1J2oKi2i6UVffZ+u4UnSRcde50CifaVzRudOOuiai0+DCrGVl/GLP2WZt6IkSrNe8tP93CXNeh5R4bYKQFWRGtbey5Jtr8oXFDeTba9eilQlovZNJN5GntP1Qoe6kA9jRbnyKqW9n6pMIT7eKl+hZ6Iwxlvd6/WcWZrt1fmufKzys9GtWBVv9WKB8r/ZJPI8V9H5Q61H1050iztdi7q/ucTb0s5bUZSu+PJMnV+KJS5p1jOYetFRzFEF6nbbbWcyLd6qTGD+/PlJt1f5I8XbSPmBPurpSnTMtf2Zsr16eV55Npd4G4mv0RSDwxhvS9peHVsdN5cyXJUZRQ9FqZciNXREtpbhxu+rGTNm2PgV1niFSszLYp06dfKOOOKIor9XrlzpNWrUyLv66qtjpnvrrbe8999/v1TzhkVBQYG3zTbbeKecckrRd3/++adXu3Zt77bbbouZ9rXXXvNGjx4d890rr7ziValSxbvlllu8TLBx40avSZMm3nnnnVf03aJFi7waNWp4DzzwQMy0L774ovf5558nXV6HDh28k046yQurNWvWeA0aNPCuvPLKou/mzJnjVa1a1Xv66adjpn3mmWe8cePGFf29efNmr1WrVt4555xT9F1hYaHXo0cPb6+99vLC6O+///by8/NjzsepU6d6OTk53uuvvx4z7eOPP+5NnDix6O8HH3zQTqf9E70Pdtxxx9BubzQdT4XmJUuWFPtt06ZNdnu//fbbou8WL17sVa9e3bvvvvuKvtP+0DI++OCDtKX7yCOP9HbYYQe7ryMxqXv37t6gQYNiptO1qGuyNPOGyf777+/16tXLXkuRY7Pddtt5xx13XMx0irWKudF0fHR+a1ptaybYddddvT322KPo7w0bNnitW7f2zjjjjJjpdC/VPTWZww47zB7fMOvWrZt30EEHxcRg3XMuvvjimOnefvtt79133435bt999/V22223mO90f2natGnR+RI2ugcec8wxRX8vX77cq1+/vnf99dfHTPfmm296H374YdHfX3zxhY018fvg6KOPtvsr7CZPnmzTH58nis4bffLJJ0V/K0ZtvfXW3tChQ4u++/33371atWp5d911l5cu//d//+fVq1fPW7ZsWdF3J5xwgteuXbuYc2zKlCn2nqHrNdV5w+Syyy7zGjdu7K1atarou0MPPdTe16N98803dnsj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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "phase_space.struphy.plot.panels(\n", " x=\"e1\",\n", @@ -305,7 +635,6 @@ "cell_type": "markdown", "id": "19", "metadata": {}, - "outputs": [], "source": [ "## Interactive plots\n", "\n", @@ -314,10 +643,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "20", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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i+igOzk5Wbp162buRHTppZdK2bJl5Y033nB8W9j7g9KvXz9zvC+77DI566yzTA/k7OhF0csvv2wGyNepdu3a5mZHkyZN4nTkgeHDh5tz1apVKylXrpzceeedJqDLjY8//liSkpLMEE9w3oQJE8y5atmypVSsWFGuv/76HF8w6fi3N9xwg/k5veSSS8xtqz/66CPH97mgmzZtmlStWlWaNm1qjvkVV1xh7j6ZE8OGDTM3PGnUqJFpo27dujJv3jzH97kg06SRxg169+RatWqZY58bS5YsMX/LLrjgAjn33HPlwgsvlFWrVjm2v8gDVgS78847rapVq1qbN28281OmTLGioqKsadOmObotgvf8889bcXFx1vLly838woULraJFi1pvvvlmlttt2LDBiomJsd5++23L4/GYZS+//LKm36xvvvmGU+KgDz/80CpcuLD1ww8/mPm1a9daZ511lvXYY4/luI1t27ZZpUuXti677DLrzDPPdHBvoebOnWsVKlTIev/998389u3brcTERKtbt27ZHqBjx45ZderUsTp06GDt27fPLNu9e7f13HPPcXAdpD9X+rvw2WefNfOHDh2ymjdvbrVs2TLbbSdPnmx+F+qjOnHihNW1a1erZMmSvnOI0Fu3bp111113WYsWLbL69+9vxcfH53jb/fv3W2XKlDHbnz592pyz6667zqpRo4b5N/KniA269QNbpEgR69VXXw1Yfvnll1tt2rRxbFsET4PlcuXKWQ8//HDA8l69epmAICsaNMyZMyfd8mrVqlk9e/bktDioUaNG5o+BvyFDhlglSpSwjh8/nqM22rZta/64DBw4kKA7D3Tq1Mlq0qRJwLJx48aZQFx/lrLywgsvWAkJCdbBgwcd3kv4e+ihh6yKFSv6kgpq1qxZJpjWoC4rd9xxh1W9evWAZb/88ovZdsaMGRzoPJDboFuTRmeccYZ14MAB37K///7bnLOPP/7Yob2E0yK2vGThwoWm5EC/+vSn8z/99FOWNYh2tkXwtJZ3586dGR53rYXT5zLjLQPyp+dJv94rVqwYp8XB2vtff/01w3Om/SF+++23bNt46623ZPny5fLCCy9wnvLIjz/+mOE50/Iu/R2XlcmTJ8tVV11lbuaxbds22bdvn8N7C+85u/jii6VQodQ/29757MpESpcuLYcOHQoo+fLWhOtzcOf5rl+/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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "phase_viewer = phase_space.struphy.plot.viewer(x=\"e1\", y=\"v1\")\n", "phase_viewer" @@ -327,17 +667,27 @@ "cell_type": "markdown", "id": "21", "metadata": {}, - "outputs": [], "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "22", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=12, show_paths=True)" ] @@ -346,17 +696,10796 @@ "cell_type": "markdown", "id": "23", "metadata": {}, - "outputs": [], "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "24", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "animation = phase_space.struphy.plot.animation(x=\"e1\", y=\"v1\", step=4)\n", "HTML(animation.to_jshtml())" @@ -364,10 +11493,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "25", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote: ['frame_0000.png', 'frame_0001.png', 'frame_0002.png', 'frame_0003.png', 'frame_0004.png', 'frame_0005.png', 'frame_0006.png', 'frame_0007.png', 'frame_0008.png', 'frame_0009.png', 'frame_0010.png']\n" + ] + } + ], "source": [ "frames = phase_space.struphy.plot.frames(os.path.join(demo_root, \"frames\"), x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" @@ -377,17 +11514,27 @@ "cell_type": "markdown", "id": "26", "metadata": {}, - "outputs": [], "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "27", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAHGCAYAAACmS4sdAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAALIxJREFUeJzt3Qd0VGX6x/EnkISaEHpvAtIJUkQp0gVFqtKEZbGBgCCK/IUVbOAGXUEFlKKIoiAoICsgijRpCoiE3qX3GkogEDL/87y7MzsTkjAJSd4k8/2cMyfMzZR3boa5v3necv0cDodDAAAALMlk64kBAAAUYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRIIl+/fVXGT9+vMe2hQsXyrRp05L0eHdz39QUVzu93TZ37lz55ptvUqQNvurMmTPy7bffykcffWT2S1RUlHz44Yfyxx9/uG5z69Yts239+vVW2wrEx48VWJFRHD16VGbPnu26nilTJsmdO7fcf//9Ur58+WR/voEDB8rEiRPl+vXrrm1PPPGEbNu2TXbt2pXox7ub+6amuNrp7bZmzZrJxYsXPQ6UydUGX7R7926pV6+eVKtWzVyqV68u7dq1M+/7sLAwGTJkiLmdvkezZcsmI0aMkGHDhtluNnAb/9s3AenTvn375KWXXpJGjRpJaGioREdHy44dO6RHjx7SuXNn+eqrryQgICBF2/DYY49JzZo1JSPz9jWm5L7whf3sjTFjxkiOHDlk6dKl4ufnZ7Zdu3ZNXnzxRaldu7bt5gFeI4wgw3n88cflhRdecF0fNWqUDB06VOrXr++xPSX07NlTMjpvX2NK7gtf2M/e0MpQyZIlXUFEaQVEu2SA9IQwggxPD1waRpYsWeIRRlasWCHh4eGuLp2QkBB54IEH5N57773tMW7cuCG//PKLHDlyRCpXriwNGjSI87m0z/7cuXOmGpOU50nsgej33383JfhKlSqZNrkflOJrt4512bp1q8e+0PEXWtpv3bq1x/217Vpd6tu3b4Kv0dt94W7nzp2yevVqCQoKkpYtW5r9Ent8iY5/6Nq1q6l6rVy5UvLlyydt2rSJ87ET8xrcH1vbsWbNGilQoIC0atVKMmfO7Nq/ul3bp5WY7NmzJ/h643rcpL4+denSJVm2bJkcP35c8ufPL40bNza/d++SPHDggAQGBsYZPjR816pV645tvnnzpnnuvXv3miDz0EMPSenSpT1uExMTY26zf/9+U4mpUaPGXb9/AXeEEWR4zoOLDuJzpwezgwcPmn9rl45+0D799NPSp08fGTdunOt2+sHfokULOX36tPk5f/58+eSTT8zBK7apU6easQzuB0lvn8dbV69eNff/97//bdqjB+C3337bHEB0m/sBK752T5o0ySOM/POf/5QKFSrcdiDXA94XX3zhcSCP6zXGJaHb6cFz5syZJkTpAbtfv36m7XoAddK26viSs2fPytdff20Ofnrg1IN1XI+dmNfg/tg6+LNs2bLy448/mvsvXrzY/F3mzZsn5cqVk0WLFslbb70l69atk5w5cyb4mp2Pe+rUqbt6fRq2/va3v0mhQoVMcN2yZYsJ1Tpg+qmnnjIBVN9TGmY0KDjfX0q36VgmHTNypzCyceNG6dSpk3leDTvanl69epmxJvqalb5/9HdXrlyRpk2bmv9HOvZEx6pMnjw5wccHvEUYQYanBxXlfiBwdufoxZ1WER5++GFzQNOfSg8CFy5ckE2bNkmxYsXMthkzZsgrr7zi1fN7+zze0hCjByudGaGDFp2BRwfq9u7dW+bMmZMs7U4phw4dMpUareooPbBqWNJBqVohcD/g6231278GAXXy5Mlka8fhw4dNaFi1apW5vn37dqlataoJLXny5DEhwlkh0VChAW7QoEEp/vp0mwYEHeyrQUrHOTkcDvN3f+6556RKlSpmPIgGOm2jPp57ZUQDhYaRO9H3zCOPPGLeQz/88IOr8rNgwQLzvtTBsO3btzePrdUZ3V9a5VHaHg1tQHJhai8yHC3L6wfo+++/b75F6sGlbdu2MmDAgNtuqx/c+o1Vv6XqffSApB/+Wp5Xf/31lxkcqN9snQd09eSTT8ZZGYnPnZ7HW3pQ0G/RemByBhGVN29eM2jx+++/N1M9k6vdKUG7H9xndGTNmtV8C9dgoO13FxERIa+99prrulYKksvly5dds02UdmPpRffv66+/7tqu1RLd18uXL0+V1/fll19KZGSkqWw4B1xr95tWfrTKp6EoOWh1Sd8ro0eP9uiC0i4pDTvOqocGWuU+a0zbowELSC5URpDhOLtFtFz9559/mm+Ozz77rOkPd6cf+hpUtESvfeDBwcHmQ1YvWppW2hWgdHZObLrNm6ml3jyPt7TKod9K9SCiJXv9t9KfGnD05549e8w+uNt2p5TixYubrqXYbXLf305FixY1QSsl6MDP2N0uBQsWNLNRnBUA95Bw7NixVHl9+nfMkiWLVKxY0WO7Vmu0zbEfI6m0i0bHMOkYIq0O6XvH+X7Srhjne0SrMVpR0/evjqnR2WrNmze/bVwJcDcII8jQs2m0P10HCGqJfO3atSYMKD1Ya5dGt27dZMqUKR4DPnW8gPNDWe/vPu7Enb//nf/7ePs83tK+fee3VS35u9OwpdURHeyoYSUx7dbbOV+rOz0wJ7eE2hS7DYkJIol9DXGN/9B2xLfdue+9aUdc9/f29WkQ0MeIPRjZ+Tixxz4llb4efTytosWmA501NCv9P6MBV8fWaNVRqz76ntZuQH1Pa6AB7hZhBBmaflB+/PHHpn9buzacffM6u0IrJ86ZC04668X9gKEDGJV+S9T+dXc6W+JOvH0eb+nYBef4l8GDB8d7O+eB09t2FylSJM4qTUpUUHTsgQYE90qVs03O/Z0UqfkaUvL16W103I8+TokSJVzbtetGK366qFly0PeSPs+rr74qhQsXTvC2WjHq37+/uWgYevPNN2XkyJFm/R6dKQTcLSItMjydXaKDNnXAp7PPvlSpUuanTnF1P4DrmiQ6VdJJxxDcd999MmHCBDPGwElL2xo07sTb5/GWzrjQAa8ffPCBnDhx4rbfb9iwIUntrlOnjunScu+K0G/B3gSuxNLX7T6LSKtD7777rqlIdOjQIcmPm5qvISVfn1bRtDLyzjvveGzXsR0acpJrjZVnnnnGjBXRcTOxg7E+j7M7SPepezVG21a3bt3bxpEAd4PKCHyCdl+MHTvWlJh1MKv26z///PNmSqzOfNBvhjq1U8vPurZD7DEfOqVRB/V17NjRdJFo2Tp210tcEvM83po+fbo5qOnASu2C0sCjA1u1G0q/wepMm8S2W/fP559/bsYD6O91sKV29XTp0iXZzwGj3/z18fWxNTTpQFttu74u57TkpEjN15CSr09ny2iY0QHXWgnRKtjmzZvNuiQ6sDaxs6/io+NPNJzroGadOaMVNB0ro91/GuKGDx9u2qLPqxWQJk2amHEiOhVZ96e+tx599NFkaQtAGEGGobNG9ICkH6yx6bdSnR2gMyJ0nQ89YGjVQLtPdAqm9p1/+umnZqChfhg7x5Yone6p3651EJ+u3aFrN+hMHV1/IvZCWHEtU+7t83i7xLke0HQBKr3o1E4NGXruHf2m6z5gNb52u88gcdJxJnrA0wOmTi9t2LChGWejM4BiD+aMq53ebtPxPPqt++WXXzahTGcT6awM3SexuzCct41LXI+dmNcQ32PrVFadDRObBtjz58+Lt7SKcTevT7sU9X7adg2aGii1a0TDgTsNErErbDr4NfZy8Pq+021aPXKnwUYDj07t1YqZjmXSkKHLzOuAWaXdMRqef/rpJzO+RGdj6ZRjXRwNSC6cKA/wMXGd4A/JI7lOBAj4GsaMAAAAqwgjAADAKsaMAD5Gx1I4V/ZE8kpoHAiA+DFmBAAAWEU3DQAAsIowAgAArEoXY0Z0dUCda69rBcR1vgYAAJD26ArEugq0nq4hofMYpYswokFEV7IEAADpj65ArQtTpusw4lw9UV+M80ySAAAgbdMVjbWYEHsV5HQZRpxdMxpECCMAAKQvdxpiwQBWAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAQPoMI9evXzfnjEmMxN4eAABkfIkKIxERETJu3DipXLmyZMuWTWbMmOHV/T799FMpUaKEBAQESNmyZWX27NlJbS8AAPDlMKLhY/fu3TJr1iyv7zN//nzp16+fvP/+++bMfS+//LJ06dJF1q1bl5T2AgCADMbPoef3Tcod/fzkq6++ku7duyd4uyZNmkiePHk8qiEPPPCAlClTRqZPn+71iXZy5cplKjOcmwYAgPTB2+N3ip4oT3OOVkBGjRrlsb1x48aJqq6kCM1gNyPttgEAgLQiILtWGqw8dYqGEe2WiYyMlPz583tsL1CggJw6dSre+0VFRZmLe7JKdhpE/lkk+R8XAID06B/HRQJzZNypvbFn0ej1hE4nHBYWZso6zkvx4sVToZUAACDDVUaCgoIkR44ccvr0aY/ter1QoULx3m/o0KFmoKt7ZSTZA4mWozQFAgAAMcfFjBJGoqOj5datW5IlSxZT/ahXr54sX75cBg4c6LrN0qVLzfb46H31kqK0MmOpHAUAAJLYTaPdK7rYmV7UzZs3zb81gDiNHDlSChYs6Lo+ePBgWbhwoUyePFlOnDhhumC2bNniUfkAAAC+K1Fh5Ndff5WQkBBz0cpFnz59zL/79+/vuo2/v79kzZrVdb1Zs2ZmCq8ullahQgWZM2eOWXskNDQ0eV8JAADwrXVGUhPrjAAAkP54e/zmRHkAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACs8k/Knfbs2SOHDh2ScuXKSalSpe54+5iYGNm2bZucPn1aihQpIpUqVUrK0wIAAF8PI9HR0dKtWzdZtGiRVK1aVcLDw6Vnz54yfvx48fPzize4tG7dWq5duyb33nuvbN26VYoWLSoLFy6UwoULJ9frAAAAvtBN89FHH8nSpUtly5YtsmbNGnOZMmWKfPPNN/HeZ8iQIZI7d27566+/ZMmSJebnpUuXZOTIkcnRfgAA4EthZNq0adKpUydX10z16tWlRYsWZnt8Ll++bCoi/v7/KcLkyJFDSpcubbYDAAD4J6aLZvv27dK3b1+P7RpIJk+eHO/93n77bRNgRowYYcaKrFu3Tvbv3y/jxo2L9z5RUVHm4qSVFAAA4ONh5MqVK3Lr1i3JkyePx/a8efPKxYsX472fVkUaNmwokyZNMv/euXOndOjQQUqUKBHvfcLCwuStt97ytmkAAMAXumkCAwPNz8jISI/tet35u7h07txZTp48acaKLFu2TPbu3Str166VPn36xHufoUOHSkREhOty5MgRb5sJAAAyahjJnj27FChQQI4ePeqxXYOCjgGJi1ZSli9fLl26dHEFlpw5c8rjjz8uP//8c7zPlSVLFgkODva4AACAjClRA1hbtmwp8+bNE4fDYa7fuHFD5s+fb7a7T+V1Bo3MmTObAKNjRNzt27fPrDcCAACQqHVG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Vppx/vx5R5s2bRw5c+Z01KpVy1GiRAnHzJkzHRmBT1ZG+vXrZ5an1WSZM2dO+fDDD6V79+5y//338w3lvxWjr7/+Wn788UcZP3687T9XmrJ37155//335cEHHzTX9Ztt3bp15Z577pH/+7//E1938OBBmThxolSrVs1c379/v9x3333mMnDgQNvNSzPeeustKVy4MOfbisM//vEP6dmzZ+r/UdI4h8Mh7dq1kxs3bsihQ4ckT548cuXKFZk7d65kBJl8cU3/OXPmyEsvvWSCiHrhhRckR44cMmPGDNvNSxPee+8918EEnvRD0hlEVMWKFaVRo0ayevVqdpWIdOrUyeO9U6ZMGXPA1eCP/1ixYoVMmzZNJk2axC6Jw9mzZ+WPP/4wXRH4n19++UVWrlwpn3zyiQkiSo9hPXr0kIzA58LItm3b5ObNm1KzZk3XNn9/f6levbps2rTJatuQ/uh7afPmzVK2bFnbTUkz9JubhjP98NRqiH57e/755203K80caPXg8cUXX0hISIjt5qRJb7/9tjz77LOm2qhB/8CBA7ablCYsXbpUihYtaqqMu3btku3bt982viY987lumvPnz5ufefPm9diu152/A7w1dOhQMxBxwIAB7LT/0kHhQ4YMMVVI7abR7isdyAqRp556Sp588klzkEXcQUTDmn5B1ODWtm1b6dy5s/z+++8JnvHVFxw/ftwcp5o3by6HDx82Z7PXY9bHH38sXbp0kfTO5/66AQEB5uf169c9tl+7dk0CAwMttQrp0ejRo80HwezZsznYusmfP7+pjOhsLK0aTZgwQd58803xddo1o7NFHn74YbN/9KLfbPXA8ttvv9luXprw9NNPmyCi8uXLJ++8847ZZxpqfV1AQID5P9WmTRvZvXu3Gb+moV+7jvU9lN75XBhxThGL3Yet15leB2/poOdhw4bJ999/b76pIG7afdW+fXtZtGgRu+i/++P11183BxG9XLhwQZYsWWLOM4LbFSxYMM7Pa19UqlQp8fPzk969e7u2afenBtr169dLeudzYaR8+fJmQN0PP/zg2qYjk8PDwzmowCtjx4413TM6ir1ly5bsNTdXr16Nc3ZW7G5RX6TdD86KiPNSqFAhUw1YvHix+Lq43ju6XzJnzmwGivu6Fi1amBk12l3jdPToUVc1Mr3zuTEjmizDwsJMaUv/gDraX0uBderUMdOm8J9BvhcvXjQhzTkYUdWqVSvjLLCTRFOmTJEXX3xRXnvtNQkKCnLtG/13aGio+LpWrVpJ06ZNpUaNGhIdHW26sHQGAAdb3In+X9KZfN26dTMDNdeuXWuu65gjZ4XEl9WpU0c6duxoLjr9WYPJiBEjpF69elK/fn1J73z2rL0LFiyQqVOnmsF2OlVz8ODBEhwcbLtZaYJOe9Z+2thmzZplPiR82fDhw2X58uW3ba9QoYJ89tln4ut0/R6derhmzRoT/HW/9OnThzE18dADyyOPPGKqIxAzUPXzzz83X4S0gt21a1cTbvG/2Xu69pOGex3jqEGkf//+GWIFcZ8NIwAAIG3wuTEjAAAgbSGMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqn1uBFUDa8Mcff5hVWlXOnDnNasgZYfEmAInHomcAUp2eIC5PnjxSuXJlE0ROnjxpTkEwefJk6dSpE38RwMfQTQMg1f3555/m58KFC80S4AcOHDDnhnr22WclJiaGvwjgYwgjAFLdxo0bzbmgSpYsaa7reWyaNGlizm1z/vx5/iKAjyGMALASRqpUqeKxTc/SWrp0acmXLx9/EcDHMIAVgJUwUrFiRdNFc+3aNVm0aJF89913MnfuXP4agA9iACuAVBURESG5c+eWcuXKmZ/Hjx+XEydOyOzZs6Vt27b8NQAfRBgBkKqWL19uxods2bJFqlatKg6HQ3r06CErVqyQgwcPSubMmfmLAD6GMSMAUr2LJkeOHFKpUiXX4NWhQ4fK0aNHZfHixfw1AB9EGAGQ6mGkZs2aHhUQDSa65oiOGwHgewgjAFKVVkJ0TZHY+vTpYxY/A+B7GDMCAACsojICAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAADEpv8HHVmCw+8BK9gAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out.plot.equilibrium()" ] @@ -396,7 +11543,6 @@ "cell_type": "markdown", "id": "28", "metadata": {}, - "outputs": [], "source": [ "## Derived quantities\n", "\n", @@ -405,10 +11551,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "29", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "largest drift of the total energy: 1.586e-06\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2QAAAIgCAYAAADqVqoGAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAcaNJREFUeJzt3Qd0VNXWwPGdHhISkkBC7yBNjPQiggIWVFRARCkClie2p6J82J+KPXZs2EWe4AMrYkNEQOlKVQxFQKoJEEp6m2/tk8wwCSkzySRT8v+xZs2ddu+ZO5fJ3XP22cfPYrFYBAAAAABQ7fyrf5MAAAAAAEVABgAAAABuQkAGAAAAAG5CQAYAAAAAbkJABgAAAABuQkAGAAAAAG5CQAYAAAAAbkJABgAAAABuQkAGAAAAAG5CQAagwn788UeZN29ejdyD+/fvl8cee8zdzYCLbdy4Ud54441KrWP27Nnyxx9/iC9Yu3atfPHFF+5uhlftU0/ZZwC8BwEZgAojIPO8gIxA0b0B2c6dO2XKlCnSvHlz8QX6PiZNmiSHDh1yWxu8bZ96wj4D4F0C3d0AAL5h7969MnPmTLnvvvsqHeStXr1a4uLiZMSIEVKnTp0KPT8/P18effTRU57/0EMPib+/d/8WdeTIEXn55Zfl3//+t8TExFR4PcuXL5fvv/++1Md79+4tF154ocPrW79+vfks/vWvf5V5X1XYunWrfPTRR9KnTx+54IILimz/p59+kjvuuEOqw/PPPy+jR4+W8PBwc1tPyl955RXb45dccol07979lNf9/fffsmDBAsnNzZXhw4dL48aNHdqeo+uv6OtjY2PN/nz11VflP//5j7hD8X2qkpKS5JtvvpHDhw9LfHy8DBo0yOH11YR9BsC7ePdZCQCPCsieeOKJSq3j7rvvlquvvlr27dsn77//vpx55pnyzz//VOj5GpA98sgjPvkrtQZk+t70urhGjRrJAw884PQ6U1NTzToPHjxY4XZp8PPmm2+We19VBWTa/rFjx8qxY8eKbP/FF1+U6qDB1H//+1+58sorS3xc26HpbMXpazp16iRLly41gdnFF18sf/75p9PbL239lX39FVdcYf5/uUNJ+3TWrFnSt29fWbx4sezZs0fGjRsnl112mfk/7yxf3GcAvA89ZAAclpmZKZ9++qkkJydLv379bPenp6fL22+/LdnZ2fLwww+b+/RX/jPOOMPhdScmJppfwlesWCG9evWSvLw8c9L15JNPlnhC7ejzb731Vmnfvr3Tn/LRo0dt69Ff5vVX+PPPP7/M12RkZMhnn31m9o+25cSJEyZo0pMzpW3UXhANHjStaejQoRIaGiquTll89913TwnKtJdo3bp1pkdNt6vX2ka9KA3EnnvuOZNqpYHt8ePHzf7VffvDDz+YNmvamJ4gf/nllyYA79ChgwwZMsT0OGog8fnnn5vtW48BDSyK36c9ZRo06r7QHg4NPJo2bWpOqK374rfffjMX7Z1buHChuW/ixInlvnftHdXPWn8YePrpp0t9Xlnbrgxts/4f0f1nVa9ePdt7L+kEfdeuXXLdddeZ/1cXXXSRuU8DSg2QHVHe+l3xej1GtJ2aOtiyZcty12n9/PT/y3fffWcCJT3m9HN3xT7t2LGjSS0NCwuz/R9v06aN+S4466yzvHKfAajZCMgAOCQrK8sEYXpyde6558oNN9wgaWlp0qVLl1JfowFaeb1m2stVu3ZtkzrXokULEwCogIAAGTVqVKm9K44+X4sB6Pr1RF1PePV5ztJet5tvvtm877feeqvE5+hJo56E6frPOeccuemmm0xQ07VrVxOQ6b7Q1+vJtqYzaTEUTWf65ZdfJDo62ql95cgYMmtAZrFYzPY3b95sUqt0e5pW+vPPP5d5oqht1x4nDTC7desmzZo1M/th4MCBUr9+fbPfNQh/4YUXzEm3MzRQ1RQzPz8/6d+/vwmOtM16Qq3vT0/Ctf0vvfSSDB48WDp37uzwujWoHDBggNn/enw4u+2S9mdZPXwayN1zzz1mecuWLWY/BQY6/qdVe3/02GzdurUkJCRI3bp1TSDraMpiddDgXY9RLarhaECmn9+zzz5rguodO3bI/fffL0uWLDE9gZXdp/p/yl5UVJT5PDXQ9tZ9BqBmIyAD4BA9+daT2U2bNklwcLAJ0PTkSukv1ddff73MmTPH9sux0iDDUZp61KRJkyL36W29vyLP114b6/gNPQHUQg06fkxPCiMjI8ttj57k2b8XDWL0xEpPFPXkuTgN1HSf6C/3evKo7926f6yPa6+S9srUqlXLnDzqGK2nnnqqzN6cytJxfZq2p5+btUdBx1Ppe/vggw/Kff2dd94pEyZMMMu33Xab6eXQVDHr/m3btq3ZhvZgXX755eY92u+3ku679957TfCzaNEicyKt9MR9+vTp5jGVkpIiq1atcrqQQ8+ePWXYsGFmPRqMF6dBb3nbrijtVS0vYC5OjwftDdMeZW2Hpi3eddddpkdTe2U9hf6fsU8FLY/2Eus+tv4f0GNIgzQN8F29T/U41F4zHT/oSZzdZwBqLgIyAA7Rk2NNT9NgTIWEhJgTyLLGaOlz7U/Ey2M9QbbS3p3i9zn6fA0Y7LetJ+Ka9qS/2pdU7KMkGtxp74+m82mqngZS+ot9SQHZmjVrTM+X9Zd8fe9627p/tLCF7j9dh9KetEsvvdSctFZkXzlKT/A1le+ZZ56x3XfgwAHTY+YI+wIZ+h50zJ61KIqecGoPlh4bjqQU2rdJ00C1B84qJyenyFgc7TWqaFU9TVvV12t7K7JtexqAOvq5aBDvaKqhlQbmmu75119/2X5gGDlypAkyNN3TU2iPaXkFduxpoG7/g4QG5rfccovL96n2bmqQpwFsUFCQeBJn9xmAmouADIDDSgqAyuJMGp6O5Sk+p5kW6yjeC2bl7PM1EDr77LNNQOUIrd6oY4v0otvSANT6i31JStoX9veVF1y6KmWxpBP+4il0OvbLmupZHmsA6ch7qEyb9LNp1apVidt1lgZyWoFy8uTJpufW2W3bcya9ToNADa40eHc0bVHT8Ro2bFjkuO3Ro4fpdfQUOg5Seyz1uHHFd4Wr9qmmQWqBDw2yS/qRxNv2GYCaiyqLABxOBdPxNtqbYA0g7Eumazqc3ldekFYaLQCgg+C1t8V64vzxxx/byq7rCZn+qm6tPlfe87XohxYbsR/jpeOm7At8aNGKlStXltgeDfa0N0hP+LTHRccklfVLvZ5Ea2+atrOk/WPdf9oOa3u1OIbeX5U02NBUUz3B1f2nF00d0/FsztK2aq+N9TPWHgAt+GF9D3oMaNqmvZLu0zbpOqzt0YsWDCmr9Li1YMz27dsdaqummOox8Mknn1R6247SsU0auG/YsMHh1+jxqgGKpnXa97a2a9euQu+7uMq+Xun4Og1yrUGrI+vctm1bkR8/5s+fb/6PuGKf6uenAffcuXNl2bJlpwRjnrjPAKAs9JABcIj2NGj1Ph33pCfzOo7I/ldwrXKm6TladlzTlaxVFh1NTdITUO3R0DQ+Lc6hJ2BaRMI6rkcDHU0z07RDDarKe76e4GobtNCG9ip99dVXJjjQXiYrDbQ0lU/fU3FaGEKr32kalKYTajBVVq+NFjl55513zDgWfa2mUClrep8+rgUc9KRU0wA1OLQGShVNWdS5yOznIdMiFqeffnqR52gq4bfffmsKY2ixCD3Z1B4FTR8rqyBLSbRHQgu76PvTIEz3qY7dueaaa8zjGtRoNUateqeV6LSiYkn36Vi+8847zzymBVC0J0H3h1a11PWVRPeVfv667/RYK48ei7o/dbv2qY/ObtuZ9Do9VsaMGWMCBS2EYp9Wp8ev9q7qPtMUWN229aKBv+5XLb6iQaT+SKCfkaPvu6z1V/b11h8n7FNSHVmnFifRsXwacGqlQd3H1v8Tld2n06ZNM2P+brzxRvOdZKXfBRrAeeI+A4CyEJABcIimEWmFPmvZ+xkzZpgeEutcWBrs6C/7+kt4SfNjOULHd2klRO310oIGeoJqHYOh6Uo6rsa+h6us52slPU071DLz1hL2enJorbKoJ74aBF111VUltkWDPE1V1BNJfW/a26WBjbWUf/H5vjRY01/FrWXvX3/9dVMIxRqQ6a/8ui49edMARYtl6AlkRVLzNAgrbcLZ4u3S7evJrH52Wv1OT3A1kC1+oqlBq66zQYMGtvFhetu+HLym1mnREmvZe60KqEGe9T1qb4B+Frrf9dgo7b6IiAizr7RHUavQaRD5+OOP27atJ9XFUw1132mwW1rhhtNOO80W3FrpCbt+9vYTCpe3bf18tfR/RWlBDu2F08DPWkTFqrTJqTU9Uduj0xLouCsttmINtMt73+Wtv7Kv12NZj3v93J1ZpxbA0R5rfV/6f/a1116rcOXI4vtUP6OyJlz2xH0GAGXxs1Q0vwgAvJhWHdQeNS1K4Sqa4mQNdHRKAO3F01/SrVUKUXHaI6tlxO3no/JUWt1RfyAorbevOt93ZV+vxU50bKaOpXR0nfpDhF5KSweu6n3qifsMAMpCQAYALjJixAjTA6cFGrQnTOfr0jFW1oIgQE1QFQEZAPgyAjIAcBEdU6KBmPaUaQqdpvNVZCJqwJtpaqxeiqedAgBKRkAGAAAAAG5C2XsAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAAAAAcBMCMgAAAABwEwIyAAAAAHATAjIAqEZbt26Vu+66SwYMGCA9evSQkSNHyvTp0yUtLY3Pwc7s2bOlb9++HrdPfvnlF7nsssvkzDPPlBtuuMGp177zzjsyePBg8TWe+lkBgLcgIAOAajJz5kzp3LmzHDlyRO69915544035OKLL5bXXntNzjnnnBr5OcyaNUvOPvvsU+4/fPiw/PHHH+JJMjMzZejQodK+fXv54IMP5P7773fqPSUnJ8uff/5ZZe0rbbtV/VpP/KwAwJsEursBAFATrFu3Tq677jqZNm2a3HPPPbb7u3XrZnrJ7rvvPqmJDh06JFu2bDnl/tGjR8ugQYPEk+zcuVNSUlLkmmuukU6dOjn9nqpaZbbrrjYDAOghA4Bq8fzzz0vdunXl7rvvPuWx8PBw87jV+vXrZdy4cSZY056zZ555RrKzs09Jffv555/l6quvll69epmgbsOGDUXWe+zYMXnggQfMOvr16yeTJ0+WpKSkIs/5+++/5fbbb5fevXtLnz595JFHHpGMjIxTtvXTTz/JpZdealL1Xn/9dTn99NPl999/L7IubWPPnj1N75FauHCheZ5eunbtKsOGDZO5c+cWSXV7+umn5ejRo7bnaVvUd999ZwJYe67aL6Upa/1vvvmm6R1T+j60rV988cUp6yjrPVk50r7yPhdnt1vWeyvrteV9hs5w9Fhzxf4p6bj99ttvzWMzZsywpQzffPPN5nPU93fixAnba8866yzJzc0tss1Vq1aZ523fvr1C7x8ASmUBAFS5Zs2aWUaMGFHu8zZt2mQJDQ21jB8/3rJ06VLLRx99ZGnYsKHliiuusD3nySeftAQEBFi6dOli+fzzzy3Lly+3jBo1yhITE2M5fPiw7XmXX365pXv37pZvv/3WsmLFCsvLL79sGTp0qO3xxMRES926dS0DBgwwz9HLGWecYRk2bNgp2+rUqZPl008/tWzcuNGSkpJiad26tWXy5MlF2v7xxx+b5+7Zs8fcPnbsmHk/elm7dq3ljTfesERFRVlef/1187i2derUqeY+6/N2795tHps+fbqlTp06VbJfKrLfk5KSLPPmzbPon80vv/zSPF/3Q3FlvSdH2+fI5+LMdst7b2W9trzPsKTPqiTOHGuu2D8lHbdHjx61JCQkWMLCwkybdf0PPvigJSQkxHyu1s9z3759lsDAQMsnn3xS5D2MHTvW0q1btzLfJwBUBAEZAFQDPSGeNGlSuc/TgKlHjx5F7tMTTj1h1BNI68mm3raeNFtPnP38/MyJrFWDBg3MCbS9rKws2/Lw4cMtjRo1sqSlpdnu27ZtmzmRLb6t33//vch6pk2bZomLi7NkZ2fb7rvgggssQ4YMKfP96Qlxx44dbbdfeOEFc3JdXPGTfFful5I4sv5169aZ21u2bClzXaW9J0fb58jn4sx2HXlvpb3Wkc/QkYDMmWPNFfunpOM2IyPDtPOpp54q0rZx48YVCcjUyJEjLYMHD7bdTk5ONoHbO++849A+AgBnUNQDAKpBWFiYpKenl/u8NWvWyJAhQ4rcp6lXQUFB5jGr+vXrS7NmzWy3IyMjzeXgwYO2+zRV68EHH5SHH35YlixZIllZWRIcHGx7/Mcff5TLL7/ctM2qTZs20rZtW1m6dKntvqioKOnYsWORNk2YMMGMO1qwYIG5vW/fPpPeZp9mmJeXJ++++65pR/fu3U2610svvSQ7duxwaJ9V1X6pzPory5H2Ofq5OKoy781Vn6Gj78mV+6f4cauphprGW3xs4nnnnXdKe2+55RZZtGiRqYqq3nvvPalVq5ZcddVVTr1vAHAERT0AoBrEx8ebcTzl0fL39ieaKiAgQEJCQiQ1NdV2X2BgyV/fmvlgpVUcddyTBk16grlr1y5T3dFaHVADxE8++cQEa8XH5+zevbvIGLfimjRpYk5k9WRdT47ff/99iYmJsY2zUrodPZF97LHHTBEMPbH+8ssvS61OWF37pTLrryxH2ufo5+Koyrw3V32Gjr4nV+6f4setdWqJ4vui+G2lY8z0/ep4s2effdZcjx8/vsTnAkBlEZABQDXQkzntVdJ5rLRgQHHz5883wcxpp512ShGDv/76y5w4t2vXzqlt+vn5ySWXXGIuas6cOaZYwogRI0zpdu090CIPd9xxxymv1eCqPNdee62MGTNGDhw4YAIyLRph3wOnJ83//ve/i8zXpaX/iwcG5QVLypX7parX7+h7Kk1FP5fStuvIeyvttY58hlX5nly5Lu1F0/8TWozGvuds8+bNJT5ff8TQwFOnA9AewUmTJjnVTgBwFCmLAFANtFS6pn1dccUVpoKg9eRXU6imTp1q63HQynHz5s2T77//3tzWynF6X+vWrW2BlSM01UxPWPfv32+7Lycnx5yQWnsOtPz+559/buZFs1bSa9WqlalMt3fv3nK3oRMka4/JjTfeaNLBNECzp+lny5cvt1Xz03QynXPNXqNGjcw+0DaUxVX7pTrW7+h7Kk1FP5fStuvIeyvttY58hlX5nly5Lq1yeuWVV5qpJ/755x9zn86f9uqrr5b4fP2BQf8faRruueeea37EAICqQA8ZAFQDDYS0t0HLi0+cONGM59Jf83XsVZcuXeSJJ56wnQTqr/HDhw83Y2D0pFN7MT799FOTYuYo7fFo2bKlKR2en58v/v7+JiDTkt5NmzY1zxk1apQ50dZt6kl67dq1zSS/2pOnPV/l0fbo86ZPn27K3euJsT0da6QBaIMGDUyqlwahWjZdJyG20omx9bXaVk2D1LFN+rriXLVfSuPK9Tv6nkpT0c+ltO068t5Ke60jn2FVvidXr+vll182QZn+H9AgVCf71pTbt956y4yps6c/XGjPtr7mpptucqqNAOAMP63s4dQrAACVpoGYngzqiaF9mp+VBmw6JkZPNhs2bFjkMS2moSegxVPpdGJfPXGOjo4+ZVtK16OBWUm0Z0F7A7Q99s8pbVtW2quyZ88eqVevntl2cRoM6uNKizUcP37c3C4evGmPhW5H368+T4MGva9Dhw5Vtl9KUtb69fPSnkBNASzpMyuu+HuqSPtK+1yc2a4j762s15b3GZb2WZXG2WOtIvunvONWe441mGvevLkJOh9//PESezR1Hj/9EUP3W/GADQBchYAMAADUCL/++quZHP3CCy80vdY6fkyXdfymTnhuT4M6DTJ1MndNKwaAqkJABgAAagRraqOOi9MUTA3ONC3x+eefN72CVuecc46pinrGGWeYMZ9a8h4AqgoBGQAAqFG0BH5ycrIZL1dSqf3ExESTltqiRQvTkwYAVYmADAAAAADchLL3AAAAAOAmBGQAAAAA4CY1PiD7/fff5bHHHjO55AAAAABQnWr0GDKdWFInv1y3bp2sWLHilHlxyqLz0WzatEliY2NLHBAMAAAAoGbKzc01HT6dO3eW0NDQMp9boyOJhx9+WK677roKzS+iwVjPnj2rpF0AAAAAvN/q1aulR48e3hWQHThwwMwTctppp5mSsyXJz8+X3bt3m2izYcOGFd45f/zxhzzx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Lyh5r5iHDAAAuENpgdfEiRPl4osvdkubvNXq1atl6NChMnDgwFKfk52dbQKjivSeVea1cD16yJyUkJAgcXFxEh8fL542fkzRQwYAANSff/5pekH0xPvWW2+Vc845R5588knz2J49e2TKlCnmhP+CCy6QV155RfLyCrJtSvLll1+adellwIABMmHCBPnpp5+KpCVqD5kGZdbnTZs2zTy2cOFCefTRR83yihUrpE+fPrJ///4i609NTZWzzz5bvvjiC9t9zrbR0de99tprMmbMGFmyZImMGjVK+vXrZ9LvNHB84YUX5K233jIBpLZTX5eWliZPPfWUWde5554rd911l+zbt8+2vgMHDpj3q/vj7rvvNtvVHq7SlLU+6/7Tz+69994zy3feeecp69BAyhrk/vvf/7btcw20yttGea/V/aG39f1rqqme+2ZmZoqzdN+98847cvnll5tj5qabbpLdu3efst+WLl0qDzzwgFx00UVmex9//HGF16WfwSOPPCKDBw+2fQabN2+Wa665Rvr37y+jR482Pxqcd9558tlnn5nHdb/oey2egmmxWOSSSy6R119/XaqcBRWyZ88ei+4+va4Oubm5lgMHDpjr4n7elmxpPvUrc0nPOvVx1GxlHTsAxw743nFMVk6e5a/kVLdetA3OWLNmjTlXqVu3ruXpp5+2LFmyxLJjxw7L77//bomJibEMGDDA8vnnn1tmz55tadasmeWmm26yvXbatGmW5s2b224nJSVZVqxYYS6LFy+2PPHEE5bQ0FDLZ599Zh7fu3ev5brrrrM0adLE9rzt27efsq6srCzTnieffLJIW999911LcHCwJTk52fy9+umnn8ptY0kceW9Tp061hISEWDp16mQe17YePXrU0q1bN/OeLr/8cstXX31l7s/Pz7f079/f0qpVK8tHH31k+eKLL8ztBg0aWA4dOmTWt3PnTrOf69SpY3n44YfN/klMTCyxfeWtLzMz02xX95fuT13esmXLKevJycmxLFiwwGz35Zdftu1zXX952yjrtWrlypXm9vLly83+OfPMMy0XXHCBbdv6Gelrv/nmm1I/B13XsGHDzD7RbXz77beWMWPGWOrXr2+OJfv9FhMTY3nssccsixYtsjz11FMWf39/y6efflqhdelz7r77bssPP/xgPgO9v3bt2paRI0davv76a8tbb71ladiw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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "total_energy = out.scalars.total_energy\n", "energy_error = total_energy.struphy.analysis.relative_error()\n", @@ -422,17 +11586,42 @@ "cell_type": "markdown", "id": "30", "metadata": {}, - "outputs": [], "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "31", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/diagn_tools.py:246: UserWarning: No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.\n", + " ax.legend()\n" + ] + }, + { + "data": { + "image/png": "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ABc98MADcuONN8p///tfaWxslMWLFwe27mnTpslBBx0ktbW1UlZWpm97/PHHZfvtt28NNpdffrncdtttss4668i8efOkuLhYbr/9dtlvv/2y/j9UXgJsxAIA7EaVBR0dF4Ra2G7atGly6623ylVXXdXhci0tLbqCoi4q5HRGBZMjjzxSNt98c5k9e7bMnDlThg8fLocddpg0NDS0Vlyqqqrkm2++kU8++UQvc8QRR+j7qb9li/ACAPAeDVNke1wQZGCzMWPGtFZC2vPKK6/IhhtuqKsoKlisu+668uSTT3Z4nw8//FA+++wzufjii3U1Rbnkkkt0QHnxxRf19fLycjnvvPNktdVW09eLiopk6NChuvqjqjXZotsYAMBbfJOOQo6J5LKcqwRxq6urS7teUVGhL7lSVZORI0fKnXfeKYcffri+7ZlnnpGjjz5avvjiC+nTp0/G+02aNEmHlsGDB7fetsEGG0jPnj313/bff//W21WXtVmzZsn3338vV1xxhZxzzjmy5pprZr2NhBcAgFcILAj6mCDEIG592oSKyy67TI8vydX999+vg4RanxrQr7qD9ejRQ6qrq+XVV1+V4447LuP9FixYIN26dWutuiSp+6q/pXrqqafkscce093HVLhRwSgXhBcAgBcILQj7mEhdH9UYRGnmzJk6YLQef3lUXZQvv/xSFi5cqLt3perbt29rMFHjYNQyirpt55131gP0ly9fdfz3smXLdHexVGeffba+qHE1KmDtscceeuaxXr16ZbWNhBcAgNMILYjjeKAagyhVV1enhZd8denSRY93efPNN9td5t5775V33323NSRNnDhRdxFbunSpLFq0SFdgFDUN8ty5c2X99dfPuB4VfM4991zddUz9PzW4PxuEFwCAkwgtMOFYIMTAJsOGDdNTF6vzsAwaNKj1dhVE1KWyslLGjRuX8X6q+vL000/L8ccfr29TA/VVoNl777319SVLlsjqq6+edr+vv/66tXtZtggvAAAnJbvtEGL8ZsrzT4iBKVMlz5s3T6ZPn667bSUrLFtvvbV07dpVDjnkED3F8YgRI/TMYaq7mLrPXXfdJePHj5d+/fplXK8aJ6O6mp111lk65KiKzAUXXCAnnXSS9O/fXy+jxsz89a9/laOOOkqPqVFdxa699lrdbUzNOpatooQaiePBDAw1NTUyd9yuUt3l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GlqX+9Of/iT33nuvfPDBBzJp0iSZMWOGrL/++pILwgtiH6zPRAEAgLCEGWBMC0embQ/MM2bMGNl+++07XOaVV17RVZODDjpIB5J1111XnnzyyQ7v8+GHH8pnn30mF198sRQXr4wXl1xyiQ4xL774or6+YMECufHGG/XtW2yxhb6tqKhITj/99JweA+EFAIAQ0W0pfjTq4bq6urq0S2q1IxeqajJy5Ei5/vrr5euvv5YpU6bIPffcI8cdd5wOIu1RVRQVWgYPHtx62wYbbCA9e/bUf1PeeustWb58uRxwwAEya9Ysfbva1lwRXgAAgPMIMHBZnz59pKampvUyduzYvNZz//33y5prrqnX984778jbb78tPXr0kOrqann11VfbvZ+qqnTr1q216pKk7qv+lgxG6u9/+ctfdPXnhBNO0GNfLrroopy2kdnGAACAVwEmiGoYYQgmmTlzpg4YSRUV+Z0A/Msvv5SFCxfKeeedl3Z73759W4OJGgejllHUbTvvvLOUlZXpqkpby5Ytk/Lycv27WkaNpZk3b558++23UlpaKm+88Ybssccesu2228qhhx6a1TYSXgAACAldxsykggfPDVxSXV2dFl7y1aVLFz3e5c0332x3GTXg/t13320NSRMnTtRdxJYuXSqLFi3SFRhFTZc8d+7c1gH5ar3KKaecooOLomY9U+NfVIjJNrzQbQxGDJg3YRsAAP5wrXLi2uNBPIYNGyaffvqpTJ48Oe12FUSSlZVx48bpcKMuKrgk76cqK08//XTrfdRAfRVo9t57b31dnVNGBSw13iVpxYoVOuCstdZaWW8jlZccFdesWhIDAAD+dCMjKMDmqZLnzZsn06dP1124khWWrbfeWrp27SqHHHKInuJ4xIgReuYw1V1M3eeuu+6S8ePHS79+/TKuV42TUV3NzjrrLB10VEVGnc/lpJNOkv79+7dWda644go5//zz9fTLvXv3lrvvvluam5v1ctkivAAAEAK6JdmDbmTwxYMPPqinQlbUCSIvvPBC/bsKESpkqDEsTz31lO4aNmHCBFmyZInu1vXEE0+0G1ySrrzySh121LIqwKgwc8YZZ6Qt8/vf/15Pvay2Q1VlBg4cKH/96191kMlWUUKdVcZxaho2NfPC3HG7SnWXUimpyr964mLlxZQuW4na/AaXmaqltjKv+zXXr3q/pvou0pjh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/fffy4IFC6Rfv376uMlEnWD366+/1u9d6623nvhIDcv85ptvZOnSpXpftT3Pwg8//CDTp09f5X677rqr+Ei9l9fW1spGG23U7utKnStN7Te1P7t37y6+HlfquFEnwe7bt68+NUOq//znP/LTTz+tcv6R9j4DfJB8rW2yySay9tprr/L3L7/8Ur/Pq4mVmCAJoUo47NJLL01UVFQkNt98c/3z5JNPTjQ3N8e9Wca54oorElVVVYlddtml9TJixIiE7xobGxNjx45N9O3bN1FTU5MYOnRoxuWee+65RI8ePfRy3bt3T2y77baJ2bNnJ3xSV1eXuPjiixPrrbdeomvXroljjjlmlWU++OADNTlIYscdd0w71qZOnZrwydNPP50YOHBgYt11100MGjQosfrqq+v3qrauu+66RGVlZWKzzTZLdOnSJXHkkUcmGhoaEj657777Ev369UtstNFGej+o1+Ett9yStszNN9+s90/qMaUuvnn++ef1caX2l/qp9sl5552XaGlpSXtP++Uvf9l6XKmfV199dcI3Dz30kH6/HjBgQKJ///768++aa65JW0a93tZee+20Y+r0009P+GrJkiW6LaXew++88860v/3www+J7bffPtGtWze9X9dYY43Es88+G9u2wn3Ohpfx48cnysrKEm+99Za+Pm3aNP3Bd9NNN8W9aUaGlx122CHuzTDOokWLEhdccEFi+vTpOvhmCi8//fST/uD785//rK8vW7ZMN87/53/+J+ETtY/UPlChbfjw4R2Gl4ULFyZ8pt6DpkyZ0nr9tdde0+9VjzzySOttr776aqKoqCjx73//W1//9ttvEz179kyMHj064ZOrrroqMWPGjNbrjz76qN4vb775Zlp4UQ1Q39199936dZj07rvv6n315JNPtt42ZswYfRx98803+voLL7ygl5k4cWLCJyoAp37B9Pjjj+v3JvUelRpe1Ps+VjrxxBMT5557bqKkpGSV8LL//vvrNsTSpUtb2xTqS6w5c+aw+xAKZ8e83HPPPbLXXnvJzjvvrK+rrjyHH364vh2rampqkk8//VSXytXvWNlF4Oqrr9ZdCtrz2GOPSXNzs5x77rn6uurSct5558nzzz+vS+y+UPvokksukd69e3e6rOoGpbqPLVmyRHz0+9//XnerSNp9991bz1+VpN6ndthhB9l333319fXXX1+OP/54796/1MmJN9xww9br6j28R48e8tZbb6Utp16DkydPli+++EJ3XfTRSSedlPZetf322+vuiKqLWJI6ftRxtMEGG+jr6gTQ6jjz7bj63e9+l/Zetccee+ifqftKUV0VP/zwQ91t0eezSvzjH/+Qjz/+WK688spV/jZnzhz517/+Jeeff7506dJF36Y+D9X+evTRR2PYWvjA2fAyadIk2XbbbdNuU2/mU6ZM8fbDrbP99ctf/lIHPtW//v777497k6zZb5tttllaf2l1nKk37kzjOyByyCGHyNFHH6372p911lneh+WFCxfqfuQbb7xxp+9fajyDWt5XavyiGieUuq8Utf+OOOIIGTFihKy55ppy++23i4/q6up0CJ4wYYIce+yxOvip11ryb2r/ZTqu1PHmm3nz5ul99cwzz8gxxxyjv0RQx0+qxx9/XE4++WQZMmSIPuZeffVV8Y16ban36QcffDDjOBb1paf6vEs9rlSIUV/I+HhcIRrODthXH3DqG7pU6rr6hk69ibf9m8+22247/aGW/DbuxhtvlBNOOEEPymPQfn7HWfJv+JkKKy+99JIOyMr777+vf1f7S1VtfKQ+9E899VS9b371q19lfVz5OMhafemk3pe23HJLOeCAA1pvHzhwoEybNq11goh77723tQqRrFz5QoXbCy+8UB8jqoowduxYPXlG6vtRpuPKx/cq1ei+7LLLdOVATXBw8803pzXODz30ULntttv0a00de6piOnLkSPn888+zqjC7QD1uFX7V+3NqtTgVxxXi4GzlpaysTM8ikmrZsmX6Z3l5eUxbZabhw4e3BhdFfcuigovqEoWOcZxlT81slAwuyW981beaDz/8sLeHmWoQvfzyy/L000/rbopJHFfp1JdOqjKsGufjx4/X+ydp2LBhaTPbnXjiibor1COPPCK+UTNhqWqCamC/8MILuivP3Xffrf+W3GeZPhd9/ExU70VqX3311Ve625yqvqjuvqldFJNfEqh995e//EXPpJW6jOuuu+46/WXv1ltvrfdVsmur6vqrupEpHFeIg7PhRTXG2/ZfVde7devm5bSsuVLTILbdf8j+OEuOUwDHWXvOPvts3RXjxRdf1NNFZ3NcqYaCb1O+q+CiGpbvvfeevPLKK2lftLSH9y/RAU51hVJdyBR13KjKQqbjyvf3qv333193/+0omKjxjNXV1V59LqpQq7phqmpe8qJej6o73fXXX6+XSb4eOa4QJWfDixqI+Nxzz+kXWtJTTz2lb0e6tgOn1dz2aryG6o6Bzo8z1SdYfdOZepyprhrbbLMNu6+D40xRDXcfj7NzzjlH7rvvPv34Mx0n6rhS35yr8welHleqypBadXCdev9WYzfefvttPd4g0+QZbY8r9U2xCjq+HVdt94Pad2qgebKbWElJiR6Yrqp8Ser4Ug12nz4XVVeotuNe1cB8NcFKcl+pSWsaGxvTllGVBtVFyqfjSk0+k6y4JC/qOLrgggv0Fy+KqsqogJN6XKmJM1RFy6fjCtEqdblx8H//939y1FFH6dlV1GwYqkH+t7/9Le5NM476QDv44INl8ODBehDjtddeKz179pTf/va34js104zqZpHsF50smydPgKe6Hqh+9aqLwejRo2X27Nl6RhbVxcCnRqaiZoBSYzjUiRXVT7Wv1De9akxVcuYo1ShQ+0ydBPWBBx6Qd955x6tuGMqll14qN910kz5GVJed5DGlGgADBgzQv6vX3h133KEnNzjttNN0w12NF3rttdfEJ7/+9a/lySef1PtCvbbURVl33XX1SRgVNf5FVRjUcaaCyw033KBfe+ozwCfq8auxPqrrWH19vR778+OPP+oKX9Kf//xn2W233fRte+65p/48VK/RM844Q3yhJrxQ79mnnHKKnoVUfVl366236sqKOt4U9V6vvihQ19VrUn1BNWbMGH1b6ngrrDyhtfrMU11gVddX9dq8/PLL9fHl25gzRKdIzZcsjpoxY4ZuiKvpf1VZXL1ht+2egZVv5rfccov+tlLNmqXGIqjGU9szDvtIhZJMUx6nTmurvrVTDabXX39d7zPVxUXdzzfqg73tNNsqBD/xxBOt3wSrWexUNxb1LbFqFJx55plZdQNyiWo0qQHmmb5EuOKKK1qvq24Y11xzjUydOlUPEFb7SnUF8slBBx0k8+fPX+V2NbOYaiwpqqGuGp8qPKtuLqqSpRrjqouPT+bOnavfx9UXLmq2JzWxwemnn65fg6k++OAD+etf/6qDoOoqpboCrbfeeuITNXZK7Ss1vbY6TlTwUxNnpI47S11GfbGgGuNqUg1VefCZep9XFRnV1S6Vep9X7+9qXJAKyGq6ZDVVNxAGp8MLAAAAAHc4O+YFAAAAgFsILwAAAACsQHgBAAAAYAXCCwAAAAArEF4AAAAAWIHwAgAAAMAKhBcAAAAAViC8AECIRowYoU/MmQ114sUhQ4boy5dffhn585L832qbAQAwEeEFAEKyYMEC+fe//y29e/fOankVWNRZz2+//Xbp06dP5M+L+r/qrPaffPJJ5P8bAIBslGa1FAAgZ5MmTZKysjLZfPPNs77P2muvrasfcVD/d9q0abH8bwAAskHlBQBCoioYKriUl5fr63PnzpVDDz1ULrroImlubs5qHfPnz2/tzrXTTjvJL3/5S3n99dc7vM9TTz0lBx54YNptn376qV5HY2NjAY8IAIB4UXkBgBDDy+DBg/Xv77zzjhxxxBE6fIwZM0aKi7P77qimpkZ351JU8Hj77bdl+PDh8uKLL8quu+6a8T4//fSTfPbZZ2m31dfXy0cffSQtLS0FPy4AAOJCeAGAEMPLKaecIrfccotceumlcscdd8hhhx2W0zpKS0vTupHtvPPOMnPmTLntttvaDS8AALiK8AIAIWhoaNDjR+666y5Zvny5vPHGG7LFFlvkta6JEyfKvffeK99++60sW7ZM5syZI+utt17g2wwAgOkY8wIAIZgyZYoe17LGGmvIBhtsIP37989rPWq2sv33318GDRoko0eP1l3I1HgWFWIAAPAN4QUAQuoytvHGG8v48eN1N68zzjgjr/Wo+x988MFywQUXyJ577qm7kC1evLjD+6y++uqydOnStNt++OGHvP4/AAAmIbwAQEjhZauttpLu3bvLM888I48++qjceOONOa9HVW6mTp3aWmlRM409/PDDacuosTR77LFH6/Utt9xS5s2bJ6+99pq+vmjRIrn++uvT7qMmEFBBSHVFAwDAFoQXAAgxvCibbrqpPPbYY3LhhRfKc889l9N6zjrrLD0zmRrjsskmm8hRRx0l++67b9oys2fP1ueUSVJja1SlRi2npmpWJ75MbktSbW2tnn2M7mcAAJsUJRKJRNwbAQCumTx5sqyzzjrSo0eP1tu+/PJLPQ5GhYlMRowYoUOPCioPPfSQDj2Kmt54xowZsmLFCh1gVFVFnf8lefJLFV7UOWS23nrrtPUtWLBAT5vcr18/PWmAmkBg2223laKiIqmrq9PbM3DgQKmsrNTLq0qMuo/qcvbjjz+GuHcAAMgP4QUADPHVV1/pikiyetKlS5dI//+HH36of5aVla1SqQEAwASEFwAAAABWYMwLAAAAACsQXgAAAABYgfACAAAAwAqEFwAAAABWILwAAAAAsALhBQAAAIAVCC8AAAAArEB4AQAAAGAFwgsAAAAAKxBeAAAAAFiB8AIAAABAbPD/ADgD2QHoX/IqAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spectrum: (50, 8)\n" + ] + } + ], "source": [ "omega, kvec, spectrum, _ = out.em_fields.e_field.struphy.analysis.dispersion(\n", " slice_at=(None, 0, 0),\n", @@ -445,7 +11634,6 @@ "cell_type": "markdown", "id": "32", "metadata": {}, - "outputs": [], "source": [ "## Save standard output\n", "\n", @@ -454,10 +11642,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "33", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote:\n", + " post_processing/report/scalars.csv\n", + " post_processing/report/scalars.png\n", + " post_processing/report/electric_energy.png\n", + " post_processing/report/kinetic_energy.png\n", + " post_processing/report/total_energy.png\n" + ] + } + ], "source": [ "written = out.save_report()\n", "print(\"Wrote:\")\n", @@ -469,7 +11670,6 @@ "cell_type": "markdown", "id": "34", "metadata": {}, - "outputs": [], "source": [ "## Comparing runs\n", "\n", @@ -481,12 +11681,51 @@ "execution_count": null, "id": "35", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", + " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", + " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", + "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time stepping: 100%|██████████| 20/20 [00:04<00:00, 4.38step/s]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", + " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", + " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" + ] + }, + { + "data": { + "image/png": "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c3NzUwfc0icf2Q6eN8Tzhvi3hqwV/0eRvZw3FRnaxKIeREREREREFsKAjIiIiIiIyEIYkBEREREREVkIAzIiIiIiIiILYUBGRERERERkIQzIiIiIiIiILIQBGRERERERkYUwICMiIiIiIrIQBmREREREREQWwoCMiIiIiIjIQhiQERERERERWQgDMiIiIrJqt956K55//nmHb4Mt2b17N9q0aYPMzMwKv8ZgMKBRo0aYM2dOjbaNap+cC7///rtV7fqFCxeiT58+6ryzNAZkREREZNVOnz6Ns2fPVus2R40ahf/7v/+zaBvs2VNPPYVhw4bBy8urwq/RNA0nTpxAdnZ2jbaNat/JkycrFZzXhmuvvVb9Tn/33XewNGdLN4CIiIiotiUlJcHPz6/Cz582bRpcXFxqtE32YsuWLVi2bBl+/vlnSzeF6JKcnJzw8MMP47333sODDz6o7lsKe8iIiIjIombNmoXBgwejWbNm6NmzJ3799ddqec3s2bMxZMgQxMTEYODAgZg7d25xCqQEDZJCJSlycps5c6Z67MYbb8RLL72EN954Ax06dEDjxo1VStOTTz6JSZMmVWj7V9Nu8/u/++676N27t0r1euihh8rtnavotsp+luTkZNx///2q3e3bt8fbb7+NZ555BnfeeWfxa4cPH46nn376ovf8z3/+o3oWLkd6HK655hqEhoYWr7vhhhvw7LPPXvTcF198Ef3797/ktjp16qSOj7S9S5cueOyxx3DmzJlSz/ntt9/UNpo3b47rr78eM2bMKPV4eno6nnvuObRr10595tGjR2PHjh1X3bayZF9erq0ZGRnq8R9++EHtfzkm8ho5r/Lz8/Hmm2+qzyvHfMKECcjJybnotXLOymvldfL6jz76SPUsllRYWIj3338fPXr0QNOmTTF06FBs2rTpom1NmTIF9913H1q2bInbbrtNPRYfH49HHnkEsbGxav2jjz6KtLQ0VMb69etx3XXXqffu168flixZUu7zrvRef/zxhzq3582bp3pb5TkDBgzAihUrcPz4cYwZMwYtWrRA9+7d1TlQVlxcHG655RZ1zOXYT5w4EefPny/1HPlbINuSLxAsSqMqiY+Pl7Nf/bS0rHPJWmL8ca2oqMjSTSEbIudLUlISzxvieWMn8gsN2tGULIvfpB2V+Vvz4Ycfanq9Xnv11Ve1HTt2aL/9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Iqq21MWpD3TFpuNp6BBRT036TERERESXx4CMiKpNPQ9XDGrZgHuUiIiIqIJYZZGIiIiIiMhCGJARERERERFZCAMyIiIiIiIiC2FARkREREREZCEMyIiIiIiIiCyEARkREREREZGFMCAjIiIiIiKyEAZklTRp0iQEBgYiNja2Zo4IERERERE5DAZklTRx4kQkJycjLi6uZo4IERERERE5DAZkREREREREFsKAjIiIiIiIyEIYkBEREREREVkIAzIiIiIiIiILYUBGRERERERkIQzIiIiIiIiILIQBGRERERERkYUwICMiIiIiIrIQZ0u9sa0rKipSP5OSkizdFBgMBqSmpiI/Px96vd7SzSEbwfOGeN4Q/9aQteL/KLL188YcI5hjhsthQFZFKSkp6mfnzp2rugkiIiIiIrLzmKFRo0aXfY5O0zSt1lpkR/Ly8rBr1y4EBATA2dmyca1E4BIYbtq0CcHBwRZtC9kOnjfE84b4t4asFf9Hka2fN9IzJsFY69at4e7uftnnsoesimTHdurUCdZETrywsDBLN4NsDM8b4nlD/FtD1or/o8iWz5sr9YyZsagHERERERGRhTAgIyIiIiIishAGZHbA29sbr7zyivpJxPOG+PeGrAn/RxHPG+Lfm8tjUQ8iIiIiIiILYQ8ZERERERGRhTAgIyIiIiIishAGZERERERERBbCecjsgMztXVBQADc3N0s3hWxIdnY2jEYjvLy8LN0UshFyvsjfG71eb+mmkA2cKxkZGWpZp9Opwh7yk6gysrKy1OS6Pj4+PH/oiudJWb6+vrAV7CGzYbm5uXjwwQfh6emp/tm1aNECS5YssXSzyIrl5+dj2rRpGDhwoPoH165dO0s3iaycBGB///03+vTpo/7OSADfoUMHzJs3z9JNIyt24MABNSGq3Bo2bAgPDw/069cPmzdvtnTTyEbs3bsXAQEBqFevHhITEy3dHLJiAwYMQFBQUPHfHPPNYDDAVjAgs2Hjxo1TAdiuXbuQk5OD2267DTfccAP2799v6aaRldq2bRtmzZqFZ555Bnfffbelm0M24MyZMxg9ejR69+6NpKQknDt3DsOGDVN/axYvXmzp5pGVat68Oc6fP69u6enpiI+Ph7+/PwYNGoRTp05Zunlk5QoLC3HXXXehVatWlm4K2dA18fkLf3PMN1vK5mBAZqMSEhLw66+/4rXXXkNkZKQ66V588UWEhobiww8/tHTzyEp169YNU6dOVT1kTk789acrk78tv//+u/pbI71jkhr98ssvq787v/zyC3chVYifnx8++OADFdAzk4Ou5PXXX4eLiwsee+wx7ixyCLwis1ErVqxQqUT9+/cvXif5+X379sXy5cst2jYish+SMnTrrbdetF4Ceo4Josowj/FwdXXljqNLkrRW+WL5hx9+4BeHVCl5eXnq2tgWMSCzUcePH1ffXAcHB5daHx4erh4jIqopCxYswKFDh9QXQERXGussvWI7d+5UKUXt27fHTTfdxJ1GlzxfJFXxueeeQ8uWLbmXqMK+/vprNTZexqv27NkTK1euhC1hQGbDf7QkdajsN9Tu7u7qW8jyqs0QEV2tEydO4J577kGPHj04DpGuSC6sZXB9bGws9uzZg88++0z9nyK61PkiPagvvPACdxBV2LXXXot169apa2PplIiJiVEZZMuWLYOtYEBmo+QbAOmaldLCZUuZyx8zZ2fOaEBE1Ss5OVkVZZDxQDNmzODfGbqiTz/9VBX1kBL49913nyoOY0sXSVR7tm/fji+//FKlK8q1jBRlkIJlQs4f8zJRWW+88YbqfZdU+sDAQHzzzTdo3LgxPvnkE9gKBmQ2Kjo6WgVjJ0+eLLVevhmQx4iIqpNcHEkwJr3vUpRBxpYRVZQUhJHCMHKR9NNPP3HHUblf+Mg0PiNHjiwuW/7UU08VF6SSlFeiipAhPc2aNVMF8GwFAzIbJWM3pALRP//8U7zOfKEkF01ERNVFvq2+/vrr1Vgg6d0ICQnhzqVKkzmB5FySdHuisuTapWzZcunpEJLuyqquVFGSQRYXF4eoqCjYCua12Sj5dvqJJ57AK6+8onJlmzRporpsJX92woQJlm4eWTH5JycKCgpUL6v5vnwzyVRXKkvOEynCIEU8pJiHTA5tPmfkfJHzhqis999/X6WZ3XjjjarYlMxDJuvS0tLw8MMPc4cRUbVYvXq1Kujx4IMPqmth6RV79dVXkZqaqsYk2goGZDbsnXfeUTPYmyfDk/xZKYcfFhZm6aaRlcrPz1dpICWZ78uE0TK+g6ikI0eOqDLUouQ0G6JTp06cHJrKNf7/27t7FFWCKACjd+CJYDRM5hYMFFSMBgNzQ0NXMKsQTAdchGsx08DAn9jQVNFAceiO50H7+kEhnJMI0tGN/Kzqqq+v/EdSdo9UtpU++xOx1+vFcrmMRqNhahSSvROfnZzn3kz+JjtR8Xg8xnQ6jc1mEx8fH9HtdvMVsizQXsXb41UP7AcAAHhx3iEDAABIRJABAAAkIsgAAAASEWQAAACJCDIAAIBEBBkAAEAiggwAACARQQYAAJCIIAMAAEhEkAEAACQiyADgP+j3+zGZTMwSgKcIMgAo6Xw+x2KxiFarZZYAPEWQAUAJ8/k86vV63O/3GI/H8f7+HoPBwEwBKORPsccAgN+MRqM4HA4xm81iv9/n31UqFcMCoBBBBgAlVKvV2O120W6389UxAHiGLYsAUNJqtYpOp2OOADxNkAFACZfLJbbbbb5CBgDPEmQAUMJ6vc4P9BBkAPwLQQYAJZxOp/zzer2aIwBPE2QAUMLn52cMh8NoNpv5oR7f39/mCUBhb4/H41H8cQDgN7fbLb8gularOfYegMIEGQAAQCK2LAIAACQiyAAAABIRZAAAAIkIMgAAgEQEGQAAQCKCDAAAIBFBBgAAkIggAwAASESQAQAAJCLIAAAAEhFkAAAAiQgyAACARAQZAABApPEDPNACBu87wzcAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "sim_coarse = Simulation(\n", " model=build_model(),\n", " env=EnvironmentOptions(out_folders=demo_root, sim_folder=\"vlasov_ampere_coarse\", save_restart=False),\n", - " time_opts=Time(dt=0.1, Tend=2.0),\n", + " time_opts=Time(dt=0.1, Tend=5.0),\n", " domain=domains.Cuboid(r1=2 * 3.141592653589793),\n", " equil=equils.HomogenSlab(),\n", " grid=grids.TensorProductGrid(num_elements=(16, 1, 1)),\n", @@ -504,7 +11743,6 @@ "cell_type": "markdown", "id": "36", "metadata": {}, - "outputs": [], "source": [ "## Other models\n", "\n", @@ -515,7 +11753,6 @@ "cell_type": "markdown", "id": "37", "metadata": {}, - "outputs": [], "source": [ "### SPH densities\n", "\n", @@ -524,10 +11761,77 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "38", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time stepping: 100%|██████████| 80/80 [00:01<00:00, 43.56step/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "No post-processed data in /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/sph_soundwave, processing with default options (call out.process(...) to choose them)\n", + "\n", + "Post-processing path /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/sph_soundwave\n", + "\n", + "No feec fields found in hdf5 file, skipping post-processing of fields.\n", + "Evaluation of 3 marker orbits for euler_fluid\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "100%|██████████| 81/81 [00:00<00:00, 1169.69it/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluation of distribution functions for euler_fluid\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "0 starting post-processing of distribution functions for /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/sph_soundwave/post_processing/kinetic_data/euler_fluid ...\n", + "100%|██████████| 1/1 [00:00<00:00, 1607.01it/s]\n", + " 0%| | 0/1 [00:00 \u001b[39m\u001b[32m1\u001b[39m density = out_sph.euler_fluid.view_0.n_sph.isel(e2=\u001b[32m0\u001b[39m, e3=\u001b[32m0\u001b[39m)\n\u001b[32m 2\u001b[39m density.plot(x=\u001b[33m\"t\"\u001b[39m, y=\u001b[33m\"e1\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/post_processing/output.py:106\u001b[39m, in \u001b[36mProductNamespace.__getattr__\u001b[39m\u001b[34m(self, name)\u001b[39m\n\u001b[32m 104\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mtype\u001b[39m(\u001b[38;5;28mself\u001b[39m)(\u001b[38;5;28mself\u001b[39m._mapping, key)\n\u001b[32m 105\u001b[39m location = \u001b[38;5;28mself\u001b[39m._prefix \u001b[38;5;129;01mor\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mproducts\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m; available names under \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mlocation\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mself\u001b[39m)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n", + "\u001b[31mAttributeError\u001b[39m: 'n_sph'; available names under 'euler_fluid/view_0': ('n',)" + ] + } + ], "source": [ "density = out_sph.euler_fluid.view_0.n_sph.isel(e2=0, e3=0)\n", "density.plot(x=\"t\", y=\"e1\")" @@ -585,7 +11901,6 @@ "cell_type": "markdown", "id": "41", "metadata": {}, - "outputs": [], "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" ] @@ -604,7 +11919,6 @@ "cell_type": "markdown", "id": "43", "metadata": {}, - "outputs": [], "source": [ "### Vector fields on a mapped domain\n", "\n", @@ -685,7 +11999,6 @@ "cell_type": "markdown", "id": "47", "metadata": {}, - "outputs": [], "source": [ "## Apply the workflow to another run\n", "\n", From 4c7a4bc72c79060765d76943052e9879d4763606 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Sep 2026 23:33:11 +0200 Subject: [PATCH 047/193] pproc --- src/struphy/post_processing/output.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 9c5225ef1..a5bdf4903 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -8,6 +8,7 @@ from collections.abc import Callable, Iterator, Mapping from functools import cached_property from pathlib import Path +from typing import Any import h5py import numpy as np @@ -87,7 +88,7 @@ class ProductNamespace: Use :attr:`catalog` when generic iteration over arbitrary products is needed. """ - def __init__(self, mapping, prefix=""): + def __init__(self, mapping: "ProductMapping | ProductCatalog", prefix: str = ""): self._mapping, self._prefix = mapping, prefix def __repr__(self): @@ -95,7 +96,10 @@ def __repr__(self): products = tuple(self.catalog) return f"{type(self).__name__}({location!r}, products={products!r})" - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: + """A leaf product (``xr.DataArray``) or a nested :class:`ProductNamespace`; only known at + run time, so callers get ``Any`` here rather than a static type a type checker cannot verify. + """ key = f"{self._prefix}/{name}" if self._prefix else name if key in self._mapping: return self._mapping[key] @@ -105,7 +109,8 @@ def __getattr__(self, name): location = self._prefix or "products" raise AttributeError(f"{name!r}; available names under {location!r}: {tuple(self)}") - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: + """A leaf product or a nested namespace, see :meth:`__getattr__`.""" if "/" in key: key = f"{self._prefix}/{key}" if self._prefix else key return self._mapping[key] From a30125a7188a63b3ed7d5d43951d33225fbe39b1 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Sep 2026 16:25:05 +0200 Subject: [PATCH 048/193] Update managed dependency bounds (#374) Co-authored-by: github-actions[bot] --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cbe4b9315..ebb378469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,9 @@ dependencies = [ "tqdm<=4.70.0", "argcomplete<=3.7.0", "ipywidgets<=8.1.8", - "plotly<=6.9.0", - "pyvista<=0.48.0", - "trame<=3.13.0", + "plotly<=7.1.0", + "pyvista<=0.49.0", + "trame<=4.0.0", "trame-vtk<=2.11.8", "trame-vuetify<=3.2.2", "nest_asyncio2<=1.7.2", @@ -49,7 +49,7 @@ dependencies = [ "pytest-testmon<=2.2.0", "ruff==0.15.0, <=0.16.0", "line_profiler<=5.0.2", - "scope-profiler >= 0.5.0, <=0.5.0", + "scope-profiler>=0.5.0, <=0.7.0", ] [project.license] From df67469845040960087471d1f89728868db16cc6 Mon Sep 17 00:00:00 2001 From: Byung Kyu Na Date: Fri, 18 Sep 2026 11:47:32 +0100 Subject: [PATCH 049/193] fix pproc to proceed create_vtk without any saved data (#381) **Solves the following issue(s):** Closes #380 **Core changes:** In the `create_vtk` function, fix `nt = len(t_grid) - 1` as `nt = max(len(t_grid) - 1, 1)`. This prevents `log_nt = int(xp.log10(nt))` blowing up. **Model-specific changes:** None **Documentation changes:** None Co-authored-by: Byung --- src/struphy/post_processing/post_processing_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index be0e20c4f..d30295ebb 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -781,7 +781,7 @@ def _create_vtk( os.makedirs(species_path) # time loop - nt = len(t_grid) - 1 + nt = max(len(t_grid) - 1, 1) log_nt = int(xp.log10(nt)) + 1 logger.warning(f"\nCreating vtk in {path} ...") From 25df651e61e9f25b2525150414f33c0d311f17c9 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 17:11:21 +0200 Subject: [PATCH 050/193] Implemented the lazy SimulationOutput class --- .../cyclone/pproc_cyclone.py | 6 +- .../itg_cylindre/pproc_drift_kinetic.py | 6 +- src/struphy/post_processing/output.py | 66 ++++++++++++++----- .../post_processing/tests/test_output.py | 24 ++++--- src/struphy/simulation/sim.py | 8 +-- 5 files changed, 74 insertions(+), 36 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 3c48a0553..ea77a2513 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -21,10 +21,10 @@ def main(path_out=DEFAULT_OUTPUT): - run = Output(path_out).process(physical=True) + run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run[FIT_QUANTITY].struphy.plot.timeseries( + run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", @@ -35,7 +35,7 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run[name].struphy.plot.viewer( + run.evaluate(name).struphy.plot.viewer( x="e1", y="e2", coords="physical", plane=plane, **selection ).show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 37ae22417..d2dd1c400 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -21,10 +21,10 @@ def main(path_out=DEFAULT_OUTPUT): - run = Output(path_out).process(physical=True) + run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run[FIT_QUANTITY].struphy.plot.timeseries( + run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", @@ -35,7 +35,7 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run[name].struphy.plot.viewer( + run.evaluate(name).struphy.plot.viewer( x="e1", y="e2", coords="physical", plane=plane, **selection ).show() diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index a5bdf4903..4dd522f0b 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -150,19 +150,21 @@ class OrbitProducts(ProductNamespace): class Output: - """The output of one Struphy simulation, loaded lazily from its output folder. + """A lightweight, lazy handle to the output of one Struphy simulation. Obtain it from :attr:`Simulation.output` (or the return value of :meth:`Simulation.run`) - or construct ``Output(path_out)`` in a separate process. Nothing is read at construction. + or construct ``Output(path_out)`` in a separate process. Construction reads saved metadata + when it is available, but does not load field or particle data. + + Call :meth:`evaluate` to obtain one product as an :class:`xarray.DataArray`. It materializes + post-processing products on demand; call :meth:`pproc` explicitly to choose its options. + The :attr:`xarray` property exposes the complete post-processed product tree. * :attr:`scalars` are read directly from the raw HDF5 output. - * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` need - post-processed data. When there is none, the first access processes the run with - default options; call :meth:`process` beforehand to choose options. + * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` are retained + as compatibility views over the post-processed products. * :attr:`model`, :attr:`domain` and numerical options are reconstructed lazily from saved metadata. No simulation object is created or retained. - * Products plot themselves, e.g. ``out["en_phi"].struphy.plot.timeseries(fit=(0, 40))``; - :attr:`plot` holds the plots that need the whole run. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. Parameters @@ -184,6 +186,12 @@ def __init__(self, path_out, *, time_units: str = "normalized", comm=None): self.time_units = time_units self.comm = MPI.COMM_WORLD if comm is None else comm self._reset() + # A Simulation can expose its Output before it has written metadata. In that case, + # keep the handle usable and let metadata raise its normal error when requested. + try: + self.metadata + except FileNotFoundError: + pass def __repr__(self): return f"{type(self).__name__}({str(self.path_out)!r}, processed={self.is_processed})" @@ -201,9 +209,7 @@ def _reset(self): self._seconds = None def __getitem__(self, name: str) -> xr.DataArray: - """Any product by name: a scalar (``"en_tot"``), a field (``"em_fields/phi_log"``), a binned - distribution or SPH density (``"kinetic_ions/e1_v1_density/f"``) or orbits (``"kinetic_ions"``). - """ + """Compatibility shorthand for :meth:`evaluate`.""" if name in self.scalars.data_vars: return self.scalars[name] for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): @@ -218,6 +224,16 @@ def __getitem__(self, name: str) -> xr.DataArray: ) raise KeyError(f"{name!r} not found; available products: {available}") + def evaluate(self, name: str) -> xr.DataArray: + """Return a named simulation product as an :class:`xarray.DataArray`. + + Scalars are read directly from raw output. Other products are materialized with + :meth:`pproc` on first use when no complete post-processing output exists. The returned + array is an ordinary xarray object, so use xarray for selection, arithmetic and further + analysis. + """ + return self[name] + def _stamp(self, array: xr.DataArray) -> xr.DataArray: array.attrs.update(run=self.label, run_name=self.path_out.name) if self.time_units == "normalized" and "t" in array.dims and self.seconds_per_time is not None: @@ -321,7 +337,7 @@ def is_processed(self) -> bool: return is_processed(str(self.path_out)) - def process( + def pproc( self, *, step: int = 1, @@ -333,7 +349,7 @@ def process( parallel: bool = False, force: bool = False, ) -> "Output": - """Post-process the raw output; reuses existing products made with the same options. + """Materialize post-processed products; reuse matching existing products. Call this on every MPI rank. Serial processing (the default) runs on rank 0 while the other ranks wait. Parallel processing reconstructs the field decomposition @@ -361,7 +377,7 @@ def process( Returns ------- Output - This run, so that ``run = Output(path).process(physical=True)`` reads naturally. + This run, so that ``run = Output(path).pproc(physical=True)`` reads naturally. """ from struphy.post_processing.post_processing_tools import PostProcessor @@ -383,16 +399,20 @@ def process( self._reset() return self + def process(self, **options) -> "Output": + """Compatibility alias for :meth:`pproc`.""" + return self.pproc(**options) + def _ensure_processed(self): if self.is_processed: return if self.comm.Get_size() > 1: - raise RuntimeError(f"{self.path_out} has no post-processed data; call out.process() on all ranks first") + raise RuntimeError(f"{self.path_out} has no post-processed data; call out.pproc() on all ranks first") logger.warning( - "\nNo post-processed data in %s, processing with default options (call out.process(...) to choose them)", + "\nNo post-processed data in %s, processing with default options (call out.pproc(...) to choose them)", self.path_out, ) - self.process() + self.pproc() def _product_mappings(self) -> dict[str, ProductMapping]: if self._products is None: @@ -642,7 +662,7 @@ def info(self) -> str: lines += ["scalars (no post-processing needed)"] lines += [f" out.scalars.{name}" for name in scalars] or [" (none)"] if not self.is_processed: - lines += ["", "products (not post-processed yet; run out.process(...) to choose options)"] + lines += ["", "products (not post-processed yet; run out.pproc(...) to choose options)"] lines += [f" out.{name}.*" for name in sorted(self._raw_species())] return "\n".join(lines) for kind, catalog in ( @@ -687,6 +707,16 @@ def tree(self) -> xr.DataTree: self._tree = store.open_tree(store.store_path(self.path_pproc)) return self._tree + @property + def xarray(self) -> xr.DataTree: + """The complete post-processed product tree as an xarray :class:`DataTree`. + + Prefer :meth:`evaluate` when requesting one named product. Accessing this property + materializes post-processing output if it does not exist, but preserves xarray's lazy + backing arrays. + """ + return self.tree + def _groups(self) -> dict[str, xr.Dataset]: """Every group of the store that holds products, by path without the leading slash.""" return {path.lstrip("/"): node.ds for path, node in self.tree.subtree_with_keys if node.ds.data_vars} @@ -721,7 +751,7 @@ def _load(self, group: str, name: str) -> xr.DataArray: def open_output(path_out, *, time_units: str = "normalized") -> Output: """Open the output folder of a finished simulation. - Nothing is allocated and no MPI is needed; products are read on first access. + Saved metadata is read immediately; products are materialized and opened on demand. Parameters ---------- diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 4dd090c23..86524c9a1 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -162,7 +162,7 @@ def forbidden(*args, **kwargs): raise AssertionError("Output must not construct a Simulation") monkeypatch.setattr(Simulation, "__init__", forbidden) run = Output(root) - assert "metadata" not in vars(run) + assert run.metadata["model"] == Maxwell(base_units=BaseUnits(x=2.0)).to_dict() assert "model" not in vars(run) assert not hasattr(run, "sim") assert "_sim" not in vars(run) @@ -175,25 +175,32 @@ def forbidden(*args, **kwargs): assert run.with_time_units("physical").model.to_dict() == run.model.to_dict() -def test_products_trigger_default_processing_when_missing(tmp_path, monkeypatch): +def test_evaluate_triggers_default_processing_when_missing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) run = Output(root, time_units="normalized") calls = [] - def fake_process(self, **options): + def fake_pproc(self, **options): calls.append(options) write_manifest(root) self._reset() return self - monkeypatch.setattr(Output, "process", fake_process) + monkeypatch.setattr(Output, "pproc", fake_pproc) assert set(run.scalars.data_vars) == {"en_tot"} assert calls == [], "scalars come from the raw output" - assert tuple(run.fields) == ("em_fields",) + assert run.evaluate("em_fields/E").name == "E" assert calls == [{}] +def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): + field = run.evaluate("em_fields/E") + assert isinstance(field, xr.DataArray) + assert field is run["em_fields/E"] + assert run.xarray is run.tree + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) @@ -307,8 +314,9 @@ def test_normalized_time_carries_seconds_as_a_coordinate(run): def test_a_failing_property_reports_its_own_error(tmp_path): - run = Output(write_tree(str(tmp_path))) - (run.path_out / "run_metadata.json").unlink() + root = write_tree(str(tmp_path)) + (tmp_path / "run_metadata.json").unlink() + run = Output(root) with pytest.raises(FileNotFoundError, match="run_metadata.json"): run.domain @@ -325,6 +333,6 @@ def test_saved_rank_count_does_not_block_serial_implicit_processing(tmp_path, mo run.metadata["mpi_ranks"] = 8 (run.path_pproc / "manifest.json").unlink() calls = [] - monkeypatch.setattr(Output, "process", lambda self: calls.append(self.path_out)) + monkeypatch.setattr(Output, "pproc", lambda self: calls.append(self.path_out)) run._ensure_processed() assert calls == [run.path_out] diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index f841bd3f4..5005e6380 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -889,7 +889,7 @@ def output(self) -> Output: """The output of this simulation in ``env.path_out``, see :class:`~struphy.Output`. Scalars are available as soon as data is written; fields and particle products are - post-processed on first access, or explicitly with ``sim.output.process(...)``. + post-processed on first access, or explicitly with ``sim.output.pproc(...)``. """ if self._output is None or self._output.path_out != Path(self.env.path_out).resolve(): self._output = Output(self.env.path_out, comm=self.comm) @@ -911,13 +911,13 @@ def pproc( force: bool = True, load: bool = False, ) -> Output | None: - """Deprecated, use ``sim.output.process(...)``, see :meth:`struphy.Output.process`.""" + """Deprecated, use ``sim.output.pproc(...)``, see :meth:`struphy.Output.pproc`.""" warnings.warn( - "Simulation.pproc() is deprecated; use sim.output.process(...) instead.", + "Simulation.pproc() is deprecated; use sim.output.pproc(...) instead.", DeprecationWarning, stacklevel=2, ) - self.output.process( + self.output.pproc( step=step, celldivide=celldivide, physical=physical, From eacb9fe5bcf226370a578400c4dd5787726196b0 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 17:51:49 +0200 Subject: [PATCH 051/193] Move plotters to the Output class --- .../cyclone/pproc_cyclone.py | 8 ++- .../itg_cylindre/pproc_drift_kinetic.py | 8 ++- src/struphy/post_processing/output.py | 68 ++++++++++++++++++- .../tests/test_output_accessors.py | 16 ++++- .../post_processing/xarray_accessors.py | 6 +- 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index ea77a2513..b4ad74afe 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -24,7 +24,8 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( + run.timeseries( + FIT_QUANTITY, fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", @@ -35,11 +36,12 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run.evaluate(name).struphy.plot.viewer( + run.viewer( + name, x="e1", y="e2", coords="physical", plane=plane, **selection ).show() - run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() + run.trajectories("kinetic_ions", max_markers=1000).show() if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index d2dd1c400..fef1f9ff5 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -24,7 +24,8 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( + run.timeseries( + FIT_QUANTITY, fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", @@ -35,11 +36,12 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run.evaluate(name).struphy.plot.viewer( + run.viewer( + name, x="e1", y="e2", coords="physical", plane=plane, **selection ).show() - run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() + run.trajectories("kinetic_ions", max_markers=1000).show() if __name__ == "__main__": diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 4dd522f0b..c2abb293f 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -159,6 +159,7 @@ class Output: Call :meth:`evaluate` to obtain one product as an :class:`xarray.DataArray`. It materializes post-processing products on demand; call :meth:`pproc` explicitly to choose its options. The :attr:`xarray` property exposes the complete post-processed product tree. + Render products through this object too, e.g. ``out.viewer("em_fields/E", x="e1", y="e2")``. * :attr:`scalars` are read directly from the raw HDF5 output. * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` are retained @@ -234,6 +235,68 @@ def evaluate(self, name: str) -> xr.DataArray: """ return self[name] + def _array(self, product: str | xr.DataArray) -> xr.DataArray: + """Resolve a saved product name or accept an already-derived xarray array.""" + if isinstance(product, str): + return self.evaluate(product) + if isinstance(product, xr.DataArray): + return product + raise TypeError(f"product must be a product name or xarray.DataArray, got {type(product).__name__}") + + def timeseries(self, product: str | xr.DataArray, *others: str | xr.DataArray, **kwargs): + """Plot one or more scalar products; see :meth:`ArrayPlots.timeseries`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).timeseries(*(self._array(other) for other in others), **kwargs) + + def view(self, product: str | xr.DataArray, **kwargs): + """Configure a reusable slice view of one product; see :meth:`ArrayPlots.view`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).view(**kwargs) + + def slice(self, product: str | xr.DataArray, *, ax=None, **kwargs): + """Render one two-dimensional slice; see :meth:`ArrayPlots.slice`.""" + return self.view(product, **kwargs).slice(ax=ax) + + def panels(self, product: str | xr.DataArray, **kwargs): + """Render evenly spaced snapshots; see :meth:`ArrayPlots.panels`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).panels(**kwargs) + + def viewer(self, product: str | xr.DataArray, **kwargs): + """Create an interactive slice viewer; see :meth:`ArrayPlots.viewer`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).viewer(**kwargs) + + def animation(self, product: str | xr.DataArray, **kwargs): + """Create a slice animation; see :meth:`ArrayPlots.animation`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).animation(**kwargs) + + def frames(self, product: str | xr.DataArray, directory, **kwargs): + """Export slice frames; see :meth:`ArrayPlots.frames`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).frames(directory, **kwargs) + + def trajectories(self, product: str | xr.DataArray, **kwargs): + """Plot saved marker trajectories; see :meth:`ArrayPlots.trajectories`.""" + from struphy.post_processing.xarray_accessors import ArrayPlots + + return ArrayPlots(self._array(product)).trajectories(**kwargs) + + def plot_scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): + """Plot an overview of the scalar time series of this run.""" + return OutputPlots(self).scalars(names=names, relative_to=relative_to, logy=logy) + + def equilibrium(self, ax=None): + """Plot the radial equilibrium profiles saved with this run.""" + return OutputPlots(self).equilibrium(ax=ax) + def _stamp(self, array: xr.DataArray) -> xr.DataArray: array.attrs.update(run=self.label, run_name=self.path_out.name) if self.time_units == "normalized" and "t" in array.dims and self.seconds_per_time is not None: @@ -522,9 +585,10 @@ def orbit_catalog(self) -> ProductMapping: @property def plot(self) -> OutputPlots: - """Plots of the whole run: ``out.plot.scalars()`` and ``out.plot.equilibrium()``. + """Compatibility namespace for whole-run plots. - A single product plots itself, e.g. ``out.em_fields.phi_log.struphy.plot.slice(...)``. + Prefer :meth:`plot_scalars` and :meth:`equilibrium`; product plots are direct methods + of :class:`Output`, such as :meth:`viewer` and :meth:`timeseries`. """ return OutputPlots(self) diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 33e29095c..7c36c1da8 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -62,6 +62,20 @@ def test_timeseries_by_name_with_growth_fit(run): assert result.fig._suptitle.get_text() == run.label +def test_output_owns_product_plotting(run): + result = run.timeseries("en_phi", fit=True) + assert result.fit_results[0].rate == pytest.approx(RATE) + + phase_space = run.evaluate("kinetic_ions/e1_v1_density/f").isel(t=-1) + assert run.slice(phase_space, x="e1", y="v1").ax.get_xlabel() == r"$\eta_1$" + + viewer = run.viewer("em_fields/E", x="e1", y="e2", component=0) + viewer.draw() + assert set(viewer.sliders) == {"t", "e3"} + + assert run.trajectories("kinetic_ions", max_markers=2).ax.name == "3d" + + def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): first, second = make_run(str(tmp_path), "sim_1"), make_run(str(tmp_path), "sim_2") result = first.scalars.en_phi.struphy.plot.timeseries(second.scalars.en_phi) @@ -77,7 +91,7 @@ def test_timeseries_into_given_axes_keeps_the_figure_layout(run): def test_scalar_overview_draws_every_scalar_in_one_axes(run): - result = run.plot.scalars() + result = run.plot_scalars() assert sorted(line.get_label() for line in result.artists) == ["en_phi", "en_tot"] assert result.fig.axes == [result.ax] diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index 440ec8b6e..f19174f6b 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -1,8 +1,8 @@ -"""``array.struphy.(...)``: plots and diagnostics of a single labeled array. +"""Compatibility accessors for plots and diagnostics of a single labeled array. Every product of an :class:`~struphy.Output` carries this accessor, and so does every array -derived from one, e.g. ``out.ions.eta1_v1.f.isel(v1=0).struphy.plot.timeseries()``. Plots that -need the whole run (the scalar overview, the equilibrium profiles) live on ``out.plot``. +derived from one. New code should use the direct methods of ``Output`` instead, for example +``out.timeseries("en_phi")`` or ``out.slice(array, x="e1", y="v1")``. Dimensions that are neither displayed nor swept are selected by naming them: an integer is a position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest From 27129688d0910cc5533c693a57a1aa8619e6e406 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 17:54:25 +0200 Subject: [PATCH 052/193] Added as_numpy=True to evaluate() --- src/struphy/post_processing/output.py | 33 +++++++++++++++++-- .../post_processing/tests/test_output.py | 21 ++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index c2abb293f..d712a2187 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -225,15 +225,42 @@ def __getitem__(self, name: str) -> xr.DataArray: ) raise KeyError(f"{name!r} not found; available products: {available}") - def evaluate(self, name: str) -> xr.DataArray: + def evaluate( + self, + name: str, + *, + sel: Mapping[str, Any] | None = None, + isel: Mapping[str, Any] | None = None, + method: str | None = None, + drop: bool = False, + as_numpy: bool = False, + ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. Scalars are read directly from raw output. Other products are materialized with :meth:`pproc` on first use when no complete post-processing output exists. The returned array is an ordinary xarray object, so use xarray for selection, arithmetic and further - analysis. + analysis. ``isel`` selects positions (for example ``{"t": -1}``) and ``sel`` selects + dimension-coordinate values (for example ``{"e3": 0.5}``). Positional selection is + applied first, followed by coordinate selection. ``method`` and ``drop`` have xarray's + usual ``.sel``/``.isel`` meanings. Set ``as_numpy=True`` to return only the selected + values as a :class:`numpy.ndarray`. + + Physical auxiliary coordinates such as ``X``, ``Y`` and ``Z`` describe the evaluated + logical grid; selecting an arbitrary physical point requires a separate interpolation or + inverse-coordinate operation. """ - return self[name] + array = self[name] + if isel: + array = array.isel(isel, drop=drop) + if sel: + options = {"drop": drop} + if method is not None: + options["method"] = method + array = array.sel(sel, **options) + elif method is not None: + raise ValueError("method requires a coordinate selection through sel") + return array.to_numpy() if as_numpy else array def _array(self, product: str | xr.DataArray) -> xr.DataArray: """Resolve a saved product name or accept an already-derived xarray array.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 86524c9a1..f68d997d8 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -201,6 +201,27 @@ def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): assert run.xarray is run.tree +def test_evaluate_selects_positions_coordinates_and_slices(run): + field = run.evaluate("em_fields/E", isel={"t": -1, "component": 2}) + assert field.dims == ("e1", "e2", "e3") + np.testing.assert_allclose(field, 3.0) + + phase_space = run.evaluate( + "kinetic_ions/e1_v1_density/f", + sel={"e1": 0.49}, + method="nearest", + drop=True, + ) + assert phase_space.dims == ("t", "v1") + + history = run.evaluate("en_tot", isel={"t": slice(1, None)}) + assert history.sizes["t"] == NT - 1 + + values = run.evaluate("en_tot", isel={"t": -1}, as_numpy=True) + assert isinstance(values, np.ndarray) + np.testing.assert_allclose(values, 2.0) + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) From f0674d2e4dc5a1506317405fddddfb585ea3e8f9 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 18:05:34 +0200 Subject: [PATCH 053/193] return fig, ax --- .../cyclone/pproc_cyclone.py | 9 +- .../itg_cylindre/pproc_drift_kinetic.py | 9 +- src/struphy/post_processing/output.py | 93 ++++++++++++------- .../post_processing/tests/test_output.py | 53 +++++++---- .../tests/test_output_accessors.py | 21 +++-- src/struphy/simulation/sim.py | 2 +- 6 files changed, 117 insertions(+), 70 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index b4ad74afe..1d8e67d5d 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,6 +1,8 @@ import sys from pathlib import Path +from matplotlib import pyplot as plt + from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" @@ -29,7 +31,7 @@ def main(path_out=DEFAULT_OUTPUT): fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", - ).show() + ) if SHOW_EQUIL_PROFILE: run.plot.equilibrium() @@ -39,9 +41,10 @@ def main(path_out=DEFAULT_OUTPUT): run.viewer( name, x="e1", y="e2", coords="physical", plane=plane, **selection - ).show() + ) - run.trajectories("kinetic_ions", max_markers=1000).show() + run.trajectories("kinetic_ions", max_markers=1000) + plt.show() if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index fef1f9ff5..2ea6c6f11 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,6 +1,8 @@ import sys from pathlib import Path +from matplotlib import pyplot as plt + from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" @@ -29,7 +31,7 @@ def main(path_out=DEFAULT_OUTPUT): fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", - ).show() + ) if SHOW_EQUIL_PROFILE: run.plot.equilibrium() @@ -39,9 +41,10 @@ def main(path_out=DEFAULT_OUTPUT): run.viewer( name, x="e1", y="e2", coords="physical", plane=plane, **selection - ).show() + ) - run.trajectories("kinetic_ions", max_markers=1000).show() + run.trajectories("kinetic_ions", max_markers=1000) + plt.show() if __name__ == "__main__": diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index d712a2187..6a32f1453 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -22,6 +22,11 @@ logger = logging.getLogger("struphy") +def mpi_comm_world(): + """The communicator used by output post-processing.""" + return MPI.COMM_WORLD + + class ProductMapping(Mapping[str, xr.DataArray]): """A discoverable mapping whose products are loaded on first access.""" @@ -172,20 +177,18 @@ class Output: ---------- path_out: The simulation output folder, ``sim.env.path_out``. - comm: - Communicator for post-processing; defaults to MPI.COMM_WORLD. time_units: ``"normalized"`` (the default) keeps Struphy time units, in which the analytic results of the models are expressed; every product then also carries seconds as the coordinate ``t_seconds``. ``"physical"`` makes ``t`` itself seconds. """ - def __init__(self, path_out, *, time_units: str = "normalized", comm=None): + def __init__(self, path_out, *, time_units: str = "normalized"): if time_units not in {"physical", "normalized"}: raise ValueError("time_units must be 'physical' or 'normalized'") self.path_out = Path(path_out).resolve() self.time_units = time_units - self.comm = MPI.COMM_WORLD if comm is None else comm + self.comm = mpi_comm_world() self._reset() # A Simulation can expose its Output before it has written metadata. In that case, # keep the handle usable and let metadata raise its normal error when requested. @@ -199,7 +202,7 @@ def __repr__(self): def with_time_units(self, time_units: str) -> "Output": """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" - return type(self)(self.path_out, time_units=time_units, comm=self.comm) + return type(self)(self.path_out, time_units=time_units) def _reset(self): if getattr(self, "_tree", None) is not None: @@ -262,6 +265,15 @@ def evaluate( raise ValueError("method requires a coordinate selection through sel") return array.to_numpy() if as_numpy else array + def keys(self) -> tuple[str, ...]: + """Return the names accepted by :meth:`evaluate`, without loading their arrays. + + As with evaluating a non-scalar product, this materializes default post-processing when + needed. Call :meth:`pproc` first when its options should be chosen explicitly. + """ + catalogs = (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog) + return tuple(sorted((*self.scalars.data_vars, *(key for catalog in catalogs for key in catalog)))) + def _array(self, product: str | xr.DataArray) -> xr.DataArray: """Resolve a saved product name or accept an already-derived xarray array.""" if isinstance(product, str): @@ -274,7 +286,8 @@ def timeseries(self, product: str | xr.DataArray, *others: str | xr.DataArray, * """Plot one or more scalar products; see :meth:`ArrayPlots.timeseries`.""" from struphy.post_processing.xarray_accessors import ArrayPlots - return ArrayPlots(self._array(product)).timeseries(*(self._array(other) for other in others), **kwargs) + result = ArrayPlots(self._array(product)).timeseries(*(self._array(other) for other in others), **kwargs) + return result.fig, result.ax def view(self, product: str | xr.DataArray, **kwargs): """Configure a reusable slice view of one product; see :meth:`ArrayPlots.view`.""" @@ -284,25 +297,32 @@ def view(self, product: str | xr.DataArray, **kwargs): def slice(self, product: str | xr.DataArray, *, ax=None, **kwargs): """Render one two-dimensional slice; see :meth:`ArrayPlots.slice`.""" - return self.view(product, **kwargs).slice(ax=ax) + result = self.view(product, **kwargs).slice(ax=ax) + return result.fig, result.ax def panels(self, product: str | xr.DataArray, **kwargs): """Render evenly spaced snapshots; see :meth:`ArrayPlots.panels`.""" from struphy.post_processing.xarray_accessors import ArrayPlots - return ArrayPlots(self._array(product)).panels(**kwargs) + result = ArrayPlots(self._array(product)).panels(**kwargs) + return result.fig, result.ax def viewer(self, product: str | xr.DataArray, **kwargs): """Create an interactive slice viewer; see :meth:`ArrayPlots.viewer`.""" from struphy.post_processing.xarray_accessors import ArrayPlots - return ArrayPlots(self._array(product)).viewer(**kwargs) + viewer = ArrayPlots(self._array(product)).viewer(**kwargs) + result = viewer.draw() + result.fig._struphy_viewer = viewer + return result.fig, result.ax def animation(self, product: str | xr.DataArray, **kwargs): """Create a slice animation; see :meth:`ArrayPlots.animation`.""" from struphy.post_processing.xarray_accessors import ArrayPlots - return ArrayPlots(self._array(product)).animation(**kwargs) + animation = ArrayPlots(self._array(product)).animation(**kwargs) + animation._fig._struphy_animation = animation + return animation._fig, animation._fig.axes[0] def frames(self, product: str | xr.DataArray, directory, **kwargs): """Export slice frames; see :meth:`ArrayPlots.frames`.""" @@ -314,15 +334,18 @@ def trajectories(self, product: str | xr.DataArray, **kwargs): """Plot saved marker trajectories; see :meth:`ArrayPlots.trajectories`.""" from struphy.post_processing.xarray_accessors import ArrayPlots - return ArrayPlots(self._array(product)).trajectories(**kwargs) + result = ArrayPlots(self._array(product)).trajectories(**kwargs) + return result.fig, result.ax def plot_scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): """Plot an overview of the scalar time series of this run.""" - return OutputPlots(self).scalars(names=names, relative_to=relative_to, logy=logy) + result = OutputPlots(self).scalars(names=names, relative_to=relative_to, logy=logy) + return result.fig, result.ax def equilibrium(self, ax=None): """Plot the radial equilibrium profiles saved with this run.""" - return OutputPlots(self).equilibrium(ax=ax) + result = OutputPlots(self).equilibrium(ax=ax) + return result.fig, result.ax def _stamp(self, array: xr.DataArray) -> xr.DataArray: array.attrs.update(run=self.label, run_name=self.path_out.name) @@ -742,33 +765,31 @@ def label(self) -> str: return self._label def info(self) -> str: - """A table of everything this output holds, printed by ``print(out.info())``. + """A table of every key accepted by :meth:`evaluate`, with a short description. - Names are listed as they are reached, e.g. ``out.kinetic_ions.e1_v1_density.f`` - and ``out["kinetic_ions/e1_v1_density/f"]``. Distribution and density products carry - their symbol, e.g. ``f`` ($f$) vs. ``delta_f`` ($\\delta f$). Nothing is loaded. + Use ``print(out.info())`` interactively. As with :meth:`keys`, this materializes default + post-processing when needed; call :meth:`pproc` first to choose its options. """ - lines = [f"Output of {self.path_out}", f" {self.label}", ""] - scalars = tuple(self.scalars.data_vars) - lines += ["scalars (no post-processing needed)"] - lines += [f" out.scalars.{name}" for name in scalars] or [" (none)"] - if not self.is_processed: - lines += ["", "products (not post-processed yet; run out.pproc(...) to choose options)"] - lines += [f" out.{name}.*" for name in sorted(self._raw_species())] - return "\n".join(lines) - for kind, catalog in ( - ("fields", self.field_catalog), - ("distributions", self.distribution_catalog), - ("densities", self.density_catalog), - ("orbits", self.orbit_catalog), - ): - lines += ["", kind] - entries = [f" out.{key.replace('/', '.')}{self._quantity_hint(key)}" for key in catalog] - if kind == "orbits": - entries = [f" out.{key}.orbits" for key in catalog] - lines += entries or [" (none)"] + rows = [(key, self._product_description(key)) for key in self.keys()] + key_width = max((len(key) for key, _ in rows), default=3) + lines = [f"Output: {self.path_out}", self.label, "", f"{'Key':<{key_width}} Description", f"{'-' * key_width} -----------"] + lines.extend(f"{key:<{key_width}} {description}" for key, description in rows) return "\n".join(lines) + def _product_description(self, key: str) -> str: + """A stable description for a key, without loading its data array.""" + if key in self.scalars.data_vars: + return f"scalar time series ({key.replace('_', ' ')})" + if key in self.field_catalog: + return f"field ({key.rsplit('/', 1)[-1]})" + if key in self.distribution_catalog: + label = BINNED_LABELS.get(key.rsplit("/", 1)[-1], key.rsplit("/", 1)[-1]) + return f"particle distribution ({label})" + if key in self.density_catalog: + label = BINNED_LABELS.get(key.rsplit("/", 1)[-1], key.rsplit("/", 1)[-1]) + return f"SPH density ({label})" + return "marker trajectories" + @staticmethod def _quantity_hint(key: str) -> str: """Static label for a catalog key, e.g. distinguishing ``f`` from ``delta_f``.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index f68d997d8..fbd307b1b 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -12,6 +12,7 @@ from struphy.models import Maxwell from struphy.post_processing.output import Output, open_output +from struphy.post_processing import output as output_module from struphy.post_processing import store from struphy.post_processing.arrays import orbit_quantities from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint @@ -95,6 +96,11 @@ def Barrier(self): self.barriers += 1 +def output_with_comm(monkeypatch, path, comm, **kwargs): + monkeypatch.setattr(output_module, "mpi_comm_world", lambda: comm) + return Output(path, **kwargs) + + @pytest.fixture def run(tmp_path): return Output(write_tree(str(tmp_path)), time_units="normalized") @@ -108,6 +114,19 @@ def test_products_are_discovered_without_loading_arrays(run): assert run.is_processed +def test_keys_list_every_evaluable_product_without_loading_arrays(run): + assert run.keys() == ( + "em_fields/E", + "en_tot", + "kinetic_ions", + "kinetic_ions/e1_v1_density/delta_f", + "kinetic_ions/e1_v1_density/f", + "kinetic_ions/view_0/n", + ) + assert run.field_catalog._cache == {} + assert run.distribution_catalog._cache == {} + + def test_field_has_named_and_curvilinear_coordinates(run): field = run.fields["em_fields/E"] assert field.dims == ("t", "component", "e1", "e2", "e3") @@ -222,12 +241,12 @@ def test_evaluate_selects_positions_coordinates_and_slices(run): np.testing.assert_allclose(values, 2.0) -def test_products_refuse_implicit_processing_on_many_ranks(tmp_path): +def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) comm = FakeComm(size=2) with pytest.raises(RuntimeError, match="on all ranks"): - Output(root, comm=comm).fields + output_with_comm(monkeypatch, root, comm).fields def test_processing_options_are_part_of_the_manifest(tmp_path): @@ -260,7 +279,7 @@ def process(self, **options): monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) comm = FakeComm(rank=rank, size=2) - run = Output(write_tree(str(tmp_path)), comm=comm) + run = output_with_comm(monkeypatch, write_tree(str(tmp_path)), comm) assert run.process(physical=True) is run expected = [ ("construct", False), @@ -288,7 +307,7 @@ def process(self, **options): pass monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) - Output(write_tree(str(tmp_path)), comm=FakeComm(rank=3, size=4)).process(parallel=True) + output_with_comm(monkeypatch, write_tree(str(tmp_path)), FakeComm(rank=3, size=4)).process(parallel=True) assert calls == [True] @@ -306,21 +325,21 @@ def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): assert {"em_fields", "kinetic_ions"} <= set(dir(run)), "species are known before processing" -def test_info_lists_products_without_loading(run): +def test_info_lists_evaluable_products_with_descriptions(run): text = run.info() - assert "out.scalars.en_tot" in text - assert "out.kinetic_ions.e1_v1_density.f" in text - assert "out.kinetic_ions.orbits" in text - assert "out.em_fields.E" in text + assert "Key" in text and "Description" in text + assert "en_tot" in text and "scalar time series" in text + assert "kinetic_ions/e1_v1_density/f" in text and "particle distribution" in text + assert "kinetic_ions" in text and "marker trajectories" in text + assert "em_fields/E" in text and "field" in text assert run.field_catalog._cache == {}, "listing must not load arrays" -def test_info_hints_distribution_and_density_symbols(run): +def test_info_labels_distribution_and_density_symbols(run): text = run.info() - assert "out.kinetic_ions.e1_v1_density.f ($f$)" in text - assert "out.kinetic_ions.e1_v1_density.delta_f ($\\delta f$)" in text - assert "out.kinetic_ions.view_0.n ($n$)" in text - assert "out.em_fields.E" in text and "($" not in text.split("out.em_fields.E")[1].split("\n")[0] + assert "particle distribution ($f$)" in text + assert "particle distribution ($\\delta f$)" in text + assert "SPH density ($n$)" in text def test_normalized_time_carries_seconds_as_a_coordinate(run): @@ -342,15 +361,15 @@ def test_a_failing_property_reports_its_own_error(tmp_path): run.domain -def test_parallel_processing_rejects_a_different_rank_count(tmp_path): - run = Output(write_tree(str(tmp_path)), comm=FakeComm(size=2)) +def test_parallel_processing_rejects_a_different_rank_count(tmp_path, monkeypatch): + run = output_with_comm(monkeypatch, write_tree(str(tmp_path)), FakeComm(size=2)) with pytest.raises(ValueError, match="same number of MPI ranks"): run.process(parallel=True) def test_saved_rank_count_does_not_block_serial_implicit_processing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) - run = Output(root, comm=FakeComm()) + run = output_with_comm(monkeypatch, root, FakeComm()) run.metadata["mpi_ranks"] = 8 (run.path_pproc / "manifest.json").unlink() calls = [] diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 7c36c1da8..f5b2e8a48 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -63,17 +63,18 @@ def test_timeseries_by_name_with_growth_fit(run): def test_output_owns_product_plotting(run): - result = run.timeseries("en_phi", fit=True) - assert result.fit_results[0].rate == pytest.approx(RATE) + fig, ax = run.timeseries("en_phi", fit=True) + assert fig is ax.figure phase_space = run.evaluate("kinetic_ions/e1_v1_density/f").isel(t=-1) - assert run.slice(phase_space, x="e1", y="v1").ax.get_xlabel() == r"$\eta_1$" + _, ax = run.slice(phase_space, x="e1", y="v1") + assert ax.get_xlabel() == r"$\eta_1$" - viewer = run.viewer("em_fields/E", x="e1", y="e2", component=0) - viewer.draw() - assert set(viewer.sliders) == {"t", "e3"} + fig, _ = run.viewer("em_fields/E", x="e1", y="e2", component=0) + assert set(fig._struphy_viewer.sliders) == {"t", "e3"} - assert run.trajectories("kinetic_ions", max_markers=2).ax.name == "3d" + _, ax = run.trajectories("kinetic_ions", max_markers=2) + assert ax.name == "3d" def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): @@ -91,9 +92,9 @@ def test_timeseries_into_given_axes_keeps_the_figure_layout(run): def test_scalar_overview_draws_every_scalar_in_one_axes(run): - result = run.plot_scalars() - assert sorted(line.get_label() for line in result.artists) == ["en_phi", "en_tot"] - assert result.fig.axes == [result.ax] + fig, ax = run.plot_scalars() + assert sorted(line.get_label() for line in ax.lines) == ["en_phi", "en_tot"] + assert fig.axes == [ax] def test_slices_panels_and_viewer_take_keyword_views(run): diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 5005e6380..121c11ebe 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -892,7 +892,7 @@ def output(self) -> Output: post-processed on first access, or explicitly with ``sim.output.pproc(...)``. """ if self._output is None or self._output.path_out != Path(self.env.path_out).resolve(): - self._output = Output(self.env.path_out, comm=self.comm) + self._output = Output(self.env.path_out) return self._output # ------------------------------------------------------------------ From 29ebcdcb403885b121e0fae8acae7e6957c20933 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 18:07:57 +0200 Subject: [PATCH 054/193] Added doc/markdown/output-api.md --- doc/markdown/output-api.md | 142 +++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 doc/markdown/output-api.md diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md new file mode 100644 index 000000000..bf33a949a --- /dev/null +++ b/doc/markdown/output-api.md @@ -0,0 +1,142 @@ +# Working with simulation output + +`Output` is a lightweight handle to one completed Struphy run. It reads saved metadata when it +is created, but does not load field or particle data until requested. + +```python +from struphy import Output + +out = Output("path/to/run") +``` + +When working directly after a simulation, `Simulation.run()` returns the same kind of object: + +```python +out = sim.run() +``` + +## Discover available data + +Use `keys()` to list the names accepted by `evaluate()`, or `info()` for the same names with +short descriptions. + +```python +print(out.info()) + +for key in out.keys(): + print(key) +``` + +`keys()` and `info()` do not load product arrays. If post-processed products do not exist yet, +they materialize them using default post-processing options. Call `pproc()` first when those +options matter. + +## Materialize post-processing products + +Use `pproc()` explicitly to choose how fields and particle diagnostics are generated. + +```python +out.pproc( + physical=True, # also create physical field components and coordinates + step=1, # use every saved time step + celldivide=1, +) +``` + +Matching existing products are reused. `evaluate()` also calls `pproc()` automatically for a +missing non-scalar product when running serially. + +## Evaluate data + +`evaluate()` returns an ordinary `xarray.DataArray`. Use xarray for selections, arithmetic, +reductions, and interoperability with other scientific Python packages. + +```python +rho = out.evaluate("diagnostics/rho_xyz") +phi = out.evaluate("phi_integral") + +# Last saved time and one vector component, selected by integer position +electric_field = out.evaluate("em_fields/E", isel={"t": -1, "component": 2}) + +# Select the nearest logical-coordinate plane +midplane = out.evaluate( + "diagnostics/rho_xyz", + sel={"e3": 0.5}, + method="nearest", + drop=True, +) + +# Select a time range +history = out.evaluate("phi_integral", isel={"t": slice(100, None)}) +``` + +`isel` uses integer positions and `sel` uses named dimension-coordinate values. Selections are +applied in that order. Physical `X`, `Y`, and `Z` coordinates describe the mapped logical grid; +evaluating at an arbitrary physical point requires interpolation or an inverse-coordinate map. + +To return only values, without xarray coordinates and attributes, use `as_numpy=True`. + +```python +rho_values = out.evaluate("diagnostics/rho_xyz", isel={"t": -1}, as_numpy=True) +``` + +`out.xarray` provides the complete lazy xarray `DataTree` when access to the grouped product +store is useful. Prefer `evaluate(key)` for normal single-product work. + +## Plot data + +Struphy-aware plotting is performed by `Output`, not by modifying xarray arrays. Rendering +methods return `(fig, ax)` (or `(fig, axes)` for panels), so normal Matplotlib controls display, +saving, and further customization. + +```python +from matplotlib import pyplot as plt + +fig, ax = out.timeseries("phi_integral", fit=(0.0, None), fit_amplitude=True) +ax.set_title("Potential growth") + +fig, ax = out.viewer( + "diagnostics/rho_xyz", + x="e1", + y="e2", + coords="physical", + plane="RZ", +) + +fig, axes = out.panels("kinetic_ions/e1_v1_density/f", x="e1", y="v1") +fig, ax = out.trajectories("kinetic_ions", max_markers=1000) + +plt.show() +``` + +Plotting methods also accept a derived `DataArray` instead of a saved-product name. + +```python +rho_last = out.evaluate("diagnostics/rho_xyz", isel={"t": -1}) +fig, ax = out.slice(rho_last, x="e1", y="e2", coords="physical", plane="RZ") +``` + +The available product plotting methods are `timeseries`, `slice`, `panels`, `viewer`, +`animation`, and `trajectories`. `view` creates a reusable view configuration, while `frames` +writes PNG files and returns their paths. Whole-run plots are `plot_scalars` and `equilibrium`. + +## MPI post-processing + +`Output` always uses `MPI.COMM_WORLD`; no communicator is passed to its constructor. + +For serial post-processing under MPI, call `pproc()` on every rank. Rank 0 does the work and the +other ranks wait at the synchronization barrier. + +```python +out.pproc(physical=True) +``` + +For parallel post-processing, also call it on every rank and pass `parallel=True`. The current +world communicator must have the same number of ranks as the run that wrote the raw output. + +```python +out.pproc(parallel=True, physical=True) +``` + +Automatic materialization through `evaluate()` is intentionally disabled when more than one MPI +rank is active. Call `pproc()` explicitly first in that case. From 763e3d2669b501e1b73610e76a6f4987ae25aa60 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 18:24:00 +0200 Subject: [PATCH 055/193] Added data-oriented helpers --- doc/markdown/output-api.md | 30 ++++++ src/struphy/post_processing/output.py | 96 ++++++++++++++++++- .../post_processing/tests/test_output.py | 13 +++ 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index bf33a949a..b511c1053 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -25,6 +25,9 @@ print(out.info()) for key in out.keys(): print(key) + +# Machine-readable product metadata +catalog = out.catalog(details=True) ``` `keys()` and `info()` do not load product arrays. If post-processed products do not exist yet, @@ -80,6 +83,33 @@ To return only values, without xarray coordinates and attributes, use `as_numpy= rho_values = out.evaluate("diagnostics/rho_xyz", isel={"t": -1}, as_numpy=True) ``` +For domains with an analytical inverse map, evaluate a field at a physical point directly: + +```python +value = out.evaluate( + "em_fields/phi_xyz", + physical={"X": 1.0, "Y": 0.0, "Z": 0.2}, +) +``` + +## Analyze and report data + +Numerical helpers stay on `Output` and return values or xarray arrays rather than figures. + +```python +fit = out.growth_rate("phi_integral", window=(20.0, 60.0), amplitude=True) +energy_error = out.relative_error("en_tot") +energy_drift = out.drift("en_tot") +``` + +Write a compact data report with metadata and the product catalog. Add selected products to +record their dimensions and units. + +```python +report = out.report("report", products=["en_tot", "diagnostics/rho_xyz"]) +html_report = out.report("report", format="html") +``` + `out.xarray` provides the complete lazy xarray `DataTree` when access to the grouped product store is useful. Prefer `evaluate(key)` for normal single-product work. diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 6a32f1453..6f616da0f 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -9,6 +9,7 @@ from functools import cached_property from pathlib import Path from typing import Any +from html import escape import h5py import numpy as np @@ -237,6 +238,7 @@ def evaluate( method: str | None = None, drop: bool = False, as_numpy: bool = False, + physical: Mapping[str, float] | None = None, ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. @@ -249,13 +251,25 @@ def evaluate( usual ``.sel``/``.isel`` meanings. Set ``as_numpy=True`` to return only the selected values as a :class:`numpy.ndarray`. - Physical auxiliary coordinates such as ``X``, ``Y`` and ``Z`` describe the evaluated - logical grid; selecting an arbitrary physical point requires a separate interpolation or - inverse-coordinate operation. + ``physical={"X": x, "Y": y, "Z": z}`` evaluates a field at a physical point when + its domain supplies an analytical ``inverse_map``. It converts the point to logical + coordinates and uses xarray interpolation. """ + physical_sel = None + if physical: + required = {"X", "Y", "Z"} + if set(physical) != required: + raise ValueError("physical selection requires exactly X, Y and Z") + inverse = getattr(self.domain, "inverse_map", None) + if inverse is None: + raise NotImplementedError(f"{type(self.domain).__name__} has no inverse_map for physical evaluation") + eta = inverse(*(float(physical[axis]) for axis in ("X", "Y", "Z"))) + physical_sel = dict(zip(("e1", "e2", "e3"), map(float, eta))) array = self[name] if isel: array = array.isel(isel, drop=drop) + if physical_sel: + array = array.interp(physical_sel, method=method or "linear") if sel: options = {"drop": drop} if method is not None: @@ -265,6 +279,24 @@ def evaluate( raise ValueError("method requires a coordinate selection through sel") return array.to_numpy() if as_numpy else array + def growth_rate(self, product: str | xr.DataArray, *, window=(None, None), amplitude: bool = False): + """Fit exponential growth of a scalar product and return a ``FitResult``.""" + from struphy.diagnostics.analysis import GrowthFit, growth_rate + + return growth_rate(self._array(product), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def drift(self, product: str | xr.DataArray, *, ref=None) -> xr.DataArray: + """Return the deviation of a time series from a reference or its initial value.""" + from struphy.diagnostics.analysis import drift + + return drift(self._array(product), ref=ref) + + def relative_error(self, product: str | xr.DataArray, *, ref=None, skip_first: bool = True) -> xr.DataArray: + """Return the absolute relative deviation of a time series.""" + from struphy.diagnostics.analysis import relative_error + + return relative_error(self._array(product), ref=ref, skip_first=skip_first) + def keys(self) -> tuple[str, ...]: """Return the names accepted by :meth:`evaluate`, without loading their arrays. @@ -274,6 +306,23 @@ def keys(self) -> tuple[str, ...]: catalogs = (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog) return tuple(sorted((*self.scalars.data_vars, *(key for catalog in catalogs for key in catalog)))) + def catalog(self, *, details: bool = False) -> xr.Dataset: + """Return a structured catalog of evaluable products. + + Set ``details=True`` to include dimensions and units; this opens each product but leaves + its numerical values lazily backed by the product store. + """ + keys = self.keys() + data = { + "kind": ("product", [self._product_kind(key) for key in keys]), + "description": ("product", [self._product_description(key) for key in keys]), + } + if details: + arrays = [self.evaluate(key) for key in keys] + data["dimensions"] = ("product", [", ".join(array.dims) for array in arrays]) + data["units"] = ("product", [str(array.attrs.get("units", "")) for array in arrays]) + return xr.Dataset(data, coords={"product": list(keys)}) + def _array(self, product: str | xr.DataArray) -> xr.DataArray: """Resolve a saved product name or accept an already-derived xarray array.""" if isinstance(product, str): @@ -790,6 +839,17 @@ def _product_description(self, key: str) -> str: return f"SPH density ({label})" return "marker trajectories" + def _product_kind(self, key: str) -> str: + if key in self.scalars.data_vars: + return "scalar" + if key in self.field_catalog: + return "field" + if key in self.distribution_catalog: + return "distribution" + if key in self.density_catalog: + return "density" + return "orbits" + @staticmethod def _quantity_hint(key: str) -> str: """Static label for a catalog key, e.g. distinguishing ``f`` from ``delta_f``.""" @@ -811,6 +871,36 @@ def save_report(self, directory=None, **kwargs) -> list[str]: directory = Path(directory) if directory else self.path_pproc / "report" return save_all_scalars(self.scalars, directory, run_label=self.label, **kwargs) + def report(self, directory=None, *, products=(), format: str = "markdown") -> str: + """Write a compact, reproducible data report and return its path. + + The report records run metadata, the full product catalog, and dimensions/units of any + explicitly requested products. ``format`` is ``"markdown"`` or ``"html"``. + """ + if format not in {"markdown", "html"}: + raise ValueError("format must be 'markdown' or 'html'") + directory = Path(directory) if directory else self.path_pproc / "report" + directory.mkdir(parents=True, exist_ok=True) + catalog = self.catalog(details=False) + requested = [self.evaluate(key) for key in products] + rows = [ + (str(key), str(kind), str(description)) + for key, kind, description in zip(catalog.product.values, catalog.kind.values, catalog.description.values) + ] + if format == "markdown": + lines = [f"# Struphy output report", "", f"- Path: `{self.path_out}`", f"- Run: {self.label}", "", "## Products", "", "| Key | Kind | Description |", "| --- | --- | --- |"] + lines += [f"| `{key}` | {kind} | {description} |" for key, kind, description in rows] + if requested: + lines += ["", "## Requested data", "", "| Key | Dimensions | Units |", "| --- | --- | --- |"] + lines += [f"| `{array.name}` | {', '.join(array.dims)} | {array.attrs.get('units', '')} |" for array in requested] + path = directory / "report.md" + path.write_text("\n".join(lines) + "\n") + else: + body = "".join(f"{escape(key)}{escape(kind)}{escape(description)}" for key, kind, description in rows) + path = directory / "report.html" + path.write_text(f"

Struphy output report

{escape(str(self.path_out))}
{escape(self.label)}

{body}
KeyKindDescription
") + return str(path) + @property def tree(self) -> xr.DataTree: """The product store as an :class:`xarray.DataTree`, read lazily.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index fbd307b1b..9a1688a43 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -2,6 +2,7 @@ import json import os +from pathlib import Path import h5py import numpy as np @@ -127,6 +128,18 @@ def test_keys_list_every_evaluable_product_without_loading_arrays(run): assert run.distribution_catalog._cache == {} +def test_catalog_is_structured_and_report_is_written(run, tmp_path): + catalog = run.catalog() + assert list(catalog.product.values) == list(run.keys()) + assert set(catalog.data_vars) == {"kind", "description"} + assert "dimensions" in run.catalog(details=True) + + report = run.report(tmp_path / "report", products=["en_tot"]) + text = Path(report).read_text() + assert "Struphy output report" in text + assert "en_tot" in text + + def test_field_has_named_and_curvilinear_coordinates(run): field = run.fields["em_fields/E"] assert field.dims == ("t", "component", "e1", "e2", "e3") From 051ab663e84f969b4435f68e8c01cc685e9d1667 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 18:27:51 +0200 Subject: [PATCH 056/193] Added cli commands --- src/struphy/console/main.py | 23 +++++++++++++++++- src/struphy/post_processing/output.py | 34 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/struphy/console/main.py b/src/struphy/console/main.py index 95ecaee0c..c6f45ec21 100644 --- a/src/struphy/console/main.py +++ b/src/struphy/console/main.py @@ -88,6 +88,9 @@ def struphy(): # 6. "format" and "lint" sub-commands add_parser_format(subparsers) + # 7. output inspection and post-processing + add_parser_output(subparsers) + # parse argument argcomplete.autocomplete(parser) args = parser.parse_args() @@ -130,6 +133,7 @@ def struphy(): "params": ("struphy.console.params", "struphy_params"), "profile": ("struphy.console.profile", "struphy_profile"), "test": ("struphy.console.test", "struphy_test"), + "output": ("struphy.console.output", "struphy_output"), } # import struphy.console.MODULE.FUNC_NAME as func @@ -140,6 +144,7 @@ def struphy(): raise ValueError(f"Unknown command: {args.command}") # transform parser Namespace object to dictionary and remove "command" key + is_output = args.command == "output" kwargs = vars(args) for key in [ "command", @@ -149,12 +154,13 @@ def struphy(): "hybrid", # These options are stored in kwargs.config "input_type", - "path", "linters", "iterations", "output_format", ]: kwargs.pop(key, None) + if not is_output: + kwargs.pop("path", None) # start sub-command function with all parameters of that function # for k, v in kwargs.items(): @@ -423,6 +429,21 @@ def add_parser_likwid_profile(subparsers): ) +def add_parser_output(subparsers): + """Add the lightweight command-line interface for completed simulation output.""" + parser = subparsers.add_parser("output", help="inspect, process, report, or plot a simulation output") + parser.add_argument("action", choices=("info", "keys", "pproc", "report", "plot")) + parser.add_argument("path", help="simulation output directory") + parser.add_argument("--physical", action="store_true", help="materialize physical field components") + parser.add_argument("--parallel", action="store_true", help="use MPI.COMM_WORLD for parallel pproc") + parser.add_argument("--format", choices=("markdown", "html"), default="markdown", help="report format") + parser.add_argument("--directory", help="report directory") + parser.add_argument("--kind", choices=("timeseries", "slice", "panels", "viewer", "trajectories"), default="timeseries") + parser.add_argument("--product", help="product key for plot") + parser.add_argument("--x", help="first displayed dimension") + parser.add_argument("--y", help="second displayed dimension") + + def add_parser_test(subparsers, list_models): try: import pytest_mpi diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 6f616da0f..84e2d8181 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -205,6 +205,28 @@ def with_time_units(self, time_units: str) -> "Output": """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" return type(self)(self.path_out, time_units=time_units) + def clear_cache(self): + """Close lazy product files and discard loaded arrays while retaining metadata.""" + self._reset() + + close = clear_cache + + def __enter__(self): + return self + + def __exit__(self, *_): + self.close() + + @staticmethod + def compare(first: "Output", second: "Output", product: str, *, method: str = "linear") -> xr.Dataset: + """Align one product from two runs and return both values, their difference and ratio.""" + left = first.evaluate(product) + right = second.evaluate(product) + right = right.interp_like(left, method=method) + difference = left - right + ratio = xr.where(right != 0, left / right, np.nan) + return xr.Dataset({"first": left, "second": right, "difference": difference, "ratio": ratio}) + def _reset(self): if getattr(self, "_tree", None) is not None: self._tree.close() # an open store would block the next process() from writing it @@ -323,6 +345,18 @@ def catalog(self, *, details: bool = False) -> xr.Dataset: data["units"] = ("product", [str(array.attrs.get("units", "")) for array in arrays]) return xr.Dataset(data, coords={"product": list(keys)}) + def provenance(self, product: str | None = None) -> dict: + """Return stored post-processing provenance and current raw-output freshness.""" + from struphy.post_processing.post_processing_tools import source_fingerprint + + path = self.path_pproc / "manifest.json" + manifest = json.loads(path.read_text()) if path.exists() else {} + manifest["current"] = manifest.get("source_fingerprint") == source_fingerprint(str(self.path_out)) + if product is not None: + manifest["product"] = product + manifest["available"] = product in self.keys() + return manifest + def _array(self, product: str | xr.DataArray) -> xr.DataArray: """Resolve a saved product name or accept an already-derived xarray array.""" if isinstance(product, str): From 34022593388d2e10b5f390bab3d4444bcb3a8a63 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 18:30:07 +0200 Subject: [PATCH 057/193] fix path --- src/struphy/post_processing/output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 84e2d8181..de87bf690 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -913,10 +913,10 @@ def report(self, directory=None, *, products=(), format: str = "markdown") -> st """ if format not in {"markdown", "html"}: raise ValueError("format must be 'markdown' or 'html'") - directory = Path(directory) if directory else self.path_pproc / "report" - directory.mkdir(parents=True, exist_ok=True) catalog = self.catalog(details=False) requested = [self.evaluate(key) for key in products] + directory = Path(directory) if directory else self.path_pproc / "report" + directory.mkdir(parents=True, exist_ok=True) rows = [ (str(key), str(kind), str(description)) for key, kind, description in zip(catalog.product.values, catalog.kind.values, catalog.description.values) From c5fab2d0dd9ede8df33d2317c152e98e893f22ae Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 18 Sep 2026 18:32:27 +0200 Subject: [PATCH 058/193] Improve the report --- src/struphy/post_processing/output.py | 35 ++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index de87bf690..f50972cfc 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -905,11 +905,12 @@ def save_report(self, directory=None, **kwargs) -> list[str]: directory = Path(directory) if directory else self.path_pproc / "report" return save_all_scalars(self.scalars, directory, run_label=self.label, **kwargs) - def report(self, directory=None, *, products=(), format: str = "markdown") -> str: + def report(self, directory=None, *, products=(), format: str = "markdown", max_scalar_rows: int = 200) -> str: """Write a compact, reproducible data report and return its path. The report records run metadata, the full product catalog, and dimensions/units of any - explicitly requested products. ``format`` is ``"markdown"`` or ``"html"``. + explicitly requested products. ``format`` is ``"markdown"`` or ``"html"``. The full + scalar history is written as ``scalars.csv``; HTML embeds up to ``max_scalar_rows`` rows. """ if format not in {"markdown", "html"}: raise ValueError("format must be 'markdown' or 'html'") @@ -917,22 +918,44 @@ def report(self, directory=None, *, products=(), format: str = "markdown") -> st requested = [self.evaluate(key) for key in products] directory = Path(directory) if directory else self.path_pproc / "report" directory.mkdir(parents=True, exist_ok=True) + csv_path = save_scalars(self.scalars, str(directory / "scalars.csv")) rows = [ (str(key), str(kind), str(description)) for key, kind, description in zip(catalog.product.values, catalog.kind.values, catalog.description.values) ] + scalar_names = tuple(self.scalars.data_vars) + scalar_time = np.asarray(self.scalars.coords["t"]) if "t" in self.scalars.coords else np.empty(0) + scalar_values = np.column_stack([np.asarray(self.scalars[name]) for name in scalar_names]) if scalar_names else np.empty((len(scalar_time), 0)) + scalar_rows = len(scalar_time) + indices = np.linspace(0, scalar_rows - 1, min(scalar_rows, max_scalar_rows), dtype=int) if scalar_rows else [] + summaries = [] + for name, values in zip(scalar_names, scalar_values.T): + finite = values[np.isfinite(values)] + summaries.append((name, float(finite[0]) if finite.size else np.nan, float(finite[-1]) if finite.size else np.nan, float(finite.min()) if finite.size else np.nan, float(finite.max()) if finite.size else np.nan)) + requested_summary = [] + for array in requested: + values = np.asarray(array) + finite = values[np.isfinite(values)] + requested_summary.append((array.name, ", ".join(array.dims), str(array.attrs.get("units", "")), int(values.size), float(finite.min()) if finite.size else np.nan, float(finite.max()) if finite.size else np.nan)) if format == "markdown": lines = [f"# Struphy output report", "", f"- Path: `{self.path_out}`", f"- Run: {self.label}", "", "## Products", "", "| Key | Kind | Description |", "| --- | --- | --- |"] lines += [f"| `{key}` | {kind} | {description} |" for key, kind, description in rows] + lines += ["", "## Scalar summary", "", "| Scalar | Initial | Final | Min | Max |", "| --- | ---: | ---: | ---: | ---: |"] + lines += [f"| `{name}` | {initial:.6g} | {final:.6g} | {minimum:.6g} | {maximum:.6g} |" for name, initial, final, minimum, maximum in summaries] + lines += ["", f"Full scalar values: `{Path(csv_path).name}`"] if requested: - lines += ["", "## Requested data", "", "| Key | Dimensions | Units |", "| --- | --- | --- |"] - lines += [f"| `{array.name}` | {', '.join(array.dims)} | {array.attrs.get('units', '')} |" for array in requested] + lines += ["", "## Requested data", "", "| Key | Dimensions | Units | Values | Min | Max |", "| --- | --- | --- | ---: | ---: | ---: |"] + lines += [f"| `{name}` | {dims} | {units} | {size} | {minimum:.6g} | {maximum:.6g} |" for name, dims, units, size, minimum, maximum in requested_summary] path = directory / "report.md" path.write_text("\n".join(lines) + "\n") else: - body = "".join(f"{escape(key)}{escape(kind)}{escape(description)}" for key, kind, description in rows) + product_body = "".join(f"{escape(key)}{escape(kind)}{escape(description)}" for key, kind, description in rows) + summary_body = "".join(f"{escape(name)}{initial:.6g}{final:.6g}{minimum:.6g}{maximum:.6g}" for name, initial, final, minimum, maximum in summaries) + values_body = "".join(f"{float(scalar_time[index]):.6g}" + "".join(f"{value:.6g}" for value in scalar_values[index]) + "" for index in indices) + requested_body = "".join(f"{escape(str(name))}{escape(dims)}{escape(units)}{size}{minimum:.6g}{maximum:.6g}" for name, dims, units, size, minimum, maximum in requested_summary) path = directory / "report.html" - path.write_text(f"

Struphy output report

{escape(str(self.path_out))}
{escape(self.label)}

{body}
KeyKindDescription
") + style = "body{max-width:1200px;margin:2rem auto;padding:0 1rem;background:#f7f8fa;color:#1f2937;font:15px system-ui,sans-serif}h1,h2{color:#123b5d}.cards{display:flex;gap:1rem;flex-wrap:wrap}.card{background:white;padding:1rem;border-radius:8px;box-shadow:0 1px 3px #0002;min-width:220px}table{border-collapse:collapse;width:100%;background:white;margin:1rem 0}th{background:#123b5d;color:white;text-align:left}th,td{padding:.55rem;border-bottom:1px solid #dbe1e8}tr:nth-child(even){background:#f3f6f9}code{color:#8a2558}.scroll{overflow:auto}.muted{color:#52606d}a{color:#075985}" + path.write_text(f"Struphy output report

Struphy output report

Run
{escape(self.label)}
Output directory
{escape(str(self.path_out))}
Products
{len(rows)}
Scalar samples
{scalar_rows}

Scalar summary

{summary_body}
ScalarInitialFinalMinMax

Showing {len(indices)} of {scalar_rows} rows. Download all scalar values (CSV).

{''.join(f'' for name in scalar_names)}{values_body}
t{escape(name)}

Products

{product_body}
KeyKindDescription
{'

Requested data

'+requested_body+'
KeyDimensionsUnitsValuesMinMax
' if requested_body else ''}") return str(path) @property From 886a5f2635bd595a96ebe4eed0d4725c205b9a06 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 18 Sep 2026 19:48:13 +0200 Subject: [PATCH 059/193] Added some helpers --- doc/markdown/output-api.md | 19 +++++++ src/struphy/diagnostics/analysis.py | 35 ++++++++++++ src/struphy/post_processing/output.py | 43 +++++++++++++++ .../tests/test_output_accessors.py | 54 +++++++++++++++++++ .../post_processing/xarray_accessors.py | 18 +++++++ 5 files changed, 169 insertions(+) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index b511c1053..7e0a478d6 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -102,6 +102,25 @@ energy_error = out.relative_error("en_tot") energy_drift = out.drift("en_tot") ``` +For oscillating signals such as the field energy in Landau damping, `damping_rate` fits the +exponential to the local maxima (`envelope`) instead of the raw series. `norm` reduces a field +to a time series (by default over every dimension except `t`), which can then be fitted. + +```python +damping = out.damping_rate("electric_energy", window=(0.0, 8.0), amplitude=True) +peaks = out.envelope("electric_energy") + +growth = out.growth_rate(out.norm("diagnostics/rho", squared=True), amplitude=True) +``` + +Fields carry mapped `X`, `Y`, `Z` coordinates; binned products (such as `e1_e2_density`) do not. +`with_physical_coords` attaches them by evaluating the run's domain on the array's logical grid. + +```python +density = out.with_physical_coords("kinetic_ions/e1_e2_density/f").isel(t=-1) +radius = np.hypot(density.X, density.Y) +``` + Write a compact data report with metadata and the product catalog. Add selected products to record their dimensions and units. diff --git a/src/struphy/diagnostics/analysis.py b/src/struphy/diagnostics/analysis.py index 47ae10da3..a9a42d187 100644 --- a/src/struphy/diagnostics/analysis.py +++ b/src/struphy/diagnostics/analysis.py @@ -35,6 +35,8 @@ def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") fit = fit or GrowthFit() time, values = np.asarray(data.t), np.asarray(data) + if len(time) < 2: + return None lo = time[0] if fit.window[0] is None else fit.window[0] hi = time[-1] if fit.window[1] is None else fit.window[1] lo, hi = sorted((lo, hi)) @@ -49,6 +51,39 @@ def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | return FitResult(float(rate), float(intercept), selected_time, fitted) +def envelope(data: xr.DataArray) -> xr.DataArray: + """Local maxima of a time series: the interior samples not smaller than their neighbours.""" + validate_array(data, required_dims=("t",)) + if data.dims != ("t",): + raise ValueError(f"envelope input must have dims ('t',), got {data.dims}") + values = np.asarray(data) + peak = np.zeros(len(values), dtype=bool) + peak[1:-1] = (values[1:-1] > values[:-2]) & (values[1:-1] >= values[2:]) + return data.isel(t=np.flatnonzero(peak)) + + +def damping_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: + """Fit ``exp(rate*t + intercept)`` to the envelope of an oscillating time series. + + Use this for signals such as the field energy in Landau damping, where :func:`growth_rate` on + the raw series would fit the oscillation. ``fit.window`` restricts the peaks that are used. + The rate is negative for damping. + """ + return growth_rate(envelope(data), fit) + + +def norm(data: xr.DataArray, *, dims=None, squared: bool = False) -> xr.DataArray: + """L2 norm over ``dims`` (default: every dimension except ``t``), as a function of the rest.""" + validate_array(data) + dims = [dim for dim in data.dims if dim != "t"] if dims is None else list(dims) + total = (data**2).sum(dims) + out = total if squared else np.sqrt(total) + out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + label = _label(data) + out.attrs["label"] = f"squared norm of {label}".strip() if squared else f"norm of {label}".strip() + return out + + def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: """Signed deviation from an explicit reference or the first time sample.""" validate_array(data, required_dims=("t",)) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index f50972cfc..d7d1b3f09 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -307,6 +307,49 @@ def growth_rate(self, product: str | xr.DataArray, *, window=(None, None), ampli return growth_rate(self._array(product), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + def damping_rate(self, product: str | xr.DataArray, *, window=(None, None), amplitude: bool = False): + """Fit exponential decay to the envelope of an oscillating scalar; returns a ``FitResult``.""" + from struphy.diagnostics.analysis import GrowthFit, damping_rate + + return damping_rate(self._array(product), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def envelope(self, product: str | xr.DataArray) -> xr.DataArray: + """Return the local maxima of a time series, e.g. to overlay on the signal.""" + from struphy.diagnostics.analysis import envelope + + return envelope(self._array(product)) + + def norm(self, product: str | xr.DataArray, *, dims=None, squared: bool = False) -> xr.DataArray: + """Return the L2 norm over ``dims`` (default: all but ``t``), as a function of time.""" + from struphy.diagnostics.analysis import norm + + return norm(self._array(product), dims=dims, squared=squared) + + def with_physical_coords(self, product: str | xr.DataArray) -> xr.DataArray: + """Attach mapped ``X``, ``Y``, ``Z`` coordinates to a product on a logical grid. + + Fields already carry them; this is for products that do not, such as binned densities. + The coordinates are evaluated with the run's domain on the array's ``e1``, ``e2``, ``e3`` + grid; a missing logical dimension is evaluated at ``0.5``. + """ + array = self._array(product) + if all(name in array.coords for name in ("X", "Y", "Z")): + return array + dims = tuple(dim for dim in ("e1", "e2", "e3") if dim in array.dims) + if not dims: + raise ValueError(f"{array.name!r} has no logical dimensions e1, e2, e3; its dimensions are {array.dims}") + missing = [dim for dim in dims if dim not in array.coords] + if missing: + raise ValueError(f"{array.name!r} has no coordinate values for {missing}") + grids = [np.asarray(array.coords[dim]) if dim in dims else np.array([0.5]) for dim in ("e1", "e2", "e3")] + mapped = self.domain(*grids) + shape = tuple(len(grid) for grid in grids) + for name, values in zip(("X", "Y", "Z"), mapped): + values = np.asarray(values).reshape(shape) + keep = tuple(slice(None) if dim in dims else 0 for dim in ("e1", "e2", "e3")) + array = array.assign_coords({name: (dims, values[keep])}) + return array + def drift(self, product: str | xr.DataArray, *, ref=None) -> xr.DataArray: """Return the deviation of a time series from a reference or its initial value.""" from struphy.diagnostics.analysis import drift diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index f5b2e8a48..0a3a1b83a 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -195,3 +195,57 @@ def test_products_by_name_and_by_attribute_agree(run): def test_selection_rejects_unknown_dimensions(run): with pytest.raises(TypeError, match="not a dimension"): run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", time=-1) + + +def oscillating_energy(rate=-0.3, omega=3.0): + import xarray as xr + + time = np.linspace(0.0, 20.0, 4001) + values = np.exp(2 * rate * time) * np.cos(omega * time) ** 2 + 1e-12 + return xr.DataArray(values, dims="t", coords={"t": time}, name="energy") + + +def test_damping_rate_fits_the_envelope_not_the_oscillation(run): + energy = oscillating_energy(rate=-0.3) + fit = run.damping_rate(energy, amplitude=True) + assert fit.rate == pytest.approx(-0.3, rel=1e-2) + assert energy.struphy.analysis.damping_rate(window=(2.0, 10.0), amplitude=True).rate == pytest.approx(-0.3, rel=1e-2) + + peaks = run.envelope(energy) + assert 0 < peaks.sizes["t"] < energy.sizes["t"] // 10 + assert np.all(peaks > 1e-3 * np.exp(-0.6 * peaks.t)) + + +def test_damping_rate_without_peaks_is_none(run): + assert run.damping_rate(run["en_phi"]) is None + + +def test_norm_reduces_all_but_time(run): + e_field = run.evaluate("em_fields/E") + squared = run.norm(e_field, squared=True) + assert squared.dims == ("t",) + np.testing.assert_allclose(squared, (np.asarray(e_field) ** 2).sum(axis=(1, 2, 3, 4))) + np.testing.assert_allclose(run.norm("em_fields/E") ** 2, squared) + assert e_field.struphy.analysis.norm(dims=["e1"]).dims == ("t", "component", "e2", "e3") + assert run.growth_rate(run.norm("em_fields/E", squared=True), amplitude=True) is not None + + +def test_physical_coords_are_attached_to_products_without_them(run): + density = run.evaluate("kinetic_ions/view_0/n") + assert "X" not in density.coords + mapped = run.with_physical_coords(density) + expected = run.domain(*(np.asarray(density[dim]) for dim in ("e1", "e2", "e3"))) + for name, values in zip(("X", "Y", "Z"), expected): + assert mapped[name].dims == ("e1", "e2", "e3") + np.testing.assert_allclose(mapped[name], values) + + plane = run.with_physical_coords(density.isel(e3=0, drop=True)) + assert plane.X.dims == ("e1", "e2") + + phase_space = run.with_physical_coords("kinetic_ions/e1_v1_density/f") + assert phase_space.X.dims == ("e1",) + + field = run.evaluate("em_fields/E") + assert run.with_physical_coords(field) is field + with pytest.raises(ValueError, match="no logical dimensions"): + run.with_physical_coords("en_tot") diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index f19174f6b..3ffc73ae5 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -387,6 +387,24 @@ def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None return growth_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + def damping_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): + """Fit exponential decay to the envelope of this oscillating time series; see ``growth_rate``.""" + from struphy.diagnostics.analysis import GrowthFit, damping_rate + + return damping_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def envelope(self) -> xr.DataArray: + """Local maxima of this time series.""" + from struphy.diagnostics.analysis import envelope + + return envelope(self._array) + + def norm(self, *, dims=None, squared: bool = False) -> xr.DataArray: + """L2 norm over ``dims`` (default: every dimension except ``t``).""" + from struphy.diagnostics.analysis import norm + + return norm(self._array, dims=dims, squared=squared) + def drift(self, *, ref=None) -> xr.DataArray: """Signed deviation of this time series from ``ref`` or from its first sample.""" from struphy.diagnostics.analysis import drift From e2187b2acc5373fc1f6694629ce47ed25c849a38 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 08:18:58 +0200 Subject: [PATCH 060/193] Added reductions and profiling data --- doc/markdown/output-api.md | 74 +++ src/struphy/diagnostics/analysis.py | 87 +++ src/struphy/post_processing/output.py | 41 ++ src/struphy/post_processing/profiling.py | 138 +++++ src/struphy/post_processing/si.py | 65 +++ .../tests/test_derived_products.py | 301 ++++++++++ .../post_processing/xarray_accessors.py | 19 + tutorials/tutorial_post_processing.ipynb | 533 +++++++++++++++++- 8 files changed, 1236 insertions(+), 22 deletions(-) create mode 100644 src/struphy/post_processing/profiling.py create mode 100644 src/struphy/post_processing/si.py create mode 100644 src/struphy/post_processing/tests/test_derived_products.py diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 7e0a478d6..2998b5832 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -132,6 +132,80 @@ html_report = out.report("report", format="html") `out.xarray` provides the complete lazy xarray `DataTree` when access to the grouped product store is useful. Prefer `evaluate(key)` for normal single-product work. +## Reduce a distribution function + +A binned distribution usually has more dimensions than a question needs. `spatial_average` +averages over the logical space dimensions `e1`, `e2` and `e3` (or the ones passed as `dims`), +so an `e1_v1` product becomes f(v1, t). The mean is uniform in the logical coordinates, which is +the volume average on a Cartesian domain; on a mapped domain it is not weighted by the Jacobian. + +`velocity_moments` integrates over the velocity dimensions instead and returns a dataset with the +`density`, and the mean `mean_v1` and variance `variance_v1` along every velocity direction, as +functions of the remaining dimensions. In normalized units the variance is the temperature divided +by the mass. Mean and variance are NaN where the density is not positive, and a `delta_f` product +has only the density (its perturbation). + +```python +f = "kinetic_ions/e1_v1_density/f" + +f_of_v = out.spatial_average(f) # dimensions (t, v1) +moments = out.velocity_moments(f) # density, mean_v1, variance_v1 over (t, e1) +temperature_over_mass = out.spatial_average(moments.variance_v1) +``` + +Both are also available on any product as `array.struphy.analysis.spatial_average()` and +`array.struphy.analysis.velocity_moments()`. + +## Convert to SI units + +Products are in the normalization of the model, whose units are `out.units`. `to_si` converts +the coordinates of a product whenever they are present: time `t` to seconds, the mapped `X`, `Y`, +`Z` to meters and the velocities `v1`, `v2`, `v3` to m/s. Values are converted only when `unit` +names the unit that the variable was normalized with, because a product does not record it: +`"x"`, `"B"`, `"n"`, `"v"`, `"t"`, `"p"`, `"rho"`, `"j"` or `"kBT"`. For a composite unit, pass a +number and its `label`. The original product is not modified, and converting coordinates twice +changes nothing. + +```python +print(out.units.x, out.units.v, out.units.t) # meters, m/s and seconds per unit + +f_si = out.to_si("kinetic_ions/e1_v1_density/f") # coordinates only: v1 in m/s, t in s + +# values, for a variable that the model normalizes with the unit B +b_si = out.to_si("em_fields/b_field", "B") # in tesla + +# values, for a quantity normalized with a product of units +flux_si = out.to_si(flux, out.units.n * out.units.v, label="m^-2 s^-1") +``` + +## Profile a run + +A run started with `sim.run(profiling_activated=True)` writes `profiling_data.h5`, and +`out.profile` reads it. Regions are the setup steps, every propagator (`prop: ...`), pusher, +accumulation, compiled kernel (`kernel: ...`) and linear solve. Regions nest, so the time of a +region includes the regions it calls; times of different regions must not be added up. + +`summary()` returns an xarray dataset along `region` with `calls` and `total_time` (per rank), +`mean_time`, `min_time` and `max_time` (per call, in seconds) and `fraction`, the share of the run. +It is sorted by `total_time` by default. `table()` prints the same as text. + +```python +print(out.profile.table(top=10)) + +kernels = out.profile.summary(prefix="kernel:", sort_by="total_time") +kernels.total_time.to_series() +``` + +`compare()` puts one statistic of several runs side by side, with NaN for a region that a run +does not have. It accepts `Output` objects or `Profile` objects. + +```python +times = out.profile.compare(other_out, metric="total_time", prefix="prop:") +ratio = times.isel(run=1) / times.isel(run=0) +``` + +For anything not covered here, `out.profile.results` is the full `scope_profiler` result. + ## Plot data Struphy-aware plotting is performed by `Output`, not by modifying xarray arrays. Rendering diff --git a/src/struphy/diagnostics/analysis.py b/src/struphy/diagnostics/analysis.py index a9a42d187..3615e2321 100644 --- a/src/struphy/diagnostics/analysis.py +++ b/src/struphy/diagnostics/analysis.py @@ -94,6 +94,93 @@ def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: return out +SPATIAL_DIMS = ("e1", "e2", "e3") +VELOCITY_DIMS = ("v1", "v2", "v3") + + +def _provenance(data: xr.DataArray) -> dict: + return {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + + +def _select_dims(data: xr.DataArray, dims, default) -> list[str]: + if dims is None: + selected = [dim for dim in default if dim in data.dims] + if not selected: + raise ValueError(f"{data.name!r} has none of the dimensions {default}; its dimensions are {data.dims}") + return selected + selected = [dims] if isinstance(dims, str) else list(dims) + missing = [dim for dim in selected if dim not in data.dims] + if missing: + raise ValueError(f"{data.name!r} has no dimensions {missing}; its dimensions are {data.dims}") + return selected + + +def spatial_average(data: xr.DataArray, *, dims=None) -> xr.DataArray: + """Mean over the logical space dimensions, e.g. a binned f(t, e1, v1) becomes f(t, v1). + + ``dims`` defaults to every one of ``e1``, ``e2``, ``e3`` that ``data`` has. The mean is + uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a + mapped domain it is not weighted by the Jacobian. Physical ``X``, ``Y``, ``Z`` coordinates + that depend on the averaged dimensions are dropped. + """ + validate_array(data) + averaged = _select_dims(data, dims, SPATIAL_DIMS) + out = data.mean(averaged, keep_attrs=True) + out.attrs["label"] = f"average of {_label(data)}".strip() + out.attrs.pop("long_name", None) + return out + + +def _bin_widths(data: xr.DataArray, dim: str) -> xr.DataArray: + coordinate = np.asarray(data.coords[dim]) if dim in data.coords else None + if coordinate is None or len(coordinate) < 2: + raise ValueError(f"dimension {dim!r} needs a coordinate with at least two bins") + return xr.DataArray(np.gradient(coordinate), dims=(dim,), coords={dim: data.coords[dim]}) + + +def velocity_moments(f: xr.DataArray, *, dims=None) -> xr.Dataset: + """Moments of a binned distribution function over its velocity dimensions. + + ``dims`` defaults to every one of ``v1``, ``v2``, ``v3`` that ``f`` has; the moments are + functions of the remaining dimensions, for example ``(t, e1)`` for an ``e1_v1`` product. + The integrals are sums over the bins, weighted by the bin widths. + + Returns a Dataset with + + * ``density``: the zeroth moment, :math:`\\int f\\,\\mathrm{d}v`. + * ``mean_``: the mean velocity :math:`u = \\int v f\\,\\mathrm{d}v / n` along each dimension. + * ``variance_``: :math:`\\int (v-u)^2 f\\,\\mathrm{d}v / n`. In normalized units this is the + temperature over the particle mass along that direction, :math:`T/m`. + + Where the density is not positive, the mean and variance are NaN. A ``delta_f`` product has + only the density, which is then the density perturbation, because its mean and variance are + not defined. The values keep the normalization of the run; see ``Output.to_si``. + """ + validate_array(f) + integrated = _select_dims(f, dims, VELOCITY_DIMS) + volume = 1.0 + for dim in integrated: + volume = volume * _bin_widths(f, dim) + density = (f * volume).sum(integrated) + + label = _label(f) + variables = {"density": (density, f"density of {label}")} + if f.name != "delta_f": + weight = density.where(density > 0) + for dim in integrated: + mean = (f * f[dim] * volume).sum(integrated) / weight + variance = (f * (f[dim] - mean) ** 2 * volume).sum(integrated) / weight + variables[f"mean_{dim}"] = (mean, f"mean {dim}") + variables[f"variance_{dim}"] = (variance, f"variance of {dim}") + + provenance = _provenance(f) + out = {} + for name, (values, description) in variables.items(): + values.attrs = {**provenance, "label": description.strip()} + out[name] = values.rename(name) + return xr.Dataset(out, attrs=provenance) + + def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: """Absolute relative deviation from an explicit reference or first sample.""" validate_array(data, required_dims=("t",)) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index d7d1b3f09..7815d1dc9 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -19,6 +19,8 @@ from struphy.post_processing import store from struphy.post_processing.arrays import BINNED_LABELS, data_array, save_scalars from struphy.post_processing.output_accessors import OutputPlots +from struphy.post_processing.profiling import Profile +from struphy.post_processing.si import to_si logger = logging.getLogger("struphy") @@ -231,6 +233,7 @@ def _reset(self): if getattr(self, "_tree", None) is not None: self._tree.close() # an open store would block the next process() from writing it self._time = self._grids_log = self._grids_phy = self._scalars = self._products = self._label = None + self._profile = None self._tree = None self._species = None self._seconds = None @@ -325,6 +328,21 @@ def norm(self, product: str | xr.DataArray, *, dims=None, squared: bool = False) return norm(self._array(product), dims=dims, squared=squared) + def spatial_average(self, product: str | xr.DataArray, *, dims=None) -> xr.DataArray: + """Mean of a product over ``e1``, ``e2``, ``e3`` (or ``dims``), e.g. f(t, v1) from f(t, e1, v1).""" + from struphy.diagnostics.analysis import spatial_average + + return spatial_average(self._array(product), dims=dims) + + def velocity_moments(self, product: str | xr.DataArray, *, dims=None) -> xr.Dataset: + """Density, mean velocity and variance of a binned distribution, as functions of the other dimensions. + + See :func:`struphy.diagnostics.analysis.velocity_moments`. + """ + from struphy.diagnostics.analysis import velocity_moments + + return velocity_moments(self._array(product), dims=dims) + def with_physical_coords(self, product: str | xr.DataArray) -> xr.DataArray: """Attach mapped ``X``, ``Y``, ``Z`` coordinates to a product on a logical grid. @@ -473,6 +491,29 @@ def equilibrium(self, ax=None): result = OutputPlots(self).equilibrium(ax=ax) return result.fig, result.ax + @property + def units(self): + """The units of the run's normalization, in SI; see :class:`struphy.physics.physics.Units`.""" + return self.model.units + + def to_si(self, product: str | xr.DataArray, unit: str | float | None = None, *, label: str | None = None) -> xr.DataArray: + """A product in SI units: coordinates always, values when ``unit`` names their normalization. + + ``unit`` is one of ``x``, ``B``, ``n``, ``v``, ``t``, ``p``, ``rho``, ``j``, ``kBT``, or a + number for a composite unit, described by ``label``. See :func:`struphy.post_processing.si.to_si`. + """ + return to_si(self._array(product), self.units, unit, label=label) + + @property + def profile(self) -> Profile: + """The timing regions of this run; needs ``sim.run(profiling_activated=True)``.""" + if self._profile is None: + path = self.path_out / "profiling_data.h5" + if not path.is_file(): + raise FileNotFoundError(f"no profiling data in {self.path_out}; run with sim.run(profiling_activated=True)") + self._profile = Profile(path, label=self.label) + return self._profile + def _stamp(self, array: xr.DataArray) -> xr.DataArray: array.attrs.update(run=self.label, run_name=self.path_out.name) if self.time_units == "normalized" and "t" in array.dims and self.seconds_per_time is not None: diff --git a/src/struphy/post_processing/profiling.py b/src/struphy/post_processing/profiling.py new file mode 100644 index 000000000..2b7d73fda --- /dev/null +++ b/src/struphy/post_processing/profiling.py @@ -0,0 +1,138 @@ +"""Timing regions of a run recorded with ``sim.run(profiling_activated=True)``.""" + +from __future__ import annotations + +from functools import cached_property +from pathlib import Path + +import numpy as np +import xarray as xr + +SESSION_REGION = "scope_profiler.session" +METRICS = ("calls", "total_time", "mean_time", "min_time", "max_time", "fraction") + + +class Profile: + """The timing regions of one run, read from its ``profiling_data.h5``. + + Obtain it from :attr:`Output.profile`. Every region is a named span of the run, such as a + propagator (``prop: ``), a compiled kernel (``kernel: ``) or a linear solve. + Regions nest, so a region's time includes the regions it calls: times of different regions + must not be added up. All times are in seconds. + + With several MPI ranks, ``calls`` and ``total_time`` are averages over the ranks, and + ``mean_time``, ``min_time`` and ``max_time`` refer to single calls on any rank. + """ + + def __init__(self, path, label: str = ""): + self.path = Path(path) + self.label = label or self.path.parent.name + + def __repr__(self): + return f"{type(self).__name__}({str(self.path)!r})" + + @cached_property + def results(self): + """The full ``scope_profiler`` results of this run, for anything not covered here.""" + from scope_profiler import read_h5 + + return read_h5(self.path) + + @property + def num_ranks(self) -> int: + return int(self.results.num_ranks) + + def summary( + self, *, prefix: str | None = None, ranks=None, sort_by: str = "total_time", top: int | None = None + ) -> xr.Dataset: + """Statistics of every region, as a Dataset along the dimension ``region``. + + Parameters + ---------- + prefix: + Keep only regions whose name starts with this, e.g. ``"kernel:"`` or ``"prop:"``. + ranks: + An MPI rank or a list of ranks to use; all ranks by default. + sort_by: + The variable to sort the regions by, largest first: ``calls``, ``total_time``, + ``mean_time``, ``min_time``, ``max_time`` or ``fraction``. + top: + Keep only this many regions after sorting. + + The variables are ``calls`` and ``total_time`` per rank, ``mean_time``, ``min_time`` and + ``max_time`` per call, and ``fraction``, the ``total_time`` as a share of the whole run. + """ + if sort_by not in METRICS: + raise ValueError(f"cannot sort by {sort_by!r}; choose one of {METRICS}") + from scope_profiler import collect_region_statistics + + statistics = collect_region_statistics(self.results, ranks=ranks)["files"][0] + regions = statistics["region_statistics"] + n_ranks = len(next(iter(regions.values()))["per_rank"]) if regions else 1 + n_ranks = max(n_ranks, 1) + + def number(value): + return np.nan if value is None else float(value) + + names = [name for name in regions if prefix is None or name.startswith(prefix)] + rows = { + "calls": [number(regions[name]["count"]) / n_ranks for name in names], + "total_time": [number(regions[name]["total_duration_seconds"]) / n_ranks for name in names], + "mean_time": [number(regions[name]["average_duration_seconds"]) for name in names], + "min_time": [number(regions[name]["min_duration_seconds"]) for name in names], + "max_time": [number(regions[name]["max_duration_seconds"]) for name in names], + } + session = regions.get(SESSION_REGION) + run_time = number(session["total_duration_seconds"]) / n_ranks if session else statistics["total_time_seconds"] + rows["fraction"] = [value / run_time for value in rows["total_time"]] + + out = xr.Dataset( + {key: ("region", np.asarray(values)) for key, values in rows.items()}, coords={"region": names} + ) + out["calls"].attrs["label"] = "calls per rank" + for key in ("total_time", "mean_time", "min_time", "max_time"): + out[key].attrs.update(label=key.replace("_", " "), units="s") + out["fraction"].attrs["label"] = "share of the run" + out.attrs.update(run=self.label, num_ranks=n_ranks, run_time=run_time) + # stable and NaN-last, so regions that tie keep the order in which the profiler lists them + order = np.argsort(-np.nan_to_num(out[sort_by].values, nan=-np.inf), kind="stable") + return out.isel(region=order[:top]) + + def table(self, **kwargs) -> str: + """The :meth:`summary` as a text table; use ``print(profile.table(top=10))``.""" + summary = self.summary(**kwargs) + width = max((len(name) for name in summary.region.values), default=6) + lines = [ + f"Profile: {self.label} ({summary.attrs['num_ranks']} rank(s), {summary.attrs['run_time']:.3f} s)", + "", + f"{'Region':<{width}} {'Calls':>8} {'Total [s]':>10} {'Mean [ms]':>10} {'Share':>7}", + f"{'-' * width} {'-' * 8} {'-' * 10} {'-' * 10} {'-' * 7}", + ] + for name in summary.region.values: + row = summary.sel(region=name) + lines.append( + f"{name:<{width}} {row.calls.item():>8.0f} {row.total_time.item():>10.3f} " + f"{row.mean_time.item() * 1e3:>10.3f} {row.fraction.item() * 100:>6.1f}%" + ) + return "\n".join(lines) + + def compare(self, *others, metric: str = "total_time", prefix: str | None = None, ranks=None) -> xr.DataArray: + """One :meth:`summary` variable of this and other runs, side by side. + + ``others`` are :class:`Profile` objects, or anything with a ``profile`` attribute such as an + :class:`Output`. The result has the dimensions ``region`` and ``run``; a region missing + from a run is NaN. Regions are sorted by their largest value over the runs. For a ratio + between two runs, divide ``result.isel(run=1) / result.isel(run=0)``. + """ + if metric not in METRICS: + raise ValueError(f"cannot compare {metric!r}; choose one of {METRICS}") + profiles = [self] + [getattr(other, "profile", other) for other in others] + labels = [profile.label for profile in profiles] + if len(set(labels)) < len(labels): + labels = [f"{profile.label} [{profile.path.parent.name}]" for profile in profiles] + arrays = [profile.summary(prefix=prefix, ranks=ranks)[metric] for profile in profiles] + out = xr.concat(arrays, dim=xr.DataArray(labels, dims="run", name="run"), join="outer") + out.attrs = dict(arrays[0].attrs) + out.name = metric + order = np.argsort(-np.nan_to_num(out.max("run").values, nan=-np.inf), kind="stable") + return out.isel(region=order) diff --git a/src/struphy/post_processing/si.py b/src/struphy/post_processing/si.py new file mode 100644 index 000000000..9db60533c --- /dev/null +++ b/src/struphy/post_processing/si.py @@ -0,0 +1,65 @@ +"""Conversion of normalized products to SI units.""" + +from __future__ import annotations + +import xarray as xr + +# Attributes of :class:`struphy.physics.physics.Units` that can scale a product, with their SI unit. +UNITS = { + "x": "m", + "B": "T", + "n": "m^-3", + "v": "m/s", + "t": "s", + "p": "Pa", + "rho": "kg/m^3", + "j": "A/m^2", + "kBT": "keV", +} + +# Coordinates that are converted whenever they occur, and the unit that scales each. +COORDINATE_UNITS = {"t": "t", "X": "x", "Y": "x", "Z": "x", "v1": "v", "v2": "v", "v3": "v"} + + +def _unit_factor(units, name: str) -> float: + value = getattr(units, name, None) + if value is None: + raise ValueError(f"this run has no unit {name!r}") + return float(value) + + +def to_si(array: xr.DataArray, units, unit: str | float | None = None, *, label: str | None = None) -> xr.DataArray: + """A copy of ``array`` in SI units. + + Coordinates are converted whenever present: time ``t`` to seconds, the mapped ``X``, ``Y``, + ``Z`` to meters and the velocities ``v1``, ``v2``, ``v3`` to m/s, each with the matching unit + of ``units``. Logical coordinates ``e1``, ``e2``, ``e3`` are dimensionless and unchanged. + + The values are only converted if ``unit`` is given, because a product does not record which + unit its variable was normalized with. Pass the name of that unit, one of ``x``, ``B``, ``n``, + ``v``, ``t``, ``p``, ``rho``, ``j`` or ``kBT``, or a number for a composite unit, together with + ``label``, its name, e.g. ``unit=units.v * units.B, label="V/m"``. + """ + out = array.copy() + for name, unit_name in COORDINATE_UNITS.items(): + if name not in out.coords or out.coords[name].attrs.get("units") == UNITS[unit_name]: + continue + attrs = dict(out.coords[name].attrs, units=UNITS[unit_name]) + out = out.assign_coords({name: out.coords[name] * _unit_factor(units, unit_name)}) + out.coords[name].attrs.update(attrs) + if out.coords.get("t") is not None and "t_seconds" in out.coords: + out = out.drop_vars("t_seconds") # the time coordinate is in seconds now + + if unit is not None: + if out.attrs.get("units"): + raise ValueError(f"{array.name!r} already has units {out.attrs['units']!r}") + if isinstance(unit, str): + if unit not in UNITS: + raise ValueError(f"unknown unit {unit!r}; choose one of {tuple(UNITS)} or pass a number") + factor, si_label = _unit_factor(units, unit), UNITS[unit] + else: + factor, si_label = float(unit), label or "SI" + out = out * factor + out.attrs = {**array.attrs, "units": si_label} + out.name = array.name + return out diff --git a/src/struphy/post_processing/tests/test_derived_products.py b/src/struphy/post_processing/tests/test_derived_products.py new file mode 100644 index 000000000..80fc2dc82 --- /dev/null +++ b/src/struphy/post_processing/tests/test_derived_products.py @@ -0,0 +1,301 @@ +"""Tests for distribution reductions, SI conversion and profiling access.""" + +import os +import time + +import numpy as np +import pytest +import xarray as xr + +from struphy.diagnostics.analysis import spatial_average, velocity_moments +from struphy.post_processing.arrays import data_array +from struphy.post_processing.output import Output +from struphy.post_processing.tests.test_output import write_tree + +F = "kinetic_ions/e1_v1_density/f" + + +def gaussian(v, density, mean, variance): + return density * np.exp(-((v - mean) ** 2) / (2 * variance)) / np.sqrt(2 * np.pi * variance) + + +def binned(values, dims, coords, name="f"): + return data_array(values, dims, coords, name=name, label="$f$") + + +@pytest.fixture +def run(tmp_path): + return Output(write_tree(str(tmp_path)), time_units="normalized") + + +# --- reductions --------------------------------------------------------------------------------- + + +def test_moments_of_a_maxwellian_recover_its_parameters(): + v = np.linspace(-8, 8, 321) + density = np.array([1.0, 2.0])[:, None, None] # depends on t + mean = np.array([-0.5, 0.0, 0.5])[None, :, None] # depends on e1 + f = binned( + gaussian(v[None, None, :], density, mean, 0.64), + ("t", "e1", "v1"), + {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": v}, + ) + moments = velocity_moments(f) + assert moments.density.dims == ("t", "e1") + np.testing.assert_allclose(moments.density, np.broadcast_to(density[:, :, 0], (2, 3)), rtol=1e-8) + np.testing.assert_allclose(moments.mean_v1, np.broadcast_to(mean[:, :, 0], (2, 3)), atol=1e-8) + np.testing.assert_allclose(moments.variance_v1, 0.64, rtol=1e-8) + + +def test_moments_over_two_velocity_dimensions_are_taken_per_direction(): + v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) + f = binned( + (gaussian(v1[:, None], 1.0, 1.0, 0.5) * gaussian(v2[None, :], 3.0, -0.5, 0.25))[None], + ("t", "v1", "v2"), + {"t": [0.0], "v1": v1, "v2": v2}, + ) + moments = velocity_moments(f) + assert set(moments.data_vars) == {"density", "mean_v1", "variance_v1", "mean_v2", "variance_v2"} + np.testing.assert_allclose(moments.density, 3.0, rtol=1e-8) + np.testing.assert_allclose(moments.mean_v1, 1.0, atol=1e-8) + np.testing.assert_allclose(moments.variance_v1, 0.5, rtol=1e-8) + np.testing.assert_allclose(moments.mean_v2, -0.5, atol=1e-8) + np.testing.assert_allclose(moments.variance_v2, 0.25, rtol=1e-8) + + +def test_one_velocity_dimension_can_be_selected(): + v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) + f = binned(np.ones((1, 181, 121)), ("t", "v1", "v2"), {"t": [0.0], "v1": v1, "v2": v2}) + moments = velocity_moments(f, dims="v2") + assert moments.density.dims == ("t", "v1") + assert "mean_v1" not in moments + + +def test_delta_f_has_only_a_density(): + v = np.linspace(-3, 3, 7) + delta_f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}, name="delta_f") + assert tuple(velocity_moments(delta_f).data_vars) == ("density",) + + +def test_mean_and_variance_are_nan_without_particles(): + v = np.linspace(-3, 3, 7) + f = binned(np.zeros((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}) + moments = velocity_moments(f) + assert moments.density.item() == 0.0 + assert np.isnan(moments.mean_v1.item()) and np.isnan(moments.variance_v1.item()) + + +def test_moments_carry_the_run_and_a_label(): + f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": np.linspace(-3, 3, 7)}) + f.attrs.update(run="dt=0.1", run_name="sim_1") + moments = velocity_moments(f) + assert moments.attrs["run_name"] == "sim_1" + assert moments.mean_v1.attrs["run"] == "dt=0.1" + assert moments.density.attrs["label"] == "density of $f$" + + +def test_moments_reject_missing_velocity_dimensions_and_single_bins(): + no_velocity = binned(np.ones((2, 3)), ("t", "e1"), {"t": [0.0, 1.0], "e1": [0.1, 0.2, 0.3]}) + with pytest.raises(ValueError, match="none of the dimensions"): + velocity_moments(no_velocity) + with pytest.raises(ValueError, match="no dimensions"): + velocity_moments(no_velocity, dims="v1") + one_bin = binned(np.ones((1, 1)), ("t", "v1"), {"t": [0.0], "v1": [0.0]}) + with pytest.raises(ValueError, match="at least two bins"): + velocity_moments(one_bin) + + +def test_spatial_average_removes_the_space_dimensions_only(): + values = np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4) + f = binned(values, ("t", "e1", "v1"), {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": np.arange(4.0)}) + f.attrs["run_name"] = "sim_1" + mean = spatial_average(f) + assert mean.dims == ("t", "v1") + np.testing.assert_allclose(mean, values.mean(axis=1)) + assert mean.attrs["run_name"] == "sim_1" + assert mean.attrs["label"] == "average of $f$" + assert spatial_average(f, dims="e1").dims == ("t", "v1") + + +def test_spatial_average_drops_physical_coordinates_it_averaged_over(): + logical = {f"e{i + 1}": np.linspace(0, 1, n) for i, n in enumerate((3, 4, 1))} + mapped = np.meshgrid(*logical.values(), indexing="ij") + field = data_array( + np.ones((2, 3, 4, 1)), + ("t", "e1", "e2", "e3"), + {"t": [0.0, 1.0], **logical, "X": (("e1", "e2", "e3"), mapped[0])}, + name="E", + ) + mean = spatial_average(field) + assert mean.dims == ("t",) + assert "X" not in mean.coords + + +def test_spatial_average_needs_space_dimensions(): + series = data_array(np.ones(3), ("t",), {"t": [0.0, 1.0, 2.0]}, name="energy") + with pytest.raises(ValueError, match="none of the dimensions"): + spatial_average(series) + + +def test_reductions_are_available_from_the_run_and_the_accessor(run): + moments = run.velocity_moments(F) + # write_tree has f = 1 on v = -3..3 in unit bins: n = 7, u = 0 and = 4 + np.testing.assert_allclose(moments.density, 7.0) + np.testing.assert_allclose(moments.mean_v1, 0.0, atol=1e-12) + np.testing.assert_allclose(moments.variance_v1, 4.0) + assert moments.density.attrs["run_name"] == run.path_out.name + + average = run.spatial_average(F) + assert average.dims == ("t", "v1") + xr.testing.assert_identical(average, run[F].struphy.analysis.spatial_average()) + xr.testing.assert_identical(moments, run[F].struphy.analysis.velocity_moments()) + + +# --- SI units --------------------------------------------------------------------------------- + + +def test_coordinates_are_converted_and_values_left_alone(run): + units = run.units + f = run.to_si(F) + np.testing.assert_allclose(f.v1, run[F].v1 * units.v) + assert f.v1.attrs["units"] == "m/s" + np.testing.assert_allclose(f.t, run[F].t * units.t) + assert f.t.attrs["units"] == "s" + assert "t_seconds" not in f.coords + np.testing.assert_array_equal(f.e1, run[F].e1) # logical coordinates are dimensionless + np.testing.assert_array_equal(f, run[F]) + assert "units" not in f.attrs + assert run[F].v1.attrs.get("units") is None # the run's own product is untouched + assert "t_seconds" in run[F].coords + + +def test_mapped_coordinates_are_scaled_by_the_length_unit(run): + assert run.units.x == 2.0 + field = run.to_si("em_fields/E") + np.testing.assert_allclose(field.X, run["em_fields/E"].X * 2.0) + assert field.X.attrs["units"] == "m" + + +def test_values_are_converted_with_a_named_unit(run): + field = run.to_si("em_fields/E", "B") + np.testing.assert_allclose(field, run["em_fields/E"] * run.units.B) + assert field.attrs["units"] == "T" + assert field.name == "E" + assert field.attrs["run_name"] == run.path_out.name + + +def test_values_are_converted_with_a_composite_unit(run): + field = run.to_si("em_fields/E", run.units.v * run.units.B, label="V/m") + np.testing.assert_allclose(field, run["em_fields/E"] * run.units.v * run.units.B) + assert field.attrs["units"] == "V/m" + + +def test_conversion_is_idempotent_for_coordinates_and_refuses_values_twice(run): + once = run.to_si(F) + np.testing.assert_array_equal(run.to_si(once).v1, once.v1) + converted = run.to_si("em_fields/E", "B") + with pytest.raises(ValueError, match="already has units"): + run.to_si(converted, "B") + + +def test_unknown_units_are_rejected(run): + with pytest.raises(ValueError, match="unknown unit"): + run.to_si("em_fields/E", "furlong") + + +def test_physical_time_units_are_not_converted_twice(tmp_path): + physical = Output(write_tree(str(tmp_path)), time_units="physical") + np.testing.assert_allclose(physical.to_si(F).t, physical[F].t) + + +# --- profiling -------------------------------------------------------------------------------- + + +def write_profile(path_out, *, calls=3, setup=True): + from scope_profiler import ProfileManager, ProfilingOptions + + with ProfileManager.session( + options=ProfilingOptions(), + deactivate_profiling=False, + file_path=os.path.join(path_out, "profiling_data.h5"), + ): + if setup: + with ProfileManager.profile_region("setup: total"): + time.sleep(0.001) + for _ in range(calls): + with ProfileManager.profile_region("prop: A"): + with ProfileManager.profile_region("kernel: k"): + time.sleep(0.01) + + +@pytest.fixture +def profiled(tmp_path, capfd): + write_profile(write_tree(str(tmp_path))) + return Output(str(tmp_path)) + + +def test_a_run_without_profiling_says_how_to_enable_it(run): + with pytest.raises(FileNotFoundError, match="profiling_activated=True"): + run.profile + + +def test_summary_lists_every_region_with_times(profiled): + summary = profiled.profile.summary() + assert summary.sizes == {"region": 4} + assert list(summary.region.values[:1]) == ["scope_profiler.session"] + assert summary.region.values[-1] == "setup: total" + assert summary.calls.sel(region="prop: A").item() == 3 + assert summary.calls.sel(region="setup: total").item() == 1 + assert summary.total_time.sel(region="kernel: k").item() >= 0.03 + assert summary.mean_time.sel(region="kernel: k").item() >= 0.01 + assert summary.fraction.sel(region="scope_profiler.session").item() == pytest.approx(1.0) + assert 0 < summary.fraction.sel(region="prop: A").item() <= 1.0 + assert summary.attrs["run"] == profiled.label + assert summary.attrs["num_ranks"] == 1 + assert summary.total_time.attrs["units"] == "s" + + +def test_summary_filters_and_sorts(profiled): + profile = profiled.profile + assert list(profile.summary(prefix="kernel:").region.values) == ["kernel: k"] + assert len(profile.summary(top=2).region) == 2 + by_calls = profile.summary(sort_by="calls") + assert by_calls.region.values[0] == "prop: A" + with pytest.raises(ValueError, match="cannot sort by"): + profile.summary(sort_by="size") + + +def test_the_profile_is_cached_and_reset_with_the_output(profiled): + assert profiled.profile is profiled.profile + first = profiled.profile + profiled.clear_cache() + assert profiled.profile is not first + + +def test_table_is_readable_text(profiled): + table = profiled.profile.table(top=3) + lines = table.splitlines() + assert lines[0].startswith("Profile: ") and "1 rank(s)" in lines[0] + assert "Total [s]" in lines[2] + assert len(lines) == 4 + 3 + assert "scope_profiler.session" in table and "100.0%" in table + + +def test_runs_are_compared_side_by_side(tmp_path, capfd): + first = Output(write_tree(str(tmp_path / "a"))) + second = Output(write_tree(str(tmp_path / "b"))) + write_profile(first.path_out, calls=3) + write_profile(second.path_out, calls=2, setup=False) + + table = first.profile.compare(second, metric="calls") + assert set(table.dims) == {"region", "run"} + assert list(table.run.values) == [f"{first.label} [a]", f"{second.label} [b]"] # identical labels are told apart + assert table.sel(run=table.run.values[0], region="prop: A").item() == 3 + assert table.sel(run=table.run.values[1], region="prop: A").item() == 2 + assert np.isnan(table.sel(run=table.run.values[1], region="setup: total").item()) + assert table.name == "calls" + assert first.profile.compare(second, prefix="kernel:").region.values.tolist() == ["kernel: k"] + assert first.profile.compare(second.profile).shape == table.shape # a Profile works as well as an Output + + with pytest.raises(ValueError, match="cannot compare"): + first.profile.compare(second, metric="size") diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index 3ffc73ae5..c6ca13e1f 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -417,6 +417,25 @@ def relative_error(self, *, ref=None, skip_first: bool = True) -> xr.DataArray: return relative_error(self._array, ref=ref, skip_first=skip_first) + def spatial_average(self, *, dims=None) -> xr.DataArray: + """Mean over the logical space dimensions ``e1``, ``e2``, ``e3`` (or ``dims``). + + For a binned ``e1_v1`` distribution this is f(v1, t) averaged over space; see + :func:`struphy.diagnostics.analysis.spatial_average`. + """ + from struphy.diagnostics.analysis import spatial_average + + return spatial_average(self._array, dims=dims) + + def velocity_moments(self, *, dims=None) -> xr.Dataset: + """Density, mean velocity and variance of a binned distribution over its velocity dimensions. + + See :func:`struphy.diagnostics.analysis.velocity_moments` for the definitions. + """ + from struphy.diagnostics.analysis import velocity_moments + + return velocity_moments(self._array, dims=dims) + def dispersion(self, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs): """Space-time power spectrum of this field and fitted dispersion branches. diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 9a9fbb3a5..df16f2a9b 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -148,7 +148,7 @@ " grid=grids.TensorProductGrid(num_elements=(16, 1, 1)),\n", " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", ")\n", - "out = sim.run()\n", + "out = sim.run(profiling_activated=True)\n", "print(f\"Raw output: {sim.env.path_out}\")" ] }, @@ -11630,6 +11630,174 @@ "print(\"spectrum:\", spectrum.shape)" ] }, + { + "cell_type": "markdown", + "id": "reductions-md", + "metadata": {}, + "source": [ + "## Reducing distribution functions\n", + "\n", + "A binned distribution usually has more dimensions than the question needs. `.struphy.analysis.spatial_average()` averages over `e1`, `e2` and `e3`, so the $(\\eta_1, v_1)$ product becomes $f(v_1, t)$: how the velocity distribution of the whole plasma evolves, without the spatial structure. The mean is uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a mapped domain it is not weighted by the Jacobian." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "reductions-average", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('t', 'v1')\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "f_of_v = phase_space.struphy.analysis.spatial_average()\n", + "print(f_of_v.dims)\n", + "f_of_v.plot(x=\"t\", y=\"v1\")" + ] + }, + { + "cell_type": "markdown", + "id": "reductions-moments-md", + "metadata": {}, + "source": [ + "`.struphy.analysis.velocity_moments()` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean velocity `mean_v1` and the variance `variance_v1` along each velocity direction, all as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. For a `delta_f` product only the density (its perturbation) is returned, because a mean and variance of a perturbation are not defined. Where the density is not positive, mean and variance are NaN." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "reductions-moments", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Size: 79kB\n", + "Dimensions: (t: 101, e1: 32)\n", + "Coordinates:\n", + " * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.85 4.9 4.95 5.0\n", + " t_seconds (t) float64 808B 0.0 1.668e-10 ... 1.651e-08 1.668e-08\n", + " * e1 (e1) float64 256B 0.01562 0.04688 0.07812 ... 0.9531 0.9844\n", + "Data variables:\n", + " density (t, e1) float64 26kB 1.001 1.189 1.501 ... 0.6874 0.7502 0.7503\n", + " mean_v1 (t, e1) float64 26kB -0.03906 0.3207 ... -0.07846 -0.1825\n", + " variance_v1 (t, e1) float64 26kB 1.06 1.34 0.952 ... 0.7471 0.5391 0.9026\n", + "Attributes:\n", + " run: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)\n", + " run_name: vlasov_ampere_demo\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "moments = phase_space.struphy.analysis.velocity_moments()\n", + "print(moments)\n", + "\n", + "mean_density = moments.density.struphy.analysis.spatial_average()\n", + "mean_density.struphy.plot.timeseries(logy=False, title=\"Mean density of the binned distribution\")\n", + "\n", + "mean_variance = moments.variance_v1.struphy.analysis.spatial_average()\n", + "mean_variance.struphy.plot.timeseries(logy=False, title=\"Mean velocity variance\")" + ] + }, + { + "cell_type": "markdown", + "id": "units-md", + "metadata": {}, + "source": [ + "## Physical units\n", + "\n", + "Products are in the normalization of the model. `out.units` holds the units of that normalization and `out.to_si(product)` converts a product: the time coordinate to seconds, the mapped coordinates `X`, `Y`, `Z` to meters and the velocities `v1`, `v2`, `v3` to m/s. The values are converted only when `unit=` names the unit the variable was normalized with (`\"x\"`, `\"B\"`, `\"n\"`, `\"v\"`, `\"t\"`, `\"p\"`, `\"rho\"`, `\"j\"` or `\"kBT\"`), or a number for a composite unit together with its `label`, because a product does not record which unit its variable uses. The original product is not modified." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "units-si", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 length unit = 1.0 m, 1 velocity unit = 2.998e+08 m/s, 1 time unit = 3.336e-09 s\n", + "m/s s\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(f\"1 length unit = {out.units.x} m, 1 velocity unit = {out.units.v:.4g} m/s, 1 time unit = {out.units.t:.4g} s\")\n", + "\n", + "phase_space_si = out.to_si(phase_space)\n", + "print(phase_space_si.v1.attrs[\"units\"], phase_space_si.t.attrs[\"units\"])\n", + "phase_space_si.isel(t=-1).plot(x=\"e1\", y=\"v1\")" + ] + }, { "cell_type": "markdown", "id": "32", @@ -11731,7 +11899,7 @@ " grid=grids.TensorProductGrid(num_elements=(16, 1, 1)),\n", " derham_opts=DerhamOptions(degree=(2, 1, 1)),\n", ")\n", - "out_coarse = sim_coarse.run()\n", + "out_coarse = sim_coarse.run(profiling_activated=True)\n", "\n", "out.scalars.electric_energy.struphy.plot.timeseries(\n", " out_coarse.scalars.electric_energy,\n", @@ -11739,6 +11907,95 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "profiling-md", + "metadata": {}, + "source": [ + "## Profiling\n", + "\n", + "A run started with `sim.run(profiling_activated=True)` records how long each region of the code takes, and `out.profile` reads that record back. Regions are the setup steps, every propagator (`prop: ...`), pusher, accumulation, compiled kernel (`kernel: ...`) and linear solve. Regions nest, so a region's time includes the regions it calls and the times of different regions must not be added up. `summary()` returns a dataset along the dimension `region`, sorted by total time; `table()` prints it. Filter with `prefix` and limit with `top`." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "profiling-summary", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Profile: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1) (1 rank(s), 0.776 s)\n", + "\n", + "Region Calls Total [s] Mean [ms] Share\n", + "-------------------------- -------- ---------- ---------- -------\n", + "scope_profiler.session 1 0.776 775.640 100.0%\n", + "model.integrate 100 0.329 3.292 42.4%\n", + "save data 101 0.274 2.709 35.3%\n", + "prop: VlasovAmpereCoupling 100 0.253 2.532 32.6%\n", + "solve: SchurSolver 100 0.120 1.203 15.5%\n", + "setup: total 1 0.104 104.367 13.5%\n", + "accum: vlasov_maxwell 100 0.084 0.838 10.8%\n", + "prop: PushEta 100 0.075 0.755 9.7%" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "region\n", + "kernel: vlasov_maxwell 0.043829\n", + "kernel: push_v_with_efield 0.013737\n", + "kernel: push_eta_stage 0.010213\n", + "kernel: charge_density_0form 0.000802\n", + "Name: total_time, dtype: float64\n" + ] + } + ], + "source": [ + "print(out.profile.table(top=8))\n", + "\n", + "kernels = out.profile.summary(prefix=\"kernel:\")\n", + "print(kernels.total_time.to_series())" + ] + }, + { + "cell_type": "markdown", + "id": "profiling-compare-md", + "metadata": {}, + "source": [ + "`compare()` puts the same statistic of several runs side by side, with runs whose region is missing as NaN. Here the two runs of the previous section differ only in the time step, so the number of calls per propagator halves for `dt = 0.1`." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "profiling-compare", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Size: 32B\n", + "array([[100., 100.],\n", + " [ 50., 50.]])\n", + "Coordinates:\n", + " * run (run) \u001b[39m\u001b[32m1\u001b[39m density = out_sph.euler_fluid.view_0.n_sph.isel(e2=\u001b[32m0\u001b[39m, e3=\u001b[32m0\u001b[39m)\n\u001b[32m 2\u001b[39m density.plot(x=\u001b[33m\"t\"\u001b[39m, y=\u001b[33m\"e1\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/post_processing/output.py:106\u001b[39m, in \u001b[36mProductNamespace.__getattr__\u001b[39m\u001b[34m(self, name)\u001b[39m\n\u001b[32m 104\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mtype\u001b[39m(\u001b[38;5;28mself\u001b[39m)(\u001b[38;5;28mself\u001b[39m._mapping, key)\n\u001b[32m 105\u001b[39m location = \u001b[38;5;28mself\u001b[39m._prefix \u001b[38;5;129;01mor\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mproducts\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m; available names under \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mlocation\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mself\u001b[39m)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n", - "\u001b[31mAttributeError\u001b[39m: 'n_sph'; available names under 'euler_fluid/view_0': ('n',)" - ] + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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euGFF2jVqlXUo0cPGj16NAWDwvx0gh9++IHefvtt2rBhA/Xq1YtGjBhBpuOakzp15s2bRx988AFZlkV9+/alI444IunnM2bMoFmzZiWVNW/enK6//npqKIgcAAAAKEBr166l/fffn5599ll1/7bbbqMBAwZQVVVVjW2efvpp6tmzJy1YsEB1gq699loaPny46vikU2fw4ME0ceJE1anauHEjjRo1ik477bSk5/rwww/ptddeo3bt2sVvbdu2pYaEkSYAAIACXNN04403UpMmTWjOnDlqdGnChAnUtWtXevjhh+niiy921Y9Go3TBBRfQNddcozpCjDs+Xbp0UaNV3OnRqcNuueUWOuigg+KPffLJJ1OfPn3onHPOocMPPzxe3qFDB7r00kvJKzDSBAAA4KVOUya3NEybNi1pOo5HcY455hhVLvn1119p+/btqnMT06JFC+revTv961//0q7DEjtMjEemDMOg1atXu56TO3d33nmnmspraOg0AQAA5JFt27Yl3aqrq8W1TL/99hvtvvvuSeV8n9cjSXbZZRdq2bKlGpmKWbduHX377be0ZMkS7TqS5557Tk3l9evXL6mcHysSidDy5ctVh+6MM86ghoTpOQAAgDyanuMprUQ33HCDGq1JVFFREV9YnYhHhcrLy8WHN02THnroIRo3bhx988036nl4oXa3bt3U2iTdOk5ff/21mg68+uqrkzpxZ511lprGizn77LPVgvHjjjtOTec1hILtNP1YuRMV+YRUuwSWMBAXlQLdLHegW1hIr3MGv4WFdiGxTC94UgqyDAv1qjWDLMOOIDwWcdSTQvVsSxjAtDRDMKXTbU1bK0BRChvUOQVXyrY0NR/fjAhlQXdbIX+PzKgQWimFcUqvwfHFKub2ZTsdWAy3lOpphlv6ag+7ZMLHQQyQtKRATSnsVCqTwi19qetJ7eRtc5dpn1Yu7WDhs2RHhSDPiBBuSbV/56mHF55T+q4Sg3OF0Mpin/tDEpTKhA9TkVAWcHwg/KYQblnHIEvpO766MjlMszFEDvCZcImdoaIi9/vStGlT9e+WLVuSyvl+s2bNanyKU045hQ499FB6//331SgWr1eaPHmy6iClUyeGR6COPvpoOvXUU+mmm25K+lnnzp2T7vfu3Zv23Xdfmjt3LjpNAAAAkDnuMDlHkJy4Y9S+fXvXlNn3339Pe+65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U6tNKrFRKquqKnVl5DTVv4wySvIg6KeQc5qo8fPUEYicpqzkNMV+VyTOcORKxK7OaIotQuGsbg+4GXZ9HAk14JGkhx9+mGbPnq2m0UaPHk3Dhw9PSv4+8cQTadKkSTRo0CCtNqnw9B4WgwMAQDp4KUj79u1zstOqqqqoc+fOtHbt2owfq127dirDUHcgARpRp6kh8KLxX375hZo1a0ZlZWWqA8UfBqx10sexDdhv6cN+qzvsO+y3hjrmVq5cSYZh0C677KLO1M4V7jjxiU2Z4pOi0GHK42vP1Tc+6GN/LfAHgWGBeN1gv2G/1Tccc9hv9Y2jaurjj2ru6KCz430NthAcAAAAoDFBpwkAAABAQ0F3mngh+Q033KD+Bew3HG/ehc8q9huOOfCCglsIDgAAAFAXBT3SBAAAAKALnSYAAAAADeg0AQAAABRyThMv1Xrsscfo1VdfVSnigwcPposvvpgCgUBW2+Sjr7/+mv7+97/TihUraI899qArr7ySunbtWmP9p556iqZOnZpUxuGh//nPf6iQbNmyhZ555hl1PcSePXvSfffdl7INp97zZYDmzp1LTZs2pdNPP51OPvlkKiT8uZs5cyY9+uijKrH/zTffpLZt29ba5qKLLqJPP/00qWzAgAF0xx13UKHgIMRnn32W3nnnHXXs7bfffnTJJZfQrrvuWms7/lxPnjyZvvvuO5VZx99xffr0oULCxw5fnuuHH36gnXfemcaMGUNHH310rW34+KqurnYdh3xZLygcedtpuvbaa+mhhx5Sv/xLS0vpsssuU52B5557Lqtt8s2iRYuof//+6kuEO0vcCTj44IPVfqjpEgL8i46vE8gdzhi/P28PLdHGjRvVL62RI0eqTvbixYu1OgvHHnssbd68WV1Dkfcj73e+puL48eOpUPAvHd5/vXv3Vh1O5y8mCe/fbt260fnnnx8va926NRWSUaNG0Y477kgnnXSSCmB88MEHqVevXvTFF1/UeKmodevWqYub840/39xZ/cMf/kAffvghHXjggVQIXnrpJbrnnnto7Nixat99/vnndNxxx9G9995b6+du/vz5dOutt9Khhx4aL+vYsWM9bTV4hp2HNm7caAcCAfvJJ5+Ml/3nP//hswTtRYsWZa1NPjr55JPtQw89NH4/EonYu+++uz1hwoQa29x8881237597UIWDoft8vJy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ - "density = out_sph.euler_fluid.view_0.n_sph.isel(e2=0, e3=0)\n", + "density = out_sph.euler_fluid.view_0.n.isel(e2=0, e3=0)\n", "density.plot(x=\"t\", y=\"e1\")" ] }, @@ -11907,10 +12172,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "42", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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Gt/V1bUy30L84XAgVfm4nasGtzO3sybhv2f1buuXZaJkeLA5Y4wxxlg5cSDFGGOMMVZOHEipKSMjI3z++efimnE7qzv+e+Z21iT896xdbc2DzRljjDHGyokzUowxxhhj5cSBFGOMMcZYOXEgxRhjjDFWThxIqai//voLAwcORPfu3cVAurS0tEp5jrY7d+4cRo4cia5du2LWrFl49uzZC/ePi4vDN998g379+mHQoEFYsmQJ0tPTq+x41VVAQAAmTZqELl26YPz48bh9+7bCz7127RratWuH6dOnV+oxaoKYmBh88MEH4u95xIgROHHihELPO3jwIEaNGoWePXti8eLFyM7OrvRjVWeZmZn49ttv0aNHD/FdsG3btpc+5/Hjx3jvvffw2muvied8+eWXSExMrJLjVVc5OTnYvXu3aK+OHTsqXEhz48aN6Nu3r/h7/uGHHyr975kDKRW0Zs0a8WPTq1cv8eO+d+9e9O/f/4Vr/pTnOdru9OnT4ouwTp06mDdvHh49eoQOHTrIl/YpLi8vDy1bthRfojNmzMDYsWOxZcsW9O7dG7m5uVV+/OoiJCREBEL0Zfa///1PzK6hdn748KFCVYvfeustJCUl4f79+1VyvOoqIyMDnTt3hp+fHz788EM0btwYffr0weHDh1/4vIULF2LMmDFo3749PvvsM/Hj9cknn1TZcasjCjq3bt2Kd999F4MHDxbfB999912p+0dFRaFt27YIDQ3FRx99hHfeeQd79uwR3x2sdN26dcMff/wBd3d3XLlyBYpYtGgR5s6di+HDh2Pq1Knit3HcuHGoVBKmUnJyciR2dnaSr776Sn7fgwcPKBqSHD16tMKewySS9u3bS0aOHClvirS0NImlpaVk6dKlpTZPampqkdt+fn6inS9evMhNWorp06dLGjRoIMnPz5ff17JlS8mYMWNe2mb0+SxYsEAyfvx4SZcuXbiNX2D16tUSExMTSVJSkvw+aremTZuW+hwfHx+Jjo6O5J9//ilyf0ZGBrd1KS5duiT+zV+/fl1+37JlyyRmZmbPfT/I7NmzRzwnMTFRft+RI0fEfREREdzWpZC11+bNmyV6enqSl0lISJAYGRlJfv31V/l9p0+fFu3s6+srqSyckVIxvr6+ovtowIAB8vvq16+PunXrlpqmL89ztB11x9EZTuE2MzU1FangF7WZmZlZ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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" ] @@ -11927,10 +12215,173 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "id": "44", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r", + "Time stepping: 0%| | 0/40 [00:00\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " e_field: \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Creation of Struphy Fields done.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "Evaluating fields ...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r", + " 0%| | 0/41 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out_coaxial.em_fields.b_field_xyz.struphy.plot.slice(\n", " x=\"e1\",\n", @@ -11977,10 +12447,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "46", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/plotting.py:296: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.\n", + " mesh = ax.pcolormesh(xg, yg, values, shading=\"auto\", vmin=lo, vmax=hi, cmap=self.cmap)\n" + ] + }, + { + "data": { + "image/png": "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G+w77rjD93R+0Vk+Sf2+ffuyxx9/nC1evJi9+uqrga0fAJB53nvvPXbOOedk/IVi//792YMPPshWrFjBxYwuJLtFfUrylyTwKaecorw+XHDBBezyyy9nw4cPZwMHDmTnnnuu8h5ed7p0kSvbCQDwz2effcbOOOOMjO7C//u//2NDhw5lc+bMYQcddBDbunUrO/DAA/nzsQ6/+tWv2Pnnn88DP+he+OKLL2a//OUv0z5dusiV7QQgrzBAznLdddcZw4YNy+g6n376aSMcDhvz5s2LD/vjH/9olJWVGRs3bgxk3qCn88OsWbOMM888M6PfBwCQG9x5551Gz549M7pOqis6d+5s3H777abhGzZsMJqbmx3n/fTTTw263K9YscJxujfeeINPN3Xq1Piwv//970ZJSYmxbNkyz9P5RXf59fX1xtq1a/nnSy65JOO/CQAgWB555BGjvLw8o7u1oaHBWLlyJf/829/+NuX103eg+mvRokXxYZ9//jkf9t5778WHvfDCC0YoFOL3m16n88P69euNH/3oR8amTZtsp8mG7QQApJ8nn3ySn8OZ5OqrrzZee+0107CbbrrJKC4udn0e/v7773n98sorr8SHTZo0iX+HKVOmpG06P2zdupXXtatWrcrY9wEA6IHI7xyFmhpS9OHKlSv5m1sqjz32WNrXSxExY8aM4W8fBWeddRbbsWMHe/PNNwOZN+jpCHq7TFE95513Hn9b+tZbbzlu6/r163mkZCa/DwAg+/nf//7HW93U1dXF616qW9LNP//5T7Zz504eASJDLUnC4WAu5VRX1dTUsH333Tc+jL4fRZjLUdW60xHbt29njz76KI9aufDCC3nTzaC2o2PHjqxr164pfWcAQHbw4Ycf8vvY3bt3x+vWu+66K+3rbdu2LY/gDgrRooaiyOU6rVu3buzII4+MD6NocUrV9N///tfzdMSuXbv4/qII7J///Of8npWaxNtBdTFNQ/eedqRjOwEA2QWl4BQp4kRde/PNN6d9vb///e/ZCSecYBq2//778/R1y5Ytc5yX6pV27dqZ5qdUeHQPLLfWCXo6Ys+ePezf//43TzNI97JPPfUUvx+1g65hVNeSe8jU9wEA6IGc3zkKNRsaNGgQa2ho4DecBDU71MlL6AQ1H6ebVztmzJjBm9nL0A0wSYiZM2c6Llt33qCnW7p0KU8RQBcP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rFhhlJaWGvfcc0982A033GB06NDB2LRpk+fp/vKXvxh9+vRJuoelbZs+fbpy+5csWcK3i9ZhR9DbCQDITt5++21eH6xfvz5j69y8ebOx9957G4MHDzbWrl1rOx2No7qW7hUFtJ3t2rUzfv/738eH3XHHHUZZWRm/P07XdP/617+Mbt26GQsXLjRt41dffWV8/fXXyu3fsGED37d0L2tH0NsJANAD8juHWbRokdG+fXtj//335xeJf/zjHxlZ75/+9CejTZs2xrhx4/hFrHv37lx2y5CMoYpffkjQnTfo6UgwX3DBBXxfHXXUUcaECROMvn37GieeeKLtTbqO/E7H9wEAZD9r1qwxunTpwl+qUd37wAMPZGS99MKO5MIBBxzA6zKqU+QbYlF3Ud37+uuvx4c98cQTRv/+/Y2DDz7YOO644/i2jxo1ypg8ebJS7NNyafljxozhgvzjjz/2NV1zc7Nx5ZVX8hv3I444wjj++OONAQMGGMccc4yxatUqx++qux20/6kMGjSITy/+luU/ACA3IDlSXV1tjBgxwjj99NONO++8MyPrPe+883i9UVtbaxQXF8frEfke9rvvvuN165NPPhkfRkENJHIOOugg46STTjKGDh3K61e657Py3HPPcbF++OGHGz/4wQ94Xf7mm2/6nu7GG2/kdethhx1mnHDCCXw7Dj30UGPx4sW+5Xc6thMAkH1s376d349RnUV17U033ZT2dV544YW8DqJ6StSxqnu2BQsW8OnoRaL1Hpie5Q855BBe77Vt25a/MLUS9HQkm8V05A5onx144IG2cltHfqdjOwEA7oToH90ocZB9UC/AU6dO5b2rU8dn++67b0bWu2bNGr5ean5/8MEHJ+Xronxen376KU81Ind2pjNvuqZbu3Ytz41LnUwMHz6cd65mB+XSotQnp59+esr7wut0AIDsh3L6UV8KlA+wd+/eGctzSk0fKY82NVmlTs2s/QZQ3fXBBx+wQw891JRrkPJsU6eXdK2gPIGUk9au2Sstg75bq1ateF1F6UlSmY5yzX799de8cyFKNSWa5buhs3zqVE6F9fsDAHIDatJN5z3VG9RhMHWSm25eeumleCdlMlSvi3zhdL/91ltvmYYRNN/333/P7/EoPyt1MGx3f0f175dffsnrXqrT7FJWeZmO7ispJy01r5c7ZbNCKbKo8zZKQ+V2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MVaOX8ZcGXujTgKYP//5z+rUU09tGGtHjx6t5syZk+9DAxDQ7t271b333qsGDBhg/nuW7bLLLlPr1q2L1GspmYFkB5IhyLm5ZApRLziT43/ppZfUmWeeab4PyueO4cOHq3/84x/5PjQgegwUjOuuu86orKw05s2bZ369bNky4/jjjze++tWvGlFx7733Gm3atDHmz59vfv3nP//ZKC0tNV599VUjymbPnm20bNnSeOqpp4xDhw4Zu3fvNq688kqjRYsWxtq1a/N9eAACuPXWW43jjjvOeOutt4xYLGasXr3aGDp0qLkdOXIkEq/l448/blRUVBhz5swxv545c6ZRXl5uPP/880aUyfuf/FwPP/ywceDAAaO6utq49tprjWbNmhnLly/P9+EBCHhO2LNnT2PWrFlGbW2tsX79emP06NFG3759jYMHD0bitZQxVcZWGWOFjLkyRskYHGUrV640mjRpYtx3333Gvn37jP379xuTJk0yysrKGj6HAIiGRx55xOjSpYsxY8YMc6ytqqoyzjzzTKNHjx7meVQUSFYgmYFkB0KyBMkU5H0kyjZt2mT+XHfccYexd+9e873vrrvuMkpKSow33ngj34cHREqJ/F++A3hkxh//+Ed16aWXmlcE46ZPn66uvPJK9emnn6rjjjsu9FPUpSLmhhtuUP/zP//TcPsFF1ygqqur1TvvvKOiasGCBeafUo0Yt2/fPvMKrvyst912Wx6PDkAQUu127rnnmlUlcX//+9/VhRdeqBYuXKhOPvnk0L+gxxxzjBo3bpxZkRh3xRVXqGXLlqmlS5eqqJJj37VrlzrjjDMSWk3JWCszo375y1/m9fgA+CfVbl/5yldU165dG26bO3euOu2009Ts2bPV2WefHfqXc/DgwWrIkCHqueeea7jtxhtvNN8z/vOf/6iokorQVatWqfPPP7/hNvlIKefxl19+ufrd736X1+MD4N/MmTPNcUrODeM++ugjddJJJ6lXXnlFXXzxxaF/OeV9Qc71rBXRP//5z9UTTzxhzswsLy9XUbRt2zb1z3/+U337299OuF1yHZltI1kPAH9oe1JArr766oTgW5SW1v2KS0pKVNjJm6yEFtIOxOqss84yp6wfOnRIRZWE3tbgO/47iW8AokOm3VuD76iNtWvWrFGff/65dqyV8FtaDUSVfHizBt/W308UfjcAGklB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MOH266+/Xi1cuFCNHTs2b8cGAIU01r744ovqnnvuSbj9pptuUnPmzDHPewEA6ZGxdObMmer2229PuP22225Tf//739WFF17ISwwUOcJvhMro0aPVzTffrCZNmqSqqqrM2x599FH19ttvm9UxzZo1y/chAkDkVVZWmjNtZFu9erV52/Tp09Urr7yi/vjHP6pWrVrl+xABIPJ69+5tzrSRbfHixeZtEsTIxUepVOzQoUO+DxEAIq9bt25mQcfDDz+s3n33XfO2f/3rX+bsm8cee0x17do134cIIM9KDCmrBULk4MGDaujQoapv375mFfiJJ56ofvazn6lf/OIX+T40ACgYR44cUaNGjVLNmzdXzz//vBoyZIi65ppr1AMPPJDvQwOAgiEftcaMGaN27dqlXnvtNTVs2DCzSvEPf/hDvg8NAArK+eefr9auXaveeecd8xxXCuteeOGFfB8WgBAg/EYoyRXb008/XXXq1En16tVLvffee6qsrCzfhwUABWXp0qXq5JNPNqsP27dvrz744ANVUVGR78MCgIIiYYxcYGzTpo05i3HZsmWqdevW+T4sACgoMnNcZjdKYYf4+OOPzfNbAKDtCULp1FNPVRdddJHavHmz+vWvf03wDQBZIGGMLLomY+2vfvUrgm8AyAKZzXjttdeaY+1dd91F8A0AWdCjRw/1ox/9yBxrZdY4wTeAOCq/EUpLlixRI0eOVOXl5eprX/ua+r//+798HxIAFGQ1orSZkmn5I0aMMPsjlpSU5PuwAKCgbNy40axGPHTokBowYIB6//33zXNcAEDmbN++3Rxrq6ur1THHHGOutcCaYQAEld8InZqaGjVhwgSz+vsf//iHuTDQc889l+/DAoCCEovFzLF24MCBZugt7aZkUSAAQGbJegqdO3c22/itXLlS3X///bzEAJBh119/vdnyZMGCBWYLlClTpvAaAzARfiN07rjjDrV+/Xr1zDPPqDPPPFP98Ic/VD/+8Y/Vli1b8n1oAFAwpKXUhx9+qKZNm2bOtLn11lvNTcZfAEBmPPHEE+rNN99Uf/rTn8yZNnfffbe5SS9aAEBmTJ8+Xb3yyivq2WefVSeccIJ5nvub3/xGLVq0iJcYAG1PEC5SEXPaaaepqVOnqquvvtq8bd++fWrw4MHqpJNOUi+99FK+DxEAIk8WW5OFLqX68KabbmqYdTN8+HCzOnH27Nm0PwGANK1bt85cW+GWW25pqECUWTcyu7G2tlbNmzePdW0AIE1ffPGF2e5EZtk88MAD5m3S0u/ss89WW7duNRd0b9q0Ka8zUMTo+Y3QkJD7xBNPVIMGDVIzZsxIuG/OnDnmm9fzzz+vLr/88rwdIwBEnYTcUul91FFHmWOrtce3VMeMHj1aPfroo+q6667L63ECQJRJyH3GGWeoAwcOqPnz5yf0+F61apVZ1CGBuMy4AQCkbuzYsWabEwm5KyoqGm6X2YxSRCeFHvfccw8vMVDECL8RGp9//rnZn2vMmDGqQ4cOSffLlNGysjLzfgBAajZv3qzmzp1rVh527do16f63337bXJTt61//Oi8xAKRox44d5noKcrGxV69eSfdL1bfsM27cOF5jAEjRnj17zBmLw4YNU3369Em6f+HCheaiw9/4xjd4jYEiRvgNAAAAAAAAACg4LHgJAAAAAAAAACg4hN8AAAAAAAAAgIJD+A0AAAAAAAAAKDiE3wAAAAAAAACAgkP4DQAAAAAAAAAoOITfAAAAAAAAAICCQ/gNAAAAAAAAACg4hN8AAAAAAAAAgIJD+A0AAAAAAAAAKDiE3wAAAAAAAACAgkP4DQAAAAAAAAAoOITfAAAAAAAAAICCQ/gNAAAAAAAAAFCF5v8DlZiMEVExuY0AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "out_coaxial.em_fields.b_field_xyz.struphy.plot.panels(\n", " x=\"e1\",\n", From 019be8e9b1039b0fbbdf2300ca6624f2ec9aaacd Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 08:47:05 +0200 Subject: [PATCH 061/193] formatting --- .../cyclone/params_cyclone.py | 137 +++++++++++------- .../cyclone/pproc_cyclone.py | 5 +- .../itg_cylindre/params_drift_kinetic.py | 112 ++++++++------ .../itg_cylindre/pproc_drift_kinetic.py | 5 +- .../diocotron_instability/params_diocotron.py | 8 +- .../diocotron_instability/pproc_diocotron.py | 8 +- .../bump_on/params_bump_on.py | 45 +++--- .../bump_on/pproc_bump_on.py | 4 +- .../params_strong_Landau_damping.py | 32 ++-- .../pproc_strong_Landau_damping.py | 4 +- .../two_stream/params_two_stream.py | 39 ++--- .../two_stream/pproc_two_stream.py | 4 +- .../params_weak_Landau_damping.py | 37 ++--- .../pproc_weak_Landau_damping.py | 4 +- .../params_weibel_instability.py | 69 ++++----- .../pproc_weibel_instability.py | 16 +- src/struphy/console/main.py | 4 +- src/struphy/diagnostics/diagn_tools.py | 4 +- src/struphy/diagnostics/plotting.py | 1 - .../diagnostics/tests/test_plotting.py | 6 +- .../verification/test_verif_LinearMHD.py | 4 +- .../tests/verification/test_verif_Maxwell.py | 2 +- src/struphy/post_processing/output.py | 110 +++++++++++--- .../post_processing/post_processing_tools.py | 10 +- .../post_processing/tests/test_output.py | 77 ++++++---- .../tests/test_output_accessors.py | 12 +- .../post_processing/xarray_accessors.py | 4 +- 27 files changed, 452 insertions(+), 311 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py index b31813ae3..a2717b916 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/params_cyclone.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. name = "Cyclone instability" @@ -62,11 +62,13 @@ # --------------------- -base_units = BaseUnits(kBT=0.1916) # provides the correct value for epsilon = 1.4142e-3 = 0.36/(180*sqrt(2)) from the paper +base_units = BaseUnits( + kBT=0.1916 +) # provides the correct value for epsilon = 1.4142e-3 = 0.36/(180*sqrt(2)) from the paper model = DriftKineticElectrostaticAdiabatic( base_units=base_units, use_diagnostic_poisson=True, - ) +) # List all variables and decide whether to save their data model.em_fields.phi.save_data = True @@ -83,24 +85,26 @@ time_opts = Time(dt=0.001, Tend=0.01, split_algo="LieTrotter") a, r_min, R0 = 0.36, 0.01, 1.0 -num_elements = (32, 5*27, 5) +num_elements = (32, 5 * 27, 5) degree = (3, 3, 3) # Fluid equilibrium (can be used as part of initial conditions) -equil = equils.AdhocTorus(a=a, R0=R0, B0=1.0, q_kind=2, q0=0.86, q1=2.52+0.86, l=-0.16, psi_k=5, psi_nel=200) +equil = equils.AdhocTorus(a=a, R0=R0, B0=1.0, q_kind=2, q0=0.86, q1=2.52 + 0.86, l=-0.16, psi_k=5, psi_nel=200) # Geometry # domain = domains.Tokamak(equil, num_elements=num_elements[:2], degree=degree[:2], r_min=r_min, num_elements_pre=(128, 512), p_pre=(4, 4), xi_param="sfl", tor_period=19) -domain = domains.HollowTorus(a1=r_min, a2=a, R0=R0, sfl=True, pol_period=1, tor_period=19) # use a hollowtorus to avoid premaping +domain = domains.HollowTorus( + a1=r_min, a2=a, R0=R0, sfl=True, pol_period=1, tor_period=19 +) # use a hollowtorus to avoid premaping # Grid -grid = grids.TensorProductGrid(num_elements=num_elements, mpi_dims_mask=(True,True,False)) +grid = grids.TensorProductGrid(num_elements=num_elements, mpi_dims_mask=(True, True, False)) # Derham options derham_opts = DerhamOptions( - degree=degree, + degree=degree, bcs=(("dirichlet", "dirichlet"), None, None), - ) +) # Simulation object @@ -121,41 +125,45 @@ # Particle parameters # ------------------- -ppc = 50 # run with 200 minimum -loading_params = LoadingParameters(ppc = ppc, loading="sobol_standard", spatial="uniform", moments=(0, 0, 4, 4)) +ppc = 50 # run with 200 minimum +loading_params = LoadingParameters(ppc=ppc, loading="sobol_standard", spatial="uniform", moments=(0, 0, 4, 4)) weights_params = WeightsParameters(control_variate=True) boundary_params = BoundaryParameters(bc=("remove", "periodic", "periodic")) -sorting_params = SortingParameters(boxes_per_dim=(12,12,6), do_sort=True, sorting_frequency=5) +sorting_params = SortingParameters(boxes_per_dim=(12, 12, 6), do_sort=True, sorting_frequency=5) # density binning -eta_bin = BinningPlot(slice='e1_e2', n_bins= (64,64), ranges= ((0.01, 0.99), (0.0, 1.0))) -eta_bin2 = BinningPlot(slice='e2_e3', n_bins= (64,64), ranges= ((0.0, 1.0), (0.0, 1.0))) +eta_bin = BinningPlot(slice="e1_e2", n_bins=(64, 64), ranges=((0.01, 0.99), (0.0, 1.0))) +eta_bin2 = BinningPlot(slice="e2_e3", n_bins=(64, 64), ranges=((0.0, 1.0), (0.0, 1.0))) saving_params = SavingParameters(n_markers=100, binning_plots=(eta_bin,)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize=1.0, - ) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=1.0, +) # ------------------ # Propagator options # ------------------ -model.propagators.gc_poisson.options = model.propagators.gc_poisson.Options(which_geometry="toroidal", - solver_params=SolverParameters(tol=1e-12,maxiter=3000, recycle=False), - filter_params={model.kinetic_ions.var: FilterParameters("fourier_in_tor", (1,), repeat=1)}, - ) -model.propagators.push_gc_bxe.options = model.propagators.push_gc_bxe.Options(algo="explicit", - evaluate_e_field=True, - maxiter=100, - ) -model.propagators.push_gc_para.options = model.propagators.push_gc_para.Options(algo="explicit", - evaluate_e_field=True, - maxiter=100, - ) +model.propagators.gc_poisson.options = model.propagators.gc_poisson.Options( + which_geometry="toroidal", + solver_params=SolverParameters(tol=1e-12, maxiter=3000, recycle=False), + filter_params={model.kinetic_ions.var: FilterParameters("fourier_in_tor", (1,), repeat=1)}, +) +model.propagators.push_gc_bxe.options = model.propagators.push_gc_bxe.Options( + algo="explicit", + evaluate_e_field=True, + maxiter=100, +) +model.propagators.push_gc_para.options = model.propagators.push_gc_para.Options( + algo="explicit", + evaluate_e_field=True, + maxiter=100, +) # ------------------ # Initial conditions @@ -179,60 +187,77 @@ n0 = 1.0 Ti0 = 1.0 + def n_r(r): - return n0 * xp.exp(-kappa_n*a*Delta_n*xp.tanh((r-r0)/(Delta_n*a))) + return n0 * xp.exp(-kappa_n * a * Delta_n * xp.tanh((r - r0) / (Delta_n * a))) + def n_init(*etas): - if len(etas)==1: - eta1=etas[0][:,0] + if len(etas) == 1: + eta1 = etas[0][:, 0] else: - eta1=etas[0] + eta1 = etas[0] r = r_min + (a - r_min) * eta1 return n_r(r) + def Ti_r(r): - return Ti0 * xp.exp(-kappa_Ti*a*Delta_Ti*xp.tanh((r-r0)/(Delta_Ti*a))) + return Ti0 * xp.exp(-kappa_Ti * a * Delta_Ti * xp.tanh((r - r0) / (Delta_Ti * a))) + def vth_init(*etas): - if len(etas)==1: - eta1=etas[0][:,0] + if len(etas) == 1: + eta1 = etas[0][:, 0] else: - eta1=etas[0] + eta1 = etas[0] r = r_min + (a - r_min) * eta1 return xp.sqrt(Ti_r(r)) -def n_xyz(x,y,z): - r = xp.sqrt((xp.sqrt(x**2 + y**2)-R0)**2 + z**2) + +def n_xyz(x, y, z): + r = xp.sqrt((xp.sqrt(x**2 + y**2) - R0) ** 2 + z**2) return n_r(r) -def p_xyz(x,y,z): - r = xp.sqrt((xp.sqrt(x**2 + y**2)-R0)**2 + z**2) - return n_r(r)*Ti_r(r) + +def p_xyz(x, y, z): + r = xp.sqrt((xp.sqrt(x**2 + y**2) - R0) ** 2 + z**2) + return n_r(r) * Ti_r(r) + equil.p_xyz = p_xyz equil.n_xyz = n_xyz + def pert_func(*etas): - if len(etas)==1: - e1,e2,e3 = etas[0][:,0], etas[0][:,1], etas[0][:,2] + if len(etas) == 1: + e1, e2, e3 = etas[0][:, 0], etas[0][:, 1], etas[0][:, 2] else: e1, e2, e3 = etas[0], etas[1], etas[2] r = (a - r_min) * e1 + r_min - teta = 2*xp.arctan(xp.sqrt((R0+r)/(R0-r))*xp.tan(xp.pi*e2)) - phi = 2*xp.pi * e3 - return n_r(r)*amps*xp.exp(-(r-r0)**2/delta_r**2)*xp.cos(ms*teta - ns*phi) + teta = 2 * xp.arctan(xp.sqrt((R0 + r) / (R0 - r)) * xp.tan(xp.pi * e2)) + phi = 2 * xp.pi * e3 + return n_r(r) * amps * xp.exp(-((r - r0) ** 2) / delta_r**2) * xp.cos(ms * teta - ns * phi) + # Background for kinetic species -background = maxwellians.GyroMaxwellian2D(n=(n_init, None), vth_para=(vth_init, None), vth_perp=(vth_init, None),)# B0=equil.absB0) +background = maxwellians.GyroMaxwellian2D( + n=(n_init, None), + vth_para=(vth_init, None), + vth_perp=(vth_init, None), +) # B0=equil.absB0) model.kinetic_ions.var.add_background(background) -#background.plot_density_profile("e1", "e2", domain=domain, plot_3D=True, in_physical=True) -#background.plot_density_profile("e1", "v1", domain=domain) -#background.plot_density_profile("e1", "v2", domain=domain, use_mu=True, equil=equil) +# background.plot_density_profile("e1", "e2", domain=domain, plot_3D=True, in_physical=True) +# background.plot_density_profile("e1", "v1", domain=domain) +# background.plot_density_profile("e1", "v2", domain=domain, use_mu=True, equil=equil) from struphy.initial.base import GenericPerturbation perturbation = GenericPerturbation(pert_func, given_in_basis="0") -init = maxwellians.GyroMaxwellian2D(n=(n_init, perturbation), vth_para=(vth_init, None), vth_perp=(vth_init, None),)# B0=equil.absB0) +init = maxwellians.GyroMaxwellian2D( + n=(n_init, perturbation), + vth_para=(vth_init, None), + vth_perp=(vth_init, None), +) # B0=equil.absB0) model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 1d8e67d5d..2c661cb68 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -38,10 +38,7 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run.viewer( - name, - x="e1", y="e2", coords="physical", plane=plane, **selection - ) + run.viewer(name, x="e1", y="e2", coords="physical", plane=plane, **selection) run.trajectories("kinetic_ions", max_markers=1000) plt.show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py index e0834ac2d..ba8688302 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/params_drift_kinetic.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. name = "Default DriftKineticElectrostaticAdiabatic" @@ -53,9 +53,6 @@ # ------------------ - - - # --------------------- # Instance of the model # --------------------- @@ -71,7 +68,7 @@ base_units=base_units, epsilon=1.0, use_diagnostic_poisson=True, - ) +) # List all variables and decide whether to save their data model.em_fields.phi.save_data = True @@ -95,12 +92,12 @@ equil = equils.HomogenSlab(B0x=0.0, B0y=0.0, B0z=1.0) # Grid -Nx, Ny, Nz = (32, 5*10, 10) +Nx, Ny, Nz = (32, 5 * 10, 10) num_element = (Nx, Ny, Nz) -grid = grids.TensorProductGrid(num_elements=num_element, mpi_dims_mask=(True,True,True)) +grid = grids.TensorProductGrid(num_elements=num_element, mpi_dims_mask=(True, True, True)) # Derham options -derham_opts = DerhamOptions(degree=(3,3,3), bcs=(("dirichlet", "dirichlet"), None, None)) +derham_opts = DerhamOptions(degree=(3, 3, 3), bcs=(("dirichlet", "dirichlet"), None, None)) # Simulation object from feectools.ddm.mpi import mpi as MPI @@ -123,26 +120,27 @@ # Particle parameters # ------------------- -ppc = 50 # run with 200 minimum +ppc = 50 # run with 200 minimum loading_params = LoadingParameters(ppc=ppc, loading="sobol_standard", spatial="uniform", moments=(0.0, 0.0, 2.0, 2.0)) weights_params = WeightsParameters(control_variate=True) -boundary_params = BoundaryParameters(bc=('remove','periodic','periodic')) +boundary_params = BoundaryParameters(bc=("remove", "periodic", "periodic")) sorting_params = SortingParameters( do_sort=True, boxes_per_dim=(12, 12, 6), sorting_frequency=0, ) -binplot = BinningPlot(slice='e1_e2', n_bins=(64,128), ranges=((0.0, 1.0), (0.0, 1.0))) +binplot = BinningPlot(slice="e1_e2", n_bins=(64, 128), ranges=((0.0, 1.0), (0.0, 1.0))) saving_params = SavingParameters(binning_plots=(binplot,)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize=2.0, - ) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=2.0, +) # ------------------ # Propagator options @@ -159,10 +157,10 @@ # If backgrounds or perturbations are not specified, they are assumed to be zero. # Background for (some) FEEC variables -#model.em_fields.phi.add_background(FieldsBackground(values=(0.0,))) +# model.em_fields.phi.add_background(FieldsBackground(values=(0.0,))) # Perturbations for (some) FEEC variables -#model.em_fields.phi.add_perturbation(perturbations.TorusModesCos()) +# model.em_fields.phi.add_perturbation(perturbations.TorusModesCos()) # For kinetic species the background is mandatory. # For kinetic species, if add_initial_condition() is not called, the background is taken as the kinetic initial condition. @@ -171,8 +169,8 @@ # Background for kinetic species -rp = (a2+a1)/2 -amps=1e-6 +rp = (a2 + a1) / 2 +amps = 1e-6 ms, ns = 5, 1 kappa_n0 = 0.055 kappa_Ti = kappa_Te = 0.27586 @@ -181,67 +179,89 @@ delta_r = 4 * delta_r_n0 / delta_r_Ti C_Ti = C_Te = 1.0 N_integrate = 1000000 -C_n0 = (a2-a1) / xp.sum(xp.exp(-kappa_n0*delta_r_n0*xp.tanh((xp.linspace(a1,a2,N_integrate)-rp)/delta_r_n0))*(a2-a1)/N_integrate) +C_n0 = (a2 - a1) / xp.sum( + xp.exp(-kappa_n0 * delta_r_n0 * xp.tanh((xp.linspace(a1, a2, N_integrate) - rp) / delta_r_n0)) + * (a2 - a1) + / N_integrate +) + def n0(r): - return C_n0 * xp.exp(-kappa_n0*delta_r_n0*xp.tanh((r-rp)/delta_r_n0)) + return C_n0 * xp.exp(-kappa_n0 * delta_r_n0 * xp.tanh((r - rp) / delta_r_n0)) + + from struphy.initial.base import GenericPerturbation def n_init(*etas): - if len(etas)==1: - eta1=etas[0][:,0] + if len(etas) == 1: + eta1 = etas[0][:, 0] else: - eta1=etas[0] - r = (a1 + (a2 - a1) * eta1) + eta1 = etas[0] + r = a1 + (a2 - a1) * eta1 return n0(r) + def pert_func(*etas): - if len(etas)==1: - e1,e2,e3 = etas[0][:,0], etas[0][:,1], etas[0][:,2] + if len(etas) == 1: + e1, e2, e3 = etas[0][:, 0], etas[0][:, 1], etas[0][:, 2] else: e1, e2, e3 = etas[0], etas[1], etas[2] - r = (a1 + (a2 - a1) * e1) - teta = 2*xp.pi * e2 + r = a1 + (a2 - a1) * e1 + teta = 2 * xp.pi * e2 z = Lz * e3 - return n0(r)*amps*xp.exp(-(r-rp)**2/delta_r**2)*xp.cos(2*xp.pi*ns*z/Lz + ms*teta) + return n0(r) * amps * xp.exp(-((r - rp) ** 2) / delta_r**2) * xp.cos(2 * xp.pi * ns * z / Lz + ms * teta) + def Ti(r): - return C_Ti * xp.exp(-kappa_Ti*delta_r_Ti*xp.tanh((r-rp)/delta_r_Ti)) + return C_Ti * xp.exp(-kappa_Ti * delta_r_Ti * xp.tanh((r - rp) / delta_r_Ti)) + def vth_i(*etas): - if len(etas)==1: - eta1=etas[0][:,0] + if len(etas) == 1: + eta1 = etas[0][:, 0] else: - eta1=etas[0] - r = (a1 + (a2 - a1) * eta1) + eta1 = etas[0] + r = a1 + (a2 - a1) * eta1 return xp.sqrt(Ti(r)) + def vth_e(*etas): - if len(etas)==1: - eta1=etas[0][:,0] + if len(etas) == 1: + eta1 = etas[0][:, 0] else: - eta1=etas[0] - r = (a1 + (a2 - a1) * eta1) + eta1 = etas[0] + r = a1 + (a2 - a1) * eta1 return xp.sqrt(Ti(r)) + def n0_xyz(x, y, z): r = xp.sqrt(x**2 + y**2) return n0(r) + def p_xyz(x, y, z): r = xp.sqrt(x**2 + y**2) - return n0(r)*Ti(r) + return n0(r) * Ti(r) + equil.p_xyz = p_xyz equil.n_xyz = n0_xyz perturbation = GenericPerturbation(pert_func) -background = maxwellians.GyroMaxwellian2D(n=(n_init, None), vth_para=(vth_i,None), vth_perp=(vth_i,None),)# B0=equil.absB0) +background = maxwellians.GyroMaxwellian2D( + n=(n_init, None), + vth_para=(vth_i, None), + vth_perp=(vth_i, None), +) # B0=equil.absB0) model.kinetic_ions.var.add_background(background) -init = maxwellians.GyroMaxwellian2D(n=(n_init, perturbation), vth_para=(vth_i,None), vth_perp=(vth_i,None),)# B0=equil.absB0) +init = maxwellians.GyroMaxwellian2D( + n=(n_init, perturbation), + vth_para=(vth_i, None), + vth_perp=(vth_i, None), +) # B0=equil.absB0) model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": - sim.run(profiling_activated=True) \ No newline at end of file + sim.run(profiling_activated=True) diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 2ea6c6f11..6cbcd5a90 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -38,10 +38,7 @@ def main(path_out=DEFAULT_OUTPUT): for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run.viewer( - name, - x="e1", y="e2", coords="physical", plane=plane, **selection - ) + run.viewer(name, x="e1", y="e2", coords="physical", plane=plane, **selection) run.trajectories("kinetic_ions", max_markers=1000) plt.show() diff --git a/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py index f0c3eac8a..a219ad841 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/params_diocotron.py @@ -168,7 +168,9 @@ def n_init(etas, r_minus=r_minus, r_plus=r_plus): # Background for kinetic species -background = maxwellians.GyroMaxwellian2D(n=(0.0, None),)# B0=equil.absB0) +background = maxwellians.GyroMaxwellian2D( + n=(0.0, None), +) # B0=equil.absB0) model.kinetic_ions.var.add_background(background) @@ -179,7 +181,9 @@ def n_init(etas, r_minus=r_minus, r_plus=r_plus): # for non linear case amps = (0.5,) perturbation = perturbations.ModesCos(amps=(1e-6,), ms=(ms,), perb_domain=((eta_minus, eta_plus), None, None)) -init = maxwellians.GyroMaxwellian2D(n=(n_init, perturbation),)# B0=equil.absB0) +init = maxwellians.GyroMaxwellian2D( + n=(n_init, perturbation), +) # B0=equil.absB0) model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 3fa20a95b..55f30a478 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -37,9 +37,7 @@ def main(paths=(DEFAULT_OUTPUT,)): ).show() for each, result in zip(runs, plot.fit_results): - print( - f"{each.path_out.name}: growth rate = {None if result is None else result.rate}" - ) + print(f"{each.path_out.name}: growth rate = {None if result is None else result.rate}") if len(runs) > 1: return @@ -48,9 +46,7 @@ def main(paths=(DEFAULT_OUTPUT,)): run.plot.equilibrium() for name in SWEEPS: - run[name].struphy.plot.viewer( - x="e1", y="e2", coords="physical", plane="XY" - ).show() + run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() diff --git a/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py index 5f7b690cf..086f4db77 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/params_bump_on.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. description = """ @@ -51,7 +51,7 @@ base_units = BaseUnits() # Model instance -model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0 = False) +model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False) # List all variables and decide whether to save their data model.em_fields.e_field.save_data = True @@ -66,10 +66,10 @@ env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping -time_opts = Time(dt = 0.1, Tend = 60.0, split_algo = "LieTrotter") +time_opts = Time(dt=0.1, Tend=60.0, split_algo="LieTrotter") # Geometry -domain = domains.Cuboid(r1 = 62.83) +domain = domains.Cuboid(r1=62.83) # Fluid equilibrium (can be used as part of initial conditions) equil = None @@ -101,23 +101,28 @@ boundary_params = BoundaryParameters() sorting_params = SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True) -binplot_1 = BinningPlot(slice="e1_v1", n_bins= (128, 128), ranges= ((0.,1.), (-10.0,10.0))) #for initial velocity distribution -binplot_2 = BinningPlot(slice = "v1", n_bins = 128, ranges = (-10.0,10.0)) # for progression of velocity and space distribution +binplot_1 = BinningPlot( + slice="e1_v1", n_bins=(128, 128), ranges=((0.0, 1.0), (-10.0, 10.0)) +) # for initial velocity distribution +binplot_2 = BinningPlot( + slice="v1", n_bins=128, ranges=(-10.0, 10.0) +) # for progression of velocity and space distribution saving_params = SavingParameters(binning_plots=(binplot_1, binplot_2)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize = 0.4, - ) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=0.4, +) # ------------------ # Propagator options # ------------------ -model.propagators.push_eta.options = model.propagators.push_eta.Options() +model.propagators.push_eta.options = model.propagators.push_eta.Options() if model.with_B0: model.propagators.push_vxb.options = model.propagators.push_vxb.Options() model.propagators.coupling_va.options = model.propagators.coupling_va.Options() @@ -134,17 +139,17 @@ # For kinetic species the perturbations are added to the moments of the distribution function (defined as tuples). # Background for kinetic species -maxwellian_1 = maxwellians.Maxwellian3D(n=(9/10, None), u1 = (3.0, None)) -maxwellian_2 = maxwellians.Maxwellian3D(n=(1/10, None), u1 = (-4.5, None), vth1 = (0.5, None)) +maxwellian_1 = maxwellians.Maxwellian3D(n=(9 / 10, None), u1=(3.0, None)) +maxwellian_2 = maxwellians.Maxwellian3D(n=(1 / 10, None), u1=(-4.5, None), vth1=(0.5, None)) background = maxwellian_1 + maxwellian_2 model.kinetic_ions.var.add_background(background) # Perturbations for (some) kinetic species -perturbation = perturbations.ModesCos(amps = (0.05,), ls = (1,)) -init1 = maxwellians.Maxwellian3D(n=(9/10, None), u1 = (3.0, None)) -init2 = maxwellians.Maxwellian3D(n = (1/10, perturbation), u1 = (-4.5, None), vth1 = (0.5, None)) +perturbation = perturbations.ModesCos(amps=(0.05,), ls=(1,)) +init1 = maxwellians.Maxwellian3D(n=(9 / 10, None), u1=(3.0, None)) +init2 = maxwellians.Maxwellian3D(n=(1 / 10, perturbation), u1=(-4.5, None), vth1=(0.5, None)) init = init1 + init2 model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": - sim.run() \ No newline at end of file + sim.run() diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 73ffa1d7e..e45da3f66 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -25,9 +25,7 @@ def main(path_out=DEFAULT_OUTPUT): run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f.struphy.plot.panels( - x="e1", y="v1", nrows=3, ncols=4, title="full-$f$" - ).show() + run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py index b88e7ed0b..a7b87881c 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/params_strong_Landau_damping.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. description = """ @@ -66,10 +66,10 @@ env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping -time_opts = Time(dt = 0.05, Tend = 75.0, split_algo = "LieTrotter") +time_opts = Time(dt=0.05, Tend=75.0, split_algo="LieTrotter") # Geometry -domain = domains.Cuboid(r1 = 12.56) +domain = domains.Cuboid(r1=12.56) # Fluid equilibrium (can be used as part of initial conditions) equil = None @@ -96,26 +96,28 @@ # Particle parameters # ------------------- -loading_params = LoadingParameters(ppc = 1000) +loading_params = LoadingParameters(ppc=1000) weights_params = WeightsParameters(control_variate=True) boundary_params = BoundaryParameters() sorting_params = SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True) -binplot = BinningPlot(slice='e1_v1', n_bins= (128, 128), ranges= ((0.,1.), (-5.,5.))) +binplot = BinningPlot(slice="e1_v1", n_bins=(128, 128), ranges=((0.0, 1.0), (-5.0, 5.0))) saving_params = SavingParameters(binning_plots=(binplot,)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize = 0.4,) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=0.4, +) # ------------------ # Propagator options # ------------------ -model.propagators.push_eta.options = model.propagators.push_eta.Options() +model.propagators.push_eta.options = model.propagators.push_eta.Options() if model.with_B0: model.propagators.push_vxb.options = model.propagators.push_vxb.Options() model.propagators.coupling_va.options = model.propagators.coupling_va.Options() @@ -136,9 +138,9 @@ model.kinetic_ions.var.add_background(background) # Perturbations for (some) kinetic species -perturbation = perturbations.ModesCos(amps = (0.5,), ls = (1,)) -init = maxwellians.Maxwellian3D(n = (1.0, perturbation)) +perturbation = perturbations.ModesCos(amps=(0.5,), ls=(1,)) +init = maxwellians.Maxwellian3D(n=(1.0, perturbation)) model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": - sim.run() \ No newline at end of file + sim.run() diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index 316905b53..75867f644 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -13,9 +13,7 @@ def main(path_out=DEFAULT_OUTPUT): run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f.struphy.plot.panels( - x="e1", y="v1", nrows=3, ncols=4, title="full-$f$" - ).show() + run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py index d76d35441..ddb1e4455 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/params_two_stream.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. description = """ @@ -51,7 +51,7 @@ base_units = BaseUnits() # Model instance -model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0 = False) +model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False) # List all variables and decide whether to save their data model.em_fields.e_field.save_data = True @@ -66,10 +66,10 @@ env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping -time_opts = Time(dt = 0.1, Tend = 50.0, split_algo = "LieTrotter") +time_opts = Time(dt=0.1, Tend=50.0, split_algo="LieTrotter") # Geometry -domain = domains.Cuboid(r1 = 31.42) +domain = domains.Cuboid(r1=31.42) # Fluid equilibrium (can be used as part of initial conditions) equil = None @@ -101,22 +101,23 @@ boundary_params = BoundaryParameters() sorting_params = SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True) -binplot = BinningPlot(slice='e1_v1', n_bins= (128, 128), ranges= ((0.,1.), (-10.0,10.0))) +binplot = BinningPlot(slice="e1_v1", n_bins=(128, 128), ranges=((0.0, 1.0), (-10.0, 10.0))) saving_params = SavingParameters(binning_plots=(binplot,)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize = 0.4, - ) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=0.4, +) # ------------------ # Propagator options # ------------------ -model.propagators.push_eta.options = model.propagators.push_eta.Options() +model.propagators.push_eta.options = model.propagators.push_eta.Options() if model.with_B0: model.propagators.push_vxb.options = model.propagators.push_vxb.Options() model.propagators.coupling_va.options = model.propagators.coupling_va.Options() @@ -133,17 +134,17 @@ # For kinetic species the perturbations are added to the moments of the distribution function (defined as tuples). # Background for kinetic species -maxwellian_1 = maxwellians.Maxwellian3D(n=(0.5, None), u1 = (3.0, None)) -maxwellian_2 = maxwellians.Maxwellian3D(n=(0.5, None), u1 = (-3.0, None)) +maxwellian_1 = maxwellians.Maxwellian3D(n=(0.5, None), u1=(3.0, None)) +maxwellian_2 = maxwellians.Maxwellian3D(n=(0.5, None), u1=(-3.0, None)) background = maxwellian_1 + maxwellian_2 model.kinetic_ions.var.add_background(background) # Perturbations for (some) kinetic species -perturbation = perturbations.ModesCos(amps = (0.001,), ls = (1,)) -init1 = maxwellians.Maxwellian3D(n = (0.5, perturbation), u1 = (3.0, None)) -init2 = maxwellians.Maxwellian3D(n = (0.5, perturbation), u1 = (-3.0, None)) +perturbation = perturbations.ModesCos(amps=(0.001,), ls=(1,)) +init1 = maxwellians.Maxwellian3D(n=(0.5, perturbation), u1=(3.0, None)) +init2 = maxwellians.Maxwellian3D(n=(0.5, perturbation), u1=(-3.0, None)) init = init1 + init2 model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": - sim.run() \ No newline at end of file + sim.run() diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index b928e1420..86aeaff11 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -14,9 +14,7 @@ def main(path_out=DEFAULT_OUTPUT): # electric field growth against the analytical rate (0.2845 in units of m/c) energy = run.scalars.electric_energy - analytical = energy.copy( - data=10 ** (0.2845 * energy.t - 5.3) - ) # t is in Struphy units + analytical = energy.copy(data=10 ** (0.2845 * energy.t - 5.3)) # t is in Struphy units analytical.attrs["label"] = "analytical" energy.struphy.plot.timeseries(analytical, title="Electric energy").show() diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py index 9adbaf169..8d91554a7 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/params_weak_Landau_damping.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. description = """ @@ -50,7 +50,7 @@ base_units = BaseUnits() # Model instance -model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0 = False) +model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False) # List all variables and decide whether to save their data model.em_fields.e_field.save_data = True @@ -65,10 +65,10 @@ env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping -time_opts = Time(dt = 0.05, Tend = 20.0, split_algo = "LieTrotter") +time_opts = Time(dt=0.05, Tend=20.0, split_algo="LieTrotter") # Geometry -domain = domains.Cuboid(r1 = 12.56) # r1 -> pi * 4 -> k = 0.5 +domain = domains.Cuboid(r1=12.56) # r1 -> pi * 4 -> k = 0.5 # Fluid equilibrium (can be used as part of initial conditions) equil = None @@ -95,27 +95,28 @@ # Particle parameters # ------------------- -loading_params = LoadingParameters(ppc = 1000) -weights_params = WeightsParameters(control_variate= True) +loading_params = LoadingParameters(ppc=1000) +weights_params = WeightsParameters(control_variate=True) boundary_params = BoundaryParameters() sorting_params = SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True) -binplot = BinningPlot(slice='e1_v1', n_bins= (128, 128), ranges= ((0.,1.), (-5.,5.))) +binplot = BinningPlot(slice="e1_v1", n_bins=(128, 128), ranges=((0.0, 1.0), (-5.0, 5.0))) saving_params = SavingParameters(binning_plots=(binplot,)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize = 0.4, - ) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=0.4, +) # ------------------ # Propagator options # ------------------ -model.propagators.push_eta.options = model.propagators.push_eta.Options() +model.propagators.push_eta.options = model.propagators.push_eta.Options() if model.with_B0: model.propagators.push_vxb.options = model.propagators.push_vxb.Options() @@ -137,9 +138,9 @@ model.kinetic_ions.var.add_background(background) # Perturbations for (some) kinetic species -perturbation = perturbations.ModesCos(amps = (0.001,), ls = (1,)) -init = maxwellians.Maxwellian3D(n = (1.0,perturbation)) +perturbation = perturbations.ModesCos(amps=(0.001,), ls=(1,)) +init = maxwellians.Maxwellian3D(n=(1.0, perturbation)) model.kinetic_ions.var.add_initial_condition(init) if __name__ == "__main__": - sim.run() \ No newline at end of file + sim.run() diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 50a8e173c..e320744e7 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -23,9 +23,7 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): # electric field energy against the analytical damping energy = run.scalars.electric_energy.copy() energy.attrs["label"] = "numerical" - analytical = energy.copy( - data=E_exact(energy.t.values, eps=amplitude) - ) # t is in Struphy units + analytical = energy.copy(data=E_exact(energy.t.values, eps=amplitude)) # t is in Struphy units analytical.attrs["label"] = "analytical" energy.struphy.plot.timeseries(analytical, title="Electric energy").show() diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py index 591353626..70baef1f9 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/params_weibel_instability.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. description = """ @@ -56,10 +56,7 @@ base_units = BaseUnits() # Model instance -model = VlasovMaxwellOneSpecies(base_units=base_units, - alpha=1.0, - epsilon=-1.0, - measure_gauss_law=True) +model = VlasovMaxwellOneSpecies(base_units=base_units, alpha=1.0, epsilon=-1.0, measure_gauss_law=True) # --------------------- # Parameters setup @@ -70,7 +67,7 @@ k = 1.25 B_pert_amp = -1e-4 -vth1_background_val = 0.02/xp.sqrt(2) +vth1_background_val = 0.02 / xp.sqrt(2) vth2_background_val = vth1_background_val * xp.sqrt(12) # List all variables and decide whether to save their data @@ -86,19 +83,19 @@ env = EnvironmentOptions(out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data") # Time stepping -time_opts = Time(dt = 0.05, Tend = 400, split_algo = "LieTrotter") +time_opts = Time(dt=0.05, Tend=400, split_algo="LieTrotter") # Geometry -domain = domains.Cuboid(r1 = 2*xp.pi/k) +domain = domains.Cuboid(r1=2 * xp.pi / k) # Fluid equilibrium (can be used as part of initial conditions) equil = None # Grid -grid = grids.TensorProductGrid(num_elements = (32,1,1)) +grid = grids.TensorProductGrid(num_elements=(32, 1, 1)) # Derham options -derham_opts = DerhamOptions(degree = (3,1,1)) +derham_opts = DerhamOptions(degree=(3, 1, 1)) # Siumlation object sim = Simulation( @@ -116,29 +113,35 @@ # Particle parameters # ------------------- -loading_params = LoadingParameters(Np = 100000, - set_zero_velocity = (False, False, True), - moments = (0.0,0.0,0.0,vth1_background_val,vth2_background_val,1.0), - seed=1234, - ) -weights_params = WeightsParameters(control_variate = False) +loading_params = LoadingParameters( + Np=100000, + set_zero_velocity=(False, False, True), + moments=(0.0, 0.0, 0.0, vth1_background_val, vth2_background_val, 1.0), + seed=1234, +) +weights_params = WeightsParameters(control_variate=False) boundary_params = BoundaryParameters() -sorting_params = SortingParameters(boxes_per_dim = (16,1,1), do_sort = True) +sorting_params = SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True) -binplot_dens = BinningPlot(slice="e1_v1", n_bins= (128, 128), ranges= ((0.,1.), (-0.1,0.1))) -binplot_velocity = BinningPlot(slice="v1_v2", n_bins= (128, 128), ranges= ((-0.1,0.1), (-0.1,0.1))) +binplot_dens = BinningPlot(slice="e1_v1", n_bins=(128, 128), ranges=((0.0, 1.0), (-0.1, 0.1))) +binplot_velocity = BinningPlot(slice="v1_v2", n_bins=(128, 128), ranges=((-0.1, 0.1), (-0.1, 0.1))) binplot_current = tuple( - [BinningPlot(slice=f"e{i}", n_bins= 32, ranges= (0.,1.), output_quantity=f"current_{j}") for j in range(1,4) for i in range(1,4)] - ) + [ + BinningPlot(slice=f"e{i}", n_bins=32, ranges=(0.0, 1.0), output_quantity=f"current_{j}") + for j in range(1, 4) + for i in range(1, 4) + ] +) saving_params = SavingParameters(binning_plots=(binplot_dens, binplot_velocity, *binplot_current)) -model.kinetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - bufsize = 2.0, - ) +model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + bufsize=2.0, +) # ------------------ # Propagator options @@ -160,13 +163,13 @@ # For kinetic species, if add_initial_condition() is not called, the background is taken as the kinetic initial condition. # For kinetic species the perturbations are added to the moments of the distribution function (defined as tuples). -maxwellian = maxwellians.Maxwellian3D( - vth1=(vth1_background_val, None) , vth2=(vth2_background_val, None) - ) +maxwellian = maxwellians.Maxwellian3D(vth1=(vth1_background_val, None), vth2=(vth2_background_val, None)) model.kinetic_ions.var.add_background(maxwellian) # Perturbations of initial magnetic field -model.em_fields.b_field.add_perturbation(perturbation = perturbations.ModesCos(amps=(B_pert_amp,), ls = (1,), comp = 2)) # Initial Bz depending on x-axis +model.em_fields.b_field.add_perturbation( + perturbation=perturbations.ModesCos(amps=(B_pert_amp,), ls=(1,), comp=2) +) # Initial Bz depending on x-axis if __name__ == "__main__": - sim.run() \ No newline at end of file + sim.run() diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 949b0cc2d..9c1df37f7 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -42,9 +42,7 @@ def main(path_out=DEFAULT_OUTPUT): def field_energy(field): """Energy of each component over space, as array of shape (component, t).""" - return ( - (field**2).sum(spatial).transpose("component", "t").values * unit_volume / 2 - ) + return (field**2).sum(spatial).transpose("component", "t").values * unit_volume / 2 electric_energy = field_energy(e_field) magnetic_energy = field_energy(b_field) @@ -102,9 +100,7 @@ def field_energy(field): ("v1_v2_density", "v1", "v2"), ): for quantity in ("f", "delta_f"): - getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels( - x=x, y=y, nrows=5, ncols=4 - ).show() + getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels(x=x, y=y, nrows=5, ncols=4).show() # ------------------ # EM field at selected times @@ -113,9 +109,7 @@ def plot_EM_state(time_step: float, n_dim=3): electric_field = e_field.sel(t=time_step, method="nearest").isel(e2=0, e3=0) magnetic_field = b_field.sel(t=time_step, method="nearest").isel(e2=0, e3=0) - fig, axs = plt.subplots( - nrows=2, ncols=3, figsize=(8, 6), sharex=True, sharey=True - ) + fig, axs = plt.subplots(nrows=2, ncols=3, figsize=(8, 6), sharex=True, sharey=True) for i in range(n_dim): axs[0, i].plot(electric_field.e1, electric_field.isel(component=i)) axs[0, i].set_title(rf"$E_{i + 1}$") @@ -140,9 +134,7 @@ def plot_EM_state(time_step: float, n_dim=3): # Current density evolution # ------------------ def current_1D(time_step: float): - fig, ax = plt.subplots( - nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True - ) + fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True) for i in range(3): for j in range(3): current = getattr(distributions, f"e{i + 1}_current_{j + 1}").f diff --git a/src/struphy/console/main.py b/src/struphy/console/main.py index c6f45ec21..e98fa2716 100644 --- a/src/struphy/console/main.py +++ b/src/struphy/console/main.py @@ -438,7 +438,9 @@ def add_parser_output(subparsers): parser.add_argument("--parallel", action="store_true", help="use MPI.COMM_WORLD for parallel pproc") parser.add_argument("--format", choices=("markdown", "html"), default="markdown", help="report format") parser.add_argument("--directory", help="report directory") - parser.add_argument("--kind", choices=("timeseries", "slice", "panels", "viewer", "trajectories"), default="timeseries") + parser.add_argument( + "--kind", choices=("timeseries", "slice", "panels", "viewer", "trajectories"), default="timeseries" + ) parser.add_argument("--product", help="product key for plot") parser.add_argument("--x", help="first displayed dimension") parser.add_argument("--y", help="second displayed dimension") diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 153e362a7..f60610b9a 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -7,11 +7,11 @@ """ import logging -import warnings -from functools import wraps import os import shutil import subprocess +import warnings +from functools import wraps import cunumpy as xp import matplotlib.colors as colors diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 91d9088fc..0fc1c1a02 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -23,7 +23,6 @@ growth_rate, relative_error, ) # noqa: F401 (backward-compatible imports) - from struphy.post_processing.arrays import ( SCALARS_EXCLUDE, axis_label, diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py index 2f86f8562..c02c269d7 100644 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ b/src/struphy/diagnostics/tests/test_plotting.py @@ -186,9 +186,10 @@ def test_slice_can_display_the_sweep_dimension(): def test_every_presentation_uses_the_full_selected_color_range(tmp_path, monkeypatch): - import struphy.post_processing.xarray_accessors # noqa: F401 from matplotlib.figure import Figure + import struphy.post_processing.xarray_accessors # noqa: F401 + data = phase_space(nt=3).astype(float) data[1] = data[1] * 100 # extrema in a frame omitted by panels and export view = data.struphy.plot.view(x="e1", y="v1", cmap="plasma", equal_aspect=True) @@ -221,9 +222,10 @@ def capture(fig, *args, **kwargs): @pytest.mark.parametrize("shared_clim", [True, False]) def test_explicit_color_limits_work_for_all_renderers(tmp_path, monkeypatch, shared_clim): - import struphy.post_processing.xarray_accessors # noqa: F401 from matplotlib.figure import Figure + import struphy.post_processing.xarray_accessors # noqa: F401 + data = phase_space(nt=2) options = dict(x="e1", y="v1", vmin=-5, vmax=100, shared_clim=shared_clim, cmap="coolwarm") panels = data.struphy.plot.panels(nrows=1, ncols=2, **options) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 25e4032c8..111bf14a2 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -88,7 +88,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} _1, _2, _3, coeffs = run["mhd/velocity"].struphy.analysis.dispersion( - physical=True, + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -108,7 +108,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): # second fft _1, _2, _3, coeffs = run["mhd/pressure"].struphy.analysis.dispersion( - physical=True, + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 445ef4061..b9f0e942a 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -73,7 +73,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: # fft _1, _2, _3, coeffs = run["em_fields/e_field"].struphy.analysis.dispersion( - physical=True, + physical=True, component=0, slice_at=[0, 0, None], do_plot=do_plot, diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 7815d1dc9..15be35773 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -7,9 +7,9 @@ import warnings from collections.abc import Callable, Iterator, Mapping from functools import cached_property +from html import escape from pathlib import Path from typing import Any -from html import escape import h5py import numpy as np @@ -496,7 +496,9 @@ def units(self): """The units of the run's normalization, in SI; see :class:`struphy.physics.physics.Units`.""" return self.model.units - def to_si(self, product: str | xr.DataArray, unit: str | float | None = None, *, label: str | None = None) -> xr.DataArray: + def to_si( + self, product: str | xr.DataArray, unit: str | float | None = None, *, label: str | None = None + ) -> xr.DataArray: """A product in SI units: coordinates always, values when ``unit`` names their normalization. ``unit`` is one of ``x``, ``B``, ``n``, ``v``, ``t``, ``p``, ``rho``, ``j``, ``kBT``, or a @@ -510,7 +512,9 @@ def profile(self) -> Profile: if self._profile is None: path = self.path_out / "profiling_data.h5" if not path.is_file(): - raise FileNotFoundError(f"no profiling data in {self.path_out}; run with sim.run(profiling_activated=True)") + raise FileNotFoundError( + f"no profiling data in {self.path_out}; run with sim.run(profiling_activated=True)" + ) self._profile = Profile(path, label=self.label) return self._profile @@ -939,7 +943,13 @@ def info(self) -> str: """ rows = [(key, self._product_description(key)) for key in self.keys()] key_width = max((len(key) for key, _ in rows), default=3) - lines = [f"Output: {self.path_out}", self.label, "", f"{'Key':<{key_width}} Description", f"{'-' * key_width} -----------"] + lines = [ + f"Output: {self.path_out}", + self.label, + "", + f"{'Key':<{key_width}} Description", + f"{'-' * key_width} -----------", + ] lines.extend(f"{key:<{key_width}} {description}" for key, description in rows) return "\n".join(lines) @@ -1009,37 +1019,102 @@ def report(self, directory=None, *, products=(), format: str = "markdown", max_s ] scalar_names = tuple(self.scalars.data_vars) scalar_time = np.asarray(self.scalars.coords["t"]) if "t" in self.scalars.coords else np.empty(0) - scalar_values = np.column_stack([np.asarray(self.scalars[name]) for name in scalar_names]) if scalar_names else np.empty((len(scalar_time), 0)) + scalar_values = ( + np.column_stack([np.asarray(self.scalars[name]) for name in scalar_names]) + if scalar_names + else np.empty((len(scalar_time), 0)) + ) scalar_rows = len(scalar_time) indices = np.linspace(0, scalar_rows - 1, min(scalar_rows, max_scalar_rows), dtype=int) if scalar_rows else [] summaries = [] for name, values in zip(scalar_names, scalar_values.T): finite = values[np.isfinite(values)] - summaries.append((name, float(finite[0]) if finite.size else np.nan, float(finite[-1]) if finite.size else np.nan, float(finite.min()) if finite.size else np.nan, float(finite.max()) if finite.size else np.nan)) + summaries.append( + ( + name, + float(finite[0]) if finite.size else np.nan, + float(finite[-1]) if finite.size else np.nan, + float(finite.min()) if finite.size else np.nan, + float(finite.max()) if finite.size else np.nan, + ) + ) requested_summary = [] for array in requested: values = np.asarray(array) finite = values[np.isfinite(values)] - requested_summary.append((array.name, ", ".join(array.dims), str(array.attrs.get("units", "")), int(values.size), float(finite.min()) if finite.size else np.nan, float(finite.max()) if finite.size else np.nan)) + requested_summary.append( + ( + array.name, + ", ".join(array.dims), + str(array.attrs.get("units", "")), + int(values.size), + float(finite.min()) if finite.size else np.nan, + float(finite.max()) if finite.size else np.nan, + ) + ) if format == "markdown": - lines = [f"# Struphy output report", "", f"- Path: `{self.path_out}`", f"- Run: {self.label}", "", "## Products", "", "| Key | Kind | Description |", "| --- | --- | --- |"] + lines = [ + f"# Struphy output report", + "", + f"- Path: `{self.path_out}`", + f"- Run: {self.label}", + "", + "## Products", + "", + "| Key | Kind | Description |", + "| --- | --- | --- |", + ] lines += [f"| `{key}` | {kind} | {description} |" for key, kind, description in rows] - lines += ["", "## Scalar summary", "", "| Scalar | Initial | Final | Min | Max |", "| --- | ---: | ---: | ---: | ---: |"] - lines += [f"| `{name}` | {initial:.6g} | {final:.6g} | {minimum:.6g} | {maximum:.6g} |" for name, initial, final, minimum, maximum in summaries] + lines += [ + "", + "## Scalar summary", + "", + "| Scalar | Initial | Final | Min | Max |", + "| --- | ---: | ---: | ---: | ---: |", + ] + lines += [ + f"| `{name}` | {initial:.6g} | {final:.6g} | {minimum:.6g} | {maximum:.6g} |" + for name, initial, final, minimum, maximum in summaries + ] lines += ["", f"Full scalar values: `{Path(csv_path).name}`"] if requested: - lines += ["", "## Requested data", "", "| Key | Dimensions | Units | Values | Min | Max |", "| --- | --- | --- | ---: | ---: | ---: |"] - lines += [f"| `{name}` | {dims} | {units} | {size} | {minimum:.6g} | {maximum:.6g} |" for name, dims, units, size, minimum, maximum in requested_summary] + lines += [ + "", + "## Requested data", + "", + "| Key | Dimensions | Units | Values | Min | Max |", + "| --- | --- | --- | ---: | ---: | ---: |", + ] + lines += [ + f"| `{name}` | {dims} | {units} | {size} | {minimum:.6g} | {maximum:.6g} |" + for name, dims, units, size, minimum, maximum in requested_summary + ] path = directory / "report.md" path.write_text("\n".join(lines) + "\n") else: - product_body = "".join(f"{escape(key)}{escape(kind)}{escape(description)}" for key, kind, description in rows) - summary_body = "".join(f"{escape(name)}{initial:.6g}{final:.6g}{minimum:.6g}{maximum:.6g}" for name, initial, final, minimum, maximum in summaries) - values_body = "".join(f"{float(scalar_time[index]):.6g}" + "".join(f"{value:.6g}" for value in scalar_values[index]) + "" for index in indices) - requested_body = "".join(f"{escape(str(name))}{escape(dims)}{escape(units)}{size}{minimum:.6g}{maximum:.6g}" for name, dims, units, size, minimum, maximum in requested_summary) + product_body = "".join( + f"{escape(key)}{escape(kind)}{escape(description)}" + for key, kind, description in rows + ) + summary_body = "".join( + f"{escape(name)}{initial:.6g}{final:.6g}{minimum:.6g}{maximum:.6g}" + for name, initial, final, minimum, maximum in summaries + ) + values_body = "".join( + f"{float(scalar_time[index]):.6g}" + + "".join(f"{value:.6g}" for value in scalar_values[index]) + + "" + for index in indices + ) + requested_body = "".join( + f"{escape(str(name))}{escape(dims)}{escape(units)}{size}{minimum:.6g}{maximum:.6g}" + for name, dims, units, size, minimum, maximum in requested_summary + ) path = directory / "report.html" style = "body{max-width:1200px;margin:2rem auto;padding:0 1rem;background:#f7f8fa;color:#1f2937;font:15px system-ui,sans-serif}h1,h2{color:#123b5d}.cards{display:flex;gap:1rem;flex-wrap:wrap}.card{background:white;padding:1rem;border-radius:8px;box-shadow:0 1px 3px #0002;min-width:220px}table{border-collapse:collapse;width:100%;background:white;margin:1rem 0}th{background:#123b5d;color:white;text-align:left}th,td{padding:.55rem;border-bottom:1px solid #dbe1e8}tr:nth-child(even){background:#f3f6f9}code{color:#8a2558}.scroll{overflow:auto}.muted{color:#52606d}a{color:#075985}" - path.write_text(f"Struphy output report

Struphy output report

Run
{escape(self.label)}
Output directory
{escape(str(self.path_out))}
Products
{len(rows)}
Scalar samples
{scalar_rows}

Scalar summary

{summary_body}
ScalarInitialFinalMinMax

Showing {len(indices)} of {scalar_rows} rows. Download all scalar values (CSV).

{''.join(f'' for name in scalar_names)}{values_body}
t{escape(name)}

Products

{product_body}
KeyKindDescription
{'

Requested data

'+requested_body+'
KeyDimensionsUnitsValuesMinMax
' if requested_body else ''}") + path.write_text( + f"Struphy output report

Struphy output report

Run
{escape(self.label)}
Output directory
{escape(str(self.path_out))}
Products
{len(rows)}
Scalar samples
{scalar_rows}

Scalar summary

{summary_body}
ScalarInitialFinalMinMax

Showing {len(indices)} of {scalar_rows} rows. Download all scalar values (CSV).

{''.join(f'' for name in scalar_names)}{values_body}
t{escape(name)}

Products

{product_body}
KeyKindDescription
{'

Requested data

' + requested_body + '
KeyDimensionsUnitsValuesMinMax
' if requested_body else ''}" + ) return str(path) @property @@ -1091,6 +1166,7 @@ def _load(self, group: str, name: str) -> xr.DataArray: array.coords["t"].attrs["units"] = "s" return array + def open_output(path_out, *, time_units: str = "normalized") -> Output: """Open the output folder of a finished simulation. diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index d30295ebb..ef4e1a905 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -125,8 +125,10 @@ def __init__(self, output: "Output", parallel_pproc: bool = False): self.derham = None if output.grid is not None and output.derham_opts is not None: self.derham = Derham( - output.grid, output.derham_opts, - comm=self.comm if parallel_pproc else None, domain=self.domain, + output.grid, + output.derham_opts, + comm=self.comm if parallel_pproc else None, + domain=self.domain, ) # the directory is only cleared in process(), so that constructing a @@ -1048,7 +1050,9 @@ def _binned_dataset(self, grids: dict, variables: dict) -> xr.Dataset: dims = tuple(dim for dim in grids) coords = {"t": self.t_grid, **grids} coords.update(self._mapped_coords(grids)) - return xr.Dataset({name: wrap_binned_data(values, dims, coords, name=name) for name, values in variables.items()}) + return xr.Dataset( + {name: wrap_binned_data(values, dims, coords, name=name) for name, values in variables.items()} + ) def _mapped_coords(self, grids: dict) -> dict: """``X``, ``Y``, ``Z`` on the logical directions of ``grids``, when there are two or three.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 9a1688a43..bd354c86b 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -11,11 +11,10 @@ from struphy import BaseUnits, Time, domains from struphy.models import Maxwell - -from struphy.post_processing.output import Output, open_output from struphy.post_processing import output as output_module from struphy.post_processing import store from struphy.post_processing.arrays import orbit_quantities +from struphy.post_processing.output import Output, open_output from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 @@ -32,38 +31,64 @@ def write_tree(root): mapped = np.meshgrid(*logical.values(), indexing="ij") path = store.store_path(pproc) store.create(path) - store.write_group(path, "/em_fields", xr.Dataset( - {"E": (("t", "component", "e1", "e2", "e3"), - np.stack([np.stack([np.full((N1, N2, N3), i + time) for i in range(3)]) for time in t]))}, - coords={"t": t, "component": [0, 1, 2], **logical, - **{name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), mapped)}}, - )) - store.write_group(path, "/kinetic_ions/e1_v1_density", xr.Dataset( - {"f": (("t", "e1", "v1"), np.ones((NT, N1, NV))), "delta_f": (("t", "e1", "v1"), np.zeros((NT, N1, NV)))}, - coords={"t": t, "e1": logical["e1"], "v1": np.linspace(-3, 3, NV)}, - )) - store.write_group(path, "/kinetic_ions/view_0", xr.Dataset( - {"n": (("t", "e1", "e2", "e3"), np.ones((NT, N1, N2, 1)))}, - coords={"t": t, "e1": logical["e1"], "e2": logical["e2"], "e3": np.zeros(1)}, - )) - store.write_group(path, "/kinetic_ions", xr.Dataset( - {"orbits": (("t", "marker", "quantity"), - np.stack([np.full((N_MARKERS, 8), step) for step in range(NT)]))}, - coords={"t": t, "marker": np.arange(N_MARKERS), "quantity": orbit_quantities(8)}, - )) + store.write_group( + path, + "/em_fields", + xr.Dataset( + { + "E": ( + ("t", "component", "e1", "e2", "e3"), + np.stack([np.stack([np.full((N1, N2, N3), i + time) for i in range(3)]) for time in t]), + ) + }, + coords={ + "t": t, + "component": [0, 1, 2], + **logical, + **{name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), mapped)}, + }, + ), + ) + store.write_group( + path, + "/kinetic_ions/e1_v1_density", + xr.Dataset( + {"f": (("t", "e1", "v1"), np.ones((NT, N1, NV))), "delta_f": (("t", "e1", "v1"), np.zeros((NT, N1, NV)))}, + coords={"t": t, "e1": logical["e1"], "v1": np.linspace(-3, 3, NV)}, + ), + ) + store.write_group( + path, + "/kinetic_ions/view_0", + xr.Dataset( + {"n": (("t", "e1", "e2", "e3"), np.ones((NT, N1, N2, 1)))}, + coords={"t": t, "e1": logical["e1"], "e2": logical["e2"], "e3": np.zeros(1)}, + ), + ) + store.write_group( + path, + "/kinetic_ions", + xr.Dataset( + {"orbits": (("t", "marker", "quantity"), np.stack([np.full((N_MARKERS, 8), step) for step in range(NT)]))}, + coords={"t": t, "marker": np.arange(N_MARKERS), "quantity": orbit_quantities(8)}, + ), + ) data_dir = os.path.join(root, "data") os.makedirs(data_dir) with h5py.File(os.path.join(data_dir, "data_proc0.hdf5"), "w") as file: file.create_dataset("time/value", data=t) - file.create_group("feec/em_fields") # the raw output names the species, - file.create_group("kinetic/kinetic_ions") # as a real run does + file.create_group("feec/em_fields") # the raw output names the species, + file.create_group("kinetic/kinetic_ions") # as a real run does file.create_dataset("scalar/en_tot", data=np.full(NT, 2.0)) metadata = { "model": Maxwell(base_units=BaseUnits(x=2.0)).to_dict(), "domain": domains.Cuboid().to_dict(), - "equil": None, "grid": None, "derham_opts": None, - "time_opts": Time().to_dict(), "mpi_ranks": 1, + "equil": None, + "grid": None, + "derham_opts": None, + "time_opts": Time().to_dict(), + "mpi_ranks": 1, } with open(os.path.join(root, "run_metadata.json"), "w") as stream: json.dump(metadata, stream) @@ -190,8 +215,10 @@ def test_configuration_is_restored_lazily_without_a_simulation(tmp_path, monkeyp from struphy import Simulation root = write_tree(str(tmp_path)) + def forbidden(*args, **kwargs): raise AssertionError("Output must not construct a Simulation") + monkeypatch.setattr(Simulation, "__init__", forbidden) run = Output(root) assert run.metadata["model"] == Maxwell(base_units=BaseUnits(x=2.0)).to_dict() diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 0a3a1b83a..c0274d745 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -157,17 +157,13 @@ def test_products_of_one_species_sit_on_the_output(run): def test_product_namespaces_expose_a_scoped_lazy_catalog(run): products = run.kinetic_ions - assert tuple(sorted(products.catalog)) == ( - "e1_v1_density/delta_f", "e1_v1_density/f", "orbits", "view_0/n" - ) + assert tuple(sorted(products.catalog)) == ("e1_v1_density/delta_f", "e1_v1_density/f", "orbits", "view_0/n") assert "e1_v1_density/f" in products.catalog assert "em_fields/E" not in products.catalog assert "e1_v1_density/f" in repr(products) assert run.distribution_catalog._cache == {} assert products["e1_v1_density/f"].dims == ("t", "e1", "v1") - assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f"] is products.catalog[ - "e1_v1_density/f" - ] + assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f"] is products.catalog["e1_v1_density/f"] def test_arrays_plot_themselves(run): @@ -209,7 +205,9 @@ def test_damping_rate_fits_the_envelope_not_the_oscillation(run): energy = oscillating_energy(rate=-0.3) fit = run.damping_rate(energy, amplitude=True) assert fit.rate == pytest.approx(-0.3, rel=1e-2) - assert energy.struphy.analysis.damping_rate(window=(2.0, 10.0), amplitude=True).rate == pytest.approx(-0.3, rel=1e-2) + assert energy.struphy.analysis.damping_rate(window=(2.0, 10.0), amplitude=True).rate == pytest.approx( + -0.3, rel=1e-2 + ) peaks = run.envelope(energy) assert 0 < peaks.sizes["t"] < energy.sizes["t"] // 10 diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py index c6ca13e1f..1f8fd81d6 100644 --- a/src/struphy/post_processing/xarray_accessors.py +++ b/src/struphy/post_processing/xarray_accessors.py @@ -334,11 +334,9 @@ def _view(self, **selection): def slice(self, *, ax=None, **selection): """Draw a snapshot, e.g. ``view.slice(t="last")``; return a PlotResult.""" - from struphy.diagnostics.plotting import plot_slice - # Resolve shared limits before selecting a single snapshot, so it uses # the same scale as panels, animation and export of this configured view. - from struphy.diagnostics.plotting import _SliceRenderer + from struphy.diagnostics.plotting import _SliceRenderer, plot_slice options = dict(self._options) if options["shared_clim"]: From b78e72da69e80bd991c0b259a10ded66e091b338 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 08:54:20 +0200 Subject: [PATCH 062/193] Formatting --- src/struphy/diagnostics/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py index 0fc1c1a02..4fa07aa4b 100644 --- a/src/struphy/diagnostics/plotting.py +++ b/src/struphy/diagnostics/plotting.py @@ -22,7 +22,7 @@ drift, growth_rate, relative_error, -) # noqa: F401 (backward-compatible imports) +) from struphy.post_processing.arrays import ( SCALARS_EXCLUDE, axis_label, From 405df8dd99f706af30a9a7f58fcb38da38c93d26 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 08:55:08 +0200 Subject: [PATCH 063/193] ruff check --fix --- src/struphy/post_processing/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 15be35773..14692e7d9 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -1054,7 +1054,7 @@ def report(self, directory=None, *, products=(), format: str = "markdown", max_s ) if format == "markdown": lines = [ - f"# Struphy output report", + "# Struphy output report", "", f"- Path: `{self.path_out}`", f"- Run: {self.label}", From 0f337a9195afed0cf1b73db54060146cf6c45a19 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 09:20:54 +0200 Subject: [PATCH 064/193] Support the legacy get_plot_data() method --- src/struphy/diagnostics/diagn_tools.py | 34 ++++++++ .../diagnostics/tests/test_diagn_tools.py | 25 ++++++ src/struphy/fields_background/equils.py | 2 +- src/struphy/post_processing/legacy.py | 67 ++++++++++++++++ .../post_processing/tests/test_legacy.py | 80 +++++++++++++++++++ src/struphy/simulation/sim.py | 14 ++-- src/struphy/simulation/tests/test_output.py | 17 ++-- 7 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 src/struphy/post_processing/legacy.py create mode 100644 src/struphy/post_processing/tests/test_legacy.py diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index f60610b9a..a53848925 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -43,6 +43,40 @@ def wrapped(*args, **kwargs): return decorate +def _accept_legacy_values(function): + """Also accept the call ``function(values, name, grids, grids_mapped=None, ...)`` of earlier versions. + + ``values`` maps time to the list of components of a field, as ``sim.spline_values...data`` + of :meth:`Simulation.load_plotting_data`. It is converted to a field with a logical and, if + ``grids_mapped`` is given, a physical fft coordinate. + """ + + def from_legacy_values(values, name, grids, grids_mapped=None, **kwargs): + times = sorted(values) + data = xp.stack([xp.stack([xp.asarray(comp) for comp in values[t]]) for t in times]) + coords = {"t": times, "component": xp.arange(data.shape[1])} + coords.update({f"e{n}": xp.asarray(grid) for n, grid in enumerate(grids, 1)}) + if grids_mapped is not None: + coords.update({X: (("e1", "e2", "e3"), xp.asarray(grid)) for X, grid in zip("XYZ", grids_mapped)}) + field = xr.DataArray(data, dims=("t", "component", "e1", "e2", "e3"), coords=coords, name=name) + return function(field, physical=grids_mapped is not None, **kwargs) + + @wraps(function) + def wrapped(field, *args, **kwargs): + if not isinstance(field, dict): + return function(field, *args, **kwargs) + warnings.warn( + f"diagn_tools.{function.__name__}(values, name, grids, ...) is deprecated; " + "pass a field of an Output, e.g. run.fields.em_fields.e_field_log.", + DeprecationWarning, + stacklevel=2, + ) + return from_legacy_values(field, *args, **kwargs) + + return wrapped + + +@_accept_legacy_values def power_spectrum_2d( field: xr.DataArray, component: int = 0, diff --git a/src/struphy/diagnostics/tests/test_diagn_tools.py b/src/struphy/diagnostics/tests/test_diagn_tools.py index d1146287b..a5e5c52a5 100644 --- a/src/struphy/diagnostics/tests/test_diagn_tools.py +++ b/src/struphy/diagnostics/tests/test_diagn_tools.py @@ -45,3 +45,28 @@ def test_fitted_phase_speed(physical): def test_needs_exactly_one_fft_direction(): with pytest.raises(AssertionError, match="slice_at"): power_spectrum_2d(standing_waves(tend=1.0), slice_at=(None, None, 0)) + + +def test_legacy_call_with_a_dict_of_time_snapshots(): + field = standing_waves() + values = {float(t): list(snapshot) for t, snapshot in zip(field.t.values, field.values)} + grids_log = [field[dim].values for dim in ("e1", "e2", "e3")] + grids_phy = [field[dim].values for dim in ("X", "Y", "Z")] + + with pytest.deprecated_call(): + omega, kvec, dispersion, coeffs = power_spectrum_2d( + values, + "e_field_log", + grids=grids_log, + grids_mapped=grids_phy, + component=1, + slice_at=[0, 0, None], + fit_branches=1, + noise_level=0.5, + ) + assert coeffs[0][0] == pytest.approx(SPEED, rel=0.02) + with pytest.deprecated_call(): + *_, coeffs = power_spectrum_2d( + values, "e_field_log", grids_log, component=1, slice_at=[0, 0, None], fit_branches=1, noise_level=0.5 + ) + assert coeffs[0][0] == pytest.approx(SPEED / LENGTH, rel=0.02) diff --git a/src/struphy/fields_background/equils.py b/src/struphy/fields_background/equils.py index d777e11fe..4aa0d98bb 100644 --- a/src/struphy/fields_background/equils.py +++ b/src/struphy/fields_background/equils.py @@ -1917,7 +1917,7 @@ def psi(self, R, Z, dR=0, dZ=0): # remove all "dimensions" for point-wise evaluation if is_float: - assert out.ndim == 0 + assert out.size == 1 # scipy >= 1.18 returns shape (1,) for a single point out = out.item() # rescale to Struphy units diff --git a/src/struphy/post_processing/legacy.py b/src/struphy/post_processing/legacy.py new file mode 100644 index 000000000..6a9cddb76 --- /dev/null +++ b/src/struphy/post_processing/legacy.py @@ -0,0 +1,67 @@ +"""Views of an :class:`~struphy.post_processing.output.Output` in the shapes of earlier versions. + +They back the deprecated :meth:`Simulation.load_plotting_data`, so that code written for +``sim.orbits``, ``sim.f``, ``sim.spline_values`` and ``sim.n_sph`` keeps working. New code should use +the :class:`xarray.DataArray` products of :class:`~struphy.post_processing.output.Output` directly. +""" + +from types import SimpleNamespace + +import numpy as np + + +def _set(namespace: SimpleNamespace, path: tuple[str, ...], value): + """Set ``namespace.....`` to ``value``, creating the levels in between.""" + for name in path[:-1]: + if not hasattr(namespace, name): + setattr(namespace, name, SimpleNamespace()) + namespace = getattr(namespace, name) + setattr(namespace, path[-1], value) + + +def _field_data(array) -> dict[float, list[np.ndarray]]: + """Time -> list of components, each a 3d array, as stored for the fields of earlier versions.""" + values = np.asarray(array) + if "component" not in array.dims: + values = values[:, None] + return {float(t): list(comps) for t, comps in zip(np.asarray(array["t"]), values)} + + +def legacy_views(output) -> SimpleNamespace: + """The products of ``output`` as ``orbits``, ``f``, ``spline_values`` and ``n_sph``. + + * ``orbits.``: array of shape ``(time, marker, quantity)``. + * ``f..``: ``f_binned``, ``delta_f_binned`` and one ``grid_`` per bin axis. + * ``spline_values.._log`` (``_phy``): ``.data``, see :func:`_field_data`. + * ``n_sph..``: ``n_sph`` and its meshgrid ``grid_n_sph``. + """ + views = SimpleNamespace( + orbits=SimpleNamespace(), + f=SimpleNamespace(), + spline_values=SimpleNamespace(), + n_sph=SimpleNamespace(), + ) + + for species, array in output.orbit_catalog.items(): + setattr(views.orbits, species, np.asarray(array)) + + for key, array in output.field_catalog.items(): + species, name = key.split("/", 1) + # the fields in logical coordinates were saved as ``_log``, the pushed-forward ones as ``_phy`` + name = f"{name.removesuffix('_xyz')}_phy" if name.endswith("_xyz") else f"{name}_log" + _set(views.spline_values, (species, name), SimpleNamespace(data=_field_data(array))) + + for key, array in output.distribution_catalog.items(): + species, slice_name, name = key.split("/") + _set(views.f, (species, slice_name, f"{name}_binned"), np.asarray(array)) + for dim in array.dims: + if dim != "t": + _set(views.f, (species, slice_name, f"grid_{dim}"), np.asarray(array[dim])) + + for key, array in output.density_catalog.items(): + species, view, _ = key.split("/") + grid = np.meshgrid(*(np.asarray(array[dim]) for dim in ("e1", "e2", "e3")), indexing="ij") + _set(views.n_sph, (species, view, "n_sph"), np.asarray(array)) + _set(views.n_sph, (species, view, "grid_n_sph"), tuple(grid)) + + return views diff --git a/src/struphy/post_processing/tests/test_legacy.py b/src/struphy/post_processing/tests/test_legacy.py new file mode 100644 index 000000000..385541d6a --- /dev/null +++ b/src/struphy/post_processing/tests/test_legacy.py @@ -0,0 +1,80 @@ +"""Tests for the views of an Output in the shapes of earlier versions.""" + +from types import SimpleNamespace + +import numpy as np +import xarray as xr + +from struphy.post_processing.legacy import legacy_views + +t = np.array([0.0, 0.1, 0.2]) +e1, e2, e3 = np.linspace(0, 1, 4), np.linspace(0, 1, 5), np.array([0.0]) + + +def make_output(): + scalar_field = xr.DataArray( + np.random.rand(3, 4, 5, 1), dims=("t", "e1", "e2", "e3"), coords={"t": t, "e1": e1, "e2": e2, "e3": e3} + ) + vector_field = xr.DataArray( + np.random.rand(3, 3, 4, 5, 1), + dims=("t", "component", "e1", "e2", "e3"), + coords={"t": t, "component": [0, 1, 2], "e1": e1, "e2": e2, "e3": e3}, + ) + binned = xr.DataArray( + np.random.rand(3, 6, 7), dims=("t", "e1", "v1"), coords={"t": t, "e1": np.arange(6), "v1": np.arange(7)} + ) + return SimpleNamespace( + orbit_catalog={"ions": xr.DataArray(np.random.rand(3, 8, 8), dims=("t", "marker", "quantity"))}, + field_catalog={ + "em_fields/phi": scalar_field, + "em_fields/e_field": vector_field, + "em_fields/e_field_xyz": vector_field, + }, + distribution_catalog={"ions/e1_v1_density/f": binned, "ions/e1_v1_density/delta_f": binned * 2}, + density_catalog={"fluid/view_0/n": scalar_field}, + ) + + +def test_orbits_are_arrays(): + out = make_output() + orbits = legacy_views(out).orbits.ions + assert isinstance(orbits, np.ndarray) + assert orbits.shape == (3, 8, 8) + + +def test_fields_map_time_to_a_list_of_components(): + out = make_output() + fields = legacy_views(out).spline_values.em_fields + + assert sorted(vars(fields)) == ["e_field_log", "e_field_phy", "phi_log"] + data = fields.phi_log.data + assert list(data) == [0.0, 0.1, 0.2] + assert len(data[0.2]) == 1 + np.testing.assert_array_equal(data[0.2][0], out.field_catalog["em_fields/phi"].values[2]) + + data = fields.e_field_log.data + assert len(data[0.1]) == 3 + np.testing.assert_array_equal(data[0.1][2], out.field_catalog["em_fields/e_field"].values[1, 2]) + assert max(fields.phi_log.data) == 0.2 + + +def test_binned_distributions_carry_their_grids(): + out = make_output() + sli = legacy_views(out).f.ions.e1_v1_density + + np.testing.assert_array_equal(sli.f_binned, out.distribution_catalog["ions/e1_v1_density/f"].values) + np.testing.assert_array_equal(sli.delta_f_binned, 2 * sli.f_binned) + np.testing.assert_array_equal(sli.grid_e1, np.arange(6)) + np.testing.assert_array_equal(sli.grid_v1, np.arange(7)) + assert not hasattr(sli, "grid_t") + + +def test_sph_density_comes_with_its_meshgrid(): + out = make_output() + view = legacy_views(out).n_sph.fluid.view_0 + + assert view.n_sph.shape == (3, 4, 5, 1) + ee1, ee2, ee3 = view.grid_n_sph + assert ee1.shape == ee2.shape == ee3.shape == (4, 5, 1) + np.testing.assert_array_equal(ee1[:, 0, 0], e1) + np.testing.assert_array_equal(ee2[0, :, 0], e2) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 121c11ebe..acbb31234 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -65,6 +65,7 @@ from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.physics.physics import Units from struphy.pic.base import Particles +from struphy.post_processing.legacy import legacy_views from struphy.post_processing.output import Output from struphy.propagators.base import Propagator from struphy.simulation.base import SimulationBase @@ -932,6 +933,8 @@ def pproc( def load_plotting_data(self) -> Output | None: """Deprecated, use :attr:`output`; attaches its products as attributes of the simulation. + They have the shapes of earlier versions, see :func:`struphy.post_processing.legacy.legacy_views`. + Returns the :class:`struphy.Output` on rank 0 and ``None`` on the other ranks. """ warnings.warn( @@ -942,13 +945,14 @@ def load_plotting_data(self) -> Output | None: if self.rank != 0: return None output = self.output - self.orbits = output.orbits - self.f = output.distributions - self.spline_values = output.fields - self.n_sph = output.densities + views = legacy_views(output) + self.orbits = views.orbits + self.f = views.f + self.spline_values = views.spline_values + self.n_sph = views.n_sph self.grids_log = output.grids_log self.grids_phy = output.grids_phy - self.t_grid = output.time + self.t_grid = xp.array(output.time) return output @property diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index e4978823d..e770cafa0 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -3,8 +3,10 @@ import json import os from pathlib import Path +from types import SimpleNamespace import h5py +import numpy as np import pytest from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time @@ -153,20 +155,15 @@ def test_deprecated_pproc_delegates_to_the_output(tmp_path, monkeypatch): def test_deprecated_load_plotting_data_attaches_the_products(tmp_path, monkeypatch): sim = make_sim(tmp_path) output = sim.output - for name, value in ( - ("orbits", "o"), - ("distributions", "f"), - ("fields", "s"), - ("densities", "n"), - ("grids_log", "gl"), - ("grids_phy", "gp"), - ("time", "t"), - ): + views = SimpleNamespace(orbits="o", f="f", spline_values="s", n_sph="n") + monkeypatch.setattr("struphy.simulation.sim.legacy_views", lambda out: views if out is output else None) + for name, value in (("grids_log", "gl"), ("grids_phy", "gp"), ("time", np.array([0.0, 0.5]))): monkeypatch.setattr(type(output), name, property(lambda self, value=value: value)) with pytest.deprecated_call(): assert sim.load_plotting_data() is output assert (sim.orbits, sim.f, sim.spline_values, sim.n_sph) == ("o", "f", "s", "n") - assert (sim.grids_log, sim.grids_phy, sim.t_grid) == ("gl", "gp", "t") + assert (sim.grids_log, sim.grids_phy) == ("gl", "gp") + assert sim.t_grid.tolist() == [0.0, 0.5] with pytest.deprecated_call(): assert sim.plotting_data is output From 0118957084d053aabbb97a12e262697f479fc5fd Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 09:26:13 +0200 Subject: [PATCH 065/193] Added hint on migration postprocessing --- src/struphy/diagnostics/diagn_tools.py | 8 ++++++-- src/struphy/simulation/sim.py | 20 +++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index a53848925..187625d8d 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -66,8 +66,12 @@ def wrapped(field, *args, **kwargs): if not isinstance(field, dict): return function(field, *args, **kwargs) warnings.warn( - f"diagn_tools.{function.__name__}(values, name, grids, ...) is deprecated; " - "pass a field of an Output, e.g. run.fields.em_fields.e_field_log.", + f"diagn_tools.{function.__name__}(values, name, grids, ...) is deprecated; pass a field of an Output.\n" + "How to update your script, with out = sim.output:\n" + " power_spectrum_2d(E_of_t, 'e_field_log', grids=sim.grids_log, grids_mapped=sim.grids_phy, ...)\n" + " -> out.fields.em_fields.e_field_log.struphy.analysis.dispersion(physical=True, ...)\n" + "'physical=True' replaces 'grids_mapped'; 'grids' and 'name' are read from the field itself. " + "Take the field from out.with_time_units('normalized') if you compare with normalized dispersion relations.", DeprecationWarning, stacklevel=2, ) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index acbb31234..c72dfe370 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -914,7 +914,11 @@ def pproc( ) -> Output | None: """Deprecated, use ``sim.output.pproc(...)``, see :meth:`struphy.Output.pproc`.""" warnings.warn( - "Simulation.pproc() is deprecated; use sim.output.pproc(...) instead.", + "Simulation.pproc() is deprecated; use sim.output.pproc(...) instead.\n" + "How to update your script: replace 'sim.pproc(physical=True)' by 'out = sim.output' and " + "'out.pproc(physical=True)' (same options). Post-processing also runs on first access of a product, " + "so the call can be dropped if the default options suffice; drop 'sim.load_plotting_data()' as well " + "and read the products from 'out', see the warning of load_plotting_data().", DeprecationWarning, stacklevel=2, ) @@ -938,7 +942,16 @@ def load_plotting_data(self) -> Output | None: Returns the :class:`struphy.Output` on rank 0 and ``None`` on the other ranks. """ warnings.warn( - "Simulation.load_plotting_data() is deprecated; use sim.output (a struphy.Output) instead.", + "Simulation.load_plotting_data() is deprecated; use sim.output (a struphy.Output) instead.\n" + "How to update your script, with out = sim.output:\n" + " sim.orbits. -> out.orbits.\n" + " sim.f...f_binned -> out.distributions...f\n" + " sim.f...grid_e1 -> out.distributions...e1\n" + " sim.spline_values.._log.data -> out.fields..\n" + " sim.spline_values.._phy.data -> out.fields.._xyz\n" + " sim.n_sph...n_sph -> out.densities...n\n" + " sim.grids_log / sim.grids_phy / sim.t_grid -> out.grids_log / out.grids_phy / out.time\n" + "The products are xarray.DataArrays with named dimensions and coordinates (e.g. arr.t, arr.e1).", DeprecationWarning, stacklevel=2, ) @@ -959,7 +972,8 @@ def load_plotting_data(self) -> Output | None: def plotting_data(self) -> Output: """Deprecated alias of :attr:`output`.""" warnings.warn( - "Simulation.plotting_data is deprecated; use sim.output instead.", + "Simulation.plotting_data is deprecated; use sim.output instead " + "(e.g. 'sim.plotting_data.orbits' -> 'sim.output.orbits').", DeprecationWarning, stacklevel=2, ) From b58e09bafb5a7b5f7fc619c26a48927053799cc2 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 19 Sep 2026 11:56:19 +0200 Subject: [PATCH 066/193] mat_w = loc_weight.copy() since assembly can be repeated as density changes --- src/struphy/feec/mass.py | 4 +- .../feec/tests/test_mass_reassembly.py | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/struphy/feec/tests/test_mass_reassembly.py diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index c8b63ae9d..8fac3058f 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -2208,7 +2208,9 @@ def assemble(self, weights=None, clear=True): PTS = xp.meshgrid(*pts, indexing="ij") mat_w = loc_weight(*PTS).copy() elif isinstance(loc_weight, xp.ndarray): - mat_w = loc_weight + # Spline factors below must not modify the stored geometric + # weight: assembly can be repeated as density changes. + mat_w = loc_weight.copy() elif loc_weight is not None: raise TypeError( "weights must be callable or xp.ndarray or None but is {}".format( diff --git a/src/struphy/feec/tests/test_mass_reassembly.py b/src/struphy/feec/tests/test_mass_reassembly.py new file mode 100644 index 000000000..49d862443 --- /dev/null +++ b/src/struphy/feec/tests/test_mass_reassembly.py @@ -0,0 +1,39 @@ +"""Regression for reassembling mass operators with an evolving spline weight.""" + +import numpy as np +from feectools.ddm.mpi import mpi as MPI + +from struphy import DerhamOptions, domains, grids +from struphy.feec.mass import WeightedMassOperators +from struphy.feec.psydac_derham import Derham + + +def test_density_weighted_mass_reassembly(): + # Non-unit Jacobian and non-unit density expose accumulation of the spline + # factor into the cached geometric weights on subsequent assemblies. + domain = domains.Cuboid(r1=2.0, r2=3.0) + derham = Derham( + grids.TensorProductGrid(num_elements=(4, 4, 1)), + DerhamOptions(degree=(2, 2, 1)), + comm=MPI.COMM_WORLD, + domain=domain, + ) + masses = WeightedMassOperators(derham, domain) + weighted = masses.WMMnew + rho = weighted.spline_functions["l2_field"] + rho.vector = derham.projectors["3"](lambda e1, e2, e3: 2.0 + 0 * e1) + probe = derham.projectors["v"]([ + lambda e1, e2, e3: 1.0 + 0 * e1, + lambda e1, e2, e3: 2.0 + 0 * e1, + lambda e1, e2, e3: 3.0 + 0 * e1, + ]) + reference = masses.Mv.dot(probe).toarray() * (2.0 / 6.0) + weighted.assemble() + np.testing.assert_allclose(weighted.dot(probe).toarray(), reference, rtol=1e-12, atol=1e-12) + weighted.assemble() + np.testing.assert_allclose(weighted.dot(probe).toarray(), reference, rtol=1e-12, atol=1e-12) + rho.vector *= 1.5 + weighted.assemble() + np.testing.assert_allclose(weighted.dot(probe).toarray(), 1.5 * reference, rtol=1e-12, atol=1e-12) + weighted.assemble() + np.testing.assert_allclose(weighted.dot(probe).toarray(), 1.5 * reference, rtol=1e-12, atol=1e-12) From 49f5e448c7cdbc11f2e689fc37f8b148c58282fa Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 00:32:38 +0200 Subject: [PATCH 067/193] Bugfixes, make sure scalars are up to date and M2Bn built B_eq as curl of the projected vector potential. In a periodic box that loses a uniform field, so the Hall term silently vanished. Now uses the projected field like M2B, matching its own docstring. --- src/struphy/feec/mass.py | 44 +++--------------------- src/struphy/models/scalars.py | 12 +++++-- src/struphy/models/tests/test_scalars.py | 28 +++++++++++++++ 3 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 src/struphy/models/tests/test_scalars.py diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index 8fac3058f..519fc2108 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -681,46 +681,12 @@ def M2Bn(self): assert self.eq_mhd is not None, ( "M2Bn requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) - a_eq = self.derham.P1( - [ - self.eq_mhd.a1_1, - self.eq_mhd.a1_2, - self.eq_mhd.a1_3, - ], - ) - - tmp_b2 = self.derham.curl.dot(a_eq) - b02fun = self.derham.create_spline_function("b02", "Hdiv") - b02fun.vector = tmp_b2 - - def b02funx(x, y, z): - return b02fun( - x, - y, - z, - local=True, - )[0] - - def b02funy(x, y, z): - return b02fun( - x, - y, - z, - local=True, - )[1] - - def b02funz(x, y, z): - return b02fun( - x, - y, - z, - local=True, - )[2] - + # The equilibrium field itself, as in M2B: the curl of the projected vector potential (M2B_div0) + # loses a uniform field in periodic directions, where the potential is a ramp. rot_B = LocalRotationMatrix( - b02funx, - b02funy, - b02funz, + self.eq_mhd.b2_1, + self.eq_mhd.b2_2, + self.eq_mhd.b2_3, ) self._M2Bn = self.create_weighted_mass( diff --git a/src/struphy/models/scalars.py b/src/struphy/models/scalars.py index 35acedb34..84c88660c 100644 --- a/src/struphy/models/scalars.py +++ b/src/struphy/models/scalars.py @@ -156,9 +156,17 @@ def dct(self) -> dict[str, Scalar]: def update(self): for scalar in self.dct.values(): scalar.update() - # reset status to False for next update + # reset status to False for next update, including the summands of sums: `a + b + c` nests + # SumOfScalars(SumOfScalars(a, b), c), and an inner sum left up to date would keep its first value for scalar in self.dct.values(): - scalar.uptodate = False + _mark_outdated(scalar) + + +def _mark_outdated(scalar: Scalar): + scalar.uptodate = False + for variable in scalar.variables: + if isinstance(variable, Scalar): + _mark_outdated(variable) @auto_convert_docstring diff --git a/src/struphy/models/tests/test_scalars.py b/src/struphy/models/tests/test_scalars.py new file mode 100644 index 000000000..f8b0e9cc4 --- /dev/null +++ b/src/struphy/models/tests/test_scalars.py @@ -0,0 +1,28 @@ +from struphy.models.scalars import Scalar, Scalars + + +class _Constant(Scalar): + """A scalar whose value is set from outside, to follow updates without a simulation.""" + + def __init__(self, value): + super().__init__() + self.current = value + + def _local_update(self): + self.local_value[0] = self.current + + def _mpi_sum(self): + self.value[0] = self.local_value[0] + + +def test_nested_sum_follows_its_summands(): + """`a + b + c` is SumOfScalars(SumOfScalars(a, b), c); the inner sum must be recomputed at every update.""" + a, b, c = _Constant(1.0), _Constant(2.0), _Constant(3.0) + scalars = Scalars(a=a, b=b, c=c, total=a + b + c) + + scalars.update() + assert scalars.dct["total"].value[0] == 6.0 + + a.current, b.current, c.current = 10.0, 20.0, 30.0 + scalars.update() + assert scalars.dct["total"].value[0] == 60.0 From 911d41966407ae34f765a522af4b60e862f5fc18 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 09:16:00 +0200 Subject: [PATCH 068/193] Fix vlasov maxwell one species equation alignment --- src/struphy/models/vlasov_maxwell_one_species.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index b16415fff..de56361b5 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -277,9 +277,9 @@ def doc_pde(cls): .. math:: - \int_{\Omega} \nabla \psi^{\top} \cdot \nabla \phi \, \textrm{d} \mathbf{x} &= \frac{\alpha^2}{\varepsilon} \int_{\Omega} \int_{\mathbb{R}^3} \psi \, (f - f_0) \, \text{d}^3 \mathbf{v} \, \textrm{d} \mathbf{x} \qquad \forall \ \psi \in H^1 + \int_{\Omega} \nabla \psi^{\top} \cdot \nabla \phi \, \textrm{d} \mathbf{x} = \frac{\alpha^2}{\varepsilon} \int_{\Omega} \int_{\mathbb{R}^3} \psi \, (f - f_0) \, \text{d}^3 \mathbf{v} \, \textrm{d} \mathbf{x} \qquad \forall \ \psi \in H^1 \\[2mm] - \mathbf{E}(t=0) &= -\nabla \phi(t=0) + \mathbf{E}(t=0) = -\nabla \phi(t=0) Moreover, it is assumed that From e5868c06dd8a4a2602204ca0cb5aabf1a0b8bea0 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 09:18:07 +0200 Subject: [PATCH 069/193] bugfix: Fix nested scalar sums in Scalars.update() --- src/struphy/models/scalars.py | 12 ++++++++-- src/struphy/models/tests/test_scalars.py | 28 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/struphy/models/tests/test_scalars.py diff --git a/src/struphy/models/scalars.py b/src/struphy/models/scalars.py index 35acedb34..84c88660c 100644 --- a/src/struphy/models/scalars.py +++ b/src/struphy/models/scalars.py @@ -156,9 +156,17 @@ def dct(self) -> dict[str, Scalar]: def update(self): for scalar in self.dct.values(): scalar.update() - # reset status to False for next update + # reset status to False for next update, including the summands of sums: `a + b + c` nests + # SumOfScalars(SumOfScalars(a, b), c), and an inner sum left up to date would keep its first value for scalar in self.dct.values(): - scalar.uptodate = False + _mark_outdated(scalar) + + +def _mark_outdated(scalar: Scalar): + scalar.uptodate = False + for variable in scalar.variables: + if isinstance(variable, Scalar): + _mark_outdated(variable) @auto_convert_docstring diff --git a/src/struphy/models/tests/test_scalars.py b/src/struphy/models/tests/test_scalars.py new file mode 100644 index 000000000..f8b0e9cc4 --- /dev/null +++ b/src/struphy/models/tests/test_scalars.py @@ -0,0 +1,28 @@ +from struphy.models.scalars import Scalar, Scalars + + +class _Constant(Scalar): + """A scalar whose value is set from outside, to follow updates without a simulation.""" + + def __init__(self, value): + super().__init__() + self.current = value + + def _local_update(self): + self.local_value[0] = self.current + + def _mpi_sum(self): + self.value[0] = self.local_value[0] + + +def test_nested_sum_follows_its_summands(): + """`a + b + c` is SumOfScalars(SumOfScalars(a, b), c); the inner sum must be recomputed at every update.""" + a, b, c = _Constant(1.0), _Constant(2.0), _Constant(3.0) + scalars = Scalars(a=a, b=b, c=c, total=a + b + c) + + scalars.update() + assert scalars.dct["total"].value[0] == 6.0 + + a.current, b.current, c.current = 10.0, 20.0, 30.0 + scalars.update() + assert scalars.dct["total"].value[0] == 60.0 From 3fbb6536a7016933aa3765324ba42f0fb7fcf58c Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 09:21:07 +0200 Subject: [PATCH 070/193] preserve uniform periodic fields in M2Bn --- src/struphy/feec/mass.py | 44 +++++----------------------------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index c8b63ae9d..5c0748b04 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -681,46 +681,12 @@ def M2Bn(self): assert self.eq_mhd is not None, ( "M2Bn requires an MHD equilibrium to be provided when initializing the WeightedMassOperators object." ) - a_eq = self.derham.P1( - [ - self.eq_mhd.a1_1, - self.eq_mhd.a1_2, - self.eq_mhd.a1_3, - ], - ) - - tmp_b2 = self.derham.curl.dot(a_eq) - b02fun = self.derham.create_spline_function("b02", "Hdiv") - b02fun.vector = tmp_b2 - - def b02funx(x, y, z): - return b02fun( - x, - y, - z, - local=True, - )[0] - - def b02funy(x, y, z): - return b02fun( - x, - y, - z, - local=True, - )[1] - - def b02funz(x, y, z): - return b02fun( - x, - y, - z, - local=True, - )[2] - + # The equilibrium field itself, as in M2B: the curl of the projected vector potential (M2B_div0) + # loses a uniform field in periodic directions, where the potential is a ramp. rot_B = LocalRotationMatrix( - b02funx, - b02funy, - b02funz, + self.eq_mhd.b2_1, + self.eq_mhd.b2_2, + self.eq_mhd.b2_3, ) self._M2Bn = self.create_weighted_mass( From 1f9b1776428f9bdd9a659907fd9d5f40f0d642a6 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 10:11:57 +0200 Subject: [PATCH 071/193] Read marker arrays at every kinetic-energy update --- src/struphy/models/scalars.py | 26 ++++++------ src/struphy/models/tests/test_scalars.py | 53 +++++++++++++++++++++++- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/struphy/models/scalars.py b/src/struphy/models/scalars.py index 84c88660c..9877b3f0f 100644 --- a/src/struphy/models/scalars.py +++ b/src/struphy/models/scalars.py @@ -293,14 +293,14 @@ class KineticEnergyPIC(PICScalar): """ def _local_update(self): - if not hasattr(self, "velocities"): - self.velocities = self.variables[ - 0 - ].particles.velocities # TODO: velocities need to redefined for Particles5d? Put magnetic moment as COM. - self.weights = self.variables[0].particles.weights - self.Np = self.variables[0].particles.Np - - energy = self.normalization * 0.5 / self.Np * xp.sum(self.weights * xp.sum(self.velocities**2, axis=1)) + # `particles.velocities` and `.weights` are fancy-indexed copies of the marker array, not views, + # so they must be read at every update: a cached copy keeps the state of the first call forever. + # TODO: velocities need to be redefined for Particles5d? Put magnetic moment as COM. + particles = self.variables[0].particles + velocities = particles.velocities + weights = particles.weights + + energy = self.normalization * 0.5 / particles.Np * xp.sum(weights * xp.sum(velocities**2, axis=1)) self.local_value[0] = energy @@ -339,12 +339,12 @@ class KineticEnergySPH(SPHScalar): """ def _local_update(self): - if not hasattr(self, "velocities"): - self.velocities = self.variables[0].particles.velocities - self.weights = self.variables[0].particles.weights - self.Np = self.variables[0].particles.Np + # As in KineticEnergyPIC: the marker arrays are copies, so they are read at every update. + particles = self.variables[0].particles + velocities = particles.velocities + weights = particles.weights - energy = self.normalization * 0.5 / self.Np * xp.sum(self.weights * xp.sum(self.velocities**2, axis=1)) + energy = self.normalization * 0.5 / particles.Np * xp.sum(weights * xp.sum(velocities**2, axis=1)) self.local_value[0] = energy diff --git a/src/struphy/models/tests/test_scalars.py b/src/struphy/models/tests/test_scalars.py index f8b0e9cc4..3ce184adb 100644 --- a/src/struphy/models/tests/test_scalars.py +++ b/src/struphy/models/tests/test_scalars.py @@ -1,4 +1,7 @@ -from struphy.models.scalars import Scalar, Scalars +import cunumpy as xp + +from struphy.models.scalars import KineticEnergyPIC, KineticEnergySPH, Scalar, Scalars +from struphy.models.variables import PICVariable, SPHVariable class _Constant(Scalar): @@ -26,3 +29,51 @@ def test_nested_sum_follows_its_summands(): a.current, b.current, c.current = 10.0, 20.0, 30.0 scalars.update() assert scalars.dct["total"].value[0] == 60.0 + + +class _MovingParticles: + """Markers whose accessors return copies, as `Particles.velocities` does (it is fancy-indexed).""" + + def __init__(self): + self._markers = xp.ones((4, 3), dtype=float) + self._weights = xp.ones(4, dtype=float) + self._valid = xp.ones(4, dtype=bool) + self.Np = 4 + + @property + def velocities(self): + return self._markers[self._valid] # boolean indexing, as Particles does: a copy, not a view + + @property + def weights(self): + return self._weights[self._valid] + + def accelerate(self, factor): + self._markers *= factor + + +class _FakePICVariable(PICVariable): + def __init__(self, particles): + self._particles = particles + + +class _FakeSPHVariable(SPHVariable): + def __init__(self, particles): + self._particles = particles + + +def test_kinetic_energy_follows_the_markers(): + """The marker arrays are copies, so a cached one would report the first step's energy forever.""" + for variable_class, scalar_class in ((_FakePICVariable, KineticEnergyPIC), (_FakeSPHVariable, KineticEnergySPH)): + particles = _MovingParticles() + scalar = scalar_class(variable_class(particles)) + + scalar._local_update() + before = float(scalar.local_value[0]) + + particles.accelerate(2.0) # four times the kinetic energy + scalar._local_update() + after = float(scalar.local_value[0]) + + assert before > 0.0 + assert after == 4.0 * before, f"{scalar_class.__name__} did not follow the markers: {before} -> {after}" From bf68b24f0fd6f73809dcfd577c047b412c946fb3 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 10:14:47 +0200 Subject: [PATCH 072/193] Implemented the quadratic thermal channel --- src/struphy/feec/mass.py | 30 +++++++++++++++++++ .../models/linear_extended_mh_duniform.py | 14 +++++++-- src/struphy/models/linear_mhd.py | 15 ++++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index c8b63ae9d..60fcfdcae 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -268,6 +268,36 @@ def M3(self): return self._M3 + @auto_convert_docstring + @property + def M3p_inv(self): + r""" + Pressure-weighted mass matrix for 3-forms: + + .. math:: + + \mathbb M^{3,p^{-1}}_{ijk,mno} = + \int \Lambda^3_{ijk}\,\Lambda^3_{mno} + \frac{1}{p_0\sqrt{g}}\,\mathrm d\boldsymbol\eta. + + Here :math:`p_0` is the equilibrium pressure. This operator is used + for the quadratic pressure energy of linear MHD perturbations. + """ + if not hasattr(self, "_M3p_inv"): + assert self.eq_mhd is not None, "M3p_inv requires an MHD equilibrium with positive pressure." + + def inv_p0(e1, e2, e3): + return 1.0 / self.eq_mhd.p0(e1, e2, e3) + + self._M3p_inv = self.create_weighted_mass( + "L2", + "L2", + weights=(inv_p0, "1/sqrt_g"), + name="M3p_inv", + assemble=True, + ) + return self._M3p_inv + @auto_convert_docstring @property def Mv(self): diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index a06c0aa43..175a754ca 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -106,7 +106,13 @@ def __init__( # 5. define scalars to be tracked during simulation kinetic_energy = BilinearEnergyFEEC(self.mhd.velocity, bilinear_form_name="M2n") + # Keep the linear pressure integral as a mean-pressure diagnostic. pressure_energy = VolumeFormEnergyFEEC(self.mhd.pressure, normalization=1.0 / (5.0 / 3.0 - 1.0)) + thermal_energy = BilinearEnergyFEEC( + self.mhd.pressure, + bilinear_form_name="M3p_inv", + normalization=1.0 / (5.0 / 3.0), + ) magnetic_energy = BilinearEnergyFEEC(self.em_fields.b_field) background_pressure = FunctionScalarFEEC(self._compute_en_p_eq) background_magnetic = FunctionScalarFEEC(self._compute_en_B_eq) @@ -115,11 +121,12 @@ def __init__( self.scalars = Scalars( en_U=kinetic_energy, en_p=pressure_energy, + en_thermal=thermal_energy, en_B=magnetic_energy, en_p_eq=background_pressure, en_B_eq=background_magnetic, en_B_tot=total_magnetic, - en_tot=kinetic_energy + pressure_energy + magnetic_energy, + en_tot=kinetic_energy + thermal_energy + magnetic_energy, helicity=helicity, ) @@ -232,12 +239,13 @@ def doc_scalar_quantities(cls): r"""**The following scalars are tracked during simulation:** - Flow kinetic energy: ``en_U`` - - Thermal pressure energy: ``en_p`` + - Mean pressure perturbation (linear diagnostic): ``en_p`` + - Thermal perturbation energy: ``en_thermal`` - Magnetic perturbation energy: ``en_B`` - Background pressure energy: ``en_p_eq`` - Background magnetic energy: ``en_B_eq`` - Total magnetic energy: ``en_B_tot`` - - Total perturbation energy: ``en_tot`` + - Total perturbation energy (kinetic + magnetic + thermal): ``en_tot`` - Magnetic helicity-like invariant: ``helicity``""" @classmethod diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index 21a353a72..eeda0a577 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -86,7 +86,14 @@ def __init__( # 5. define scalars to be tracked during simulation kinetic_energy = BilinearEnergyFEEC(self.mhd.velocity, bilinear_form_name="M2n") + # The volume integral is retained as a diagnostic of the mean pressure + # perturbation. It is linear and is not part of the linearized energy. pressure_energy = VolumeFormEnergyFEEC(self.mhd.pressure, normalization=1.0 / (5 / 3 - 1)) + thermal_energy = BilinearEnergyFEEC( + self.mhd.pressure, + bilinear_form_name="M3p_inv", + normalization=1.0 / (5 / 3), + ) magnetic_energy = BilinearEnergyFEEC(self.em_fields.b_field) background_pressure = FunctionScalarFEEC(self._compute_en_p_eq) background_magnetic = FunctionScalarFEEC(self._compute_en_B_eq) @@ -94,11 +101,12 @@ def __init__( self.scalars = Scalars( en_U=kinetic_energy, en_p=pressure_energy, + en_thermal=thermal_energy, en_B=magnetic_energy, en_p_eq=background_pressure, en_B_eq=background_magnetic, en_B_tot=total_magnetic, - en_tot=kinetic_energy + pressure_energy + magnetic_energy, + en_tot=kinetic_energy + thermal_energy + magnetic_energy, ) @property @@ -180,8 +188,9 @@ def doc_scalar_quantities(cls): - Kinetic energy (perturbation): :math:`E_U = \frac{1}{2} \int \rho_0 |\tilde{\mathbf{U}}|^2 \, \mathrm{d}V` - Magnetic energy (perturbation): :math:`E_B = \frac{1}{2} \int \frac{|\tilde{\mathbf{B}}|^2}{\mu_0} \, \mathrm{d}V` - - Internal energy (perturbation): :math:`E_p = \int \frac{\tilde{p}}{\gamma - 1} \, \mathrm{d}V` with :math:`\gamma = 5/3` - - Total perturbed energy: :math:`E_{\mathrm{tot}} = E_U + E_B + E_p` + - Mean pressure perturbation: :math:`E_p = \int \frac{\tilde{p}}{\gamma - 1} \, \mathrm{d}V` (``en_p``). This is a linear diagnostic, not an energy. + - Thermal perturbation energy: :math:`E_{\mathrm{thermal}} = \frac{1}{2}\int \frac{\tilde{p}^2}{\gamma p_0} \, \mathrm{d}V` (``en_thermal``), with :math:`\gamma = 5/3` + - Total perturbed energy: :math:`E_{\mathrm{tot}} = E_U + E_B + E_{\mathrm{thermal}}` (``en_tot``) - Equilibrium magnetic energy: :math:`E_{B0} = \frac{1}{2} \int \frac{|\mathbf{B}_0|^2}{\mu_0} \, \mathrm{d}V` - Equilibrium internal energy: :math:`E_{p0} = \int \frac{p_0}{\gamma - 1} \, \mathrm{d}V` - Total magnetic energy: :math:`E_{B,\mathrm{tot}} = \frac{1}{2} \int \frac{|\mathbf{B}_0 + \tilde{\mathbf{B}}|^2}{\mu_0} \, \mathrm{d}V`""" From ce57a9f4bed7ec936ddcd32d73887828528eaeef Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 10:25:03 +0200 Subject: [PATCH 073/193] bugfix: do not divide the kinetic-energy scalars by Np twice --- src/struphy/models/scalars.py | 20 ++++++++++++-------- src/struphy/models/tests/test_scalars.py | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/struphy/models/scalars.py b/src/struphy/models/scalars.py index 9877b3f0f..f60da8399 100644 --- a/src/struphy/models/scalars.py +++ b/src/struphy/models/scalars.py @@ -286,10 +286,12 @@ class KineticEnergyPIC(PICScalar): :math: - \mathcal E = \frac{\alpha}{2 N_p} \sum_{i=0}^{N_p-1} w_i v_i^2\,, + \mathcal E = \frac{\alpha}{2} \sum_{i=0}^{N_p-1} w_i v_i^2\,, - where :math:`\alpha` is a normalization constant, :math:`N_p` is the total number of particles, - and :math:`w_i` and :math:`v_i` are the weight and velocity of particle :math:`i`, respectively. + where :math:`\alpha` is a normalization constant and :math:`w_i` and :math:`v_i` are the weight and + velocity of particle :math:`i`. The marker weights already carry the :math:`1/N_p` of the Monte-Carlo + estimate (:meth:`~struphy.pic.base.Particles.initialize_weights` sets + :math:`w_i = f_i / (s_i N_p)`), so the sum must not be divided by :math:`N_p` again. """ def _local_update(self): @@ -300,7 +302,7 @@ def _local_update(self): velocities = particles.velocities weights = particles.weights - energy = self.normalization * 0.5 / particles.Np * xp.sum(weights * xp.sum(velocities**2, axis=1)) + energy = self.normalization * 0.5 * xp.sum(weights * xp.sum(velocities**2, axis=1)) self.local_value[0] = energy @@ -332,10 +334,12 @@ class KineticEnergySPH(SPHScalar): :math: - \mathcal E = \frac{\alpha}{2 N_p} \sum_{i=0}^{N_p-1} w_i v_i^2\,, + \mathcal E = \frac{\alpha}{2} \sum_{i=0}^{N_p-1} w_i v_i^2\,, - where :math:`\alpha` is a normalization constant, :math:`N_p` is the total number of particles, - and :math:`w_i` and :math:`v_i` are the weight and velocity of particle :math:`i`, respectively. + where :math:`\alpha` is a normalization constant and :math:`w_i` and :math:`v_i` are the weight and + velocity of particle :math:`i`. The marker weights already carry the :math:`1/N_p` of the Monte-Carlo + estimate (:meth:`~struphy.pic.base.Particles.initialize_weights` sets + :math:`w_i = f_i / (s_i N_p)`), so the sum must not be divided by :math:`N_p` again. """ def _local_update(self): @@ -344,7 +348,7 @@ def _local_update(self): velocities = particles.velocities weights = particles.weights - energy = self.normalization * 0.5 / particles.Np * xp.sum(weights * xp.sum(velocities**2, axis=1)) + energy = self.normalization * 0.5 * xp.sum(weights * xp.sum(velocities**2, axis=1)) self.local_value[0] = energy diff --git a/src/struphy/models/tests/test_scalars.py b/src/struphy/models/tests/test_scalars.py index 3ce184adb..87366386e 100644 --- a/src/struphy/models/tests/test_scalars.py +++ b/src/struphy/models/tests/test_scalars.py @@ -75,5 +75,7 @@ def test_kinetic_energy_follows_the_markers(): scalar._local_update() after = float(scalar.local_value[0]) - assert before > 0.0 + # Four markers of weight 1 and |v|^2 = 3: the energy is 0.5 * sum(w |v|^2) = 6. The weights already + # carry the 1/Np of the Monte-Carlo estimate, so the sum must not be divided by Np a second time. + assert before == 6.0, f"{scalar_class.__name__} is normalized wrongly: {before} instead of 6.0" assert after == 4.0 * before, f"{scalar_class.__name__} did not follow the markers: {before} -> {after}" From a6e44e63bb76d862413242635a116f5548de0e3a Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 14:19:01 +0200 Subject: [PATCH 074/193] Print a line in the logger --- src/struphy/simulation/sim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index c72dfe370..6c3f0898d 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -831,7 +831,8 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No # print current time and scalar quantities to screen step = str(int(self.time_state["index"][0])).zfill(len(total_steps_str)) - message = "time step:".ljust(25) + f"{step}/{total_steps + start_step}".rjust(25) + message = "\n" + "-" * 80 + message += "time step:".ljust(25) + f"{step}/{total_steps + start_step}".rjust(25) message += ( "\n" + "normalized time:".ljust(25) From 3553c64c371f558c2330ba16fc6159802ba6b7d5 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 15:11:50 +0200 Subject: [PATCH 075/193] =?UTF-8?q?Hasegawa=E2=80=93Wakatani=20bugfixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/struphy/models/hasegawa_wakatani.py | 19 +-- .../linear_vlasov_ampere_one_species.py | 9 +- .../models/tests/test_hasegawa_wakatani.py | 8 ++ ...test_verif_LinearVlasovAmpereOneSpecies.py | 129 ++++++++++++++++++ .../propagators/hasegawa_wakatani_step.py | 6 +- 5 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 src/struphy/models/tests/test_hasegawa_wakatani.py create mode 100644 src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index a0bc130a9..47c901e8d 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -1,8 +1,6 @@ import copy import logging -from feectools.linalg.stencil import StencilVector - from struphy.io.options import BaseUnits, LiteralOptions from struphy.models.base import StruphyModel from struphy.models.species import ( @@ -10,7 +8,6 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators.hasegawa_wakatani_step import HasegawaWakataniStep from struphy.propagators.poisson_solve import PoissonSolve @@ -48,8 +45,8 @@ def __init__(self, mass_number: float = 1.0): ## propagators class Propagators: - def __init__(self, phi: FEECVariable = None): - self.poisson = PoissonSolve() + def __init__(self, phi: FEECVariable = None, omega: FEECVariable = None): + self.poisson = PoissonSolve(rho=omega) self.hw = HasegawaWakataniStep(phi=phi) ## abstract methods @@ -67,7 +64,7 @@ def __init__(self, base_units: BaseUnits = BaseUnits(), mass_number: float = 1.0 self.setup_equation_params(base_units=base_units) # 3. instantiate all propagators - self.propagators = self.Propagators(phi=self.em_fields.phi) + self.propagators = self.Propagators(phi=self.em_fields.phi, omega=self.plasma.vorticity) # 4. assign variables to propagators self.propagators.poisson.variables.phi = self.em_fields.phi @@ -84,23 +81,13 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def update_rho(self): - omega = self.plasma.vorticity.spline.vector - self._rho = Propagator.mass_ops.M0.dot(omega, out=self._rho) - self._rho.update_ghost_regions() - return self._rho - def post_allocate(self): """Solve initial Poisson equation. :meta private: """ - self._rho: StencilVector = Propagator.derham.V0.zeros() - self.update_rho() - logger.info("\nINITIAL POISSON SOLVE:") - self.update_rho() self.propagators.poisson(1.0) logger.info("Done.") diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index b929e1df3..f640d9fda 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -174,10 +174,11 @@ def post_allocate(self): logger.info("\nINITIAL POISSON SOLVE:") - # use control variate method - particles = self.kinetic_ions.var.particles - particles.update_weights() - + # No control variate here: the markers of a DeltaFParticles6D species already carry + # delta_f alone (w = delta_f / s0 / Np, set by initialize_weights), so subtracting the + # background f0 a second time -- which is what update_weights does -- would leave every + # weight offset by the constant -f0 / s0 / Np. That constant is a net charge, and it makes + # the periodic Poisson problem below singular. self.initial_poisson.allocate() # Solve with dt=1. and compute electric field diff --git a/src/struphy/models/tests/test_hasegawa_wakatani.py b/src/struphy/models/tests/test_hasegawa_wakatani.py new file mode 100644 index 000000000..9c5343752 --- /dev/null +++ b/src/struphy/models/tests/test_hasegawa_wakatani.py @@ -0,0 +1,8 @@ +from struphy.models import HasegawaWakatani + + +def test_hasegawa_wakatani_couples_vorticity_to_potential(): + model = HasegawaWakatani() + + assert model.propagators.poisson.rho is model.plasma.vorticity + assert model.propagators.hw.options.coupling == 1.0 diff --git a/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py new file mode 100644 index 000000000..2f47f2172 --- /dev/null +++ b/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py @@ -0,0 +1,129 @@ +import logging +import os +import shutil + +import cunumpy as xp +from feectools.ddm.mpi import mpi as MPI + +from struphy import ( + BoundaryParameters, + DerhamOptions, + EnvironmentOptions, + LoadingParameters, + SavingParameters, + Simulation, + SortingParameters, + Time, + WeightsParameters, + domains, + grids, + maxwellians, + perturbations, +) +from struphy.models import LinearVlasovAmpereOneSpecies + +logger = logging.getLogger("struphy") + + +def test_delta_f_initial_condition(exit_before_run: bool = False): + """The delta-f markers must carry the density perturbation alone. + + A ``DeltaFParticles6D`` species solves for :math:`\\delta f = f - f_0`, so its weights + are :math:`\\delta f / (s_0 N_p)` already and no control variate may be applied to them. + Subtracting the background a second time offsets every weight by the same + :math:`-f_0 / (s_0 N_p)`, which is a net charge: the markers then carry a uniform + :math:`\\delta f` instead of the cosine, and the periodic Poisson problem solved in + ``post_allocate`` becomes singular. + + This runs zero time steps -- everything asserted here is set up during allocation -- + and checks that + + 1. the weights have no net charge, and + 2. the electric field built from them is the one the perturbation implies, + :math:`\\int E^2 / 2 \\, \\textrm{d}x = A^2 r_1 / (4 k^2)`. + """ + amplitude = 1e-3 + r1 = 12.56 # a box of length 4*pi, i.e. k = 2*pi/r1 = 0.5 + + model = LinearVlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False, with_E0=False) + + test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + out_folders = os.path.join(test_folder, "LinearVlasovAmpereOneSpecies") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="delta_f_init") + + # no time stepping: post_allocate does the initial Poisson solve this test is about + time_opts = Time(dt=0.05, Tend=0.0) + + domain = domains.Cuboid(r1=r1) + grid = grids.TensorProductGrid(num_elements=(32, 1, 1)) + derham_opts = DerhamOptions(degree=(3, 1, 1)) + + ppc = 2000 + model.kinetic_ions.set_markers( + loading_params=LoadingParameters(ppc=ppc, seed=1234), + weights_params=WeightsParameters(), + boundary_params=BoundaryParameters(), + sorting_params=SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True), + saving_params=SavingParameters(), + bufsize=0.4, + ) + + model.propagators.push_eta.options = model.propagators.push_eta.Options() + model.propagators.coupling_Eweights.options = model.propagators.coupling_Eweights.Options() + # The constant mode of the periodic Poisson problem needs a finite stabilization: left at + # the default the solver is free to wander along it, and the physical part of phi then + # drowns in the solver tolerance applied to a solution of arbitrary size. + model.initial_poisson.options = model.initial_poisson.Options(stab_mat="M0", stab_eps=1e-6) + + model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(1.0, None))) + perturbation = perturbations.ModesCos(ls=(1,), amps=(amplitude,)) + model.kinetic_ions.var.add_initial_condition(maxwellians.Maxwellian3D(n=(1.0, perturbation))) + + sim = Simulation( + model=model, + env=env, + time_opts=time_opts, + domain=domain, + grid=grid, + derham_opts=derham_opts, + ) + + if exit_before_run: + logger.info("Exiting before running simulation.") + return sim + + sim.run() + + comm = MPI.COMM_WORLD + + # 1. no net charge: the weights of a cosine perturbation cancel over the box, so their + # mean is Monte-Carlo noise, small against their own spread. The control variate applied + # by mistake made every weight equal, i.e. |mean| == max|w|. + # Checked per rank: each one holds a random subset of the markers, so its own mean is + # as good an estimator of the net charge as the global one. + weights = xp.asarray(model.kinetic_ions.var.particles.weights) + mean = float(weights.mean()) + rms = float(xp.sqrt((weights**2).mean())) + assert abs(mean) < 0.1 * rms, ( + f"Assertion for delta-f weights failed: net charge {mean =} is not small against {rms =}; " + "the markers do not carry the density perturbation alone." + ) + logger.info(f"Assertion for delta-f weights passed ({mean =}, {rms =}).") + + # 2. the field energy of the cosine mode, int E^2/2 dx with E = A/k + k = 2.0 * xp.pi / r1 + energy_exact = amplitude**2 * r1 / (4.0 * k**2) + + if comm.Get_rank() == 0: + energy = float(xp.asarray(sim.output.evaluate("en_E"))[0]) + rel_error = abs(energy - energy_exact) / energy_exact + assert rel_error < 0.05, ( + f"Assertion for the initial delta-f field energy failed: {energy =} vs. {energy_exact =}." + ) + logger.info(f"Assertion for the initial delta-f field energy passed ({rel_error =}).") + + shutil.rmtree(test_folder) + + +if __name__ == "__main__": + test_delta_f_initial_condition() diff --git a/src/struphy/propagators/hasegawa_wakatani_step.py b/src/struphy/propagators/hasegawa_wakatani_step.py index 07713f220..5e5b1bed3 100644 --- a/src/struphy/propagators/hasegawa_wakatani_step.py +++ b/src/struphy/propagators/hasegawa_wakatani_step.py @@ -113,6 +113,9 @@ class Options(OptionsBase): c_fun : {"const"}, default="const" Choice of coupling profile :math:`C(x,y)` used in the model. + coupling : float, default=1.0 + Constant value of :math:`C` when ``c_fun="const"``. + kappa : float, default=1.0 Constant multiplying the background-gradient drift term. @@ -138,6 +141,7 @@ class Options(OptionsBase): OptsCfun = Literal["const"] # propagator options c_fun: OptsCfun = "const" + coupling: float = 1.0 kappa: float = 1.0 nu: float = 0.01 butcher: ButcherTableau = None @@ -181,7 +185,7 @@ def allocate(self): # default c-function if self.options.c_fun == "const": - c_fun = lambda e1, e2, e3: 0.0 + 0.0 * e1 + c_fun = lambda e1, e2, e3: self.options.coupling + 0.0 * e1 else: raise NotImplementedError(f"{self.options.c_fun =} is not available.") From 15639c13f9b5c09b7ad8439747960b313e745324 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 15:28:24 +0200 Subject: [PATCH 076/193] =?UTF-8?q?Fix=20Hasegawa=E2=80=93Wakatani=20coupl?= =?UTF-8?q?ing=20and=20linear=20delta-f=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/struphy/models/hasegawa_wakatani.py | 19 +-- .../linear_vlasov_ampere_one_species.py | 9 +- .../models/tests/test_hasegawa_wakatani.py | 8 ++ ...test_verif_LinearVlasovAmpereOneSpecies.py | 129 ++++++++++++++++++ .../propagators/hasegawa_wakatani_step.py | 6 +- 5 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 src/struphy/models/tests/test_hasegawa_wakatani.py create mode 100644 src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index a0bc130a9..47c901e8d 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -1,8 +1,6 @@ import copy import logging -from feectools.linalg.stencil import StencilVector - from struphy.io.options import BaseUnits, LiteralOptions from struphy.models.base import StruphyModel from struphy.models.species import ( @@ -10,7 +8,6 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators.hasegawa_wakatani_step import HasegawaWakataniStep from struphy.propagators.poisson_solve import PoissonSolve @@ -48,8 +45,8 @@ def __init__(self, mass_number: float = 1.0): ## propagators class Propagators: - def __init__(self, phi: FEECVariable = None): - self.poisson = PoissonSolve() + def __init__(self, phi: FEECVariable = None, omega: FEECVariable = None): + self.poisson = PoissonSolve(rho=omega) self.hw = HasegawaWakataniStep(phi=phi) ## abstract methods @@ -67,7 +64,7 @@ def __init__(self, base_units: BaseUnits = BaseUnits(), mass_number: float = 1.0 self.setup_equation_params(base_units=base_units) # 3. instantiate all propagators - self.propagators = self.Propagators(phi=self.em_fields.phi) + self.propagators = self.Propagators(phi=self.em_fields.phi, omega=self.plasma.vorticity) # 4. assign variables to propagators self.propagators.poisson.variables.phi = self.em_fields.phi @@ -84,23 +81,13 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def update_rho(self): - omega = self.plasma.vorticity.spline.vector - self._rho = Propagator.mass_ops.M0.dot(omega, out=self._rho) - self._rho.update_ghost_regions() - return self._rho - def post_allocate(self): """Solve initial Poisson equation. :meta private: """ - self._rho: StencilVector = Propagator.derham.V0.zeros() - self.update_rho() - logger.info("\nINITIAL POISSON SOLVE:") - self.update_rho() self.propagators.poisson(1.0) logger.info("Done.") diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index b929e1df3..f640d9fda 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -174,10 +174,11 @@ def post_allocate(self): logger.info("\nINITIAL POISSON SOLVE:") - # use control variate method - particles = self.kinetic_ions.var.particles - particles.update_weights() - + # No control variate here: the markers of a DeltaFParticles6D species already carry + # delta_f alone (w = delta_f / s0 / Np, set by initialize_weights), so subtracting the + # background f0 a second time -- which is what update_weights does -- would leave every + # weight offset by the constant -f0 / s0 / Np. That constant is a net charge, and it makes + # the periodic Poisson problem below singular. self.initial_poisson.allocate() # Solve with dt=1. and compute electric field diff --git a/src/struphy/models/tests/test_hasegawa_wakatani.py b/src/struphy/models/tests/test_hasegawa_wakatani.py new file mode 100644 index 000000000..9c5343752 --- /dev/null +++ b/src/struphy/models/tests/test_hasegawa_wakatani.py @@ -0,0 +1,8 @@ +from struphy.models import HasegawaWakatani + + +def test_hasegawa_wakatani_couples_vorticity_to_potential(): + model = HasegawaWakatani() + + assert model.propagators.poisson.rho is model.plasma.vorticity + assert model.propagators.hw.options.coupling == 1.0 diff --git a/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py new file mode 100644 index 000000000..2f47f2172 --- /dev/null +++ b/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py @@ -0,0 +1,129 @@ +import logging +import os +import shutil + +import cunumpy as xp +from feectools.ddm.mpi import mpi as MPI + +from struphy import ( + BoundaryParameters, + DerhamOptions, + EnvironmentOptions, + LoadingParameters, + SavingParameters, + Simulation, + SortingParameters, + Time, + WeightsParameters, + domains, + grids, + maxwellians, + perturbations, +) +from struphy.models import LinearVlasovAmpereOneSpecies + +logger = logging.getLogger("struphy") + + +def test_delta_f_initial_condition(exit_before_run: bool = False): + """The delta-f markers must carry the density perturbation alone. + + A ``DeltaFParticles6D`` species solves for :math:`\\delta f = f - f_0`, so its weights + are :math:`\\delta f / (s_0 N_p)` already and no control variate may be applied to them. + Subtracting the background a second time offsets every weight by the same + :math:`-f_0 / (s_0 N_p)`, which is a net charge: the markers then carry a uniform + :math:`\\delta f` instead of the cosine, and the periodic Poisson problem solved in + ``post_allocate`` becomes singular. + + This runs zero time steps -- everything asserted here is set up during allocation -- + and checks that + + 1. the weights have no net charge, and + 2. the electric field built from them is the one the perturbation implies, + :math:`\\int E^2 / 2 \\, \\textrm{d}x = A^2 r_1 / (4 k^2)`. + """ + amplitude = 1e-3 + r1 = 12.56 # a box of length 4*pi, i.e. k = 2*pi/r1 = 0.5 + + model = LinearVlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False, with_E0=False) + + test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + out_folders = os.path.join(test_folder, "LinearVlasovAmpereOneSpecies") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="delta_f_init") + + # no time stepping: post_allocate does the initial Poisson solve this test is about + time_opts = Time(dt=0.05, Tend=0.0) + + domain = domains.Cuboid(r1=r1) + grid = grids.TensorProductGrid(num_elements=(32, 1, 1)) + derham_opts = DerhamOptions(degree=(3, 1, 1)) + + ppc = 2000 + model.kinetic_ions.set_markers( + loading_params=LoadingParameters(ppc=ppc, seed=1234), + weights_params=WeightsParameters(), + boundary_params=BoundaryParameters(), + sorting_params=SortingParameters(boxes_per_dim=(16, 1, 1), do_sort=True), + saving_params=SavingParameters(), + bufsize=0.4, + ) + + model.propagators.push_eta.options = model.propagators.push_eta.Options() + model.propagators.coupling_Eweights.options = model.propagators.coupling_Eweights.Options() + # The constant mode of the periodic Poisson problem needs a finite stabilization: left at + # the default the solver is free to wander along it, and the physical part of phi then + # drowns in the solver tolerance applied to a solution of arbitrary size. + model.initial_poisson.options = model.initial_poisson.Options(stab_mat="M0", stab_eps=1e-6) + + model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(1.0, None))) + perturbation = perturbations.ModesCos(ls=(1,), amps=(amplitude,)) + model.kinetic_ions.var.add_initial_condition(maxwellians.Maxwellian3D(n=(1.0, perturbation))) + + sim = Simulation( + model=model, + env=env, + time_opts=time_opts, + domain=domain, + grid=grid, + derham_opts=derham_opts, + ) + + if exit_before_run: + logger.info("Exiting before running simulation.") + return sim + + sim.run() + + comm = MPI.COMM_WORLD + + # 1. no net charge: the weights of a cosine perturbation cancel over the box, so their + # mean is Monte-Carlo noise, small against their own spread. The control variate applied + # by mistake made every weight equal, i.e. |mean| == max|w|. + # Checked per rank: each one holds a random subset of the markers, so its own mean is + # as good an estimator of the net charge as the global one. + weights = xp.asarray(model.kinetic_ions.var.particles.weights) + mean = float(weights.mean()) + rms = float(xp.sqrt((weights**2).mean())) + assert abs(mean) < 0.1 * rms, ( + f"Assertion for delta-f weights failed: net charge {mean =} is not small against {rms =}; " + "the markers do not carry the density perturbation alone." + ) + logger.info(f"Assertion for delta-f weights passed ({mean =}, {rms =}).") + + # 2. the field energy of the cosine mode, int E^2/2 dx with E = A/k + k = 2.0 * xp.pi / r1 + energy_exact = amplitude**2 * r1 / (4.0 * k**2) + + if comm.Get_rank() == 0: + energy = float(xp.asarray(sim.output.evaluate("en_E"))[0]) + rel_error = abs(energy - energy_exact) / energy_exact + assert rel_error < 0.05, ( + f"Assertion for the initial delta-f field energy failed: {energy =} vs. {energy_exact =}." + ) + logger.info(f"Assertion for the initial delta-f field energy passed ({rel_error =}).") + + shutil.rmtree(test_folder) + + +if __name__ == "__main__": + test_delta_f_initial_condition() diff --git a/src/struphy/propagators/hasegawa_wakatani_step.py b/src/struphy/propagators/hasegawa_wakatani_step.py index 07713f220..5e5b1bed3 100644 --- a/src/struphy/propagators/hasegawa_wakatani_step.py +++ b/src/struphy/propagators/hasegawa_wakatani_step.py @@ -113,6 +113,9 @@ class Options(OptionsBase): c_fun : {"const"}, default="const" Choice of coupling profile :math:`C(x,y)` used in the model. + coupling : float, default=1.0 + Constant value of :math:`C` when ``c_fun="const"``. + kappa : float, default=1.0 Constant multiplying the background-gradient drift term. @@ -138,6 +141,7 @@ class Options(OptionsBase): OptsCfun = Literal["const"] # propagator options c_fun: OptsCfun = "const" + coupling: float = 1.0 kappa: float = 1.0 nu: float = 0.01 butcher: ButcherTableau = None @@ -181,7 +185,7 @@ def allocate(self): # default c-function if self.options.c_fun == "const": - c_fun = lambda e1, e2, e3: 0.0 + 0.0 * e1 + c_fun = lambda e1, e2, e3: self.options.coupling + 0.0 * e1 else: raise NotImplementedError(f"{self.options.c_fun =} is not available.") From cf2db0417d2887138a7eecdbe03b4944c97457a4 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 16:12:07 +0200 Subject: [PATCH 077/193] Add newline to timestep print --- src/struphy/simulation/sim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 6c3f0898d..c5927ac9d 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -831,7 +831,7 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No # print current time and scalar quantities to screen step = str(int(self.time_state["index"][0])).zfill(len(total_steps_str)) - message = "\n" + "-" * 80 + message = "\n" + "-" * 80 + "\n" message += "time step:".ljust(25) + f"{step}/{total_steps + start_step}".rjust(25) message += ( "\n" From cc7031f786ba6929ca694679382ff33cad712531 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 20:01:09 +0200 Subject: [PATCH 078/193] Fix verification test --- .../verification/test_verif_LinearVlasovAmpereOneSpecies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py index 2f47f2172..2f16ac79f 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/verification/test_verif_LinearVlasovAmpereOneSpecies.py @@ -115,7 +115,7 @@ def test_delta_f_initial_condition(exit_before_run: bool = False): energy_exact = amplitude**2 * r1 / (4.0 * k**2) if comm.Get_rank() == 0: - energy = float(xp.asarray(sim.output.evaluate("en_E"))[0]) + energy = float(model.scalars.dct["en_E"].value[0]) rel_error = abs(energy - energy_exact) / energy_exact assert rel_error < 0.05, ( f"Assertion for the initial delta-f field energy failed: {energy =} vs. {energy_exact =}." From 16b79eca47d065f539676989661dabdddade72fd Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 21 Sep 2026 10:47:05 +0200 Subject: [PATCH 079/193] Update pitagora modules --- setup/modules.pitagora.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/setup/modules.pitagora.sh b/setup/modules.pitagora.sh index 795dca625..085db7430 100644 --- a/setup/modules.pitagora.sh +++ b/setup/modules.pitagora.sh @@ -2,6 +2,18 @@ MODULES_INTEL="intel-oneapi-compilers-classic/2021.10.0 \ intel-oneapi-mkl/2024.0.0--intel-oneapi-mpi--2021.12.1 \ python/3.11.7" + +# openmpi/4.1.6--gcc--12.3.0 MODULES_GCC="gcc/12.3.0 \ -openmpi/4.1.6--gcc--12.3.0 \ -python/3.11.7" +python/3.11.7 \ +hdf5/1.14.3--gcc--12.3.0 \ +cmake/3.27.9 \ +netcdf-fortran/4.6.1--gcc--12.3.0 \ +netlib-scalapack/2.2.0--openmpi--4.1.6--gcc--12.3.0-ucx1.20" + + +# For GVEC +# Should be fixed so it works with both gcc and intel +export FC=`which gfortran` +export CC=`which gcc` +export CXX=`which g++` From e5ec3d9b365aad4270fc4e63d57f226e567a6d0f Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Mon, 21 Sep 2026 12:09:59 +0200 Subject: [PATCH 080/193] Replace lists with plain scalar selection in mass_kernels.py --- src/struphy/feec/mass_kernels.py | 55 ++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/struphy/feec/mass_kernels.py b/src/struphy/feec/mass_kernels.py index 8093033aa..54fa90d42 100644 --- a/src/struphy/feec/mass_kernels.py +++ b/src/struphy/feec/mass_kernels.py @@ -1187,26 +1187,41 @@ def surface_kernel_3d_mat( nq1 = shape(w1)[1] nq2 = shape(w2)[1] - starts = [starts0, starts1, starts2] - pads = [pads0, pads1, pads2] - pi = [pi0, pi1, pi2] - pj = [pj0, pj1, pj2] - - surf_dirs = [d for d in range(3) if d != normal_dir] - - pi_s1 = pi[surf_dirs[0]] - pi_s2 = pi[surf_dirs[1]] - - pj_s1 = pj[surf_dirs[0]] - pj_s2 = pj[surf_dirs[1]] - - starts_n = starts[normal_dir] - starts_s1 = starts[surf_dirs[0]] - starts_s2 = starts[surf_dirs[1]] - - pads_n = pads[normal_dir] - pads_s1 = pads[surf_dirs[0]] - pads_s2 = pads[surf_dirs[1]] + # Select the normal (n) and the two tangential (s1, s2) directions with scalars only: + # Python lists would make pyccel generate gFTL containers, which we want to avoid as a dependency. + if normal_dir == 0: + pi_s1 = pi1 + pi_s2 = pi2 + pj_s1 = pj1 + pj_s2 = pj2 + starts_n = starts0 + starts_s1 = starts1 + starts_s2 = starts2 + pads_n = pads0 + pads_s1 = pads1 + pads_s2 = pads2 + elif normal_dir == 1: + pi_s1 = pi0 + pi_s2 = pi2 + pj_s1 = pj0 + pj_s2 = pj2 + starts_n = starts1 + starts_s1 = starts0 + starts_s2 = starts2 + pads_n = pads1 + pads_s1 = pads0 + pads_s2 = pads2 + else: + pi_s1 = pi0 + pi_s2 = pi1 + pj_s1 = pj0 + pj_s2 = pj1 + starts_n = starts2 + starts_s1 = starts0 + starts_s2 = starts1 + pads_n = pads2 + pads_s1 = pads0 + pads_s2 = pads1 i_local_n = boundary_index - starts_n From 17b934c1dd2979af2948d4353203ef63df9dfb0e Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Wed, 23 Sep 2026 10:50:23 +0200 Subject: [PATCH 081/193] formatting --- src/struphy/feec/tests/test_mass_reassembly.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/struphy/feec/tests/test_mass_reassembly.py b/src/struphy/feec/tests/test_mass_reassembly.py index 49d862443..e026e9cc2 100644 --- a/src/struphy/feec/tests/test_mass_reassembly.py +++ b/src/struphy/feec/tests/test_mass_reassembly.py @@ -22,11 +22,13 @@ def test_density_weighted_mass_reassembly(): weighted = masses.WMMnew rho = weighted.spline_functions["l2_field"] rho.vector = derham.projectors["3"](lambda e1, e2, e3: 2.0 + 0 * e1) - probe = derham.projectors["v"]([ - lambda e1, e2, e3: 1.0 + 0 * e1, - lambda e1, e2, e3: 2.0 + 0 * e1, - lambda e1, e2, e3: 3.0 + 0 * e1, - ]) + probe = derham.projectors["v"]( + [ + lambda e1, e2, e3: 1.0 + 0 * e1, + lambda e1, e2, e3: 2.0 + 0 * e1, + lambda e1, e2, e3: 3.0 + 0 * e1, + ] + ) reference = masses.Mv.dot(probe).toarray() * (2.0 / 6.0) weighted.assemble() np.testing.assert_allclose(weighted.dot(probe).toarray(), reference, rtol=1e-12, atol=1e-12) From d14b9cb025654153eb1d5798d3fed25041c8187c Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Wed, 23 Sep 2026 10:55:02 +0200 Subject: [PATCH 082/193] Bump scope-profiler version to 0.7.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ebb378469..b83b22640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ "pytest-testmon<=2.2.0", "ruff==0.15.0, <=0.16.0", "line_profiler<=5.0.2", - "scope-profiler>=0.5.0, <=0.7.0", + "scope-profiler>=0.5.0, <=0.7.2", ] [project.license] From 9b736d07c67f5695d0e3434b8734c52494c71bf9 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 22 Sep 2026 15:09:15 +0200 Subject: [PATCH 083/193] Improve show distribution function (#390) Some new features for quickly plotting the kinetic initial condition. --- src/struphy/kinetic_background/base.py | 52 ++++++--- src/struphy/models/base.py | 18 +-- src/struphy/pic/base.py | 108 ++++++++++++++---- src/struphy/pic/particles.py | 10 ++ .../pic/tests/test_show_distribution.py | 76 ++++++++++++ 5 files changed, 218 insertions(+), 46 deletions(-) create mode 100644 src/struphy/pic/tests/test_show_distribution.py diff --git a/src/struphy/kinetic_background/base.py b/src/struphy/kinetic_background/base.py index 21acc2e92..e9f24f85e 100644 --- a/src/struphy/kinetic_background/base.py +++ b/src/struphy/kinetic_background/base.py @@ -3,10 +3,11 @@ import copy import logging from abc import ABCMeta, abstractmethod -from typing import Callable +from typing import Callable, get_args import cunumpy as xp import matplotlib.pyplot as plt +import numpy as np from matplotlib import cm from matplotlib.colors import Normalize @@ -170,10 +171,10 @@ def __repr_no_defaults__(self): def reduced_eval( self, - dim_1: LiteralOptions.KineticDimensionsToPlot = "e1", - dim_2: LiteralOptions.KineticDimensionsToPlot | None = None, + dim_1: LiteralOptions.KineticDimensionsToPlot | int = "e1", + dim_2: LiteralOptions.KineticDimensionsToPlot | int | None = None, v_lim: float | tuple[float] = 5.0, - resol: int | tuple[int] = 100, + resol: int | tuple[int] | np.ndarray | tuple[np.ndarray] = 100, integrate_resol: tuple[int | float] | None = None, max_points: int = 1e8, domain: Domain | None = None, @@ -186,7 +187,7 @@ def reduced_eval( Parameters ---------- - dim_1, dim_2 : LiteralOptions.KineticDimensionsToPlot = ["e1","e2","e3","v1","v2","v3"] + dim_1, dim_2 : LiteralOptions.KineticDimensionsToPlot | int The axis (or axes) along which the reduced distribution is evaluated (i.e. the axes that are not integrated out). They refere to logical phase space axes. If dim_2 is not defined the reduced distribution is 1D, otherwise it is 2D. @@ -199,9 +200,9 @@ def reduced_eval( For a Cartesian velocity coordinate (and v_parallel), the limits are [-v_lim, v_lim]. For a positive velocity coordinate (such as mu or v_perp), the limits are [0, v_lim]. - resol : int | tuple[int] + resol : int | tuple[int] | np.ndarray | tuple[np.ndarray] Resolution of the evaluation grid along the plotted axis (axes). If a single integer is provided, - it is used for both dim_1 and dim_2. + it is used for both dim_1 and dim_2. If a numpy array is provided, it is used as the evaluation points along the corresponding axis (or axes). integrate_resol : tuple[int | float] | None Number of quadrature points for integration along each phase space axis. @@ -232,6 +233,10 @@ def reduced_eval( Dictionary with keys "x", "y", "z" holding the domain-mapped position arrays (broadcast to the shape of ``reduced_density``), or None if no domain was given. """ + if isinstance(dim_1, int): + dim_1 = get_args(LiteralOptions.KineticDimensionsToPlot)[dim_1] + if isinstance(dim_2, int): + dim_2 = get_args(LiteralOptions.KineticDimensionsToPlot)[dim_2] if domain is not None: assert dim_1 in ["e1", "e2", "e3"] and dim_2 in ["e1", "e2", "e3"], ( @@ -241,7 +246,8 @@ def reduced_eval( n_axes_plot = 1 + (dim_2 is not None) n_v_to_plot = ("v" in dim_1) + ("v" in dim_2 if dim_2 is not None else 0) - if isinstance(v_lim, float): + if isinstance(v_lim, (int, float)): + v_lim = float(v_lim) v_lim = (v_lim, v_lim) else: assert isinstance(v_lim, tuple) @@ -250,10 +256,20 @@ def reduced_eval( if isinstance(resol, int): resol = (resol,) * n_axes_plot + n_eval_pts = resol + elif isinstance(resol, np.ndarray): + resol = (resol,) + n_eval_pts = (resol[0].size,) + elif isinstance(resol, tuple): + assert len(resol) == n_axes_plot, ( + f"resol must have length {n_axes_plot} for this evaluation (dim_1, dim_2)." + ) + n_eval_pts = tuple(r.size if isinstance(r, np.ndarray) else r for r in resol) n_axes_integration = 3 + self.vdim - n_axes_plot max_quad_points = max_points - for r in resol: + + for r in n_eval_pts: max_quad_points //= r # phase space grid, first add plotting points for the axes that are plotted @@ -362,7 +378,7 @@ def _get_plot_pts( self, dim: LiteralOptions.KineticDimensionsToPlot, v_lim: float, - resol: int, + resol: int | np.ndarray, ): """Resolve a single dimension key to its phase-space axis index and its array of evaluation points. @@ -376,8 +392,9 @@ def _get_plot_pts( span [0, 1]). For a Cartesian velocity coordinate (and v_parallel) the range is [-v_lim, v_lim]; for a positive velocity coordinate (such as mu or v_perp) the range is [0, v_lim]. - resol : int - Number of evaluation points along the axis. + resol : int | np.ndarray + Resolution of the evaluation grid along the axis. If an integer is provided, it is used to generate a linearly spaced array of that many points spanning the axis range. + If a numpy array is provided, it is used directly as the evaluation points along the axis. Returns ------- @@ -416,10 +433,15 @@ def _get_plot_pts( v_left = 0.0 v_right = v_lim - if axe_to_plot < 3: - plot_pts = xp.linspace(0.0, 1.0, resol) + if isinstance(resol, int): + if axe_to_plot < 3: + plot_pts = xp.linspace(0.0, 1.0, resol) + else: + plot_pts = xp.linspace(v_left, v_right, resol) + elif isinstance(resol, np.ndarray): + plot_pts = resol else: - plot_pts = xp.linspace(v_left, v_right, resol) + raise AssertionError("resol argument must be an int or a numpy array") return axe_to_plot, plot_pts diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 643efca1b..5a7ffe976 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -745,16 +745,16 @@ def generate_default_parameter_file( init_pert_pic += "maxwellian_1pt = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n" init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" + elif "5Dvperp" in var.space: + init_bckgr_pic += "maxwellian_1 = maxwellians.GyroMaxwellian2Dvperp(n=(1.0, None))\n" + init_bckgr_pic += "maxwellian_2 = maxwellians.GyroMaxwellian2Dvperp(n=(0.1, None))\n" + init_pert_pic += "maxwellian_1pt = maxwellians.GyroMaxwellian2Dvperp(n=(1.0, perturbation))\n" + init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" + init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" elif "5D" in var.space: - init_bckgr_pic += ( - "maxwellian_1 = maxwellians.GyroMaxwellian2Dvperp(n=(1.0, None), equil=equil)\n" - ) - init_bckgr_pic += ( - "maxwellian_2 = maxwellians.GyroMaxwellian2Dvperp(n=(0.1, None), equil=equil)\n" - ) - init_pert_pic += ( - "maxwellian_1pt = maxwellians.GyroMaxwellian2Dvperp(n=(1.0, perturbation), equil=equil)\n" - ) + init_bckgr_pic += "maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None))\n" + init_bckgr_pic += "maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None))\n" + init_pert_pic += "maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation))\n" init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" if "3D" in var.space: diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index fb219f7b5..fe21ac046 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -1,4 +1,3 @@ -import copy import logging import os import warnings @@ -16,6 +15,7 @@ class Intracomm: import cunumpy as xp +import numpy as np from cunumpy import PyccelKernel from feectools.ddm.mpi import MockComm from feectools.ddm.mpi import mpi as MPI @@ -421,6 +421,13 @@ def vdim(self): """Dimension of the velocity space.""" pass + @property + @abstractmethod + def coordinate_labels(self) -> tuple[str]: + """Labels for the coordinates in the phase space. + Length must be 3 + vdim, where the first 3 are the spatial coordinates and the last vdim are the velocity coordinates.""" + pass + @property @abstractmethod def mu_idx(self): @@ -1581,7 +1588,7 @@ def binning( return f_slice, df_slice - def show_distribution_function(self, components, bin_edges): + def show_distribution_function(self, components: list[bool], bin_edges: list[np.ndarray], do_plot=False): """ 1D and 2D plots of slices of the distribution function via marker binning. This routine is mainly for de-bugging. @@ -1589,15 +1596,26 @@ def show_distribution_function(self, components, bin_edges): Parameters ---------- components : list[bool] - List of length 6 giving the directions in phase space in which to bin. + List of length 3+vdim giving the directions in phase space in which to bin. + Up to two entries can be True, the rest must be False. The True entries correspond to the axes of the binning. - bin_edges : list[array] + bin_edges : list[np.ndarray] List of bin edges (resolution) having the length of True entries in components. + + do_plot : bool + Whether to show the plot (default: False). + + Returns + ------- + err : float + Maximum relative error between the binned distribution function and the analytic initial condition. """ import matplotlib.pyplot as plt - n_dim = xp.count_nonzero(components) + assert len(components) == 3 + self.vdim, f"components must be of length {3 + self.vdim}, is {len(components)}." + + n_dim = np.count_nonzero(components) assert n_dim == 1 or n_dim == 2, f"Distribution function can only be shown in 1D or 2D slices, not {n_dim}." @@ -1605,27 +1623,73 @@ def show_distribution_function(self, components, bin_edges): bin_centers = [bi[:-1] + (bi[1] - bi[0]) / 2 for bi in bin_edges] - labels = { - 0: r"$\eta_1$", - 1: r"$\eta_2$", - 2: r"$\eta_3$", - 3: "$v_1$", - 4: "$v_2$", - 5: "$v_3$", - } - indices = xp.nonzero(components)[0] + indices = np.nonzero(components)[0] if n_dim == 1: - plt.plot(bin_centers[0], f_slice) - plt.xlabel(labels[indices[0]]) + plt.plot(bin_centers[0], f_slice, linewidth=2, label="binned f") + i = int(indices[0]) + resol = bin_centers[0] + integrate_resol = [0.5, 0.5, 0.5] + [32] * self.vdim + integrate_resol[i] = None + if i < 3: + v_lim = 5 + else: + v_lim = bin_edges[0][-1] + f_init, pts, _, _ = self.f_init.reduced_eval( + dim_1=i, v_lim=v_lim, resol=resol, integrate_resol=integrate_resol + ) + + if do_plot: + plt.plot(pts, f_init, "r--", label="analytic initial condition") + plt.xlabel(self.coordinate_labels[i]) + plt.ylabel("f") + plt.legend() else: - plt.contourf(bin_centers[0], bin_centers[1], df_slice.T, levels=20) - plt.colorbar() - # plt.axis('square') - plt.xlabel(labels[indices[0]]) - plt.ylabel(labels[indices[1]]) + i = int(indices[0]) + j = int(indices[1]) + + if do_plot: + plt.subplot(1, 2, 1) + plt.contourf(bin_centers[0], bin_centers[1], f_slice.T, levels=20) + plt.colorbar() + plt.xlabel(self.coordinate_labels[i]) + plt.ylabel(self.coordinate_labels[j]) + plt.title("Binned f") + + resol = tuple(bin_centers) + integrate_resol = [0.5, 0.5, 0.5] + [100] * self.vdim + integrate_resol[i] = None + integrate_resol[j] = None + + v_lim = [5, 5] + if i > 2: + v_lim[0] = bin_edges[0][-1] + if j > 2: + v_lim[1] = bin_edges[1][-1] + v_lim = tuple(v_lim) + + f_init, pts1, pts2, _ = self.f_init.reduced_eval( + dim_1=i, + dim_2=j, + v_lim=v_lim, + resol=resol, + integrate_resol=integrate_resol, + ) + + if do_plot: + plt.subplot(1, 2, 2) + plt.contourf(pts1, pts2, f_init.T, levels=20) + plt.colorbar() + plt.xlabel(self.coordinate_labels[i]) + plt.ylabel(self.coordinate_labels[j]) + plt.title("Analytic initial condition") + + if do_plot: + plt.show() + + err = np.max(np.abs(f_init - f_slice)) / np.max(f_init) - plt.show() + return err @profile @ProfileManager.profile("mpi_sort_markers") diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 19d5965a7..86e37ab78 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -29,6 +29,8 @@ class Particles6D(Particles): # Class properties vdim = 3 """Dimension of the (Cartesian) velocity space, here 3.""" + coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_x$", "$v_y$", "$v_z$") + """Labels for the coordinates in the phase space. Length is 6, with the first 3 being the spatial coordinates and the last 3 being the velocity coordinates.""" default_background = maxwellians.Maxwellian3D() """Default kinetic background is a 3D Cartesian Maxwellian.""" default_n_cols = {"diagnostics": 0, "aux": 5} @@ -258,6 +260,8 @@ class Particles5D(Particles): # Class properties vdim = 2 """Dimension of the velocity space, here 2 (:math:`v_\\parallel, \\mu`).""" + coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_\\parallel$", "$\\mu$") + """Labels for the coordinates in the phase space. Length is 5, with the first 3 being the spatial coordinates and the last 2 being the velocity coordinates.""" mu_idx = 4 """Column index of particle magnetic moment.""" default_background = maxwellians.GyroMaxwellian2D() @@ -514,6 +518,8 @@ class Particles5Dvperp(Particles): # Class properties vdim = 2 """Dimension of the velocity space, here 2 (:math:`v_\\parallel, v_\\perp`).""" + coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_\\parallel$", "$v_\\perp$") + """Labels for the coordinates in the phase space. Length is 5, with the first 3 being the spatial coordinates and the last 2 being the velocity coordinates.""" default_background = maxwellians.GyroMaxwellian2Dvperp() """Default kinetic background is a gyrotropic Maxwellian in :math:`(v_\\parallel, v_\\perp)`.""" default_n_cols = {"diagnostics": 3, "aux": 12} @@ -805,6 +811,8 @@ class Particles3D(Particles): # Class properties vdim = 0 """Dimension of the velocity space, here 0 (no velocity coordinates).""" + coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$") + """Labels for the coordinates in the phase space. Length is 3, with all being spatial coordinates.""" default_background = maxwellians.ColdPlasma() """Default kinetic background is a cold-plasma (velocity-independent) density.""" default_n_cols = {"diagnostics": 0, "aux": 5} @@ -880,6 +888,8 @@ class ParticlesSPH(Particles): # Class properties vdim = 3 """Dimension of the per-marker Cartesian velocity attribute, here 3 (not a sampled coordinate, see class docstring).""" + coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_x$", "$v_y$", "$v_z$") + """Labels for the coordinates in the phase space. Length is 6, with the first 3 being the spatial coordinates and the last 3 being the velocity coordinates.""" default_background = equils.ConstantVelocity() """Default fluid background is a spatially constant velocity field.""" default_n_cols = {"diagnostics": 0, "aux": 24} diff --git a/src/struphy/pic/tests/test_show_distribution.py b/src/struphy/pic/tests/test_show_distribution.py new file mode 100644 index 000000000..40a3d9e76 --- /dev/null +++ b/src/struphy/pic/tests/test_show_distribution.py @@ -0,0 +1,76 @@ +import logging + +import pytest + +from struphy import set_logging_level + +set_logging_level(logging.WARNING) + +logger = logging.getLogger("struphy") + + +@pytest.mark.mpi_skip +def test_gyro_maxwellian_2d(do_plot=False): + + from struphy import LoadingParameters, Simulation, equils, maxwellians + from struphy.models import LinearMHDDriftkineticCC + + model = LinearMHDDriftkineticCC() + equil = equils.HomogenSlab() + + sim = Simulation( + model=model, + equil=equil, + ) + + loading_params = LoadingParameters(Np=100000, seed=3928) + model.energetic_ions.set_markers(loading_params=loading_params) + + # Background for kinetic species + maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None)) + # maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None)) + # background = maxwellian_1 + maxwellian_2 + model.energetic_ions.var.add_background(maxwellian_1) + + sim.allocate() + + import numpy as np + + for i in range(5): + components = [False] * 5 + components[i] = True + if i < 3: + bin_edges = (np.linspace(0, 1, 32),) + elif i == 3: + bin_edges = (np.linspace(-5, 5, 32),) + else: + bin_edges = (np.linspace(0, 5, 32),) + err = model.energetic_ions.var.particles.show_distribution_function(components, bin_edges, do_plot=do_plot) + print(f"1d {components = }, {err = }") + assert err < 0.05 + + components = [False] * 5 + components[3] = True + components[4] = True + bin_edges = (np.linspace(-5, 5, 32), np.linspace(0, 2, 32)) + err = model.energetic_ions.var.particles.show_distribution_function(components, bin_edges, do_plot=do_plot) + print(f"2d {components = }, {err = }") + assert err < 0.05 + + components = [False] * 5 + components[0] = True + components[1] = True + bin_edges = (np.linspace(0, 1, 32), np.linspace(0, 1, 32)) + err = model.energetic_ions.var.particles.show_distribution_function(components, bin_edges, do_plot=do_plot) + print(f"2d {components = }, {err = }") + assert err < 0.31 + + # Perturbations for (some) kinetic species + # perturbation = perturbations.TorusModesCos() + # maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil) + # init = maxwellian_1pt + maxwellian_2 + # model.energetic_ions.var.add_initial_condition(init) + + +if __name__ == "__main__": + test_gyro_maxwellian_2d(do_plot=True) From 8267caedc1680b41cc4dbbd8567583b574a07a48 Mon Sep 17 00:00:00 2001 From: Byung Kyu Na Date: Wed, 23 Sep 2026 07:03:45 +0100 Subject: [PATCH 084/193] Fix B0 handling in GyroMaxwellian2D class for callable cases (#394) **Solves the following issue(s):** Closes #392 **Core changes:** Fix `GyroMaxwellian2D.velocity_jacobian_det` as ``` assert eta1.ndim == eta2.ndim == eta3.ndim == 1 assert v_para.ndim == mu.ndim == 1 B0 = self.params["B0"] if callable(B0): etas = xp.stack((eta1, eta2, eta3), axis=1) return B0(etas) return B0 + 0 * eta1 ``` Then it returns an array with the size Np. --- src/struphy/kinetic_background/maxwellians.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/struphy/kinetic_background/maxwellians.py b/src/struphy/kinetic_background/maxwellians.py index f73d39a89..708d3e613 100644 --- a/src/struphy/kinetic_background/maxwellians.py +++ b/src/struphy/kinetic_background/maxwellians.py @@ -214,7 +214,11 @@ def velocity_jacobian_det(self, eta1, eta2, eta3, v_para, mu): B0 = self.params["B0"] - return B0(eta1, eta2, eta3) if callable(B0) else B0 + 0 * eta1 + if callable(B0): + etas = xp.stack((eta1, eta2, eta3), axis=1) + return B0(etas) + + return B0 + 0 * eta1 @property def volume_form(self) -> bool: From 5e322610b50345c83c0da2ab3d3ab3c5ed898468 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Wed, 23 Sep 2026 11:16:50 +0200 Subject: [PATCH 085/193] Added safe yaml representers for numpy scalars and arrays --- src/struphy/utils/utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/struphy/utils/utils.py b/src/struphy/utils/utils.py index 1d7757043..946c6c002 100644 --- a/src/struphy/utils/utils.py +++ b/src/struphy/utils/utils.py @@ -9,6 +9,7 @@ import tempfile from typing import Literal, get_args +import numpy as np import yaml from feectools.ddm.mpi import mpi as MPI @@ -133,6 +134,20 @@ def ignore_aliases(self, data): return True +def _represent_numpy_scalar(dumper, data): + """Represent NumPy scalar values using their native Python value.""" + return dumper.represent_data(data.item()) + + +def _represent_numpy_array(dumper, data): + """Represent NumPy arrays as regular YAML sequences.""" + return dumper.represent_data(data.tolist()) + + +MyDumper.add_multi_representer(np.generic, _represent_numpy_scalar) +MyDumper.add_multi_representer(np.ndarray, _represent_numpy_array) + + def subp_run(cmd, cwd="libpath", check=True): """Call subprocess.run and print run command.""" from struphy.utils.utils import STRUPHY_LIBPATH From ce7383203023d5bab004edb92227e9079775996a Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 23 Sep 2026 11:28:55 +0200 Subject: [PATCH 086/193] remove notebook output; split some cells for better readability. --- tutorials/tutorial_post_processing.ipynb | 11969 +-------------------- 1 file changed, 162 insertions(+), 11807 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index df16f2a9b..40dd83de7 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "1", "metadata": {}, "outputs": [], @@ -58,21 +58,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "3", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" - ] - } - ], + "outputs": [], "source": [ "def build_model():\n", " model = VlasovAmpereOneSpecies(alpha=1.0, epsilon=-1.0, with_B0=False)\n", @@ -110,26 +99,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "4", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 100/100 [00:22<00:00, 4.40step/s]\n", - "Raw output: /var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo\n" - ] - } - ], + "outputs": [], "source": [ "demo_tmp = tempfile.TemporaryDirectory(prefix=\"struphy_postprocessing_\")\n", "demo_root = demo_tmp.name\n", @@ -168,83 +141,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "6", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", - "\n", - "Post-processing path /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo\n", - "\n", - "Reading hdf5 data of following species:\n", - "em_fields:\n", - " e_field: \n", - " phi: \n", - "Creation of Struphy Fields done.\n", - "\n", - "Evaluating fields ...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "100%|██████████| 101/101 [00:00<00:00, 146.08it/s]\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of 12 marker orbits for kinetic_ions\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "100%|██████████| 101/101 [00:00<00:00, 1306.96it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Evaluation of distribution functions for kinetic_ions\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "0 starting post-processing of distribution functions for /private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo/post_processing/kinetic_data/kinetic_ions ...\n", - "100%|██████████| 1/1 [00:00<00:00, 1620.05it/s]\n", - " 0%| | 0/1 [00:00 Size: 827kB\n", - "[103424 values with dtype=float64]\n", - "Coordinates:\n", - " * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.8 4.85 4.9 4.95 5.0\n", - " t_seconds (t) float64 808B 0.0 1.668e-10 3.336e-10 ... 1.651e-08 1.668e-08\n", - " * e1 (e1) float64 256B 0.01562 0.04688 0.07812 ... 0.9531 0.9844\n", - " * v1 (v1) float64 256B -4.844 -4.531 -4.219 ... 4.219 4.531 4.844\n", - "Attributes:\n", - " label: $f$\n", - " long_name: $f$\n", - " run: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)\n", - " run_name: vlasov_ampere_demo\n" - ] - } - ], + "outputs": [], + "source": [ + "print(out.info())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "print(out.kinetic_ions)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], "source": [ - "print(out.info())\n", - "print(out.kinetic_ions)\n", - "\n", "phase_space = out.kinetic_ions.e1_v1_density.f\n", "print(phase_space)" ] }, { "cell_type": "markdown", - "id": "8a", + "id": "11", "metadata": {}, "source": [ "### Inspecting `out` itself\n", @@ -326,120 +200,47 @@ }, { "cell_type": "code", - "execution_count": 6, - "id": "8b", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"vars(out):\", vars(out)) # only private cache slots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"dir(out):\", [name for name in dir(out) if not name.startswith(\"_\")])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "vars(out): {'path_out': PosixPath('/private/var/folders/41/knyv28q16gv84h05plqzft_00000gn/T/struphy_postprocessing_3aawuvfp/vlasov_ampere_demo'), 'time_units': 'normalized', 'comm': , '_time': None, '_grids_log': None, '_grids_phy': None, '_scalars': Size: 4kB\n", - "Dimensions: (t: 101)\n", - "Coordinates:\n", - " * t (t) float64 808B 0.0 0.05 0.1 0.15 ... 4.85 4.9 4.95 5.0\n", - " t_seconds (t) float64 808B 0.0 1.668e-10 ... 1.651e-08 1.668e-08\n", - "Data variables:\n", - " electric_energy (t) float64 808B 1.662e-06 1.651e-06 ... 1.488e-07\n", - " kinetic_energy (t) float64 808B 0.01887 0.01887 ... 0.01887 0.01887\n", - " total_energy (t) float64 808B 0.01887 0.01887 ... 0.01887 0.01887, '_products': {'fields': , 'distributions': , 'densities': , 'orbits': }, '_label': 'dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)', '_tree': \n", - "Group: /\n", - "│ Attributes:\n", - "│ schema_version: 1\n", - "│ options: {\"step\": 1, \"celldivide\": [1, 1, 1], \"physical\": true, \"...\n", - "├── Group: /em_fields\n", - "│ Dimensions: (t: 101, component: 3, e1: 17, e2: 2, e3: 2)\n", - "│ Coordinates:\n", - "│ * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.85 4.9 4.95 5.0\n", - "│ * component (component) int64 24B 0 1 2\n", - "│ * e1 (e1) float64 136B 0.0 0.0625 0.125 0.1875 ... 0.875 0.9375 1.0\n", - "│ * e2 (e2) float64 16B 0.0 1.0\n", - "│ * e3 (e3) float64 16B 0.0 1.0\n", - "│ X (e1, e2, e3) float64 544B ...\n", - "│ Y (e1, e2, e3) float64 544B ...\n", - "│ Z (e1, e2, e3) float64 544B ...\n", - "│ Data variables:\n", - "│ e_field (t, component, e1, e2, e3) float64 165kB ...\n", - "│ e_field_xyz (t, component, e1, e2, e3) float64 165kB ...\n", - "│ phi (t, e1, e2, e3) float64 55kB ...\n", - "│ phi_xyz (t, e1, e2, e3) float64 55kB ...\n", - "└── Group: /kinetic_ions\n", - " │ Dimensions: (t: 101, marker: 12, quantity: 8)\n", - " │ Coordinates:\n", - " │ * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.8 4.85 4.9 4.95 5.0\n", - " │ * marker (marker) int64 96B 0 1 2 3 4 5 6 7 8 9 10 11\n", - " │ * quantity (quantity) " - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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65csWSLNmzfPtzLRQMNeAAB8ZtCgQTJ16lRZv369eayNdr/55huzPuj555+X7t272yoTbb7KxAAAAJFbbrlFfv75Z2nSpInUqFFDfv/9dxk7dqycf/75odOjTS+WLVtmq0y0+SqI0bpBbeSUXR0hOH9ce7GJ9y3nj2sv/xUsWFBeeeUVeeihh+TPP/80471ou5f0brrpJrn00kttlYk2Xw12BwAA4gdtYgAAgCcRxAAAAE8iiAEAAJ4UV0HM5s2b5cYbb5SOHTvKZZddJosWLYpImXikE3k9/fTTct5550nXrl1l4sSJkpaWdsIp2fXcderUyUwANmfOHPGrr7/+Wvr162euo3/961/y119/5bnslClTpHXr1vLUU0+JH/39998ybNgwc+4uueSSPM/D8vHHH0v//v2lc+fOZvK5/BgszGu0SePLL78sF1xwgXTp0kXGjx9/wtFTd+zYIffff7+Z80bf77fffrvpheJHK1askCFDhpj335dffpmnMvq5p593HTp0MGV1oDq4J26CmN27d5spwTUoueuuu8zIsvqhmFtQ4qRMvNJW6I8++qhcc801MnDgQBkzZowMHz48x+3ff/99U6Zx48by73//W5o2bSo9e/aU5557TvxGP/w0kKtbt67ceeed8uuvv8pZZ50lKSkpJyy7bt06M6W9XoO//fab+M2hQ4fk7LPPlpUrV8odd9xhriftrjl7du5DsOtNWG8kbdu2lfvuu8/cuO+55x7xm3vvvVfuvvtuE0APHjxYnn32WfMezklqaqqcc8458tlnn8nQoUNNAKNdZvVzcM+ePeInEyZMMIO2NWjQwAzYFpyWJjfLly+X9u3bS4UKFcz7Vsvoudu2bVtU9hnZsOLEAw88YFWsWNE6cuRIaF337t2tzp0752uZeLRhwwYrEAhYH330UWjd1KlTrYIFC1pbtmzJtkxKSkqWdXfeeadVq1Yty2/atm1r9evXL/T4wIEDVunSpa3x48fnWu7w4cNW06ZNrXfffdf8vPXWWy2/ef75563ixYtbe/fuDa276qqrrGbNmuVYZtmyZeZ6nTlzZob1hw4dsvxk586dVuHCha3XX389tG7u3Lna29RavXp1tmXWrVtnfr9w4cLQuuTkZLPus88+s/xkz549oetGj3/69OknLHPxxRdbnTp1Cj0+duyYVa1aNWvEiBER3Vfk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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" ] }, { "cell_type": "markdown", - "id": "11", + "id": "18", "metadata": {}, "source": [ "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." @@ -488,7 +268,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "19", "metadata": {}, "source": [ "## Scalar overview and time series\n", @@ -500,50 +280,20 @@ }, { "cell_type": "code", - "execution_count": 8, - "id": "13", + "execution_count": null, + "id": "20", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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X+92snXr1iDPDkkhLAIAgFSndxwJJMwFuj5SD5NyAwCAVKWhT+8+ovP66WTTxYsXN/eFVj///LO5K8nZs2fN3Uv69OljXV/vJ63VxoiICClVqpS5k8ktt9wS0LnonVEmTpwohw4dkqZNm8oTTzwhkZGRMnv2bDMXYcGCBeWHH34wt9174YUXzHH83VYn9tYJtfv27Su33XabeQ+LFi2ScuXKSatWrWT+/Pny7rvvyoYNG+Sll16Szz//3L3vVatWyfjx483dZMINlUUAADIol8sl5+LPpfpDj5OU6OhoczcSvafxiy++KIMGDTLLNfx17NhRbrzxRunWrZuMGTNGRo8ebV3fea4hTm+Jp9v88ccffl+P9evXm/BZvnx56d+/vyxevFgGDhzovi+zBtHt27dLv3795NSpU9KrV6+AttUwOWDAAKlTp455L++8847ce++9Jiw+8MADsnbtWvf7+O2332TlypXu/b///vtewTScUFkEACCDOv/veWkws0GqH2dlz5USlS3K+rreHaRChQpy/PhxE7gcb775pgwZMsTcwk4VK1bM3Iv5mWeeSXT9a665xjyULtd7KX/88cdSv359v85Tq3mPP/64PPXUU+Z5s2bNTNXyjTfeMM9r164t48aNc/+9TJkycuXKFcmSJYvPbStVqmSqjg6tEmrFsWXLlua53udZw7HKmjWr9O7dWz766CNp0KCBnDt3TubMmSPLli2TcERYBAAAIbFr1y53dU5p6NOm59jY2ETX11Cl1boVK1aYyt+RI0dMSPPX5s2bZefOnfLLL7+Yaqg+NAzu3bvXvK4B1VGoUCGJj483x8yTJ4/PbatWrere9syZM3Ls2DGpVauWe5mGTycsKq00NmrUSN566y0zmEcrltWrV5dwRFgEACCDyhWZy1T90uI4vmg/w4Sc+xI7NADqelqJTGx97SOo4ew///mP6VuofQA1xPlLj6f9IrW/oafrrrvOeo7+bqt9Fx1RUVHmuQZfPU/l+T6VhtzKlSvLV199JdOmTfNq8g439FkEACCD0vCjzcOp/UgqZDk0AGqzsScd9KF99S5duuRuutVqm4atxNbXPoHafNu5c2dp3LixbNy4MaDroU3c2ldQm361GVuDn4bNvHnzpui2WbNmNX0qJ02aZJ5fvHjRBMKEdDDPyy+/bJqfe/bsKeGKsAgAAFKdBsPV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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.plot.scalars()" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "14", + "execution_count": null, + "id": "21", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "growth rate of the energy: -1.3534170635343759\n", - "growth rate of the amplitude: -0.6767085317671879\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "t_fit = 2.0 # Struphy time units, like every time coordinate of this run\n", "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", @@ -561,7 +311,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "22", "metadata": {}, "source": [ "## Two-dimensional data\n", @@ -571,21 +321,10 @@ }, { "cell_type": "code", - "execution_count": 10, - "id": "16", + "execution_count": null, + "id": "23", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzwAAAIgCAYAAABAlWy6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAAVeBJREFUeJzt3Qd8FNX2wPETejOUAEoTMDQViNKkKU2wgGCj2ABFBCvN3hHl/54FfFhRVBQUFaygYAFpAqKIQUWKgEiTEumdZP6fc327L5u6m9zNTCa/L5/5hJ3s3J22mzl77j0T4ziOIwAAAADgQ4XcXgEAAAAAiBYCHgAAAAC+RcADAAAAwLcIeAAAAAD4FgEPAAAAAN8i4AEAAADgWwQ8AAAAAHyLgAcAAACAbxHwAAAAAPAtAh4AOTZnzhyZNm1agdyDW7dulccff9zt1YBlK1askJdffjlXbUyZMkVWrlwpfvDDDz/IJ5984vZq5Kt96pV9BuB/CHgA5BgBj/cCHgIxdwOeDRs2yF133SU1a9YUP9DtGDx4sOzatcu1dchv+9QL+wxAqCJpHgNAjmzevFneeustuf/++3MdRC1dulQqV64sV1xxhZQtWzZHz09JSZHHHnss3fMffvhhKVQof3/X8/fff8u4cePkjjvukAoVKuS4nUWLFsmXX36Z6e9btmwpF154Ydjt/fTTT+ZY3HTTTVnOi4Y1a9bIO++8I61atZILLrgg5PXnzp0rQ4cOlbwwZswYufrqq6V06dLmsV70Pv/888Hfd+vWTZo1a5ZuuT///FM+++wzOXHihFx++eVSrVq1sF4v3PZzunylSpXM/nzhhRfkkUceETek3adqx44dMnPmTElKSpKEhATp1KlT2O0VhH0GIFT+/qsPwFMBz+jRo3PVxp133ilXXXWVbNmyRSZOnChnnXWWbN++PUfP14Bn5MiRvvyWVQMe3Tb9mVbVqlXlwQcfjLjNAwcOmDb/+uuvHK+XBhevvPJKtvOiFfDo+l977bWyd+/ekNd/9tlnJS9osPL2229Lr169Mvy9rod2d0pLlznzzDNl/vz5JvDp2rWrrFq1KuLXz6z93C5/5ZVXmveXGzLap5MnT5bWrVvLN998I5s2bZLrrrtOevToYd7zkfLjPgOQHhkeAGE7cuSIfPjhh7Jz505p27ZtcP6hQ4dkwoQJcuzYMXn00UfNPP2WunHjxmG3vXr1avNN7uLFi+Wcc86R5ORkc1Hzf//3fxlesIb7/Ntuu00aNGgQ8VHes2dPsB39Zlm/Re7SpUuWyxw+fFg++ugjs390Xfbv32+CEr34UbqO+i2+Xpxrt5dLLrlESpQoIba7tL3++uvpgh7NcixfvtxkhPR19aeuo05KA51nnnnGdMXRwHHfvn1m/+q+/frrr806a7civQD99NNPTYB7+umny0UXXWQyZnqh/vHHH5vXD5wDeuGedp5mejQo032h39DrhX2NGjXMBWtgX/z4449m0uzSV199ZeZdf/312W67Zvf0WGvg/e9//zvT52X12rmh66zvEd1/ARUrVgxue0YXwH/88YcMGDDAvK8uvvhiM08DNg1Aw5Fd+zaW13NE11O7ltWuXTvbNgPHT98vX3zxhQlE9JzT425jn55xxhmm62GpUqWC7/E6deqYz4I2bdrky30GILoIeACE5ejRoybI0YuXDh06yMCBA+XgwYNy9tlnZ7qMBkDZZX00S1OmTBnTtapWrVrmAlsVLlxYevfunWl2INzn62BnbV8vhPWCUp8XKc0a3XLLLWa7X3311QyfoxdlepGj7bdv315uvvlmEzQ0adLEBDy6L3R5vZjV7i5a7EG7u3z77bdSvnz5iPZVOGN4AgGP4zjm9X/55RfT9UZfT7sdLly4MMsLMV13zZhoANe0aVM59dRTzX7o2LGjnHzyyWa/a5A7duxYc1EbCQ0EtQtSTEyMnHfeeSb40HXWC1bdPr3I1fX/z3/+I+eff740atQo7LY1aGvXrp3Z/3p+RPraGe3PrDJUGijde++95v+//fab2U9FioT/p1WzF3puxsfHy1NPPSVxcXEmUAy3S1te0OBYz1EtGhBuwKPH7+mnnzZB67p16+SBBx6QefPmmUxWbvepvqdSK1eunDmeGsjm130GILoIeACERS9u9WLx559/lmLFipkASC9elH7TeuONN8q7774b/OZT6UV8uLRrSvXq1UPm6WOdn5Pna9Yh0H9eL7B0ILqO39GLrtjY2GzXRy+iUm+LBgl64aIXYnpxmpYGQrpP9JtnvTjTbQ/sn8DvNSuiWYWSJUuaizMdI/Ovf/0ry2xEbum4Ku3Wpcct8I24jmfRbXvzzTezXX7YsGHSv39/8//bb7/dfEuvXYkC+7du3brmNTQDc+mll5ptTL3fMpp33333meBi9uzZ5kJV6YXxc889Z36ndu/eLd99913EA9VbtGghl112mWlHg920NKjM7rVzSrOC2QWkaen5oNkczYjqemi3thEjRpiMnGYVvULfM6m7CmZHs5y6jwPvAT2HNAjSANr2PtXzULM+On7LSyLdZwCih4AHQFj04lO7L2mwo4oXL24u0LIaI6PPTX2hm53ABWiAZifSzgv3+XpBnvq19UJXu8Xot84ZFTPIiAZPmr3Q7l7alUsDFf3GOaOA5/vvvzeZm8A30brt+jiwf3Tgvu4/bUNpJqh79+7mojAn+ypcegGtXb2efPLJ4Lxt27aZjE84UhcA0G3QMVOBog96QacZGD03wulylnqdtJugZpACjh8/HjIWQrMeOa3Kpd0adXld35y8dmoa4IV7XDRIDrcrWoAGvtodcP369cEAvmfPnuYiXrsDeoVm/LIrIJKaBsKpA34NfG+99Vbr+1SzcxpEaYBYtGhR8ZJI9xmA6CHgARC2jAKMrETSTUvHUqS9p48WI0ibxQmI9PkaaJx77rkmYAmHVn/TsR066WtpgBf4xjkjGe2L1POyC95sdWnL6II6bRcrHXsT6AqYnUCAFs425Gad9NicdtppGb5upDRQ0gp2w4cPN5nHSF87tUi6X2mQpcGLBsfhdmvT7lpVqlQJOW+bN29usmZeoePQNOOm542Nzwpb+1S7yWkBAw1iM/oSIr/tMwDRQ5U2AGF3FdLxDvpteOACPXVJY+0upfOyC4IyowOcdZCvZgsCF6bvvfdesCyyXvDot8KB6lXZPV+LGmgxhdRjbHTcSuoCBjoof8mSJRmujwZTms3QCyrNGOiYkKy+adaLVM0G6XpmtH8C+0/XI7C+Ovhf50eTXsxrV0S9gNT9p5N2LdLxRJHSddWsQ+AY6zfYWtAgsA16Dmi3vtQymqfrpG0E1kcnLYiQVWngQEGM33//Pax11S6Ieg588MEHuX7tcOnYEg2MExMTw15Gz1cNALTbX+psYf369XO03Wnldnml45s0iAwEheG0uXbt2pAvF6ZPn27eIzb2qR4/DWinTp0qCxYsSBfseHGfAXAXGR4AYdFvyrX6l4470YtlHceR+ltcrZKk3Te0LLB2ZwlUaQu364pe4Ok38trNS4sP6AWODpIPjKvQQEK7IWm3NA1asnu+XkDqOmghAc2KzJgxw1x8a5YkQAMZ7eql25SWDnzX6lnaTUa7m2mwklXWQYs4vPbaa2YcgS6rXWxUoPuX/l4HqOtFn3YT0+ArEIjktEub3osn9X14dJB+w4YNQ56jXc1mzZplBv7rYHi9mNNvxLV7UVYFJzKi36hr4QrdPg1ydJ/q2Im+ffua32vQoNXctGqWVrLSimwZzdOxVJ07dza/0wIP+k247g+tiqftZUT3lR5/3Xd6rmVHz0Xdn/q6qbvGRfrakXS/0nPlmmuuMRfiWughdbcrPX81O6j7TLtI6msHJg2sdb9qcQkN0jQI12MU7nZn1X5ulw8E/6m7LIbTphZf0LFUGtBppTLdx4H3RG736ahRo8yYq0GDBpnPpAD9LNAAyYv7DIC7CHgAhEW7mWiFr0BZ6vHjx5tv+AP3gtFgQr+Z1m9yM7o/TDh0fI1WUtOsjQ7Y1gvAQB947c6i4xpSZ2iyer5W4tJuaVoGOlBiWi++AlXa9MJSg4w+ffpkuC4aRGlXNr1Q023TbI0GDoFS22nvd6PBkH6rGyhL/dJLL5lCD4GAR7+l1rb04kgDAC0GoBdoOem6pUFOZjc0TLte+vp6sajHTqtn6QWkBoppL+Q0KNQ2TznllOD4HH2culyzdr3SogyBstRaVUyDqMA26rfZeix0v+u5kdm8k046yewrzYhpFSsN0p544onga+tFa9quaLrvNJjMbGB6vXr1gsFjgF4Q67FPfcPK7F5bj6+W5s4pLTigWSQNrAJFIgIyu/mpdl/T9dGy4TruRYtJBALZ7LY7u/Zzu7yey3re63GPpE0t8KEZV90ufc+++OKLOa48l3af6jHK6oaeXtxnANwV4+S0/wkA5GNatUwzQjro3hbtAhMIJLRkt2ah9JvgQJUz5JxmFLXMb+r7sXiVVofTADyzbFVebndul9diDjo2TseyhdumBvo6ZdZdNNr71Iv7DIC7CHgAwJIrrrjCZJB0ALpmcvR+NTrGJVDwACgIohHwAEBuEPAAgCXap18DHc30aBcr7e6VkxudAvmZdp3UKW23RABwCwEPAAAAAN+iLDUAAAAA3yLgAQAAAOBbBDwAAAAAfIuABwAAAIBvEfAAAAAA8C0CHgAAAAC+RcADAAAAwLcIeAAAAAD4FgEPAAAAAN8i4AEAAADgWwQ8AAAAAHyLgAcAAACAbxHwAAAAAPAtAh4AAAAAvkXAAwAAAMC3CHgAAAAA+BYBDwAAAADfIuABAAAA4FsEPAAAAAB8i4AHAAAAgG8R8AAAAADwLQIeABHbunWrNGzYMMNpw4YNMn/+fPP//fv3W9+7NtueNWuWaSs5OdnKuiEy559/vkycODH4OJrnTUbt59Xxj/Z2AQCyRsADIGLHjh2TX3/9Va699lp59913Q6Zq1arJvn37zO+jcSFps+09e/aYthzHsbJuiMyqVatk165dOT62c+bMMYHE4cOHw3p+2vZtH//M1iea7wcAQPaKhPEcAMhQ9erVzQVeWu3atZOff/5ZYmNj2XMIW6TnTaSBRLTPy8zWh/cDALiLDA8A65YvXy59+vSRgwcPhnQdSkxMlP79+0urVq2ke/fu8s0334QspxeLga5xZ511llxyySXyyiuvSEpKSkSvH3i9H374wWShzjnnHOnRo4csWrQow+fr6+Z2vY4ePSpPPvmk6aal7QwaNMh070stKSlJ7r//fmnbtq20aNFCRowYIbt3785yW7JrN9xtDXff6jo++OCDct5555n1fPTRR0MyFjnZBrVy5UpzTjRp0kS6du2abh9ndN5kte3aTWzIkCHm/7oeul1XXnllyD758ccfpWfPnuY1X3vttXTtp37drPbdlClT5Nxzz023vu3bt5fJkydnuz4Zve769evN9jRv3lzatGkjDz30kAmYAsJ9zwAAwuAAQIQ2bNigfYCcSZMmZfj76dOnm9/v3r3bPJ4yZYp5XK9ePeedd95xlixZ4txxxx1OsWLFnDVr1gSXO3z4sPPzzz+bafny5ab9GjVqOPfcc0+mbWck8Honn3yy88YbbzgLFy50Bg8ebF5P2037PBvrNXToUCc+Pt75+OOPnaVLlzoTJkxwWrVqFfz9jh07nNq1azsNGzZ0PvnkE2fOnDlOhw4dnKZNmzrJycmZbkt27Ya7reFsw19//eXUrFnTadasmfPhhx+ath599FFnxIgRudqGLVu2OGXLlnV69uzpzJs3z3n//fedU0891SlcuLDz1FNPZXpss9r2ffv2Of/5z3/M8/V3ul2///57yD6pVauWM3nyZOenn35ydu7cmel5md2+e+6555y4uLh026XLjR07Ntv1Sfu6up+1vYsvvtjsQ92X9evXd1q0aBHcj+GemwCA7BHwAMhxwFO9enXnzDPPDE56QaYyu7CcP39+sA29sKtU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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.struphy.plot.slice(\n", " x=\"e1\",\n", @@ -598,7 +337,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "24", "metadata": {}, "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." @@ -606,21 +345,10 @@ }, { "cell_type": "code", - "execution_count": 11, - "id": "18", + "execution_count": null, + "id": "25", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAB44AAAFACAYAAABDU1H7AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAQ6wAAEOsBUJTofAAApsBJREFUeJzt3Qd8FMX///EJASEQOkoXBCkKIoJ0FBt2RbH3BvaCvbefvSG2r733CurXig1BpNhQQIoISLUgvQnJ/h/v8Xv5310ut3PJ5rJ3eT0fjxNzt2W2fXZ2Zmcmx/M8zwAAAAAAAAAAAAAAKq0qFZ0AAAAAAAAAAAAAAEDFouIYAAAAAAAAAAAAACo5Ko4BAAAAAAAAAAAAoJKj4hgAAAAAAAAAAAAAKjkqjgEAAAAAAAAAAACgkqPiGAAAAAAAAAAAAAAqOSqOAQAAAAAAAAAAAKCSo+IYAAAAAAAAAAAAACo5Ko4BAAAAAAAAAAAAoJKj4hgAACDL7LXXXmbo0KEVnQxkiEMPPdQcd9xxGbfssPvvf/9rOnfubCZMmFDRScla2XZ+Zdv2VBa77LKLueCCCwJf7vnnn2+GDBkS+HKzxYYNG0zfvn3Ns88+azJRZTy+mX7MAAAAKgsqjgEAADLAiSeeaCuhIp+ddtrJHHDAAea+++6zBXHRZsyYYebOnVthaUVmmT17tpkzZ06p599///3t+Vkey85kK1asMNOmTTNr1qxx2ldltXbtWnPzzTebAQMGmJ49e5pTTz3V/Pjjj+WyjJtuuikmHkV/3n77bZMumXJ+ReJ3QUFBuWzPrbfeWuLxiP888MADpqx0blx00UUp/1ZRdN1p20855ZSEv1966aX2d12zpfHzzz+b+fPnmyBNmjTJPPjgg+bII48s9tsPP/xgrrvuOtOrVy+nl1O+++47c9ZZZ5l+/fqZ3Xff3Vx//fVm+fLlZU5jqukIetk1atQwe+65p7nsssvM6tWrTSYp6fguWrTI3HHHHWbw4MH2Who0aJC9vleuXBnIejlmAAAAcFHVaSoAAABUqF9//dVWQsUXAL7//vvmxRdfNF9++aUtRAXSbdasWWbVqlUJfxs1apSpUoV3VV32VVlomWr1GF3JO3nyZBsbPvjgA7PHHnsEugxVbsTHo4ggKqSyNX57npd0utJeL4sXLy7xeMT7/fffTVlNnz7dbL311in/VpHXnSrktY/0MoTO82gLFiywv23evNmExTXXXGO6dOli9t5775jvzzzzTPPoo4/GfBf9ckq84cOH24rxwsLCou+++OIL+4KH8hCllWo6ymvZw4YNM3feead9IeKqq64ymSLR8dXx6N69e8yxknfeeceMGDHCfPbZZ7YSvbQ4ZgAAAHBFKQ4AAEAGUcHiTz/9ZFurPPLII2arrbaylTtPP/10RScNKGbbbbc1bdq0Yc+UM7USVoWvKh30MsnEiRNthco///xjK8r0b3ks49tvv7XxKPpzyCGHlNNWZr/SXi9XX311seNQr14907Jly2Lfq3vcyqhRo0amdu3a5oorrjBhp5bnn3zyScLeCdQrwI477mhuvPFGe10m8+GHH5qLL77Y/v8555xjK4x1XasFa61atcqUxlTSUZ7LbtiwoW1Rrorm+ArXTDu+6j2madOm9hx98803bT7v1Vdftfvizz//NBdeeGGZ1ssxAwAAgCtaHAMAAGSQTp06mapV/83C9ejRwxaGH3744WbcuHG2K8po69evN7fffrstoFQrNnVtfckllxTNL6oMUsGkKorUIis/P98MHDjQnHfeefb/41tl3X///eb777+33UKqkuOoo44yBx98cMx0S5Yssa1/lCYVVGq6M844w6nVYyrr0ljO22yzjbnnnnvMLbfcYltdb7HFFrZrR6W/WrVqpdrOyDY89NBD5quvvrLb0LFjR7sN6uoz6G11WcZBBx1kNm3aZCsC4o0fP96cfvrptvLomGOOKfpelYBarv7NyckxXbt2teNwbrfddr5pUqumww47zBbcR1MrNa1HLypoGp2DOlbahuiWUGPGjLEF+hqztWbNmrbVajTXtEWOsc4Fv3O5In3++ed2e3777TfTtm1b23Vq/LXot69KS90f63ioIkjnt14mEXVzunTpUvPKK6+Yjz/+2Bx44IGBL2P77bcPvKeDVK9Vl+OhFpfaDlXIPPHEE6Z3796BXCdBK+l68YsRqmzSJ1pubq6Nh9HnmioMX3rpJfuykVo3P/fcc7b1+FtvvWWXqViiF5I03IHip85ZvTzQunVrO7/O3/3228+ez6NHj45Ztlqll/Tb1KlTY1pfa1tUKaZjrdiqY6tzLZqOg5an61zXvmKx0qg0l0bdunXNCSecYG644QbbgjP+vpWIa1qDpuOv1umJXsLQ8YlU+uplD79WraL9p2sgyO7EU0lHeS9b143OZ+UBdtttt1KvN3LOqYJW61Ulu2KO8h4aizio3jNKOr4agkQxK3o9ugZ1H2zevLnND5VFNh4zAAAAlBMPAAAAodevXz/1cept2rQp5vuvvvrKfn/YYYcVfde8eXNvt912sx/9Fv05/fTTY+Y/8MADi02jz8477+xt2LChaLolS5Z4TZs2TTjtww8/XDTdpEmTvAYNGiSc7qGHHnLaVtd1aTsHDBjg9ezZs9h0Bx98sFdYWJjydsrXX39d4jaMHz8+0G11Xca1115rv5s8eXKxZZx66qlebm6ut3jx4qLvRo4c6VWrVq3YMmvUqOF99NFHMfN36tTJ69WrV8x3mva0004rtq6nn37a/vb55597q1evTphufXQMS1p2KmlL5VyuKC+//LKXk5NTbFvOP/98+/+jR4922ldXX3213V8unyuuuKJo/dOnT7fLOfzww4ul7YMPPrC/RU+fSKrLOOOMM+x3xx13nNe9e3dv991396655pqibSmLVK7VROfXSy+9lPB4nHfeeUXHozTnYmmPT7L4HS/R9pQ2zjRs2NBr27ZtzHfnnHNO0bGMXs6UKVO8xx9/vNh+06du3bo2DTJ79uwSz+Nkv0V8/PHHXs2aNYv9rvj12muvxaRV3x1yyCFex44di6br06ePVxraD/roOtxqq63sfi4oKCj6/aijjrLL//PPP0uVVu3rQYMGxXyna8T1fLn77rtj5tV9rVGjRr7bddNNNxU7pyPmz59vf2vcuLHveVdWydKRjmXPnDnTTnPdddeVaV06tjqO7dq1K3bchw4dmvbjG61ly5bejjvu6AUlW44ZAAAAygddVQMAAGSoZcuWmZtuusn+f/v27WN+UysOtQ579tlnbasZtSZUq8Ynn3zSdnkYoRZ25557rm1xplZoaomlMfe++eYb2zIv4r///a9t9abWzWpNpmnVCnHw4MFm48aNdhqND6mWOStXrrSt1NQ6VC21Hn/8cdtaRq14NBanH5d1RYwdO9ZO+8wzz5gJEybY7VPLO22H5kl1O9WqTNvw999/29aVI0eOtPvvhRdeMP379w90W1NZRqRLy/jWdmrd98Ybb9gWSZEWh2qReNppp9kWymqJrHERtWy1tlNXmCeddFKx/Vgaahmprm/VHa5aSkV3h6uW8ImUJm2u53JF0Lib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Rub//PPPA93WVJexefNmuw0tW7a0v1933XUJl3v22WcXLaNx48belltuWfT3ZZddFjNtp06dvF69esV89/DDDxcdg1atWnn169e3fw8YMKDYfjjttNPsd3Xr1rXHQ8v766+/Slx2KmlzPZcrUmT79VF6dRz1/3369LH/Kq0u+6osvv/+e69GjRp22Q0aNLDnbCRN11xzTcy0Ome03kmTJpVqGbNnz445Z9u0aeNVr1696Lt77703ZrkvvfSSXd/zzz/vux2pXquJzq+TTz455nhEzt3evXvbfz/77LNSnYtBxG+lN9Hn1VdfLXF7ShtntM/i99c555xj51m9enWx6QcPHly0zGbNmhXtN32GDx8eM+0BBxxQdJ5EzmOX36644oqiZSp92pZatWoVfbdgwYKiaXNzc73jjjsu4bbdf//9drkffPCB50L7IX5fyMSJE4vWrc+ff/5ZqrTq9+h7U1lF4tsbb7xR7Lc333yz6LzZaqut7HStW7cu+k7XZ8SUKVPs9aNpdDy1D7bYYgv7t+6tf//9d6n3aSrpKM9ly7Bhw7y8vLyY7SnNenXOdevWze4jxUKtU99p/R07dvTWr1/vlefx/e9//2u/1zpLihUbN27M6mMGAACAcKDiGAAAoJJWHMtzzz3nNWrUqKgwPFKhpArC6IL26dOne6eeeqpXu3btmErVM844o1jB3+uvv+517tw5pkBelUu33Xabt2bNGt/0u64rsp2qxNx6662Lpu3Ro4ddRjTX7Yx4/PHHbYVpZHoVcJ577rneihUrAt3W0izj4osvLpouvjA2uoL52muvtRWUkWlVkaNK8cLCwphpE1VW6TxTJWek0Dw/P9+78847vaeffrpYxfHcuXO9Ll26xKR/yZIlJS47lbRlQsWxCvLPPPPMopcWVOFw5ZVX2nMuvuI42b4qK1WIqnI1slxVLqqSOH6fRiqvo49hKsvYsGGDd88993jt27eP2Q5VEr744ovFlqlp9fuTTz7ptB2pXKuJzi+lb+jQoTHH46qrrvLuuOMO+/c333xTqnMxiPhd0kcvapS0PaWNM6lWHGu/nX/++TGVo6q8/89//lNsWlVIatnR6XH5LRJb9UJA5De9nKJz57777vP++ecfp4rjSy+91M773nvveWWpOJbDDjssYcVxKmkNuuJY554qqg855JBivylNyc6ln376KWb6cePGeV27di36Xft1//33t7GoLPs0lXSU57J1DeulD70wEi/V9UbOubfeessuM7K+3Xff3fvtt9+88j6+I0eOTLrd+kRXXmfjMQMAAEA45Og/5dGSGQAAAMGZO3euWbt2bbFxOxOZOXOm7WKxdevWMd9rfi2nZcuWthvLCHUBre5n1T10pMtTTbd58+Zi488p67ho0SLb5WWTJk2KdVMaP4bd33//bZeprnBT5bcudUGsblU/+eQTuw0aM1DbXVK3ralsZ8TixYttN7ytWrWKGfc16G1NZRnqzlfbqq50tf3JaNs0rfahuhVOdLx++eUXu22JugFW19/qqlP7Oi8vz3atqX2oLrIj3YhHj+OoblC1n5UupS/Zsl3Sluq5XJHWrFlj94HGD9W+0b5Tt6eu+yooOmfVVbv2jbqYTvS7zrNE6XJdRvS5uHTpUjvObklj7Q4ePNh8++239lj6dWWe6rWa7PxSN7JKm8ahrlmzpu1q+91337XncPw543IuBhG/S6I0alzxZNuTapyZMWOG3Y7o/aXzTsvYfvvtS4xn6hpc5632vY5/svFVdYx0nitWx9+bkv0mGkNd508ktsTT+LE6Tom6mu/bt68dH/W7775LGpfjx95OFOcj16lst912CY+9X1q1r3WORbpuD4K6CFd38Eq77j8Rihu6L5ZE2xgZFzya4rjOfZ076k68rPs0lXSU57LVDf2xxx5rpkyZUmw86lTXqzh89NFH22EGIjFB19lWW21lgpbo+CpmqQv6ZLSNkWsyG48ZAAAAwoGKYwAAAGSk6IpjAOGjyheN7/nQQw+Z4447Li3rHDNmjK0cHTRokK0E1Fi7Gq9U4xP37t3bfP3112lJB8qHKj91Tmks6iDHQw8bvTyhsaZ79OhhKxkzdZ+W57K1j/RSgsYMfuSRR8q83uiK47Af32w8ZgAAAAiP4F5tBwAAAADgf9Ta9KuvvrItXNNFrWqPOeYY27pNLXnVCk69BqiV3o033sixyXA6rj/99JNtHZzN1Mpz9OjRtiVnJu/T8ly2XkxR61X1npDO9Ybh+GbjMQMAAEB40OIYAAAAGYkWxwDiqXvqYcOGmZEjR9pul0WVFHfddZc57LDD2GEAKrzFMQAAABBmVBwDAAAgI5U0/i0AqIvqyLjnGncaAJJJNq42AAAAUJlQcQwAAAAAAAAAAAAAlVyVik4AAAAAAAAAAAAAAKBiUXEMAAAAAAAAAAAAAJUcFccAAAAAAAAAAAAAUMlRcQwAAAAAAAAAAAAAlRwVxwAAAAAAAAAAAABQyVFxDAAAAAAAAAAAAACVHBXHAAAAAAAAAAAAAFDJUXEMAAAAAAAAAAAAAJVc1YpOAJBu69evN4899pj54YcfTP369c3xxx9vunXrFui8QU9XXjIlnQAy0+bNm817771nRo0aZWrWrGkeeugh53kVb9555x0zf/5807JlS3Psscea9u3bx0wzc+ZMc/311xebd//99zcnnnhizHeFhYXmxRdfNGPGjDFbbLGFGTRokNlnn31Muriuv6LTCSAzzZ0717z22mtm1qxZpnHjxjZ29OrVy3e+1atXm6FDhxb7vmvXruaKK64o9v37779v3n33XbNp0yazxx57mGOOOcbk5OSUerrykinpBJBZ1q1bZ0499VT7/4oVjRo1ss/Fxx13nM23JfP999+bO+64I+FvO+ywg7n66quL/j7zzDPNihUrYqZp2LBhwrz0pEmTzEsvvWRWrlxpunfvboYMGWJq1Khh0sV1/RWdTgCZ55ZbbjE//fRTse+feuopW77g55dffrHTLlq0yHTo0MGcccYZNpaW93TlJVPSCSA70OIYFUYPPbfffnvaH/T69+9vnn32Wftvbm6u6d27t3nrrbcCmzfo6cpLpqQTQNmpAvacc85J665UpW7r1q3tA8uvv/5q3n77bed5L7roIlvhsXTpUtO3b1/z22+/2QK15557Lma6P//807z66qtm1113NYccckjRR9PGO+qoo2yB3E477WQrog8//HD7IJouruuv6HQCKJtvvvnGHH300Wbjxo1p25UPP/ywadeunX3hRjFT6x4wYIC57rrrfOfVtIqj2267bUwc7dOnT7Fpr732Wluxus0225gdd9zRXHrppcVe0klluvKSKekEUDa///67jbcqIE+XatWqFcVJvaDTqlUrc9NNN9nnYz03J6OXeqLjrD4777yzjcGrVq2KmVYvXVavXj1m2kQvEr788stml112sRWwiv962Xv33XdP2z3Idf0VnU4AZac80ueff57WXan1/fHHH8Vip2KxH72sovzdkiVLbLzRsvSMrb/Lc7rykinpBJBFPKCCHHfccd4+++yT1nXecccdXn5+vvfnn38WfXfBBRd4TZo08f75559A5g16utIYO3asd+6556Z1ewCE12233eY1b948retcvny5t3TpUvv/55xzTkrrP/nkk72vvvoq5rvTTjvNq1+/vrdp06aYWKeszIIFC5Iu77333rPTTZ48uei7xx9/3KtWrZo3f/58ryzmzZvnHXXUUd6GDRvKvP7yTCeA9Bg5cqS9jlevXp22XX7ttdd6r776asx3d911l1elShXvt99+Szqv8ndK7+jRo5NON2vWLC83N9d74YUXisXgzz77LOXpSkP5TsXbOXPmhDqdANJj9uzZ9pr9+uuvQ5GORx99NOV5b775ZjvvjBkzYr5v3Lixd++99yadd926dV7Dhg29K664oui7xYsXezVq1PDuv/9+r6z0zP/FF1+Uef3lnU4A6VG9enX7bJpOe+65py0HKI0ePXp4Bx54YNHfGzdu9Nq0aeOdfvrp5Tpdadx9990xedJ0bA8A+KHFMSqEWkaMGzfOTJkyxb4lrM/IkSPLfb1vvPGG2XfffW2XUhHqdlmt2pSeIOYNejrRG3bq1urkk0825513nvnqq6+SpnXevHm22710bg+AcPrvf/9r3/L/+++/i+Lt/fffX+7rrVevnm1ZURr33XefbY0QTS2Qly9fbrcjVYpjHTt2tK06IrQf1C10fEvotWvXmkceecScdtpp5vTTT7ctPpJRmtRSRN2clnX9qaQTQPioG9J77rnH/r/ybLp+E3X3HLRLLrnEHHnkkcVipmKHurAOgvLpVatWtb0gRKgnGrW2U+xKdbrIcAbqtlRdsqrr16efftoUFBSUmAb9pnib7D5QHukEED7qdWbYsGFFvQco3qq76IqgHhvUknbBggUpz6vedJTnVVeiqVIrsmXLltln84imTZuaPffcM2Ec+/DDD835559vTjrpJHuvWrNmje8zRLJ7iOv6U00ngPBRvlbPu+otIFKmsHjxYhNW6rFs8uTJMXFHwwkcccQRMXEn6OkiNO1ll11mW2nfcMMNtvw0mbFjx9rniHRtDwC4YIxjVAiNA6QuODWumroZkWQPSx988IHtKjkZPaw988wzSaeZOnWqrQSNtt122xX9pm48yjpv0NMp8zBw4EA7tqdu/hqjQg9ZTz75ZJkejoNOJ4BwUvelnTp1MgsXLiyKtyoYL4mu65tvvtl3uQ888IDZcsstTXmoU6dOse8+/fRTOy7PVlttVew3jXOs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vEwv9qhQS8uJj6OPPfaYLXiKdH+tilzFwEglRirTqVWeKijit19/Rwq14ltkq4Weut/WdqplXyJBpxNAeKXaUjXIrqqjKY+qfOLcuXOd59FL8ZpHvSy4Uh43Pi6Lxs2MjDkaeVaPzjcq3k6ZMiVhvFWcLalFtlrHaTkllSm4rt91OgDZE3OD6qraJRYmou6tVUYcH2f0t3rlibyAHvR0ird6jld5Qvz+0suikXLr+JfCX3nlFftyUEnxNuh0AoATD6ggF154odetW7e0rnPkyJFeTk6ON27cOPv3pk2bvD322MPr3bt3zHQfffSRd9RRR3l//PFHyvMGPd3RRx/t7bPPPt66deuKvisoKPBeeOEFb/Xq1Qm38/nnn/datWoVyL5wnQ5AeD355JNerVq17PVbEc455xyvefPmCX+bNm2ajbffffdd0XeLFi3yttlmGxtnVq1aVeJy33zzTW/58uVFf2taxaeGDRvGzPfzzz97ubm53mOPPVb03dlnn+01btzYW7NmTdF3N998s9exY0fv999/j1nP6NGjvVmzZiVMw/fff+8pO1VSPE5l/a7TAQivr776ysaEX3/9NW3rVHzo06eP17p1a++3334rcboVK1bYePvuu+/G5HkXLFhQ9Pc///zjnXrqqV7VqlW9mTNnxsyr2HrRRRcVfXffffd5W2yxRcy2uk73xhtv2OmmT58ek0bdC8aPH58w/evXr7f7dvLkyUm3Mch0AgivlStX2pjwzjvvpG2dEydO9KZOnVosL5ifn+9deeWVMd8PHTrU+89//lNsGcrnKr932223JVzHpEmTvClTpsR8d+eddxbb1sLCQq9r167efvvt523evNl+9+mnn9rpPvzww6LpfvzxRy8vL8/G+2jz5s3z3nvvvRK3tW3btt7TTz9d4u+u63edDkC4tWzZ0rv11lvTtj7FKD2HR1MMrFKlSrF03Hjjjd5VV10V891xxx3ndejQoeg5fc6cOTZW33333eU2ncomGjVq5N1///0x8/7999/eyy+/XOK2Dho0yLv44ouT7o+gtwcA/FBxjAozYcIEr0aNGt5uu+1mC7HeeuuttKz38ssv92rWrOkdeOCBtoJg22239X755ZeYaR544AH7IDN37tyU5w16OlV+HHbYYV6DBg1sBbI+LVq08I4//nhvw4YNpa44Lo/tARBOS5YssQ8w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7zJrSP1ArmfXeOty38kGtRcWmOo1C8q879sOm+C0vvUOeUmXc7Buu15ZfRydYrI45c3d9kHBVIfnD4f9vs1lM5zWt7TPSv9phvnfV/M7+T8LVASXvHKQz025DvvB5dqS/NkOEzk8MtSd73YvzPXJX+RuWmWM22kVemu3qWs25yW5DwVYJtX+yb99p3GJ783GBNfEwjUmL0rz85pLzHUrC3FMk0Oe2mVZTnHbGNPqev/1LdrL7Vl6U7/V/hN97D9J3blu8cElXS7PVy4x0jVOBhm7Xe4VLs8Crsvy2w823rqdVpVCkM90Lnko1/tA/iyHZ1vH+8naVoWBPGO5xG3X69XlecEl/+AaJ13X51R25JAm53y+w7JcYmTD593O4/zZ/rFtyyd+d1qWGeKlrSzENf653HdclhP0fQAhkaUtjg877DDz7bfflvj75MmTzc4771yqZVNxDAAAAAAAAAAAACALK46TtTg2Gemll14y69ati/lu0aJF5qabbjK9evUynTp1KvWyqTgGAAAAAAAAAAAAkFU8799Pst8zUfv2xXsa6dq1q6003mGHHczw4cNLvewqZUwbAAAAAAAAAAAAAIRLYY7/J4s0atTI1K9f38ydO7fUy6DFMQAAAAAAAAAAAICsomrhnCStirOr2tjY7qrnzZtnatWqVeplUHEMAAAAAAAAAAAAIAvHOPb5PQNdd9115tdff435bu3atWbMmDGmX79+pmnTpuHsqlp9aP/888/luQoAQAXQTem2225j3wNAOVu/fr05//zz2c8AkAaXXXaZWb58OfsaAMrZiBEjzLRp09jPAIDy5+X4fzLQ6tWrzYoVK2I+1atXN1dccYUZNWpUmZZdri2O33//fdOlSxez3Xbblds6CgoKzA8//GDWrVtndtxxR1OnTp1SL2vpLvVNtdr1Sp5gYF+n5bR/YpnvNAWOacrt3MF3mib3jvedZs6I3k7ra/+Ew0Nyjv+FtGivhk7rqzvPf0+saVffd5pa890ubpdlmXa9nJbVbEyhCcKigY1MUJqP/sthKod9YIxZ2To3sGUtHuD/jsouffwfGGZNcFtfszHBvCaUP3uFCUreyIm+06w/1O3cC4M//vjDvP322+bKK68s1fy15q0yNaoln2bRwIYBxZAkcT3F4918dHDxfemFfQNZn3PMPcT//Mob5X+eyoI3dvCdptEL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+qz9k3+nrQzANZa65pWd4rLDvWnjuhXGfPi+0zpRzirZGMdBKFWTYI37o/GF1bpXfWSfddZZae9CT+MnR7cajtB3fuNkXHTRRbagLvKZNGlSOaYUADLLFVdcYR/yHnroIbP99tubt99+u9TLIt4CQMkOOugg8+GHH5ovvvjCtGvXzjz88MOmoKB0lbjEWwAombpp++abb8wff/xhe3i44YYbzPr160u9y4i5AFByzw0qM9Y48j179jSnn366+euv0r9sQLwFAJRZocMnQy1btszcd9995rzzzrMvbkV/NAxPaZW6L2m9ofv666/bLk01prAKux588MFSF3alqlOnTnanxGc+Zs6caSs6/MaRUwYm8mna1L+lIwBUFhpn6MQTT7Tx9KijjrKfffbZx/z8s38PCPGItwCQnFrAqctqVWJcc801pnv37ubLL78k3gJAwNRT2pgxY8yzzz5rnnnmGVuZrDKN0iCPCwAl23rrrW131Xo58ttvv7VlxirUVu+RxFsAQLqpm2q/TyZasGCBHYZHDcD0jPPdd9+Z//73v3aM48mTJ5uNG0vfA0KZByHeddddbSbg9ttvN//3f/9nH8Y+//xzU972339/2/L51VdfLfpu3rx5thJb3U8BAMomPz/f3HrrrbbCWP+v8RH0tu/KlSvZtQAQIA21cu6555rZs2eb/v37mz333NMcffTR9iEAABCsI444wraG01v4GoJr9913Nz/99BO7GQACpnytCq41RMBtt91myxQ++eQT9jMAIL08h08Guuuuu8x+++1nn23at29vHnvsMbNw4UJzySWX2KHQ9F2FVRzbhVSpYrse0biYqlxQpW55a9KkiW2ZoS5V9daa3mQbNGiQ6dWrlzn++OPLff0AUFlss8025rXXXrOxVm8wqXcJAEDwGjRoYB544AEzcuRIW6h22WWXsZsBoBzUqFHDXHvttbY1nF7SUW87AIDgqcz4tNNOsw19NLSgCrgBAEirLK04njZtmjnppJNs76G63/7zzz+2YcIdd9xhpk6dan7//fdSL7tqaWecMmWK+fHHH+2buZHPokWL7G/q/jkdrrzyStsttSo0ND6RKozVWkM7BwBQOn///bftSUJxPRLnp0+fbjZs2GDjq8aYBwCU3bp162y8jc9Tr1692mb8VZEMACi7TZs2me+//z4m3ur/NfyVqKcHAEAw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8XJycryTTjrJC7NvvvnGa9y4sde7d2/viCOO8GrWrOldffXVTvNu3rzZO+uss7w6dep4hx56qD3OnTt39qZPn+6F1bhx47yGDRt6/fv3t+nVOXnrrbc6zXvfffd5NWrUsOey9lXt2rW9gw8+2Pvnn3+8MNI1quPTpEkTG3MUmpcsWeI8/yeffGKv9913390eX+2rESNGeOm0Zs0ab8CAAfaaVKxs1aqV17dvX2/lypXlOm9FWbFihb0WFXOV5ubNm3t77LGHt3bt2qTzrVq1yjv88MO96tWre/vvv789L3WunnHGGfY6Das///zT69q1q7ftttva7VUsOuCAA7wNGzaktBxdy4q3itthtmjRIm/77bf3OnbsaPMAjRo1ssdt06ZNTvPffffd9p607777esccc4zXoUMH76OPPvLCau7cufbY6r6g9NavX9878cQTnfI87777rp2+R48eNu/RtGlTb6eddrLnTFjdcccd3tZbb23PacXb0aNHO887c+ZMG6O0jdpe3VcVv9N9zzj11FO9evXq2eOlfEybNm1sfq48560oio3K5ylPoOsxcm0uXLjQd95zzz3X3hN32WUXew1ru/fbbz8bw8NKeVPdG7baaisbb/U8ouP0+++/p7Sc6667zsZb3W/CbN26dd7AgQO9Zs2a2e3V+ah48vfffzvN/+abb9rnNB3j448/3ttuu+28Rx55xAsr5QOUVsUgba/+1d+rV6/2nffHH3+08UfnhK5f/au/9X1YvfTSS/aa1X1f8fbee+91nlfngM4FnRPaVzpHdK7onEn3PUNxZPDgwd6uu+5qz7cvv/yy3OetKIodes5UzOzTp4+NRZMmTfKdT8dWcTpSFqF8ve6Vv/zyixdml1xyic2zHXnkkfZ803k2ZcqUlJbx+uuv23irc1xlMmEWKRPQMdLxUQxR3saFzoNIeZPirea/6KKLvLBSPlb5WeVTFTOVz1V+V/leP8rHavuUr9W+0rmh5SjfG1Zjxoyxz9C6N6j847TTTnOeV884uub1zKO8lvJZit2LFy/20um1117zatWqZZ+h9KypWPTCCy+U+7wV5ZlnnrH3iAMPPNDbZ599bPqVr3GJObp2dX/U+anjpWtTZWhh9vDDD9vtHTRokC2r03n6/vvvp7SM7777zttiiy1svA3z9RgpE9D2qoxO5V2KIZ9//rnTvPPm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kCqylDcpT1qPzqdk12N5zFtRdL2p0DKoNCs/oBcinnvuOZOp8TaZzz//3Dz66KO2IiO6YC6syrK9qjhUjFKeVpU2u+yyi61AVsHaM888YwYNGmQycXuTFZA99thj5sgjjzTHHXec6dWrl5k4caLN22fK/TXofZWOF0CUDt3LS/NMUpZ5w/RcoYJbPU+lmubJkyebl19+2ZYrhJW2KX57VWaiwnq/7VW80bbpvqLn9LDTixirVq0q9f1FeVyVH+hFgp49e9p8n+LvbbfdZi688EITNqqA0D2itNt7880323irl+9VjqAXe1SBo3xveef10i1SflbR+cOylAGVZd6KojQnegmhNGnWSxz33HOPOeigg+zLSWHdXlUcl2Z7FW9OOukkc91119kXsOJfosu27VW8jTQcUkMnvQRx/PHHmyFDhpgHH3zQhFGia1Bl8ipH8Nves88+25ad7LnnnnY7VeeiZzWVI6VSrpYpEu0r3Vfy8/NDG69QeWVtxbEKmSS+8jBSiJKsBU1Z5q0oQab5mmuusW84vf766wkzX2HZ3kQVw9peZXr1u260iag1ggqUMqVQzW97/Y6tXnbQ/CosVksV7RdVBOihTw95r776qsmm7RVtoyo0dIyVcdb26oFe+yLblHVfBZUGId6mHm/VQuquu+4yN954Y7G3DsOiLMdXBfiXX365LWTJhEK1sm5vJMboDVm1llLmX6211ULjhBNOSNp6LlPzD7rn6tjq/qLW52pxrbeLszneVmT+kHhb+n2uSja95KCCYF2PYVSW46sKgVNOOcVcffXV9s39yhJv1epYBYhbb721/fuMM84wJ598spkzZ45tGZZN8Vbz68Uc9X4h+le9zoTx2bSswvI8XpZ8dhjy6KkKKs3qTWDw4MG2VZzyuGEUKTMozfZqPlVi6CX7ML6kkso1FcnXuJQpfPrpp2bMmDFm1113td/pZW29iL/77rsX6xkt02OItldlYcrTT5gwwVbMqWFFZLnZJEzxVr0blDbelnbeTI+3qlRVHNILL4888ogJq7Jsr8otlae75JJLTKYoaxmuXj7XC4NDhw6133388cf2RZYBAwaYI444wmTb9qo8QeW2el7TM43KczOlvD5VmZg/ROWVnVehMUUFBQo80fS3LlC9NVse81aUZGlWwFXhtYvhw4fbVkJ6s+ewww4zYaXtVUWoPvHbqwx9SZXGkbdHVUGj7prVKkwftcJRBYf+P4xd3Gl7dfPUA2789voVikVaiunttMh+0U1ZrRtHjx5twkjbFH8uu27vu+++awuGH3/8cfsChCrG1b3Se++9Z1tdZ5uy7Ksg0xBZZ3waon8Pet6KElSadU6qcFtvWF577bUmrMqyvbqnqFB72bJlRfFWLTRUwaj/V8F+Nm1vJN6qNVjkvqueItRVtwr41QVTJm2vHl6SVfirK3kVVOj+ooJEvQihynG1ElWBcbbRvtJ9WHmG+H2lN8E1xEJ5U08rkRfA4tMQSWN5zFtRInnusqZZFYsq4O/fv39oWxuXNf6o1ZuuPcWhSLxV/kcFEPp/tf7Lxni73377FVUaiyqONX8Yu3ory/aq96i9997btkhWq3kNhaB/1fJGBYn6PZuEJX9Ylnx2GPLoqSopzbrvuaZZcUgvzCkf9OGHH9r7YxhFynXit1cv/GmIkWTbq5a2X375pX0ROxJvVakqevleBfxhozyKWjTFb6+OrfI2LmUKeikpUmkskQoNvcATNsq/Kh9b2hiifKzOZeVrlb9VPve0006zQ++EcSilsiDeVtx+L+s9QuWhehbTi7sq2wtzN+ql3V61wFSM7devn3n66aft/6snL1GsVczNtuOreBspQ4hQHnCbbbbJyjLcc889126XKstVfvvBBx/YOonTTz/d/n+2SbSv9Lyml0DCmj9E5ZW1Fcdq7SLxrXv0t7p60NtY5TFvRUmWZmXwXd7U0Q1Yb3Cpi5Owd+kb2d7IG/fR2xv5rSQqOFShi94cjXzUdZMeDPT/YSx40Tapq6X4cS1ctlcPtBL/0K6/tcz4yugw0DbpRhpfyeKyvXqo04Oi3nyOUKHAHnvsYX/LNtof2k/xGQ+XfRUUjcOj8ylR/FGGN1m3i2WZt6IopqryJVGaVSjjMm66xr09/PDD7Vjrels/zJLdX3RtNW/evMR5e/fubbscio63qkRWnNX/K+6GTWTMqETbq24xk1Wkal/pfpso3oq6Ms6k46sKiehxt+KNHz/eviEcXUms61Zd+qm7qfiXuzJdsn2Vri71I3ExURqi0xj0vBVFld3qGr4saVb3kqrE0LSjRo0KdReTKvBTF3al2d4OHTqYY445xva2Eom36s5Z+Tz9v1oAho0KwFS5lGh7FUuTjd2r/aThVjIp3uoYKaYm2l59r99LouFH1J2oKi2i6UVffZ+u4UnSRcde50CifaVzRudOOuiai0+DCrGVl/GLP2WZt6IkSrNe8tP93CXNeh5R4bYKQFWRGtbey5Jtr8oXFDeTba9eilQlovZNJN5GntP1Qoe6kA9jRbnyKqW9n6pMIT7eKl+hZ6Iwxlvd6/WcWZrt1fmufKzys9GtWBVv9WKB8r/ZJPI8V9H5Q61H1050iztdi7q/ucTb0s5bUZSu+PJMnV+KJS5p1jOYetFRzFEF6nbbbWcyLd6qTGD+/PlJt1f5I8XbSPmBPurpSnTMtf2Zsr16eV55Npd4G4mv0RSDwxhvS9peHVsdN5cyXJUZRQ9FqZciNXREtpbhxu+rGTNm2PgV1niFSszLYp06dfKOOOKIor9XrlzpNWrUyLv66qtjpnvrrbe8999/v1TzhkVBQYG3zTbbeKecckrRd3/++adXu3Zt77bbbouZ9rXXXvNGjx4d890rr7ziValSxbvlllu8TLBx40avSZMm3nnnnVf03aJFi7waNWp4DzzwQMy0L774ovf5558nXV6HDh28k046yQurNWvWeA0aNPCuvPLKou/mzJnjVa1a1Xv66adjpn3mmWe8cePGFf29efNmr1WrVt4555xT9F1hYaHXo0cPb6+99vLC6O+///by8/NjzsepU6d6OTk53uuvvx4z7eOPP+5NnDix6O8HH3zQTqf9E70Pdtxxx9BubzQdT4XmJUuWFPtt06ZNdnu//fbbou8WL17sVa9e3bvvvvuKvtP+0DI++OCDtKX7yCOP9HbYYQe7ryMxqXv37t6gQYNiptO1qGuyNPOGyf777+/16tXLXkuRY7Pddtt5xx13XMx0irWKudF0fHR+a1ptaybYddddvT322KPo7w0bNnitW7f2zjjjjJjpdC/VPTWZww47zB7fMOvWrZt30EEHxcRg3XMuvvjimOnefvtt79133435bt999/V22223mO90f2natGnR+RI2ugcec8wxRX8vX77cq1+/vnf99dfHTPfmm296H374YdHfX3zxhY018fvg6KOPtvsr7CZPnmzTH58nis4bffLJJ0V/K0ZtvfXW3tChQ4u++/33371atWp5d911l5cu//d//+fVq1fPW7ZsWdF3J5xwgteuXbuYc2zKlCn2nqHrNdV5w+Syyy7zGjdu7K1atarou0MPPdTe16N98803dnsj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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_space.struphy.plot.panels(\n", " x=\"e1\",\n", @@ -633,7 +361,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "26", "metadata": {}, "source": [ "## Interactive plots\n", @@ -643,21 +371,10 @@ }, { "cell_type": "code", - "execution_count": 12, - "id": "20", + "execution_count": null, + "id": "27", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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i+igOzk5Wbp162buRHTppZdK2bJl5Y033nB8W9j7g9KvXz9zvC+77DI566yzTA/k7OhF0csvv2wGyNepdu3a5mZHkyZN4nTkgeHDh5tz1apVKylXrpzceeedJqDLjY8//liSkpLMEE9w3oQJE8y5atmypVSsWFGuv/76HF8w6fi3N9xwg/k5veSSS8xtqz/66CPH97mgmzZtmlStWlWaNm1qjvkVV1xh7j6ZE8OGDTM3PGnUqJFpo27dujJv3jzH97kg06SRxg169+RatWqZY58bS5YsMX/LLrjgAjn33HPlwgsvlFWrVjm2v8gDVgS78847rapVq1qbN28281OmTLGioqKsadOmObotgvf8889bcXFx1vLly838woULraJFi1pvvvlmlttt2LDBiomJsd5++23L4/GYZS+//LKm36xvvvmGU+KgDz/80CpcuLD1ww8/mPm1a9daZ511lvXYY4/luI1t27ZZpUuXti677DLrzDPPdHBvoebOnWsVKlTIev/998389u3brcTERKtbt27ZHqBjx45ZderUsTp06GDt27fPLNu9e7f13HPPcXAdpD9X+rvw2WefNfOHDh2ymjdvbrVs2TLbbSdPnmx+F+qjOnHihNW1a1erZMmSvnOI0Fu3bp111113WYsWLbL69+9vxcfH53jb/fv3W2XKlDHbnz592pyz6667zqpRo4b5N/KniA269QNbpEgR69VXXw1Yfvnll1tt2rRxbFsET4PlcuXKWQ8//HDA8l69epmAICsaNMyZMyfd8mrVqlk9e/bktDioUaNG5o+BvyFDhlglSpSwjh8/nqM22rZta/64DBw4kKA7D3Tq1Mlq0qRJwLJx48aZQFx/lrLywgsvWAkJCdbBgwcd3kv4e+ihh6yKFSv6kgpq1qxZJpjWoC4rd9xxh1W9evWAZb/88ovZdsaMGRzoPJDboFuTRmeccYZ14MAB37K///7bnLOPP/7Yob2E0yK2vGThwoWm5EC/+vSn8z/99FOWNYh2tkXwtJZ3586dGR53rYXT5zLjLQPyp+dJv94rVqwYp8XB2vtff/01w3Om/SF+++23bNt46623ZPny5fLCCy9wnvLIjz/+mOE50/Iu/R2XlcmTJ8tVV11lbuaxbds22bdvn8N7C+85u/jii6VQodQ/29757MpESpcuLYcOHQoo+fLWhOtzcOf5rl+/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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "phase_viewer = phase_space.struphy.plot.viewer(x=\"e1\", y=\"v1\")\n", "phase_viewer" @@ -665,7 +382,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "28", "metadata": {}, "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." @@ -673,28 +390,17 @@ }, { "cell_type": "code", - "execution_count": 13, - "id": "22", + "execution_count": null, + "id": "29", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=12, show_paths=True)" ] }, { "cell_type": "markdown", - "id": "23", + "id": "30", "metadata": {}, "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." @@ -702,10790 +408,10 @@ }, { "cell_type": "code", - "execution_count": 14, - "id": "24", + "execution_count": null, + "id": "31", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "animation = phase_space.struphy.plot.animation(x=\"e1\", y=\"v1\", step=4)\n", "HTML(animation.to_jshtml())" @@ -11493,18 +419,10 @@ }, { "cell_type": "code", - "execution_count": 15, - "id": "25", + "execution_count": null, + "id": "32", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wrote: ['frame_0000.png', 'frame_0001.png', 'frame_0002.png', 'frame_0003.png', 'frame_0004.png', 'frame_0005.png', 'frame_0006.png', 'frame_0007.png', 'frame_0008.png', 'frame_0009.png', 'frame_0010.png']\n" - ] - } - ], + "outputs": [], "source": [ "frames = phase_space.struphy.plot.frames(os.path.join(demo_root, \"frames\"), x=\"e1\", y=\"v1\", step=10)\n", "print(\"Wrote:\", [os.path.basename(path) for path in frames])" @@ -11512,7 +430,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "33", "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." @@ -11520,28 +438,17 @@ }, { "cell_type": "code", - "execution_count": 16, - "id": "27", + "execution_count": null, + "id": "34", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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Cq8ullahQgWZM2eOWXskNDQ0eV8JAADwrXVGUhPrjAAAkP54e/zmRHkAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACs8k/Knfbs2SOHDh2ScuXKSalSpe54+5iYGNm2bZucPn1aihQpIpUqVUrK0wIAAF8PI9HR0dKtWzdZtGiRVK1aVcLDw6Vnz54yfvx48fPzize4tG7dWq5duyb33nuvbN26VYoWLSoLFy6UwoULJ9frAAAAvtBN89FHH8nSpUtly5YtsmbNGnOZMmWKfPPNN/HeZ8iQIZI7d27566+/ZMmSJebnpUuXZOTIkcnRfgAA4EthZNq0adKpUydX10z16tWlRYsWZnt8Ll++bCoi/v7/KcLkyJFDSpcubbYDAAD4J6aLZvv27dK3b1+P7RpIJk+eHO/93n77bRNgRowYYcaKrFu3Tvbv3y/jxo2L9z5RUVHm4qSVFAAA4ONh5MqVK3Lr1i3JkyePx/a8efPKxYsX472fVkUaNmwokyZNMv/euXOndOjQQUqUKBHvfcLCwuStt97ytmkAAMAXumkCAwPNz8jISI/tet35u7h07txZTp48acaKLFu2TPbu3Str166VPn36xHufoUOHSkREhOty5MgRb5sJAAAyahjJnj27FChQQI4ePeqxXYOCjgGJi1ZSli9fLl26dHEFlpw5c8rjjz8uP//8c7zPlSVLFgkODva4AACAjClRA1hbtmwp8+bNE4fDYa7fuHFD5s+fb7a7T+V1Bo3MmTObAKNjRNzt27fPrDcCAACQqHVG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Vppx/vx5R5s2bRw5c+Z01KpVy1GiRAnHzJkzHRmBT1ZG+vXrZ5an1WSZM2dO+fDDD6V79+5y//338w3lvxWjr7/+Wn788UcZP3687T9XmrJ37155//335cEHHzTX9Ztt3bp15Z577pH/+7//E1938OBBmThxolSrVs1c379/v9x3333mMnDgQNvNSzPeeustKVy4MOfbisM//vEP6dmzZ+r/UdI4h8Mh7dq1kxs3bsihQ4ckT548cuXKFZk7d65kBJl8cU3/OXPmyEsvvWSCiHrhhRckR44cMmPGDNvNSxPee+8918EEnvRD0hlEVMWKFaVRo0ayevVqdpWIdOrUyeO9U6ZMGXPA1eCP/1ixYoVMmzZNJk2axC6Jw9mzZ+WPP/4wXRH4n19++UVWrlwpn3zyiQkiSo9hPXr0kIzA58LItm3b5ObNm1KzZk3XNn9/f6levbps2rTJatuQ/uh7afPmzVK2bFnbTUkz9JubhjP98NRqiH57e/755203K80caPXg8cUXX0hISIjt5qRJb7/9tjz77LOm2qhB/8CBA7ablCYsXbpUihYtaqqMu3btku3bt982viY987lumvPnz5ufefPm9diu152/A7w1dOhQMxBxwIAB7LT/0kHhQ4YMMVVI7abR7isdyAqRp556Sp588klzkEXcQUTDmn5B1ODWtm1b6dy5s/z+++8JnvHVFxw/ftwcp5o3by6HDx82Z7PXY9bHH38sXbp0kfTO5/66AQEB5uf169c9tl+7dk0CAwMttQrp0ejRo80HwezZsznYusmfP7+pjOhsLK0aTZgwQd58803xddo1o7NFHn74YbN/9KLfbPXA8ttvv9luXprw9NNPmyCi8uXLJ++8847ZZxpqfV1AQID5P9WmTRvZvXu3Gb+moV+7jvU9lN75XBhxThGL3Yet15leB2/poOdhw4bJ999/b76pIG7afdW+fXtZtGgRu+i/++P11183BxG9XLhwQZYsWWLOM4LbFSxYMM7Pa19UqlQp8fPzk969e7u2afenBtr169dLeudzYaR8+fJmQN0PP/zg2qYjk8PDwzmowCtjx4413TM6ir1ly5bsNTdXr16Nc3ZW7G5RX6TdD86KiPNSqFAhUw1YvHix+Lq43ju6XzJnzmwGivu6Fi1amBk12l3jdPToUVc1Mr3zuTEjmizDwsJMaUv/gDraX0uBderUMdOm8J9BvhcvXjQhzTkYUdWqVSvjLLCTRFOmTJEXX3xRXnvtNQkKCnLtG/13aGio+LpWrVpJ06ZNpUaNGhIdHW26sHQGAAdb3In+X9KZfN26dTMDNdeuXWuu65gjZ4XEl9WpU0c6duxoLjr9WYPJiBEjpF69elK/fn1J73z2rL0LFiyQqVOnmsF2OlVz8ODBEhwcbLtZaYJOe9Z+2thmzZplPiR82fDhw2X58uW3ba9QoYJ89tln4ut0/R6derhmzRoT/HW/9OnThzE18dADyyOPPGKqIxAzUPXzzz83X4S0gt21a1cTbvG/2Xu69pOGex3jqEGkf//+GWIFcZ8NIwAAIG3wuTEjAAAgbSGMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqn1uBFUDa8Mcff5hVWlXOnDnNasgZYfEmAInHomcAUp2eIC5PnjxSuXJlE0ROnjxpTkEwefJk6dSpE38RwMfQTQMg1f3555/m58KFC80S4AcOHDDnhnr22WclJiaGvwjgYwgjAFLdxo0bzbmgSpYsaa7reWyaNGlizm1z/vx5/iKAjyGMALASRqpUqeKxTc/SWrp0acmXLx9/EcDHMIAVgJUwUrFiRdNFc+3aNVm0aJF89913MnfuXP4agA9iACuAVBURESG5c+eWcuXKmZ/Hjx+XEydOyOzZs6Vt27b8NQAfRBgBkKqWL19uxods2bJFqlatKg6HQ3r06CErVqyQgwcPSubMmfmLAD6GMSMAUr2LJkeOHFKpUiXX4NWhQ4fK0aNHZfHixfw1AB9EGAGQ6mGkZs2aHhUQDSa65oiOGwHgewgjAFKVVkJ0TZHY+vTpYxY/A+B7GDMCAACsojICAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAADEpv8HHVmCw+8BK9gAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out.plot.equilibrium()" ] }, { "cell_type": "markdown", - "id": "28", + "id": "35", "metadata": {}, "source": [ "## Derived quantities\n", @@ -11551,28 +458,10 @@ }, { "cell_type": "code", - "execution_count": 17, - "id": "29", + "execution_count": null, + "id": "36", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "largest drift of the total energy: 1.586e-06\n" - ] - }, - { - "data": { - "image/png": "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63HQynHz5s2T77//3tzWynF6X+vWrW2BlSM01UxPWPfv32+7Lycnx5yQWnsOtPz+559/buZFs1bSa9WqlalMt3fv3nK3oRMka4/JjTfeaNLBNECzp+lny5cvt1Xz03QynXPNXqNGjcw+0DaUxVX7pTrW7+h7Kk1FP5fStuvIeyvttY58hlX5nly5Lq1yeuWVV5qpJ/755x9zn86f9uqrr5b4fP2BQf8faRruueeea37EAICqQA8ZAFQDDYS0t0HLi0+cONGM59Jf83XsVZcuXeSJJ56wnQTqr/HDhw83Y2D0pFN7MT799FOTYuYo7fFo2bKlKR2en58v/v7+JiDTkt5NmzY1zxk1apQ50dZt6kl67dq1zSS/2pOnPV/l0fbo86ZPn27K3euJsT0da6QBaIMGDUyqlwahWjZdJyG20omx9bXaVk2D1LFN+rriXLVfSuPK9Tv6nkpT0c+ltO068t5Ke60jn2FVvidXr+vll182QZn+H9AgVCf71pTbt956y4yps6c/XGjPtr7mpptucqqNAOAMP63s4dQrAACVpoGYngzqiaF9mp+VBmw6JkZPNhs2bFjkMS2moSegxVPpdGJfPXGOjo4+ZVtK16OBWUm0Z0F7A7Q99s8pbVtW2quyZ88eqVevntl2cRoM6uNKizUcP37c3C4evGmPhW5H368+T4MGva9Dhw5Vtl9KUtb69fPSnkBNASzpMyuu+HuqSPtK+1yc2a4j762s15b3GZb2WZXG2WOtIvunvONWe441mGvevLkJOh9//PESezR1Hj/9EUP3W/GADQBchYAMAADUCL/++quZHP3CCy80vdY6fkyXdfymTnhuT4M6DTJ1MndNKwaAqkJABgAAagRraqOOi9MUTA3ONC3x+eefN72CVuecc46pinrGGWeYMZ9a8h4AqgoBGQAAqFG0BH5ycrIZL1dSqf3ExESTltqiRQvTkwYAVYmADAAAAADchLL3AAAAAOAmBGQAAAAA4CY1PiD7/fff5bHHHjO55AAAAABQnWr0GDKdWFInv1y3bp2sWLHilHlxyqLz0WzatEliY2NLHBAMAAAAoGbKzc01HT6dO3eW0NDQMp9boyOJhx9+WK677roKzS+iwVjPnj2rpF0AAAAAvN/q1aulR48e3hWQHThwwMwTctppp5mSsyXJz8+X3bt3m2izYcOGFd45f/zxhzzx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Lyh5r5iHDAAAuENpgdfEiRPl4osvdkubvNXq1atl6NChMnDgwFKfk52dbQKjivSeVea1cD16yJyUkJAgcXFxEh8fL542fkzRQwYAANSff/5pekH0xPvWW2+Vc845R5588knz2J49e2TKlCnmhP+CCy6QV155RfLyCrJtSvLll1+adellwIABMmHCBPnpp5+KpCVqD5kGZdbnTZs2zTy2cOFCefTRR83yihUrpE+fPrJ///4i609NTZWzzz5bvvjiC9t9zrbR0de99tprMmbMGFmyZImMGjVK+vXrZ9LvNHB84YUX5K233jIBpLZTX5eWliZPPfWUWde5554rd911l+zbt8+2vgMHDpj3q/vj7rvvNtvVHq7SlLU+6/7Tz+69994zy3feeecp69BAyhrk/vvf/7btcw20yttGea/V/aG39f1rqqme+2ZmZoqzdN+98847cvnll5tj5qabbpLdu3efst+WLl0qDzzwgFx00UVmex9//HGF16WfwSOPPCKDBw+2fQabN2+Wa665Rvr37y+jR482Pxqcd9558tlnn5nHdb/oey2egmmxWOSSSy6R119/XaqcBRWyZ88ei+4+va4Oubm5lgMHDpjr4n7elmxpPvUrc0nPOvVx1GxlHTsAxw743nFMVk6e5a/kVLdetA3OWLNmjTlXqVu3ruXpp5+2LFmyxLJjxw7L77//bomJibEMGDDA8vnnn1tmz55tadasmeWmm26yvXbatGmW5s2b224nJSVZVqxYYS6LFy+2PPHEE5bQ0FDLZ599Zh7fu3ev5brrrrM0adLE9rzt27efsq6srCzTnieffLJIW999911LcHCwJTk52fy9+umnn8ptY0kceW9Tp061hISEWDp16mQe17YePXrU0q1bN/OeLr/8cstXX31l7s/Pz7f079/f0qpVK8tHH31k+eKLL8ztBg0aWA4dOmTWt3PnTrOf69SpY3n44YfN/klMTCyxfeWtLzMz02xX95fuT13esmXLKevJycmxLFiwwGz35Zdftu1zXX952yjrtWrlypXm9vLly83+OfPMMy0XXHCBbdv6Gelrv/nmm1I/B13XsGHDzD7RbXz77beWMWPGWOrXr2+OJfv9FhMTY3nssccsixYtsjz11FMWf39/y6efflqhdelz7r77bssPP/xgPgO9v3bt2paRI0davv76a8tbb71ladiw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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "total_energy = out.scalars.total_energy\n", "energy_error = total_energy.struphy.analysis.relative_error()\n", @@ -11584,7 +473,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "37", "metadata": {}, "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." @@ -11592,36 +481,10 @@ }, { "cell_type": "code", - "execution_count": 18, - "id": "31", + "execution_count": null, + "id": "38", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/diagn_tools.py:246: UserWarning: No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.\n", - " ax.legend()\n" - ] - }, - { - "data": { - "image/png": "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ABc98MADcuONN8p///tfaWxslMWLFwe27mnTpslBBx0ktbW1UlZWpm97/PHHZfvtt28NNpdffrncdtttss4668i8efOkuLhYbr/9dtlvv/2y/j9UXgJsxAIA7EaVBR0dF4Ra2G7atGly6623ylVXXdXhci0tLbqCoi4q5HRGBZMjjzxSNt98c5k9e7bMnDlThg8fLocddpg0NDS0Vlyqqqrkm2++kU8++UQvc8QRR+j7qb9li/ACAPAeDVNke1wQZGCzMWPGtFZC2vPKK6/IhhtuqKsoKlisu+668uSTT3Z4nw8//FA+++wzufjii3U1Rbnkkkt0QHnxxRf19fLycjnvvPNktdVW09eLiopk6NChuvqjqjXZotsYAMBbfJOOQo6J5LKcqwRxq6urS7teUVGhL7lSVZORI0fKnXfeKYcffri+7ZlnnpGjjz5avvjiC+nTp0/G+02aNEmHlsGDB7fetsEGG0jPnj313/bff//W21WXtVmzZsn3338vV1xxhZxzzjmy5pprZr2NhBcAgFcILAj6mCDEIG592oSKyy67TI8vydX999+vg4RanxrQr7qD9ejRQ6qrq+XVV1+V4447LuP9FixYIN26dWutuiSp+6q/pXrqqafkscce093HVLhRwSgXhBcAgBcILQj7mEhdH9UYRGnmzJk6YLQef3lUXZQvv/xSFi5cqLt3perbt29rMFHjYNQyirpt55131gP0ly9fdfz3smXLdHexVGeffba+qHE1KmDtscceeuaxXr16ZbWNhBcAgNMILYjjeKAagyhVV1enhZd8denSRY93efPNN9td5t5775V33323NSRNnDhRdxFbunSpLFq0SFdgFDUN8ty5c2X99dfPuB4VfM4991zddUz9PzW4PxuEFwCAkwgtMOFYIMTAJsOGDdNTF6vzsAwaNKj1dhVE1KWyslLGjRuX8X6q+vL000/L8ccfr29TA/VVoNl777319SVLlsjqq6+edr+vv/66tXtZtggvAAAnJbvtEGL8ZsrzT4iBKVMlz5s3T6ZPn667bSUrLFtvvbV07dpVDjnkED3F8YgRI/TMYaq7mLrPXXfdJePHj5d+/fplXK8aJ6O6mp111lk65KiKzAUXXCAnnXSS9O/fXy+jxsz89a9/laOOOkqPqVFdxa699lrdbUzNOpatooQaiePBDAw1NTUyd9yuUt3l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GlqX+9Of/iT33nuvfPDBBzJp0iSZMWOGrL/++pILwgtiH6zPRAEAgLCEGWBMC0embQ/MM2bMGNl+++07XOaVV17RVZODDjpIB5J1111XnnzyyQ7v8+GHH8pnn30mF198sRQXr4wXl1xyiQ4xL774or6+YMECufHGG/XtW2yxhb6tqKhITj/99JweA+EFAIAQ0W0pfjTq4bq6urq0S2q1IxeqajJy5Ei5/vrr5euvv5YpU6bIPffcI8cdd5wOIu1RVRQVWgYPHtx62wYbbCA9e/bUf1PeeustWb58uRxwwAEya9Ysfbva1lwRXgAAgPMIMHBZnz59pKampvUyduzYvNZz//33y5prrqnX984778jbb78tPXr0kOrqann11VfbvZ+qqnTr1q216pKk7qv+lgxG6u9/+ctfdPXnhBNO0GNfLrroopy2kdnGAACAVwEmiGoYYQgmmTlzpg4YSRUV+Z0A/Msvv5SFCxfKeeedl3Z73759W4OJGgejllHUbTvvvLOUlZXpqkpby5Ytk/Lycv27WkaNpZk3b558++23UlpaKm+88Ybssccesu2228qhhx6a1TYSXgAACAldxsykggfPDVxSXV2dFl7y1aVLFz3e5c0332x3GTXg/t13320NSRMnTtRdxJYuXSqLFi3SFRhFTZc8d+7c1gH5ar3KKaecooOLomY9U+NfVIjJNrzQbQxGDJg3YRsAAP5wrXLi2uNBPIYNGyaffvqpTJ48Oe12FUSSlZVx48bpcKMuKrgk76cqK08//XTrfdRAfRVo9t57b31dnVNGBSw13iVpxYoVOuCstdZaWW8jlZccFdesWhIDAAD+dCMjKMDmqZLnzZsn06dP1124khWWrbfeWrp27SqHHHKInuJ4xIgReuYw1V1M3eeuu+6S8ePHS79+/TKuV42TUV3NzjrrLB10VEVGnc/lpJNOkv79+7dWda644go5//zz9fTLvXv3lrvvvluam5v1ctkivAAAEAK6JdmDbmTwxYMPPqinQlbUCSIvvPBC/bsKESpkqDEsTz31lO4aNmHCBFmyZInu1vXEE0+0G1ySrrzySh121LIqwKgwc8YZZ6Qt8/vf/15Pvay2Q1VlBg4cKH/96191kMlWUUKdVcZxaho2NfPC3HG7SnWXUimpyr964mLlxZQuW4na/AaXmaqltjKv+zXXr3q/pvou0pjh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/fffy4IFC6Rfv376uMlEnWD366+/1u9d6623nvhIDcv85ptvZOnSpXpftT3Pwg8//CDTp09f5X677rqr+Ei9l9fW1spGG23U7utKnStN7Te1P7t37y6+HlfquFEnwe7bt68+NUOq//znP/LTTz+tcv6R9j4DfJB8rW2yySay9tprr/L3L7/8Ur/Pq4mVmCAJoUo47NJLL01UVFQkNt98c/3z5JNPTjQ3N8e9Wca54oorElVVVYlddtml9TJixIiE7xobGxNjx45N9O3bN1FTU5MYOnRoxuWee+65RI8ePfRy3bt3T2y77baJ2bNnJ3xSV1eXuPjiixPrrbdeomvXroljjjlmlWU++OADNTlIYscdd0w71qZOnZrwydNPP50YOHBgYt11100MGjQosfrqq+v3qrauu+66RGVlZWKzzTZLdOnSJXHkkUcmGhoaEj657777Ev369UtstNFGej+o1+Ett9yStszNN9+s90/qMaUuvnn++ef1caX2l/qp9sl5552XaGlpSXtP++Uvf9l6XKmfV199dcI3Dz30kH6/HjBgQKJ///768++aa65JW0a93tZee+20Y+r0009P+GrJkiW6LaXew++88860v/3www+J7bffPtGtWze9X9dYY43Es88+G9u2wn3Ohpfx48cnysrKEm+99Za+Pm3aNP3Bd9NNN8W9aUaGlx122CHuzTDOokWLEhdccEFi+vTpOvhmCi8//fST/uD785//rK8vW7ZMN87/53/+J+ETtY/UPlChbfjw4R2Gl4ULFyZ8pt6DpkyZ0nr9tdde0+9VjzzySOttr776aqKoqCjx73//W1//9ttvEz179kyMHj064ZOrrroqMWPGjNbrjz76qN4vb775Zlp4UQ1Q39199936dZj07rvv6n315JNPtt42ZswYfRx98803+voLL7ygl5k4cWLCJyoAp37B9Pjjj+v3JvUelRpe1Ps+VjrxxBMT5557bqKkpGSV8LL//vvrNsTSpUtb2xTqS6w5c+aw+xAKZ8e83HPPPbLXXnvJzjvvrK+rrjyHH364vh2rampqkk8//VSXytXvWNlF4Oqrr9ZdCtrz2GOPSXNzs5x77rn6uurSct5558nzzz+vS+y+UPvokksukd69e3e6rOoGpbqPLVmyRHz0+9//XnerSNp9991bz1+VpN6ndthhB9l333319fXXX1+OP/54796/1MmJN9xww9br6j28R48e8tZbb6Utp16DkydPli+++EJ3XfTRSSedlPZetf322+vuiKqLWJI6ftRxtMEGG+jr6gTQ6jjz7bj63e9+l/Zetccee+ifqftKUV0VP/zwQ91t0eezSvzjH/+Qjz/+WK688spV/jZnzhz517/+Jeeff7506dJF36Y+D9X+evTRR2PYWvjA2fAyadIk2XbbbdNuU2/mU6ZM8fbDrbP99ctf/lIHPtW//v777497k6zZb5tttllaf2l1nKk37kzjOyByyCGHyNFHH6372p911lneh+WFCxfqfuQbb7xxp+9fajyDWt5XavyiGieUuq8Utf+OOOIIGTFihKy55ppy++23i4/q6up0CJ4wYYIce+yxOvip11ryb2r/ZTqu1PHmm3nz5ul99cwzz8gxxxyjv0RQx0+qxx9/XE4++WQZMmSIPuZeffVV8Y16ban36QcffDDjOBb1paf6vEs9rlSIUV/I+HhcIRrODthXH3DqG7pU6rr6hk69ibf9m8+22247/aGW/DbuxhtvlBNOOEEPymPQfn7HWfJv+JkKKy+99JIOyMr777+vf1f7S1VtfKQ+9E899VS9b371q19lfVz5OMhafemk3pe23HJLOeCAA1pvHzhwoEybNq11goh77723tQqRrFz5QoXbCy+8UB8jqoowduxYPXlG6vtRpuPKx/cq1ei+7LLLdOVATXBw8803pzXODz30ULntttv0a00de6piOnLkSPn888+zqjC7QD1uFX7V+3NqtTgVxxXi4GzlpaysTM8ikmrZsmX6Z3l5eUxbZabhw4e3BhdFfcuigovqEoWOcZxlT81slAwuyW981beaDz/8sLeHmWoQvfzyy/L000/rbopJHFfp1JdOqjKsGufjx4/X+ydp2LBhaTPbnXjiibor1COPPCK+UTNhqWqCamC/8MILuivP3Xffrf+W3GeZPhd9/ExU70VqX3311Ve625yqvqjuvqldFJNfEqh995e//EXPpJW6jOuuu+46/WXv1ltvrfdVsmur6vqrupEpHFeIg7PhRTXG2/ZfVde7devm5bSsuVLTILbdf8j+OEuOUwDHWXvOPvts3RXjxRdf1NNFZ3NcqYaCb1O+q+CiGpbvvfeevPLKK2lftLSH9y/RAU51hVJdyBR13KjKQqbjyvf3qv333193/+0omKjxjNXV1V59LqpQq7phqmpe8qJej6o73fXXX6+XSb4eOa4QJWfDixqI+Nxzz+kXWtJTTz2lb0e6tgOn1dz2aryG6o6Bzo8z1SdYfdOZepyprhrbbLMNu6+D40xRDXcfj7NzzjlH7rvvPv34Mx0n6rhS35yr8welHleqypBadXCdev9WYzfefvttPd4g0+QZbY8r9U2xCjq+HVdt94Pad2qgebKbWElJiR6Yrqp8Ser4Ug12nz4XVVeotuNe1cB8NcFKcl+pSWsaGxvTllGVBtVFyqfjSk0+k6y4JC/qOLrgggv0Fy+KqsqogJN6XKmJM1RFy6fjCtEqdblx8H//939y1FFH6dlV1GwYqkH+t7/9Le5NM476QDv44INl8ODBehDjtddeKz179pTf/va34js104zqZpHsF50smydPgKe6Hqh+9aqLwejRo2X27Nl6RhbVxcCnRqaiZoBSYzjUiRXVT7Wv1De9akxVcuYo1ShQ+0ydBPWBBx6Qd955x6tuGMqll14qN910kz5GVJed5DGlGgADBgzQv6vX3h133KEnNzjttNN0w12NF3rttdfEJ7/+9a/lySef1PtCvbbURVl33XX1SRgVNf5FVRjUcaaCyw033KBfe+ozwCfq8auxPqrrWH19vR778+OPP+oKX9Kf//xn2W233fRte+65p/48VK/RM844Q3yhJrxQ79mnnHKKnoVUfVl366236sqKOt4U9V6vvihQ19VrUn1BNWbMGH1b6ngrrDyhtfrMU11gVddX9dq8/PLL9fHl25gzRKdIzZcsjpoxY4ZuiKvpf1VZXL1ht+2egZVv5rfccov+tlLNmqXGIqjGU9szDvtIhZJMUx6nTmurvrVTDabXX39d7zPVxUXdzzfqg73tNNsqBD/xxBOt3wSrWexUNxb1LbFqFJx55plZdQNyiWo0qQHmmb5EuOKKK1qvq24Y11xzjUydOlUPEFb7SnUF8slBBx0k8+fPX+V2NbOYaiwpqqGuGp8qPKtuLqqSpRrjqouPT+bOnavfx9UXLmq2JzWxwemnn65fg6k++OAD+etf/6qDoOoqpboCrbfeeuITNXZK7Ss1vbY6TlTwUxNnpI47S11GfbGgGuNqUg1VefCZep9XFRnV1S6Vep9X7+9qXJAKyGq6ZDVVNxAGp8MLAAAAAHc4O+YFAAAAgFsILwAAAACsQHgBAAAAYAXCCwAAAAArEF4AAAAAWIHwAgAAAMAKhBcAAAAAViC8AECIRowYoU/MmQ114sUhQ4boy5dffhn585L832qbAQAwEeEFAEKyYMEC+fe//y29e/fOankVWNRZz2+//Xbp06dP5M+L+r/qrPaffPJJ5P8bAIBslGa1FAAgZ5MmTZKysjLZfPPNs77P2muvrasfcVD/d9q0abH8bwAAskHlBQBCoioYKriUl5fr63PnzpVDDz1ULrroImlubs5qHfPnz2/tzrXTTjvJL3/5S3n99dc7vM9TTz0lBx54YNptn376qV5HY2NjAY8IAIB4UXkBgBDDy+DBg/Xv77zzjhxxxBE6fIwZM0aKi7P77qimpkZ351JU8Hj77bdl+PDh8uKLL8quu+6a8T4//fSTfPbZZ2m31dfXy0cffSQtLS0FPy4AAOJCeAGAEMPLKaecIrfccotceumlcscdd8hhhx2W0zpKS0vTupHtvPPOMnPmTLntttvaDS8AALiK8AIAIWhoaNDjR+666y5Zvny5vPHGG7LFFlvkta6JEyfKvffeK99++60sW7ZM5syZI+utt17g2wwAgOkY8wIAIZgyZYoe17LGGmvIBhtsIP37989rPWq2sv33318GDRoko0eP1l3I1HgWFWIAAPAN4QUAQuoytvHGG8v48eN1N68zzjgjr/Wo+x988MFywQUXyJ577qm7kC1evLjD+6y++uqydOnStNt++OGHvP4/AAAmIbwAQEjhZauttpLu3bvLM888I48++qjceOONOa9HVW6mTp3aWmlRM409/PDDacuosTR77LFH6/Utt9xS5s2bJ6+99pq+vmjRIrn++uvT7qMmEFBBSHVFAwDAFoQXAAgxvCibbrqpPPbYY3LhhRfKc889l9N6zjrrLD0zmRrjsskmm8hRRx0l++67b9oys2fP1ueUSVJja1SlRi2npmpWJ75MbktSbW2tnn2M7mcAAJsUJRKJRNwbAQCumTx5sqyzzjrSo0eP1tu+/PJLPQ5GhYlMRowYoUOPCioPPfSQDj2Kmt54xowZsmLFCh1gVFVFnf8lefJLFV7UOWS23nrrtPUtWLBAT5vcr18/PWmAmkBg2223laKiIqmrq9PbM3DgQKmsrNTLq0qMuo/qcvbjjz+GuHcAAMgP4QUADPHVV1/pikiyetKlS5dI//+HH36of5aVla1SqQEAwASEFwAAAABWYMwLAAAAACsQXgAAAABYgfACAAAAwAqEFwAAAABWILwAAAAAsALhBQAAAIAVCC8AAAAArEB4AQAAAGAFwgsAAAAAKxBeAAAAAFiB8AIAAABAbPD/ADgD2QHoX/IqAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "spectrum: (50, 8)\n" - ] - } - ], + "outputs": [], "source": [ "omega, kvec, spectrum, _ = out.em_fields.e_field.struphy.analysis.dispersion(\n", " slice_at=(None, 0, 0),\n", @@ -11632,7 +495,7 @@ }, { "cell_type": "markdown", - "id": "reductions-md", + "id": "39", "metadata": {}, "source": [ "## Reducing distribution functions\n", @@ -11642,38 +505,10 @@ }, { "cell_type": "code", - "execution_count": 19, - "id": "reductions-average", + "execution_count": null, + "id": "40", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('t', 'v1')\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "f_of_v = phase_space.struphy.analysis.spatial_average()\n", "print(f_of_v.dims)\n", @@ -11682,7 +517,7 @@ }, { "cell_type": "markdown", - "id": "reductions-moments-md", + "id": "41", "metadata": {}, "source": [ "`.struphy.analysis.velocity_moments()` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean velocity `mean_v1` and the variance `variance_v1` along each velocity direction, all as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. For a `delta_f` product only the density (its perturbation) is returned, because a mean and variance of a perturbation are not defined. Where the density is not positive, mean and variance are NaN." @@ -11690,50 +525,10 @@ }, { "cell_type": "code", - "execution_count": 20, - "id": "reductions-moments", + "execution_count": null, + "id": "42", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Size: 79kB\n", - "Dimensions: (t: 101, e1: 32)\n", - "Coordinates:\n", - " * t (t) float64 808B 0.0 0.05 0.1 0.15 0.2 ... 4.85 4.9 4.95 5.0\n", - " t_seconds (t) float64 808B 0.0 1.668e-10 ... 1.651e-08 1.668e-08\n", - " * e1 (e1) float64 256B 0.01562 0.04688 0.07812 ... 0.9531 0.9844\n", - "Data variables:\n", - " density (t, e1) float64 26kB 1.001 1.189 1.501 ... 0.6874 0.7502 0.7503\n", - " mean_v1 (t, e1) float64 26kB -0.03906 0.3207 ... -0.07846 -0.1825\n", - " variance_v1 (t, e1) float64 26kB 1.06 1.34 0.952 ... 0.7471 0.5391 0.9026\n", - "Attributes:\n", - " run: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1)\n", - " run_name: vlasov_ampere_demo\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "moments = phase_space.struphy.analysis.velocity_moments()\n", "print(moments)\n", @@ -11747,7 +542,7 @@ }, { "cell_type": "markdown", - "id": "units-md", + "id": "43", "metadata": {}, "source": [ "## Physical units\n", @@ -11757,39 +552,10 @@ }, { "cell_type": "code", - "execution_count": 21, - "id": "units-si", + "execution_count": null, + "id": "44", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1 length unit = 1.0 m, 1 velocity unit = 2.998e+08 m/s, 1 time unit = 3.336e-09 s\n", - "m/s s\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj8AAAHFCAYAAAAdTZjVAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAQ+NJREFUeJzt3Qd4FFXXwPGTEHoLvdeAIAhRESmiKMUCKHZULKAi+uorCDZUELCABayviAqKHRUUC4oKoqDSpAhSRESkSK/Sk53vOVd3v2xI2Zmtk/n/nmeMOzszmZ3dzRzOPffeJMuyLAEAAPCI5HifAAAAQCwR/AAAAE8h+AEAAJ5C8AMAADyF4AcAAHgKwQ8AAPAUgh8AAOApBD8AAMBTCH4AQEQefPBBSUlJCSx2TZ48OWj/JUuWcF2BBEXwA+ThpZdeMjeydevWxeU6LV68WO677z5p2LChOY/777/f9jFWrFghV111ldSoUUPKlSsnZ599tsyePTusbZctW2a2q1evnpQqVcqc36233iobN26USDt8+LAMHjxY6tevLyVLlpSmTZvK888/LzkNTh/OeWVmZprl0KFDZrHroosuMvu9/vrr5jgMng8kLoIfIA8+ny9uNzK9Yffq1cvcxPVm77852zFjxgw55ZRTpFq1aiaI0WM+8MADMmLECMfb/vrrr9KqVStZu3atTJkyRf766y8ZN26cTJ06Vdq0aSP79u2TSNJgZuzYseZ3bNmyRYYMGSJ33nmnDB06NCrn5TTzk5SUZPZLTubPKpDwdG4vADkbM2aMRj3W2rVr43qJVq9ebc7jnnvuCXmfvXv3WlWqVLFuuummiG47bNgwcy4//PBD0Ppx48aZ9ZMnT7YiZcaMGeaYeuys7rrrLqtw4cLWhg0bInZe999/v9kuXO+88445zqJFi8I+FoDo4J8oQC60iUmbTFSDBg0CGYEvv/zSFdfs7bffNpmSfv36RXTb0qVLm5/ZMxz+x/7n/TQDc8stt0idOnWkRIkS5lpq9ubIkSP5/q5JkyaZjIo2KWV1ySWXyNGjR+Wjjz5yfF527Nq1y1ybtLQ0c5wmTZqY7NPOnTsdHxNA/BD8ALl46KGH5LnnnjP/v2rVqkAtSOfOnXO9ZlrkmrXoNa9l9OjRUb32M2fONE1mK1eulFNPPdX8f9WqVeXqq6+WP//80/G2vXv3lkaNGsldd91lmpo0CJk/f748/PDD0qlTJ+nQoUNgW206a9mypSxYsEDeeecd2bRpk7z44ovy6quvypVXXpnva/j555+lcuXKpv4oKw0+/M87OS+7tPlRm8/0NWgwp81qNWvWNDVhANyH4AfI7cuRnBzIGhQqVCgQtGgmIjdaG+Svzclv0XqiaNqwYYMcPHjQ3LjvuOMOE8RoJmXu3LmmBmbbtm2Otk1NTTXZL91eg40iRYqYgEn/X/fJmnnRYu29e/fKJ598Im3btjX7aiDywgsvmN5RP/zwQ56vYfv27Waf7DT7or9nx44djs7LLq2HuvjiiwOBoRZS9+/fX+69917HxwQQPwQ/Nhw4cEDGjx8vZ5xxhrRo0cLxRdd/OWoKvV27duZYTz/9tO1CViSmE0880WQcQlkGDhwY1XPxB1l33323ybKUL19eTjvtNHnllVdMBsaf1bK77erVq6V169ZSrFgxWbhwoezZs0e+/fZb+f333+X00083wY6fBj36GdcsUlYaAGkQqRknzaplz4p9/PHHgW1zCzZ1fdZCdDvnZVezZs1M8bRmraLRow1AbBH82KBp81mzZpkb3C+//OLogmuNgP7rcc2aNaYXjf7L8eWXX5bbb7/d0fGQeEJt9sorgxQJFStWND/bt28ftF6D7sKFC8u8efMcbTts2DBTH/TBBx/ISSedJGXKlDEBjv7DQJuh/IGSdlHXWpkvvvjCBCRFixY1i2ZkdB8NXDSzk1O2zJ8V0yBs9+7dx7w27bml21WoUMH2eTnx7rvvmuZObVLT5i6tW9L/p+YHcCf7/Tk97JtvvpHixYubfw3nRlPuWsuhf/CVdr3V7sL+1L2m+rUJYeLEiWbMEuX/I33PPfdI7dq1Y/RqEA1a8xNqVvDxxx+XAQMGRO2NaN68ucm8ZA+y9HH2dXa21bqgKlWqmCWr9PT0wFhBSgMdbSLq2rWrvPnmm3k2LWomLCttZvRnXLRpTAOgrM1feg7+5+2elxP6vdR6Hz1PHXtJ638ee+wx0yz43XffOT4ugPgg82ODBj550X+J6h96/WOovVlGjhxpaic0sPH/cddUvPZ48Qc+qlKlSuZfv1pXgMTif59C6ZmUaDU/l112mfmZfZDCOXPmmNejgbmTbWvVqmUyLJq1yT7AoMoawOv3QT/XmqnJKfvlr8PJLSumdTZ6TbM2g6kPP/zQbNe9e3dH5+WUZsG0gFtHg7722mvN9aLJGnChKHWhL9Befvllq2jRosesnzRpklW5cmXr0KFDgXVHjhyxKlWqZH300Ufm8YIFC6zk5GRzDJWRkWH17dvXjAui45Qgsfz000/mvXnmmWfMexWKo0ePhrT4fL6IjfOzbNkyq1ChQtbtt98etF7H7Slbtqz5/B04cMBauHCh1aRJE6tWrVrW9u3bHW07c+ZMKyUlxerUqZO1cuVK6/Dhw9a8efOsZs2aWampqUFjIv3xxx9WtWrVrDZt2lizZ8+29u/fb46lx7jiiius+fPn5/vau3TpYlWtWtWM36PXbcqUKVbx4sWtu+++O2g7O+dlZ5yf3bt3W507d7Y++eQT66+//jLnoNcmLS3NOu20047ZnnF+gMRH8BPB4Kd///5WyZIlrfT0dKt58+bmj64uxYoVsx5//PHAdi+99JL5Y1yjRg2rQoUKVp8+fcwAc0OGDAnv3URUDB482Lw/GrRqgDFt2rSYXWm9werv1EVvzElJSYHH06dPD2y3dOlS8/ytt94atL8GbI8++qhVr149c/7ly5e3evbsGTQ4oJNt586da3Xr1s0ENhpw1K5d27rmmmus33777ZhtN23aZN1yyy0miNLz1n8MdOzY0Zo4cWJIAaUGTAMGDDABkO6v5/fYY49ZmZmZYZ2XnUEOv/rqK+vCCy8051CkSBGrbt265vuePYBUBD9A4kvS/8Q7++Q2WvNz2223HTP/T9++fc14JhMmTDhmH61D0OYtP23yWL9+vWlW0ea0smXLypgxY6RPnz4xeQ2wz9+kpfUo0S5W9strao3s55GRkRHUPR/2aG3eI488EvZUJlocrT3mFi1aZDpHAEg8FDxH0HHHHWcKmXVSxaw1PTnRG5SOeKt0H/2Dm9fgeYg//9xNseQv/A1FrM+toPJfRw0m7dDODJdffjkTmgIuwD8RI0gLIPUP580332zGBMra+0vHIPHTx/7ntYeKjq2i+9StWzeSpwPAhuHDh5uOCeHO6q5d/PU4ZH2AxEXwY4OO63HCCSeYnh7aA0b/Xxf/EPvarPXVV1+ZgEbHJ9EMkA7upoMa1qhRI2h0Ws366DxBOou29rR55plnIv/uAgiZZmOz9jhzmhl0uj/gFpZlyU8//RQ0vUx+dMwvbQrO3hszXqj5sUGnAMhpwDUNYrJ3g9+6davs37/fBDk51WDovwy1G7wGRToAHAAAiczn85mWi7Fjx5ogRqeO0eEw8qIDAmviQMfE0vuhzrt37rnnymuvvWbGAYsXgh8AAJAvLeMYPHiwKdPQaZm0g09+wc9HH31kBjw977zzzGNtCdF5/vSxzvEXL+RmAQBAvrSF48knnxQ7LrzwwqDH1apVM+umT58u8UTwE2KqTyd31FqdWHVxBgC4sx5GRzSvXr16VIed0OL6UEeeD+Wck7Ld2/xz8UWDZox0frx4IvgJgQY+OnQ+AACh0HHcdBLcaAU+9eqUks1bMyNyvFKlSsnff/8dtE479gwdOlQi7fnnnzdNZd9//73EE8FPCDTjo9pJF0mRwtF+TwAALpUhR2W2TA3cN6JBMz4a+Kz7qa6UKR1edmnvPp/UafGHCdZ0km2/aGR93nvvPTOZsw4UfOqpp0o8EfyEwJ8O1MAnJYngBwCQi38HCI9FiUSp0klmCYdP/tlfA5+swU+kffDBB3LNNdfIiy++KNddd53EG+P8AADgQpmWLyJLJOnI6LNnz5Zt27YF1k2aNEl69uxpenddf/31kggIfgAAQEgWLlxoghvtsq6F3fr/uvjnxNOx8E4//XSZNm2aeaw/da47zfrouED+7XXcn3ii2QsAABfyiWWWcI9hx6hRo2TdunXm/8uVKyf33nuv+f8ZM2ZIkSJFpHDhwnLaaadJ5cqVzfrffvvN1PfozAf+bVVqaqp8+umnEi8MchiCvXv3mlnXz5Tu1PwAAHKVYR2VmTJF9uzZE7UaGv89adOqmhEpeK7eaENUzzcR0ewFAAA8hWYvAABcKNOyzBLuMbyI4AcAABeKR81PQUHwAwCAC2ngkknw4wg1PwAAwFPI/AAA4EI0ezlH8AMAgAtR8OwczV4AAMBTyPwAAOBCOitXuDNz+cSbCH4AAHChzAj09sr0aFd3mr0AAICnkPkBAMCFMq1/lnCP4UUEPwAAuBA1P87R7AUAADyFzA8AAC7kkyTJlKSwj+FFBD8AALiQz/pnCfcYXkTwAwCAC2VGIPOT6dHMDzU/AADAU8j8AADgQmR+nCP4AQDAhXxWklnCPYYX0ewFAAA8hcwPAAAuRLOXcwQ/AAC4UKYkmyW8Y3gTzV4AAMBTyPwAAOBCVgQKni2PFjwT/AAA4ELU/DhHsxcAAPAUMj8AALhQppVslvCOIZ5E8AMAgAvpjOy+MBtwfOLN6IfgBwAAF6LmxzlqfgAAgKeQ+QEAwLM1P5Z4EcEPAACurfkJc2JT8eY4PzR7AQAATyHzAwCAC/kiMLeXj95eAADALaj5cY5mLwAA4Ck0ewEA4NJmLwY5dIbgBwAAF8q0kswS7jG8iGYvAADgKWR+AABwocwI9PbKpLcXAABwC5+VbJbwjmGJF5H5AQDAhcj8OEfNDwAA8BQyPwAAuJAvAr21fOJNBD8AAHh2nJ9k8SJvvmoAAOBZZH4AAPDs3F7J4kUEPwAAuJBPkswS7jG8yJshHwAA8CxXBT8+n08+/fRT6datmzRo0EDmzp0b0n5TpkyRjh07ygknnCBXXnml/Pbbb1E/VwAAYtHsFe5i9z78+eefS+/evWXw4MEh7WNZlrz55ptyww03yG233SazZs2SeHNVs9c999wjy5cvlwsvvFA+++wzOXjwYL77aLB06aWXyhNPPCFt2rSRp556Sk4//XT55ZdfpHz58jE5bwCwrXW6ve3nLOEie0xkBjlMDnnbw4cPS9OmTU3y4cCBA7JixQp56KGH8t2vT58+MnXqVLnzzjtl27Zt0qFDBxk/frxcc801Ei+uCn4eeeQRKVKkiGzYsCHkfYYOHSpXX3219O/f3zx+/fXXpVq1ajJmzBi5//77o3i2AAAUHIUKFZKvvvpK6tWrZzI4CxYsyHefxYsXy7hx42TmzJnSvn17sy4lJUUGDhxoWmL0/+PBVc1eGvjYsW/fPlm4cKGcc845Qcfo1KmTeSMAAHArn5UUkSVUGqho4GOHZnwqV64sZ5xxRmBdjx49TAZo3rx5Ei+uyvzYpRkibWusWrVq0Hp9/PPPP+eZ2tPFb+/evVE9TwAAnAxQmBmhQQ73ZrvPFS1a1CzhWrNmjdSqVUuSkv4/yKpTp475+fvvv0vbtm0lHlyV+bFLC7NU4cKFg9Zr9iczMzPX/UaMGCFly5YNLPrGAQCQiLO6h7sovc9lve/pfTASDh06JKVKlQpaV7JkycBz8VKgMz8VK1Y0P3fs2BG0fvv27YHncjJo0CAZMGBA4LFGxARAAICCav369VKmTJnA40hkfZQGUkuXLg1at3PnTvMzNTVV4qVAZ36qVKligpYff/wxaP33338vp5xySq776ZuuH4KsCwAAiSRTkiKyqOz3vEgFP82bNzdNX1l7Z/uDoWbNmkm8FLjg57777pMuXboEHt98883yyiuvyMqVK83jl156ybQzatc7AADcKpLNXpGiHY2uuOIKmT17tnl80UUXmZ9jx441P7UOV4ecadGihTRq1EjixVXNXpMnT5a77747UK/Ts2dPKV68uNx+++1mUVu3bpU///wzaGwgLXxOT0837YzaVU8HW4pnxAkAgBsNHDhQNm7caHpSa/OVBjrqjTfeMPW12llo4sSJZjDidu3amRYYTUBowkHv4VqGsmfPHvniiy/i+jqSLA3DXEIjyi1bthyzXgcr9A9YqN3ntIgqe43O/v37zRulY/zYHVdAa3603fJM6S4pScHF0wAQFQxy6EoZ1lGZKVPMDT5aJRP+e9KQuZ2kWKnw7kmH/j4qw1t9HfL5atf1nHpAX3755ZKcnCxHjhwxQY4OKuzv1eVPTMyZM8ckLDQo0p/x5KrMT+nSpc2Sl0qVKuW4XrM+/gpzAADcLhLNVj6b+2ctK8mJ9qb2Z4Oy0rF+LrjgAkkUBa7mBwAAoMBkfgDAM01MzNWFfDiZmDSnY3gRwQ8AAC5kSZL4/u2qHs4xvMibIR8AAPAsMj8AALgQzV7OEfwAAOBCdmdlz+0YXkTwAwCAC2VGYFb3TI9Wv3jzVQMAAM8i8wMAgAvR7OUcwQ8AAC7kk2SzhHsML/LmqwYAAJ5F5gcAABfKtJLMEu4xvIjgB0DBmhbC7jkBLkXNj3M0ewEAAE8h8wMAgAtZVrL4wpyY1GJiUwAA4BaZkmSWcI/hRTR7AQAAT6HZCwAAF/JZ4c/N5bPEkwh+AABwIV8Ean581PwAAAC38EmSWcI9hhdR8wMAADyFZi8AAFyIEZ6dI/gBAMCFqPlxjmYvAADgKWR+gByseaqNreuSNvFAwZjjyu552d0+Ec/Jy/OmJeLcbLBX8BxuV3fxZsEzwQ8AAC5kRaC3l+XR4IdmLwAA4ClkfgAAcCFt8gp/hOck8SKCHwAAXIjeXs7R7AUAADyFzA8AAC5Es5dzBD8AALgQc3s5R/ADAIALkflxjpofAADgKWR+AABwITI/zhH8RMmOvm1tbV9h7A8SdbGYWiAWYjAkv+3pKgqIqH9umYIheteqoHy/ETKCH+do9gIAAJ5C5gcAABci8+McwQ8AAC5kRWBWdku8iWYvAADgKWR+AABwIZq9nCP4AQDAhQh+nKPZCwAAeAqZHwAAXIjMj3MEPwAAuBDBj3MEPwAAuJBlJZkl3GN4ETU/AADAU8j82NHyBJGUYokzV5dX5/GJxeuI9u+IxTxMTl5DetuEm2ctFu93Qs7F59Xvdyw+U9E8p4xDIvOnSCzoAIfhDnLoC3N/tyL4AQDAhaj58Vjws2nTJtm6das0aNBASpUqlee2a9eulS1btgStK1GihDRv3jzKZwkAQMF08OBBSUlJkcKFC4e8z6FDh8w+usSbq2p+Dh8+LD169JC0tDS57LLLpEqVKjJmzJg893nssceka9eu0r9//8Ci6wAAKAgFz+Eudixfvlxat24tZcuWlZIlS8oVV1whe/fuzXOf119/XerXry/ly5c3++j+c+fOlXhyVfDz0EMPyezZs+W3336T1atXmwt66623yvz58/Pc76yzzpI5c+YElrfeeitm5wwAQDSbvcJd7GRuNJmgrS67du2S33//XX7++Wfp27dvrvssXrxYevXqJXfffbfs379fdu/eLccdd5ycf/75kpmZKfHiquBn3LhxcuONN0qNGjXM40suuUSaNGki48ePz3O/I0eOmDdo3bp1YllencMWAADnPvnkE/nzzz9l1KhRJoNTs2ZNGTJkiLz33nvHlJf4rVq1ytx3NQBKSkqS4sWLy5VXXinbtm2THTt2xO3tSHZTnc/mzZulZcuWQetbtWolixYtynPfqVOnmtTcySefLPXq1ZNp06ZF+WwBAChYzV5z5syRhg0bmpITv/bt24vP58u1Beacc84xTV4aJP3xxx+ybNkyGT16tFx66aVSuXJliRfXBD87d+40P7XNMKsKFSrkGT127txZNmzYYNopNTLVbJEumq7Lq7ZI2zCzLgAAJBIrAk1e1r/BT/Z7nt4Hs9NsTaVKlYLWVaxYMfBcTlJTU+XVV1+VCRMmSOPGjSU9Pd00fT3zzDMST64JfvwV5dnfEK04z6vaXAOdqlWrmv/XCnMtdtafU6bkPg7DiBEjTDGXf6lVq1bEXgcAAImmVq1aQfc9vQ9mp81W2et0NOujkpNzDicWLFggZ599tqnZPXDggOzbt0+aNWsm7dq1MzVA8ZLspjdGL642f2Wlj+vUqRPycTTw0Uh148aNuW4zaNAg2bNnT2BZv359WOcOAECkaQWrlrGGtcg/9D6X9b6n98Hsqlevfkxtj5ajqGrVquV4jtrBSJu9brrpJnMP16FmRo4caYah+frrr+P2oXBN8KMXTLvHacGVn0aRevE6duwYWOevPldaZKXV6VlpTzFtd2zatGmuv6to0aJSpkyZoAUAgETiH+E53EVlv+fpfTC7M844w9xjs5aNfPnll1KkSBFTf+u/727fvj3QSqP3bm2hycqf8dGi6XiJ/0hDNjz88MMmfTZ48GBp06aNPPvss6YGSCNKv0cffdQUZWlR1dGjR6VFixamG54GO1qlrsfQNketNrdt/jKRpNAHdPLiMPCxmCbA9u9YEr/UqmuG5P/X7sZ2e0Pa++NVYY4UiM85omdHegw+U3bZ+UxZR6N5JnGd2PScc84x99TevXvLc889Z4IcvR/rkDPaVKa0Blfrgt544w25+uqrzZh8TzzxhAwcONBsp81ed955p+l8pAmNeHFN5sc/Xs/06dNN1zmt3dGxBr7//vugzIwOgKjBjdJoVHt2acGzptk+//xzM8jhjz/+KMWKhTZHFwAAENNspb2n69atK926dZNbbrnFJBcef/zxoG20I5L/HnviiSeaFpoVK1aYVhrtea3NZ3ovz2+GhmhyVebHn3bTJTfZ2yl1HIKsbwwAAAWB9tZKCjPz47O5v3ZP155budHWGM0I2blvx4Prgh8AAPD/RcvhsDw67q+rmr0AAADCReYHAAAXinXBc0FC8AMAgAsR/DhHsxcAAPAUMj8AALhQPHp7FRQEPwAAuBC9vZyj2QsAAHgKmR8AAFyb+Qm3t5d4EsFPonAwD5Nda55qY3uftDt+lER73bbnA4vBtY32/GRO5ihb06NE9N9vm9c2Fq/bCSdzzEX7M2h3jisnbH+mrihua/u0/vYn3krIufjsvH8Zh0TmT5FYoLeXcwQ/AAC4kCZtwk3cWOJN1PwAAABPIfMDAIAL0ezlHMEPAABuRLuXYzR7AQAATyHzAwCAG0VgYlNhhGcAAOAWjPDsHM1eAADAU2j2AgDAhejt5RzBDwAAbqT1OtT8OELwkyDD2TuaimDigcSaqiJGUzBID/vTdNhl91o5mbbBtjlLbG2eZn9mAdv2DLH3GazQxd5rcHJtK0j0pzPpNn6mre2f+dLetBAq7d39UZ8Ow+4+qSsk6t+L3Y2tqH/O7U71Y/dvbaxQ8+McNT8AAMBTyPwAAOBGDHLoGMEPAAAuRMGzczR7AQAATyHzAwCAW9mrD8e/CH4AAHAhmr2co9kLAAB4SkiZn23btsnGjRtDPmjlypWlevXq4ZwXAADIC729ohv8TJgwQe66666QDzpw4EB58sknnZ8VAADIh87oHuas7hLu/gW85uc///mPDBs2LN/tXnjhBdm7d2+45wUAABC/4KdevXqSmpoqFStWzHfbpk2byq5duyJxbgAAIDc0e0U3+LnkkktCPqCdbfH/UlfaTz3anssnvW3Cze3lRMVG221tX3a4/fnD1jzd2tb2af1/iP5cYDbni7M7F5ijebSG74/ua9DfMfaHqP8Ou3PMTXimq63t0xx8L6ZNft3W9udcfG3U/4bYfS+cfM7t/i108jvSJkb/71RMEPzEt7fXhg0bZPbs2XL48OFIHA4AAIQ6q3u4iwc5Cn569Oghc+b8M5Xu3LlzpWHDhnL66adLu3bt5OjRo5E+RwAAgPgFP5rh2bp1q7Ru/U+zwKhRo0wx9M6dO8WyLPnwww8jd3YAACBHlhWZxYtsj/C8dOlSU9SsNNiZMWOGyQKVK1dOunfvLsuXL4/GeQIAgKyo+Yld5qd8+fKybNky8/+zZs2SEiVKSIMGDczjLVu2hNQjDAAAwDXBT5cuXWTlypVy0kknycUXXyx9+vQx67XWZ/r06dK1q71eEAAAwAEKnmPX7FW6dGlZsGCBTJo0SapUqSKXX365Wb969Wq5++67zZhAAAAgupKsf5Zwj+FFIQc/n3zyifz666/SrVs3adSokfTr1y/o+SZNmpgFAACgQDR7VapUyRQ3a3OXdm2/4447TDMXXdsBAIhjwXO4iweFHPxo1/bPPvtMduzYIaNHj5YDBw7IddddZwqcL7vsMjP5qc7+DgAAYoCan9gVPBcvXlzOP/98GTt2rBnZeebMmdK8eXMzoWn16tWlTZs28sgjj8iaNWucnxUAAECiFDxnp81gugwePNgMfqjZoU8//VQyMzNlyJAhUpCsfaylJBcrFtK2af3/GQE7VClDGto+H7tzVtmeCywWc3U5qLbLmFLJ5h72X0PauwejOyeWg+tqd/6pVAdzuV3X7zNb2396/ZlR/wxeN97eRMmfXm/7V0jaHT/a2n7NU21sbV/B/jRr0nrxpba2L2v/V0hKd3vZ+jWN7b1uJ20qduf22t3YSbtNFOc0s2I4ywHj/MQv+MmqcuXK0rt3b7MAAIAoIviJ7dxe7733nrRq1coEO6mpqUGLZoAAAECUUfAcu8zPwoUL5dprrzW9vbTWp3DhwkHPazd4AACAAhP86DxeOrDhiBEjonNGAAAg9N5e4bDC3N8rwU/VqlXl8OHD0TkbAAAQEkZ4jmHwo3N7DR8+XN544w255JJLzMSmsfTXX3/Jm2++aSZRbdasmVx11VXHNL1FYh8AAHCs+fPnm2FudOibCy64QGrXri350bEBtSf4H3/8ISeffLJ06tQp120ty5KkpKTEKnguVqyYnH766abup2TJkpKSkhK03HPPPdE5UxEzvYYGL998842ZY0zHEzr77LNNt/pI7gMAQMKLQ8HziBEj5KyzzjLzeeosD40bNzazP+Rl+fLlcvzxx8uoUaNk586d8txzz8nNN9+c5+947LHHzP8fPGhv2JGoZX70xb700kvy8MMP51jwHM2JTTWwOuGEE8xYQhoV3nDDDZKWliZvvfWWCcYitQ8AAAimgxdrj+633347MKn5TTfdZBYNhnLK1mRkZMhFF10kp556qukp7t9m6dKlkhvNKGmLjdKASafRGjZsmMQ1+Fm5cqVpNrr//vsllvTFf/755/Lss88GLl7NmjXlzDPPlClTpuQYyDjZBwAAHEvvm9qCcvHFFwfW3XjjjfLyyy/L4sWLzYDH2ek9WFtgPv7446DgSFtkcnPiiSfKU089Je3bt5e9e/dKkSJFJNJsBz+a2dEmpFj7888/TaF19sxS/fr15fvvv4/YPkr3yVrUrRcfAIBEkuRskPwgSbnc54oWLWqWrFatWiV169Y1JS5+OtG5/7mcgp+5c+dKlSpVzDygL774omnGatGihZxxxhmSG21W07lDe/bsKXv27DETq2umSIOi9PR081Pv6+HUBdkOfjRrok1JOreXpr00CswqOTnZLJHmb/fL/vvKlCljCqkitY+/vTGnFFvZVclSqEhor23aJnvj2Z9TXaLPwXQHTqYjsCOtv41h4x2yO/WE7eHsdXtJt/cL5tif78DudBV2X4PRL7E+H06m0Cgotv9awdb2ZeVg1KfIyUiPfrfo3cf7bG2fuiI56r9DbPwNyTxySGT8FHFbV/datWoFrX7wwQdl6NChQev+/vtvKVs2eCIV/2N9Lie7d+82MYHWCuuiWZyHHnrIdJ7Sjki50W10iqw777xTVqxYYYqqNfHyzDPPmIBI7+NaeqNjDmbNREUt+NEJTJctW2aKlXIqWBo4cKA8+eSTEmmlSpUKXMisdu3aZS5CpPZRgwYNkgEDBgQea0Sc/YMBAEBBsX79+qD7Yvasj/+eqoFHVv7H/vttdtoxSut3NGGik6IrLTnRGiCtwdUsT260pvjKK6809/Bzzz03sH7t2rWmmW3JkiXi89kMZJ0GPz169JBTTjkl1+ejFSRo1KcXVyPArBdBHzdp0iRi++SW7gMAoKDO7VWmTJk8kwL+GRy0aFmLmP1NX1ro7H8uJ9rLS5122mmBdS1btjQZIG0qyyv4Ua1btz5mnTZ56aKF1E7ZzhdqcKNNX7kt2pMqGjRtdtlll8lrr70WaM7SyO+HH34wAZmfVqE/+uijtvYBAMB1YtzVvXv37rJv3z6ZPHlyYN0rr7xi6mi1DkdpSYk2l/3888/mcdeuXc14gFnrbHWcoCNHjkjTpk0lXkLK/OzYscP0nNLRnSO5rV0jR440AZYWS+mF/uKLL6RXr16BVJrS8QZ0Co777rsv5H0AAHCbWI/wnJaWZgY5vv766+Xrr7+W7du3m3vqJ598Eig+1uBHa2YbNGhganK0WFl7XF999dWmCUubsnSomT59+pgaoIQOfl599VXZvHlzSLU8dra1S2eRX7RokUybNs2M1nz77bcfkxLT6vCOHTva2gcAAORPEws6OrOO8Kzd1bVLep06dQLPa5ZHi6U18PHT2p62bduaQElbZKZOnRr3+3DINT+zZs2Se++9N9/t5s2bZ4aujhatxdHhtHOTU/thfvsAAODlmh87tFhZl5xo8JO9l5i/9sdf/5MIQgp+NKrT7uILFizId1uN6o477rhInBsAAEiw4KcgCCn40aJhXQAAANzOdld3AADgvYLngoTgBwAAN4rgCM9eE/l5KAAAABIYmR8b9hxnSXKx0HKEDSYeO/VHnp4S29Im5j4/WU4qLNlv+3dEfe6m1jbnxHLA0RxXUb5Ou3u0sf07UldGf06zCc9Ed46ktHftzz9ldx60bst3RX3+MLvfvT1D7G1v/Frc1ubdxs+0/SsmPNNVoiml+zbb+6ROqWRreyd/1yqMXRK1v1MZGYckZih4dozgBwAAF6LmJ8GavXRQwazDXwMAABTo4EdnWv3444+jcWgAABCHub0KkrCavSZOnCj9+vU7Zr1OIhrObKsAACAfEejqLgQ/9mmQ06FDB3nhhReC1uvM6jrNBQAAiBIKnuOT+SlXrpyZyj41NTVofePGjSUjIyOcQwMAACRG8LNt2zapWLGimb6+e/fuZslOs0G6AACAKCHzE7uC5wkTJkirVq3MdPYAACC+Xd3DXbzIdvBz4403mqxOly5dpGvXrrJ06dLonBkAAEAiBD9a3zNy5EhZvXq1VK1aVVq0aCG9e/eW9evXR+P8AAAAEqPguUaNGjJu3DgZOHCgDBo0SBo1aiT//e9/zf9nL4AuKOpNOiApKfaG84/WEP4xmxrCwdDxiTaVRAWJ/nWyK3VlYk4maHcqkAqSeJxM2VBB9kf1+5oxxf5UI3b/in7av5zt31GhdXRftyxx8t1LvL85tl63dTSaZ5Ltd0Wgq7olnhT29BZNmjSR999/X1599VUZMGCA+Hw+eeKJJyJzdgAAAPEOfjZu3GiKnZcvXx5Y1qxZI5mZmaYXWO3atSN9jgAAIBvm9oph8PPOO+/IkCFDTManWbNm0rdvX/NTF60BAgAAMeLRZqtw2Q5+brnlFtO8lZwclWnBAAAAEiv4KVnSXrEpAACIAgqe41fwDAAAYo+aH+cIfgAAcCMyP45RuAMAADyFzA8AAC5Es5dzBD8AALgRzV6O0ewFAAA8hcyPDTublZRCRYpF540Y0tD2Ptt/LW5r+7R3D9r+Hbbn8rE539ieIQfsHV9Edtt83RXGJt68absbW1GfD8zuPF1O7Ohrf86qhJxfzubnfM1TbWxtn3bHD9G/tg4+s2t6lLC1fWp626i/d3bPKe2OH6N+ba/r91nI2x76O0NmtpTYIPPjGMEPAAAuRM2PczR7AQAATyHzAwCAG9Hs5RjBDwAAbkTw4xjNXgAAwFPI/AAA4EIUPDtH8AMAgBvR7OUYwQ8AAC5E5sc5an4AAICnkPkBAMCNaPZyjODHhvLj50pKUuHoDDW/xN6Q7qrsnDkSbXaHgbc7pULZLjZPSPdpbf9a2bUjvWRUX3cFJ9Nn2JyCISZTT8RgCg27Kkh0pyZxNKWCg/c7Ftc2LdrXyu70OA6m0HDC7rQbnzYpF/K2GdZRiRmCH8do9gIAAJ5C5gcAABfSqY6TInAMLyL4AQDAjWj2coxmLwAA4ClkfgAAcCHG+XGO4AcAADei2csxmr0AAICnkPkBAMDN2R/YRvADAIALUfPjHMEPAABuRM2Pd2p+xo8fL+np6VK1alXp3LmzLFq0KM/tBwwYIKmpqUFL27bRHz4dAICCZuvWrdKrVy+pU6eONG7cWIYOHSoZGRkh7btr1y5p2rSpVKxY0RwnnlyV+XnnnXfklltukXHjxkmbNm3kiSeekA4dOsjy5culWrVqOe5z4MABad++vUyYMCGwrlChQs5OoOUJIinFojOnjZO5nuyKxfxCdn+Hg7l/7O6z5qk2tn9F2h1RnlfJyeu2eW0dzQ1l9/2Lxee2IHDyfieiGPxdszvvlqPPYAF5P2Ld7OXz+aRr165SokQJ+eSTT2Tbtm1y1VVXyb59+2TUqFH57n/DDTdI5cqVzT1bjxVPrsr8jBgxQnr37i1XX321pKWlyf/+9z8pWrSojBkzJs/9ChcuHJT5KV26dMzOGQCAqDZ7hbuEaNq0abJgwQJ59dVXpXnz5tKxY0d5+OGH5fnnn5c9e/bkua/er3fs2CF33XWXJALXBD+7d++WpUuXmoudNYOjmZ/Zs2fnue/06dOlevXqcvzxx8tNN90kW7ZsicEZAwBQcHz33XdSv359s/idffbZcuTIEZk7d26u+/3888/y0EMPyRtvvCHJyYkRdiTGWYRg06ZN5meVKlWC1msKzf9cTmrUqCHPPvuszJkzx9QLabrttNNOk/37c0+tHj58WPbu3Ru0AACQiM1e4S4q+z1P74PZ6b02+z1Y62/VX3/9JbmVnvTo0UNGjx4ttWvXlkQR1+DnP//5zzHFyNmXtWvXBu2TPWpMSUkRy8o9bzd48GC55pprzEXXOqEPP/xQ1q9fL++++26ezWtly5YNLLVq1YrAqwUAIDGbvWrVqhV039P74DG/zrKOqZn135Nzq+G5/fbbpUWLFqY2KJHEteD5ySeflEcffTTPbcqUKRPI8Kjt27cHPa8V4/7nQlGpUiVTpb5q1apctxk0aJDpJeanUTABEACgoFq/fn3gfqu0njan++e8efOC1vnvyfpcTr744gtTEK09vNTRo0fNT+31ddttt8mwYcPEc8GPVozrEgq9cNrOOGvWLLnwwguD2iAvvfTSkH+nNndt2LAhz4BJ3/Sc3ngAAAriOD9lypQJCn5y0qpVK3n66aeDkg7ffvutyf60bNkyx320VjczMzPweMaMGaYZTO/ldevWlXhxTc2P6tevn7zyyivy/fffmwIrTctt3rxZ+vbtG5Ri84/jo22W2jvs119/Nek6bZO89tprpUiRInLllVfG8ZUAAJA4NT+huOCCC0wriLaM+BMJw4cPl8suuyxQC7Rz506TrPjggw/M43LlypnH/sUfYJUvXz7k5Id4fZyf//73vybF1qVLF3PhNWr86KOPpGHDhkHFVf4CZc3edOrUyWSGVq9ebbq8n3766SZ40kJoAAAQmmLFisnUqVNNUkGDGnXRRRfJSy+9FNhGa3+0S/uhQ4ckkbkq+ElKSjJRprYR6oUtXrz4Mds899xzQaNN9uzZ0yyaKdKMDwAABUIcprdo0qSJ6dauiQZNKOiSVYUKFczgh7mNp6fD1ejzul08uSr4yRoE5RT4qNzWE/gAAAqSJMsyS7jHcCK3Jiu9P/uLm3OiwVJez8eKK4MfV4jFNA92FZTfYfPapt3xY9R/h20xmNajoAz7v6Nv2+hP61EQJOL7nYCfpwKFiU29UfAMAAAQLjI/AAC4UKwnNi1ICH4AAHAjmr0co9kLAAB4CpkfAABciGYv5wh+AABwI5q9HKPZCwAAeAqZHwAAXIhmL+cIfgAAcCOavRyj2QsAAHgKmR8AAFzKq4MUhovgx0tz2sRi7p9YzGmWiNc2EefdcnKdov3+OXjdnp2rqyB8L7w6p1ms6KSkYU5sKuHu71IEPwAAuBAFz85R8wMAADyFzA8AAG5Eby/HCH4AAHChJN8/S7jH8CKavQAAgKeQ+QEAwI1o9nKM4AcAABeit5dzNHsBAABPIfMDAIAbMcihYwQ/AAC4EM1eztHsBQAAPIXMjx3zl4kkFRbXSsS5nhJVtF9Hol4nr75uuJeXP1P09nKM4AcAABei2cs5gh8AANyIgmfHqPkBAACeQuYHAAAXotnLOYIfAADciIJnx2j2AgAAnkLmBwAAF6LZyzmCHwAA3Mhn/bOEewwPotkLAAB4CpkfAADciIJnxwh+kDcvDx0PAAks6d+6n3CP4UU0ewEAAE8h8wMAgBsxvYVjBD8AALgQXd2dI/gBAMCNKHh2jJofAADgKWR+AABwoSTLMku4x/Aigh8AANzI9+8S7jE8iGYvAADgKWR+AABwIZq9nCP4AQDAjejt5RjNXgAAwFPI/AAA4EaM8OwYwQ8AAC7ECM8eC35+/fVXs7Rp00YqVKgQ0j5Lly6VLVu2SJMmTaR69epRP0cAAAqizZs3y48//ijFixeXM844Q0qUKJHn9pZlyZIlS2Tt2rVSu3ZtOfnkkyUpKb7zybsq+Jk1a5YMGzZMVq1aJRs2bJBvvvlGzjzzzDz3+fvvv6V79+7mwjds2FAWL14s999/vzzwwAMxO28AAApCs9dbb70lN910k7Rq1Uq2b99ulmnTpkmzZs1y3P7777+XW265RQoVKiT16tWTefPmSbVq1eTTTz+VKlWqSLy4quB506ZNcs8998gPP/wQ8j6DBw820aYGTBqpfvzxx2bdd999F9VzBQAgmpJ8kVnsZHz69OkjI0eOlBkzZpikggZBvXr1ynWfo0ePyrvvviuLFi2SyZMnm1abQ4cOycCBAyWeXBX89OjRQzp37hxyukxTbW+88YbceOONgeYx3V9Tbq+//nqUzxYAgBhkfsJdQvThhx+a+6/eU5X+f79+/WThwoWycuXKHPfR1hktN/HTJjK9D//8888ST65q9rJr48aNsmPHDklPTw9af+KJJ5qINTeHDx82i9/evXujep4AAMTT3mz3uaJFi5ole+1sgwYNTK2Pn7+5S59r3Lhxvr/H5/PJ9OnTc20m80TwowHI+vXr89ymQ4cO+RZT5Wb37t3mZ/ny5YPWaxbI/1xORowYYWqLAADwwiCHtWrVClr94IMPytChQ4PW7dmzR8qVKxe0zv9YnwuF1tz+/vvv8v7774tngx8tWP7666/z3KZFixaOg58iRYqYnwcOHAhar4/9z+Vk0KBBMmDAgKCIOPsHAwCAgjK9xfr166VMmTKB9dmzPkozPtqJKCv/46zZoNyMHj1ann76aVN7e9xxx4lng5/+/fubJVq0S51WmGvPsKz0Tdaq89zklO4DAKCgKlOmTFDwk5P69eubwEXraf21t+vWrQs8lxcNejTro3VDWvMTb64qeA6FFlH5e3IVK1ZM2rdvbyrM/bS5S9sbzz333DieJQAA7ip47tq1q2zbtk1mzpwZWKc9uSpXriwtW7Y0j7Ve9s033zS9rP2effZZ06Ki9+JEufe6quBZMzg6To8WMSvtuq4pN02f+VNoepHnzJkjy5YtC9Tv6CBMOs6ADor44osvSt26deWGG26I62sBgAKvdXBnk5DMyb0zCrLRuMVGV/UcWaFvqp2HdIyfK6+80pSG6Bg/Tz31lEyYMEFSUv4JJ/bt2yfXXHON6WmtLSzvvPOO6RGm6/TerYGRPzlx6aWXxu0tdVXwo2P1aPDij0B18CRdrrrqqkDwo29O1iarU0891QyqpPtNmTJFzj77bNPUFkr7JAAA+H96L9Xu61qzq/fRb7/9Vtq2bRt4XoOanj17BjWD6WPt5fXFF18E1pUuXTquwU+SpY13yJMWPJctW1bOlO6SklSYqwUAofBg5ifDOiozZYrp/ZRfDU2496QOJ90rKYWKhXWsjMxDMmPRyKiebyJyVeYHAABk7eoe7vQW4kkFruAZAAAgL2R+AABwozhMbFpQEPwAAOBG2tMrKQLH8CCCHwAAPD7Cs9dQ8wMAADyFzA8AAG5EzY9jBD8AALgRwY9jNHsBAABPIfMDAIgOl4/WnPDI/DhG8AMAgBvR1d0xmr0AAICnkPkBAMCFGOfHOYIfAADciJofx2j2AgAAnkLmBwAAN/JZ2vYV/jE8iOAHAAA3otnLMYIfAABcyfonAAr3GB5EzQ8AAPAUMj8AALgRzV6OEfwAAOBGpliZgmcnaPYCAACeQuYHAAA3snz/LOEew4MIfgAAcCNqfhyj2QsAAHgKmR8AANyIgmfHCH4AAHAjmr0co9kLAAB4CpkfAADcyAzzE+Y4P5Z4EsEPAABuRLOXYwQ/AAC4kU/H6PFF4BjeQ80PAADwFDI/AAC4Ec1ejhH8AADgRgQ/jtHsBQAAPIXMDwAAbsQIz44R/AAA4EKW5TNLuMfwIpq9AACAp5D5AQDArQXPpukrzGN4EMEPAABuZAIXgh8naPYCAACeQuYHAAA30qkpksIsWLa8WfBM8AMAgBvR7OUYwQ8AAC5k+XxihZn5sTya+aHmBwAAeAqZHwAA3IhmL8cIfgAAcCMd4yeJru5O0OwFAAA8hcwPAACubfYKt6u7JV5E8AMAgAtZPkusMJu9LI8GPzR7AQCAkBw8eFAefvhh6dixo3Tr1k3efPPNqOwTbWR+AABwIzNGT2xHeL7ssstkzZo18sgjj8i2bdukb9++snnzZrnzzjsjuk+0EfwAAOBCsW72mjVrlnz22WeyaNEiOfHEE826ffv2yfDhw+XWW2+V4sWLR2SfWKDZCwAA5Gv69OlSs2bNQBCjunfvboKZefPmRWyfWCDzYyMyzpCjIt6sDQMAhMDcJ2JUSJxhHQ57YtKMf8937969QeuLFi1qlqzWrVsn1atXD1rnf6zP5cTJPrFA8BOCHTt2mJ+zZWq03w8AQAG5b5QtWzYqxy5SpIhUrVpVZm+OzD2pVKlSUqtWraB1Dz74oAwdOjRo3dGjR48JiIoVKxZ4LidO9okFgp8QlC9f3vz8888/o/ZhLqj0XxP6pVq/fr2UKVMm3qfjKlw7rh+fPffZs2eP1K5dO3DfiAYNHtauXStHjhyJyPEsy5KkpKSgddkDFqWvafHixUHrdu7caX5WqFAhx2M72ScWCH5CkJz8T2mUBj7cwJ3R68a149rFA589rl087xvRDID8GZRYOfnkk2Xs2LEmwPMnAubOnWt+Zq3pCXefWKDgGQAA5Ouiiy4yTWQjRowwjw8fPiyPPfaYGb+nbt26Zp0GOa1bt5Zp06aFvE88EPwAAIB8aebmgw8+kNdee8007Wnd0YEDB8xjP63j0cyOjucT6j7xQLNXCLTtU4u/cmoDBdcuWvjccf3ihc8e1y43Z511lqnhXLFihRmjp2HDhkHPp6amyo8//igNGjQIeZ94SLK8OrEHAADwJJq9AACApxD8AAAATyH4AQAAnuL54EdLnl5++WU577zzpHPnzvLkk0/mO+qkk30KqgULFsjVV18t7du3NzP16sBb+Q3cp9frggsukG7duslDDz1kukZ6kb7uQYMGmWLACy+8UKZMmRLyvrt37zafPe1S6lVvv/22nH/++dKhQwczSaL2IMnPX3/9JXfddZfpZnvttdfK0qVLxYuWL18uvXv3Nt9b/amP8/PRRx/JFVdcIWeeeab5+eGHH4oX6ZxUY8aMMd/bG2+8MaR9Dh48KA8//LD53OnfvTfffDPq54l8WB533333WeXKlbNeffVVa+LEiVbt2rWtnj17RnyfgmjhwoVWsWLFrH79+llTp061LrnkEqtKlSrW5s2bc92nadOm1l133WVNmTLF+uijj6xTTjnFatasmbV//37LSzIyMqxTTz3VLHotnnzySSslJcV64403Qtpfr/UJJ5ygnRUsLxo9erRVokQJa8yYMdakSZOs448/3jrnnHPy3GfFihVWpUqVrEsvvdT64osvrA8++MBq1aqVdeDAActLVq9ebZUtW9bq3bu3+d726tXLPP7tt99y3Wfs2LFWkSJFrKeeesr65ptvrFGjRlmFCxc2671EPyvVqlWz+vbta3Xt2tV8fkKh2zZu3Nh8Vl988UXz2X3iiSeifr7InTf/cv5rx44d5gusQYzfV199ZW4oy5Yti9g+BdUFF1xgdezYMfD46NGjVs2aNa17770313327dsX9Hjjxo3m2mkA4CXvvfeelZycbG3YsCGwbuDAgVatWrUsn8+X574vvPCC1a5dO2vcuHGeDH4OHTpkbtZZbx6LFi0y12LmzJm57te+fXurQ4cOQdf38OHDVmZmpuUl119/vXXiiScGroP+1H+A3Hjjjbnuc95555mgMXsAruu9RD8r/r9ht956a0jBz3fffWc+m/oZ9dPPbunSpT0XeCcSTzd7fffdd6a5SlPnfprKLF26tHz99dcR26cg0sB5xowZQdchJSVFunTpkud10JE+sypRooSZUyZSc9S4xfTp082w7zVq1Ais6969uxkL49dff811v2XLlsmwYcNM2jzaw+cnqvnz55smw6yfPR0mXwdQy+2zp9f122+/lZtvvjloDiOdINJr11E/e9r04r8O+lOvZV7f21NOOcU0EWqTj7/5Wh+feuqp4iX6Wcn+NyyU612zZs2gqRz0u67Xct68eVE4S4TCW9/6bNatW2f++GWdXK1QoUJSpUoV81yk9imIdGK6v//+W6pXrx60Xh/buQ465LkGjhpAeoleo5yunf+5nGhNS48ePWTUqFFSp04d8Sr/9bHz2fPXtFSuXFl69epl6oT69Oljgkkv0X+0aCCY07XTiZtzo4O86g1bJyk+6aSTTKB58cUXy5AhQ2Jw1t77riP6PB38aAYnp1GbdQTK3AqYnexTEPlfa/ZrYec6vPXWW+ZGPn78+LjO7hsPOX2O9Nr5n8vJ7bffLunp6dKzZ0/xMiefvUOHDpmfWtzbpk0bue+++8y/4jWjsWTJEvGKzMxM8fl8OV47Xa/P51bs/OKLL8oDDzxgvrP684UXXrBVpO9VOX3X/ROSeumekWg8Pb1F+fLlTepRP4CFCxcOrN+xY0euN2Mn+xREOoS53jw0A5RVqNfh/fffl+uvv970mrvkkkvEa/RzlNO1U7ldvwkTJkijRo0CPbz8c+foYw2MrrrqKvHKtVN6/XSeoKzXr0mTJnnuc+utt5peiapTp05mGH6dcVpv5F6gTdOaac3ps6dzMGkWOyd33nmnyZTpT6WZs82bN8vAgQPNxJXInX72Fi9eHLTOf/29dM9INJ7O/GjNhb+7tt+GDRtMd1hN7UZqn4JI/+Vy/PHHm/qLrHRCu/yuw6RJk0z3eL3h6L/EvUg/R/oHMSMjI+jaaUDdtGnTHPeZPXu2vPLKK/L000+bxR/s6P97qdnQ//nK+tnTGpSVK1fm+tlr3ry5ubbZmx80eNq1a5d47bNn93urN+us9WlKr2X2IAo5X+/Vq1cHDemh11tlrQNCjFke16JFC9MNUbseqz59+lg1atSwDh48GNjm/PPPt5599llb+3ilu7F2+V+1apV5PH36dNOD6fPPPw9s87///c/q0qVL4PHkyZNNl1ntqeRlf/75pxkmQLsOqz179phhALIOmbBmzRrTm2TevHk5HkN7HHr1K3z22Wdbp512WuA7pz0MU1NTrZ07dwa2ueaaa6zhw4cHPe7UqVOgh83ixYut4sWLW6+88orlJa+//rp53T/99JN5vGDBAvNZzDrMwltvvWWur1+3bt1MDzH/9dWf+ljXe1Vuvb12795t1utwCv7H5cuXt+65555Ab0XtrZm1pyxiz5t/ObONeaE3nQoVKljVq1c3XY3nzp0btI0GNtoN2c4+XqDB3w033GAVLVrUOu6448zPRx55JGgb/cLr2D9+OkyAdvHUPw5ZF/2D7DU65ofesOvVq2eVKlXKOvPMM4Nu3kuXLjXBjQ6lkBMvBz86RIKOEaVd3vX7V7lyZevrr78O2iY9Pd267rrrAo/1JqRBk96IdIwkHWtFv9f5DS1QEOnr1n+E6PdWf2b9++bvil2oUKHA4/Xr11tnnHGG+Zw2b97c/NShA3S91/To0cP8zdK/a3od/H/DdNgEtW3bNvO9zBpMzpgxw2yvn1X9zp988smevHaJhFnd/+0BsWrVKtMEoU052du9Fy1aZNpmtYdDqPt4yZYtW2TTpk1Sr149UwuUlfYs0doUf3PhnDlzcjyGXtvsTRJeoIW42lyj9RZ6/bKPCqvFuFrHUqZMmWP21eu6Zs0aT4/y/Ntvv5lecPodzFqDp7Qrtg6lkJaWdsxnUpu66tevb7vbckGidT7a20h7DmavPdFmfO391apVq6D1W7duNd91/a5qzzkv0u+kfjez02ulwwboPUHLIho0aCAVK1YMPK91oitWrDDF5Q0bNozxWSM7gh8AAOApni54BgAA3kPwAwAAPIXgBwAAeArBDwAA8BSCHwAA4CkEPwAAwFMIfgAAgKcQ/AAAAE8h+AEAAJ6SEu8TAJD4dAoOnSpCpy/58ssvzTD+HTt2PGY6EwBwA6a3AJCvxo0bS3p6upkvq0aNGoH57pYvX+7pee0AuBOZHwB52r9/v6xevdpMgDlv3jwzGehPP/0kp5xyipmo8YQTTuAKAnAVgh8AeVq8eLH4fD4ZM2ZMYBZ0/4zeJUuWNMHRc889Zx5rINStWzeuKICERsEzgDwtXLhQGjVqJPXr1w+qASpevLjUrl1bLMuS3bt3y9y5c+Xdd9/lagJIeAQ/APINfk4++eRj1jVv3tzU+2g2aOTIkdKzZ0+uJABXIPgBkCctbs4p+Mm+DgDcguAHQK4OHz5senTlFPycdNJJXDkArkTwAyBXBw8elMcff1xatmwZWLd3715Zs2YNwQ8A12KcHwC2zJw5Uzp37ix///23FC1a1KwbPXq0yQb98ssv0qNHD7nqqqtMMTQAJCK6ugOwRYOcJk2aBAIftWfPHqlZs6ZZtOdXRkYGVxVAwiLzA8B28HPgwAFp164dVw6AKxH8AAAAT6HgGQAAeArBDwAA8BSCHwAA4CkEPwAAwFMIfgAAgKcQ/AAAAE8h+AEAAJ5C8AMAADyF4AcAAIiX/B9M2/hFnFjgSQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "print(f\"1 length unit = {out.units.x} m, 1 velocity unit = {out.units.v:.4g} m/s, 1 time unit = {out.units.t:.4g} s\")\n", "\n", @@ -11800,7 +566,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "45", "metadata": {}, "source": [ "## Save standard output\n", @@ -11810,23 +576,10 @@ }, { "cell_type": "code", - "execution_count": 19, - "id": "33", + "execution_count": null, + "id": "46", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wrote:\n", - " post_processing/report/scalars.csv\n", - " post_processing/report/scalars.png\n", - " post_processing/report/electric_energy.png\n", - " post_processing/report/kinetic_energy.png\n", - " post_processing/report/total_energy.png\n" - ] - } - ], + "outputs": [], "source": [ "written = out.save_report()\n", "print(\"Wrote:\")\n", @@ -11836,7 +589,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "47", "metadata": {}, "source": [ "## Comparing runs\n", @@ -11847,48 +600,9 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "48", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n", - "Stabilizing Poisson solve with self.options.sigma_1 =1e-14\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time stepping: 100%|██████████| 20/20 [00:04<00:00, 4.38step/s]\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:191: UserWarning: Override equation parameter self.alpha =1.0\n", - " warnings.warn(f\"Override equation parameter {self.alpha =}\")\n", - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/models/species.py:198: UserWarning: Override equation parameter self.epsilon =-1.0\n", - " warnings.warn(f\"Override equation parameter {self.epsilon =}\")\n" - ] - }, - { - "data": { - "image/png": "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c3NzUwfc0icf2Q6eN8Tzhvi3hqwV/0eRvZw3FRnaxKIeREREREREFsKAjIiIiIiIyEIYkBEREREREVkIAzIiIiIiIiILYUBGRERERERkIQzIiIiIiIiILIQBGRERERERkYUwICMiIiIiIrIQBmREREREREQWwoCMiIiIiIjIQhiQERERERERWQgDMiIiIrJqt956K55//nmHb4Mt2b17N9q0aYPMzMwKv8ZgMKBRo0aYM2dOjbaNap+cC7///rtV7fqFCxeiT58+6ryzNAZkREREZNVOnz6Ns2fPVus2R40ahf/7v/+zaBvs2VNPPYVhw4bBy8urwq/RNA0nTpxAdnZ2jbaNat/JkycrFZzXhmuvvVb9Tn/33XewNGdLN4CIiIiotiUlJcHPz6/Cz582bRpcXFxqtE32YsuWLVi2bBl+/vlnSzeF6JKcnJzw8MMP47333sODDz6o7lsKe8iIiIjIombNmoXBgwejWbNm6NmzJ3799ddqec3s2bMxZMgQxMTEYODAgZg7d25xCqQEDZJCJSlycps5c6Z67MYbb8RLL72EN954Ax06dEDjxo1VStOTTz6JSZMmVWj7V9Nu8/u/++676N27t0r1euihh8rtnavotsp+luTkZNx///2q3e3bt8fbb7+NZ555BnfeeWfxa4cPH46nn376ovf8z3/+o3oWLkd6HK655hqEhoYWr7vhhhvw7LPPXvTcF198Ef3797/ktjp16qSOj7S9S5cueOyxx3DmzJlSz/ntt9/UNpo3b47rr78eM2bMKPV4eno6nnvuObRr10595tGjR2PHjh1X3bayZF9erq0ZGRnq8R9++EHtfzkm8ho5r/Lz8/Hmm2+qzyvHfMKECcjJybnotXLOymvldfL6jz76SPUsllRYWIj3338fPXr0QNOmTTF06FBs2rTpom1NmTIF9913H1q2bInbbrtNPRYfH49HHnkEsbGxav2jjz6KtLQ0VMb69etx3XXXqffu168flixZUu7zrvRef/zxhzq3582bp3pb5TkDBgzAihUrcPz4cYwZMwYtWrRA9+7d1TlQVlxcHG655RZ1zOXYT5w4EefPny/1HPlbINuSLxAsSqMqiY+Pl7Nf/bS0rHPJWmL8ca2oqMjSTSEbIudLUlISzxvieWMn8gsN2tGULIvfpB2V+Vvz4Ycfanq9Xnv11Ve1HTt2aL/9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Iqq21MWpD3TFpuNp6BBRT036TERERESXx4CMiKpNPQ9XDGrZgHuUiIiIqIJYZZGIiIiIiMhCGJARERERERFZCAMyIiIiIiIiC2FARkREREREZCEMyIiIiIiIiCyEARkREREREZGFMCAjIiIiIiKyEAZklTRp0iQEBgYiNja2Zo4IERERERE5DAZklTRx4kQkJycjLi6uZo4IERERERE5DAZkREREREREFsKAjIiIiIiIyEIYkBEREREREVkIAzIiIiIiIiILYUBGRERERERkIQzIiIiIiIiILIQBGRERERERkYUwICMiIiIiIrIQZ0u9sa0rKipSP5OSkizdFBgMBqSmpiI/Px96vd7SzSEbwfOGeN4Q/9aQteL/KLL188YcI5hjhsthQFZFKSkp6mfnzp2rugkiIiIiIrLzmKFRo0aXfY5O0zSt1lpkR/Ly8rBr1y4EBATA2dmyca1E4BIYbtq0CcHBwRZtC9kOnjfE84b4t4asFf9Hka2fN9IzJsFY69at4e7uftnnsoesimTHdurUCdZETrywsDBLN4NsDM8b4nlD/FtD1or/o8iWz5sr9YyZsagHERERERGRhTAgIyIiIiIishAGZHbA29sbr7zyivpJxPOG+PeGrAn/RxHPG+Lfm8tjUQ8iIiIiIiILYQ8ZERERERGRhTAgIyIiIiIishAGZERERERERBbCecjsgMztXVBQADc3N0s3hWxIdnY2jEYjvLy8LN0UshFyvsjfG71eb+mmkA2cKxkZGWpZp9Opwh7yk6gysrKy1OS6Pj4+PH/oiudJWb6+vrAV7CGzYbm5uXjwwQfh6emp/tm1aNECS5YssXSzyIrl5+dj2rRpGDhwoPoH165dO0s3iaycBGB///03+vTpo/7OSADfoUMHzJs3z9JNIyt24MABNSGq3Bo2bAgPDw/069cPmzdvtnTTyEbs3bsXAQEBqFevHhITEy3dHLJiAwYMQFBQUPHfHPPNYDDAVjAgs2Hjxo1TAdiuXbuQk5OD2267DTfccAP2799v6aaRldq2bRtmzZqFZ555Bnfffbelm0M24MyZMxg9ejR69+6NpKQknDt3DsOGDVN/axYvXmzp5pGVat68Oc6fP69u6enpiI+Ph7+/PwYNGoRTp05Zunlk5QoLC3HXXXehVatWlm4K2dA18fkLf3PMN1vK5mBAZqMSEhLw66+/4rXXXkNkZKQ66V588UWEhobiww8/tHTzyEp169YNU6dOVT1kTk789acrk78tv//+u/pbI71jkhr98ssvq787v/zyC3chVYifnx8++OADFdAzk4Ou5PXXX4eLiwsee+wx7ixyCLwis1ErVqxQqUT9+/cvXif5+X379sXy5cst2jYish+SMnTrrbdetF4Ceo4Josowj/FwdXXljqNLkrRW+WL5hx9+4BeHVCl5eXnq2tgWMSCzUcePH1ffXAcHB5daHx4erh4jIqopCxYswKFDh9QXQERXGussvWI7d+5UKUXt27fHTTfdxJ1GlzxfJFXxueeeQ8uWLbmXqMK+/vprNTZexqv27NkTK1euhC1hQGbDf7QkdajsN9Tu7u7qW8jyqs0QEV2tEydO4J577kGPHj04DpGuSC6sZXB9bGws9uzZg88++0z9nyK61PkiPagvvPACdxBV2LXXXot169apa2PplIiJiVEZZMuWLYOtYEBmo+QbAOmaldLCZUuZyx8zZ2fOaEBE1Ss5OVkVZZDxQDNmzODfGbqiTz/9VBX1kBL49913nyoOY0sXSVR7tm/fji+//FKlK8q1jBRlkIJlQs4f8zJRWW+88YbqfZdU+sDAQHzzzTdo3LgxPvnkE9gKBmQ2Kjo6WgVjJ0+eLLVevhmQx4iIqpNcHEkwJr3vUpRBxpYRVZQUhJHCMHKR9NNPP3HHUblf+Mg0PiNHjiwuW/7UU08VF6SSlFeiipAhPc2aNVMF8GwFAzIbJWM3pALRP//8U7zOfKEkF01ERNVFvq2+/vrr1Vgg6d0ICQnhzqVKkzmB5FySdHuisuTapWzZcunpEJLuyqquVFGSQRYXF4eoqCjYCua12Sj5dvqJJ57AK6+8onJlmzRporpsJX92woQJlm4eWTH5JycKCgpUL6v5vnwzyVRXKkvOEynCIEU8pJiHTA5tPmfkfJHzhqis999/X6WZ3XjjjarYlMxDJuvS0tLw8MMPc4cRUbVYvXq1Kujx4IMPqmth6RV79dVXkZqaqsYk2goGZDbsnXfeUTPYmyfDk/xZKYcfFhZm6aaRlcrPz1dpICWZ78uE0TK+g6ikI0eOqDLUouQ0G6JTp06cHJrKNf7/27t7FFWCKACjd+CJYDRM5hYMFFSMBgNzQ0NXMKsQTAdchGsx08DAn9jQVNFAceiO50H7+kEhnJMI0tGN/Kzqqq+v/EdSdo9UtpU++xOx1+vFcrmMRqNhahSSvROfnZzn3kz+JjtR8Xg8xnQ6jc1mEx8fH9HtdvMVsizQXsXb41UP7AcAAHhx3iEDAABIRJABAAAkIsgAAAASEWQAAACJCDIAAIBEBBkAAEAiggwAACARQQYAAJCIIAMAAEhEkAEAACQiyADgP+j3+zGZTMwSgKcIMgAo6Xw+x2KxiFarZZYAPEWQAUAJ8/k86vV63O/3GI/H8f7+HoPBwEwBKORPsccAgN+MRqM4HA4xm81iv9/n31UqFcMCoBBBBgAlVKvV2O120W6389UxAHiGLYsAUNJqtYpOp2OOADxNkAFACZfLJbbbbb5CBgDPEmQAUMJ6vc4P9BBkAPwLQQYAJZxOp/zzer2aIwBPE2QAUMLn52cMh8NoNpv5oR7f39/mCUBhb4/H41H8cQDgN7fbLb8gularOfYegMIEGQAAQCK2LAIAACQiyAAAABIRZAAAAIkIMgAAgEQEGQAAQCKCDAAAIBFBBgAAkIggAwAASESQAQAAJCLIAAAAEhFkAAAAiQgyAACARAQZAABApPEDPNACBu87wzcAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "sim_coarse = Simulation(\n", " model=build_model(),\n", @@ -11909,7 +623,7 @@ }, { "cell_type": "markdown", - "id": "profiling-md", + "id": "49", "metadata": {}, "source": [ "## Profiling\n", @@ -11919,42 +633,10 @@ }, { "cell_type": "code", - "execution_count": 24, - "id": "profiling-summary", + "execution_count": null, + "id": "50", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Profile: dt=0.05, algo=LieTrotter, Nel=(16, 1, 1), p=(2, 1, 1) (1 rank(s), 0.776 s)\n", - "\n", - "Region Calls Total [s] Mean [ms] Share\n", - "-------------------------- -------- ---------- ---------- -------\n", - "scope_profiler.session 1 0.776 775.640 100.0%\n", - "model.integrate 100 0.329 3.292 42.4%\n", - "save data 101 0.274 2.709 35.3%\n", - "prop: VlasovAmpereCoupling 100 0.253 2.532 32.6%\n", - "solve: SchurSolver 100 0.120 1.203 15.5%\n", - "setup: total 1 0.104 104.367 13.5%\n", - "accum: vlasov_maxwell 100 0.084 0.838 10.8%\n", - "prop: PushEta 100 0.075 0.755 9.7%" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "region\n", - "kernel: vlasov_maxwell 0.043829\n", - "kernel: push_v_with_efield 0.013737\n", - "kernel: push_eta_stage 0.010213\n", - "kernel: charge_density_0form 0.000802\n", - "Name: total_time, dtype: float64\n" - ] - } - ], + "outputs": [], "source": [ "print(out.profile.table(top=8))\n", "\n", @@ -11964,7 +646,7 @@ }, { "cell_type": "markdown", - "id": "profiling-compare-md", + "id": "51", "metadata": {}, "source": [ "`compare()` puts the same statistic of several runs side by side, with runs whose region is missing as NaN. Here the two runs of the previous section differ only in the time step, so the number of calls per propagator halves for `dt = 0.1`." @@ -11972,25 +654,10 @@ }, { "cell_type": "code", - "execution_count": 25, - "id": "profiling-compare", + "execution_count": null, + "id": "52", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Size: 32B\n", - "array([[100., 100.],\n", - " [ 50., 50.]])\n", - "Coordinates:\n", - " * run (run) " - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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euGFF2jVqlXUo0cPGj16NAWDwvx0gh9++IHefvtt2rBhA/Xq1YtGjBhBpuOakzp15s2bRx988AFZlkV9+/alI444IunnM2bMoFmzZiWVNW/enK6//npqKIgcAAAAKEBr166l/fffn5599ll1/7bbbqMBAwZQVVVVjW2efvpp6tmzJy1YsEB1gq699loaPny46vikU2fw4ME0ceJE1anauHEjjRo1ik477bSk5/rwww/ptddeo3bt2sVvbdu2pYaEkSYAAIACXNN04403UpMmTWjOnDlqdGnChAnUtWtXevjhh+niiy921Y9Go3TBBRfQNddcozpCjDs+Xbp0UaNV3OnRqcNuueUWOuigg+KPffLJJ1OfPn3onHPOocMPPzxe3qFDB7r00kvJKzDSBAAA4KVOUya3NEybNi1pOo5HcY455hhVLvn1119p+/btqnMT06JFC+revTv961//0q7DEjtMjEemDMOg1atXu56TO3d33nmnmspraOg0AQAA5JFt27Yl3aqrq8W1TL/99hvtvvvuSeV8n9cjSXbZZRdq2bKlGpmKWbduHX377be0ZMkS7TqS5557Tk3l9evXL6mcHysSidDy5ctVh+6MM86ghoTpOQAAgDyanuMprUQ33HCDGq1JVFFREV9YnYhHhcrLy8WHN02THnroIRo3bhx988036nl4oXa3bt3U2iTdOk5ff/21mg68+uqrkzpxZ511lprGizn77LPVgvHjjjtOTec1hILtNP1YuRMV+YRUuwSWMBAXlQLdLHegW1hIr3MGv4WFdiGxTC94UgqyDAv1qjWDLMOOIDwWcdSTQvVsSxjAtDRDMKXTbU1bK0BRChvUOQVXyrY0NR/fjAhlQXdbIX+PzKgQWimFcUqvwfHFKub2ZTsdWAy3lOpphlv6ag+7ZMLHQQyQtKRATSnsVCqTwi19qetJ7eRtc5dpn1Yu7WDhs2RHhSDPiBBuSbV/56mHF55T+q4Sg3OF0Mpin/tDEpTKhA9TkVAWcHwg/KYQblnHIEvpO766MjlMszFEDvCZcImdoaIi9/vStGlT9e+WLVuSyvl+s2bNanyKU045hQ499FB6//331SgWr1eaPHmy6iClUyeGR6COPvpoOvXUU+mmm25K+lnnzp2T7vfu3Zv23Xdfmjt3LjpNAAAAkDnuMDlHkJy4Y9S+fXvXlNn3339Pe+65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U6tNKrFRKquqKnVl5DTVv4wySvIg6KeQc5qo8fPUEYicpqzkNMV+VyTOcORKxK7OaIotQuGsbg+4GXZ9HAk14JGkhx9+mGbPnq2m0UaPHk3Dhw9PSv4+8cQTadKkSTRo0CCtNqnw9B4WgwMAQDp4KUj79u1zstOqqqqoc+fOtHbt2owfq127dirDUHcgARpRp6kh8KLxX375hZo1a0ZlZWWqA8UfBqx10sexDdhv6cN+qzvsO+y3hjrmVq5cSYZh0C677KLO1M4V7jjxiU2Z4pOi0GHK42vP1Tc+6GN/LfAHgWGBeN1gv2G/1Tccc9hv9Y2jaurjj2ru6KCz430NthAcAAAAoDFBpwkAAABAQ0F3mngh+Q033KD+Bew3HG/ehc8q9huOOfCCglsIDgAAAFAXBT3SBAAAAKALnSYAAAAADeg0AQAAABRyThMv1Xrsscfo1VdfVSnigwcPposvvpgCgUBW2+Sjr7/+mv7+97/TihUraI899qArr7ySunbtWmP9p556iqZOnZpUxuGh//nPf6iQbNmyhZ555hl1PcSePXvSfffdl7INp97zZYDmzp1LTZs2pdNPP51OPvlkKiT8uZs5cyY9+uijKrH/zTffpLZt29ba5qKLLqJPP/00qWzAgAF0xx13UKHgIMRnn32W3nnnHXXs7bfffnTJJZfQrrvuWms7/lxPnjyZvvvuO5VZx99xffr0oULCxw5fnuuHH36gnXfemcaMGUNHH310rW34+KqurnYdh3xZLygcedtpuvbaa+mhhx5Sv/xLS0vpsssuU52B5557Lqtt8s2iRYuof//+6kuEO0vcCTj44IPVfqjpEgL8i46vE8gdzhi/P28PLdHGjRvVL62RI0eqTvbixYu1OgvHHnssbd68WV1Dkfcj73e+puL48eOpUPAvHd5/vXv3Vh1O5y8mCe/fbt260fnnnx8va926NRWSUaNG0Y477kgnnXSSCmB88MEHqVevXvTFF1/UeKmodevWqYub840/39xZ/cMf/kAffvghHXjggVQIXnrpJbrnnnto7Nixat99/vnndNxxx9G9995b6+du/vz5dOutt9Khhx4aL+vYsWM9bTV4hp2HNm7caAcCAfvJJ5+Ml/3nP//hswTtRYsWZa1NPjr55JPtQw89NH4/EonYu+++uz1hwoQa29x8881237597UIWDoft8vJy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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "density = out_sph.euler_fluid.view_0.n.isel(e2=0, e3=0)\n", "density.plot(x=\"t\", y=\"e1\")" @@ -12164,7 +743,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "58", "metadata": {}, "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" @@ -12172,40 +751,17 @@ }, { "cell_type": "code", - "execution_count": 28, - "id": "42", + "execution_count": null, + "id": "59", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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Gt/V1bUy30L84XAgVfm4nasGtzO3sybhv2f1buuXZaJkeLA5Y4wxxlg5cSDFGGOMMVZOHEipKSMjI3z++efimnE7qzv+e+Z21iT896xdbc2DzRljjDHGyokzUowxxhhj5cSBFGOMMcZYOXEgxRhjjDFWThxIqai//voLAwcORPfu3cVAurS0tEp5jrY7d+4cRo4cia5du2LWrFl49uzZC/ePi4vDN998g379+mHQoEFYsmQJ0tPTq+x41VVAQAAmTZqELl26YPz48bh9+7bCz7127RratWuH6dOnV+oxaoKYmBh88MEH4u95xIgROHHihELPO3jwIEaNGoWePXti8eLFyM7OrvRjVWeZmZn49ttv0aNHD/FdsG3btpc+5/Hjx3jvvffw2muvied8+eWXSExMrJLjVVc5OTnYvXu3aK+OHTsqXEhz48aN6Nu3r/h7/uGHHyr975kDKRW0Zs0a8WPTq1cv8eO+d+9e9O/f/4Vr/pTnOdru9OnT4ouwTp06mDdvHh49eoQOHTrIl/YpLi8vDy1bthRfojNmzMDYsWOxZcsW9O7dG7m5uVV+/OoiJCREBEL0Zfa///1PzK6hdn748KFCVYvfeustJCUl4f79+1VyvOoqIyMDnTt3hp+fHz788EM0btwYffr0weHDh1/4vIULF2LMmDFo3749PvvsM/Hj9cknn1TZcasjCjq3bt2Kd999F4MHDxbfB999912p+0dFRaFt27YIDQ3FRx99hHfeeQd79uwR3x2sdN26dcMff/wBd3d3XLlyBYpYtGgR5s6di+HDh2Pq1Knit3HcuHGoVBKmUnJyciR2dnaSr776Sn7fgwcPKBqSHD16tMKewySS9u3bS0aOHClvirS0NImlpaVk6dKlpTZPampqkdt+fn6inS9evMhNWorp06dLGjRoIMnPz5ff17JlS8mYMWNe2mb0+SxYsEAyfvx4SZcuXbiNX2D16tUSExMTSVJSkvw+aremTZuW+hwfHx+Jjo6O5J9//ilyf0ZGBrd1KS5duiT+zV+/fl1+37JlyyRmZmbPfT/I7NmzRzwnMTFRft+RI0fEfREREdzWpZC11+bNmyV6enqSl0lISJAYGRlJfv31V/l9p0+fFu3s6+srqSyckVIxvr6+ovtowIAB8vvq16+PunXrlpqmL89ztB11x9EZTuE2MzU1FangF7WZmZlZ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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" ] }, { "cell_type": "markdown", - "id": "43", + "id": "60", "metadata": {}, "source": [ "### Vector fields on a mapped domain\n", @@ -12215,173 +771,10 @@ }, { "cell_type": "code", - "execution_count": 29, - "id": "44", + "execution_count": null, + "id": "61", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r", - "Time stepping: 0%| | 0/40 [00:00\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - " e_field: \n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Creation of Struphy Fields done.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "Evaluating fields ...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r", - " 0%| | 0/41 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out_coaxial.em_fields.b_field_xyz.struphy.plot.slice(\n", " x=\"e1\",\n", @@ -12447,29 +821,10 @@ }, { "cell_type": "code", - "execution_count": 31, - "id": "46", + "execution_count": null, + "id": "63", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/max/git_repos/struphy-hub.github.io/submodules/struphy/src/struphy/diagnostics/plotting.py:296: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.\n", - " mesh = ax.pcolormesh(xg, yg, values, shading=\"auto\", vmin=lo, vmax=hi, cmap=self.cmap)\n" - ] - }, - { - "data": { - "image/png": "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G+w77rjD93R+0Vk+Sf2+ffuyxx9/nC1evJi9+uqrga0fAJB53nvvPXbOOedk/IVi//792YMPPshWrFjBxYwuJLtFfUrylyTwKaecorw+XHDBBezyyy9nw4cPZwMHDmTnnnuu8h5ed7p0kSvbCQDwz2effcbOOOOMjO7C//u//2NDhw5lc+bMYQcddBDbunUrO/DAA/nzsQ6/+tWv2Pnnn88DP+he+OKLL2a//OUv0z5dusiV7QQgrzBAznLdddcZw4YNy+g6n376aSMcDhvz5s2LD/vjH/9olJWVGRs3bgxk3qCn88OsWbOMM888M6PfBwCQG9x5551Gz549M7pOqis6d+5s3H777abhGzZsMJqbmx3n/fTTTw263K9YscJxujfeeINPN3Xq1Piwv//970ZJSYmxbNkyz9P5RXf59fX1xtq1a/nnSy65JOO/CQAgWB555BGjvLw8o7u1oaHBWLlyJf/829/+NuX103eg+mvRokXxYZ9//jkf9t5778WHvfDCC0YoFOL3m16n88P69euNH/3oR8amTZtsp8mG7QQApJ8nn3ySn8OZ5OqrrzZee+0107CbbrrJKC4udn0e/v7773n98sorr8SHTZo0iX+HKVOmpG06P2zdupXXtatWrcrY9wEA6IHI7xyFmhpS9OHKlSv5m1sqjz32WNrXSxExY8aM4W8fBWeddRbbsWMHe/PNNwOZN+jpCHq7TFE95513Hn9b+tZbbzlu6/r163mkZCa/DwAg+/nf//7HW93U1dXF616qW9LNP//5T7Zz504eASJDLUnC4WAu5VRX1dTUsH333Tc+jL4fRZjLUdW60xHbt29njz76KI9aufDCC3nTzaC2o2PHjqxr164pfWcAQHbw4Ycf8vvY3bt3x+vWu+66K+3rbdu2LY/gDgrRooaiyOU6rVu3buzII4+MD6NocUrV9N///tfzdMSuXbv4/qII7J///Of8npWaxNtBdTFNQ/eedqRjOwEA2QWl4BQp4kRde/PNN6d9vb///e/ZCSecYBq2//778/R1y5Ytc5yX6pV27dqZ5qdUeHQPLLfWCXo6Ys+ePezf//43TzNI97JPPfUUvx+1g65hVNeSe8jU9wEA6IGc3zkKNRsaNGgQa2ho4DecBDU71MlL6AQ1H6ebVztmzJjBm9nL0A0wSYiZM2c6Llt33qCnW7p0KU8RQBcP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rFhhlJaWGvfcc0982A033GB06NDB2LRpk+fp/vKXvxh9+vRJuoelbZs+fbpy+5csWcK3i9ZhR9DbCQDITt5++21eH6xfvz5j69y8ebOx9957G4MHDzbWrl1rOx2No7qW7hUFtJ3t2rUzfv/738eH3XHHHUZZWRm/P07XdP/617+Mbt26GQsXLjRt41dffWV8/fXXyu3fsGED37d0L2tH0NsJANAD8juHWbRokdG+fXtj//335xeJf/zjHxlZ75/+9CejTZs2xrhx4/hFrHv37lx2y5CMoYpffkjQnTfo6UgwX3DBBXxfHXXUUcaECROMvn37GieeeKLtTbqO/E7H9wEAZD9r1qwxunTpwl+qUd37wAMPZGS99MKO5MIBBxzA6zKqU+QbYlF3Ud37+uuvx4c98cQTRv/+/Y2DDz7YOO644/i2jxo1ypg8ebJS7NNyafljxozhgvzjjz/2NV1zc7Nx5ZVX8hv3I444wjj++OONAQMGGMccc4yxatUqx++qux20/6kMGjSITy/+luU/ACA3IDlSXV1tjBgxwjj99NONO++8MyPrPe+883i9UVtbaxQXF8frEfke9rvvvuN165NPPhkfRkENJHIOOugg46STTjKGDh3K61e657Py3HPPcbF++OGHGz/4wQ94Xf7mm2/6nu7GG2/kdethhx1mnHDCCXw7Dj30UGPx4sW+5Xc6thMAkH1s376d349RnUV17U033ZT2dV544YW8DqJ6StSxqnu2BQsW8OnoRaL1Hpie5Q855BBe77Vt25a/MLUS9HQkm8V05A5onx144IG2cltHfqdjOwEA7oToH90ocZB9UC/AU6dO5b2rU8dn++67b0bWu2bNGr5ean5/8MEHJ+Xronxen376KU81Ind2pjNvuqZbu3Ytz41LnUwMHz6cd65mB+XSotQnp59+esr7wut0AIDsh3L6UV8KlA+wd+/eGctzSk0fKY82NVmlTs2s/QZQ3fXBBx+wQw891JRrkPJsU6eXdK2gPIGUk9au2Sstg75bq1ateF1F6UlSmY5yzX799de8cyFKNSWa5buhs3zqVE6F9fsDAHIDatJN5z3VG9RhMHWSm25eeumleCdlMlSvi3zhdL/91ltvmYYRNN/333/P7/EoPyt1MGx3f0f175dffsnrXqrT7FJWeZmO7ispJy01r5c7ZbNCKbKo8zZKQ+V2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MVaOX8ZcGXujTgKYP//5z+rUU09tGGtHjx6t5syZk+9DAxDQ7t271b333qsGDBhg/nuW7bLLLlPr1q2L1GspmYFkB5IhyLm5ZApRLziT43/ppZfUmWeeab4PyueO4cOHq3/84x/5PjQgegwUjOuuu86orKw05s2bZ369bNky4/jjjze++tWvGlFx7733Gm3atDHmz59vfv3nP//ZKC0tNV599VUjymbPnm20bNnSeOqpp4xDhw4Zu3fvNq688kqjRYsWxtq1a/N9eAACuPXWW43jjjvOeOutt4xYLGasXr3aGDp0qLkdOXIkEq/l448/blRUVBhz5swxv545c6ZRXl5uPP/880aUyfuf/FwPP/ywceDAAaO6utq49tprjWbNmhnLly/P9+EBCHhO2LNnT2PWrFlGbW2tsX79emP06NFG3759jYMHD0bitZQxVcZWGWOFjLkyRskYHGUrV640mjRpYtx3333Gvn37jP379xuTJk0yysrKGj6HAIiGRx55xOjSpYsxY8YMc6ytqqoyzjzzTKNHjx7meVQUSFYgmYFkB0KyBMkU5H0kyjZt2mT+XHfccYexd+9e873vrrvuMkpKSow33ngj34cHREqJ/F++A3hkxh//+Ed16aWXmlcE46ZPn66uvPJK9emnn6rjjjsu9FPUpSLmhhtuUP/zP//TcPsFF1ygqqur1TvvvKOiasGCBeafUo0Yt2/fPvMKrvyst912Wx6PDkAQUu127rnnmlUlcX//+9/VhRdeqBYuXKhOPvnk0L+gxxxzjBo3bpxZkRh3xRVXqGXLlqmlS5eqqJJj37VrlzrjjDMSWk3JWCszo375y1/m9fgA+CfVbl/5yldU165dG26bO3euOu2009Ts2bPV2WefHfqXc/DgwWrIkCHqueeea7jtxhtvNN8z/vOf/6iokorQVatWqfPPP7/hNvlIKefxl19+ufrd736X1+MD4N/MmTPNcUrODeM++ugjddJJJ6lXXnlFXXzxxaF/OeV9Qc71rBXRP//5z9UTTzxhzswsLy9XUbRt2zb1z3/+U337299OuF1yHZltI1kPAH9oe1JArr766oTgW5SW1v2KS0pKVNjJm6yEFtIOxOqss84yp6wfOnRIRZWE3tbgO/47iW8AokOm3VuD76iNtWvWrFGff/65dqyV8FtaDUSVfHizBt/W308UfjcAGklB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MOH266+/Xi1cuFCNHTs2b8cGAIU01r744ovqnnvuSbj9pptuUnPmzDHPewEA6ZGxdObMmer2229PuP22225Tf//739WFF17ISwwUOcJvhMro0aPVzTffrCZNmqSqqqrM2x599FH19ttvm9UxzZo1y/chAkDkVVZWmjNtZFu9erV52/Tp09Urr7yi/vjHP6pWrVrl+xABIPJ69+5tzrSRbfHixeZtEsTIxUepVOzQoUO+DxEAIq9bt25mQcfDDz+s3n33XfO2f/3rX+bsm8cee0x17do134cIIM9KDCmrBULk4MGDaujQoapv375mFfiJJ56ofvazn6lf/OIX+T40ACgYR44cUaNGjVLNmzdXzz//vBoyZIi65ppr1AMPPJDvQwOAgiEftcaMGaN27dqlXnvtNTVs2DCzSvEPf/hDvg8NAArK+eefr9auXaveeecd8xxXCuteeOGFfB8WgBAg/EYoyRXb008/XXXq1En16tVLvffee6qsrCzfhwUABWXp0qXq5JNPNqsP27dvrz744ANVUVGR78MCgIIiYYxcYGzTpo05i3HZsmWqdevW+T4sACgoMnNcZjdKYYf4+OOPzfNbAKDtCULp1FNPVRdddJHavHmz+vWvf03wDQBZIGGMLLomY+2vfvUrgm8AyAKZzXjttdeaY+1dd91F8A0AWdCjRw/1ox/9yBxrZdY4wTeAOCq/EUpLlixRI0eOVOXl5eprX/ua+r//+798HxIAFGQ1orSZkmn5I0aMMPsjlpSU5PuwAKCgbNy40axGPHTokBowYIB6//33zXNcAEDmbN++3Rxrq6ur1THHHGOutcCaYQAEld8InZqaGjVhwgSz+vsf//iHuTDQc889l+/DAoCCEovFzLF24MCBZugt7aZkUSAAQGbJegqdO3c22/itXLlS3X///bzEAJBh119/vdnyZMGCBWYLlClTpvAaAzARfiN07rjjDrV+/Xr1zDPPqDPPPFP98Ic/VD/+8Y/Vli1b8n1oAFAwpKXUhx9+qKZNm2bOtLn11lvNTcZfAEBmPPHEE+rNN99Uf/rTn8yZNnfffbe5SS9aAEBmTJ8+Xb3yyivq2WefVSeccIJ5nvub3/xGLVq0iJcYAG1PEC5SEXPaaaepqVOnqquvvtq8bd++fWrw4MHqpJNOUi+99FK+DxEAIk8WW5OFLqX68KabbmqYdTN8+HCzOnH27Nm0PwGANK1bt85cW+GWW25pqECUWTcyu7G2tlbNmzePdW0AIE1ffPGF2e5EZtk88MAD5m3S0u/ss89WW7duNRd0b9q0Ka8zUMTo+Y3QkJD7xBNPVIMGDVIzZsxIuG/OnDnmm9fzzz+vLr/88rwdIwBEnYTcUul91FFHmWOrtce3VMeMHj1aPfroo+q6667L63ECQJRJyH3GGWeoAwcOqPnz5yf0+F61apVZ1CGBuMy4AQCkbuzYsWabEwm5KyoqGm6X2YxSRCeFHvfccw8vMVDECL8RGp9//rnZn2vMmDGqQ4cOSffLlNGysjLzfgBAajZv3qzmzp1rVh527do16f63337bXJTt61//Oi8xAKRox44d5noKcrGxV69eSfdL1bfsM27cOF5jAEjRnj17zBmLw4YNU3369Em6f+HCheaiw9/4xjd4jYEiRvgNAAAAAAAAACg4LHgJAAAAAAAAACg4hN8AAAAAAAAAgIJD+A0AAAAAAAAAKDiE3wAAAAAAAACAgkP4DQAAAAAAAAAoOITfAAAAAAAAAICCQ/gNAAAAAAAAACg4hN8AAAAAAAAAgIJD+A0AAAAAAAAAKDiE3wAAAAAAAACAgkP4DQAAAAAAAAAoOITfAAAAAAAAAICCQ/gNAAAAAAAAAFCF5v8DlZiMEVExuY0AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "out_coaxial.em_fields.b_field_xyz.struphy.plot.panels(\n", " x=\"e1\",\n", @@ -12486,7 +841,7 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "64", "metadata": {}, "source": [ "## Apply the workflow to another run\n", @@ -12506,7 +861,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.3)", + "display_name": "env (3.12.3.final.0)", "language": "python", "name": "python3" }, From 5ab34fa9aa58be3d67cd36b1ac0df1a82bb4bb53 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 23 Sep 2026 13:17:13 +0200 Subject: [PATCH 087/193] use new post-processing in feec_bcs notebook --- doc/conf.py | 2 +- tutorials/dev_tutorial_feec_bcs.ipynb | 47 ++++++++++----------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 98b5cae91..72f335fbb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -65,7 +65,7 @@ def _struphy_is_compiled(): # Notebooks are slow to run and many depend on compiled Struphy kernels, # so only execute them when Struphy has been compiled; otherwise reuse stored outputs. nbsphinx_execute = "auto" if _struphy_is_compiled() else "never" -# nbsphinx_kernel_name = 'local-env' # This is just for Stefan's local machine, where the system kernel does not work. +nbsphinx_kernel_name = 'local-env' # This is just for Stefan's local machine, where the system kernel does not work. napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True diff --git a/tutorials/dev_tutorial_feec_bcs.ipynb b/tutorials/dev_tutorial_feec_bcs.ipynb index 701000673..4bc15da8d 100644 --- a/tutorials/dev_tutorial_feec_bcs.ipynb +++ b/tutorials/dev_tutorial_feec_bcs.ipynb @@ -336,7 +336,7 @@ " derham_opts=derham_options,\n", " )\n", "\n", - "sim.run(one_time_step=True)" + "out = sim.run(one_time_step=True)" ] }, { @@ -344,23 +344,12 @@ "id": "24", "metadata": {}, "source": [ - "Post porcessing and loading the plotting data in `verbose` mode yields:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25", - "metadata": {}, - "outputs": [], - "source": [ - "sim.pproc()\n", - "sim.load_plotting_data()" + "Field data is post-processed lazily, the first time it is accessed through the `out` object returned by `run` (equivalently `sim.output`):" ] }, { "cell_type": "markdown", - "id": "26", + "id": "25", "metadata": {}, "source": [ "Let's extract the grid in `eta1` direction (corresponds to the `x`-direction for the unit cube mapping) and the solution `phi`:" @@ -369,28 +358,28 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "26", "metadata": {}, "outputs": [], "source": [ - "e1h = sim.plotting_data.grids_log[0]\n", + "e1h = out.grids_log[0]\n", "print(e1h)" ] }, { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "27", "metadata": {}, "outputs": [], "source": [ - "phi = sim.plotting_data.spline_values.em_fields.phi_log\n", + "phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0)\n", "print(phi)" ] }, { "cell_type": "markdown", - "id": "29", + "id": "28", "metadata": {}, "source": [ "We plot the solution to verify that it matches the exact solution:" @@ -399,19 +388,19 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "29", "metadata": {}, "outputs": [], "source": [ "plt.plot(e1, mfct_solution(e1), label=\"exact\")\n", - "plt.plot(e1h, phi.data[0.0][0][:, 0, 0], \"go\", label=\"numerical solution\")\n", + "plt.plot(e1h, phi.values, \"go\", label=\"numerical solution\")\n", "plt.xlabel('e1')\n", "plt.legend()" ] }, { "cell_type": "markdown", - "id": "31", + "id": "30", "metadata": {}, "source": [ "The boundary condition at `e1=1.0` was set to `dirichlet`, which was correctly captured by the solver. The left boundary condition at `e1=0.0` was set to `free`, so how did the solver capture the the Neumann boundary condition? The answer lies in the weak formulation of the problem, which is implemented in the `Poisson` model: find $\\phi \\in H^1$ such that\n", @@ -435,7 +424,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "31", "metadata": {}, "source": [ "## Lifting boundary conditions\n", @@ -448,7 +437,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -458,7 +447,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -479,7 +468,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -499,7 +488,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -509,7 +498,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -537,7 +526,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "37", "metadata": {}, "outputs": [], "source": [ From 06988269087073fa9c8bbad0a224a635e7d03d03 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 24 Sep 2026 08:18:52 +0200 Subject: [PATCH 088/193] adapt all tutorials to new pproc with out --- tutorials/tutorial_beltrami_sph.ipynb | 42 +-- tutorials/tutorial_dam_break_sph.ipynb | 16 +- tutorials/tutorial_gas_expansion_sph.ipynb | 48 +--- tutorials/tutorial_hagen_poiseuille_sph.ipynb | 14 +- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 26 +- tutorials/tutorial_maxwell.ipynb | 42 ++- tutorials/tutorial_particle_tracing.ipynb | 270 +++++------------- tutorials/tutorial_poisson.ipynb | 49 ++-- .../tutorial_pressureless_sph_shock.ipynb | 46 ++- .../tutorial_velocity_diffusion_sph.ipynb | 24 +- tutorials/tutorial_viscous_euler_sph.ipynb | 40 ++- 11 files changed, 201 insertions(+), 416 deletions(-) diff --git a/tutorials/tutorial_beltrami_sph.ipynb b/tutorials/tutorial_beltrami_sph.ipynb index e66e02bf6..d1596e2e5 100644 --- a/tutorials/tutorial_beltrami_sph.ipynb +++ b/tutorials/tutorial_beltrami_sph.ipynb @@ -284,13 +284,11 @@ "id": "17", "metadata": {}, "source": [ - "### Step 8: Run, Post-Process, and Visualize\n", + "### Step 8: Run and Visualize\n", "\n", "Execute the simulation pipeline in order:\n", "\n", - "1. `run()` to advance particles in time\n", - "2. `pproc()` to build post-processed outputs\n", - "3. `load_plotting_data()` to bring diagnostics into memory\n", + "1. `run()` to advance particles in time and get the post-processed `Output` object\n", "\n", "Then plot marker trajectories to inspect how the Beltrami-driven flow evolves." ] @@ -302,7 +300,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim.run()" + "out = sim.run()" ] }, { @@ -312,7 +310,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim.pproc()" + "print(out.info())" ] }, { @@ -322,7 +320,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()" + "print(out.cold_fluid.orbits)" ] }, { @@ -336,14 +334,14 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = sim.orbits.cold_fluid\n", + "orbits = np.asarray(out.orbits.cold_fluid)\n", "\n", "coloring = np.select(\n", " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", - "Nt = sim.t_grid.size - 1\n", + "Nt = out.time.size - 1\n", "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", @@ -456,7 +454,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim_tess.run()" + "out_tess = sim_tess.run()" ] }, { @@ -465,39 +463,19 @@ "id": "29", "metadata": {}, "outputs": [], - "source": [ - "sim_tess.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30", - "metadata": {}, - "outputs": [], - "source": [ - "sim_tess.load_plotting_data()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31", - "metadata": {}, - "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = sim_tess.orbits.cold_fluid\n", + "orbits = np.asarray(out_tess.orbits.cold_fluid)\n", "\n", "coloring = np.select(\n", " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", - "Nt = sim_tess.t_grid.size - 1\n", + "Nt = out_tess.time.size - 1\n", "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", diff --git a/tutorials/tutorial_dam_break_sph.ipynb b/tutorials/tutorial_dam_break_sph.ipynb index 0cb09045b..d6a0ea2fa 100644 --- a/tutorials/tutorial_dam_break_sph.ipynb +++ b/tutorials/tutorial_dam_break_sph.ipynb @@ -261,11 +261,8 @@ ")\n", "\n", "print(f\"Running 2D dam break: dt={dt}, Tend={Tend}\")\n", - "sim.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "sim.pproc()\n", - "print(\"Post-processing complete.\")" + "out = sim.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -283,15 +280,14 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()\n", - "\n", "# KDE density field: shape (Nt+1, pts_e1, pts_e2, 1)\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph\n", + "density = out.densities.euler_fluid.view_0.n\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", + "n_sph = density\n", "\n", "# Marker orbits: shape (Nt_orb, n_markers, n_attrs)\n", "# attrs for vdim=2: [x, y, z, v1, v2, w, diag, id]\n", - "orbits = np.asarray(sim.orbits.euler_fluid)\n", + "orbits = np.asarray(out.orbits.euler_fluid)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_gas_expansion_sph.ipynb b/tutorials/tutorial_gas_expansion_sph.ipynb index 433a8df9c..7080b7f9a 100644 --- a/tutorials/tutorial_gas_expansion_sph.ipynb +++ b/tutorials/tutorial_gas_expansion_sph.ipynb @@ -345,7 +345,7 @@ "source": [ "### Step 9: Initialize, Run, and Load Results\n", "\n", - "Attach the Gaussian background and execute the standard workflow: `run()`, `pproc()`, `load_plotting_data()`.\n", + "Attach the Gaussian background and execute the standard workflow: `out = sim.run()`.\n", "\n", "Performance note: early time steps are typically slower because particles are highly concentrated; runtime usually improves as the cloud expands. Running from a console script (especially with MPI/GPU support) is faster than notebook execution." ] @@ -368,32 +368,12 @@ "metadata": {}, "outputs": [], "source": [ - "sim.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23", - "metadata": {}, - "outputs": [], - "source": [ - "sim.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": {}, - "outputs": [], - "source": [ - "sim.load_plotting_data()" + "out = sim.run()" ] }, { "cell_type": "markdown", - "id": "25", + "id": "23", "metadata": {}, "source": [ "### Step 10: Inspect Stored Outputs\n", @@ -404,7 +384,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -416,26 +396,28 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", + "density = out.densities.euler_fluid.view_0.n\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", - "bc_x = sim.f.euler_fluid.e1_e2_density.grid_e1\n", - "bc_y = sim.f.euler_fluid.e1_e2_density.grid_e2\n", + "f_e1e2 = out.distributions.euler_fluid.e1_e2_density.f\n", + "bc_x = np.asarray(f_e1e2[\"e1\"])\n", + "bc_y = np.asarray(f_e1e2[\"e2\"])\n", "\n", "# markers\n", - "orbits = sim.orbits.euler_fluid\n", + "orbits = np.asarray(out.orbits.euler_fluid)\n", "positions = orbits[0, :, :3]\n", "weights = orbits[0, :, 6]\n", "\n", "# binning and sph eval\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph[0]\n", - "f_bin = sim.f.euler_fluid.e1_e2_density.f_binned[0]" + "n_sph = np.asarray(density)[0]\n", + "f_bin = np.asarray(f_e1e2)[0]" ] }, { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -494,12 +476,12 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "26", "metadata": {}, "outputs": [], "source": [ "dt = time_opts.dt\n", - "Nt = sim.t_grid.size - 1\n", + "Nt = out.time.size - 1\n", "\n", "positions = orbits[:, :, :3]\n", "\n", diff --git a/tutorials/tutorial_hagen_poiseuille_sph.ipynb b/tutorials/tutorial_hagen_poiseuille_sph.ipynb index ada024c24..d383acb59 100644 --- a/tutorials/tutorial_hagen_poiseuille_sph.ipynb +++ b/tutorials/tutorial_hagen_poiseuille_sph.ipynb @@ -259,11 +259,8 @@ ")\n", "\n", "print(f\"Running Hagen-Poiseuille flow: dt={dt}, Tend={Tend}\")\n", - "sim.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "sim.pproc()\n", - "print(\"Post-processing complete.\")" + "out = sim.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -281,10 +278,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()\n", - "\n", - "e2_grid = sim.f.euler_fluid.e2_current_1.grid_e2 # logical y in [0, 1]\n", - "j1_binned = sim.f.euler_fluid.e2_current_1.f_binned # shape (Nt+1, n_bins)\n", + "j1_binned = out.distributions.euler_fluid.e2_current_1.f\n", + "e2_grid = j1_binned[\"e2\"] # logical y in [0, 1]\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", @@ -296,6 +291,7 @@ "u_exact = g_x / (2.0 * mu) * y_np * (H - y_np)\n", "u_max_exact = np.max(u_exact)\n", "\n", + "j1_binned = np.asarray(j1_binned)\n", "u_num_final = np.asarray(j1_binned[-1, :]).flatten()\n", "u_max_num = np.max(u_num_final)\n", "\n", diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index 5ff5496cc..277e47e47 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -217,12 +217,8 @@ ")\n", "\n", "print(f\"Running simulation: dt={time_opts.dt}, Tend={time_opts.Tend}\")\n", - "sim.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "# Post-processing\n", - "sim.pproc()\n", - "print(\"Post-processing complete.\")" + "out = sim.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -244,12 +240,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", + "# Field data is post-processed lazily on first access through `out`\n", "# Extract velocity and pressure time-series\n", - "u_of_t = sim.spline_values.mhd.velocity_log.data\n", - "p_of_t = sim.spline_values.mhd.pressure_log.data\n", + "u_of_t = out.fields.mhd.velocity\n", + "p_of_t = out.fields.mhd.pressure\n", "\n", "# Dispersion relation parameters\n", "gamma = 5 / 3 # Adiabatic index\n", @@ -263,14 +257,14 @@ "}\n", "\n", "# 1. Shear Alfvén wave analysis from velocity\n", + "# physical=True uses the mapped X/Y/Z grid along the fft direction, matching\n", + "# the domain's physical z-extent\n", "print(\"\\n=== Shear Alfvén Wave Analysis ===\")\n", "_1, _2, _3, coeffs_alfven = power_spectrum_2d(\n", " u_of_t,\n", - " \"velocity_log\",\n", - " grids=sim.grids_log,\n", - " grids_mapped=sim.grids_phy,\n", " component=0,\n", " slice_at=[0, 0, None],\n", + " physical=True,\n", " do_plot=True,\n", " disp_name=\"MHDhomogenSlab\",\n", " disp_params=disp_params,\n", @@ -304,11 +298,9 @@ "print(\"=== Slow and Fast Magnetosonic Wave Analysis ===\")\n", "_1, _2, _3, coeffs_sonic = power_spectrum_2d(\n", " p_of_t,\n", - " \"pressure_log\",\n", - " grids=sim.grids_log,\n", - " grids_mapped=sim.grids_phy,\n", " component=0,\n", " slice_at=[0, 0, None],\n", + " physical=True,\n", " do_plot=True,\n", " disp_name=\"MHDhomogenSlab\",\n", " disp_params=disp_params,\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index 85af3dc2b..d27e6fee4 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -235,12 +235,8 @@ ")\n", "\n", "print(f\"Running simulation: dt={time_opts.dt}, Tend={time_opts.Tend}\")\n", - "sim.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "# Post-processing\n", - "sim.pproc()\n", - "print(\"Post-processing complete.\")" + "out = sim.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -262,21 +258,18 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", + "# Field data is post-processed lazily on first access through `out`\n", "# Extract electric field time-series\n", - "E_of_t = sim.spline_values.em_fields.e_field_log.data\n", + "E_of_t = out.fields.em_fields.e_field\n", "\n", - "# Compute power spectrum and fit dispersion relation\n", + "# Compute power spectrum and fit dispersion relation (physical=True uses the\n", + "# mapped X/Y/Z grid along the fft direction, matching the domain's physical z-extent)\n", "print(\"\\n=== Light Wave Dispersion Analysis ===\")\n", "_1, _2, _3, coeffs = power_spectrum_2d(\n", " E_of_t,\n", - " \"e_field_log\",\n", - " grids=sim.grids_log,\n", - " grids_mapped=sim.grids_phy,\n", " component=0,\n", " slice_at=[0, 0, None],\n", + " physical=True,\n", " do_plot=True,\n", " disp_name=\"Maxwell1D\",\n", " fit_branches=1,\n", @@ -568,11 +561,11 @@ ")\n", "\n", "print(f\"Running coaxial mode simulation: dt={time_opts.dt}, Tend={time_opts.Tend}\")\n", - "sim.run()\n", + "out = sim.run()\n", "print(\"Simulation complete.\")\n", "\n", "# Post-processing (with physical=True to extract physical fields)\n", - "sim.pproc(physical=True)\n", + "out.pproc(physical=True)\n", "print(\"Post-processing complete.\")" ] }, @@ -593,14 +586,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", "# Extract time and field data\n", - "t_grid = sim.t_grid\n", - "grids_phy = sim.grids_phy\n", - "e_field_phy = sim.spline_values.em_fields.e_field_phy.data\n", - "b_field_phy = sim.spline_values.em_fields.b_field_phy.data\n", + "t_grid = out.time\n", + "grids_phy = out.grids_phy\n", + "e_field_phy = out.fields.em_fields.e_field_xyz\n", + "b_field_phy = out.fields.em_fields.b_field_xyz\n", "\n", "# Extract coordinate arrays in the first (r-θ) plane\n", "X = grids_phy[0][:, :, 0] # Radial coordinate (Cartesian x for plotting)\n", @@ -686,9 +676,9 @@ "t_end = t_grid[-1]\n", "\n", "# Numerical fields (Cartesian components)\n", - "Ex_num = e_field_phy[t_end][0][:, :, 0]\n", - "Ey_num = e_field_phy[t_end][1][:, :, 0]\n", - "Bz_num = b_field_phy[t_end][2][:, :, 0]\n", + "Ex_num = e_field_phy.isel(t=-1, component=0, e3=0).values\n", + "Ey_num = e_field_phy.isel(t=-1, component=1, e3=0).values\n", + "Bz_num = b_field_phy.isel(t=-1, component=2, e3=0).values\n", "\n", "# Analytical fields\n", "Er_analytic = E_r_analytic(X, Y, grids_phy[0], m, t_end)\n", diff --git a/tutorials/tutorial_particle_tracing.ipynb b/tutorials/tutorial_particle_tracing.ipynb index a793f94b2..c87421399 100644 --- a/tutorials/tutorial_particle_tracing.ipynb +++ b/tutorials/tutorial_particle_tracing.ipynb @@ -279,7 +279,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim.run()" + "out = sim.run()" ] }, { @@ -299,7 +299,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim_2.run()" + "out_2 = sim_2.run()" ] }, { @@ -309,7 +309,7 @@ "source": [ "### Step 7: compare initial particle distributions\n", "\n", - "Post-process both runs, load the generated data, and plot initial particle positions on a cylinder cross section.\n", + "Load the generated data and plot initial particle positions on a cylinder cross section.\n", "\n", "This plot is the key check for understanding how loading in logical space differs from loading in physical space:" ] @@ -320,53 +320,13 @@ "id": "22", "metadata": {}, "outputs": [], - "source": [ - "sim.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23", - "metadata": {}, - "outputs": [], - "source": [ - "sim_2.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": {}, - "outputs": [], - "source": [ - "sim.load_plotting_data()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25", - "metadata": {}, - "outputs": [], - "source": [ - "sim_2.load_plotting_data()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26", - "metadata": {}, - "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", "\n", "fig = plt.figure(figsize=(10, 6))\n", "\n", - "orbits = sim.orbits.kinetic_ions\n", - "orbits_uni = sim_2.orbits.kinetic_ions\n", + "orbits = out.orbits.kinetic_ions.values\n", + "orbits_uni = out_2.orbits.kinetic_ions.values\n", "\n", "plt.subplot(1, 2, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -391,7 +351,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "23", "metadata": {}, "source": [ "### Optional: quasi-uniform marker loading (Sobol)\n", @@ -404,7 +364,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -429,13 +389,11 @@ "# initial conditions (background + perturbation)\n", "model_3.kinetic_ions.var.add_background(background)\n", "\n", - "sim_3.run()\n", - "sim_3.pproc()\n", - "sim_3.load_plotting_data()\n", + "out_3 = sim_3.run()\n", "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = sim_3.orbits.kinetic_ions\n", + "orbits_standard = out_3.orbits.kinetic_ions.values\n", "\n", "plt.subplot(1, 3, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -470,7 +428,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "25", "metadata": {}, "source": [ "### Optional: antithetic Sobol loading\n", @@ -481,7 +439,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -506,13 +464,11 @@ "# initial conditions (background + perturbation)\n", "model_3.kinetic_ions.var.add_background(background)\n", "\n", - "sim_3.run()\n", - "sim_3.pproc()\n", - "sim_3.load_plotting_data()\n", + "out_3 = sim_3.run()\n", "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = sim_3.orbits.kinetic_ions\n", + "orbits_standard = out_3.orbits.kinetic_ions.values\n", "\n", "plt.subplot(1, 3, 1)\n", "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", @@ -547,7 +503,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "27", "metadata": {}, "source": [ "## Part 2: Reflecting boundary conditions\n", @@ -560,7 +516,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -572,7 +528,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -591,7 +547,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -605,7 +561,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "31", "metadata": {}, "source": [ "Set propagator options and initial conditions as in the previous section:" @@ -614,7 +570,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -631,48 +587,28 @@ }, { "cell_type": "markdown", - "id": "37", - "metadata": {}, - "source": [ - "Run the simulation, post-process the output, and plot the resulting trajectories to verify reflections at the radial boundary:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "38", - "metadata": {}, - "outputs": [], - "source": [ - "sim.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "39", + "id": "33", "metadata": {}, - "outputs": [], "source": [ - "sim.pproc()" + "Run the simulation and plot the resulting trajectories to verify reflections at the radial boundary:" ] }, { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "34", "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()" + "out = sim.run()" ] }, { "cell_type": "markdown", - "id": "41", + "id": "35", "metadata": {}, "source": [ - "Under `sim.orbits[]`, Struphy stores orbit data in a 3D NumPy array:\n", + "Under `out.orbits.`, Struphy stores orbit data in a 3D array (indexable as NumPy via `.values`):\n", "\n", "- axis 0: time step,\n", "- axis 1: particle index,\n", @@ -684,11 +620,11 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "36", "metadata": {}, "outputs": [], "source": [ - "orbits = sim.orbits.kinetic_ions\n", + "orbits = out.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]\n", @@ -698,7 +634,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -728,7 +664,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "38", "metadata": {}, "source": [ "## Part 3: Particles in a cylinder with a magnetic field\n", @@ -741,7 +677,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -753,7 +689,7 @@ }, { "cell_type": "markdown", - "id": "46", + "id": "40", "metadata": {}, "source": [ "To evaluate the equilibrium efficiently in particle kernels, project it onto the spline basis. This requires creating a De Rham complex:" @@ -762,7 +698,7 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -772,7 +708,7 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "42", "metadata": {}, "source": [ "Create a lightweight model instance and configure species options.\n", @@ -783,7 +719,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "43", "metadata": {}, "outputs": [], "source": [ @@ -798,7 +734,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -825,50 +761,30 @@ }, { "cell_type": "markdown", - "id": "51", - "metadata": {}, - "source": [ - "Run the case, post-process results, load the data, and plot trajectories to see how the magnetic field modifies particle motion:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "52", - "metadata": {}, - "outputs": [], - "source": [ - "sim_withB.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53", + "id": "45", "metadata": {}, - "outputs": [], "source": [ - "sim_withB.pproc()" + "Run the case, load the data, and plot trajectories to see how the magnetic field modifies particle motion:" ] }, { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "46", "metadata": {}, "outputs": [], "source": [ - "sim_withB.load_plotting_data()" + "out_withB = sim_withB.run()" ] }, { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "47", "metadata": {}, "outputs": [], "source": [ - "orbits = sim_withB.orbits.kinetic_ions\n", + "orbits = out_withB.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -877,7 +793,7 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -905,7 +821,7 @@ }, { "cell_type": "markdown", - "id": "57", + "id": "49", "metadata": {}, "source": [ "## Part 4: Particles in a tokamak equilibrium\n", @@ -918,7 +834,7 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -930,7 +846,7 @@ }, { "cell_type": "markdown", - "id": "59", + "id": "51", "metadata": {}, "source": [ "`EQDSKequilibrium` inherits from `AxisymmMHDequilibrium` and `CartesianMHDequilibrium`, so in principle you could choose different mappings.\n", @@ -941,7 +857,7 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "52", "metadata": {}, "outputs": [], "source": [ @@ -954,7 +870,7 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "53", "metadata": {}, "source": [ "The `Tokamak` domain is a `PoloidalSplineTorus`. The coordinate relation between Cartesian $(x,y,z)$ and Tokamak $(R,Z,\\phi)$ variables is\n", @@ -1008,7 +924,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -1026,7 +942,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -1041,7 +957,7 @@ { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "56", "metadata": {}, "outputs": [], "source": [ @@ -1058,7 +974,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "57", "metadata": {}, "outputs": [], "source": [ @@ -1115,7 +1031,7 @@ }, { "cell_type": "markdown", - "id": "66", + "id": "58", "metadata": {}, "source": [ "As before, build a De Rham complex so the equilibrium can be projected onto the spline basis:" @@ -1124,7 +1040,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -1138,7 +1054,7 @@ }, { "cell_type": "markdown", - "id": "68", + "id": "60", "metadata": {}, "source": [ "For this example we run 15000 time steps using a second-order splitting scheme:" @@ -1147,7 +1063,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -1156,7 +1072,7 @@ }, { "cell_type": "markdown", - "id": "70", + "id": "62", "metadata": {}, "source": [ "Set up a simulation with four hand-picked particle initial conditions to study representative orbit types in this equilibrium:" @@ -1165,7 +1081,7 @@ { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -1184,7 +1100,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -1221,41 +1137,21 @@ { "cell_type": "code", "execution_count": null, - "id": "73", - "metadata": {}, - "outputs": [], - "source": [ - "sim_asdex.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74", - "metadata": {}, - "outputs": [], - "source": [ - "sim_asdex.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "75", + "id": "65", "metadata": {}, "outputs": [], "source": [ - "sim_asdex.load_plotting_data()" + "out_asdex = sim_asdex.run()" ] }, { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "66", "metadata": {}, "outputs": [], "source": [ - "orbits = sim_asdex.orbits.kinetic_ions\n", + "orbits = out_asdex.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -1264,7 +1160,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -1289,7 +1185,7 @@ }, { "cell_type": "markdown", - "id": "78", + "id": "68", "metadata": {}, "source": [ "## Part 5: Guiding centers in a tokamak equilibrium\n", @@ -1300,7 +1196,7 @@ { "cell_type": "code", "execution_count": null, - "id": "79", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -1313,7 +1209,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -1325,7 +1221,7 @@ { "cell_type": "code", "execution_count": null, - "id": "81", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -1369,7 +1265,7 @@ { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "72", "metadata": {}, "outputs": [], "source": [ @@ -1427,41 +1323,21 @@ { "cell_type": "code", "execution_count": null, - "id": "83", - "metadata": {}, - "outputs": [], - "source": [ - "sim_gc.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "84", - "metadata": {}, - "outputs": [], - "source": [ - "sim_gc.pproc()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "85", + "id": "73", "metadata": {}, "outputs": [], "source": [ - "sim_gc.load_plotting_data()" + "out_gc = sim_gc.run()" ] }, { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "74", "metadata": {}, "outputs": [], "source": [ - "orbits = sim_gc.orbits.kinetic_ions\n", + "orbits = out_gc.orbits.kinetic_ions.values\n", "\n", "Nt = orbits.shape[0]\n", "Np = orbits.shape[1]" @@ -1470,7 +1346,7 @@ { "cell_type": "code", "execution_count": null, - "id": "87", + "id": "75", "metadata": {}, "outputs": [], "source": [ diff --git a/tutorials/tutorial_poisson.ipynb b/tutorials/tutorial_poisson.ipynb index 506447def..5199eced9 100644 --- a/tutorials/tutorial_poisson.ipynb +++ b/tutorials/tutorial_poisson.ipynb @@ -135,9 +135,7 @@ " )\n", "\n", "# For a stationary Poisson solve, one step is enough\n", - "sim.run(one_time_step=True)\n", - "sim.pproc()\n", - "sim.load_plotting_data()" + "out = sim.run(one_time_step=True)" ] }, { @@ -148,10 +146,9 @@ "outputs": [], "source": [ "# Extract 1D line data and compare to analytic solution\n", - "x = sim.grids_phy[0][:, 0, 0]\n", + "x = out.grids_phy[0][:, 0, 0]\n", "\n", - "t_last = max(sim.spline_values.em_fields.phi_log.data.keys())\n", - "phi_num = sim.spline_values.em_fields.phi_log.data[t_last][0][:, 0, 0]\n", + "phi_num = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0).values\n", "phi_ref = phi_exact(x, 0.0, 0.0)\n", "\n", "err = phi_num - phi_ref\n", @@ -281,9 +278,7 @@ " derham_opts=derham_opts2,\n", " )\n", "\n", - "sim2.run(one_time_step=True)\n", - "sim2.pproc()\n", - "sim2.load_plotting_data()" + "out2 = sim2.run(one_time_step=True)" ] }, { @@ -294,11 +289,10 @@ "outputs": [], "source": [ "# 2D diagnostics and plots\n", - "t2_last = max(sim2.spline_values.em_fields.phi_log.data.keys())\n", - "X = sim2.grids_phy[0][:, :, 0]\n", - "Y = sim2.grids_phy[1][:, :, 0]\n", + "X = out2.grids_phy[0][:, :, 0]\n", + "Y = out2.grids_phy[1][:, :, 0]\n", "\n", - "phi2_num = sim2.spline_values.em_fields.phi_log.data[t2_last][0][:, :, 0]\n", + "phi2_num = out2.fields.em_fields.phi.isel(t=-1, e3=0).values\n", "phi2_ref = phi2_exact(X, Y, 0.0)\n", "err2 = phi2_num - phi2_ref\n", "err2_max = np.max(np.abs(err2))\n", @@ -417,9 +411,7 @@ " derham_opts=derham_opts3,\n", " )\n", "\n", - "sim3.run(one_time_step=True)\n", - "sim3.pproc()\n", - "sim3.load_plotting_data()" + "out3 = sim3.run(one_time_step=True)" ] }, { @@ -430,11 +422,10 @@ "outputs": [], "source": [ "# Annulus diagnostics and plots in physical coordinates only\n", - "t3_last = max(sim3.spline_values.em_fields.phi_log.data.keys())\n", - "X3 = sim3.grids_phy[0][:, :, 0]\n", - "Y3 = sim3.grids_phy[1][:, :, 0]\n", + "X3 = out3.grids_phy[0][:, :, 0]\n", + "Y3 = out3.grids_phy[1][:, :, 0]\n", "\n", - "phi3_num = sim3.spline_values.em_fields.phi_log.data[t3_last][0][:, :, 0]\n", + "phi3_num = out3.fields.em_fields.phi.isel(t=-1, e3=0).values\n", "phi3_ref = phi3_exact(X3, Y3, 0.0)\n", "err3 = phi3_num - phi3_ref\n", "err3_max = np.max(np.abs(err3))\n", @@ -582,9 +573,7 @@ ")\n", "\n", "# Run the full time-dependent simulation\n", - "sim4.run()\n", - "sim4.pproc()\n", - "sim4.load_plotting_data()" + "out4 = sim4.run()" ] }, { @@ -595,9 +584,17 @@ "outputs": [], "source": [ "# Extract and visualize time-dependent results\n", - "x4 = sim4.grids_phy[0][:, 0, 0]\n", - "phi4_log = sim4.spline_values.em_fields.phi_log.data\n", - "source4_log = sim4.spline_values.em_fields.source_log.data\n", + "# Post-processed products are xarray.DataArrays; reconstruct the legacy\n", + "# {time: [component_arrays]} shape so the plotting code below is unchanged.\n", + "def _field_data(array):\n", + " values = np.asarray(array)\n", + " if \"component\" not in array.dims:\n", + " values = values[:, None]\n", + " return {float(t): list(comps) for t, comps in zip(np.asarray(array[\"t\"]), values)}\n", + "\n", + "x4 = out4.grids_phy[0][:, 0, 0]\n", + "phi4_log = _field_data(out4.fields.em_fields.phi)\n", + "source4_log = _field_data(out4.fields.em_fields.source)\n", "t_times = sorted(phi4_log.keys())\n", "\n", "print(f\"Solution saved at {len(t_times)} time points\")\n", diff --git a/tutorials/tutorial_pressureless_sph_shock.ipynb b/tutorials/tutorial_pressureless_sph_shock.ipynb index 5f9a3f8f5..c78e18d2d 100644 --- a/tutorials/tutorial_pressureless_sph_shock.ipynb +++ b/tutorials/tutorial_pressureless_sph_shock.ipynb @@ -484,7 +484,7 @@ "print(\"\\n\" + \"=\"*60)\n", "print(\"Running LARGE-AMPLITUDE RIEMANN SHOCK simulation...\")\n", "print(\"=\"*60)\n", - "sim.run()\n", + "out = sim.run()\n", "print(\"Simulation completed.\")" ] }, @@ -493,26 +493,14 @@ "id": "22", "metadata": {}, "source": [ - "## Step 9: Post-Process and Load Results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Post-processing...\")\n", - "sim.pproc()\n", - "print(\"Loading plotting data...\")\n", - "sim.load_plotting_data()\n", - "print(\"Data loaded.\")" + "## Step 9: Access Results\n", + "\n", + "Field data is post-processed lazily, the first time it is accessed through the `out` object returned by `run`." ] }, { "cell_type": "markdown", - "id": "24", + "id": "23", "metadata": {}, "source": [ "## Step 10: Visualize Density and Velocity Evolution\n", @@ -523,14 +511,14 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "24", "metadata": {}, "outputs": [], "source": [ "# Extract binned outputs\n", - "rho_binned = sim.f.cold_fluid.e1_density.f_binned\n", - "current1_binned = sim.f.cold_fluid.e1_current_1.f_binned\n", - "t_grid = sim.t_grid\n", + "rho_binned = np.asarray(out.distributions.cold_fluid.e1_density.f)\n", + "current1_binned = np.asarray(out.distributions.cold_fluid.e1_current_1.f)\n", + "t_grid = out.time\n", "eta1_bins = np.linspace(0, 1, n_bins + 1)[:-1] # bin centers\n", "\n", "# Reconstruct velocity from binned current and density: u1 = j1 / rho\n", @@ -597,7 +585,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "25", "metadata": {}, "source": [ "# Analytical Solutions and Comparison\n", @@ -607,7 +595,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "26", "metadata": {}, "source": [ "## Analytical Solutions: Rankine-Hugoniot Conditions\n", @@ -648,7 +636,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -682,7 +670,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "28", "metadata": {}, "source": [ "## Compare Numerical Solution with Analytical Prediction\n", @@ -695,7 +683,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -743,7 +731,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "30", "metadata": {}, "source": [ "## Compare Velocity at Final Time (Numerical vs Analytical)\n", @@ -754,7 +742,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -787,7 +775,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "32", "metadata": {}, "source": [ "# Discussion: Shock Dynamics and Density Evolution\n", diff --git a/tutorials/tutorial_velocity_diffusion_sph.ipynb b/tutorials/tutorial_velocity_diffusion_sph.ipynb index 056b93324..47ebfbf3e 100644 --- a/tutorials/tutorial_velocity_diffusion_sph.ipynb +++ b/tutorials/tutorial_velocity_diffusion_sph.ipynb @@ -245,11 +245,8 @@ ")\n", "\n", "print(f\"Running velocity diffusion: dt={dt}, Tend={Tend}, {ppb * nx} particles\")\n", - "sim.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "sim.pproc()\n", - "print(\"Post-processing complete.\")" + "out = sim.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -257,7 +254,9 @@ "id": "12", "metadata": {}, "source": [ - "### Load Diagnostics" + "### Access Diagnostics\n", + "\n", + "Field data is post-processed lazily, the first time it is accessed through the `out` object returned by `run`." ] }, { @@ -267,13 +266,12 @@ "metadata": {}, "outputs": [], "source": [ - "sim.load_plotting_data()\n", - "\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = sim.f.euler_fluid.e1_current_1.f_binned # shape (Nt+1, n_bins)\n", - "e1_binned = sim.f.euler_fluid.e1_current_1.grid_e1 # logical x in [0, 1]\n", - "n_binned = sim.f.euler_fluid.e1_density.f_binned # shape (Nt+1, n_bins)\n", + "density = out.densities.euler_fluid.view_0.n\n", + "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", + "n_sph = np.asarray(density) # shape (Nt+1, plot_pts, 1, 1)\n", + "j1_binned = np.asarray(out.distributions.euler_fluid.e1_current_1.f) # shape (Nt+1, n_bins)\n", + "e1_binned = np.asarray(out.distributions.euler_fluid.e1_current_1.f[\"e1\"]) # logical x in [0, 1]\n", + "n_binned = np.asarray(out.distributions.euler_fluid.e1_density.f) # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_viscous_euler_sph.ipynb b/tutorials/tutorial_viscous_euler_sph.ipynb index 6d070a35b..d053ca328 100644 --- a/tutorials/tutorial_viscous_euler_sph.ipynb +++ b/tutorials/tutorial_viscous_euler_sph.ipynb @@ -257,12 +257,8 @@ ")\n", "\n", "print(f\"Running SPH sound wave simulation: dt={dt}, Tend={Tend}, algo={split_algo}\")\n", - "sim.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "# Post-processing\n", - "sim.pproc()\n", - "print(\"Post-processing complete.\")" + "out = sim.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -272,7 +268,7 @@ "source": [ "### Diagnostics: Round-Trip Sound Wave Verification\n", "\n", - "Extract the particle density field at initial and final times, and compute the maximum absolute error as a verification metric." + "Extract the particle density field at initial and final times (post-processed lazily through the `out` object returned by `run`), and compute the maximum absolute error as a verification metric." ] }, { @@ -282,12 +278,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Load plotting data\n", - "sim.load_plotting_data()\n", - "\n", "# Extract particle positions and density\n", - "ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim.n_sph.euler_fluid.view_0.n_sph\n", + "density = out.densities.euler_fluid.view_0.n\n", + "ee1, ee2, ee3 = xp.meshgrid(\n", + " xp.asarray(density.e1), xp.asarray(density.e2), xp.asarray(density.e3), indexing=\"ij\"\n", + ")\n", + "n_sph = xp.asarray(density)\n", "\n", "# Physical coordinates\n", "x = ee1 * r1\n", @@ -685,11 +681,8 @@ ")\n", "\n", "print(f\"Running damped sound wave: dt={dt}, Tend={Tend}\")\n", - "sim_damp.run()\n", - "print(\"Simulation complete.\")\n", - "\n", - "sim_damp.pproc()\n", - "print(\"Post-processing complete.\")" + "out_damp = sim_damp.run()\n", + "print(\"Simulation complete.\")" ] }, { @@ -699,7 +692,7 @@ "source": [ "### Diagnostics: Density Snapshots\n", "\n", - "First, inspect the density field $\\delta\\rho = \\rho - 1$ at twelve equally spaced times during the first oscillation period. The amplitude should visibly shrink over successive periods." + "First, inspect the density field $\\delta\\rho = \\rho - 1$ at twelve equally spaced times during the first oscillation period (post-processed lazily through the `out_damp` object returned by `run`). The amplitude should visibly shrink over successive periods." ] }, { @@ -711,12 +704,11 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "sim_damp.load_plotting_data()\n", - "\n", - "ee1, ee2, ee3 = sim_damp.n_sph.euler_fluid.view_0.grid_n_sph\n", - "n_sph = sim_damp.n_sph.euler_fluid.view_0.n_sph # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = sim_damp.f.euler_fluid.e1_current_1.f_binned # shape (Nt+1, n_bins)\n", - "e1_binned = sim_damp.f.euler_fluid.e1_current_1.grid_e1 # logical x in [0,1]\n", + "density_damp = out_damp.densities.euler_fluid.view_0.n\n", + "ee1, ee2, ee3 = np.meshgrid(density_damp.e1, density_damp.e2, density_damp.e3, indexing=\"ij\")\n", + "n_sph = np.asarray(density_damp) # shape (Nt+1, plot_pts, 1, 1)\n", + "j1_binned = np.asarray(out_damp.distributions.euler_fluid.e1_current_1.f) # shape (Nt+1, n_bins)\n", + "e1_binned = np.asarray(out_damp.distributions.euler_fluid.e1_current_1.f[\"e1\"]) # logical x in [0,1]\n", "\n", "Nt = j1_binned.shape[0] - 1\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", From c77178b8ae386439b6dbec3a08c85ef098aae669 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 10:31:33 +0200 Subject: [PATCH 089/193] Extend the to_dict() method --- src/struphy/models/base.py | 39 +++++++++-- src/struphy/models/species.py | 38 ++++++++++ src/struphy/models/variables.py | 18 +++++ src/struphy/simulation/sim.py | 7 +- src/struphy/simulation/tests/test_output.py | 77 ++++++++++++++++++++- 5 files changed, 171 insertions(+), 8 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 5a7ffe976..c422ed6df 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -1,6 +1,7 @@ import logging import os from abc import ABCMeta, abstractmethod +from dataclasses import fields, is_dataclass from textwrap import indent import cunumpy as xp @@ -927,10 +928,9 @@ def generate_default_parameter_file( return path def to_dict(self) -> dict: - """Serialize the model class and the arguments passed to its ``__init__``. + """Serialize the model constructor, variables and propagator options. - Configuration applied after construction (markers, backgrounds, perturbations, - propagator options) is not part of this dictionary. + Backgrounds and perturbations are not part of this dictionary. """ params = {} for key, value in self.params.items(): @@ -939,7 +939,38 @@ def to_dict(self) -> dict: elif not isinstance(value, (bool, int, float, str, tuple, list, type(None))): raise TypeError(f"cannot serialize argument {key}={value!r} of {self.__class__.__name__}") params[key] = value - return {"model": self.__class__.__name__, "params": params} + return { + "model": self.__class__.__name__, + "params": params, + "species": {name: species.to_dict() for name, species in self.species.items()}, + "propagator_options": { + name: self._serialize_propagator_option(prop.options) + for name, prop in vars(self.propagators).items() + if isinstance(prop, Propagator) + }, + } + + def _serialize_propagator_option(self, value): + """Convert nested option dataclasses and variable references to JSON data.""" + if is_dataclass(value) and not isinstance(value, type): + return { + field.name: self._serialize_propagator_option(getattr(value, field.name)) + for field in fields(value) + if field.init + } + if isinstance(value, dict): + variable_names = { + id(variable): f"{species_name}.{variable_name}" + for species_name, species in self.species.items() + for variable_name, variable in species.variables.items() + } + return { + variable_names[id(key)] if id(key) in variable_names else key: self._serialize_propagator_option(item) + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return [self._serialize_propagator_option(item) for item in value] + return value @classmethod def from_dict(cls, dct) -> "StruphyModel": diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 17daabe84..c940dbb58 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -1,6 +1,7 @@ import logging import warnings from abc import ABCMeta, abstractmethod +from dataclasses import fields, is_dataclass import cunumpy as xp from feectools.ddm.mpi import mpi as MPI @@ -20,6 +21,17 @@ logger = logging.getLogger("struphy") +def _serialize_parameter(value): + """Convert nested parameter dataclasses and sequences to JSON-compatible data.""" + if is_dataclass(value) and not isinstance(value, type): + return {field.name: _serialize_parameter(getattr(value, field.name)) for field in fields(value) if field.init} + if isinstance(value, dict): + return {key: _serialize_parameter(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_serialize_parameter(item) for item in value] + return value + + class Species(metaclass=ABCMeta): """ Abstract base class representing a single plasma species in a StruphyModel. @@ -74,6 +86,18 @@ def __repr__(self): out += f"{v}\n" return out + def to_dict(self) -> dict: + """Serialize species parameters and its variables.""" + return { + "class": type(self).__name__, + "charge_number": self.charge_number, + "mass_number": self.mass_number, + "alpha": self.alpha, + "epsilon": self.epsilon, + "kappa": self.kappa, + "variables": {name: variable.to_dict() for name, variable in self.variables.items()}, + } + def init_variables( self, charge_number: int = 1, @@ -279,6 +303,20 @@ class ParticleSpecies(Species): >>> electrons.set_markers(loading_params=load_params) """ + def to_dict(self) -> dict: + data = super().to_dict() + for name in ( + "loading_params", + "weights_params", + "boundary_params", + "sorting_params", + "saving_params", + "bufsize", + ): + if hasattr(self, name): + data[name] = _serialize_parameter(getattr(self, name)) + return data + def set_markers( self, loading_params: LoadingParameters = None, diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 9aa86a2e0..28df1201c 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -93,6 +93,14 @@ def estimate_mem(self) -> int: def __repr__(self): return f"{self.__class__.__name__} ({self.space})" + def to_dict(self) -> dict: + """Serialize the variable's discretization and output settings.""" + return { + "class": type(self).__name__, + "space": self.space, + "save_data": self.save_data, + } + @property def backgrounds(self): """The static background. Multiple backgrounds can be defined in a list, @@ -500,6 +508,11 @@ def __init__(self, space: LiteralOptions.OptsPICSpace = "Particles6D"): def space(self) -> LiteralOptions.OptsPICSpace: return self._space + def to_dict(self) -> dict: + data = super().to_dict() + data["n_as_volume_form"] = self.n_as_volume_form + return data + @property def particles_class(self) -> Particles: return self._particles_class @@ -771,6 +784,11 @@ def __init__(self): def space(self): return self._space + def to_dict(self) -> dict: + data = super().to_dict() + data["n_as_volume_form"] = self.n_as_volume_form + return data + @property def particles_class(self) -> Particles: return ParticlesSPH diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index c5927ac9d..6bbcfe482 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1679,7 +1679,8 @@ def to_run_metadata(self, file_path: str = None, **extra_data) -> str: timestamps, ...), serialized to a JSON string. The configuration snapshot can also be restored by :meth:`from_output`; - run-specific facts do not restore live simulation state. + variable details and propagator options are recorded for inspection, + while run-specific facts do not restore live simulation state. Parameters ---------- @@ -1773,8 +1774,8 @@ def from_output(cls, path_out: str) -> "Simulation": falling back to legacy ``config.json`` if absent; a copied parameter file is never executed. The metadata holds the options objects and the arguments of the model (and thus its units), which is all that post-processing and - plotting need, but not configuration applied to the model after construction, such as - markers, backgrounds, perturbations and propagator options. + plotting need. Variable details and propagator options are available in the JSON + metadata, but are not reapplied to the reconstructed model. Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. """ path_out = os.path.abspath(path_out) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index e770cafa0..c4651d416 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -10,7 +10,11 @@ import pytest from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time -from struphy.models import Maxwell, VlasovAmpereOneSpecies +from struphy.linear_algebra.solver import SolverParameters +from struphy.models import ColdPlasmaVlasov, Maxwell, Poisson, VlasovAmpereOneSpecies +from struphy.ode.utils import ButcherTableau +from struphy.particles.parameters import LoadingParameters +from struphy.pic.accumulation.filter import FilterParameters from struphy.post_processing.post_processing_tools import PostProcessor, is_processed @@ -73,6 +77,77 @@ def test_run_writes_only_metadata_and_copies_the_parameter_file(tmp_path): assert metadata["started_at_epoch_s"] == sim.start_time +def test_run_metadata_contains_variables_and_propagator_options(tmp_path): + sim = make_sim(tmp_path) + sim.model.em_fields.e_field.save_data = False + sim.model.propagators.maxwell.options = sim.model.propagators.maxwell.Options( + algo="explicit", + solver_params=SolverParameters(tol=1e-6, maxiter=42), + butcher=ButcherTableau("heun2"), + ) + os.makedirs(sim.env.path_out) + + sim._write_run_metadata() + + metadata = json.loads((tmp_path / "sim_1" / "run_metadata.json").read_text()) + assert metadata["model"] == sim.model.to_dict() + assert "species" not in metadata + assert "propagator_options" not in metadata + assert metadata["model"]["species"]["em_fields"]["variables"]["e_field"] == { + "class": "FEECVariable", + "space": "Hcurl", + "save_data": False, + } + assert metadata["model"]["species"]["em_fields"]["variables"]["b_field"]["space"] == "Hdiv" + options = metadata["model"]["propagator_options"]["maxwell"] + assert options["algo"] == "explicit" + assert options["solver_params"]["tol"] == 1e-6 + assert options["solver_params"]["maxiter"] == 42 + assert options["butcher"] == {"algo": "heun2"} + + +def test_run_metadata_names_variable_keys_in_propagator_options(tmp_path): + sim = Simulation(model=Poisson(), env=EnvironmentOptions(out_folders=str(tmp_path))) + variable = sim.model.em_fields.source + sim.model.propagators.poisson.options.filter_params = {variable: FilterParameters("fourier_in_tor", (1, 2))} + + metadata = json.loads(sim.to_run_metadata()) + assert metadata["model"] == sim.model.to_dict() + + assert metadata["model"]["species"]["em_fields"]["variables"]["source"]["space"] == "H1" + assert metadata["model"]["propagator_options"]["poisson"]["filter_params"] == { + "em_fields.source": {"use_filter": "fourier_in_tor", "modes": [1, 2], "repeat": 1, "alpha": 0.5} + } + + +def test_cold_plasma_vlasov_species_and_variables_own_their_metadata(tmp_path): + model = ColdPlasmaVlasov( + thermal_charge_number=-2, + thermal_mass_number=0.25, + thermal_alpha=3.0, + thermal_epsilon=0.5, + hot_mass_number=0.125, + hot_epsilon=0.75, + ) + model.hot_elec.set_markers(loading_params=LoadingParameters(Np=1234)) + model.hot_elec.var.save_data = False + sim = Simulation(model=model, env=EnvironmentOptions(out_folders=str(tmp_path))) + + species = json.loads(sim.to_run_metadata())["model"]["species"] + + assert species["thermal_elec"] == model.thermal_elec.to_dict() + assert species["hot_elec"] == model.hot_elec.to_dict() + assert species["thermal_elec"]["class"] == "ThermalElectrons" + assert species["thermal_elec"]["charge_number"] == -2 + assert species["thermal_elec"]["mass_number"] == 0.25 + assert species["thermal_elec"]["alpha"] == 3.0 + assert species["thermal_elec"]["epsilon"] == 0.5 + assert species["thermal_elec"]["variables"]["current"] == model.thermal_elec.current.to_dict() + assert species["hot_elec"]["loading_params"]["Np"] == 1234 + assert species["hot_elec"]["variables"]["var"] == model.hot_elec.var.to_dict() + assert species["hot_elec"]["variables"]["var"]["save_data"] is False + + def test_from_output_never_executes_the_parameter_file(tmp_path): sim = make_sim(tmp_path, time_opts=Time(dt=0.123)) os.makedirs(os.path.join(sim.env.path_out, "data")) From efddcb6580913131a5567687678dff138bc071a6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 10:41:31 +0200 Subject: [PATCH 090/193] Added initial conditions to run_metadata --- src/struphy/simulation/sim.py | 56 +++++++++++++++++++++ src/struphy/simulation/tests/test_output.py | 32 +++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 6bbcfe482..c6ca35b9b 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1673,6 +1673,61 @@ def _collect_particle_metadata(self) -> dict: particle_metadata[species_name] = species_metadata return particle_metadata + @staticmethod + def _serialize_initial_condition(value): + """Convert initial-condition definitions into JSON-compatible provenance data. + + This deliberately captures constructor parameters rather than evaluated FEEC + coefficients or particle data. It is therefore small and records the setup + that produced the initial state. The representation is not yet used to + reconstruct a simulation from output. + """ + if value is None or isinstance(value, (bool, int, float, str)): + return value + if isinstance(value, dict): + return {str(key): Simulation._serialize_initial_condition(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [Simulation._serialize_initial_condition(item) for item in value] + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return { + "type": type(value).__name__, + "params": Simulation._serialize_initial_condition(value.to_dict()), + } + if hasattr(value, "params"): + return { + "type": type(value).__name__, + "params": Simulation._serialize_initial_condition(value.params), + } + if callable(value): + return { + "type": "callable", + "module": getattr(value, "__module__", None), + "qualname": getattr(value, "__qualname__", repr(value)), + } + # CuPyJSONEncoder handles NumPy/CuPy arrays after this traversal. Keep + # other values visible in provenance rather than making metadata writing fail. + return value + + def _collect_initial_conditions_metadata(self) -> dict: + """Collect initial-condition definitions for every model variable.""" + initial_conditions = {} + for species_name, species in self.model.species.items(): + variables = {} + for variable_name, variable in species.variables.items(): + entry = { + "backgrounds": self._serialize_initial_condition(variable.backgrounds), + "perturbations": self._serialize_initial_condition(variable.perturbations), + } + if isinstance(variable, PICVariable): + # ``initial_condition`` defaults to backgrounds. Do not access the + # property here, because doing so mutates the variable's state. + entry["initial_condition"] = self._serialize_initial_condition( + getattr(variable, "_initial_condition", variable.backgrounds) + ) + variables[variable_name] = entry + initial_conditions[species_name] = variables + return initial_conditions + def to_run_metadata(self, file_path: str = None, **extra_data) -> str: """Snapshot of the reconstructible config (see :meth:`to_dict`) plus run-specific, non-reconstructible facts (MPI layout, live particle counts, caller-supplied @@ -1703,6 +1758,7 @@ def to_run_metadata(self, file_path: str = None, **extra_data) -> str: "mpi_ranks": self.comm_size, "use_mpi_comm_world": self.comm is not None, "particle_species": self._collect_particle_metadata(), + "initial_conditions": self._collect_initial_conditions_metadata(), **extra_data, }, ) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index c4651d416..46445936f 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -9,7 +9,7 @@ import numpy as np import pytest -from struphy import BaseUnits, EnvironmentOptions, Output, Simulation, Time +from struphy import BaseUnits, EnvironmentOptions, FieldsBackground, Output, Simulation, Time, maxwellians, perturbations from struphy.linear_algebra.solver import SolverParameters from struphy.models import ColdPlasmaVlasov, Maxwell, Poisson, VlasovAmpereOneSpecies from struphy.ode.utils import ButcherTableau @@ -106,6 +106,36 @@ def test_run_metadata_contains_variables_and_propagator_options(tmp_path): assert options["butcher"] == {"algo": "heun2"} +def test_run_metadata_contains_serialized_initial_conditions(tmp_path): + sim = make_sim(tmp_path) + velocity = sim.model.em_fields.b_field + velocity.add_background(FieldsBackground(values=(1.0, 2.0, 3.0))) + velocity.add_perturbation(perturbations.TorusModesCos(amps=(0.2,))) + + # A nested perturbation inside a summed kinetic distribution exercises the + # recursive serializer used for PIC initial conditions. + kinetic_sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) + perturbation = perturbations.TorusModesCos(amps=(0.3,)) + background = maxwellians.Maxwellian3D(n=(1.0, None)) + kinetic_sim.model.kinetic_ions.var.add_background(background) + kinetic_sim.model.kinetic_ions.var.add_initial_condition( + maxwellians.Maxwellian3D(n=(1.0, perturbation)) + background + ) + + metadata = json.loads(sim.to_run_metadata()) + b_field = metadata["initial_conditions"]["em_fields"]["b_field"] + assert b_field["backgrounds"] == { + "type": "FieldsBackground", + "params": {"type": "LogicalConst", "values": [1.0, 2.0, 3.0], "variable": None}, + } + assert b_field["perturbations"]["type"] == "TorusModesCos" + + kinetic = json.loads(kinetic_sim.to_run_metadata())["initial_conditions"]["kinetic_ions"]["var"] + assert kinetic["backgrounds"]["type"] == "Maxwellian3D" + assert kinetic["initial_condition"]["type"] == "SumKineticBackground" + assert kinetic["initial_condition"]["params"]["f1"]["params"]["n"][1]["type"] == "TorusModesCos" + + def test_run_metadata_names_variable_keys_in_propagator_options(tmp_path): sim = Simulation(model=Poisson(), env=EnvironmentOptions(out_folders=str(tmp_path))) variable = sim.model.em_fields.source From 04d8cf77dfc95459457b01d65f08294e91d1da45 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 10:50:16 +0200 Subject: [PATCH 091/193] serialize the user defined functions --- src/struphy/simulation/sim.py | 31 +++++++++++++++++++-- src/struphy/simulation/tests/test_output.py | 17 +++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index c6ca35b9b..5137c8ffa 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,11 +1,14 @@ # third party imports import dataclasses import glob +import hashlib +import inspect import json import logging import os import shutil import sysconfig +import textwrap import time import warnings from pathlib import Path @@ -1698,11 +1701,35 @@ def _serialize_initial_condition(value): "type": type(value).__name__, "params": Simulation._serialize_initial_condition(value.params), } + if inspect.isfunction(value): + # Top-level functions are fully captured in metadata. Their source is + # self-contained: neither the module name nor a source-file reference is + # required to recover it later. + if value.__name__ == "" or "" in value.__qualname__ or value.__closure__ is not None: + return { + "type": "python_function", + "serialization": "unsupported", + "reason": "lambdas, nested functions, and closures are not supported", + } + try: + source = textwrap.dedent(inspect.getsource(value)) + except (OSError, TypeError): + return { + "type": "python_function", + "serialization": "unsupported", + "reason": "source code is unavailable", + } + return { + "type": "python_function", + "name": value.__name__, + "source": source, + "source_sha256": hashlib.sha256(source.encode()).hexdigest(), + } if callable(value): return { "type": "callable", - "module": getattr(value, "__module__", None), - "qualname": getattr(value, "__qualname__", repr(value)), + "serialization": "unsupported", + "reason": "only top-level Python functions are currently supported", } # CuPyJSONEncoder handles NumPy/CuPy arrays after this traversal. Keep # other values visible in provenance rather than making metadata writing fail. diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 46445936f..a9a7927a0 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -18,6 +18,10 @@ from struphy.post_processing.post_processing_tools import PostProcessor, is_processed +def user_density_profile(eta1, eta2, eta3): + return 1.0 + eta1 * 0.0 + eta2 * 0.0 + eta3 * 0.0 + + def make_sim(tmp_path, **kwargs): env = EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_1") return Simulation(model=Maxwell(), env=env, **kwargs) @@ -136,6 +140,19 @@ def test_run_metadata_contains_serialized_initial_conditions(tmp_path): assert kinetic["initial_condition"]["params"]["f1"]["params"]["n"][1]["type"] == "TorusModesCos" +def test_run_metadata_embeds_user_function_source(tmp_path): + sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) + sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(user_density_profile, None))) + + density = json.loads(sim.to_run_metadata())["initial_conditions"]["kinetic_ions"]["var"]["backgrounds"][ + "params" + ]["n"][0] + assert density["type"] == "python_function" + assert density["name"] == "user_density_profile" + assert "def user_density_profile" in density["source"] + assert len(density["source_sha256"]) == 64 + + def test_run_metadata_names_variable_keys_in_propagator_options(tmp_path): sim = Simulation(model=Poisson(), env=EnvironmentOptions(out_folders=str(tmp_path))) variable = sim.model.em_fields.source From 0d33dd81b865a6bfc2e6aa89c7efd4f01d1b0e80 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 10:59:15 +0200 Subject: [PATCH 092/193] Added _deserialize_initial_condition --- src/struphy/simulation/sim.py | 72 ++++++++++++++++++++- src/struphy/simulation/tests/test_output.py | 15 +++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 5137c8ffa..9e5c19454 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -30,6 +30,7 @@ BaseUnits, DerhamOptions, EnvironmentOptions, + FieldsBackground, ProfilingOptions, Time, domains, @@ -1755,6 +1756,67 @@ def _collect_initial_conditions_metadata(self) -> dict: initial_conditions[species_name] = variables return initial_conditions + @staticmethod + def _deserialize_initial_condition(value, trust_source: bool): + """Rebuild one initial-condition definition from metadata.""" + if value is None or isinstance(value, (bool, int, float, str)): + return value + if isinstance(value, list): + return tuple(Simulation._deserialize_initial_condition(item, trust_source) for item in value) + if not isinstance(value, dict) or "type" not in value or ( + "params" not in value and value["type"] not in {"python_function", "callable"} + ): + return {key: Simulation._deserialize_initial_condition(item, trust_source) for key, item in value.items()} + + kind = value["type"] + if kind == "python_function": + if value.get("serialization") == "unsupported": + raise ValueError(f"Cannot restore initial-condition function: {value['reason']}.") + if not trust_source: + raise ValueError( + "Initial-condition metadata contains Python source. Pass trust_initial_condition_source=True " + "to Simulation.from_output() only for trusted output." + ) + source = value["source"] + if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: + raise ValueError("Initial-condition function source hash does not match its metadata.") + import numpy as np + import cunumpy as xp + + namespace = {"np": np, "numpy": np, "xp": xp, "cp": xp, "cupy": xp} + exec(source, namespace) # noqa: S102 -- explicitly gated by trust_source + return namespace[value["name"]] + if kind == "callable": + raise ValueError(f"Cannot restore initial-condition callable: {value['reason']}.") + if kind == "FieldsBackground": + return FieldsBackground(**Simulation._deserialize_initial_condition(value["params"], trust_source)) + + from struphy.initial import perturbations + from struphy.kinetic_background import maxwellians + from struphy.kinetic_background import base as kinetic_background_base + + params = Simulation._deserialize_initial_condition(value["params"], trust_source) + for module in (perturbations, maxwellians, kinetic_background_base): + initial_condition_class = getattr(module, kind, None) + if initial_condition_class is not None: + return initial_condition_class(**params) + raise ValueError(f"Unknown initial-condition type '{kind}'.") + + def _restore_initial_conditions(self, metadata: dict, trust_source: bool): + """Attach metadata initial conditions to the reconstructed model variables.""" + for species_name, variables in metadata.get("initial_conditions", {}).items(): + species = self.model.species.get(species_name) + if species is None: + continue + for variable_name, entry in variables.items(): + variable = species.variables.get(variable_name) + if variable is None: + continue + variable._backgrounds = self._deserialize_initial_condition(entry["backgrounds"], trust_source) + variable._perturbations = self._deserialize_initial_condition(entry["perturbations"], trust_source) + if isinstance(variable, PICVariable): + variable._initial_condition = self._deserialize_initial_condition(entry["initial_condition"], trust_source) + def to_run_metadata(self, file_path: str = None, **extra_data) -> str: """Snapshot of the reconstructible config (see :meth:`to_dict`) plus run-specific, non-reconstructible facts (MPI layout, live particle counts, caller-supplied @@ -1850,15 +1912,16 @@ def convert_lists_to_tuples(obj): return cls.from_dict(dct) @classmethod - def from_output(cls, path_out: str) -> "Simulation": + def from_output(cls, path_out: str, trust_initial_condition_source: bool = False) -> "Simulation": """Restore the simulation that wrote the output folder ``path_out``. The configuration is read from the ``run_metadata.json`` written by :meth:`run`, falling back to legacy ``config.json`` if absent; a copied parameter file is never executed. The metadata holds the options objects and the arguments of the model (and thus its units), which is all that post-processing and - plotting need. Variable details and propagator options are available in the JSON - metadata, but are not reapplied to the reconstructed model. + plotting need. Initial conditions are restored when present; embedded Python + functions additionally require ``trust_initial_condition_source=True`` because + restoration executes their saved source. Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. """ path_out = os.path.abspath(path_out) @@ -1874,6 +1937,9 @@ def from_output(cls, path_out: str) -> "Simulation": sim.env = dataclasses.replace( sim.env, out_folders=os.path.dirname(path_out), sim_folder=os.path.basename(path_out) ) + with open(config_path) as stream: + metadata = json.load(stream) + sim._restore_initial_conditions(metadata, trust_initial_condition_source) return sim def generate_script( diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index a9a7927a0..9a618772d 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -153,6 +153,21 @@ def test_run_metadata_embeds_user_function_source(tmp_path): assert len(density["source_sha256"]) == 64 +def test_from_output_restores_initial_conditions_and_requires_trust_for_source(tmp_path): + path_out = tmp_path / "sim_1" + path_out.mkdir() + sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) + sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(user_density_profile, None))) + sim.to_run_metadata(str(path_out / "run_metadata.json")) + + with pytest.raises(ValueError, match="trust_initial_condition_source=True"): + Simulation.from_output(path_out) + + restored = Simulation.from_output(path_out, trust_initial_condition_source=True) + density = restored.model.kinetic_ions.var.backgrounds.params["n"][0] + assert density(0.2, 0.3, 0.4) == user_density_profile(0.2, 0.3, 0.4) + + def test_run_metadata_names_variable_keys_in_propagator_options(tmp_path): sim = Simulation(model=Poisson(), env=EnvironmentOptions(out_folders=str(tmp_path))) variable = sim.model.em_fields.source From 10ab30cd1a2c6b01384af5b2ded04fbf1c24c239 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 11:03:50 +0200 Subject: [PATCH 093/193] Added versioning schema and roundtrip test --- src/struphy/simulation/sim.py | 6 +++++- src/struphy/simulation/tests/test_output.py | 23 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 9e5c19454..de7ce5dac 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1796,7 +1796,7 @@ def _deserialize_initial_condition(value, trust_source: bool): from struphy.kinetic_background import base as kinetic_background_base params = Simulation._deserialize_initial_condition(value["params"], trust_source) - for module in (perturbations, maxwellians, kinetic_background_base): + for module in (equils, perturbations, maxwellians, kinetic_background_base): initial_condition_class = getattr(module, kind, None) if initial_condition_class is not None: return initial_condition_class(**params) @@ -1804,6 +1804,9 @@ def _deserialize_initial_condition(value, trust_source: bool): def _restore_initial_conditions(self, metadata: dict, trust_source: bool): """Attach metadata initial conditions to the reconstructed model variables.""" + version = metadata.get("initial_conditions_schema_version", 1) + if version != 1: + raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") for species_name, variables in metadata.get("initial_conditions", {}).items(): species = self.model.species.get(species_name) if species is None: @@ -1847,6 +1850,7 @@ def to_run_metadata(self, file_path: str = None, **extra_data) -> str: "mpi_ranks": self.comm_size, "use_mpi_comm_world": self.comm is not None, "particle_species": self._collect_particle_metadata(), + "initial_conditions_schema_version": 1, "initial_conditions": self._collect_initial_conditions_metadata(), **extra_data, }, diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 9a618772d..393ec19e9 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -9,9 +9,9 @@ import numpy as np import pytest -from struphy import BaseUnits, EnvironmentOptions, FieldsBackground, Output, Simulation, Time, maxwellians, perturbations +from struphy import BaseUnits, EnvironmentOptions, FieldsBackground, Output, Simulation, Time, equils, maxwellians, perturbations from struphy.linear_algebra.solver import SolverParameters -from struphy.models import ColdPlasmaVlasov, Maxwell, Poisson, VlasovAmpereOneSpecies +from struphy.models import ColdPlasmaVlasov, LinearMHD, Maxwell, Poisson, VlasovAmpereOneSpecies from struphy.ode.utils import ButcherTableau from struphy.particles.parameters import LoadingParameters from struphy.pic.accumulation.filter import FilterParameters @@ -127,6 +127,7 @@ def test_run_metadata_contains_serialized_initial_conditions(tmp_path): ) metadata = json.loads(sim.to_run_metadata()) + assert metadata["initial_conditions_schema_version"] == 1 b_field = metadata["initial_conditions"]["em_fields"]["b_field"] assert b_field["backgrounds"] == { "type": "FieldsBackground", @@ -168,6 +169,24 @@ def test_from_output_restores_initial_conditions_and_requires_trust_for_source(t assert density(0.2, 0.3, 0.4) == user_density_profile(0.2, 0.3, 0.4) +def test_versioned_initial_conditions_round_trip_allocates_and_runs_one_step(tmp_path): + sim = Simulation( + model=LinearMHD(), + equil=equils.HomogenSlab(), + env=EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_1"), + ) + sim.model.mhd.velocity.add_background(FieldsBackground(type="FluidEquilibrium", variable="uv")) + sim.model.propagators.shear_alf.options = sim.model.propagators.shear_alf.Options() + sim.model.propagators.mag_sonic.options = sim.model.propagators.mag_sonic.Options() + os.makedirs(sim.env.path_out) + sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json")) + + restored = Simulation.from_output(sim.env.path_out) + assert restored.model.mhd.velocity.backgrounds == sim.model.mhd.velocity.backgrounds + assert restored._deserialize_initial_condition(sim.equil.to_dict(), trust_source=False) == sim.equil + restored.run(one_time_step=True) + + def test_run_metadata_names_variable_keys_in_propagator_options(tmp_path): sim = Simulation(model=Poisson(), env=EnvironmentOptions(out_folders=str(tmp_path))) variable = sim.model.em_fields.source From 48275de85ff9a9e90356f50bffe5e07fb716f314 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 11:06:54 +0200 Subject: [PATCH 094/193] Serialize perturbations --- src/struphy/simulation/sim.py | 74 ++++++++++++++++++++- src/struphy/simulation/tests/test_output.py | 37 +++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index de7ce5dac..a1fd5423c 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -57,6 +57,7 @@ ) from struphy.geometry.base import Domain from struphy.io.output_handling import DataContainer +from struphy.initial.base import Perturbation from struphy.models import Maxwell from struphy.models.base import StruphyModel from struphy.models.species import ( @@ -1692,6 +1693,47 @@ def _serialize_initial_condition(value): return {str(key): Simulation._serialize_initial_condition(item) for key, item in value.items()} if isinstance(value, (list, tuple)): return [Simulation._serialize_initial_condition(item) for item in value] + if type(value).__module__ not in { + "struphy.initial.perturbations", + "struphy.kinetic_background.maxwellians", + "struphy.kinetic_background.base", + "struphy.io.options", + } and not inspect.isfunction(value) and ( + isinstance(value, Perturbation) or callable(value) + ): + cls = type(value) + if "" in cls.__qualname__: + return { + "type": "python_class", + "serialization": "unsupported", + "reason": "nested classes are not supported", + } + try: + source = textwrap.dedent(inspect.getsource(cls)) + except (OSError, TypeError): + return { + "type": "python_class", + "serialization": "unsupported", + "reason": "source code is unavailable", + } + data = { + "type": "python_class", + "name": cls.__name__, + "source": source, + "source_sha256": hashlib.sha256(source.encode()).hexdigest(), + } + if hasattr(value, "params"): + data["params"] = Simulation._serialize_initial_condition(value.params) + elif hasattr(value, "__dict__"): + data["state"] = Simulation._serialize_initial_condition(vars(value)) + else: + data.update( + { + "serialization": "unsupported", + "reason": "callable objects without instance state are not supported", + } + ) + return data if dataclasses.is_dataclass(value) and not isinstance(value, type): return { "type": type(value).__name__, @@ -1764,7 +1806,7 @@ def _deserialize_initial_condition(value, trust_source: bool): if isinstance(value, list): return tuple(Simulation._deserialize_initial_condition(item, trust_source) for item in value) if not isinstance(value, dict) or "type" not in value or ( - "params" not in value and value["type"] not in {"python_function", "callable"} + "params" not in value and value["type"] not in {"python_function", "python_class", "callable"} ): return {key: Simulation._deserialize_initial_condition(item, trust_source) for key, item in value.items()} @@ -1786,6 +1828,36 @@ def _deserialize_initial_condition(value, trust_source: bool): namespace = {"np": np, "numpy": np, "xp": xp, "cp": xp, "cupy": xp} exec(source, namespace) # noqa: S102 -- explicitly gated by trust_source return namespace[value["name"]] + if kind == "python_class": + if value.get("serialization") == "unsupported": + raise ValueError(f"Cannot restore initial-condition class: {value['reason']}.") + if not trust_source: + raise ValueError( + "Initial-condition metadata contains Python source. Pass trust_initial_condition_source=True " + "to Simulation.from_output() only for trusted output." + ) + source = value["source"] + if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: + raise ValueError("Initial-condition class source hash does not match its metadata.") + import numpy as np + import cunumpy as xp + + namespace = { + "np": np, + "numpy": np, + "xp": xp, + "cp": xp, + "cupy": xp, + "Perturbation": Perturbation, + "dataclass": dataclasses.dataclass, + } + exec(source, namespace) # noqa: S102 -- explicitly gated by trust_source + initial_condition_class = namespace[value["name"]] + if "params" in value: + return initial_condition_class(**Simulation._deserialize_initial_condition(value["params"], trust_source)) + initial_condition = initial_condition_class.__new__(initial_condition_class) + initial_condition.__dict__.update(Simulation._deserialize_initial_condition(value["state"], trust_source)) + return initial_condition if kind == "callable": raise ValueError(f"Cannot restore initial-condition callable: {value['reason']}.") if kind == "FieldsBackground": diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 393ec19e9..21dd574f4 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -16,12 +16,31 @@ from struphy.particles.parameters import LoadingParameters from struphy.pic.accumulation.filter import FilterParameters from struphy.post_processing.post_processing_tools import PostProcessor, is_processed +from struphy.initial.base import Perturbation def user_density_profile(eta1, eta2, eta3): return 1.0 + eta1 * 0.0 + eta2 * 0.0 + eta3 * 0.0 +class UserCosinePerturbation(Perturbation): + def __init__(self, amplitude=0.1): + self.params = {"amplitude": amplitude} + self.given_in_basis = "0" + + def __call__(self, eta1, eta2, eta3, flat_eval=False): + return self.params["amplitude"] * np.cos(2.0 * np.pi * eta2) + + +class UserCallableProfile: + def __init__(self, offset): + self.offset = offset + + def __call__(self, *etas): + eta1 = etas[0][:, 0] if len(etas) == 1 else etas[0] + return self.offset + 0.0 * eta1 + + def make_sim(tmp_path, **kwargs): env = EnvironmentOptions(out_folders=str(tmp_path), sim_folder="sim_1") return Simulation(model=Maxwell(), env=env, **kwargs) @@ -169,6 +188,24 @@ def test_from_output_restores_initial_conditions_and_requires_trust_for_source(t assert density(0.2, 0.3, 0.4) == user_density_profile(0.2, 0.3, 0.4) +def test_from_output_restores_user_perturbation_subclasses_and_callable_objects(tmp_path): + path_out = tmp_path / "sim_1" + path_out.mkdir() + sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) + perturbation = UserCosinePerturbation(amplitude=0.25) + sim.model.em_fields.e_field.add_perturbation(perturbation) + sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(UserCallableProfile(1.5), None))) + sim.to_run_metadata(str(path_out / "run_metadata.json")) + + restored = Simulation.from_output(path_out, trust_initial_condition_source=True) + restored_perturbation = restored.model.em_fields.e_field.perturbations + restored_profile = restored.model.kinetic_ions.var.backgrounds.params["n"][0] + assert isinstance(restored_perturbation, Perturbation) + assert type(restored_perturbation).__name__ == "UserCosinePerturbation" + assert restored_perturbation(0.0, 0.0, 0.0) == 0.25 + assert restored_profile(np.array([[0.2, 0.3, 0.4]])) == 1.5 + + def test_versioned_initial_conditions_round_trip_allocates_and_runs_one_step(tmp_path): sim = Simulation( model=LinearMHD(), From b77170c6a5ce43c3acad2d6822475c14d186ea9a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 11:12:33 +0200 Subject: [PATCH 095/193] Extend the Simulation.from_file classmethod --- src/struphy/simulation/sim.py | 21 ++++++++++++++------- src/struphy/simulation/tests/test_output.py | 3 +++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index a1fd5423c..efc0685c5 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,5 +1,6 @@ # third party imports import dataclasses +import copy import glob import hashlib import inspect @@ -1959,8 +1960,13 @@ def from_dict(cls, dct) -> "Simulation": ) @classmethod - def from_file(cls, file_path: str) -> "SimulationBase": - """Deserialize a simulation configuration from a file based on the file extension.""" + def from_file(cls, file_path: str, trust_initial_condition_source: bool = False) -> "SimulationBase": + """Deserialize a simulation configuration from a YAML or JSON file. + + Initial conditions in run metadata are restored when present. Embedded + Python functions and classes require ``trust_initial_condition_source=True``. + """ + file_path = os.fspath(file_path) if file_path.endswith(".yaml") or file_path.endswith(".yml"): with open(file_path, "r") as f: dct = yaml.safe_load(f) @@ -1970,6 +1976,8 @@ def from_file(cls, file_path: str) -> "SimulationBase": else: raise ValueError("Unsupported file format. Use .yaml, .yml or .json.") + metadata = copy.deepcopy(dct) + # YAML and JSON do not have a native tuple type, # so when you load them with PyYAML or json, # sequences are always converted to lists @@ -1985,7 +1993,9 @@ def convert_lists_to_tuples(obj): # Convert lists to tuples for relevant keys dct = convert_lists_to_tuples(dct) - return cls.from_dict(dct) + sim = cls.from_dict(dct) + sim._restore_initial_conditions(metadata, trust_initial_condition_source) + return sim @classmethod def from_output(cls, path_out: str, trust_initial_condition_source: bool = False) -> "Simulation": @@ -2009,13 +2019,10 @@ def from_output(cls, path_out: str, trust_initial_condition_source: bool = False f"Neither config.json nor run_metadata.json exists in {path_out}; is it a Struphy output folder? Outputs of older " "versions can get one with sim.to_run_metadata(os.path.join(path_out, 'run_metadata.json')) from their parameter file." ) - sim = cls.from_file(config_path) + sim = cls.from_file(config_path, trust_initial_condition_source=trust_initial_condition_source) sim.env = dataclasses.replace( sim.env, out_folders=os.path.dirname(path_out), sim_folder=os.path.basename(path_out) ) - with open(config_path) as stream: - metadata = json.load(stream) - sim._restore_initial_conditions(metadata, trust_initial_condition_source) return sim def generate_script( diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 21dd574f4..0d4680164 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -187,6 +187,9 @@ def test_from_output_restores_initial_conditions_and_requires_trust_for_source(t density = restored.model.kinetic_ions.var.backgrounds.params["n"][0] assert density(0.2, 0.3, 0.4) == user_density_profile(0.2, 0.3, 0.4) + restored_from_file = Simulation.from_file(path_out / "run_metadata.json", trust_initial_condition_source=True) + assert restored_from_file.model.kinetic_ions.var.backgrounds.params["n"][0](0.2, 0.3, 0.4) == 1.0 + def test_from_output_restores_user_perturbation_subclasses_and_callable_objects(tmp_path): path_out = tmp_path / "sim_1" From eb5830162d84f04ea79502091d5555a85ca56784 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 11:17:11 +0200 Subject: [PATCH 096/193] Read the full json in the Output class --- src/struphy/post_processing/output.py | 64 +++++++++++++++++++-- src/struphy/simulation/tests/test_output.py | 11 +++- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 14692e7d9..a9d1e8f8b 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -172,8 +172,8 @@ class Output: * :attr:`scalars` are read directly from the raw HDF5 output. * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` are retained as compatibility views over the post-processed products. - * :attr:`model`, :attr:`domain` and numerical options are reconstructed lazily - from saved metadata. No simulation object is created or retained. + * :attr:`model`, :attr:`initial_conditions`, :attr:`domain` and numerical options are + reconstructed lazily from saved metadata. No simulation object is created or retained. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. Parameters @@ -184,13 +184,23 @@ class Output: ``"normalized"`` (the default) keeps Struphy time units, in which the analytic results of the models are expressed; every product then also carries seconds as the coordinate ``t_seconds``. ``"physical"`` makes ``t`` itself seconds. + trust_initial_condition_source: + Allow reconstruction of Python functions and classes embedded in initial-condition + metadata. Enable this only for output folders you trust. """ - def __init__(self, path_out, *, time_units: str = "normalized"): + def __init__( + self, + path_out, + *, + time_units: str = "normalized", + trust_initial_condition_source: bool = False, + ): if time_units not in {"physical", "normalized"}: raise ValueError("time_units must be 'physical' or 'normalized'") self.path_out = Path(path_out).resolve() self.time_units = time_units + self.trust_initial_condition_source = trust_initial_condition_source self.comm = mpi_comm_world() self._reset() # A Simulation can expose its Output before it has written metadata. In that case, @@ -205,7 +215,11 @@ def __repr__(self): def with_time_units(self, time_units: str) -> "Output": """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" - return type(self)(self.path_out, time_units=time_units) + return type(self)( + self.path_out, + time_units=time_units, + trust_initial_condition_source=self.trust_initial_condition_source, + ) def clear_cache(self): """Close lazy product files and discard loaded arrays while retaining metadata.""" @@ -564,10 +578,48 @@ def tuples(value): @cached_property def model(self): - """Model reconstructed from its saved constructor arguments.""" + """Model reconstructed from saved metadata, including initial conditions.""" from struphy.models.base import StruphyModel + from struphy.models.variables import PICVariable + + model = self._restore("model", StruphyModel) + for species_name, variables in self.initial_conditions.items(): + species = model.species.get(species_name) + if species is None: + continue + for variable_name, definition in variables.items(): + variable = species.variables.get(variable_name) + if variable is None: + continue + variable._backgrounds = definition["backgrounds"] + variable._perturbations = definition["perturbations"] + if isinstance(variable, PICVariable): + variable._initial_condition = definition["initial_condition"] + return model - return self._restore("model", StruphyModel) + @cached_property + def initial_conditions(self) -> dict: + """Initial-condition definitions reconstructed from run metadata. + + This reconstructs backgrounds, perturbations, kinetic distributions, and + supported inline Python functions/classes without creating a + :class:`~struphy.simulation.sim.Simulation` instance. + """ + from struphy.simulation.sim import Simulation + + version = self.metadata.get("initial_conditions_schema_version", 1) + if version != 1: + raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") + return { + species_name: { + variable_name: { + key: Simulation._deserialize_initial_condition(value, self.trust_initial_condition_source) + for key, value in definition.items() + } + for variable_name, definition in variables.items() + } + for species_name, variables in self.metadata.get("initial_conditions", {}).items() + } @cached_property def domain(self): diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 0d4680164..042c098ec 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -173,7 +173,7 @@ def test_run_metadata_embeds_user_function_source(tmp_path): assert len(density["source_sha256"]) == 64 -def test_from_output_restores_initial_conditions_and_requires_trust_for_source(tmp_path): +def test_from_output_restores_initial_conditions_and_requires_trust_for_source(tmp_path, monkeypatch): path_out = tmp_path / "sim_1" path_out.mkdir() sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) @@ -190,6 +190,15 @@ def test_from_output_restores_initial_conditions_and_requires_trust_for_source(t restored_from_file = Simulation.from_file(path_out / "run_metadata.json", trust_initial_condition_source=True) assert restored_from_file.model.kinetic_ions.var.backgrounds.params["n"][0](0.2, 0.3, 0.4) == 1.0 + def simulation_init_must_not_run(*args, **kwargs): + raise AssertionError("Output must not instantiate Simulation") + + monkeypatch.setattr(Simulation, "__init__", simulation_init_must_not_run) + output = Output(path_out, trust_initial_condition_source=True) + density_from_output = output.initial_conditions["kinetic_ions"]["var"]["backgrounds"].params["n"][0] + assert density_from_output(0.2, 0.3, 0.4) == 1.0 + assert output.model.kinetic_ions.var.backgrounds.params["n"][0](0.2, 0.3, 0.4) == 1.0 + def test_from_output_restores_user_perturbation_subclasses_and_callable_objects(tmp_path): path_out = tmp_path / "sim_1" From fefb91e9a4531129d882d215fa812f27db628a89 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 11:23:06 +0200 Subject: [PATCH 097/193] Update tutorial --- src/struphy/post_processing/output.py | 64 ++++++- .../post_processing/tests/test_output.py | 3 + tutorials/tutorial_post_processing.ipynb | 172 ++++++++++++------ 3 files changed, 182 insertions(+), 57 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index a9d1e8f8b..fe74a1e33 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -988,23 +988,83 @@ def label(self) -> str: return self._label def info(self) -> str: - """A table of every key accepted by :meth:`evaluate`, with a short description. + """A concise run summary, configuration reference, and product catalog. - Use ``print(out.info())`` interactively. As with :meth:`keys`, this materializes default + Use ``print(out.info())`` interactively. The summary includes model parameters, + species variables, propagator options, and initial-condition definitions saved in + metadata. As with :meth:`keys`, the product catalog materializes default post-processing when needed; call :meth:`pproc` first to choose its options. """ rows = [(key, self._product_description(key)) for key in self.keys()] key_width = max((len(key) for key, _ in rows), default=3) + model = self.metadata.get("model", {}) lines = [ f"Output: {self.path_out}", self.label, "", + "Configuration", + "-------------", + f"Model: {model.get('model', self.metadata.get('model_name', 'unknown'))}", + f"Model parameters: {json.dumps(model.get('params', {}), sort_keys=True)}", + "Species and variables:", + ] + for species_name, species in model.get("species", {}).items(): + parameters = { + key: value + for key, value in species.items() + if key + not in {"class", "variables", "loading_params", "weights_params", "boundary_params", "sorting_params", "saving_params"} + and value is not None + } + lines.append(f" {species_name} ({species.get('class', 'Species')}): {json.dumps(parameters, sort_keys=True)}") + for variable_name, variable in species.get("variables", {}).items(): + lines.append( + f" {variable_name}: {variable.get('class', 'Variable')} " + f"[{variable.get('space', 'unknown')}], save_data={variable.get('save_data', True)}" + ) + lines.append("Propagator options:") + for name, options in model.get("propagator_options", {}).items(): + lines.append(f" {name}: {json.dumps(options, sort_keys=True)}") + lines.append("Initial conditions:") + for species_name, variables in self.metadata.get("initial_conditions", {}).items(): + for variable_name, definition in variables.items(): + parts = ", ".join(f"{key}={self._initial_condition_description(value)}" for key, value in definition.items()) + lines.append(f" {species_name}.{variable_name}: {parts}") + lines = [ + *lines, + "", + "Help", + "----", + "- Use out.model for the reconstructed model and its variables.", + "- Use out.initial_conditions for reconstructed backgrounds, perturbations, and distributions.", + "- Use Output(path, trust_initial_condition_source=True) only for trusted runs with embedded Python source.", + "- Use out.keys(), out.fields, out.distributions, out.densities, and out.orbits to discover products.", + "- Use out.evaluate(key), out.process(...), and array.struphy.plot.* to load and plot products.", + "", f"{'Key':<{key_width}} Description", f"{'-' * key_width} -----------", ] lines.extend(f"{key:<{key_width}} {description}" for key, description in rows) return "\n".join(lines) + @staticmethod + def _initial_condition_description(value) -> str: + """Short, source-free description of one serialized initial condition.""" + if value is None: + return "none" + if isinstance(value, list): + return "[" + ", ".join(Output._initial_condition_description(item) for item in value) + "]" + if not isinstance(value, dict): + return repr(value) + kind = value.get("type") + if kind is None: + return "mapping" + if kind in {"python_function", "python_class"}: + return f"{kind}({value.get('name', value.get('serialization', 'unknown'))})" + if kind == "callable": + return f"callable({value.get('serialization', 'unknown')})" + return kind + def _product_description(self, key: str) -> str: """A stable description for a key, without loading its data array.""" if key in self.scalars.data_vars: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index bd354c86b..7f2286887 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -367,6 +367,9 @@ def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): def test_info_lists_evaluable_products_with_descriptions(run): text = run.info() + assert "Configuration" in text and "Species and variables" in text + assert "Propagator options" in text and "Initial conditions" in text + assert "Help" in text and "out.initial_conditions" in text assert "Key" in text and "Description" in text assert "en_tot" in text and "scalar time series" in text assert "kinetic_ions/e1_v1_density/f" in text and "particle distribution" in text diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 40dd83de7..4a5b40bf4 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -22,6 +22,9 @@ "import os\n", "import tempfile\n", "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", "from IPython.display import HTML\n", "\n", "from struphy import (\n", @@ -177,10 +180,69 @@ "print(out.kinetic_ions)" ] }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "### Reconstructed setup and initial conditions\n", + "\n", + "`out.info()` now includes the saved model parameters, species variables, propagator options, initial-condition summary, and a short API guide. The structured equivalents remain available through `out.model`, `out.metadata`, and `out.initial_conditions`; all are rebuilt from metadata without creating a `Simulation`. This demonstration run uses only built-in definitions. For a run with saved user Python functions or classes, reopen it with `Output(path, trust_initial_condition_source=True)` only when the output is trusted." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Model parameters:\", out.model.params)\n", + "print(\"Kinetic variables:\", out.model.kinetic_ions.variables)\n", + "print(\"Propagator options:\")\n", + "for name, options in out.metadata[\"model\"][\"propagator_options\"].items():\n", + " print(f\" {name}: {options}\")\n", + "\n", + "initial = out.initial_conditions[\"kinetic_ions\"][\"var\"]\n", + "print(\"Initial-condition entries:\", tuple(initial))\n", + "print(\"Background distribution:\", initial[\"backgrounds\"])\n", + "print(\"Initial distribution:\", initial[\"initial_condition\"])" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "The reconstructed kinetic distributions are ordinary Struphy background objects. Here we evaluate the saved equilibrium and initial distribution along $\\eta_1$; their difference is the density perturbation that seeded the run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "background = initial[\"backgrounds\"]\n", + "initial_distribution = initial[\"initial_condition\"]\n", + "eta1 = np.linspace(0.0, 1.0, 256)\n", + "zeros = np.zeros_like(eta1)\n", + "n_background = np.asarray(background.n(eta1, zeros, zeros))\n", + "n_initial = np.asarray(initial_distribution.n(eta1, zeros, zeros))\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.plot(eta1, n_background, label=\"background density\")\n", + "ax.plot(eta1, n_initial, label=\"initial density\")\n", + "ax.plot(eta1, n_initial - n_background, label=\"density perturbation\")\n", + "ax.set(xlabel=r\"$\\eta_1$\", ylabel=\"density\", title=\"Saved kinetic initial condition\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -190,7 +252,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "15", "metadata": {}, "source": [ "### Inspecting `out` itself\n", @@ -201,7 +263,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -211,7 +273,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -221,7 +283,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -231,7 +293,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -240,7 +302,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "20", "metadata": {}, "source": [ "### Products are xarray arrays\n", @@ -251,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -260,7 +322,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "22", "metadata": {}, "source": [ "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." @@ -268,7 +330,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "23", "metadata": {}, "source": [ "## Scalar overview and time series\n", @@ -281,7 +343,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -291,7 +353,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -311,7 +373,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "26", "metadata": {}, "source": [ "## Two-dimensional data\n", @@ -322,7 +384,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -337,7 +399,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "28", "metadata": {}, "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." @@ -346,7 +408,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -361,7 +423,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "30", "metadata": {}, "source": [ "## Interactive plots\n", @@ -372,7 +434,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -382,7 +444,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "32", "metadata": {}, "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." @@ -391,7 +453,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -400,7 +462,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "34", "metadata": {}, "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." @@ -409,7 +471,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -420,7 +482,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -430,7 +492,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "37", "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." @@ -439,7 +501,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -448,7 +510,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "39", "metadata": {}, "source": [ "## Derived quantities\n", @@ -459,7 +521,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -473,7 +535,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "41", "metadata": {}, "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." @@ -482,7 +544,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -495,7 +557,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "43", "metadata": {}, "source": [ "## Reducing distribution functions\n", @@ -506,7 +568,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -517,7 +579,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "45", "metadata": {}, "source": [ "`.struphy.analysis.velocity_moments()` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean velocity `mean_v1` and the variance `variance_v1` along each velocity direction, all as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. For a `delta_f` product only the density (its perturbation) is returned, because a mean and variance of a perturbation are not defined. Where the density is not positive, mean and variance are NaN." @@ -526,7 +588,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -542,7 +604,7 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "47", "metadata": {}, "source": [ "## Physical units\n", @@ -553,7 +615,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -566,7 +628,7 @@ }, { "cell_type": "markdown", - "id": "45", + "id": "49", "metadata": {}, "source": [ "## Save standard output\n", @@ -577,7 +639,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -589,7 +651,7 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "51", "metadata": {}, "source": [ "## Comparing runs\n", @@ -600,7 +662,7 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "52", "metadata": {}, "outputs": [], "source": [ @@ -623,7 +685,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "53", "metadata": {}, "source": [ "## Profiling\n", @@ -634,7 +696,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -646,7 +708,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "55", "metadata": {}, "source": [ "`compare()` puts the same statistic of several runs side by side, with runs whose region is missing as NaN. Here the two runs of the previous section differ only in the time step, so the number of calls per propagator halves for `dt = 0.1`." @@ -655,7 +717,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "56", "metadata": {}, "outputs": [], "source": [ @@ -665,7 +727,7 @@ }, { "cell_type": "markdown", - "id": "53", + "id": "57", "metadata": {}, "source": [ "## Other models\n", @@ -675,7 +737,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "58", "metadata": {}, "source": [ "### SPH densities\n", @@ -686,7 +748,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -724,7 +786,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "60", "metadata": {}, "source": [ "For a one-dimensional run, the clearest picture is a space-time map: the sweep dimension `t` may be used as a display axis." @@ -733,7 +795,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -743,7 +805,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "62", "metadata": {}, "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" @@ -752,7 +814,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -761,7 +823,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "64", "metadata": {}, "source": [ "### Vector fields on a mapped domain\n", @@ -772,7 +834,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -803,7 +865,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -822,7 +884,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -841,7 +903,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "68", "metadata": {}, "source": [ "## Apply the workflow to another run\n", From eb0efcafac9da04d49c608526fc33849ecacc96a Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 11:26:56 +0200 Subject: [PATCH 098/193] Make out.info() print --- doc/markdown/output-api.md | 2 +- src/struphy/post_processing/output.py | 8 ++++---- src/struphy/post_processing/tests/test_output.py | 10 ++++++---- tutorials/tutorial_beltrami_sph.ipynb | 2 +- tutorials/tutorial_post_processing.ipynb | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 2998b5832..c42061088 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -21,7 +21,7 @@ Use `keys()` to list the names accepted by `evaluate()`, or `info()` for the sam short descriptions. ```python -print(out.info()) +out.info() for key in out.keys(): print(key) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index fe74a1e33..67c1b1594 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -987,10 +987,10 @@ def label(self) -> str: self._label = ", ".join(values) or self.path_out.name return self._label - def info(self) -> str: - """A concise run summary, configuration reference, and product catalog. + def info(self) -> None: + """Print a concise run summary, configuration reference, and product catalog. - Use ``print(out.info())`` interactively. The summary includes model parameters, + Use ``out.info()`` interactively. The summary includes model parameters, species variables, propagator options, and initial-condition definitions saved in metadata. As with :meth:`keys`, the product catalog materializes default post-processing when needed; call :meth:`pproc` first to choose its options. @@ -1045,7 +1045,7 @@ def info(self) -> str: f"{'-' * key_width} -----------", ] lines.extend(f"{key:<{key_width}} {description}" for key, description in rows) - return "\n".join(lines) + print("\n".join(lines)) @staticmethod def _initial_condition_description(value) -> str: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 7f2286887..6c7c14e62 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -365,8 +365,9 @@ def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): assert {"em_fields", "kinetic_ions"} <= set(dir(run)), "species are known before processing" -def test_info_lists_evaluable_products_with_descriptions(run): - text = run.info() +def test_info_lists_evaluable_products_with_descriptions(run, capsys): + assert run.info() is None + text = capsys.readouterr().out assert "Configuration" in text and "Species and variables" in text assert "Propagator options" in text and "Initial conditions" in text assert "Help" in text and "out.initial_conditions" in text @@ -378,8 +379,9 @@ def test_info_lists_evaluable_products_with_descriptions(run): assert run.field_catalog._cache == {}, "listing must not load arrays" -def test_info_labels_distribution_and_density_symbols(run): - text = run.info() +def test_info_labels_distribution_and_density_symbols(run, capsys): + run.info() + text = capsys.readouterr().out assert "particle distribution ($f$)" in text assert "particle distribution ($\\delta f$)" in text assert "SPH density ($n$)" in text diff --git a/tutorials/tutorial_beltrami_sph.ipynb b/tutorials/tutorial_beltrami_sph.ipynb index d1596e2e5..cff47397c 100644 --- a/tutorials/tutorial_beltrami_sph.ipynb +++ b/tutorials/tutorial_beltrami_sph.ipynb @@ -310,7 +310,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(out.info())" + "out.info()" ] }, { diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 4a5b40bf4..ea2ab9669 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -167,7 +167,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(out.info())" + "out.info()" ] }, { From dfc620c8ee7ebe03ec19efe5b2fd27434f0db32b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 11:30:16 +0200 Subject: [PATCH 099/193] Added params_LinearMHDDriftkineticCC.py --- params_LinearMHDDriftkineticCC.py | 220 ++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 params_LinearMHDDriftkineticCC.py diff --git a/params_LinearMHDDriftkineticCC.py b/params_LinearMHDDriftkineticCC.py new file mode 100644 index 000000000..5a69f8847 --- /dev/null +++ b/params_LinearMHDDriftkineticCC.py @@ -0,0 +1,220 @@ +# ----------------------------- +# Description of the simulation +# ----------------------------- +# Please fill in a verbal description of the simulation. +# It will be printed at the beginning of the simulation and can be used to keep track of the different runs. + +name = "Default LinearMHDDriftkineticCC" +description = """ +This is the default simulation for the model LinearMHDDriftkineticCC. +It is meant to be a template for users to set up their own simulations with this model. +It contains all the necessary components of a Struphy simulation, including the model, +the environment options, the time stepping options, the geometry, the equilibrium, +the grid, the Derham options, and the initial conditions. +Users can modify this file to set up their own simulations with different parameters and initial conditions. +""" + +import logging +import numpy as np +from struphy import set_logging_level +from struphy.initial.base import Perturbation +set_logging_level(logging.WARNING) + +# ------------------ +# Import Struphy API +# ------------------ + +from struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + FieldsBackground, + ProfilingOptions, + Simulation, + Time, + domains, + equils, + grids, + perturbations, +) + +# For particles: +from struphy import ( + BinningPlot, + BoundaryParameters, + KernelDensityPlot, + LoadingParameters, + WeightsParameters, + SortingParameters, + SavingParameters, + maxwellians, +) + +# --------------------- +# Instance of the model +# --------------------- + +from struphy.models import LinearMHDDriftkineticCC + +# Units +base_units = BaseUnits() + +# Model instance +model = LinearMHDDriftkineticCC(base_units=base_units) + +# List all variables and decide whether to save their data +model.em_fields.b_field.save_data = True +model.mhd.density.save_data = True +model.mhd.pressure.save_data = True +model.mhd.velocity.save_data = True +model.energetic_ions.var.save_data = True + +# -------------------------- +# Instance of the simulation +# -------------------------- + +# Environment options +env = EnvironmentOptions() + +# Time stepping +time_opts = Time() + +# Geometry +domain = domains.Cuboid() + +# Fluid equilibrium (can be used as part of initial conditions) +equil = equils.HomogenSlab() + +# Grid +grid = grids.TensorProductGrid() + +# Derham options +derham_opts = DerhamOptions() + +# Profiling options +profiling_opts = ProfilingOptions() + +# Simulation object +sim = Simulation( + model=model, + name=name, + description=description, + params_path=__file__, + env=env, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + profiling_opts=profiling_opts, +) + +# ------------------- +# Particle parameters +# ------------------- + +loading_params = LoadingParameters() +weights_params = WeightsParameters() +boundary_params = BoundaryParameters() +sorting_params = SortingParameters() +saving_params = SavingParameters() +model.energetic_ions.set_markers(loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, + ) + +# ------------------ +# Propagator options +# ------------------ + +model.propagators.push_bxe.options = model.propagators.push_bxe.Options() +model.propagators.push_parallel.options = model.propagators.push_parallel.Options() +model.propagators.shearalfen_cc5d.options = model.propagators.shearalfen_cc5d.Options() +model.propagators.magnetosonic.options = model.propagators.magnetosonic.Options() +model.propagators.cc5d_density.options = model.propagators.cc5d_density.Options() +model.propagators.cc5d_gradb.options = model.propagators.cc5d_gradb.Options() +model.propagators.cc5d_curlb.options = model.propagators.cc5d_curlb.Options() + +# ------------------ +# Initial conditions +# ------------------ +# Initial conditions are the sum of the background(s) and the perturbation(s). +# If backgrounds or perturbations are not specified, they are assumed to be zero. + + +class RadialVelocityPerturbation(Perturbation): + """User-defined vector-component perturbation with a radial envelope. + + This class is saved inline in ``run_metadata.json`` and can be restored with + ``Simulation.from_output(..., trust_initial_condition_source=True)``. + """ + + def __init__(self, amplitude=0.02, comp=0): + self.params = {"amplitude": amplitude, "comp": comp} + self.given_in_basis = "v" + self.comp = comp + + def __call__(self, eta1, eta2, eta3, flat_eval=False): + return self.params["amplitude"] * np.sin(np.pi * eta1) * np.cos(2.0 * np.pi * eta2) + + +# Background for (some) FEEC variables +model.mhd.velocity.add_background(FieldsBackground()) + +# Perturbations for (some) FEEC variables +model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=0)) +model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=1)) +model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=2)) +# A custom perturbation class can be combined with built-in perturbations. +model.mhd.velocity.add_perturbation(RadialVelocityPerturbation(amplitude=0.02, comp=0)) + +# For kinetic species the background is mandatory. +# For kinetic species, if add_initial_condition() is not called, the background is taken as the kinetic initial condition. +# For kinetic species the perturbations are added to the moments of the distribution function (defined as tuples). + +# User-defined profiles can be used anywhere a kinetic Maxwellian accepts a +# callable. They are written inline to run_metadata.json and restored with +# Simulation.from_output(..., trust_initial_condition_source=True). +# The restoration namespace supplies np/numpy and xp/cp/cupy. +def energetic_ion_density(*etas): + """A weak periodic density modulation.""" + eta1, eta2, eta3 = etas[0].T if len(etas) == 1 else etas + return 1.0 + 0.05 * np.cos(2.0 * np.pi * eta2) + + +def energetic_ion_parallel_flow(*etas): + """A small parallel flow with a radial envelope.""" + eta1, eta2, eta3 = etas[0].T if len(etas) == 1 else etas + return 0.1 * np.sin(np.pi * eta1) * np.sin(2.0 * np.pi * eta3) + + +def energetic_ion_parallel_thermal_speed(*etas): + """A positive, smoothly varying parallel thermal speed.""" + eta1, eta2, eta3 = etas[0].T if len(etas) == 1 else etas + return 1.0 + 0.05 * np.cos(2.0 * np.pi * eta1) + + +# Background for kinetic species +maxwellian_1 = maxwellians.GyroMaxwellian2D( + n=(energetic_ion_density, None), + u_para=(energetic_ion_parallel_flow, None), + vth_para=(energetic_ion_parallel_thermal_speed, None), +) +maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None)) +background = maxwellian_1 + maxwellian_2 +model.energetic_ions.var.add_background(background) + +# Perturbations for (some) kinetic species +perturbation = perturbations.TorusModesCos() +maxwellian_1pt = maxwellians.GyroMaxwellian2D( + n=(energetic_ion_density, perturbation), + u_para=(energetic_ion_parallel_flow, None), + vth_para=(energetic_ion_parallel_thermal_speed, None), +) +init = maxwellian_1pt + maxwellian_2 +model.energetic_ions.var.add_initial_condition(init) + +if __name__ == "__main__": + sim.run() From 3cbef49b18c676c6099c8446c3bad2c1c5df499d Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 11:49:21 +0200 Subject: [PATCH 100/193] cleanup --- src/struphy/simulation/sim.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index efc0685c5..ad7266872 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1662,23 +1662,6 @@ def to_dict(self) -> dict: "profiling_opts": vars(self.profiling_opts).copy(), } - def _collect_particle_metadata(self) -> dict: - """Collect per-species marker metadata (Np, ppc, ppb) for the current sim.""" - particle_metadata = {} - for species_name, species in self.model.particle_species.items(): - species_metadata = {} - for variable_name, variable in species.variables.items(): - if isinstance(variable, PICVariable | SPHVariable) and hasattr(variable, "_particles"): - particles = variable.particles - species_metadata[variable_name] = { - "Np": particles.Np, - "ppc": particles.ppc, - "ppb": particles.ppb, - } - if species_metadata: - particle_metadata[species_name] = species_metadata - return particle_metadata - @staticmethod def _serialize_initial_condition(value): """Convert initial-condition definitions into JSON-compatible provenance data. @@ -1919,10 +1902,8 @@ def to_run_metadata(self, file_path: str = None, **extra_data) -> str: config = self.to_dict() config.update( { - "model_name": self.model_name, "mpi_ranks": self.comm_size, "use_mpi_comm_world": self.comm is not None, - "particle_species": self._collect_particle_metadata(), "initial_conditions_schema_version": 1, "initial_conditions": self._collect_initial_conditions_metadata(), **extra_data, From 4924d6a503653e2c4c66505cd62b0cbb193a116a Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 12:36:44 +0200 Subject: [PATCH 101/193] Removed the PostProcessor class and integrated the functionality into Output --- src/struphy/models/base.py | 26 +- src/struphy/post_processing/output.py | 1068 +++++++++++++++- .../post_processing/post_processing_tools.py | 1134 ----------------- .../post_processing/tests/test_eval_grids.py | 10 +- .../tests/test_eval_grids_mpi.py | 38 +- .../post_processing/tests/test_output.py | 30 +- src/struphy/simulation/sim.py | 40 +- src/struphy/simulation/tests/test_output.py | 60 +- 8 files changed, 1160 insertions(+), 1246 deletions(-) delete mode 100644 src/struphy/post_processing/post_processing_tools.py diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index c422ed6df..f00bc649a 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -927,10 +927,11 @@ def generate_default_parameter_file( return path - def to_dict(self) -> dict: + def to_dict(self, *, initial_condition_serializer=None) -> dict: """Serialize the model constructor, variables and propagator options. - Backgrounds and perturbations are not part of this dictionary. + Pass an initial-condition serializer to include each variable's definitions + in run metadata. The plain configuration omits them. """ params = {} for key, value in self.params.items(): @@ -939,16 +940,33 @@ def to_dict(self) -> dict: elif not isinstance(value, (bool, int, float, str, tuple, list, type(None))): raise TypeError(f"cannot serialize argument {key}={value!r} of {self.__class__.__name__}") params[key] = value - return { + species = {name: item.to_dict() for name, item in self.species.items()} + if initial_condition_serializer is not None: + for species_name, item in self.species.items(): + for variable_name, variable in item.variables.items(): + definitions = { + "backgrounds": initial_condition_serializer(variable.backgrounds), + "perturbations": initial_condition_serializer(variable.perturbations), + } + if isinstance(variable, PICVariable): + # Reading the property can mutate the variable's state. + definitions["initial_condition"] = initial_condition_serializer( + getattr(variable, "_initial_condition", variable.backgrounds) + ) + species[species_name]["variables"][variable_name]["initial_conditions"] = definitions + result = { "model": self.__class__.__name__, "params": params, - "species": {name: species.to_dict() for name, species in self.species.items()}, + "species": species, "propagator_options": { name: self._serialize_propagator_option(prop.options) for name, prop in vars(self.propagators).items() if isinstance(prop, Propagator) }, } + if initial_condition_serializer is not None: + result["initial_conditions_schema_version"] = 1 + return result def _serialize_propagator_option(self, value): """Convert nested option dataclasses and variable references to JSON data.""" diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 67c1b1594..5b820751d 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -4,26 +4,44 @@ import json import logging +import os +import shutil import warnings -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Callable, Iterator, Mapping, Sequence +from contextlib import ExitStack from functools import cached_property from html import escape from pathlib import Path from typing import Any import h5py +import cunumpy as xp import numpy as np import xarray as xr +from feectools.ddm.mpi import MockComm from feectools.ddm.mpi import mpi as MPI +from pyevtk.hl import gridToVTK +from struphy.feec.psydac_derham import Derham, SplineFunction +from struphy.models.species import ParticleSpecies +from struphy.models.variables import PICVariable, SPHVariable +from struphy.pic.base import Particles from struphy.post_processing import store -from struphy.post_processing.arrays import BINNED_LABELS, data_array, save_scalars +from struphy.post_processing.arrays import ( + BINNED_LABELS, data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits, +) +from struphy.post_processing.orbits import orbits_tools from struphy.post_processing.output_accessors import OutputPlots +from struphy.post_processing.manifest import MANIFEST_SCHEMA_VERSION, is_processed, normalize_options, source_fingerprint from struphy.post_processing.profiling import Profile from struphy.post_processing.si import to_si +from struphy.utils.progress import tqdm logger = logging.getLogger("struphy") +# Push-forward of each de Rham space to Cartesian components, see Domain.push. +PUSH_KINDS = {"H1": "0", "Hcurl": "1", "Hdiv": "2", "L2": "3", "H1vec": "v"} + def mpi_comm_world(): """The communicator used by output post-processing.""" @@ -422,8 +440,6 @@ def catalog(self, *, details: bool = False) -> xr.Dataset: def provenance(self, product: str | None = None) -> dict: """Return stored post-processing provenance and current raw-output freshness.""" - from struphy.post_processing.post_processing_tools import source_fingerprint - path = self.path_pproc / "manifest.json" manifest = json.loads(path.read_text()) if path.exists() else {} manifest["current"] = manifest.get("source_fingerprint") == source_fingerprint(str(self.path_out)) @@ -607,7 +623,9 @@ def initial_conditions(self) -> dict: """ from struphy.simulation.sim import Simulation - version = self.metadata.get("initial_conditions_schema_version", 1) + version = self.metadata.get("model", {}).get( + "initial_conditions_schema_version", self.metadata.get("initial_conditions_schema_version", 1) + ) if version != 1: raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") return { @@ -618,7 +636,21 @@ def initial_conditions(self) -> dict: } for variable_name, definition in variables.items() } - for species_name, variables in self.metadata.get("initial_conditions", {}).items() + for species_name, variables in self._initial_condition_metadata().items() + } + + def _initial_condition_metadata(self) -> dict: + """Read variable definitions, including the layout of older output folders.""" + legacy = self.metadata.get("initial_conditions") + if legacy is not None: + return legacy + return { + species_name: { + name: variable["initial_conditions"] + for name, variable in species.get("variables", {}).items() + if "initial_conditions" in variable + } + for species_name, species in self.metadata.get("model", {}).get("species", {}).items() } @cached_property @@ -669,8 +701,6 @@ def mpi_ranks(self) -> int: @property def is_processed(self) -> bool: """Whether complete post-processing of the current raw output exists.""" - from struphy.post_processing.post_processing_tools import is_processed - return is_processed(str(self.path_out)) def pproc( @@ -715,8 +745,6 @@ def pproc( Output This run, so that ``run = Output(path).pproc(physical=True)`` reads naturally. """ - from struphy.post_processing.post_processing_tools import PostProcessor - options = dict( step=step, celldivide=celldivide, @@ -726,19 +754,1023 @@ def pproc( create_vtk=create_vtk, force=force, ) - if parallel: - PostProcessor(self, parallel_pproc=True).process(**options) - else: - if self.comm.Get_rank() == 0: - PostProcessor(self).process(**options) - self.comm.Barrier() - self._reset() + try: + if parallel or self.comm.Get_rank() == 0: + self._setup_processing(parallel) + self._process_raw(**options) + if not parallel: + self.comm.Barrier() + finally: + self._reset() + for name in ( + "_pproc_derham", "_pproc_comm", "_pproc_rank", "_pproc_ranks", "_pproc_parallel", + "_pproc_t_grid", "_pproc_exist_fields", "_pproc_exist_particles", + "_pproc_kinetic_species", "_pproc_kinetic_kinds", "_collect_recv_bufs", + ): + self.__dict__.pop(name, None) return self + def _setup_processing(self, parallel: bool): + """Prepare the communicator and FEEC reconstruction for this processing run.""" + self._pproc_parallel = parallel + self._pproc_comm = self.comm if parallel else MockComm() + self._pproc_rank = self._pproc_comm.Get_rank() + if parallel and self._pproc_comm.Get_size() != self.mpi_ranks: + raise ValueError("Parallel post-processing requires the same number of MPI ranks as the saved run.") + self._pproc_ranks = range(self._pproc_rank, self._pproc_rank + 1) if parallel else range(self.mpi_ranks) + self._pproc_derham = None + if self.grid is not None and self.derham_opts is not None: + self._pproc_derham = Derham( + self.grid, self.derham_opts, comm=self._pproc_comm if parallel else None, domain=self.domain, + ) + def process(self, **options) -> "Output": """Compatibility alias for :meth:`pproc`.""" return self.pproc(**options) + def _write_manifest(self, status, *, options=None, error=None): + if self._pproc_rank != 0: + return + manifest = { + "schema_version": MANIFEST_SCHEMA_VERSION, + "status": status, + "source_fingerprint": source_fingerprint(str(self.path_out)), + "options": options or {}, + } + if error is not None: + manifest["error"] = str(error) + if status == "complete": + manifest["products"] = sorted( + os.path.relpath(os.path.join(root, name), self.path_pproc) + for root, _, files in os.walk(self.path_pproc) + for name in files + if name != "manifest.json" + ) + path = os.path.join(self.path_pproc, "manifest.json") + temporary = path + ".tmp" + with open(temporary, "w") as stream: + json.dump(manifest, stream, indent=2, sort_keys=True) + stream.write("\n") + os.replace(temporary, path) + + def _reset_pproc_dir(self): + if self._pproc_rank == 0: + if os.path.exists(self.path_pproc): + shutil.rmtree(self.path_pproc) + os.mkdir(self.path_pproc) + self._pproc_comm.Barrier() + + def _process_raw( + self, + step: int = 1, + celldivide: int | Sequence[int] = (1, 1, 1), + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = True, + force: bool = False, + ): + """Output post-processing for fields and particle data in ``self.path_out``. + + Parameters + ---------- + step : int + Interval of saved time steps to post-process (1 = every step, 2 = every second step, ...). + celldivide : int or sequence of int + Grid refinement factor when evaluating FEM fields (e.g. ``celldivide=(2, 2, 2)`` evaluates two + points per cell in each logical direction). A single int is applied to all three directions. + physical : bool + If True, also compute push-forwarded physical (x,y,z) components of fields. + guiding_center : bool + If True, compute guiding-center coordinates for particle orbits (requires + Particles6D marker data). + classify : bool + If True, run orbit classification (passing, trapped, lost) after computing orbits. + create_vtk : bool + If True, create VTK files for visualisation. + force : bool + Reprocess even when output already exists. Set False to reuse a previous + run's results, so a plotting script can be re-run cheaply. + + Returns + ------- + bool + Whether post-processing actually ran. + """ + options = normalize_options( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + ) + if not force and is_processed(str(self.path_out), options): + logger.warning(f"\nReusing existing post-processing in {self.path_pproc}") + return False + + self._reset_pproc_dir() + if self._pproc_rank == 0: + store.create(store.store_path(self.path_pproc), options=json.dumps(options)) + self._pproc_comm.Barrier() + self._write_manifest("processing", options=options) + logger.warning(f"\nPost-processing path {self.path_out}") + + # check for fields and kinetic data in hdf5 file that need post processing + with h5py.File(os.path.join(self.path_out, "data/", "data_proc0.hdf5"), "r") as file: + if self._pproc_rank == 0: + # save time grid at which post-processing data is created + xp.save(os.path.join(self.path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) + self._pproc_t_grid = xp.asarray(file["time/value"][::step]) + + if "feec" in file.keys(): + self._pproc_exist_fields = True + else: + self._pproc_exist_fields = False + + if "kinetic" in file.keys(): + self._pproc_exist_particles = {"markers": False, "f": False, "n_sph": False} + self._pproc_kinetic_species = [] + self._pproc_kinetic_kinds = [] + for name in file["kinetic"].keys(): + self._pproc_kinetic_species += [name] + self._pproc_kinetic_kinds += [next(iter(self.model.species[name].variables.values())).space] + + # check for saved markers + if "markers" in file["kinetic"][name]: + self._pproc_exist_particles["markers"] = True + # check for saved distribution function + if "f" in file["kinetic"][name]: + self._pproc_exist_particles["f"] = True + # check for saved sph density + if "n_sph" in file["kinetic"][name]: + self._pproc_exist_particles["n_sph"] = True + else: + self._pproc_exist_particles = None + + # feec variables + try: + self._process_fields(step=step, celldivide=celldivide, physical=physical, create_vtk=create_vtk) + self._process_particles(step=step, guiding_center=guiding_center, classify=classify) + except Exception as error: + self._write_manifest("failed", options=options, error=error) + raise + + self._write_manifest("complete", options=options) + + return True + + def _process_fields( + self, + step: int = 1, + celldivide: int | Sequence[int] = (1, 1, 1), + physical: bool = False, + create_vtk: bool = True, + ): + """Evaluate the FEEC fields of all saved time steps and write them to disk. + + The time steps are processed one after another: only the spline coefficients of a + single snapshot are held in memory, and each rank evaluates only those points of the + evaluation grid that lie in its own MPI domain. Arrays of the size of the global + evaluation grid therefore only ever exist on rank 0, where they are needed for output. + + Parameters + ---------- + step : int + Interval of saved time steps to post-process (1 = every step, 2 = every second step, ...). + celldivide : int or sequence of int + Grid refinement factor when evaluating FEM fields. A single int is applied to all + three directions. + physical : bool + If True, also compute push-forwarded physical (x,y,z) components of fields. + create_vtk : bool + If True, create VTK files for visualisation. + """ + if not self._pproc_exist_fields: + logger.warning("\nNo feec fields found in hdf5 file, skipping post-processing of fields.") + return + + # one set of spline functions, re-used for every time step + fields, t_grid = self._create_femfields(step=step) + + # evaluation grid; each rank only ever evaluates the points of its own domain + grids_log, grid_slices = self._create_eval_grids(celldivide=celldivide) + grids_log_loc = [grid[sl] for grid, sl in zip(grids_log, grid_slices[self._pproc_rank])] + glob_shape = tuple(grid.size for grid in grids_log) + + # the physical grid is only needed for output, hence it is only built on rank 0 + if self._pproc_rank == 0: + grids_phy = list(self.domain(*grids_log)) + else: + grids_phy = None + + # point_data[species][var][t] stays an empty list on all ranks except rank 0 + point_data = {species: {name: {} for name in vars} for species, vars in fields.items()} + point_data_phy = {species: {name: {} for name in vars} for species, vars in fields.items()} + + logger.warning("\nEvaluating fields ...") + with ExitStack() as stack: + # hdf5 files of the simulation ranks whose data is read by this rank + files = [ + stack.enter_context( + h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r"), + ) + for rank in self._pproc_ranks + ] + + for n, t in enumerate(tqdm(t_grid)): + self._load_femfields(fields, files, n, step=step) + + vals, vals_phy = self._eval_femfields( + fields, + grids_log_loc, + grid_slices, + glob_shape, + physical=physical, + ) + + if self._pproc_rank == 0: + for species, vars in vals.items(): + for name, val in vars.items(): + point_data[species][name][t] = val + point_data_phy[species][name][t] = vals_phy[species][name] + + # directory for the vtk files + path_fields = os.path.join(self.path_pproc, "fields_data") + + if self._pproc_rank == 0: + # one group per species in the product store, with the mapped grids as coordinates + for species, vars in point_data.items(): + variables = {} + for name, val in vars.items(): + variables[name] = wrap_field_data(val, grids_log, grids_phy=grids_phy, name=name) + if physical: + variables[name + "_xyz"] = wrap_field_data( + point_data_phy[species][name], grids_log, grids_phy=grids_phy, name=name + "_xyz" + ) + store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset(variables)) + + if create_vtk: + try: + os.mkdir(path_fields) + except FileExistsError: + shutil.rmtree(path_fields) + os.mkdir(path_fields) + self._create_vtk(path_fields, t_grid, grids_phy, point_data) + if physical: + self._create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) + self._pproc_comm.Barrier() + + def _process_particles( + self, + step: int = 1, + guiding_center: bool = False, + classify: bool = False, + ): + + if self._pproc_exist_particles is None: + logger.warning("\nNo kinetic data found in hdf5 file, skipping post-processing of kinetic data.") + return + + # directory for kinetic data + path_kinetics = os.path.join(self.path_pproc, "kinetic_data") + + if self._pproc_rank == 0: + try: + os.mkdir(path_kinetics) + except: + shutil.rmtree(path_kinetics) + os.mkdir(path_kinetics) + self._pproc_comm.Barrier() + + # kinetic post-processing for each species + for n, species in enumerate(self._pproc_kinetic_species): + # directory for each species + path_kinetics_species = os.path.join(path_kinetics, species) + + if self._pproc_rank == 0: + try: + os.mkdir(path_kinetics_species) + except: + shutil.rmtree(path_kinetics_species) + os.mkdir(path_kinetics_species) + self._pproc_comm.Barrier() + + # markers + if self._pproc_exist_particles["markers"]: + self._post_process_markers( + path_kinetics_species, + step, + ) + + if guiding_center: + assert self._pproc_kinetic_kinds[n] == "Particles6D" + orbits_tools.post_process_orbit_guiding_center( + self.domain, self.equil, path_kinetics_species, species + ) + + if classify: + orbits_tools.post_process_orbit_classification(path_kinetics_species, species) + + # distribution function + if self._pproc_exist_particles["f"]: + if self._pproc_kinetic_kinds[n] == "DeltaFParticles6D": + compute_bckgr = True + else: + compute_bckgr = False + + self._post_process_f( + path_kinetics_species, + step, + compute_bckgr=compute_bckgr, + ) + + # sph density + if self._pproc_exist_particles["n_sph"]: + self._post_process_n_sph( + path_kinetics_species, + step, + ) + + def _create_femfields(self, step: int = 1): + """Allocate one FEEC spline field object per saved variable. + + Only a single set of fields is allocated, no matter how many time steps are + post-processed; the coefficients of the individual snapshots are read into it one + after another by :meth:`_load_femfields`. + + Parameters + ---------- + step : int + Time-step stride when reading saved snapshots (default 1). + + Returns + ------- + fields : dict + Nested dictionary mapping species -> variable -> ``SplineFunction``. + t_grid : xp.ndarray + Array of times at which the fields were saved. + """ + # get fields names, space IDs and time grid from 0-th rank hdf5 file + with h5py.File(os.path.join(self.path_out, "data/", "data_proc0.hdf5"), "r") as file: + space_ids = {} + logger.warning("\nReading hdf5 data of following species:") + for species, dset in file["feec"].items(): + space_ids[species] = {} + logger.warning(f"{species}:") + for var, ddset in dset.items(): + space_ids[species][var] = ddset.attrs["space_id"] + logger.warning(f" {var}: {ddset}") + + t_grid = file["time/value"][::step].copy() + + # create one FemField for each variable, re-used for all snapshots + fields = {} + for species, vars in space_ids.items(): + fields[species] = {} + for var, id in vars.items(): + fields[species][var] = self._pproc_derham.create_spline_function( + var, + id, + ) + + logger.warning("Creation of Struphy Fields done.") + + return fields, t_grid + + def _load_femfields(self, fields: dict, files: list, n: int, step: int = 1): + """Read the spline coefficients of one snapshot into ``fields`` (in-place). + + Parameters + ---------- + fields : dict + Nested dictionary species -> variable -> ``SplineFunction``, as returned + by :meth:`_create_femfields`. + files : list + Open hdf5 files, one for each simulation rank processed by this rank. + n : int + Index of the snapshot in the (strided) time grid. + step : int + Time-step stride of the saved snapshots. + """ + for file in files: + for species, dset in file["feec"].items(): + for var, ddset in dset.items(): + # get global start indices, end indices and pads + gl_s = ddset.attrs["starts"] + gl_e = ddset.attrs["ends"] + pads = ddset.attrs["pads"] + + assert gl_s.shape == (3,) or gl_s.shape == (3, 3) + assert gl_e.shape == (3,) or gl_e.shape == (3, 3) + assert pads.shape == (3,) or pads.shape == (3, 3) + + vector = fields[species][var].vector + + # scalar field + if gl_s.shape == (3,): + s1, s2, s3 = gl_s + e1, e2, e3 = gl_e + p1, p2, p3 = pads + + vector[ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = ddset[n * step, p1:-p1, p2:-p2, p3:-p3] + + # vector-valued field + else: + for comp in range(3): + s1, s2, s3 = gl_s[comp] + e1, e2, e3 = gl_e[comp] + p1, p2, p3 = pads[comp] + + vector[comp][ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = ddset[str(comp + 1)][n * step, p1:-p1, p2:-p2, p3:-p3] + + vector.update_ghost_regions() + + def _create_eval_grids(self, celldivide: int | Sequence[int] = (1, 1, 1)): + """Build the logical evaluation grids and distribute them over the MPI ranks. + + The grid points are split among the ranks exactly as + :meth:`~struphy.feec.psydac_derham.SplineFunction._flag_pts_not_on_proc` does, + such that every point is evaluated by exactly one rank. This allows each rank + to allocate only its own part of the evaluation grid. + + Parameters + ---------- + celldivide : int or sequence of int + Refinement factor in each logical direction; a single int is applied to all + three directions, a sequence must have length three. + + Returns + ------- + grids_log : list + The three global logical 1d grids. + grid_slices : list + One entry per rank, holding the three slices of ``grids_log`` owned by that rank. + The slices of all ranks tile the global grid exactly. + """ + if isinstance(celldivide, int): + celldivide = (celldivide,) * 3 + + assert isinstance(celldivide, Sequence) + assert len(celldivide) == 3 + + num_elements = self._pproc_derham.num_elements + + grids_log = [ + xp.linspace(0.0, 1.0, num_elements_i * n_i + 1) for num_elements_i, n_i in zip(num_elements, celldivide) + ] + + # domain decomposition of the pproc communicator (one row per rank), see Derham.domain_array + dom_arr = self._pproc_derham.domain_array + + grid_slices = [] + for rank in range(dom_arr.shape[0]): + slices = [] + for n, grid in enumerate(grids_log): + left = dom_arr[rank, 3 * n + 0] + right = dom_arr[rank, 3 * n + 1] + + # points on an interior boundary are shifted into the process to the right of it + shifted = grid.copy() + if left != 0.0: + shifted[shifted == left] += 1e-8 + if right != 1.0: + shifted[shifted == right] += 1e-8 + + inds = xp.nonzero(xp.logical_and(shifted >= left, shifted <= right))[0] + assert inds.size > 0, f"Rank {rank} has no evaluation point in direction {n + 1}." + assert inds.size == inds[-1] - inds[0] + 1, "Evaluation points of a rank must be contiguous." + + slices += [slice(int(inds[0]), int(inds[-1]) + 1)] + + grid_slices += [tuple(slices)] + + # the local grids must tile the global evaluation grid exactly + n_points = sum( + (sl[0].stop - sl[0].start) * (sl[1].stop - sl[1].start) * (sl[2].stop - sl[2].start) for sl in grid_slices + ) + assert n_points == grids_log[0].size * grids_log[1].size * grids_log[2].size, ( + "The MPI domains do not tile the evaluation grid exactly." + ) + + return grids_log, grid_slices + + def _collect_on_root(self, loc_val: xp.ndarray, grid_slices: list, glob_shape: tuple): + """Assemble the local parts of an evaluation-grid array on rank 0. + + Only rank 0 allocates an array of the size of the global evaluation grid; + all other ranks just send the points they own. + + Parameters + ---------- + loc_val : xp.ndarray + Values on the evaluation points owned by this rank. + grid_slices : list + Slices of the global grid owned by each rank, see :meth:`_create_eval_grids`. + glob_shape : tuple + Number of points of the global evaluation grid in each direction. + + Returns + ------- + xp.ndarray or None + The global array on rank 0, None on all other ranks. + """ + if not self._pproc_parallel: + return loc_val + + if self._pproc_rank == 0: + glob_val = xp.empty(glob_shape, dtype=loc_val.dtype) + glob_val[grid_slices[0]] = loc_val + + # cache receive buffers to avoid repeated allocations in tight loops + if not hasattr(self, "_collect_recv_bufs"): + self._collect_recv_bufs = {} + + for rank in range(1, len(grid_slices)): + sl = grid_slices[rank] + shape = tuple(sl_i.stop - sl_i.start for sl_i in sl) + buf = self._collect_recv_bufs.get((rank, shape, loc_val.dtype)) + if buf is None: + buf = xp.empty(shape, dtype=loc_val.dtype) + self._collect_recv_bufs[(rank, shape, loc_val.dtype)] = buf + self._pproc_comm.Recv(buf, source=rank, tag=rank) + glob_val[sl] = buf + + return glob_val + + else: + self._pproc_comm.Send(xp.ascontiguousarray(loc_val), dest=0, tag=self._pproc_rank) + return None + + def _eval_femfields( + self, + fields: dict, + grids_log_loc: list, + grid_slices: list, + glob_shape: tuple, + *, + physical: bool = False, + ): + """Evaluate the spline fields of one snapshot on the evaluation grid. + + Each rank evaluates only the grid points of its own MPI domain, the values are + then collected on rank 0. + + Parameters + ---------- + fields : dict + Nested dictionary species -> var -> ``SplineFunction`` holding the coefficients + of one snapshot, see :meth:`_load_femfields`. + grids_log_loc : list + The three logical 1d grids restricted to the domain of this rank. + grid_slices : list + Slices of the global grid owned by each rank, see :meth:`_create_eval_grids`. + glob_shape : tuple + Number of points of the global evaluation grid in each direction. + physical : bool, optional + If True, also compute the push-forwarded physical (x,y,z) components. + + Returns + ------- + vals, vals_phy : dict + Nested dictionaries species -> var -> list of arrays (one entry for scalar-valued + and three entries for vector-valued spaces). The arrays are only assembled on + rank 0, the lists stay empty on all other ranks. ``vals_phy`` holds empty lists + if ``physical`` is False. + """ + vals = {} + vals_phy = {} + for species, vars in fields.items(): + vals[species] = {} + vals_phy[species] = {} + for name, field in vars.items(): + assert isinstance(field, SplineFunction) + + vals[species][name] = [] + vals_phy[species][name] = [] + + # evaluate the field on the grid points of this rank only + loc_val = field(*grids_log_loc, local=True) + + if physical: + # push-forward + loc_val_phy = self.domain.push( + loc_val, + *grids_log_loc, + kind=PUSH_KINDS[field.space_id], + ) + + # scalar spaces + if isinstance(loc_val, xp.ndarray): + comps = [loc_val] + comps_phy = [loc_val_phy] if physical else [] + # vector-valued spaces + else: + comps = [loc_val[j] for j in range(3)] + comps_phy = [loc_val_phy[j] for j in range(3)] if physical else [] + + # collect the values of all ranks on rank 0 + for comp in comps: + glob_val = self._collect_on_root(comp, grid_slices, glob_shape) + if self._pproc_rank == 0: + vals[species][name] += [glob_val] + + for comp in comps_phy: + glob_val = self._collect_on_root(comp, grid_slices, glob_shape) + if self._pproc_rank == 0: + vals_phy[species][name] += [glob_val] + + return vals, vals_phy + + def _create_vtk( + self, + path: str, + t_grid: xp.ndarray, + grids_phy: list, + point_data: dict, + *, + physical: bool = False, + ): + """Write evaluated field arrays to VTK (.vts) files for visualization. + + Parameters + ---------- + path : str + Directory where species subfolders and their `vtk` folders will be created. + t_grid : xp.ndarray + Time grid corresponding to entries in ``point_data``. + grids_phy : list + Physical coordinate arrays returned by :meth:`_eval_femfields`. + point_data : dict + Evaluated field values as returned by :meth:`_eval_femfields`. + physical : bool, optional + If True, writes files for push-forwarded physical components (folder suffix "_phy"). + """ + for species, vars in point_data.items(): + species_path = os.path.join(path, species, "vtk" + physical * "_phy") + if os.path.exists(species_path): + shutil.rmtree(species_path) + os.makedirs(species_path) + + # time loop + nt = max(len(t_grid) - 1, 1) + log_nt = int(xp.log10(nt)) + 1 + + logger.warning(f"\nCreating vtk in {path} ...") + for n, t in enumerate(tqdm(t_grid)): + point_data_n = {} + + for species, vars in point_data.items(): + species_path = os.path.join(path, species, "vtk" + physical * "_phy") + point_data_n[species] = {} + for name, data in vars.items(): + points_list = data[t] + + # scalar + if len(points_list) == 1: + point_data_n[species][name] = points_list[0] + + # vectorpoint_data[name] + else: + for j in range(3): + point_data_n[species][name + f"_{j + 1}"] = points_list[j] + + gridToVTK( + os.path.join(species_path, "step_{0:0{1}d}".format(n, log_nt)), + *grids_phy, + pointData=point_data_n[species], + ) + + def _post_process_markers( + self, + path_kinetic_species: str, + step: int = 1, + ): + """Compute Cartesian marker positions and write them to .npy and .txt files. + + For each saved time step this function collects marker datasets from all MPI ranks, + reconstructs full marker arrays (positions, velocities, weights, ids), maps logical + coordinates to physical coordinates via ``self.domain`` and writes per-step + ``.npy`` (binary) and ``.txt`` (ASCII) files suitable for quick inspection or + import into visualization tools. + + Parameters + ---------- + path_kinetic_species : str + Path to the per-species kinetic output directory where results will be written. + step : int, optional + Time-step stride to process (default 1). + """ + + species = path_kinetic_species.split("/")[-1] + species_obj: ParticleSpecies = self.model.particle_species[species] + + # open hdf5 files and get names and number of saved markers of kinetic species + with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: + # get number of time steps and markers + nt, n_markers, n_cols = file_0["kinetic/" + species + "/markers"].shape + + # get velocity dimension from one of the variables of the species + for _, var in species_obj.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + cls: Particles = var.particles_class + vdim = cls.vdim + break + + log_nt = int(xp.log10(int(((nt - 1) / step)))) + 1 + + # directory for .txt files and marker index which will be saved + path_orbits = os.path.join(path_kinetic_species, "orbits") + + if vdim == 2: + save_index = list(range(0, 6)) + [10] + [-1] + elif vdim == 3: + save_index = list(range(0, 7)) + [-1] + else: + save_index = list(range(0, 4)) + [-1] + + if self._pproc_rank == 0: + try: + os.mkdir(path_orbits) + except: + shutil.rmtree(path_orbits) + os.mkdir(path_orbits) + self._pproc_comm.Barrier() + + # temporary array, plus every step of it for the product store + temp = xp.empty((n_markers, len(save_index)), order="C") + orbits = [] + lost_particles_mask = xp.empty(n_markers, dtype=bool) + + logger.warning(f"Evaluation of {n_markers} marker orbits for {species}") + + # loop over time grid + for n in tqdm(range(int((nt - 1) / step) + 1)): + # clear buffer + temp[:, :] = 0.0 + + # create text file for this time step and this species + file_npy = os.path.join( + path_orbits, + species + "_{0:0{1}d}.npy".format(n, log_nt), + ) + file_txt = os.path.join( + path_orbits, + species + "_{0:0{1}d}.txt".format(n, log_nt), + ) + + for rank in self._pproc_ranks: + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + markers = file["kinetic/" + species + "/markers"] + ids = markers[n * step, :, -1].astype("int") + ids = ids[ids != -1] # exclude holes + temp[ids] = markers[n * step, : ids.size, save_index] + + if self._pproc_parallel: + if self._pproc_rank == 0: + self._pproc_comm.Reduce(MPI.IN_PLACE, temp, op=MPI.SUM, root=0) + else: + self._pproc_comm.Reduce(temp, None, op=MPI.SUM, root=0) + + # sorting out lost particles + ids = temp[:, -1].astype("int") + ids_lost_particles = xp.setdiff1d(xp.arange(n_markers), ids) + ids_removed_particles = xp.nonzero(temp[:, 0] == -1.0)[0] + ids_lost_particles = xp.array(list(set(ids_lost_particles) | set(ids_removed_particles)), dtype=int) + lost_particles_mask[:] = False + lost_particles_mask[ids_lost_particles] = True + + if len(ids_lost_particles) > 0: + # lost markers are saved as [0, ..., 0, ids] + temp[lost_particles_mask, -1] = ids_lost_particles + ids = xp.unique(xp.append(ids, ids_lost_particles)) + + assert xp.all(sorted(ids) == xp.arange(n_markers)) + + # compute physical positions (x, y, z) + pos_phys = self.domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) + temp[~lost_particles_mask, :3] = pos_phys + + if self._pproc_rank == 0: + orbits.append(temp.copy()) + # save numpy + xp.save(file_npy, temp) + # move ids to first column and save txt + temp = xp.roll(temp, 1, axis=1) + xp.savetxt(file_txt, temp[:, (0, 1, 2, 3, -1)], fmt="%12.6f", delimiter=", ") + self._pproc_comm.Barrier() + + if self._pproc_rank == 0: + values = wrap_orbits(xp.stack(orbits), self._pproc_t_grid[: len(orbits)]) + store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset({"orbits": values})) + + def _post_process_f( + self, + path_kinetic_species, + step=1, + compute_bckgr=False, + ): + """Assemble and save distribution functions from per-rank binned data. + + This reads the binned full-f and delta-f arrays produced by the simulation across + MPI ranks, sums them to global arrays, and stores the results under + ``/distribution_function/``. When ``compute_bckgr`` is + True, an analytic kinetic background is evaluated on the same grids and added. + + Parameters + ---------- + path_kinetic_species : str + Path to the per-species kinetic output directory. + step : int, optional + Time-step stride to process (default 1). + compute_bckgr : bool, optional + If True, add the background stored by the simulation to the binned delta f. + """ + print(f"{self._pproc_rank} starting post-processing of distribution functions for {path_kinetic_species} ...") + + species = path_kinetic_species.split("/")[-1] + + logger.warning("Evaluation of distribution functions for " + str(species)) + + # the bin centers of every slice, as saved by the simulation + slice_grids = {} + with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: + for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): + dims = [part for part in slice_name.split("_")] + centers = [grid[:] for _, grid in file_0["kinetic/" + species + "/f/" + slice_name].attrs.items()] + slice_grids[slice_name] = dict(zip(dims, centers)) + slice_names = list(slice_grids) + + # compute distribution function + for slice_name in tqdm(slice_names): + logger.info(f"Processing slice {slice_name} for species {species}") + grids = slice_grids[slice_name] + + for rank in self._pproc_ranks: + print(f"{rank = } ----------------------------") + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + if self._pproc_parallel: + data = file["kinetic/" + species + "/f/" + slice_name][::step] + data_df = file["kinetic/" + species + "/df/" + slice_name][::step] + else: + if rank == 0: + data = file["kinetic/" + species + "/f/" + slice_name][::step].copy() + data_df = file["kinetic/" + species + "/df/" + slice_name][::step].copy() + else: + data += file["kinetic/" + species + "/f/" + slice_name][::step] + data_df += file["kinetic/" + species + "/df/" + slice_name][::step] + + print(f"{self._pproc_rank =} with {xp.sum(data) =} and {xp.sum(data_df) =}") + + if self._pproc_parallel: + if self._pproc_rank == 0: + self._pproc_comm.Reduce( + MPI.IN_PLACE, + data, + op=MPI.SUM, + root=0, + ) + self._pproc_comm.Reduce( + MPI.IN_PLACE, + data_df, + op=MPI.SUM, + root=0, + ) + else: + self._pproc_comm.Reduce( + data, + None, + op=MPI.SUM, + root=0, + ) + self._pproc_comm.Reduce( + data_df, + None, + op=MPI.SUM, + root=0, + ) + + print(f"{self._pproc_rank =} with {xp.sum(data) =} and {xp.sum(data_df) =}") + + print(f"{self._pproc_rank =} done.") + if self._pproc_rank == 0: + full_f = data + if compute_bckgr: + # the background of a delta-f species is stored by the simulation on the bin centers + key_background = f"kinetic/{species}/f_background/{slice_name}" + with h5py.File(os.path.join(self.path_out, "data", "data_proc0.hdf5"), "r") as file: + if key_background not in file: + raise ValueError( + f"{key_background} is missing from the raw output; outputs of older versions " + "do not store the background of delta-f species." + ) + data_bckgr = file[key_background][()] + + # add extra axis for data_bckgr since data_df has axis for time series + full_f = data_df + data_bckgr[None] + + store.write_group( + store.store_path(self.path_pproc), + f"/{species}/{slice_name}", + self._binned_dataset(grids, {"f": full_f, "delta_f": data_df}), + ) + + def _binned_dataset(self, grids: dict, variables: dict) -> xr.Dataset: + """One binned product per variable, with time, bin centers and mapped coordinates.""" + dims = tuple(dim for dim in grids) + coords = {"t": self._pproc_t_grid, **grids} + coords.update(self._mapped_coords(grids)) + return xr.Dataset( + {name: wrap_binned_data(values, dims, coords, name=name) for name, values in variables.items()} + ) + + def _mapped_coords(self, grids: dict) -> dict: + """``X``, ``Y``, ``Z`` on the logical directions of ``grids``, when there are two or three.""" + logical = tuple(dim for dim in grids if dim in ("e1", "e2", "e3")) + if len(logical) not in (2, 3) or self.domain is None: + return {} + try: + if len(logical) == 2: + mesh = xp.meshgrid(*(xp.asarray(grids[dim]) for dim in logical), indexing="ij") + arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} + arguments.update(dict(zip(logical, mesh))) + mapped = self.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) + else: + mapped = self.domain(*(xp.asarray(grids[dim]) for dim in logical)) + except (TypeError, ValueError): + logger.debug("Could not map the coordinates of %s", logical, exc_info=True) + return {} + return {name: (logical, xp.asarray(grid)) for name, grid in zip(("X", "Y", "Z"), mapped)} + + def _post_process_n_sph( + self, + path_kinetic_species, + step=1, + ): + """Compute and save SPH density fields from per-rank outputs. + + Parameters + ---------- + path_kinetic_species : str + Path to the per-species kinetic output directory where results will be written. + step : int, optional + Time-step stride to process (default 1). + """ + species = path_kinetic_species.split("/")[-1] + + logger.warning("Evaluation of sph density for " + str(species)) + + # the evaluation points of every view, as saved by the simulation + view_grids = {} + with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: + for view in file_0["kinetic/" + species + "/n_sph"]: + attrs = file_0["kinetic/" + species + "/n_sph/" + view].attrs + view_grids[view] = {f"e{direction}": attrs["eta" + direction][:] for direction in ("1", "2", "3")} + views = list(view_grids) + + # compute sph density + for view in tqdm(views): + for rank in self._pproc_ranks: + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + if self._pproc_parallel: + data = file["kinetic/" + species + "/n_sph/" + view][::step] + else: + if rank == 0: + data = file["kinetic/" + species + "/n_sph/" + view][::step].copy() + else: + data += file["kinetic/" + species + "/n_sph/" + view][::step] + + if self._pproc_parallel: + if self._pproc_rank == 0: + self._pproc_comm.Reduce( + MPI.IN_PLACE, + data, + op=MPI.SUM, + root=0, + ) + else: + self._pproc_comm.Reduce( + data, + None, + op=MPI.SUM, + root=0, + ) + + if self._pproc_rank == 0: + store.write_group( + store.store_path(self.path_pproc), + f"/{species}/{view}", + self._binned_dataset(view_grids[view], {"n": data}), + ) + def _ensure_processed(self): if self.is_processed: return @@ -1026,7 +2058,7 @@ def info(self) -> None: for name, options in model.get("propagator_options", {}).items(): lines.append(f" {name}: {json.dumps(options, sort_keys=True)}") lines.append("Initial conditions:") - for species_name, variables in self.metadata.get("initial_conditions", {}).items(): + for species_name, variables in self._initial_condition_metadata().items(): for variable_name, definition in variables.items(): parts = ", ".join(f"{key}={self._initial_condition_description(value)}" for key, value in definition.items()) lines.append(f" {species_name}.{variable_name}: {parts}") diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py deleted file mode 100644 index ef4e1a905..000000000 --- a/src/struphy/post_processing/post_processing_tools.py +++ /dev/null @@ -1,1134 +0,0 @@ -import hashlib -import json -import logging -import os -import shutil -from collections.abc import Sequence -from contextlib import ExitStack -from typing import TYPE_CHECKING - -import cunumpy as xp -import h5py -import xarray as xr -from feectools.ddm.mpi import MockComm -from feectools.ddm.mpi import mpi as MPI -from pyevtk.hl import gridToVTK - -from struphy.feec.psydac_derham import Derham, SplineFunction -from struphy.models.species import ParticleSpecies -from struphy.models.variables import PICVariable, SPHVariable -from struphy.pic.base import Particles -from struphy.post_processing import store -from struphy.post_processing.arrays import wrap_binned_data, wrap_field_data, wrap_orbits -from struphy.post_processing.orbits import orbits_tools -from struphy.utils.progress import tqdm - -if TYPE_CHECKING: - from struphy.post_processing.output import Output - -logger = logging.getLogger("struphy") - -# push-forward of each de Rham space to Cartesian components, see Domain.push -PUSH_KINDS = {"H1": "0", "Hcurl": "1", "Hdiv": "2", "L2": "3", "H1vec": "v"} - - -MANIFEST_SCHEMA_VERSION = 1 - - -def source_fingerprint(path_out: str) -> str: - """Fingerprint the raw run files that determine post-processing products.""" - digest = hashlib.sha256() - for name in ("config.json", "run_metadata.json", "meta.yml", "data/data_proc0.hdf5"): - path = os.path.join(path_out, name) - if not os.path.exists(path): - continue - stat = os.stat(path) - digest.update(name.encode()) - digest.update(f"{stat.st_size}:{stat.st_mtime_ns}".encode()) - if name != "data/data_proc0.hdf5": - with open(path, "rb") as stream: - digest.update(stream.read()) - return digest.hexdigest() - - -def normalize_options(**options) -> dict: - """JSON-comparable processing options, as stored in the manifest.""" - celldivide = options.get("celldivide") - if celldivide is not None: - options["celldivide"] = [int(celldivide)] * 3 if isinstance(celldivide, int) else [int(c) for c in celldivide] - return options - - -def is_processed(path_out: str, options: dict | None = None) -> bool: - """Whether ``path_out`` holds complete post-processing of its current raw output. - - With ``options``, the stored processing options must match as well, so a request for - different products (e.g. ``physical=True``) is never answered with stale ones. - """ - path = os.path.join(path_out, "post_processing", "manifest.json") - try: - with open(path) as stream: - manifest = json.load(stream) - except (OSError, ValueError): - return False - return ( - manifest.get("schema_version") == MANIFEST_SCHEMA_VERSION - and manifest.get("status") == "complete" - and manifest.get("source_fingerprint") == source_fingerprint(path_out) - and (options is None or manifest.get("options") == normalize_options(**options)) - ) - - -class PostProcessor: - """Post-process the raw output of a finished Struphy simulation. - - Use :meth:`from_output` to reconstruct a serial processor from a saved run. - For automatic MPI rank handling, use :meth:`struphy.Output.process`. - - Parameters - ---------- - output : Output - Output folder and lazily reconstructed configuration of the saved run. - parallel_pproc : bool, optional - Whether to run post-processing in parallel using MPI. This requires the same - number of ranks as the saved run and a call on every rank. Default is False (serial post-processing). - - Attributes - ---------- - path_out : str - Path to simulation output folder. - path_pproc : str - Path to the post-processing directory inside ``path_out``. - derham : object or None - Helper used to reconstruct FEEC spline fields. - domain : Domain - Computational domain used to map logical -> physical coordinates. - model : StruphyModel - Model instance describing species and variables. - comm_size : int - Number of MPI ranks used to produce the output. - """ - - def __init__(self, output: "Output", parallel_pproc: bool = False): - self.path_out = str(output.path_out) - self.path_pproc = os.path.join(self.path_out, "post_processing") - self.parallel_pproc = parallel_pproc - self.domain = output.domain - self.equil = output.equil - self.model = output.model - self.comm_size = output.mpi_ranks - self.comm = output.comm if parallel_pproc else MockComm() - self.rank = self.comm.Get_rank() - if parallel_pproc and self.comm.Get_size() != self.comm_size: - raise ValueError("Parallel post-processing requires the same number of MPI ranks as the saved run.") - self.range_ranks = range(self.rank, self.rank + 1) if parallel_pproc else range(self.comm_size) - self.derham = None - if output.grid is not None and output.derham_opts is not None: - self.derham = Derham( - output.grid, - output.derham_opts, - comm=self.comm if parallel_pproc else None, - domain=self.domain, - ) - - # the directory is only cleared in process(), so that constructing a - # PostProcessor to inspect a run does not destroy its post-processed data - if self.rank == 0: - os.makedirs(self.path_pproc, exist_ok=True) - self.comm.Barrier() - - @classmethod - def from_output(cls, path_out: str | os.PathLike) -> "PostProcessor": - """Create a serial processor from a saved output folder. - - Reads ``run_metadata.json`` (or legacy ``config.json`` when absent), without - executing the parameter file or allocating a simulation. The folder may - have been moved. Existing post-processing products are preserved until - :meth:`process` is called. Under MPI, call this on one rank only. - """ - from struphy.post_processing.output import Output - - return cls(Output(path_out)) - - def _write_manifest(self, status, *, options=None, error=None): - if self.rank != 0: - return - manifest = { - "schema_version": MANIFEST_SCHEMA_VERSION, - "status": status, - "source_fingerprint": source_fingerprint(self.path_out), - "options": options or {}, - } - if error is not None: - manifest["error"] = str(error) - if status == "complete": - manifest["products"] = sorted( - os.path.relpath(os.path.join(root, name), self.path_pproc) - for root, _, files in os.walk(self.path_pproc) - for name in files - if name != "manifest.json" - ) - path = os.path.join(self.path_pproc, "manifest.json") - temporary = path + ".tmp" - with open(temporary, "w") as stream: - json.dump(manifest, stream, indent=2, sort_keys=True) - stream.write("\n") - os.replace(temporary, path) - - def _reset_pproc_dir(self): - if self.rank == 0: - if os.path.exists(self.path_pproc): - shutil.rmtree(self.path_pproc) - os.mkdir(self.path_pproc) - self.comm.Barrier() - - def process( - self, - step: int = 1, - celldivide: int | Sequence[int] = (1, 1, 1), - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - create_vtk: bool = True, - force: bool = False, - ): - """Output post-processing for fields and particle data in ``self.path_out``. - - Parameters - ---------- - step : int - Interval of saved time steps to post-process (1 = every step, 2 = every second step, ...). - celldivide : int or sequence of int - Grid refinement factor when evaluating FEM fields (e.g. ``celldivide=(2, 2, 2)`` evaluates two - points per cell in each logical direction). A single int is applied to all three directions. - physical : bool - If True, also compute push-forwarded physical (x,y,z) components of fields. - guiding_center : bool - If True, compute guiding-center coordinates for particle orbits (requires - Particles6D marker data). - classify : bool - If True, run orbit classification (passing, trapped, lost) after computing orbits. - create_vtk : bool - If True, create VTK files for visualisation. - force : bool - Reprocess even when output already exists. Set False to reuse a previous - run's results, so a plotting script can be re-run cheaply. - - Returns - ------- - bool - Whether post-processing actually ran. - """ - options = normalize_options( - step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, - classify=classify, - create_vtk=create_vtk, - ) - if not force and is_processed(self.path_out, options): - logger.warning(f"\nReusing existing post-processing in {self.path_pproc}") - return False - - self._reset_pproc_dir() - if self.rank == 0: - store.create(store.store_path(self.path_pproc), options=json.dumps(options)) - self.comm.Barrier() - self._write_manifest("processing", options=options) - logger.warning(f"\nPost-processing path {self.path_out}") - - # check for fields and kinetic data in hdf5 file that need post processing - with h5py.File(os.path.join(self.path_out, "data/", "data_proc0.hdf5"), "r") as file: - if self.rank == 0: - # save time grid at which post-processing data is created - xp.save(os.path.join(self.path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) - self.t_grid = xp.asarray(file["time/value"][::step]) - - if "feec" in file.keys(): - self.exist_fields = True - else: - self.exist_fields = False - - if "kinetic" in file.keys(): - self.exist_particles = {"markers": False, "f": False, "n_sph": False} - self.kinetic_species = [] - self.kinetic_kinds = [] - for name in file["kinetic"].keys(): - self.kinetic_species += [name] - self.kinetic_kinds += [next(iter(self.model.species[name].variables.values())).space] - - # check for saved markers - if "markers" in file["kinetic"][name]: - self.exist_particles["markers"] = True - # check for saved distribution function - if "f" in file["kinetic"][name]: - self.exist_particles["f"] = True - # check for saved sph density - if "n_sph" in file["kinetic"][name]: - self.exist_particles["n_sph"] = True - else: - self.exist_particles = None - - # feec variables - try: - self.process_fields(step=step, celldivide=celldivide, physical=physical, create_vtk=create_vtk) - self.process_particles(step=step, guiding_center=guiding_center, classify=classify) - except Exception as error: - self._write_manifest("failed", options=options, error=error) - raise - - self._write_manifest("complete", options=options) - - return True - - def process_fields( - self, - step: int = 1, - celldivide: int | Sequence[int] = (1, 1, 1), - physical: bool = False, - create_vtk: bool = True, - ): - """Evaluate the FEEC fields of all saved time steps and write them to disk. - - The time steps are processed one after another: only the spline coefficients of a - single snapshot are held in memory, and each rank evaluates only those points of the - evaluation grid that lie in its own MPI domain. Arrays of the size of the global - evaluation grid therefore only ever exist on rank 0, where they are needed for output. - - Parameters - ---------- - step : int - Interval of saved time steps to post-process (1 = every step, 2 = every second step, ...). - celldivide : int or sequence of int - Grid refinement factor when evaluating FEM fields. A single int is applied to all - three directions. - physical : bool - If True, also compute push-forwarded physical (x,y,z) components of fields. - create_vtk : bool - If True, create VTK files for visualisation. - """ - if not self.exist_fields: - logger.warning("\nNo feec fields found in hdf5 file, skipping post-processing of fields.") - return - - # one set of spline functions, re-used for every time step - fields, t_grid = self._create_femfields(step=step) - - # evaluation grid; each rank only ever evaluates the points of its own domain - grids_log, grid_slices = self._create_eval_grids(celldivide=celldivide) - grids_log_loc = [grid[sl] for grid, sl in zip(grids_log, grid_slices[self.rank])] - glob_shape = tuple(grid.size for grid in grids_log) - - # the physical grid is only needed for output, hence it is only built on rank 0 - if self.rank == 0: - grids_phy = list(self.domain(*grids_log)) - else: - grids_phy = None - - # point_data[species][var][t] stays an empty list on all ranks except rank 0 - point_data = {species: {name: {} for name in vars} for species, vars in fields.items()} - point_data_phy = {species: {name: {} for name in vars} for species, vars in fields.items()} - - logger.warning("\nEvaluating fields ...") - with ExitStack() as stack: - # hdf5 files of the simulation ranks whose data is read by this rank - files = [ - stack.enter_context( - h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r"), - ) - for rank in self.range_ranks - ] - - for n, t in enumerate(tqdm(t_grid)): - self._load_femfields(fields, files, n, step=step) - - vals, vals_phy = self._eval_femfields( - fields, - grids_log_loc, - grid_slices, - glob_shape, - physical=physical, - ) - - if self.rank == 0: - for species, vars in vals.items(): - for name, val in vars.items(): - point_data[species][name][t] = val - point_data_phy[species][name][t] = vals_phy[species][name] - - # directory for the vtk files - path_fields = os.path.join(self.path_pproc, "fields_data") - - if self.rank == 0: - # one group per species in the product store, with the mapped grids as coordinates - for species, vars in point_data.items(): - variables = {} - for name, val in vars.items(): - variables[name] = wrap_field_data(val, grids_log, grids_phy=grids_phy, name=name) - if physical: - variables[name + "_xyz"] = wrap_field_data( - point_data_phy[species][name], grids_log, grids_phy=grids_phy, name=name + "_xyz" - ) - store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset(variables)) - - if create_vtk: - try: - os.mkdir(path_fields) - except FileExistsError: - shutil.rmtree(path_fields) - os.mkdir(path_fields) - self._create_vtk(path_fields, t_grid, grids_phy, point_data) - if physical: - self._create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) - self.comm.Barrier() - - def process_particles( - self, - step: int = 1, - guiding_center: bool = False, - classify: bool = False, - ): - - if self.exist_particles is None: - logger.warning("\nNo kinetic data found in hdf5 file, skipping post-processing of kinetic data.") - return - - # directory for kinetic data - path_kinetics = os.path.join(self.path_pproc, "kinetic_data") - - if self.rank == 0: - try: - os.mkdir(path_kinetics) - except: - shutil.rmtree(path_kinetics) - os.mkdir(path_kinetics) - self.comm.Barrier() - - # kinetic post-processing for each species - for n, species in enumerate(self.kinetic_species): - # directory for each species - path_kinetics_species = os.path.join(path_kinetics, species) - - if self.rank == 0: - try: - os.mkdir(path_kinetics_species) - except: - shutil.rmtree(path_kinetics_species) - os.mkdir(path_kinetics_species) - self.comm.Barrier() - - # markers - if self.exist_particles["markers"]: - self._post_process_markers( - path_kinetics_species, - step, - ) - - if guiding_center: - assert self.kinetic_kinds[n] == "Particles6D" - orbits_tools.post_process_orbit_guiding_center( - self.domain, self.equil, path_kinetics_species, species - ) - - if classify: - orbits_tools.post_process_orbit_classification(path_kinetics_species, species) - - # distribution function - if self.exist_particles["f"]: - if self.kinetic_kinds[n] == "DeltaFParticles6D": - compute_bckgr = True - else: - compute_bckgr = False - - self._post_process_f( - path_kinetics_species, - step, - compute_bckgr=compute_bckgr, - ) - - # sph density - if self.exist_particles["n_sph"]: - self._post_process_n_sph( - path_kinetics_species, - step, - ) - - def _create_femfields(self, step: int = 1): - """Allocate one FEEC spline field object per saved variable. - - Only a single set of fields is allocated, no matter how many time steps are - post-processed; the coefficients of the individual snapshots are read into it one - after another by :meth:`_load_femfields`. - - Parameters - ---------- - step : int - Time-step stride when reading saved snapshots (default 1). - - Returns - ------- - fields : dict - Nested dictionary mapping species -> variable -> ``SplineFunction``. - t_grid : xp.ndarray - Array of times at which the fields were saved. - """ - # get fields names, space IDs and time grid from 0-th rank hdf5 file - with h5py.File(os.path.join(self.path_out, "data/", "data_proc0.hdf5"), "r") as file: - space_ids = {} - logger.warning("\nReading hdf5 data of following species:") - for species, dset in file["feec"].items(): - space_ids[species] = {} - logger.warning(f"{species}:") - for var, ddset in dset.items(): - space_ids[species][var] = ddset.attrs["space_id"] - logger.warning(f" {var}: {ddset}") - - t_grid = file["time/value"][::step].copy() - - # create one FemField for each variable, re-used for all snapshots - fields = {} - for species, vars in space_ids.items(): - fields[species] = {} - for var, id in vars.items(): - fields[species][var] = self.derham.create_spline_function( - var, - id, - ) - - logger.warning("Creation of Struphy Fields done.") - - return fields, t_grid - - def _load_femfields(self, fields: dict, files: list, n: int, step: int = 1): - """Read the spline coefficients of one snapshot into ``fields`` (in-place). - - Parameters - ---------- - fields : dict - Nested dictionary species -> variable -> ``SplineFunction``, as returned - by :meth:`_create_femfields`. - files : list - Open hdf5 files, one for each simulation rank processed by this rank. - n : int - Index of the snapshot in the (strided) time grid. - step : int - Time-step stride of the saved snapshots. - """ - for file in files: - for species, dset in file["feec"].items(): - for var, ddset in dset.items(): - # get global start indices, end indices and pads - gl_s = ddset.attrs["starts"] - gl_e = ddset.attrs["ends"] - pads = ddset.attrs["pads"] - - assert gl_s.shape == (3,) or gl_s.shape == (3, 3) - assert gl_e.shape == (3,) or gl_e.shape == (3, 3) - assert pads.shape == (3,) or pads.shape == (3, 3) - - vector = fields[species][var].vector - - # scalar field - if gl_s.shape == (3,): - s1, s2, s3 = gl_s - e1, e2, e3 = gl_e - p1, p2, p3 = pads - - vector[ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = ddset[n * step, p1:-p1, p2:-p2, p3:-p3] - - # vector-valued field - else: - for comp in range(3): - s1, s2, s3 = gl_s[comp] - e1, e2, e3 = gl_e[comp] - p1, p2, p3 = pads[comp] - - vector[comp][ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = ddset[str(comp + 1)][n * step, p1:-p1, p2:-p2, p3:-p3] - - vector.update_ghost_regions() - - def _create_eval_grids(self, celldivide: int | Sequence[int] = (1, 1, 1)): - """Build the logical evaluation grids and distribute them over the MPI ranks. - - The grid points are split among the ranks exactly as - :meth:`~struphy.feec.psydac_derham.SplineFunction._flag_pts_not_on_proc` does, - such that every point is evaluated by exactly one rank. This allows each rank - to allocate only its own part of the evaluation grid. - - Parameters - ---------- - celldivide : int or sequence of int - Refinement factor in each logical direction; a single int is applied to all - three directions, a sequence must have length three. - - Returns - ------- - grids_log : list - The three global logical 1d grids. - grid_slices : list - One entry per rank, holding the three slices of ``grids_log`` owned by that rank. - The slices of all ranks tile the global grid exactly. - """ - if isinstance(celldivide, int): - celldivide = (celldivide,) * 3 - - assert isinstance(celldivide, Sequence) - assert len(celldivide) == 3 - - num_elements = self.derham.num_elements - - grids_log = [ - xp.linspace(0.0, 1.0, num_elements_i * n_i + 1) for num_elements_i, n_i in zip(num_elements, celldivide) - ] - - # domain decomposition of the pproc communicator (one row per rank), see Derham.domain_array - dom_arr = self.derham.domain_array - - grid_slices = [] - for rank in range(dom_arr.shape[0]): - slices = [] - for n, grid in enumerate(grids_log): - left = dom_arr[rank, 3 * n + 0] - right = dom_arr[rank, 3 * n + 1] - - # points on an interior boundary are shifted into the process to the right of it - shifted = grid.copy() - if left != 0.0: - shifted[shifted == left] += 1e-8 - if right != 1.0: - shifted[shifted == right] += 1e-8 - - inds = xp.nonzero(xp.logical_and(shifted >= left, shifted <= right))[0] - assert inds.size > 0, f"Rank {rank} has no evaluation point in direction {n + 1}." - assert inds.size == inds[-1] - inds[0] + 1, "Evaluation points of a rank must be contiguous." - - slices += [slice(int(inds[0]), int(inds[-1]) + 1)] - - grid_slices += [tuple(slices)] - - # the local grids must tile the global evaluation grid exactly - n_points = sum( - (sl[0].stop - sl[0].start) * (sl[1].stop - sl[1].start) * (sl[2].stop - sl[2].start) for sl in grid_slices - ) - assert n_points == grids_log[0].size * grids_log[1].size * grids_log[2].size, ( - "The MPI domains do not tile the evaluation grid exactly." - ) - - return grids_log, grid_slices - - def _collect_on_root(self, loc_val: xp.ndarray, grid_slices: list, glob_shape: tuple): - """Assemble the local parts of an evaluation-grid array on rank 0. - - Only rank 0 allocates an array of the size of the global evaluation grid; - all other ranks just send the points they own. - - Parameters - ---------- - loc_val : xp.ndarray - Values on the evaluation points owned by this rank. - grid_slices : list - Slices of the global grid owned by each rank, see :meth:`_create_eval_grids`. - glob_shape : tuple - Number of points of the global evaluation grid in each direction. - - Returns - ------- - xp.ndarray or None - The global array on rank 0, None on all other ranks. - """ - if not self.parallel_pproc: - return loc_val - - if self.rank == 0: - glob_val = xp.empty(glob_shape, dtype=loc_val.dtype) - glob_val[grid_slices[0]] = loc_val - - # cache receive buffers to avoid repeated allocations in tight loops - if not hasattr(self, "_collect_recv_bufs"): - self._collect_recv_bufs = {} - - for rank in range(1, len(grid_slices)): - sl = grid_slices[rank] - shape = tuple(sl_i.stop - sl_i.start for sl_i in sl) - buf = self._collect_recv_bufs.get((rank, shape, loc_val.dtype)) - if buf is None: - buf = xp.empty(shape, dtype=loc_val.dtype) - self._collect_recv_bufs[(rank, shape, loc_val.dtype)] = buf - self.comm.Recv(buf, source=rank, tag=rank) - glob_val[sl] = buf - - return glob_val - - else: - self.comm.Send(xp.ascontiguousarray(loc_val), dest=0, tag=self.rank) - return None - - def _eval_femfields( - self, - fields: dict, - grids_log_loc: list, - grid_slices: list, - glob_shape: tuple, - *, - physical: bool = False, - ): - """Evaluate the spline fields of one snapshot on the evaluation grid. - - Each rank evaluates only the grid points of its own MPI domain, the values are - then collected on rank 0. - - Parameters - ---------- - fields : dict - Nested dictionary species -> var -> ``SplineFunction`` holding the coefficients - of one snapshot, see :meth:`_load_femfields`. - grids_log_loc : list - The three logical 1d grids restricted to the domain of this rank. - grid_slices : list - Slices of the global grid owned by each rank, see :meth:`_create_eval_grids`. - glob_shape : tuple - Number of points of the global evaluation grid in each direction. - physical : bool, optional - If True, also compute the push-forwarded physical (x,y,z) components. - - Returns - ------- - vals, vals_phy : dict - Nested dictionaries species -> var -> list of arrays (one entry for scalar-valued - and three entries for vector-valued spaces). The arrays are only assembled on - rank 0, the lists stay empty on all other ranks. ``vals_phy`` holds empty lists - if ``physical`` is False. - """ - vals = {} - vals_phy = {} - for species, vars in fields.items(): - vals[species] = {} - vals_phy[species] = {} - for name, field in vars.items(): - assert isinstance(field, SplineFunction) - - vals[species][name] = [] - vals_phy[species][name] = [] - - # evaluate the field on the grid points of this rank only - loc_val = field(*grids_log_loc, local=True) - - if physical: - # push-forward - loc_val_phy = self.domain.push( - loc_val, - *grids_log_loc, - kind=PUSH_KINDS[field.space_id], - ) - - # scalar spaces - if isinstance(loc_val, xp.ndarray): - comps = [loc_val] - comps_phy = [loc_val_phy] if physical else [] - # vector-valued spaces - else: - comps = [loc_val[j] for j in range(3)] - comps_phy = [loc_val_phy[j] for j in range(3)] if physical else [] - - # collect the values of all ranks on rank 0 - for comp in comps: - glob_val = self._collect_on_root(comp, grid_slices, glob_shape) - if self.rank == 0: - vals[species][name] += [glob_val] - - for comp in comps_phy: - glob_val = self._collect_on_root(comp, grid_slices, glob_shape) - if self.rank == 0: - vals_phy[species][name] += [glob_val] - - return vals, vals_phy - - def _create_vtk( - self, - path: str, - t_grid: xp.ndarray, - grids_phy: list, - point_data: dict, - *, - physical: bool = False, - ): - """Write evaluated field arrays to VTK (.vts) files for visualization. - - Parameters - ---------- - path : str - Directory where species subfolders and their `vtk` folders will be created. - t_grid : xp.ndarray - Time grid corresponding to entries in ``point_data``. - grids_phy : list - Physical coordinate arrays returned by :meth:`_eval_femfields`. - point_data : dict - Evaluated field values as returned by :meth:`_eval_femfields`. - physical : bool, optional - If True, writes files for push-forwarded physical components (folder suffix "_phy"). - """ - for species, vars in point_data.items(): - species_path = os.path.join(path, species, "vtk" + physical * "_phy") - if os.path.exists(species_path): - shutil.rmtree(species_path) - os.makedirs(species_path) - - # time loop - nt = max(len(t_grid) - 1, 1) - log_nt = int(xp.log10(nt)) + 1 - - logger.warning(f"\nCreating vtk in {path} ...") - for n, t in enumerate(tqdm(t_grid)): - point_data_n = {} - - for species, vars in point_data.items(): - species_path = os.path.join(path, species, "vtk" + physical * "_phy") - point_data_n[species] = {} - for name, data in vars.items(): - points_list = data[t] - - # scalar - if len(points_list) == 1: - point_data_n[species][name] = points_list[0] - - # vectorpoint_data[name] - else: - for j in range(3): - point_data_n[species][name + f"_{j + 1}"] = points_list[j] - - gridToVTK( - os.path.join(species_path, "step_{0:0{1}d}".format(n, log_nt)), - *grids_phy, - pointData=point_data_n[species], - ) - - def _post_process_markers( - self, - path_kinetic_species: str, - step: int = 1, - ): - """Compute Cartesian marker positions and write them to .npy and .txt files. - - For each saved time step this function collects marker datasets from all MPI ranks, - reconstructs full marker arrays (positions, velocities, weights, ids), maps logical - coordinates to physical coordinates via ``self.domain`` and writes per-step - ``.npy`` (binary) and ``.txt`` (ASCII) files suitable for quick inspection or - import into visualization tools. - - Parameters - ---------- - path_kinetic_species : str - Path to the per-species kinetic output directory where results will be written. - step : int, optional - Time-step stride to process (default 1). - """ - - species = path_kinetic_species.split("/")[-1] - species_obj: ParticleSpecies = self.model.particle_species[species] - - # open hdf5 files and get names and number of saved markers of kinetic species - with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: - # get number of time steps and markers - nt, n_markers, n_cols = file_0["kinetic/" + species + "/markers"].shape - - # get velocity dimension from one of the variables of the species - for _, var in species_obj.variables.items(): - assert isinstance(var, PICVariable | SPHVariable) - cls: Particles = var.particles_class - vdim = cls.vdim - break - - log_nt = int(xp.log10(int(((nt - 1) / step)))) + 1 - - # directory for .txt files and marker index which will be saved - path_orbits = os.path.join(path_kinetic_species, "orbits") - - if vdim == 2: - save_index = list(range(0, 6)) + [10] + [-1] - elif vdim == 3: - save_index = list(range(0, 7)) + [-1] - else: - save_index = list(range(0, 4)) + [-1] - - if self.rank == 0: - try: - os.mkdir(path_orbits) - except: - shutil.rmtree(path_orbits) - os.mkdir(path_orbits) - self.comm.Barrier() - - # temporary array, plus every step of it for the product store - temp = xp.empty((n_markers, len(save_index)), order="C") - orbits = [] - lost_particles_mask = xp.empty(n_markers, dtype=bool) - - logger.warning(f"Evaluation of {n_markers} marker orbits for {species}") - - # loop over time grid - for n in tqdm(range(int((nt - 1) / step) + 1)): - # clear buffer - temp[:, :] = 0.0 - - # create text file for this time step and this species - file_npy = os.path.join( - path_orbits, - species + "_{0:0{1}d}.npy".format(n, log_nt), - ) - file_txt = os.path.join( - path_orbits, - species + "_{0:0{1}d}.txt".format(n, log_nt), - ) - - for rank in self.range_ranks: - with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: - markers = file["kinetic/" + species + "/markers"] - ids = markers[n * step, :, -1].astype("int") - ids = ids[ids != -1] # exclude holes - temp[ids] = markers[n * step, : ids.size, save_index] - - if self.parallel_pproc: - if self.rank == 0: - self.comm.Reduce(MPI.IN_PLACE, temp, op=MPI.SUM, root=0) - else: - self.comm.Reduce(temp, None, op=MPI.SUM, root=0) - - # sorting out lost particles - ids = temp[:, -1].astype("int") - ids_lost_particles = xp.setdiff1d(xp.arange(n_markers), ids) - ids_removed_particles = xp.nonzero(temp[:, 0] == -1.0)[0] - ids_lost_particles = xp.array(list(set(ids_lost_particles) | set(ids_removed_particles)), dtype=int) - lost_particles_mask[:] = False - lost_particles_mask[ids_lost_particles] = True - - if len(ids_lost_particles) > 0: - # lost markers are saved as [0, ..., 0, ids] - temp[lost_particles_mask, -1] = ids_lost_particles - ids = xp.unique(xp.append(ids, ids_lost_particles)) - - assert xp.all(sorted(ids) == xp.arange(n_markers)) - - # compute physical positions (x, y, z) - pos_phys = self.domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) - temp[~lost_particles_mask, :3] = pos_phys - - if self.rank == 0: - orbits.append(temp.copy()) - # save numpy - xp.save(file_npy, temp) - # move ids to first column and save txt - temp = xp.roll(temp, 1, axis=1) - xp.savetxt(file_txt, temp[:, (0, 1, 2, 3, -1)], fmt="%12.6f", delimiter=", ") - self.comm.Barrier() - - if self.rank == 0: - values = wrap_orbits(xp.stack(orbits), self.t_grid[: len(orbits)]) - store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset({"orbits": values})) - - def _post_process_f( - self, - path_kinetic_species, - step=1, - compute_bckgr=False, - ): - """Assemble and save distribution functions from per-rank binned data. - - This reads the binned full-f and delta-f arrays produced by the simulation across - MPI ranks, sums them to global arrays, and stores the results under - ``/distribution_function/``. When ``compute_bckgr`` is - True, an analytic kinetic background is evaluated on the same grids and added. - - Parameters - ---------- - path_kinetic_species : str - Path to the per-species kinetic output directory. - step : int, optional - Time-step stride to process (default 1). - compute_bckgr : bool, optional - If True, add the background stored by the simulation to the binned delta f. - """ - print(f"{self.rank} starting post-processing of distribution functions for {path_kinetic_species} ...") - - species = path_kinetic_species.split("/")[-1] - - logger.warning("Evaluation of distribution functions for " + str(species)) - - # the bin centers of every slice, as saved by the simulation - slice_grids = {} - with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: - for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - dims = [part for part in slice_name.split("_")] - centers = [grid[:] for _, grid in file_0["kinetic/" + species + "/f/" + slice_name].attrs.items()] - slice_grids[slice_name] = dict(zip(dims, centers)) - slice_names = list(slice_grids) - - # compute distribution function - for slice_name in tqdm(slice_names): - logger.info(f"Processing slice {slice_name} for species {species}") - grids = slice_grids[slice_name] - - for rank in self.range_ranks: - print(f"{rank = } ----------------------------") - with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: - if self.parallel_pproc: - data = file["kinetic/" + species + "/f/" + slice_name][::step] - data_df = file["kinetic/" + species + "/df/" + slice_name][::step] - else: - if rank == 0: - data = file["kinetic/" + species + "/f/" + slice_name][::step].copy() - data_df = file["kinetic/" + species + "/df/" + slice_name][::step].copy() - else: - data += file["kinetic/" + species + "/f/" + slice_name][::step] - data_df += file["kinetic/" + species + "/df/" + slice_name][::step] - - print(f"{self.rank =} with {xp.sum(data) =} and {xp.sum(data_df) =}") - - if self.parallel_pproc: - if self.rank == 0: - self.comm.Reduce( - MPI.IN_PLACE, - data, - op=MPI.SUM, - root=0, - ) - self.comm.Reduce( - MPI.IN_PLACE, - data_df, - op=MPI.SUM, - root=0, - ) - else: - self.comm.Reduce( - data, - None, - op=MPI.SUM, - root=0, - ) - self.comm.Reduce( - data_df, - None, - op=MPI.SUM, - root=0, - ) - - print(f"{self.rank =} with {xp.sum(data) =} and {xp.sum(data_df) =}") - - print(f"{self.rank =} done.") - if self.rank == 0: - full_f = data - if compute_bckgr: - # the background of a delta-f species is stored by the simulation on the bin centers - key_background = f"kinetic/{species}/f_background/{slice_name}" - with h5py.File(os.path.join(self.path_out, "data", "data_proc0.hdf5"), "r") as file: - if key_background not in file: - raise ValueError( - f"{key_background} is missing from the raw output; outputs of older versions " - "do not store the background of delta-f species." - ) - data_bckgr = file[key_background][()] - - # add extra axis for data_bckgr since data_df has axis for time series - full_f = data_df + data_bckgr[None] - - store.write_group( - store.store_path(self.path_pproc), - f"/{species}/{slice_name}", - self._binned_dataset(grids, {"f": full_f, "delta_f": data_df}), - ) - - def _binned_dataset(self, grids: dict, variables: dict) -> xr.Dataset: - """One binned product per variable, with time, bin centers and mapped coordinates.""" - dims = tuple(dim for dim in grids) - coords = {"t": self.t_grid, **grids} - coords.update(self._mapped_coords(grids)) - return xr.Dataset( - {name: wrap_binned_data(values, dims, coords, name=name) for name, values in variables.items()} - ) - - def _mapped_coords(self, grids: dict) -> dict: - """``X``, ``Y``, ``Z`` on the logical directions of ``grids``, when there are two or three.""" - logical = tuple(dim for dim in grids if dim in ("e1", "e2", "e3")) - if len(logical) not in (2, 3) or self.domain is None: - return {} - try: - if len(logical) == 2: - mesh = xp.meshgrid(*(xp.asarray(grids[dim]) for dim in logical), indexing="ij") - arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} - arguments.update(dict(zip(logical, mesh))) - mapped = self.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) - else: - mapped = self.domain(*(xp.asarray(grids[dim]) for dim in logical)) - except (TypeError, ValueError): - logger.debug("Could not map the coordinates of %s", logical, exc_info=True) - return {} - return {name: (logical, xp.asarray(grid)) for name, grid in zip(("X", "Y", "Z"), mapped)} - - def _post_process_n_sph( - self, - path_kinetic_species, - step=1, - ): - """Compute and save SPH density fields from per-rank outputs. - - Parameters - ---------- - path_kinetic_species : str - Path to the per-species kinetic output directory where results will be written. - step : int, optional - Time-step stride to process (default 1). - """ - species = path_kinetic_species.split("/")[-1] - - logger.warning("Evaluation of sph density for " + str(species)) - - # the evaluation points of every view, as saved by the simulation - view_grids = {} - with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: - for view in file_0["kinetic/" + species + "/n_sph"]: - attrs = file_0["kinetic/" + species + "/n_sph/" + view].attrs - view_grids[view] = {f"e{direction}": attrs["eta" + direction][:] for direction in ("1", "2", "3")} - views = list(view_grids) - - # compute sph density - for view in tqdm(views): - for rank in self.range_ranks: - with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: - if self.parallel_pproc: - data = file["kinetic/" + species + "/n_sph/" + view][::step] - else: - if rank == 0: - data = file["kinetic/" + species + "/n_sph/" + view][::step].copy() - else: - data += file["kinetic/" + species + "/n_sph/" + view][::step] - - if self.parallel_pproc: - if self.rank == 0: - self.comm.Reduce( - MPI.IN_PLACE, - data, - op=MPI.SUM, - root=0, - ) - else: - self.comm.Reduce( - data, - None, - op=MPI.SUM, - root=0, - ) - - if self.rank == 0: - store.write_group( - store.store_path(self.path_pproc), - f"/{species}/{view}", - self._binned_dataset(view_grids[view], {"n": data}), - ) diff --git a/src/struphy/post_processing/tests/test_eval_grids.py b/src/struphy/post_processing/tests/test_eval_grids.py index 7221d792f..a3a49ba3d 100644 --- a/src/struphy/post_processing/tests/test_eval_grids.py +++ b/src/struphy/post_processing/tests/test_eval_grids.py @@ -11,16 +11,16 @@ import numpy as np import pytest -from struphy.post_processing.post_processing_tools import PostProcessor +from struphy.post_processing.output import Output def make_pproc(num_elements, domain_array): - """A PostProcessor stub that only knows about its Derham decomposition. + """A Output stub that only knows about its Derham decomposition. ``__init__`` is bypassed on purpose (it creates output folders and reads meta.yml). """ - pproc = PostProcessor.__new__(PostProcessor) - pproc.derham = SimpleNamespace( + pproc = Output.__new__(Output) + pproc._pproc_derham = SimpleNamespace( num_elements=num_elements, domain_array=np.array(domain_array, dtype=float), ) @@ -225,7 +225,7 @@ def test_gather_reproduces_the_global_array(): def test_collect_on_root_is_a_no_op_in_serial(): """With ``parallel_pproc=False`` the local array is already the global one.""" pproc = make_pproc(NUM_ELEMENTS, DOM_ARR_1_RANK) - pproc.parallel_pproc = False + pproc._pproc_parallel = False grids_log, grid_slices = pproc._create_eval_grids() shape = tuple(grid.size for grid in grids_log) diff --git a/src/struphy/post_processing/tests/test_eval_grids_mpi.py b/src/struphy/post_processing/tests/test_eval_grids_mpi.py index 0e0e18599..74f469778 100644 --- a/src/struphy/post_processing/tests/test_eval_grids_mpi.py +++ b/src/struphy/post_processing/tests/test_eval_grids_mpi.py @@ -19,7 +19,7 @@ import pytest from feectools.ddm.mpi import mpi as MPI -from struphy.post_processing.post_processing_tools import PostProcessor +from struphy.post_processing.output import Output # divisible by 1, 2, 3 and 4, so eta1 can be split evenly over the usual rank counts NUM_ELEMENTS = (12, 2, 2) @@ -56,20 +56,20 @@ def split_eta1(num_elements, n_parts): def make_mpi_pproc(comm, num_elements=NUM_ELEMENTS): - """A PostProcessor stub in parallel mode, decomposed over ``comm``. + """An Output stub in parallel mode, decomposed over ``comm``. ``__init__`` is bypassed on purpose (it creates output folders and reads meta.yml); ``_create_eval_grids`` and ``_collect_on_root`` only need the attributes set here. """ - pproc = PostProcessor.__new__(PostProcessor) - pproc.derham = SimpleNamespace( + pproc = Output.__new__(Output) + pproc._pproc_derham = SimpleNamespace( num_elements=num_elements, domain_array=xp.array(split_eta1(num_elements, comm.Get_size()), dtype=float), ) - pproc.parallel_pproc = True - pproc.comm = comm - pproc.comm_size = comm.Get_size() - pproc.rank = comm.Get_rank() + pproc._pproc_parallel = True + pproc._pproc_comm = comm + pproc.mpi_ranks = comm.Get_size() + pproc._pproc_rank = comm.Get_rank() return pproc @@ -132,11 +132,11 @@ def test_collect_on_root_reproduces_the_global_array(celldivide): shape = tuple(grid.size for grid in grids_log) glob_val = global_array(shape) - loc_val = glob_val[grid_slices[pproc.rank]] + loc_val = glob_val[grid_slices[pproc._pproc_rank]] gathered = pproc._collect_on_root(loc_val, grid_slices, shape) - if pproc.rank == 0: + if pproc._pproc_rank == 0: assert gathered.shape == shape assert xp.array_equal(gathered, glob_val) else: @@ -160,12 +160,12 @@ def test_collect_on_root_handles_non_contiguous_local_arrays(): # mimic taking one component out of a (3, *shape) array laid out component-last stacked = xp.stack([glob_val, glob_val + 1000.0, glob_val + 2000.0], axis=-1) - loc_val = stacked[grid_slices[pproc.rank]][..., 0] + loc_val = stacked[grid_slices[pproc._pproc_rank]][..., 0] assert not loc_val.flags["C_CONTIGUOUS"] gathered = pproc._collect_on_root(loc_val, grid_slices, shape) - if pproc.rank == 0: + if pproc._pproc_rank == 0: assert xp.array_equal(gathered, glob_val) else: assert gathered is None @@ -186,14 +186,14 @@ def test_collect_on_root_repeated_calls_stay_correct(): for step in range(3): glob_val = global_array(shape, offset=1000 * step) - loc_val = glob_val[grid_slices[pproc.rank]] + loc_val = glob_val[grid_slices[pproc._pproc_rank]] gathered = pproc._collect_on_root(loc_val, grid_slices, shape) - if pproc.rank == 0: + if pproc._pproc_rank == 0: assert xp.array_equal(gathered, glob_val), f"wrong data gathered in step {step}" - if pproc.rank == 0: + if pproc._pproc_rank == 0: # buffers were cached, i.e. the reuse path above was actually taken assert len(pproc._collect_recv_bufs) == comm.Get_size() - 1 @@ -209,11 +209,11 @@ def test_collect_on_root_keeps_dtypes_apart(): for dtype in (float, complex, float): glob_val = global_array(shape, dtype=dtype) - loc_val = glob_val[grid_slices[pproc.rank]] + loc_val = glob_val[grid_slices[pproc._pproc_rank]] gathered = pproc._collect_on_root(loc_val, grid_slices, shape) - if pproc.rank == 0: + if pproc._pproc_rank == 0: assert gathered.dtype == glob_val.dtype assert xp.array_equal(gathered, glob_val) @@ -227,10 +227,10 @@ def test_only_root_allocates_the_global_array(): grids_log, grid_slices = pproc._create_eval_grids() shape = tuple(grid.size for grid in grids_log) - loc_val = global_array(shape)[grid_slices[pproc.rank]] + loc_val = global_array(shape)[grid_slices[pproc._pproc_rank]] gathered = pproc._collect_on_root(loc_val, grid_slices, shape) - is_root = pproc.rank == 0 + is_root = pproc._pproc_rank == 0 assert (gathered is not None) == is_root # the local block is strictly smaller than the global grid on at least one rank diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 6c7c14e62..8634fd4ab 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -15,7 +15,7 @@ from struphy.post_processing import store from struphy.post_processing.arrays import orbit_quantities from struphy.post_processing.output import Output, open_output -from struphy.post_processing.post_processing_tools import is_processed, normalize_options, source_fingerprint +from struphy.post_processing.manifest import is_processed, normalize_options, source_fingerprint NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 @@ -306,23 +306,14 @@ def test_manifest_is_stale_when_raw_output_changes(tmp_path): @pytest.mark.parametrize("rank", [0, 1]) def test_serial_process_runs_on_rank_zero_only(tmp_path, monkeypatch, rank): - from struphy.post_processing import post_processing_tools - calls = [] - - class FakePostProcessor: - def __init__(self, output, parallel_pproc=False): - calls.append(("construct", parallel_pproc)) - - def process(self, **options): - calls.append(("process", options)) - - monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) + monkeypatch.setattr(Output, "_setup_processing", lambda self, parallel: calls.append(("setup", parallel))) + monkeypatch.setattr(Output, "_process_raw", lambda self, **options: calls.append(("process", options))) comm = FakeComm(rank=rank, size=2) run = output_with_comm(monkeypatch, write_tree(str(tmp_path)), comm) assert run.process(physical=True) is run expected = [ - ("construct", False), + ("setup", False), ( "process", dict( @@ -335,18 +326,9 @@ def process(self, **options): def test_parallel_process_runs_on_every_rank(tmp_path, monkeypatch): - from struphy.post_processing import post_processing_tools - calls = [] - - class FakePostProcessor: - def __init__(self, output, parallel_pproc=False): - calls.append(parallel_pproc) - - def process(self, **options): - pass - - monkeypatch.setattr(post_processing_tools, "PostProcessor", FakePostProcessor) + monkeypatch.setattr(Output, "_setup_processing", lambda self, parallel: calls.append(parallel)) + monkeypatch.setattr(Output, "_process_raw", lambda self, **options: None) output_with_comm(monkeypatch, write_tree(str(tmp_path)), FakeComm(rank=3, size=4)).process(parallel=True) assert calls == [True] diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index ad7266872..b7ab51929 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1762,26 +1762,6 @@ def _serialize_initial_condition(value): # other values visible in provenance rather than making metadata writing fail. return value - def _collect_initial_conditions_metadata(self) -> dict: - """Collect initial-condition definitions for every model variable.""" - initial_conditions = {} - for species_name, species in self.model.species.items(): - variables = {} - for variable_name, variable in species.variables.items(): - entry = { - "backgrounds": self._serialize_initial_condition(variable.backgrounds), - "perturbations": self._serialize_initial_condition(variable.perturbations), - } - if isinstance(variable, PICVariable): - # ``initial_condition`` defaults to backgrounds. Do not access the - # property here, because doing so mutates the variable's state. - entry["initial_condition"] = self._serialize_initial_condition( - getattr(variable, "_initial_condition", variable.backgrounds) - ) - variables[variable_name] = entry - initial_conditions[species_name] = variables - return initial_conditions - @staticmethod def _deserialize_initial_condition(value, trust_source: bool): """Rebuild one initial-condition definition from metadata.""" @@ -1860,10 +1840,23 @@ def _deserialize_initial_condition(value, trust_source: bool): def _restore_initial_conditions(self, metadata: dict, trust_source: bool): """Attach metadata initial conditions to the reconstructed model variables.""" - version = metadata.get("initial_conditions_schema_version", 1) + version = metadata.get("model", {}).get( + "initial_conditions_schema_version", metadata.get("initial_conditions_schema_version", 1) + ) if version != 1: raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") - for species_name, variables in metadata.get("initial_conditions", {}).items(): + model_species = metadata.get("model", {}).get("species", {}) + definitions = metadata.get("initial_conditions") + if definitions is None: + definitions = { + species_name: { + name: variable["initial_conditions"] + for name, variable in species.get("variables", {}).items() + if "initial_conditions" in variable + } + for species_name, species in model_species.items() + } + for species_name, variables in definitions.items(): species = self.model.species.get(species_name) if species is None: continue @@ -1900,12 +1893,11 @@ def to_run_metadata(self, file_path: str = None, **extra_data) -> str: The JSON-encoded simulation metadata. """ config = self.to_dict() + config["model"] = self.model.to_dict(initial_condition_serializer=self._serialize_initial_condition) config.update( { "mpi_ranks": self.comm_size, "use_mpi_comm_world": self.comm is not None, - "initial_conditions_schema_version": 1, - "initial_conditions": self._collect_initial_conditions_metadata(), **extra_data, }, ) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 042c098ec..d383c567f 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -15,7 +15,7 @@ from struphy.ode.utils import ButcherTableau from struphy.particles.parameters import LoadingParameters from struphy.pic.accumulation.filter import FilterParameters -from struphy.post_processing.post_processing_tools import PostProcessor, is_processed +from struphy.post_processing.manifest import is_processed from struphy.initial.base import Perturbation @@ -95,7 +95,7 @@ def test_run_writes_only_metadata_and_copies_the_parameter_file(tmp_path): sim._copy_parameter_file() assert sorted(os.listdir(sim.env.path_out)) == ["parameters.py", "run_metadata.json"] metadata = json.loads((tmp_path / "sim_1" / "run_metadata.json").read_text()) - assert metadata["model"] == sim.model.to_dict() + assert metadata["model"] == sim.model.to_dict(initial_condition_serializer=sim._serialize_initial_condition) assert metadata["mpi_ranks"] == sim.comm_size assert metadata["started_at_epoch_s"] == sim.start_time @@ -113,10 +113,10 @@ def test_run_metadata_contains_variables_and_propagator_options(tmp_path): sim._write_run_metadata() metadata = json.loads((tmp_path / "sim_1" / "run_metadata.json").read_text()) - assert metadata["model"] == sim.model.to_dict() + assert metadata["model"] == sim.model.to_dict(initial_condition_serializer=sim._serialize_initial_condition) assert "species" not in metadata assert "propagator_options" not in metadata - assert metadata["model"]["species"]["em_fields"]["variables"]["e_field"] == { + assert {key: value for key, value in metadata["model"]["species"]["em_fields"]["variables"]["e_field"].items() if key != "initial_conditions"} == { "class": "FEECVariable", "space": "Hcurl", "save_data": False, @@ -146,15 +146,17 @@ def test_run_metadata_contains_serialized_initial_conditions(tmp_path): ) metadata = json.loads(sim.to_run_metadata()) - assert metadata["initial_conditions_schema_version"] == 1 - b_field = metadata["initial_conditions"]["em_fields"]["b_field"] + assert metadata["model"]["initial_conditions_schema_version"] == 1 + assert "initial_conditions_schema_version" not in metadata + assert "initial_conditions" not in metadata + b_field = metadata["model"]["species"]["em_fields"]["variables"]["b_field"]["initial_conditions"] assert b_field["backgrounds"] == { "type": "FieldsBackground", "params": {"type": "LogicalConst", "values": [1.0, 2.0, 3.0], "variable": None}, } assert b_field["perturbations"]["type"] == "TorusModesCos" - kinetic = json.loads(kinetic_sim.to_run_metadata())["initial_conditions"]["kinetic_ions"]["var"] + kinetic = json.loads(kinetic_sim.to_run_metadata())["model"]["species"]["kinetic_ions"]["variables"]["var"]["initial_conditions"] assert kinetic["backgrounds"]["type"] == "Maxwellian3D" assert kinetic["initial_condition"]["type"] == "SumKineticBackground" assert kinetic["initial_condition"]["params"]["f1"]["params"]["n"][1]["type"] == "TorusModesCos" @@ -164,7 +166,7 @@ def test_run_metadata_embeds_user_function_source(tmp_path): sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(user_density_profile, None))) - density = json.loads(sim.to_run_metadata())["initial_conditions"]["kinetic_ions"]["var"]["backgrounds"][ + density = json.loads(sim.to_run_metadata())["model"]["species"]["kinetic_ions"]["variables"]["var"]["initial_conditions"]["backgrounds"][ "params" ]["n"][0] assert density["type"] == "python_function" @@ -173,6 +175,28 @@ def test_run_metadata_embeds_user_function_source(tmp_path): assert len(density["source_sha256"]) == 64 +def test_legacy_initial_conditions_metadata_can_still_be_restored(tmp_path): + path_out = tmp_path / "sim_1" + path_out.mkdir() + sim = make_sim(tmp_path) + sim.model.em_fields.b_field.add_background(FieldsBackground(values=(1.0, 2.0, 3.0))) + metadata = json.loads(sim.to_run_metadata()) + metadata["initial_conditions_schema_version"] = metadata["model"].pop("initial_conditions_schema_version") + metadata["initial_conditions"] = { + species_name: { + variable_name: variable.pop("initial_conditions") + for variable_name, variable in species["variables"].items() + } + for species_name, species in metadata["model"]["species"].items() + } + (path_out / "run_metadata.json").write_text(json.dumps(metadata)) + + restored = Simulation.from_output(path_out) + assert restored.model.em_fields.b_field.backgrounds.values == (1.0, 2.0, 3.0) + output = Output(path_out) + assert output.initial_conditions["em_fields"]["b_field"]["backgrounds"].values == (1.0, 2.0, 3.0) + + def test_from_output_restores_initial_conditions_and_requires_trust_for_source(tmp_path, monkeypatch): path_out = tmp_path / "sim_1" path_out.mkdir() @@ -242,7 +266,7 @@ def test_run_metadata_names_variable_keys_in_propagator_options(tmp_path): sim.model.propagators.poisson.options.filter_params = {variable: FilterParameters("fourier_in_tor", (1, 2))} metadata = json.loads(sim.to_run_metadata()) - assert metadata["model"] == sim.model.to_dict() + assert metadata["model"] == sim.model.to_dict(initial_condition_serializer=sim._serialize_initial_condition) assert metadata["model"]["species"]["em_fields"]["variables"]["source"]["space"] == "H1" assert metadata["model"]["propagator_options"]["poisson"]["filter_params"] == { @@ -265,16 +289,17 @@ def test_cold_plasma_vlasov_species_and_variables_own_their_metadata(tmp_path): species = json.loads(sim.to_run_metadata())["model"]["species"] - assert species["thermal_elec"] == model.thermal_elec.to_dict() - assert species["hot_elec"] == model.hot_elec.to_dict() + assert species == model.to_dict(initial_condition_serializer=sim._serialize_initial_condition)["species"] assert species["thermal_elec"]["class"] == "ThermalElectrons" assert species["thermal_elec"]["charge_number"] == -2 assert species["thermal_elec"]["mass_number"] == 0.25 assert species["thermal_elec"]["alpha"] == 3.0 assert species["thermal_elec"]["epsilon"] == 0.5 - assert species["thermal_elec"]["variables"]["current"] == model.thermal_elec.current.to_dict() + assert species["thermal_elec"]["variables"]["current"]["initial_conditions"] == { + "backgrounds": None, "perturbations": None + } assert species["hot_elec"]["loading_params"]["Np"] == 1234 - assert species["hot_elec"]["variables"]["var"] == model.hot_elec.var.to_dict() + assert species["hot_elec"]["variables"]["var"]["initial_conditions"]["initial_condition"] is None assert species["hot_elec"]["variables"]["var"]["save_data"] is False @@ -293,7 +318,7 @@ def test_from_output_requires_a_configuration(tmp_path): @pytest.mark.parametrize("metadata_only", [False, True]) -def test_processor_from_moved_output(tmp_path, metadata_only): +def test_processing_from_moved_output(tmp_path, metadata_only): sim = make_sim(tmp_path, grid=None, derham_opts=None, time_opts=Time(dt=0.123)) os.makedirs(os.path.join(sim.env.path_out, "data")) if metadata_only: @@ -311,13 +336,12 @@ def test_processor_from_moved_output(tmp_path, metadata_only): sentinel = products / "existing.txt" sentinel.write_text("keep until processing") - processor = PostProcessor.from_output(moved) + processor = Output(moved) - assert processor.path_out == str(moved) + assert processor.path_out == moved assert processor.model.to_dict() == sim.model.to_dict() assert processor.domain == sim.domain - assert processor.comm_size == 3 - assert list(processor.range_ranks) == [0, 1, 2] + assert processor.mpi_ranks == 3 assert sentinel.read_text() == "keep until processing" assert Output(moved).time_opts.dt == 0.123 assert processor.process(create_vtk=False) From 1f07aa09fc505bc9a603706f1b7f8f4d24784c2e Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 12:58:16 +0200 Subject: [PATCH 102/193] Move time_units to process --- doc/sections/userguide.rst | 4 +-- src/struphy/post_processing/output.py | 34 +++++++++++-------- .../tests/test_derived_products.py | 4 +-- .../post_processing/tests/test_output.py | 16 ++++++--- .../tests/test_output_accessors.py | 2 +- tutorials/tutorial_post_processing.ipynb | 19 ++++++----- 6 files changed, 46 insertions(+), 33 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 8c7ffef85..15c9ddaf9 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -530,8 +530,8 @@ Every product is an :class:`xarray.DataArray` with named dimensions Arrays are read from disk only when accessed. Time is in Struphy units, in which the models' analytic results are written; seconds come -along as the coordinate ``t_seconds``. Pass ``time_units="physical"`` to -:class:`~struphy.Output` to make ``t`` itself seconds. +along as the coordinate ``t_seconds``. Use ``out.with_time_units("physical")`` +to make ``t`` itself seconds in an independent view. In a separate process, for example a plotting script on a laptop after a cluster run, open the output folder instead. Nothing is allocated and no MPI is needed. diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 5b820751d..a4789ec17 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -198,10 +198,6 @@ class Output: ---------- path_out: The simulation output folder, ``sim.env.path_out``. - time_units: - ``"normalized"`` (the default) keeps Struphy time units, in which the analytic - results of the models are expressed; every product then also carries seconds as the - coordinate ``t_seconds``. ``"physical"`` makes ``t`` itself seconds. trust_initial_condition_source: Allow reconstruction of Python functions and classes embedded in initial-condition metadata. Enable this only for output folders you trust. @@ -211,13 +207,10 @@ def __init__( self, path_out, *, - time_units: str = "normalized", trust_initial_condition_source: bool = False, ): - if time_units not in {"physical", "normalized"}: - raise ValueError("time_units must be 'physical' or 'normalized'") self.path_out = Path(path_out).resolve() - self.time_units = time_units + self._time_units = "normalized" self.trust_initial_condition_source = trust_initial_condition_source self.comm = mpi_comm_world() self._reset() @@ -231,13 +224,25 @@ def __init__( def __repr__(self): return f"{type(self).__name__}({str(self.path_out)!r}, processed={self.is_processed})" + @property + def time_units(self) -> str: + """Time coordinates returned by this view: ``normalized`` or ``physical``.""" + return self._time_units + def with_time_units(self, time_units: str) -> "Output": - """The same output with time coordinates in ``"physical"`` or ``"normalized"`` units.""" - return type(self)( + """Open an independent view with ``t`` in normalized units or seconds. + + Normalized arrays also carry a ``t_seconds`` coordinate. This choice changes + only data returned by the view; saved products remain in normalized units. + """ + if time_units not in {"physical", "normalized"}: + raise ValueError("time_units must be 'physical' or 'normalized'") + view = type(self)( self.path_out, - time_units=time_units, trust_initial_condition_source=self.trust_initial_condition_source, ) + view._time_units = time_units + return view def clear_cache(self): """Close lazy product files and discard loaded arrays while retaining metadata.""" @@ -2071,7 +2076,7 @@ def info(self) -> None: "- Use out.initial_conditions for reconstructed backgrounds, perturbations, and distributions.", "- Use Output(path, trust_initial_condition_source=True) only for trusted runs with embedded Python source.", "- Use out.keys(), out.fields, out.distributions, out.densities, and out.orbits to discover products.", - "- Use out.evaluate(key), out.process(...), and array.struphy.plot.* to load and plot products.", + "- Use out.evaluate(key), out.pproc(...), and array.struphy.plot.* to load and plot products.", "", f"{'Key':<{key_width}} Description", f"{'-' * key_width} -----------", @@ -2311,7 +2316,7 @@ def _load(self, group: str, name: str) -> xr.DataArray: return array -def open_output(path_out, *, time_units: str = "normalized") -> Output: +def open_output(path_out) -> Output: """Open the output folder of a finished simulation. Saved metadata is read immediately; products are materialized and opened on demand. @@ -2320,9 +2325,8 @@ def open_output(path_out, *, time_units: str = "normalized") -> Output: ---------- path_out: The simulation output folder (``sim.env.path_out`` of the run). - ``"normalized"`` (the default) or ``"physical"`` (seconds) time coordinates. """ path = Path(path_out) if not (path / "data").is_dir(): raise FileNotFoundError(f"{path.resolve()} is not a Struphy output folder (it has no data/ directory)") - return Output(path, time_units=time_units) + return Output(path) diff --git a/src/struphy/post_processing/tests/test_derived_products.py b/src/struphy/post_processing/tests/test_derived_products.py index 80fc2dc82..c872c5349 100644 --- a/src/struphy/post_processing/tests/test_derived_products.py +++ b/src/struphy/post_processing/tests/test_derived_products.py @@ -25,7 +25,7 @@ def binned(values, dims, coords, name="f"): @pytest.fixture def run(tmp_path): - return Output(write_tree(str(tmp_path)), time_units="normalized") + return Output(write_tree(str(tmp_path))) # --- reductions --------------------------------------------------------------------------------- @@ -204,7 +204,7 @@ def test_unknown_units_are_rejected(run): def test_physical_time_units_are_not_converted_twice(tmp_path): - physical = Output(write_tree(str(tmp_path)), time_units="physical") + physical = Output(write_tree(str(tmp_path))).with_time_units("physical") np.testing.assert_allclose(physical.to_si(F).t, physical[F].t) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 8634fd4ab..530989d1b 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -129,7 +129,7 @@ def output_with_comm(monkeypatch, path, comm, **kwargs): @pytest.fixture def run(tmp_path): - return Output(write_tree(str(tmp_path)), time_units="normalized") + return Output(write_tree(str(tmp_path))) def test_products_are_discovered_without_loading_arrays(run): @@ -237,7 +237,7 @@ def forbidden(*args, **kwargs): def test_evaluate_triggers_default_processing_when_missing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = Output(root, time_units="normalized") + run = Output(root) calls = [] def fake_pproc(self, **options): @@ -336,7 +336,7 @@ def test_parallel_process_runs_on_every_rank(tmp_path, monkeypatch): def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = Output(root, time_units="normalized") + run = Output(root) calls = [] monkeypatch.setattr(Output, "process", lambda self, **options: calls.append(options)) @@ -375,9 +375,17 @@ def test_normalized_time_carries_seconds_as_a_coordinate(run): np.testing.assert_allclose(energy.t_seconds, energy.t * float(run.model.units.t)) assert energy.t_seconds.attrs["units"] == "s" - seconds = Output(run.path_out, time_units="physical").scalars.en_tot + physical = run.with_time_units("physical") + assert physical is not run + assert run.time_units == "normalized" + assert physical.time_units == "physical" + seconds = physical.scalars.en_tot np.testing.assert_allclose(seconds.t, energy.t * float(run.model.units.t)) assert "t_seconds" not in seconds.coords + assert "t_seconds" in run.scalars.en_tot.coords + + with pytest.raises(ValueError, match="time_units"): + run.with_time_units("hours") def test_a_failing_property_reports_its_own_error(tmp_path): diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index c0274d745..05f1ea99d 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -25,7 +25,7 @@ def make_run(root, name="sim_1"): time = np.asarray(file["time/value"]) file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) write_manifest(path) - return Output(path, time_units="normalized") + return Output(path) @pytest.fixture diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index ea2ab9669..38364fa32 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -135,11 +135,11 @@ "source": [ "## Process and load the output\n", "\n", - "`sim.run()` returns the run's output as a `Output`, which is also available later as `sim.output`. Scalars are read directly from the raw output; fields and particle products need post-processing, which runs with default options the first time they are accessed.\n", + "`sim.run()` returns an `Output`, which is also available later as `sim.output`. Scalars are read directly from the raw output; fields and particle products need post-processing, which runs with default options the first time they are accessed.\n", "\n", - "To choose options, call `out.process()` first. It evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", + "To choose options, call `out.pproc()` first. `Output` evaluates saved FEEC fields and organizes particle diagnostics; `physical=True` additionally creates physical field components. Existing products made with the same options are reused, so re-running a cell is cheap.\n", "\n", - "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `struphy.Output(path, time_units=\"physical\")` makes `t` itself seconds. Arrays are loaded only when accessed. The saved configuration is available through `out.domain`, `out.model`, and `out.time_opts`; no simulation object is created." + "Individual products are standard `xarray.DataArray` objects with named dimensions, coordinates, units, and labels. Time is in Struphy units, in which the models' analytic results are written; seconds come along as the coordinate `t_seconds`, and `out.with_time_units(\"physical\")` gives an independent view with `t` in seconds. Arrays are loaded only when accessed. The saved configuration is available through `out.domain`, `out.model`, and `out.time_opts`; no simulation object is created." ] }, { @@ -149,7 +149,7 @@ "metadata": {}, "outputs": [], "source": [ - "out.process(physical=True)" + "out.pproc(physical=True)" ] }, { @@ -187,7 +187,7 @@ "source": [ "### Reconstructed setup and initial conditions\n", "\n", - "`out.info()` now includes the saved model parameters, species variables, propagator options, initial-condition summary, and a short API guide. The structured equivalents remain available through `out.model`, `out.metadata`, and `out.initial_conditions`; all are rebuilt from metadata without creating a `Simulation`. This demonstration run uses only built-in definitions. For a run with saved user Python functions or classes, reopen it with `Output(path, trust_initial_condition_source=True)` only when the output is trusted." + "`out.info()` prints the saved model parameters, species variables, propagator options, initial-condition summary, and a short API guide. In `out.metadata`, serialized initial conditions live on each variable under `model → species → variables → initial_conditions`. `out.initial_conditions` gives the reconstructed objects; `out.model` restores them onto the model variables. This demonstration run uses only built-in definitions. For a run with saved user Python functions or classes, reopen it with `Output(path, trust_initial_condition_source=True)` only when the output is trusted." ] }, { @@ -203,8 +203,9 @@ "for name, options in out.metadata[\"model\"][\"propagator_options\"].items():\n", " print(f\" {name}: {options}\")\n", "\n", + "saved = out.metadata[\"model\"][\"species\"][\"kinetic_ions\"][\"variables\"][\"var\"][\"initial_conditions\"]\n", + "print(\"Initial-condition entries:\", tuple(saved))\n", "initial = out.initial_conditions[\"kinetic_ions\"][\"var\"]\n", - "print(\"Initial-condition entries:\", tuple(initial))\n", "print(\"Background distribution:\", initial[\"backgrounds\"])\n", "print(\"Initial distribution:\", initial[\"initial_condition\"])" ] @@ -856,7 +857,7 @@ " derham_opts=DerhamOptions(degree=(2, 2, 1), bcs=((\"dirichlet\", \"dirichlet\"), None, None)),\n", ")\n", "out_coaxial = coaxial.run()\n", - "out_coaxial.process(physical=True)\n", + "out_coaxial.pproc(physical=True)\n", "\n", "print(\"fields:\", tuple(out_coaxial.field_catalog))\n", "print(\"dimensions:\", out_coaxial.em_fields.b_field_xyz.dims)" @@ -913,7 +914,7 @@ "```python\n", "import struphy\n", "\n", - "out = struphy.Output(\"/path/to/sim_1\").process(physical=True)\n", + "out = struphy.Output(\"/path/to/sim_1\").pproc(physical=True)\n", "out.domain, out.model.units # reconstructed directly from saved metadata\n", "```\n", "\n", @@ -923,7 +924,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.3.final.0)", + "display_name": ".venv (3.12.3)", "language": "python", "name": "python3" }, From 6ee4183687f6c38fa89b5ee32372dbf4a932cb3e Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 13:23:26 +0200 Subject: [PATCH 103/193] Remove the trust flag --- src/struphy/post_processing/output.py | 56 +++++++++------------ src/struphy/simulation/sim.py | 51 ++++++++----------- src/struphy/simulation/tests/test_output.py | 17 +++---- tutorials/tutorial_post_processing.ipynb | 2 +- 4 files changed, 54 insertions(+), 72 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index a4789ec17..b58eb4a43 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -198,20 +198,11 @@ class Output: ---------- path_out: The simulation output folder, ``sim.env.path_out``. - trust_initial_condition_source: - Allow reconstruction of Python functions and classes embedded in initial-condition - metadata. Enable this only for output folders you trust. """ - def __init__( - self, - path_out, - *, - trust_initial_condition_source: bool = False, - ): + def __init__(self, path_out): self.path_out = Path(path_out).resolve() self._time_units = "normalized" - self.trust_initial_condition_source = trust_initial_condition_source self.comm = mpi_comm_world() self._reset() # A Simulation can expose its Output before it has written metadata. In that case, @@ -237,10 +228,7 @@ def with_time_units(self, time_units: str) -> "Output": """ if time_units not in {"physical", "normalized"}: raise ValueError("time_units must be 'physical' or 'normalized'") - view = type(self)( - self.path_out, - trust_initial_condition_source=self.trust_initial_condition_source, - ) + view = type(self)(self.path_out) view._time_units = time_units return view @@ -612,9 +600,11 @@ def model(self): variable = species.variables.get(variable_name) if variable is None: continue - variable._backgrounds = definition["backgrounds"] - variable._perturbations = definition["perturbations"] - if isinstance(variable, PICVariable): + if "backgrounds" in definition: + variable._backgrounds = definition["backgrounds"] + if "perturbations" in definition: + variable._perturbations = definition["perturbations"] + if isinstance(variable, PICVariable) and "initial_condition" in definition: variable._initial_condition = definition["initial_condition"] return model @@ -622,9 +612,11 @@ def model(self): def initial_conditions(self) -> dict: """Initial-condition definitions reconstructed from run metadata. - This reconstructs backgrounds, perturbations, kinetic distributions, and - supported inline Python functions/classes without creating a - :class:`~struphy.simulation.sim.Simulation` instance. + This reconstructs backgrounds, perturbations, distributions, and saved + Python functions/classes without creating a + :class:`~struphy.simulation.sim.Simulation` instance. Definitions that + cannot be deserialized are omitted; their serialized form remains in + :attr:`metadata`. """ from struphy.simulation.sim import Simulation @@ -633,16 +625,18 @@ def initial_conditions(self) -> dict: ) if version != 1: raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") - return { - species_name: { - variable_name: { - key: Simulation._deserialize_initial_condition(value, self.trust_initial_condition_source) - for key, value in definition.items() - } - for variable_name, definition in variables.items() - } - for species_name, variables in self._initial_condition_metadata().items() - } + result = {} + for species_name, variables in self._initial_condition_metadata().items(): + result[species_name] = {} + for variable_name, definition in variables.items(): + restored = {} + for key, value in definition.items(): + try: + restored[key] = Simulation._deserialize_initial_condition(value) + except ValueError as error: + logger.warning("Skipping %s.%s %s: %s", species_name, variable_name, key, error) + result[species_name][variable_name] = restored + return result def _initial_condition_metadata(self) -> dict: """Read variable definitions, including the layout of older output folders.""" @@ -2074,7 +2068,7 @@ def info(self) -> None: "----", "- Use out.model for the reconstructed model and its variables.", "- Use out.initial_conditions for reconstructed backgrounds, perturbations, and distributions.", - "- Use Output(path, trust_initial_condition_source=True) only for trusted runs with embedded Python source.", + "- Saved Python initial conditions are reconstructed from source; unsupported definitions remain in out.metadata.", "- Use out.keys(), out.fields, out.distributions, out.densities, and out.orbits to discover products.", "- Use out.evaluate(key), out.pproc(...), and array.struphy.plot.* to load and plot products.", "", diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index b7ab51929..2a001e0ab 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1763,26 +1763,21 @@ def _serialize_initial_condition(value): return value @staticmethod - def _deserialize_initial_condition(value, trust_source: bool): + def _deserialize_initial_condition(value): """Rebuild one initial-condition definition from metadata.""" if value is None or isinstance(value, (bool, int, float, str)): return value if isinstance(value, list): - return tuple(Simulation._deserialize_initial_condition(item, trust_source) for item in value) + return tuple(Simulation._deserialize_initial_condition(item) for item in value) if not isinstance(value, dict) or "type" not in value or ( "params" not in value and value["type"] not in {"python_function", "python_class", "callable"} ): - return {key: Simulation._deserialize_initial_condition(item, trust_source) for key, item in value.items()} + return {key: Simulation._deserialize_initial_condition(item) for key, item in value.items()} kind = value["type"] if kind == "python_function": if value.get("serialization") == "unsupported": raise ValueError(f"Cannot restore initial-condition function: {value['reason']}.") - if not trust_source: - raise ValueError( - "Initial-condition metadata contains Python source. Pass trust_initial_condition_source=True " - "to Simulation.from_output() only for trusted output." - ) source = value["source"] if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: raise ValueError("Initial-condition function source hash does not match its metadata.") @@ -1790,16 +1785,11 @@ def _deserialize_initial_condition(value, trust_source: bool): import cunumpy as xp namespace = {"np": np, "numpy": np, "xp": xp, "cp": xp, "cupy": xp} - exec(source, namespace) # noqa: S102 -- explicitly gated by trust_source + exec(source, namespace) # noqa: S102 -- reconstruct saved Python function return namespace[value["name"]] if kind == "python_class": if value.get("serialization") == "unsupported": raise ValueError(f"Cannot restore initial-condition class: {value['reason']}.") - if not trust_source: - raise ValueError( - "Initial-condition metadata contains Python source. Pass trust_initial_condition_source=True " - "to Simulation.from_output() only for trusted output." - ) source = value["source"] if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: raise ValueError("Initial-condition class source hash does not match its metadata.") @@ -1815,30 +1805,30 @@ def _deserialize_initial_condition(value, trust_source: bool): "Perturbation": Perturbation, "dataclass": dataclasses.dataclass, } - exec(source, namespace) # noqa: S102 -- explicitly gated by trust_source + exec(source, namespace) # noqa: S102 -- reconstruct saved Python class initial_condition_class = namespace[value["name"]] if "params" in value: - return initial_condition_class(**Simulation._deserialize_initial_condition(value["params"], trust_source)) + return initial_condition_class(**Simulation._deserialize_initial_condition(value["params"])) initial_condition = initial_condition_class.__new__(initial_condition_class) - initial_condition.__dict__.update(Simulation._deserialize_initial_condition(value["state"], trust_source)) + initial_condition.__dict__.update(Simulation._deserialize_initial_condition(value["state"])) return initial_condition if kind == "callable": raise ValueError(f"Cannot restore initial-condition callable: {value['reason']}.") if kind == "FieldsBackground": - return FieldsBackground(**Simulation._deserialize_initial_condition(value["params"], trust_source)) + return FieldsBackground(**Simulation._deserialize_initial_condition(value["params"])) from struphy.initial import perturbations from struphy.kinetic_background import maxwellians from struphy.kinetic_background import base as kinetic_background_base - params = Simulation._deserialize_initial_condition(value["params"], trust_source) + params = Simulation._deserialize_initial_condition(value["params"]) for module in (equils, perturbations, maxwellians, kinetic_background_base): initial_condition_class = getattr(module, kind, None) if initial_condition_class is not None: return initial_condition_class(**params) raise ValueError(f"Unknown initial-condition type '{kind}'.") - def _restore_initial_conditions(self, metadata: dict, trust_source: bool): + def _restore_initial_conditions(self, metadata: dict): """Attach metadata initial conditions to the reconstructed model variables.""" version = metadata.get("model", {}).get( "initial_conditions_schema_version", metadata.get("initial_conditions_schema_version", 1) @@ -1864,10 +1854,10 @@ def _restore_initial_conditions(self, metadata: dict, trust_source: bool): variable = species.variables.get(variable_name) if variable is None: continue - variable._backgrounds = self._deserialize_initial_condition(entry["backgrounds"], trust_source) - variable._perturbations = self._deserialize_initial_condition(entry["perturbations"], trust_source) + variable._backgrounds = self._deserialize_initial_condition(entry["backgrounds"]) + variable._perturbations = self._deserialize_initial_condition(entry["perturbations"]) if isinstance(variable, PICVariable): - variable._initial_condition = self._deserialize_initial_condition(entry["initial_condition"], trust_source) + variable._initial_condition = self._deserialize_initial_condition(entry["initial_condition"]) def to_run_metadata(self, file_path: str = None, **extra_data) -> str: """Snapshot of the reconstructible config (see :meth:`to_dict`) plus run-specific, @@ -1933,11 +1923,11 @@ def from_dict(cls, dct) -> "Simulation": ) @classmethod - def from_file(cls, file_path: str, trust_initial_condition_source: bool = False) -> "SimulationBase": + def from_file(cls, file_path: str) -> "SimulationBase": """Deserialize a simulation configuration from a YAML or JSON file. Initial conditions in run metadata are restored when present. Embedded - Python functions and classes require ``trust_initial_condition_source=True``. + Python functions and classes are reconstructed from their saved source. """ file_path = os.fspath(file_path) if file_path.endswith(".yaml") or file_path.endswith(".yml"): @@ -1967,20 +1957,19 @@ def convert_lists_to_tuples(obj): # Convert lists to tuples for relevant keys dct = convert_lists_to_tuples(dct) sim = cls.from_dict(dct) - sim._restore_initial_conditions(metadata, trust_initial_condition_source) + sim._restore_initial_conditions(metadata) return sim @classmethod - def from_output(cls, path_out: str, trust_initial_condition_source: bool = False) -> "Simulation": + def from_output(cls, path_out: str) -> "Simulation": """Restore the simulation that wrote the output folder ``path_out``. The configuration is read from the ``run_metadata.json`` written by :meth:`run`, falling back to legacy ``config.json`` if absent; a copied parameter file is never executed. The metadata holds the options objects and the arguments of the model (and thus its units), which is all that post-processing and - plotting need. Initial conditions are restored when present; embedded Python - functions additionally require ``trust_initial_condition_source=True`` because - restoration executes their saved source. + plotting need. Initial conditions are restored when present, including + embedded Python functions and classes from saved source. Nothing is allocated, and ``env`` points at ``path_out`` even if the folder was moved. """ path_out = os.path.abspath(path_out) @@ -1992,7 +1981,7 @@ def from_output(cls, path_out: str, trust_initial_condition_source: bool = False f"Neither config.json nor run_metadata.json exists in {path_out}; is it a Struphy output folder? Outputs of older " "versions can get one with sim.to_run_metadata(os.path.join(path_out, 'run_metadata.json')) from their parameter file." ) - sim = cls.from_file(config_path, trust_initial_condition_source=trust_initial_condition_source) + sim = cls.from_file(config_path) sim.env = dataclasses.replace( sim.env, out_folders=os.path.dirname(path_out), sim_folder=os.path.basename(path_out) ) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index d383c567f..1442f08ea 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -197,31 +197,30 @@ def test_legacy_initial_conditions_metadata_can_still_be_restored(tmp_path): assert output.initial_conditions["em_fields"]["b_field"]["backgrounds"].values == (1.0, 2.0, 3.0) -def test_from_output_restores_initial_conditions_and_requires_trust_for_source(tmp_path, monkeypatch): +def test_from_output_restores_embedded_initial_condition_source(tmp_path, monkeypatch): path_out = tmp_path / "sim_1" path_out.mkdir() sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(user_density_profile, None))) sim.to_run_metadata(str(path_out / "run_metadata.json")) - with pytest.raises(ValueError, match="trust_initial_condition_source=True"): - Simulation.from_output(path_out) - - restored = Simulation.from_output(path_out, trust_initial_condition_source=True) + restored = Simulation.from_output(path_out) density = restored.model.kinetic_ions.var.backgrounds.params["n"][0] assert density(0.2, 0.3, 0.4) == user_density_profile(0.2, 0.3, 0.4) - restored_from_file = Simulation.from_file(path_out / "run_metadata.json", trust_initial_condition_source=True) + restored_from_file = Simulation.from_file(path_out / "run_metadata.json") assert restored_from_file.model.kinetic_ions.var.backgrounds.params["n"][0](0.2, 0.3, 0.4) == 1.0 def simulation_init_must_not_run(*args, **kwargs): raise AssertionError("Output must not instantiate Simulation") monkeypatch.setattr(Simulation, "__init__", simulation_init_must_not_run) - output = Output(path_out, trust_initial_condition_source=True) + output = Output(path_out) density_from_output = output.initial_conditions["kinetic_ions"]["var"]["backgrounds"].params["n"][0] assert density_from_output(0.2, 0.3, 0.4) == 1.0 assert output.model.kinetic_ions.var.backgrounds.params["n"][0](0.2, 0.3, 0.4) == 1.0 + saved = output.metadata["model"]["species"]["kinetic_ions"]["variables"]["var"]["initial_conditions"] + assert saved["backgrounds"]["params"]["n"][0]["name"] == "user_density_profile" def test_from_output_restores_user_perturbation_subclasses_and_callable_objects(tmp_path): @@ -233,7 +232,7 @@ def test_from_output_restores_user_perturbation_subclasses_and_callable_objects( sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(UserCallableProfile(1.5), None))) sim.to_run_metadata(str(path_out / "run_metadata.json")) - restored = Simulation.from_output(path_out, trust_initial_condition_source=True) + restored = Simulation.from_output(path_out) restored_perturbation = restored.model.em_fields.e_field.perturbations restored_profile = restored.model.kinetic_ions.var.backgrounds.params["n"][0] assert isinstance(restored_perturbation, Perturbation) @@ -256,7 +255,7 @@ def test_versioned_initial_conditions_round_trip_allocates_and_runs_one_step(tmp restored = Simulation.from_output(sim.env.path_out) assert restored.model.mhd.velocity.backgrounds == sim.model.mhd.velocity.backgrounds - assert restored._deserialize_initial_condition(sim.equil.to_dict(), trust_source=False) == sim.equil + assert restored._deserialize_initial_condition(sim.equil.to_dict()) == sim.equil restored.run(one_time_step=True) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 38364fa32..97f85691e 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -187,7 +187,7 @@ "source": [ "### Reconstructed setup and initial conditions\n", "\n", - "`out.info()` prints the saved model parameters, species variables, propagator options, initial-condition summary, and a short API guide. In `out.metadata`, serialized initial conditions live on each variable under `model → species → variables → initial_conditions`. `out.initial_conditions` gives the reconstructed objects; `out.model` restores them onto the model variables. This demonstration run uses only built-in definitions. For a run with saved user Python functions or classes, reopen it with `Output(path, trust_initial_condition_source=True)` only when the output is trusted." + "`out.info()` prints the saved model parameters, species variables, propagator options, initial-condition summary, and a short API guide. In `out.metadata`, serialized initial conditions live on each variable under `model → species → variables → initial_conditions`. `out.initial_conditions` gives reconstructed objects, including saved Python functions and classes; `out.model` restores them onto the model variables. Unsupported definitions remain available in `out.metadata`." ] }, { From 5c1fd41d6ad1406ed325a86f1d40b2578c076c83 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 14:13:01 +0200 Subject: [PATCH 104/193] Added spline_fields --- src/struphy/post_processing/output.py | 103 +++++++++++++++--- .../post_processing/tests/test_output.py | 15 +++ 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index b58eb4a43..0df582a92 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -262,22 +262,9 @@ def _reset(self): self._tree = None self._species = None self._seconds = None - - def __getitem__(self, name: str) -> xr.DataArray: - """Compatibility shorthand for :meth:`evaluate`.""" - if name in self.scalars.data_vars: - return self.scalars[name] - for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): - if name in catalog: - return catalog[name] - available = ( - *self.scalars.data_vars, - *self.field_catalog, - *self.distribution_catalog, - *self.density_catalog, - *self.orbit_catalog, - ) - raise KeyError(f"{name!r} not found; available products: {available}") + self._spline_derham = None + self._spline_fields = None + self._spline_snapshot = None def evaluate( self, @@ -1996,6 +1983,90 @@ def scalars(self) -> xr.Dataset: self._scalars = xr.Dataset(variables) return self._scalars + def iter_spline_coefficients(self, *, stride: int = 1, rank: int = 0): + """Yield saved FEEC spline coefficients one snapshot at a time. + + This reads only the raw ``data_proc.hdf5`` datasets. It does not + allocate spline functions, evaluate fields, create post-processing files, + or load particle data. Scalar variables are arrays; vector variables are + tuples of component arrays. + + ``rank`` selects one MPI rank's local coefficients. Global assembly remains + part of the explicit post-processing workflow. + """ + if not isinstance(stride, int) or stride < 1: + raise ValueError("stride must be a positive integer") + if not isinstance(rank, int) or rank < 0: + raise ValueError("rank must be a non-negative integer") + + path = self.path_out / "data" / f"data_proc{rank}.hdf5" + with h5py.File(path) as file: + if "feec" not in file: + return + times = file["time/value"] + for snapshot in range(0, len(times), stride): + coefficients = {} + for species_name, species in file["feec"].items(): + variables = {} + for variable_name, variable in species.items(): + if isinstance(variable, h5py.Dataset): + variables[variable_name] = np.asarray(variable[snapshot]) + else: + variables[variable_name] = tuple( + np.asarray(variable[component][snapshot]) for component in sorted(variable, key=int) + ) + coefficients[species_name] = variables + yield float(times[snapshot]) * self.time_scale, coefficients + + def spline_fields(self, snapshot: int) -> dict: + """Return FEEC ``SplineFunction`` objects loaded with one saved snapshot. + + The spline functions are allocated once and reused. Requesting the same + snapshot performs no HDF5 reads; requesting another snapshot overwrites + their coefficients in place. Copy evaluated values before requesting a + different snapshot. + + The returned mapping is ``species -> variable -> SplineFunction``. It is + intentionally separate from :meth:`evaluate`, which serves persisted + post-processed xarray products. + """ + if not isinstance(snapshot, int): + raise TypeError("snapshot must be an integer") + if self.grid is None or self.derham_opts is None: + raise ValueError("Spline fields require saved grid and derham options") + + data_path = self.path_out / "data" / "data_proc0.hdf5" + with h5py.File(data_path) as file: + if "feec" not in file: + raise ValueError("This output contains no saved FEEC fields") + n_snapshots = len(file["time/value"]) + if snapshot < 0: + snapshot += n_snapshots + if not 0 <= snapshot < n_snapshots: + raise IndexError(f"snapshot {snapshot} is outside [0, {n_snapshots})") + + if self._spline_fields is None: + self._spline_derham = Derham(self.grid, self.derham_opts, comm=None, domain=self.domain) + self._spline_fields = { + species_name: { + variable_name: self._spline_derham.create_spline_function( + variable_name, variable.attrs["space_id"] + ) + for variable_name, variable in species.items() + } + for species_name, species in file["feec"].items() + } + + if snapshot != self._spline_snapshot: + with ExitStack() as stack: + files = [ + stack.enter_context(h5py.File(self.path_out / "data" / f"data_proc{rank}.hdf5")) + for rank in range(self.mpi_ranks) + ] + self._load_femfields(self._spline_fields, files, snapshot) + self._spline_snapshot = snapshot + return self._spline_fields + @property def label(self) -> str: """Short description of the numerical parameters, for figure titles.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 530989d1b..4450b6650 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -369,6 +369,21 @@ def test_info_labels_distribution_and_density_symbols(run, capsys): assert "SPH density ($n$)" in text +def test_iter_spline_coefficients_reads_one_raw_snapshot_at_a_time(run): + raw_path = run.path_out / "data" / "data_proc0.hdf5" + with h5py.File(raw_path, "a") as file: + file.create_dataset("feec/em_fields/phi", data=np.arange(NT * 2).reshape(NT, 2)) + file.create_group("feec/em_fields/e_field") + file.create_dataset("feec/em_fields/e_field/1", data=np.full((NT, 2), 1.0)) + file.create_dataset("feec/em_fields/e_field/2", data=np.full((NT, 2), 2.0)) + + snapshots = list(run.iter_spline_coefficients(stride=2)) + assert [time for time, _ in snapshots] == [0.0, 1.0] + assert np.array_equal(snapshots[1]["em_fields"]["phi"], np.array([4, 5])) + assert len(snapshots[0]["em_fields"]["e_field"]) == 2 + assert np.array_equal(snapshots[0]["em_fields"]["e_field"][1], np.array([2.0, 2.0])) + + def test_normalized_time_carries_seconds_as_a_coordinate(run): energy = run.scalars.en_tot assert "units" not in energy.t.attrs, "normalized time has no unit" From abc4f32413f876172288d38500dabc070c887e56 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 14:28:26 +0200 Subject: [PATCH 105/193] Removed Output.__getitem__ --- .../diocotron_instability/pproc_diocotron.py | 2 +- .../bump_on/pproc_bump_on.py | 2 +- .../verification/test_verif_LinearMHD.py | 4 +- .../tests/verification/test_verif_Maxwell.py | 2 +- src/struphy/post_processing/output.py | 18 ++++++- .../post_processing/output_accessors.py | 2 +- .../tests/test_derived_products.py | 22 ++++----- .../post_processing/tests/test_output.py | 2 +- .../tests/test_output_accessors.py | 48 +++++++++---------- tutorials/tutorial_post_processing.ipynb | 4 +- 10 files changed, 61 insertions(+), 45 deletions(-) diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 55f30a478..a417825c6 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -46,7 +46,7 @@ def main(paths=(DEFAULT_OUTPUT,)): run.plot.equilibrium() for name in SWEEPS: - run[name].struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() + run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index e45da3f66..bf686048e 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -12,7 +12,7 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # initial velocity distribution - initial = run["kinetic_ions/v1_density/f"].isel(t=0) + initial = run.evaluate("kinetic_ions/v1_density/f").isel(t=0) ax = initial.plot()[0].axes ax.set( xlabel="velocity $v$", diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 111bf14a2..b122393ea 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -87,7 +87,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} - _1, _2, _3, coeffs = run["mhd/velocity"].struphy.analysis.dispersion( + _1, _2, _3, coeffs = run.evaluate("mhd/velocity").struphy.analysis.dispersion( physical=True, component=0, slice_at=[0, 0, None], @@ -107,7 +107,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 # second fft - _1, _2, _3, coeffs = run["mhd/pressure"].struphy.analysis.dispersion( + _1, _2, _3, coeffs = run.evaluate("mhd/pressure").struphy.analysis.dispersion( physical=True, component=0, slice_at=[0, 0, None], diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index b9f0e942a..cd8dbea16 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -72,7 +72,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: # fft - _1, _2, _3, coeffs = run["em_fields/e_field"].struphy.analysis.dispersion( + _1, _2, _3, coeffs = run.evaluate("em_fields/e_field").struphy.analysis.dispersion( physical=True, component=0, slice_at=[0, 0, None], diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 0df582a92..97e9d1718 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -302,7 +302,7 @@ def evaluate( raise NotImplementedError(f"{type(self.domain).__name__} has no inverse_map for physical evaluation") eta = inverse(*(float(physical[axis]) for axis in ("X", "Y", "Z"))) physical_sel = dict(zip(("e1", "e2", "e3"), map(float, eta))) - array = self[name] + array = self._product(name) if isel: array = array.isel(isel, drop=drop) if physical_sel: @@ -316,6 +316,22 @@ def evaluate( raise ValueError("method requires a coordinate selection through sel") return array.to_numpy() if as_numpy else array + def _product(self, name: str) -> xr.DataArray: + """Resolve one saved product for :meth:`evaluate`.""" + if name in self.scalars.data_vars: + return self.scalars[name] + for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): + if name in catalog: + return catalog[name] + available = ( + *self.scalars.data_vars, + *self.field_catalog, + *self.distribution_catalog, + *self.density_catalog, + *self.orbit_catalog, + ) + raise KeyError(f"{name!r} not found; available products: {available}") + def growth_rate(self, product: str | xr.DataArray, *, window=(None, None), amplitude: bool = False): """Fit exponential growth of a scalar product and return a ``FitResult``.""" from struphy.diagnostics.analysis import GrowthFit, growth_rate diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py index 559a22827..3e2684d55 100644 --- a/src/struphy/post_processing/output_accessors.py +++ b/src/struphy/post_processing/output_accessors.py @@ -3,7 +3,7 @@ Plots and diagnostics of a single array live on the array, see :class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`: ``out.em_fields.phi_log.struphy.plot.slice(...)``, or by name -``out["em_fields/phi_log"].struphy.plot.slice(...)``. +``out.evaluate("em_fields/phi_log").struphy.plot.slice(...)``. """ from __future__ import annotations diff --git a/src/struphy/post_processing/tests/test_derived_products.py b/src/struphy/post_processing/tests/test_derived_products.py index c872c5349..5eeb33a1f 100644 --- a/src/struphy/post_processing/tests/test_derived_products.py +++ b/src/struphy/post_processing/tests/test_derived_products.py @@ -147,8 +147,8 @@ def test_reductions_are_available_from_the_run_and_the_accessor(run): average = run.spatial_average(F) assert average.dims == ("t", "v1") - xr.testing.assert_identical(average, run[F].struphy.analysis.spatial_average()) - xr.testing.assert_identical(moments, run[F].struphy.analysis.velocity_moments()) + xr.testing.assert_identical(average, run.evaluate(F).struphy.analysis.spatial_average()) + xr.testing.assert_identical(moments, run.evaluate(F).struphy.analysis.velocity_moments()) # --- SI units --------------------------------------------------------------------------------- @@ -157,28 +157,28 @@ def test_reductions_are_available_from_the_run_and_the_accessor(run): def test_coordinates_are_converted_and_values_left_alone(run): units = run.units f = run.to_si(F) - np.testing.assert_allclose(f.v1, run[F].v1 * units.v) + np.testing.assert_allclose(f.v1, run.evaluate(F).v1 * units.v) assert f.v1.attrs["units"] == "m/s" - np.testing.assert_allclose(f.t, run[F].t * units.t) + np.testing.assert_allclose(f.t, run.evaluate(F).t * units.t) assert f.t.attrs["units"] == "s" assert "t_seconds" not in f.coords - np.testing.assert_array_equal(f.e1, run[F].e1) # logical coordinates are dimensionless - np.testing.assert_array_equal(f, run[F]) + np.testing.assert_array_equal(f.e1, run.evaluate(F).e1) # logical coordinates are dimensionless + np.testing.assert_array_equal(f, run.evaluate(F)) assert "units" not in f.attrs - assert run[F].v1.attrs.get("units") is None # the run's own product is untouched - assert "t_seconds" in run[F].coords + assert run.evaluate(F).v1.attrs.get("units") is None # the run's own product is untouched + assert "t_seconds" in run.evaluate(F).coords def test_mapped_coordinates_are_scaled_by_the_length_unit(run): assert run.units.x == 2.0 field = run.to_si("em_fields/E") - np.testing.assert_allclose(field.X, run["em_fields/E"].X * 2.0) + np.testing.assert_allclose(field.X, run.evaluate("em_fields/E").X * 2.0) assert field.X.attrs["units"] == "m" def test_values_are_converted_with_a_named_unit(run): field = run.to_si("em_fields/E", "B") - np.testing.assert_allclose(field, run["em_fields/E"] * run.units.B) + np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.B) assert field.attrs["units"] == "T" assert field.name == "E" assert field.attrs["run_name"] == run.path_out.name @@ -186,7 +186,7 @@ def test_values_are_converted_with_a_named_unit(run): def test_values_are_converted_with_a_composite_unit(run): field = run.to_si("em_fields/E", run.units.v * run.units.B, label="V/m") - np.testing.assert_allclose(field, run["em_fields/E"] * run.units.v * run.units.B) + np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.v * run.units.B) assert field.attrs["units"] == "V/m" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 4450b6650..1cb278991 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -256,7 +256,7 @@ def fake_pproc(self, **options): def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): field = run.evaluate("em_fields/E") assert isinstance(field, xr.DataArray) - assert field is run["em_fields/E"] + assert field is run.evaluate("em_fields/E") assert run.xarray is run.tree diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py index 05f1ea99d..3df900681 100644 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ b/src/struphy/post_processing/tests/test_output_accessors.py @@ -40,24 +40,24 @@ def close_figures(): def test_products_are_found_by_name(run): - assert run["en_tot"].dims == ("t",) - assert run["em_fields/E"].dims[:2] == ("t", "component") - assert run["kinetic_ions/e1_v1_density/f"].dims == ("t", "e1", "v1") - assert run["kinetic_ions/view_0/n"].dims == ("t", "e1", "e2", "e3") - assert run["kinetic_ions"].dims == ("t", "marker", "quantity") + assert run.evaluate("en_tot").dims == ("t",) + assert run.evaluate("em_fields/E").dims[:2] == ("t", "component") + assert run.evaluate("kinetic_ions/e1_v1_density/f").dims == ("t", "e1", "v1") + assert run.evaluate("kinetic_ions/view_0/n").dims == ("t", "e1", "e2", "e3") + assert run.evaluate("kinetic_ions").dims == ("t", "marker", "quantity") with pytest.raises(KeyError, match="available products"): - run["t"] + run.evaluate("t") def test_every_array_carries_its_run(run): - for array in (run.scalars.en_tot, run.fields.em_fields.E, run["kinetic_ions"]): + for array in (run.scalars.en_tot, run.fields.em_fields.E, run.evaluate("kinetic_ions")): assert array.attrs["run"] == run.label assert array.attrs["run_name"] == "sim_1" assert run.scalars.en_tot.isel(t=slice(1, None)).attrs["run_name"] == "sim_1" def test_timeseries_by_name_with_growth_fit(run): - result = run["en_phi"].struphy.plot.timeseries(fit=True) + result = run.evaluate("en_phi").struphy.plot.timeseries(fit=True) assert result.fit_results[0].rate == pytest.approx(RATE) assert result.fig._suptitle.get_text() == run.label @@ -87,7 +87,7 @@ def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): def test_timeseries_into_given_axes_keeps_the_figure_layout(run): fig, ax = plt.subplots() fig.suptitle("mine") - run["en_tot"].struphy.plot.timeseries(ax=ax, logy=False) + run.evaluate("en_tot").struphy.plot.timeseries(ax=ax, logy=False) assert fig._suptitle.get_text() == "mine" @@ -99,9 +99,9 @@ def test_scalar_overview_draws_every_scalar_in_one_axes(run): def test_slices_panels_and_viewer_take_keyword_views(run): name = "kinetic_ions/e1_v1_density/f" - assert run[name].struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(run[name].struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - viewer = run["em_fields/E"].struphy.plot.viewer(x="e1", y="e2", component=0) + assert run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" + assert len(run.evaluate(name).struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + viewer = run.evaluate("em_fields/E").struphy.plot.viewer(x="e1", y="e2", component=0) viewer.draw() assert set(viewer.sliders) == {"t", "e3"} @@ -117,10 +117,10 @@ def test_report_is_written_below_post_processing(run): def test_analysis_by_name(run): - assert run["en_phi"].struphy.analysis.growth_rate(window=(0.0, None)).rate == pytest.approx(RATE) - assert run["en_phi"].struphy.analysis.growth_rate(amplitude=True).rate == pytest.approx(RATE / 2) - np.testing.assert_allclose(run["en_tot"].struphy.analysis.relative_error(), 0.0) - np.testing.assert_allclose(run["en_phi"].struphy.analysis.drift().isel(t=0), 0.0) + assert run.evaluate("en_phi").struphy.analysis.growth_rate(window=(0.0, None)).rate == pytest.approx(RATE) + assert run.evaluate("en_phi").struphy.analysis.growth_rate(amplitude=True).rate == pytest.approx(RATE / 2) + np.testing.assert_allclose(run.evaluate("en_tot").struphy.analysis.relative_error(), 0.0) + np.testing.assert_allclose(run.evaluate("en_phi").struphy.analysis.drift().isel(t=0), 0.0) def test_dispersion_rejects_fields_in_seconds(run): @@ -131,18 +131,18 @@ def test_dispersion_rejects_fields_in_seconds(run): def test_selection_keywords_take_positions_values_and_ends(run): name = "kinetic_ions/e1_v1_density/f" - times = run[name].t.values + times = run.evaluate(name).t.values - by_position = run[name].struphy.plot.slice(x="e1", y="v1", t=-1) - by_value = run[name].struphy.plot.slice(x="e1", y="v1", t=float(times[-1])) - by_end = run[name].struphy.plot.slice(x="e1", y="v1", t="last") + by_position = run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t=-1) + by_value = run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t=float(times[-1])) + by_end = run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t="last") for result in (by_value, by_end): np.testing.assert_allclose(result.artists[0].get_array(), by_position.artists[0].get_array()) with pytest.raises(TypeError, match="not a dimension"): - run[name].struphy.plot.slice(x="e1", y="v1", time=-1) + run.evaluate(name).struphy.plot.slice(x="e1", y="v1", time=-1) with pytest.raises(TypeError, match="use a number"): - run[name].struphy.plot.slice(x="e1", y="v1", t="final") + run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t="final") def test_products_of_one_species_sit_on_the_output(run): @@ -182,7 +182,7 @@ def test_the_accessor_works_on_derived_arrays(run): def test_products_by_name_and_by_attribute_agree(run): - by_output = run["kinetic_ions/e1_v1_density/f"].struphy.plot.slice(x="e1", y="v1", t="last") + by_output = run.evaluate("kinetic_ions/e1_v1_density/f").struphy.plot.slice(x="e1", y="v1", t="last") by_attribute = run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", t="last") np.testing.assert_allclose(by_output.artists[0].get_array(), by_attribute.artists[0].get_array()) assert by_output.fig._suptitle.get_text() == by_attribute.fig._suptitle.get_text() == run.label @@ -215,7 +215,7 @@ def test_damping_rate_fits_the_envelope_not_the_oscillation(run): def test_damping_rate_without_peaks_is_none(run): - assert run.damping_rate(run["en_phi"]) is None + assert run.damping_rate(run.evaluate("en_phi")) is None def test_norm_reduces_all_but_time(run): diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 97f85691e..e17f801a4 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -157,7 +157,7 @@ "id": "7", "metadata": {}, "source": [ - "Products sit on the run under the species that produced them, so VS Code and interactive shells complete them as you type: `out.kinetic_ions.e1_v1_density.f`, `out.kinetic_ions.orbits`, `out.em_fields.phi_log`. To discover the products a particular namespace contains, evaluate it or inspect its flat, lazy `.catalog`: `list(out.kinetic_ions.catalog)`. The grouped views `out.fields`, `out.distributions`, `out.densities` and `out.orbits` show the same products by kind, and `out[\"kinetic_ions/e1_v1_density/f\"]` looks one up by name, which is handy in scripts and loops." + "Products sit on the run under the species that produced them, so VS Code and interactive shells complete them as you type: `out.kinetic_ions.e1_v1_density.f`, `out.kinetic_ions.orbits`, `out.em_fields.phi_log`. To discover the products a particular namespace contains, evaluate it or inspect its flat, lazy `.catalog`: `list(out.kinetic_ions.catalog)`. The grouped views `out.fields`, `out.distributions`, `out.densities` and `out.orbits` show the same products by kind, and `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks one up by name, which is handy in scripts and loops." ] }, { @@ -336,7 +336,7 @@ "source": [ "## Scalar overview and time series\n", "\n", - "Products plot themselves: every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f.struphy.plot.slice(...)` needs no imports and completes as you type. `out[\"kinetic_ions/e1_v1_density/f\"]` looks the same product up by name, which suits scripts and loops. The plots that need a whole run, `out.plot.scalars()` and `out.plot.equilibrium()`, stay on the run.\n", + "Products plot themselves: every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f.struphy.plot.slice(...)` needs no imports and completes as you type. `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks the same product up by name, which suits scripts and loops. The plots that need a whole run, `out.plot.scalars()` and `out.plot.equilibrium()`, stay on the run.\n", "\n", "`out.plot.scalars()` gives a quick overview of every recorded scalar. `.struphy.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] From 11227e121a4a078bf0556048fbccfe020d4432db Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 14:34:12 +0200 Subject: [PATCH 106/193] Updated the out.spline_fields to use a t=.. variable --- src/struphy/post_processing/output.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 97e9d1718..8016316fa 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -2034,20 +2034,21 @@ def iter_spline_coefficients(self, *, stride: int = 1, rank: int = 0): coefficients[species_name] = variables yield float(times[snapshot]) * self.time_scale, coefficients - def spline_fields(self, snapshot: int) -> dict: - """Return FEEC ``SplineFunction`` objects loaded with one saved snapshot. + def spline_fields(self, *, t: int) -> dict: + """Return FEEC ``SplineFunction`` objects loaded at saved index ``t``. The spline functions are allocated once and reused. Requesting the same - snapshot performs no HDF5 reads; requesting another snapshot overwrites + ``t`` performs no HDF5 reads; requesting another index overwrites their coefficients in place. Copy evaluated values before requesting a - different snapshot. + different index. As with :meth:`evaluate`, ``t=0`` is the first saved + snapshot and ``t=-1`` is the last. The returned mapping is ``species -> variable -> SplineFunction``. It is intentionally separate from :meth:`evaluate`, which serves persisted post-processed xarray products. """ - if not isinstance(snapshot, int): - raise TypeError("snapshot must be an integer") + if not isinstance(t, int): + raise TypeError("t must be an integer saved-snapshot index") if self.grid is None or self.derham_opts is None: raise ValueError("Spline fields require saved grid and derham options") @@ -2056,10 +2057,10 @@ def spline_fields(self, snapshot: int) -> dict: if "feec" not in file: raise ValueError("This output contains no saved FEEC fields") n_snapshots = len(file["time/value"]) - if snapshot < 0: - snapshot += n_snapshots - if not 0 <= snapshot < n_snapshots: - raise IndexError(f"snapshot {snapshot} is outside [0, {n_snapshots})") + if t < 0: + t += n_snapshots + if not 0 <= t < n_snapshots: + raise IndexError(f"t={t} is outside the saved snapshot range") if self._spline_fields is None: self._spline_derham = Derham(self.grid, self.derham_opts, comm=None, domain=self.domain) @@ -2073,14 +2074,14 @@ def spline_fields(self, snapshot: int) -> dict: for species_name, species in file["feec"].items() } - if snapshot != self._spline_snapshot: + if t != self._spline_snapshot: with ExitStack() as stack: files = [ stack.enter_context(h5py.File(self.path_out / "data" / f"data_proc{rank}.hdf5")) for rank in range(self.mpi_ranks) ] - self._load_femfields(self._spline_fields, files, snapshot) - self._spline_snapshot = snapshot + self._load_femfields(self._spline_fields, files, t) + self._spline_snapshot = t return self._spline_fields @property From d8ad40f56f73ec747aa671f503f0303f8f002c3b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 14:41:08 +0200 Subject: [PATCH 107/193] Simplify the evaluate method --- src/struphy/post_processing/output.py | 36 ++++++++++++------- .../post_processing/tests/test_output.py | 8 ++--- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 8016316fa..a82fb9d1b 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -270,28 +270,40 @@ def evaluate( self, name: str, *, - sel: Mapping[str, Any] | None = None, - isel: Mapping[str, Any] | None = None, method: str | None = None, drop: bool = False, as_numpy: bool = False, physical: Mapping[str, float] | None = None, + t: int | float | str | None = None, + **coordinates: Any, ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. Scalars are read directly from raw output. Other products are materialized with :meth:`pproc` on first use when no complete post-processing output exists. The returned array is an ordinary xarray object, so use xarray for selection, arithmetic and further - analysis. ``isel`` selects positions (for example ``{"t": -1}``) and ``sel`` selects - dimension-coordinate values (for example ``{"e3": 0.5}``). Positional selection is - applied first, followed by coordinate selection. ``method`` and ``drop`` have xarray's - usual ``.sel``/``.isel`` meanings. Set ``as_numpy=True`` to return only the selected - values as a :class:`numpy.ndarray`. + analysis. Set ``as_numpy=True`` to return only the selected values as a + :class:`numpy.ndarray`. + + Common selections can be passed directly: integer ``t`` selects a saved + snapshot (``t=0`` first, ``t=-1`` last), while float ``t`` selects a time + coordinate. Other keyword arguments select named coordinates, for example + ``component=2`` or ``e1=0.5``. ``physical={"X": x, "Y": y, "Z": z}`` evaluates a field at a physical point when its domain supplies an analytical ``inverse_map``. It converts the point to logical coordinates and uses xarray interpolation. """ + selectors = dict(coordinates) + t_index = None + if t is not None: + if isinstance(t, (int, np.integer)): + t_index = int(t) + elif isinstance(t, (float, np.floating)): + selectors["t"] = float(t) + else: + raise TypeError("t must be an integer snapshot index or a float time coordinate") + physical_sel = None if physical: required = {"X", "Y", "Z"} @@ -303,17 +315,17 @@ def evaluate( eta = inverse(*(float(physical[axis]) for axis in ("X", "Y", "Z"))) physical_sel = dict(zip(("e1", "e2", "e3"), map(float, eta))) array = self._product(name) - if isel: - array = array.isel(isel, drop=drop) + if t_index is not None: + array = array.isel(t=t_index, drop=drop) if physical_sel: array = array.interp(physical_sel, method=method or "linear") - if sel: + if selectors: options = {"drop": drop} if method is not None: options["method"] = method - array = array.sel(sel, **options) + array = array.sel(selectors, **options) elif method is not None: - raise ValueError("method requires a coordinate selection through sel") + raise ValueError("method requires a direct coordinate selector") return array.to_numpy() if as_numpy else array def _product(self, name: str) -> xr.DataArray: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 1cb278991..8f8ee611b 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -261,22 +261,22 @@ def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): def test_evaluate_selects_positions_coordinates_and_slices(run): - field = run.evaluate("em_fields/E", isel={"t": -1, "component": 2}) + field = run.evaluate("em_fields/E", t=-1, component=2) assert field.dims == ("e1", "e2", "e3") np.testing.assert_allclose(field, 3.0) phase_space = run.evaluate( "kinetic_ions/e1_v1_density/f", - sel={"e1": 0.49}, + e1=0.49, method="nearest", drop=True, ) assert phase_space.dims == ("t", "v1") - history = run.evaluate("en_tot", isel={"t": slice(1, None)}) + history = run.evaluate("en_tot").isel(t=slice(1, None)) assert history.sizes["t"] == NT - 1 - values = run.evaluate("en_tot", isel={"t": -1}, as_numpy=True) + values = run.evaluate("en_tot", t=-1, as_numpy=True) assert isinstance(values, np.ndarray) np.testing.assert_allclose(values, 2.0) From c98a59157a967dddc26a67773a1f8fabf189ea4b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 15:00:53 +0200 Subject: [PATCH 108/193] snapshots in time --- src/struphy/post_processing/output.py | 32 +++++++++++-------- .../post_processing/tests/test_output.py | 6 +++- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index a82fb9d1b..fe2d7ffad 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -285,24 +285,17 @@ def evaluate( analysis. Set ``as_numpy=True`` to return only the selected values as a :class:`numpy.ndarray`. - Common selections can be passed directly: integer ``t`` selects a saved - snapshot (``t=0`` first, ``t=-1`` last), while float ``t`` selects a time - coordinate. Other keyword arguments select named coordinates, for example - ``component=2`` or ``e1=0.5``. + Common selections can be passed directly: ``t`` selects saved snapshots + by index (an integer, list of integers, or slice); omit it for every + saved timestep. The returned array always retains its ``t`` dimension. + A float ``t`` selects a time coordinate. Other keyword arguments select + named coordinates, for example ``component=2`` or ``e1=0.5``. ``physical={"X": x, "Y": y, "Z": z}`` evaluates a field at a physical point when its domain supplies an analytical ``inverse_map``. It converts the point to logical coordinates and uses xarray interpolation. """ selectors = dict(coordinates) - t_index = None - if t is not None: - if isinstance(t, (int, np.integer)): - t_index = int(t) - elif isinstance(t, (float, np.floating)): - selectors["t"] = float(t) - else: - raise TypeError("t must be an integer snapshot index or a float time coordinate") physical_sel = None if physical: @@ -315,8 +308,19 @@ def evaluate( eta = inverse(*(float(physical[axis]) for axis in ("X", "Y", "Z"))) physical_sel = dict(zip(("e1", "e2", "e3"), map(float, eta))) array = self._product(name) - if t_index is not None: - array = array.isel(t=t_index, drop=drop) + if t is not None: + if isinstance(t, (int, np.integer)): + array = array.isel(t=[int(t)], drop=drop) + elif isinstance(t, slice): + array = array.isel(t=t, drop=drop) + elif isinstance(t, (list, tuple, np.ndarray)): + if not all(isinstance(index, (int, np.integer)) for index in t): + raise TypeError("t sequences must contain saved-snapshot indices") + array = array.isel(t=list(t), drop=drop) + elif isinstance(t, (float, np.floating)): + selectors["t"] = [float(t)] + else: + raise TypeError("t must be a saved-snapshot index, index sequence, slice, or float time coordinate") if physical_sel: array = array.interp(physical_sel, method=method or "linear") if selectors: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 8f8ee611b..d4050d323 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -262,9 +262,13 @@ def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): def test_evaluate_selects_positions_coordinates_and_slices(run): field = run.evaluate("em_fields/E", t=-1, component=2) - assert field.dims == ("e1", "e2", "e3") + assert field.dims == ("t", "e1", "e2", "e3") + assert field.sizes["t"] == 1 np.testing.assert_allclose(field, 3.0) + every_second = run.evaluate("em_fields/E", t=slice(0, None, 2)) + assert every_second.sizes["t"] == 2 + phase_space = run.evaluate( "kinetic_ions/e1_v1_density/f", e1=0.49, From e608ad5bba302d20a09a54c21a851d2f2f7b89fa Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 15:25:19 +0200 Subject: [PATCH 109/193] Enable specify etas --- src/struphy/post_processing/output.py | 93 ++++++++++++++++++- .../post_processing/tests/test_output.py | 23 +++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index fe2d7ffad..581aa62d8 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -274,7 +274,10 @@ def evaluate( drop: bool = False, as_numpy: bool = False, physical: Mapping[str, float] | None = None, - t: int | float | str | None = None, + t: int | float | slice | Sequence[int] | None = None, + eta1: float | None = None, + eta2: float | None = None, + eta3: float | None = None, **coordinates: Any, ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. @@ -294,9 +297,25 @@ def evaluate( ``physical={"X": x, "Y": y, "Z": z}`` evaluates a field at a physical point when its domain supplies an analytical ``inverse_map``. It converts the point to logical coordinates and uses xarray interpolation. + + Supplying all of ``eta1``, ``eta2`` and ``eta3`` instead evaluates a raw FEEC + spline field directly at that logical point. This reads coefficients one saved + snapshot at a time and does not materialize a spatial post-processing product. + Use a raw field name such as ``"em_fields/e_field"``. """ selectors = dict(coordinates) + eta = (eta1, eta2, eta3) + has_eta = any(value is not None for value in eta) + if has_eta: + if any(value is None for value in eta): + raise ValueError("eta1, eta2, and eta3 must be supplied together") + if physical: + raise ValueError("physical and eta1/eta2/eta3 selections cannot be combined") + array = self._evaluate_spline_point(name, *map(float, eta), t=t, method=method) + t = None + method = None + physical_sel = None if physical: required = {"X", "Y", "Z"} @@ -307,7 +326,8 @@ def evaluate( raise NotImplementedError(f"{type(self.domain).__name__} has no inverse_map for physical evaluation") eta = inverse(*(float(physical[axis]) for axis in ("X", "Y", "Z"))) physical_sel = dict(zip(("e1", "e2", "e3"), map(float, eta))) - array = self._product(name) + if not has_eta: + array = self._product(name) if t is not None: if isinstance(t, (int, np.integer)): array = array.isel(t=[int(t)], drop=drop) @@ -332,6 +352,75 @@ def evaluate( raise ValueError("method requires a direct coordinate selector") return array.to_numpy() if as_numpy else array + def _evaluate_spline_point( + self, name: str, eta1: float, eta2: float, eta3: float, *, t: int | float | slice | Sequence[int] | None, + method: str | None, + ) -> xr.DataArray: + """Evaluate one raw FEEC field at one logical point for selected snapshots.""" + try: + species, variable = name.split("/") + except ValueError as error: + raise ValueError("raw spline fields use a 'species/variable' name") from error + + path = self.path_out / "data" / "data_proc0.hdf5" + with h5py.File(path) as file: + if "feec" not in file: + raise ValueError("This output contains no saved FEEC fields") + times = np.asarray(file["time/value"]) * self.time_scale + indices = self._snapshot_indices(t, times, method=method) + + values = [] + for snapshot in indices: + fields = self.spline_fields(t=int(snapshot)) + try: + field = fields[species][variable] + except KeyError as error: + available = tuple(f"{group}/{key}" for group, entries in fields.items() for key in entries) + raise KeyError(f"{name!r} is not a saved raw FEEC field; available fields: {available}") from error + value = field(eta1, eta2, eta3, squeeze_out=True) + if isinstance(value, (list, tuple)): + value = [component.item() if hasattr(component, "item") else component for component in value] + else: + value = value.item() if hasattr(value, "item") else value + values.append(value) + + data = np.asarray(values) + dims = ("t",) if data.ndim == 1 else ("t", "component") + coords: dict[str, Any] = {"t": times[indices]} + if data.ndim == 2: + coords["component"] = np.arange(data.shape[1]) + return self._stamp(xr.DataArray(data, dims=dims, coords=coords, name=variable)) + + @staticmethod + def _snapshot_indices( + selection: int | float | slice | Sequence[int] | None, times: np.ndarray, *, method: str | None, + ) -> np.ndarray: + """Turn the public ``t`` selector into non-negative saved-snapshot indices.""" + count = len(times) + if selection is None: + return np.arange(count) + if isinstance(selection, (int, np.integer)): + indices = np.array([int(selection)]) + elif isinstance(selection, slice): + return np.arange(count)[selection] + elif isinstance(selection, (list, tuple, np.ndarray)): + if not all(isinstance(index, (int, np.integer)) for index in selection): + raise TypeError("t sequences must contain saved-snapshot indices") + indices = np.asarray(selection, dtype=int) + elif isinstance(selection, (float, np.floating)): + matches = np.flatnonzero(np.isclose(times, float(selection))) + if matches.size: + return matches[:1] + if method == "nearest" and count: + return np.array([np.abs(times - float(selection)).argmin()]) + raise KeyError(f"time coordinate {selection} is not saved") + else: + raise TypeError("t must be a saved-snapshot index, index sequence, slice, or float time coordinate") + indices = np.where(indices < 0, indices + count, indices) + if np.any((indices < 0) | (indices >= count)): + raise IndexError("t is outside the saved snapshot range") + return indices + def _product(self, name: str) -> xr.DataArray: """Resolve one saved product for :meth:`evaluate`.""" if name in self.scalars.data_vars: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index d4050d323..e9456116a 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -285,6 +285,29 @@ def test_evaluate_selects_positions_coordinates_and_slices(run): np.testing.assert_allclose(values, 2.0) +def test_evaluate_raw_spline_field_at_logical_point(run, monkeypatch): + calls = [] + + class Field: + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): + calls.append((eta1, eta2, eta3)) + return [eta1, eta2, eta3] + + field = Field() + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"e_field": field}}) + + values = run.evaluate("em_fields/e_field", eta1=0.25, eta2=0.5, eta3=0.75, component=2) + + assert values.dims == ("t",) + np.testing.assert_allclose(values.t, np.arange(NT) / (NT - 1)) + np.testing.assert_allclose(values, 0.75) + assert calls == [(0.25, 0.5, 0.75)] * NT + + last = run.evaluate("em_fields/e_field", eta1=0.25, eta2=0.5, eta3=0.75, t=-1) + assert last.dims == ("t", "component") + assert last.sizes["t"] == 1 + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) From 11f7e210e457d301a9abf2050ac8970d519b0fad Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 15:28:54 +0200 Subject: [PATCH 110/193] Support ranges and lists --- src/struphy/post_processing/output.py | 70 ++++++++++++++----- .../post_processing/tests/test_output.py | 17 +++++ 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 581aa62d8..7c9e64627 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -275,9 +275,9 @@ def evaluate( as_numpy: bool = False, physical: Mapping[str, float] | None = None, t: int | float | slice | Sequence[int] | None = None, - eta1: float | None = None, - eta2: float | None = None, - eta3: float | None = None, + eta1: Any | None = None, + eta2: Any | None = None, + eta3: Any | None = None, **coordinates: Any, ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. @@ -299,9 +299,11 @@ def evaluate( coordinates and uses xarray interpolation. Supplying all of ``eta1``, ``eta2`` and ``eta3`` instead evaluates a raw FEEC - spline field directly at that logical point. This reads coefficients one saved - snapshot at a time and does not materialize a spatial post-processing product. - Use a raw field name such as ``"em_fields/e_field"``. + spline field directly on that logical grid. Each eta can be a scalar, a list, + a one-dimensional array, or a ``range``; mixed inputs form their tensor-product + mesh internally. This reads coefficients one saved snapshot at a time and does + not materialize a spatial post-processing product. Use a raw field name such as + ``"em_fields/e_field"``. """ selectors = dict(coordinates) @@ -312,7 +314,7 @@ def evaluate( raise ValueError("eta1, eta2, and eta3 must be supplied together") if physical: raise ValueError("physical and eta1/eta2/eta3 selections cannot be combined") - array = self._evaluate_spline_point(name, *map(float, eta), t=t, method=method) + array = self._evaluate_spline_field(name, *eta, t=t, method=method) t = None method = None @@ -352,11 +354,11 @@ def evaluate( raise ValueError("method requires a direct coordinate selector") return array.to_numpy() if as_numpy else array - def _evaluate_spline_point( - self, name: str, eta1: float, eta2: float, eta3: float, *, t: int | float | slice | Sequence[int] | None, + def _evaluate_spline_field( + self, name: str, eta1: Any, eta2: Any, eta3: Any, *, t: int | float | slice | Sequence[int] | None, method: str | None, ) -> xr.DataArray: - """Evaluate one raw FEEC field at one logical point for selected snapshots.""" + """Evaluate one raw FEEC field on a tensor-product logical grid.""" try: species, variable = name.split("/") except ValueError as error: @@ -369,6 +371,9 @@ def _evaluate_spline_point( times = np.asarray(file["time/value"]) * self.time_scale indices = self._snapshot_indices(t, times, method=method) + etas, grid_dims, grid_coords = self._logical_grid(eta1, eta2, eta3) + grid_shape = tuple(len(grid_coords[dim]) for dim in grid_dims) + values = [] for snapshot in indices: fields = self.spline_fields(t=int(snapshot)) @@ -377,20 +382,51 @@ def _evaluate_spline_point( except KeyError as error: available = tuple(f"{group}/{key}" for group, entries in fields.items() for key in entries) raise KeyError(f"{name!r} is not a saved raw FEEC field; available fields: {available}") from error - value = field(eta1, eta2, eta3, squeeze_out=True) + value = field(*etas, squeeze_out=True) if isinstance(value, (list, tuple)): - value = [component.item() if hasattr(component, "item") else component for component in value] + value = [self._reshape_spline_value(component, grid_shape) for component in value] else: - value = value.item() if hasattr(value, "item") else value + value = self._reshape_spline_value(value, grid_shape) values.append(value) - data = np.asarray(values) - dims = ("t",) if data.ndim == 1 else ("t", "component") - coords: dict[str, Any] = {"t": times[indices]} - if data.ndim == 2: + is_vector = bool(values and isinstance(values[0], list)) + data = np.asarray(values) if values else np.empty((0, *grid_shape)) + dims = ("t",) + (("component",) if is_vector else ()) + tuple(grid_dims) + coords: dict[str, Any] = {"t": times[indices], **grid_coords} + if is_vector: coords["component"] = np.arange(data.shape[1]) return self._stamp(xr.DataArray(data, dims=dims, coords=coords, name=variable)) + @staticmethod + def _logical_grid(*etas: Any) -> tuple[tuple[Any, Any, Any], tuple[str, ...], dict[str, np.ndarray]]: + """Normalize mixed logical-coordinate inputs for spline tensor-product evaluation.""" + arguments = [] + dims = [] + coords = {} + for dimension, eta in zip(("e1", "e2", "e3"), etas): + array = np.asarray(eta, dtype=float) + if array.ndim == 0: + arguments.append(float(array)) + elif array.ndim == 1: + if not array.size: + raise ValueError(f"{dimension} must contain at least one coordinate") + arguments.append(xp.asarray(array)) + dims.append(dimension) + coords[dimension] = array + else: + raise ValueError(f"{dimension} must be a scalar or one-dimensional coordinate sequence") + return tuple(arguments), tuple(dims), coords + + @staticmethod + def _reshape_spline_value(value: Any, shape: tuple[int, ...]) -> Any: + """Convert one squeezed spline result to the requested logical-grid shape.""" + if hasattr(value, "get"): + value = value.get() + array = np.asarray(value) + if not shape: + return array.item() + return array.reshape(shape) + @staticmethod def _snapshot_indices( selection: int | float | slice | Sequence[int] | None, times: np.ndarray, *, method: str | None, diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index e9456116a..2ed4f1b95 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -308,6 +308,23 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): assert last.sizes["t"] == 1 +def test_evaluate_raw_spline_field_on_mixed_logical_grid(run, monkeypatch): + class Field: + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): + e1, e2, e3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + value = e1 + 10 * e2 + 100 * e3 + return value.squeeze() if squeeze_out else value + + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) + + values = run.evaluate("em_fields/phi", eta1=[0.25, 0.5], eta2=range(2), eta3=0.75, t=0) + + assert values.dims == ("t", "e1", "e2") + np.testing.assert_allclose(values.e1, [0.25, 0.5]) + np.testing.assert_allclose(values.e2, [0.0, 1.0]) + np.testing.assert_allclose(values[0], [[75.25, 85.25], [75.5, 85.5]]) + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) From 23b0e16a3a4cf85c4c082184804ae299b171c21a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 15:30:33 +0200 Subject: [PATCH 111/193] enforce etas on logical unit cube --- src/struphy/post_processing/output.py | 2 ++ src/struphy/post_processing/tests/test_output.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 7c9e64627..406db2dac 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -405,6 +405,8 @@ def _logical_grid(*etas: Any) -> tuple[tuple[Any, Any, Any], tuple[str, ...], di coords = {} for dimension, eta in zip(("e1", "e2", "e3"), etas): array = np.asarray(eta, dtype=float) + if not np.all(np.isfinite(array)) or np.any((array < 0.0) | (array > 1.0)): + raise ValueError(f"{dimension} values must be finite and lie in the logical unit interval [0, 1]") if array.ndim == 0: arguments.append(float(array)) elif array.ndim == 1: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 2ed4f1b95..ef81ae001 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -325,6 +325,11 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): np.testing.assert_allclose(values[0], [[75.25, 85.25], [75.5, 85.5]]) +def test_evaluate_raw_spline_field_rejects_coordinates_outside_unit_cube(run): + with pytest.raises(ValueError, match="logical unit interval"): + run.evaluate("em_fields/phi", eta1=-0.01, eta2=0.5, eta3=0.5) + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) From f2dd7a9d152ea9467759e870b98e9f8bc0deb893 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 16:03:20 +0200 Subject: [PATCH 112/193] Added representation arg --- src/struphy/post_processing/output.py | 64 +++++--- .../post_processing/tests/test_output.py | 22 ++- tutorials/tutorial_post_processing.ipynb | 155 ++++++++++++------ 3 files changed, 163 insertions(+), 78 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 406db2dac..acd5cdd89 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -273,11 +273,11 @@ def evaluate( method: str | None = None, drop: bool = False, as_numpy: bool = False, - physical: Mapping[str, float] | None = None, t: int | float | slice | Sequence[int] | None = None, eta1: Any | None = None, eta2: Any | None = None, eta3: Any | None = None, + representation: str | None = None, **coordinates: Any, ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. @@ -294,41 +294,32 @@ def evaluate( A float ``t`` selects a time coordinate. Other keyword arguments select named coordinates, for example ``component=2`` or ``e1=0.5``. - ``physical={"X": x, "Y": y, "Z": z}`` evaluates a field at a physical point when - its domain supplies an analytical ``inverse_map``. It converts the point to logical - coordinates and uses xarray interpolation. - Supplying all of ``eta1``, ``eta2`` and ``eta3`` instead evaluates a raw FEEC spline field directly on that logical grid. Each eta can be a scalar, a list, a one-dimensional array, or a ``range``; mixed inputs form their tensor-product mesh internally. This reads coefficients one saved snapshot at a time and does not materialize a spatial post-processing product. Use a raw field name such as - ``"em_fields/e_field"``. + ``"em_fields/e_field"``. ``representation`` is applied after spline evaluation: + short kinds (``"0"``, ``"1"``, ``"2"``, ``"3"``, ``"v"``) push forward, and + ``"norm"`` transforms a normalized vector to Cartesian components. Use + ``"push:"``, ``"pull:"``, or ``"transform:"`` for an explicit + domain operation. Scalars default to ``"0"`` and vectors to ``"norm"``. """ selectors = dict(coordinates) + if "physical" in selectors: + raise TypeError("physical is no longer supported; use eta1, eta2, eta3 and representation") eta = (eta1, eta2, eta3) has_eta = any(value is not None for value in eta) if has_eta: if any(value is None for value in eta): raise ValueError("eta1, eta2, and eta3 must be supplied together") - if physical: - raise ValueError("physical and eta1/eta2/eta3 selections cannot be combined") - array = self._evaluate_spline_field(name, *eta, t=t, method=method) + array = self._evaluate_spline_field(name, *eta, t=t, method=method, representation=representation) t = None method = None - - physical_sel = None - if physical: - required = {"X", "Y", "Z"} - if set(physical) != required: - raise ValueError("physical selection requires exactly X, Y and Z") - inverse = getattr(self.domain, "inverse_map", None) - if inverse is None: - raise NotImplementedError(f"{type(self.domain).__name__} has no inverse_map for physical evaluation") - eta = inverse(*(float(physical[axis]) for axis in ("X", "Y", "Z"))) - physical_sel = dict(zip(("e1", "e2", "e3"), map(float, eta))) if not has_eta: + if representation is not None: + raise ValueError("representation requires eta1, eta2, and eta3") array = self._product(name) if t is not None: if isinstance(t, (int, np.integer)): @@ -343,8 +334,6 @@ def evaluate( selectors["t"] = [float(t)] else: raise TypeError("t must be a saved-snapshot index, index sequence, slice, or float time coordinate") - if physical_sel: - array = array.interp(physical_sel, method=method or "linear") if selectors: options = {"drop": drop} if method is not None: @@ -356,7 +345,7 @@ def evaluate( def _evaluate_spline_field( self, name: str, eta1: Any, eta2: Any, eta3: Any, *, t: int | float | slice | Sequence[int] | None, - method: str | None, + method: str | None, representation: str | None, ) -> xr.DataArray: """Evaluate one raw FEEC field on a tensor-product logical grid.""" try: @@ -382,7 +371,8 @@ def _evaluate_spline_field( except KeyError as error: available = tuple(f"{group}/{key}" for group, entries in fields.items() for key in entries) raise KeyError(f"{name!r} is not a saved raw FEEC field; available fields: {available}") from error - value = field(*etas, squeeze_out=True) + value = field(*etas, squeeze_out=False) + value = self._apply_representation(value, etas, representation) if isinstance(value, (list, tuple)): value = [self._reshape_spline_value(component, grid_shape) for component in value] else: @@ -397,6 +387,32 @@ def _evaluate_spline_field( coords["component"] = np.arange(data.shape[1]) return self._stamp(xr.DataArray(data, dims=dims, coords=coords, name=variable)) + def _apply_representation(self, value: Any, etas: tuple[Any, Any, Any], representation: str | None) -> Any: + """Apply one domain basis transformation to evaluated spline values.""" + is_vector = isinstance(value, (list, tuple)) + representation = representation or ("norm" if is_vector else "0") + if representation == "norm": + operation, kind, result_is_vector = "transform", "norm_to_v", True + elif representation.startswith(("push:", "pull:", "transform:")): + operation, kind = representation.split(":", maxsplit=1) + result_is_vector = kind not in {"0", "3", "0_to_3", "3_to_0"} + elif representation in {"0", "1", "2", "3", "v"}: + operation, kind = "push", representation + result_is_vector = kind in {"1", "2", "v"} + elif "_to_" in representation: + operation, kind = "transform", representation + result_is_vector = kind not in {"0_to_3", "3_to_0"} + else: + raise ValueError(f"unknown representation {representation!r}") + + try: + transformed = getattr(self.domain, operation)(value, *etas, kind=kind, squeeze_out=True) + except KeyError as error: + raise ValueError(f"{operation}:{kind} is not supported by {type(self.domain).__name__}") from error + if result_is_vector and not isinstance(transformed, (list, tuple)): + transformed = [transformed[component] for component in range(3)] + return transformed + @staticmethod def _logical_grid(*etas: Any) -> tuple[tuple[Any, Any, Any], tuple[str, ...], dict[str, np.ndarray]]: """Normalize mixed logical-coordinate inputs for spline tensor-product evaluation.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index ef81ae001..32c335f66 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -291,7 +291,7 @@ def test_evaluate_raw_spline_field_at_logical_point(run, monkeypatch): class Field: def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): calls.append((eta1, eta2, eta3)) - return [eta1, eta2, eta3] + return [np.full((1, 1, 1), eta1), np.full((1, 1, 1), eta2), np.full((1, 1, 1), eta3)] field = Field() monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"e_field": field}}) @@ -330,6 +330,26 @@ def test_evaluate_raw_spline_field_rejects_coordinates_outside_unit_cube(run): run.evaluate("em_fields/phi", eta1=-0.01, eta2=0.5, eta3=0.5) +def test_evaluate_raw_spline_field_applies_requested_representation(run, monkeypatch): + calls = [] + + class Field: + def __call__(self, *etas, **kwargs): + return np.ones((1, 1, 1)) + + class Domain: + def push(self, value, *etas, kind, squeeze_out): + calls.append((kind, etas, squeeze_out)) + return value + + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) + monkeypatch.setattr(run, "domain", Domain()) + + run.evaluate("em_fields/phi", eta1=0.5, eta2=0.5, eta3=0.5, t=0, representation="push:3") + + assert calls == [("3", (0.5, 0.5, 0.5), True)] + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index e17f801a4..c41af89a7 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -255,6 +255,55 @@ "cell_type": "markdown", "id": "15", "metadata": {}, + "source": [ + "### Evaluate a saved spline directly\n", + "\n", + "For a small number of logical points, `evaluate()` can read the saved FEEC coefficients and evaluate the spline directly, without creating a full post-processing field. Supply all three logical coordinates, each in the unit interval. Scalars, lists, NumPy arrays, and `range` objects can be mixed; non-scalar inputs form a tensor-product grid. The result remains an xarray array with a `t` coordinate.\n", + "\n", + "A representation conversion is applied after spline evaluation. Scalar fields default to the `\"0\"` push-forward and vector fields to the normalized-vector (`\"norm\"`) transformation. Use `representation=` to choose another domain operation explicitly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "eta1_line = np.linspace(0.0, 1.0, 128)\n", + "e_last = out.evaluate(\n", + " \"em_fields/e_field\",\n", + " eta1=eta1_line,\n", + " eta2=0.5,\n", + " eta3=0.5,\n", + " t=-1,\n", + ")\n", + "e_last\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# The raw 1-form representation at a two-dimensional logical grid.\n", + "e_one_form = out.evaluate(\n", + " \"em_fields/e_field\",\n", + " eta1=np.linspace(0.0, 1.0, 64),\n", + " eta2=range(2),\n", + " eta3=0.5,\n", + " t=-1,\n", + " representation=\"1\",\n", + ")\n", + "e_one_form\n" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, "source": [ "### Inspecting `out` itself\n", "\n", @@ -264,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -274,7 +323,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -284,7 +333,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -294,7 +343,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -303,7 +352,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "23", "metadata": {}, "source": [ "### Products are xarray arrays\n", @@ -314,7 +363,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -323,7 +372,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "25", "metadata": {}, "source": [ "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." @@ -331,7 +380,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "26", "metadata": {}, "source": [ "## Scalar overview and time series\n", @@ -344,7 +393,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -354,7 +403,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -374,7 +423,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "29", "metadata": {}, "source": [ "## Two-dimensional data\n", @@ -385,7 +434,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -400,7 +449,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "31", "metadata": {}, "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." @@ -409,7 +458,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -424,7 +473,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "33", "metadata": {}, "source": [ "## Interactive plots\n", @@ -435,7 +484,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -445,7 +494,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "35", "metadata": {}, "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." @@ -454,7 +503,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -463,7 +512,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "37", "metadata": {}, "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." @@ -472,7 +521,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -483,7 +532,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -493,7 +542,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "40", "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." @@ -502,7 +551,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -511,7 +560,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "42", "metadata": {}, "source": [ "## Derived quantities\n", @@ -522,7 +571,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "43", "metadata": {}, "outputs": [], "source": [ @@ -536,7 +585,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "44", "metadata": {}, "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." @@ -545,7 +594,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "45", "metadata": {}, "outputs": [], "source": [ @@ -558,7 +607,7 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "46", "metadata": {}, "source": [ "## Reducing distribution functions\n", @@ -569,7 +618,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "47", "metadata": {}, "outputs": [], "source": [ @@ -580,7 +629,7 @@ }, { "cell_type": "markdown", - "id": "45", + "id": "48", "metadata": {}, "source": [ "`.struphy.analysis.velocity_moments()` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean velocity `mean_v1` and the variance `variance_v1` along each velocity direction, all as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. For a `delta_f` product only the density (its perturbation) is returned, because a mean and variance of a perturbation are not defined. Where the density is not positive, mean and variance are NaN." @@ -589,7 +638,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "49", "metadata": {}, "outputs": [], "source": [ @@ -605,7 +654,7 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "50", "metadata": {}, "source": [ "## Physical units\n", @@ -616,7 +665,7 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "51", "metadata": {}, "outputs": [], "source": [ @@ -629,7 +678,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "52", "metadata": {}, "source": [ "## Save standard output\n", @@ -640,7 +689,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "53", "metadata": {}, "outputs": [], "source": [ @@ -652,7 +701,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "54", "metadata": {}, "source": [ "## Comparing runs\n", @@ -663,7 +712,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -686,7 +735,7 @@ }, { "cell_type": "markdown", - "id": "53", + "id": "56", "metadata": {}, "source": [ "## Profiling\n", @@ -697,7 +746,7 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "57", "metadata": {}, "outputs": [], "source": [ @@ -709,7 +758,7 @@ }, { "cell_type": "markdown", - "id": "55", + "id": "58", "metadata": {}, "source": [ "`compare()` puts the same statistic of several runs side by side, with runs whose region is missing as NaN. Here the two runs of the previous section differ only in the time step, so the number of calls per propagator halves for `dt = 0.1`." @@ -718,7 +767,7 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -728,7 +777,7 @@ }, { "cell_type": "markdown", - "id": "57", + "id": "60", "metadata": {}, "source": [ "## Other models\n", @@ -738,7 +787,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "61", "metadata": {}, "source": [ "### SPH densities\n", @@ -749,7 +798,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "62", "metadata": {}, "outputs": [], "source": [ @@ -787,7 +836,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "63", "metadata": {}, "source": [ "For a one-dimensional run, the clearest picture is a space-time map: the sweep dimension `t` may be used as a display axis." @@ -796,7 +845,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -806,7 +855,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "65", "metadata": {}, "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" @@ -815,7 +864,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -824,7 +873,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "67", "metadata": {}, "source": [ "### Vector fields on a mapped domain\n", @@ -835,7 +884,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -866,7 +915,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -885,7 +934,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -904,7 +953,7 @@ }, { "cell_type": "markdown", - "id": "68", + "id": "71", "metadata": {}, "source": [ "## Apply the workflow to another run\n", From 3a4f6e926cc81b5beda90736636e45467e6a64d9 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 16:07:20 +0200 Subject: [PATCH 113/193] Update tutorial --- tutorials/tutorial_post_processing.ipynb | 125 +++++++++++++---------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index c41af89a7..513ba7a4a 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -277,14 +277,29 @@ " eta2=0.5,\n", " eta3=0.5,\n", " t=-1,\n", + " representation=\"norm\", # the default for vector spline fields\n", ")\n", "e_last\n" ] }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "The `representation` argument is deliberately close to the domain API:\n", + "\n", + "- `representation=\"0\"`, `\"1\"`, `\"2\"`, `\"3\"`, or `\"v\"` applies that push-forward.\n", + "- `representation=\"norm\"` applies the `norm_to_v` transformation.\n", + "- `representation=\"push:1\"`, `\"pull:v\"`, or `\"transform:1_to_2\"` selects the corresponding domain method explicitly.\n", + "\n", + "Below, `e_last` is the normalized-vector-to-Cartesian result, while `e_one_form` retains the pushed-forward 1-form representation." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -302,7 +317,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "19", "metadata": {}, "source": [ "### Inspecting `out` itself\n", @@ -313,7 +328,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -323,7 +338,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -333,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -343,7 +358,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -352,7 +367,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "24", "metadata": {}, "source": [ "### Products are xarray arrays\n", @@ -363,7 +378,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -372,7 +387,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." @@ -380,7 +395,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "27", "metadata": {}, "source": [ "## Scalar overview and time series\n", @@ -393,7 +408,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -403,7 +418,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -423,7 +438,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "## Two-dimensional data\n", @@ -434,7 +449,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -449,7 +464,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." @@ -458,7 +473,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -473,7 +488,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "## Interactive plots\n", @@ -484,7 +499,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -494,7 +509,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "36", "metadata": {}, "source": [ "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." @@ -503,7 +518,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -512,7 +527,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "38", "metadata": {}, "source": [ "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." @@ -521,7 +536,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -532,7 +547,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -542,7 +557,7 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "41", "metadata": {}, "source": [ "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." @@ -551,7 +566,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -560,7 +575,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "## Derived quantities\n", @@ -571,7 +586,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -585,7 +600,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "45", "metadata": {}, "source": [ "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." @@ -594,7 +609,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -607,7 +622,7 @@ }, { "cell_type": "markdown", - "id": "46", + "id": "47", "metadata": {}, "source": [ "## Reducing distribution functions\n", @@ -618,7 +633,7 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -629,7 +644,7 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ "`.struphy.analysis.velocity_moments()` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean velocity `mean_v1` and the variance `variance_v1` along each velocity direction, all as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. For a `delta_f` product only the density (its perturbation) is returned, because a mean and variance of a perturbation are not defined. Where the density is not positive, mean and variance are NaN." @@ -638,7 +653,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -654,7 +669,7 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "51", "metadata": {}, "source": [ "## Physical units\n", @@ -665,7 +680,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "52", "metadata": {}, "outputs": [], "source": [ @@ -678,7 +693,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "53", "metadata": {}, "source": [ "## Save standard output\n", @@ -689,7 +704,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -701,7 +716,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "55", "metadata": {}, "source": [ "## Comparing runs\n", @@ -712,7 +727,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "56", "metadata": {}, "outputs": [], "source": [ @@ -735,7 +750,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "57", "metadata": {}, "source": [ "## Profiling\n", @@ -746,7 +761,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -758,7 +773,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "59", "metadata": {}, "source": [ "`compare()` puts the same statistic of several runs side by side, with runs whose region is missing as NaN. Here the two runs of the previous section differ only in the time step, so the number of calls per propagator halves for `dt = 0.1`." @@ -767,7 +782,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "60", "metadata": {}, "outputs": [], "source": [ @@ -777,7 +792,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "61", "metadata": {}, "source": [ "## Other models\n", @@ -787,7 +802,7 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "62", "metadata": {}, "source": [ "### SPH densities\n", @@ -798,7 +813,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -836,7 +851,7 @@ }, { "cell_type": "markdown", - "id": "63", + "id": "64", "metadata": {}, "source": [ "For a one-dimensional run, the clearest picture is a space-time map: the sweep dimension `t` may be used as a display axis." @@ -845,7 +860,7 @@ { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -855,7 +870,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "66", "metadata": {}, "source": [ "Products are plain `xarray.DataArray` objects, so anything xarray can do works directly, for example profiles at selected times:" @@ -864,7 +879,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -873,7 +888,7 @@ }, { "cell_type": "markdown", - "id": "67", + "id": "68", "metadata": {}, "source": [ "### Vector fields on a mapped domain\n", @@ -884,7 +899,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -915,7 +930,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -934,7 +949,7 @@ { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -953,7 +968,7 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "72", "metadata": {}, "source": [ "## Apply the workflow to another run\n", From 460d0beecfc7d38ac6a5de239db4b75ceb159a89 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 16:16:52 +0200 Subject: [PATCH 114/193] Cache the spline coeffients for each called t --- src/struphy/post_processing/output.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index acd5cdd89..f067425d6 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -263,8 +263,7 @@ def _reset(self): self._species = None self._seconds = None self._spline_derham = None - self._spline_fields = None - self._spline_snapshot = None + self._spline_snapshots = {} def evaluate( self, @@ -2196,11 +2195,12 @@ def iter_spline_coefficients(self, *, stride: int = 1, rank: int = 0): def spline_fields(self, *, t: int) -> dict: """Return FEEC ``SplineFunction`` objects loaded at saved index ``t``. - The spline functions are allocated once and reused. Requesting the same - ``t`` performs no HDF5 reads; requesting another index overwrites - their coefficients in place. Copy evaluated values before requesting a - different index. As with :meth:`evaluate`, ``t=0`` is the first saved - snapshot and ``t=-1`` is the last. + Every requested snapshot is retained in an in-memory cache. Requesting + an already loaded ``t`` performs no HDF5 reads; requesting a new one + allocates and fills one additional set of spline functions without + altering earlier snapshots. The cache therefore grows with the number + of requested snapshots. As with :meth:`evaluate`, ``t=0`` is the first + saved snapshot and ``t=-1`` is the last. The returned mapping is ``species -> variable -> SplineFunction``. It is intentionally separate from :meth:`evaluate`, which serves persisted @@ -2221,9 +2221,10 @@ def spline_fields(self, *, t: int) -> dict: if not 0 <= t < n_snapshots: raise IndexError(f"t={t} is outside the saved snapshot range") - if self._spline_fields is None: + if self._spline_derham is None: self._spline_derham = Derham(self.grid, self.derham_opts, comm=None, domain=self.domain) - self._spline_fields = { + if t not in self._spline_snapshots: + fields = { species_name: { variable_name: self._spline_derham.create_spline_function( variable_name, variable.attrs["space_id"] @@ -2233,15 +2234,15 @@ def spline_fields(self, *, t: int) -> dict: for species_name, species in file["feec"].items() } - if t != self._spline_snapshot: + if t not in self._spline_snapshots: with ExitStack() as stack: files = [ stack.enter_context(h5py.File(self.path_out / "data" / f"data_proc{rank}.hdf5")) for rank in range(self.mpi_ranks) ] - self._load_femfields(self._spline_fields, files, t) - self._spline_snapshot = t - return self._spline_fields + self._load_femfields(fields, files, t) + self._spline_snapshots[t] = fields + return self._spline_snapshots[t] @property def label(self) -> str: From 7e7b0509c312236added7c00173af027a042e7b6 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Thu, 24 Sep 2026 16:18:36 +0200 Subject: [PATCH 115/193] added manifest.py --- src/struphy/post_processing/manifest.py | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/struphy/post_processing/manifest.py diff --git a/src/struphy/post_processing/manifest.py b/src/struphy/post_processing/manifest.py new file mode 100644 index 000000000..7615aa033 --- /dev/null +++ b/src/struphy/post_processing/manifest.py @@ -0,0 +1,52 @@ +"""Manifest fingerprints and processing option comparison.""" + +import hashlib +import json +import os + + +MANIFEST_SCHEMA_VERSION = 1 + + +def source_fingerprint(path_out: str) -> str: + """Fingerprint the raw run files that determine post-processing products.""" + digest = hashlib.sha256() + for name in ("config.json", "run_metadata.json", "meta.yml", "data/data_proc0.hdf5"): + path = os.path.join(path_out, name) + if not os.path.exists(path): + continue + stat = os.stat(path) + digest.update(name.encode()) + digest.update(f"{stat.st_size}:{stat.st_mtime_ns}".encode()) + if name != "data/data_proc0.hdf5": + with open(path, "rb") as stream: + digest.update(stream.read()) + return digest.hexdigest() + + +def normalize_options(**options) -> dict: + """JSON-comparable processing options, as stored in the manifest.""" + celldivide = options.get("celldivide") + if celldivide is not None: + options["celldivide"] = [int(celldivide)] * 3 if isinstance(celldivide, int) else [int(c) for c in celldivide] + return options + + +def is_processed(path_out: str, options: dict | None = None) -> bool: + """Whether ``path_out`` holds complete post-processing of its current raw output. + + With ``options``, the stored processing options must match as well, so a request for + different products (e.g. ``physical=True``) is never answered with stale ones. + """ + path = os.path.join(path_out, "post_processing", "manifest.json") + try: + with open(path) as stream: + manifest = json.load(stream) + except (OSError, ValueError): + return False + return ( + manifest.get("schema_version") == MANIFEST_SCHEMA_VERSION + and manifest.get("status") == "complete" + and manifest.get("source_fingerprint") == source_fingerprint(path_out) + and (options is None or manifest.get("options") == normalize_options(**options)) + ) From 1d17c21daa6b0b1cfbca385afb01c9969f93b26d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 16:50:11 +0200 Subject: [PATCH 116/193] =?UTF-8?q?Output.evaluate()=20now=20infers=20the?= =?UTF-8?q?=20source=20representation=20from=20the=20saved=20field?= =?UTF-8?q?=E2=80=99s=20FEEC=20space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/struphy/geometry/base.py | 5 +- src/struphy/geometry/transform_kernels.py | 39 ++++++++++- src/struphy/post_processing/output.py | 50 ++++++-------- .../post_processing/tests/test_output.py | 12 +++- tutorials/tutorial_post_processing.ipynb | 69 +++++++++++++++++-- 5 files changed, 133 insertions(+), 42 deletions(-) diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index b2f9b3ccb..83765cfbd 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -220,6 +220,9 @@ def __init__( "v_to_2": 16, "1_to_v": 17, "2_to_v": 18, + "1_to_norm": 19, + "2_to_norm": 20, + "v_to_norm": 21, } self._dict_transformations = { @@ -1010,7 +1013,7 @@ def transform( Notes ----- - Possible choices for kind are '0_to_3', '3_to_0', '1_to_2', '2_to_1', 'norm_to_v', 'norm_to_1', 'norm_to_2', 'v_to_1', 'v_to_2', '1_to_v' and '2_to_v'. + Possible choices for kind are '0_to_3', '3_to_0', '1_to_2', '2_to_1', 'norm_to_v', 'norm_to_1', 'norm_to_2', 'v_to_1', 'v_to_2', '1_to_v', '2_to_v', '1_to_norm', '2_to_norm' and 'v_to_norm'. """ return self._pull_push_transform( diff --git a/src/struphy/geometry/transform_kernels.py b/src/struphy/geometry/transform_kernels.py index f9e6d8077..c81edd665 100644 --- a/src/struphy/geometry/transform_kernels.py +++ b/src/struphy/geometry/transform_kernels.py @@ -41,6 +41,12 @@ - 1-form --> vector : (a_1, a_2, a_3) = G^(-1) * (a^1_1, a^1_2, a^1_3) - 2-form --> vector : (a_1, a_2, a_3) = (a^2_1, a^2_2, a^2_3) / |det(DF)| + +Let h_i = ||DF[:, i]||. The normalized-vector representation stores a^*_i = h_i a_i. +Consequently, the inverse transformations added here are 1-form --> norm, 2-form --> norm, +and vector --> norm: each first obtains the Cartesian vector and then multiplies component i +by h_i. These are used when :meth:`Domain.transform` converts a saved FEEC field to the +``"norm"`` representation. """ from numpy import empty, shape, sqrt, zeros @@ -204,7 +210,15 @@ def tran( Logical evaluation points. kind_fun : int - Which transformation to be performed. + Which transformation to perform. The values are assigned by + ``Domain.dict_transformations["tran"]``: + + - 0, 1: ``0_to_3``, ``3_to_0`` + - 10, 11: ``1_to_2``, ``2_to_1`` + - 12, 13, 14: ``norm_to_v``, ``norm_to_1``, ``norm_to_2`` + - 15, 16: ``v_to_1``, ``v_to_2`` + - 17, 18: ``1_to_v``, ``2_to_v`` + - 19, 20, 21: ``1_to_norm``, ``2_to_norm``, ``v_to_norm`` args_domain : DomainArguments Domain info. @@ -290,6 +304,29 @@ def tran( elif kind_fun == 18: out[:] = a / abs(detdf) + # 1-form to normalized vector + elif kind_fun == 19: + linalg_kernels.matrix_inv_with_det(dfmat1, detdf, dfmat2) + linalg_kernels.transpose(dfmat2, dfmat3) + linalg_kernels.matrix_vector(dfmat3, a, vec1) + linalg_kernels.matrix_vector(dfmat2, vec1, out) + out[0] = out[0] * sqrt(dfmat1[0, 0] ** 2 + dfmat1[1, 0] ** 2 + dfmat1[2, 0] ** 2) + out[1] = out[1] * sqrt(dfmat1[0, 1] ** 2 + dfmat1[1, 1] ** 2 + dfmat1[2, 1] ** 2) + out[2] = out[2] * sqrt(dfmat1[0, 2] ** 2 + dfmat1[1, 2] ** 2 + dfmat1[2, 2] ** 2) + + # 2-form to normalized vector + elif kind_fun == 20: + out[:] = a / abs(detdf) + out[0] = out[0] * sqrt(dfmat1[0, 0] ** 2 + dfmat1[1, 0] ** 2 + dfmat1[2, 0] ** 2) + out[1] = out[1] * sqrt(dfmat1[0, 1] ** 2 + dfmat1[1, 1] ** 2 + dfmat1[2, 1] ** 2) + out[2] = out[2] * sqrt(dfmat1[0, 2] ** 2 + dfmat1[1, 2] ** 2 + dfmat1[2, 2] ** 2) + + # Cartesian vector to normalized vector + elif kind_fun == 21: + out[0] = a[0] * sqrt(dfmat1[0, 0] ** 2 + dfmat1[1, 0] ** 2 + dfmat1[2, 0] ** 2) + out[1] = a[1] * sqrt(dfmat1[0, 1] ** 2 + dfmat1[1, 1] ** 2 + dfmat1[2, 1] ** 2) + out[2] = a[2] * sqrt(dfmat1[0, 2] ** 2 + dfmat1[1, 2] ** 2 + dfmat1[2, 2] ** 2) + @stack_array("tmp1", "tmp2") def kernel_pullpush( diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index f067425d6..425aa5857 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -12,7 +12,7 @@ from functools import cached_property from html import escape from pathlib import Path -from typing import Any +from typing import Any, Literal import h5py import cunumpy as xp @@ -41,6 +41,7 @@ # Push-forward of each de Rham space to Cartesian components, see Domain.push. PUSH_KINDS = {"H1": "0", "Hcurl": "1", "Hdiv": "2", "L2": "3", "H1vec": "v"} +Representation = Literal["0", "1", "2", "3", "v", "norm"] def mpi_comm_world(): @@ -276,7 +277,7 @@ def evaluate( eta1: Any | None = None, eta2: Any | None = None, eta3: Any | None = None, - representation: str | None = None, + representation: Representation | None = None, **coordinates: Any, ) -> xr.DataArray | np.ndarray: """Return a named simulation product as an :class:`xarray.DataArray`. @@ -298,11 +299,10 @@ def evaluate( a one-dimensional array, or a ``range``; mixed inputs form their tensor-product mesh internally. This reads coefficients one saved snapshot at a time and does not materialize a spatial post-processing product. Use a raw field name such as - ``"em_fields/e_field"``. ``representation`` is applied after spline evaluation: - short kinds (``"0"``, ``"1"``, ``"2"``, ``"3"``, ``"v"``) push forward, and - ``"norm"`` transforms a normalized vector to Cartesian components. Use - ``"push:"``, ``"pull:"``, or ``"transform:"`` for an explicit - domain operation. Scalars default to ``"0"`` and vectors to ``"norm"``. + ``"em_fields/e_field"``. ``representation`` selects the output representation + after spline evaluation: one of ``"0"``, ``"1"``, ``"2"``, ``"3"``, ``"v"``, or + ``"norm"``. The input representation is inferred from the field's FEEC space. + Scalars default to ``"0"`` and vectors to ``"norm"``. """ selectors = dict(coordinates) if "physical" in selectors: @@ -344,7 +344,7 @@ def evaluate( def _evaluate_spline_field( self, name: str, eta1: Any, eta2: Any, eta3: Any, *, t: int | float | slice | Sequence[int] | None, - method: str | None, representation: str | None, + method: str | None, representation: Representation | None, ) -> xr.DataArray: """Evaluate one raw FEEC field on a tensor-product logical grid.""" try: @@ -371,7 +371,7 @@ def _evaluate_spline_field( available = tuple(f"{group}/{key}" for group, entries in fields.items() for key in entries) raise KeyError(f"{name!r} is not a saved raw FEEC field; available fields: {available}") from error value = field(*etas, squeeze_out=False) - value = self._apply_representation(value, etas, representation) + value = self._apply_representation(value, etas, PUSH_KINDS[field.space_id], representation) if isinstance(value, (list, tuple)): value = [self._reshape_spline_value(component, grid_shape) for component in value] else: @@ -386,29 +386,19 @@ def _evaluate_spline_field( coords["component"] = np.arange(data.shape[1]) return self._stamp(xr.DataArray(data, dims=dims, coords=coords, name=variable)) - def _apply_representation(self, value: Any, etas: tuple[Any, Any, Any], representation: str | None) -> Any: - """Apply one domain basis transformation to evaluated spline values.""" - is_vector = isinstance(value, (list, tuple)) - representation = representation or ("norm" if is_vector else "0") - if representation == "norm": - operation, kind, result_is_vector = "transform", "norm_to_v", True - elif representation.startswith(("push:", "pull:", "transform:")): - operation, kind = representation.split(":", maxsplit=1) - result_is_vector = kind not in {"0", "3", "0_to_3", "3_to_0"} - elif representation in {"0", "1", "2", "3", "v"}: - operation, kind = "push", representation - result_is_vector = kind in {"1", "2", "v"} - elif "_to_" in representation: - operation, kind = "transform", representation - result_is_vector = kind not in {"0_to_3", "3_to_0"} - else: - raise ValueError(f"unknown representation {representation!r}") - + def _apply_representation( + self, value: Any, etas: tuple[Any, Any, Any], source: str, representation: Representation | None, + ) -> Any: + """Transform a field from its FEEC-space representation to the requested target.""" + target = representation or ("norm" if source in {"1", "2", "v"} else "0") + if target == source: + return value + transformation = f"{source}_to_{target}" try: - transformed = getattr(self.domain, operation)(value, *etas, kind=kind, squeeze_out=True) + transformed = self.domain.transform(value, *etas, kind=transformation, squeeze_out=True) except KeyError as error: - raise ValueError(f"{operation}:{kind} is not supported by {type(self.domain).__name__}") from error - if result_is_vector and not isinstance(transformed, (list, tuple)): + raise ValueError(f"cannot transform {source!r} fields to representation {target!r}") from error + if target not in {"0", "3"} and not isinstance(transformed, (list, tuple)): transformed = [transformed[component] for component in range(3)] return transformed diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 32c335f66..b3e0ded05 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -289,6 +289,8 @@ def test_evaluate_raw_spline_field_at_logical_point(run, monkeypatch): calls = [] class Field: + space_id = "H1vec" + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): calls.append((eta1, eta2, eta3)) return [np.full((1, 1, 1), eta1), np.full((1, 1, 1), eta2), np.full((1, 1, 1), eta3)] @@ -310,6 +312,8 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): def test_evaluate_raw_spline_field_on_mixed_logical_grid(run, monkeypatch): class Field: + space_id = "H1" + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): e1, e2, e3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") value = e1 + 10 * e2 + 100 * e3 @@ -334,20 +338,22 @@ def test_evaluate_raw_spline_field_applies_requested_representation(run, monkeyp calls = [] class Field: + space_id = "L2" + def __call__(self, *etas, **kwargs): return np.ones((1, 1, 1)) class Domain: - def push(self, value, *etas, kind, squeeze_out): + def transform(self, value, *etas, kind, squeeze_out): calls.append((kind, etas, squeeze_out)) return value monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) monkeypatch.setattr(run, "domain", Domain()) - run.evaluate("em_fields/phi", eta1=0.5, eta2=0.5, eta3=0.5, t=0, representation="push:3") + run.evaluate("em_fields/phi", eta1=0.5, eta2=0.5, eta3=0.5, t=0, representation="0") - assert calls == [("3", (0.5, 0.5, 0.5), True)] + assert calls == [("3_to_0", (0.5, 0.5, 0.5), True)] def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 513ba7a4a..1adb9362c 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -260,7 +260,7 @@ "\n", "For a small number of logical points, `evaluate()` can read the saved FEEC coefficients and evaluate the spline directly, without creating a full post-processing field. Supply all three logical coordinates, each in the unit interval. Scalars, lists, NumPy arrays, and `range` objects can be mixed; non-scalar inputs form a tensor-product grid. The result remains an xarray array with a `t` coordinate.\n", "\n", - "A representation conversion is applied after spline evaluation. Scalar fields default to the `\"0\"` push-forward and vector fields to the normalized-vector (`\"norm\"`) transformation. Use `representation=` to choose another domain operation explicitly." + "A representation conversion is applied after spline evaluation. Its input is inferred from the saved FEEC space, so `representation=` specifies only the target: `\"0\"`, `\"1\"`, `\"2\"`, `\"3\"`, `\"v\"`, or `\"norm\"`. Scalars default to `\"0\"`; vectors default to `\"norm\"`." ] }, { @@ -287,13 +287,13 @@ "id": "17", "metadata": {}, "source": [ - "The `representation` argument is deliberately close to the domain API:\n", + "The `representation` argument names the target representation:\n", "\n", - "- `representation=\"0\"`, `\"1\"`, `\"2\"`, `\"3\"`, or `\"v\"` applies that push-forward.\n", - "- `representation=\"norm\"` applies the `norm_to_v` transformation.\n", - "- `representation=\"push:1\"`, `\"pull:v\"`, or `\"transform:1_to_2\"` selects the corresponding domain method explicitly.\n", + "- The saved field's FEEC space supplies the source (`0`, `1`, `2`, `3`, or `v`).\n", + "- `representation=\"norm\"` on an H(curl) field uses `1_to_norm`; `representation=\"2\"` uses `1_to_2`.\n", + "- `representation=\"1\"` below keeps this electric field in its native H(curl) representation.\n", "\n", - "Below, `e_last` is the normalized-vector-to-Cartesian result, while `e_one_form` retains the pushed-forward 1-form representation." + "Below, `e_last` is in the normalized-vector representation, while `e_one_form` retains the native 1-form representation." ] }, { @@ -303,7 +303,7 @@ "metadata": {}, "outputs": [], "source": [ - "# The raw 1-form representation at a two-dimensional logical grid.\n", + "# The native 1-form representation at a two-dimensional logical grid.\n", "e_one_form = out.evaluate(\n", " \"em_fields/e_field\",\n", " eta1=np.linspace(0.0, 1.0, 64),\n", @@ -970,6 +970,61 @@ "cell_type": "markdown", "id": "72", "metadata": {}, + "source": [ + "### Representation conversion on a torus\n", + "\n", + "A toroidal map makes the distinction between FEEC representations visible. The saved electric field is an H(curl) 1-form, so its source representation is inferred as `1`. We evaluate one poloidal line directly from saved spline coefficients, then request its native 1-form, normalized-vector, and Cartesian-vector representations. These differ away from a Cartesian map because the metric factors vary around the torus." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73", + "metadata": {}, + "outputs": [], + "source": [ + "torus_model = Maxwell()\n", + "torus_model.em_fields.e_field.save_data = True\n", + "torus_model.em_fields.e_field.add_perturbation(\n", + " perturbations.ModesCos(ms=(1,), amps=(0.1,), given_in_basis=\"1\", comp=1)\n", + ")\n", + "\n", + "torus = Simulation(\n", + " model=torus_model,\n", + " env=EnvironmentOptions(out_folders=demo_root, sim_folder=\"representation_torus\", save_restart=False),\n", + " time_opts=Time(dt=0.05, Tend=0.05),\n", + " domain=domains.HollowTorus(a1=0.2, a2=0.4, R0=1.0, tor_period=1),\n", + " equil=equils.HomogenSlab(),\n", + " grid=grids.TensorProductGrid(num_elements=(6, 12, 2)),\n", + " derham_opts=DerhamOptions(degree=(2, 2, 2), bcs=((\"dirichlet\", \"dirichlet\"), None, None)),\n", + ")\n", + "out_torus = torus.run()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74", + "metadata": {}, + "outputs": [], + "source": [ + "eta2_line = np.linspace(0.0, 1.0, 256)\n", + "common = dict(eta1=0.5, eta2=eta2_line, eta3=0.0, t=-1)\n", + "e_1 = out_torus.evaluate(\"em_fields/e_field\", representation=\"1\", **common)\n", + "e_norm = out_torus.evaluate(\"em_fields/e_field\", representation=\"norm\", **common)\n", + "e_v = out_torus.evaluate(\"em_fields/e_field\", representation=\"v\", **common)\n", + "\n", + "fig, ax = plt.subplots()\n", + "for field, label in ((e_1, \"1-form\"), (e_norm, \"normalized vector\"), (e_v, \"Cartesian vector\")):\n", + " ax.plot(eta2_line, field.isel(t=0, component=1), label=label)\n", + "ax.set(xlabel=r\"$\\eta_2$\", ylabel=\"component 2\", title=\"H(curl) field representations on a torus\")\n", + "ax.legend();\n" + ] + }, + { + "cell_type": "markdown", + "id": "75", + "metadata": {}, "source": [ "## Apply the workflow to another run\n", "\n", From 9a018664839eeedaa84c68c0fa0b1222689ab83a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 17:10:01 +0200 Subject: [PATCH 117/193] updat tutorial --- tutorials/tutorial_post_processing.ipynb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 1adb9362c..e3d9aa22e 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -1009,16 +1009,22 @@ "outputs": [], "source": [ "eta2_line = np.linspace(0.0, 1.0, 256)\n", - "common = dict(eta1=0.5, eta2=eta2_line, eta3=0.0, t=-1)\n", + "component=0\n", + "common = dict(eta1=0.7, eta2=eta2_line, eta3=0.0, t=-1, component=component)\n", "e_1 = out_torus.evaluate(\"em_fields/e_field\", representation=\"1\", **common)\n", "e_norm = out_torus.evaluate(\"em_fields/e_field\", representation=\"norm\", **common)\n", "e_v = out_torus.evaluate(\"em_fields/e_field\", representation=\"v\", **common)\n", "\n", - "fig, ax = plt.subplots()\n", - "for field, label in ((e_1, \"1-form\"), (e_norm, \"normalized vector\"), (e_v, \"Cartesian vector\")):\n", - " ax.plot(eta2_line, field.isel(t=0, component=1), label=label)\n", - "ax.set(xlabel=r\"$\\eta_2$\", ylabel=\"component 2\", title=\"H(curl) field representations on a torus\")\n", - "ax.legend();\n" + "fig, ax = plt.subplots(ncols=3, figsize=(15, 5))\n", + "i = 0\n", + "for field, label in ((e_1, \"1-form\"), (e_norm, \"normalized vector\"), (e_v, \"Vector field\")):\n", + " print(label, field.isel(t=0))\n", + " ax[i].plot(eta2_line, field.isel(t=0), label=label)\n", + " \n", + " ax[i].set(xlabel=r\"$\\eta_2$\", ylabel=label, title=\"H(curl) field representations on a torus\")\n", + " i += 1\n", + "# ax.legend()\n", + "\n" ] }, { From 374074bde11a4c2c1d5490c539640425a65e02b6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 17:40:31 +0200 Subject: [PATCH 118/193] Added more tests --- .../post_processing/tests/test_output.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index b3e0ded05..c9af575cb 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -356,6 +356,44 @@ def transform(self, value, *etas, kind, squeeze_out): assert calls == [("3_to_0", (0.5, 0.5, 0.5), True)] +@pytest.mark.parametrize( + "domain", + [ + pytest.param(domains.Cuboid(), id="cuboid"), + pytest.param(domains.HollowTorus(a1=0.2, a2=0.4, R0=1.0, tor_period=1), id="hollow-torus"), + pytest.param(domains.Colella(), id="non-orthogonal-colella"), + ], +) +def test_evaluate_transforms_hcurl_fields_on_mapped_domains(run, monkeypatch, domain): + """Raw evaluation uses the field's H(curl) source representation on every domain.""" + eta1 = np.linspace(0.2, 0.8, 4) + eta2 = np.linspace(0.1, 0.9, 5) + eta3 = 0.25 + + class Field: + space_id = "Hcurl" + + def __call__(self, e1, e2, e3, *, squeeze_out=False): + e1, e2, e3 = np.meshgrid(e1, e2, e3, indexing="ij") + return [1.0 + e1, 2.0 + e2, 3.0 + e3] + + field = Field() + monkeypatch.setattr(run, "domain", domain) + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"e_field": field}}) + + source = field(eta1, eta2, eta3) + for target in ("1", "2", "v", "norm"): + result = run.evaluate( + "em_fields/e_field", eta1=eta1, eta2=eta2, eta3=eta3, t=0, representation=target, + ) + expected = source if target == "1" else domain.transform( + source, eta1, eta2, eta3, kind=f"1_to_{target}", squeeze_out=True, + ) + expected = np.squeeze(np.asarray(expected)) + np.testing.assert_allclose(result.isel(t=0), expected) + assert result.dims == ("t", "component", "e1", "e2") + + def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) From 66f8bfc05ca6b34a5542e6837f2c852b7f40eb5b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 18:46:27 +0200 Subject: [PATCH 119/193] Moved plotting scripts to a separate postprocessing_external folder --- doc/markdown/output-api.md | 14 +- doc/sections/userguide.rst | 18 +- .../cyclone/pproc_cyclone.py | 11 +- .../itg_cylindre/pproc_drift_kinetic.py | 11 +- .../diocotron_instability/pproc_diocotron.py | 4 +- .../bump_on/pproc_bump_on.py | 1 + .../pproc_strong_Landau_damping.py | 1 + .../two_stream/pproc_two_stream.py | 4 +- .../pproc_weak_Landau_damping.py | 1 + .../pproc_weibel_instability.py | 1 + src/struphy/diagnostics/analysis.py | 193 ----- src/struphy/diagnostics/diagn_tools.py | 16 +- src/struphy/diagnostics/plotting.py | 664 ------------------ .../diagnostics/tests/test_plotting.py | 285 -------- .../verification/test_verif_LinearMHD.py | 7 +- .../tests/verification/test_verif_Maxwell.py | 4 +- src/struphy/post_processing/output.py | 140 +--- .../post_processing/output_accessors.py | 52 -- .../tests/test_derived_products.py | 301 -------- .../post_processing/tests/test_output.py | 10 +- .../tests/test_output_accessors.py | 249 ------- .../post_processing/xarray_accessors.py | 450 ------------ 22 files changed, 58 insertions(+), 2379 deletions(-) delete mode 100644 src/struphy/diagnostics/analysis.py delete mode 100644 src/struphy/diagnostics/plotting.py delete mode 100644 src/struphy/diagnostics/tests/test_plotting.py delete mode 100644 src/struphy/post_processing/output_accessors.py delete mode 100644 src/struphy/post_processing/tests/test_derived_products.py delete mode 100644 src/struphy/post_processing/tests/test_output_accessors.py delete mode 100644 src/struphy/post_processing/xarray_accessors.py diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index c42061088..f5297d8f6 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -139,7 +139,7 @@ averages over the logical space dimensions `e1`, `e2` and `e3` (or the ones pass so an `e1_v1` product becomes f(v1, t). The mean is uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a mapped domain it is not weighted by the Jacobian. -`velocity_moments` integrates over the velocity dimensions instead and returns a dataset with the +`struphy_plots.analysis.velocity_moments` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean `mean_v1` and variance `variance_v1` along every velocity direction, as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. Mean and variance are NaN where the density is not positive, and a `delta_f` product @@ -148,13 +148,15 @@ has only the density (its perturbation). ```python f = "kinetic_ions/e1_v1_density/f" -f_of_v = out.spatial_average(f) # dimensions (t, v1) -moments = out.velocity_moments(f) # density, mean_v1, variance_v1 over (t, e1) -temperature_over_mass = out.spatial_average(moments.variance_v1) +from struphy_plots.analysis import spatial_average, velocity_moments + +f_of_v = spatial_average(out.evaluate(f)) # dimensions (t, v1) +moments = velocity_moments(out.evaluate(f)) # density, mean_v1, variance_v1 over (t, e1) +temperature_over_mass = spatial_average(moments.variance_v1) ``` -Both are also available on any product as `array.struphy.analysis.spatial_average()` and -`array.struphy.analysis.velocity_moments()`. +Importing `struphy_plots` additionally registers the optional +`array.struphy.analysis` accessor. ## Convert to SI units diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 15c9ddaf9..080b973b2 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -581,13 +581,14 @@ serial processing runs on rank 0 while the other ranks wait, and Standard plots and analysis ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Products sit under the species that produced them and plot themselves through their -``.struphy`` accessor, which holds ``.plot`` and ``.analysis``. No imports are needed and -everything completes as you type: +Plotting and derived diagnostics are optional and live in the separate +``struphy-plots`` package. Install it separately, then import it to register its +optional xarray accessor: .. code-block:: python - # products plot themselves + import struphy_plots + f = out.kinetic_ions.e1_v1_density.f f.struphy.plot.slice(x="e1", y="v1", t="last") f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4) @@ -597,14 +598,6 @@ everything completes as you type: out.scalars.en_phi.struphy.plot.timeseries(fit=(0.0, 40.0)) # exponential fit in a window out.scalars.en_phi.struphy.analysis.growth_rate(window=(0.0, 40.0)).rate - # plots of the whole run - out.plot.scalars() # every scalar time series - out.save_report() # table + figures in post_processing/report/ - - # the same products by name, which suits scripts and loops - out["kinetic_ions/e1_v1_density/f"].struphy.plot.slice(x="e1", y="v1", t="last") - out["em_fields/e_field"].struphy.analysis.dispersion(slice_at=(0, 0, None), fit_branches=1) - Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of several runs are labeled by run: @@ -647,6 +640,7 @@ perturbation with respect to the background: .. code-block:: python f = out.distributions.kinetic_ions.e1_v1_density.f # dims (t, e1, v1) + import struphy_plots f.struphy.plot.slice(x="e1", y="v1", t="last").show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 2c661cb68..93b203d10 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -4,6 +4,8 @@ from matplotlib import pyplot as plt from struphy import Output +import struphy_plots +from struphy_plots.output_accessors import OutputPlots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" @@ -26,21 +28,20 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run.timeseries( - FIT_QUANTITY, + run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", ) if SHOW_EQUIL_PROFILE: - run.plot.equilibrium() + OutputPlots(run).equilibrium() for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run.viewer(name, x="e1", y="e2", coords="physical", plane=plane, **selection) + run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection) - run.trajectories("kinetic_ions", max_markers=1000) + run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000) plt.show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 6cbcd5a90..4cd9ce032 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -4,6 +4,8 @@ from matplotlib import pyplot as plt from struphy import Output +import struphy_plots +from struphy_plots.output_accessors import OutputPlots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" @@ -26,21 +28,20 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run.timeseries( - FIT_QUANTITY, + run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( fit=FIT_WINDOW, fit_amplitude=True, title=f"Evolution of {FIT_QUANTITY}", ) if SHOW_EQUIL_PROFILE: - run.plot.equilibrium() + OutputPlots(run).equilibrium() for name, component, plane in SWEEPS: selection = {} if component is None else {"component": component} - run.viewer(name, x="e1", y="e2", coords="physical", plane=plane, **selection) + run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection) - run.trajectories("kinetic_ions", max_markers=1000) + run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000) plt.show() diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index a417825c6..838dd6730 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -8,6 +8,8 @@ from pathlib import Path from struphy import Output +import struphy_plots +from struphy_plots.output_accessors import OutputPlots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" @@ -43,7 +45,7 @@ def main(paths=(DEFAULT_OUTPUT,)): return if SHOW_EQUIL_PROFILE: - run.plot.equilibrium() + OutputPlots(run).equilibrium() for name in SWEEPS: run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index bf686048e..96c1822b4 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -4,6 +4,7 @@ from matplotlib import pyplot as plt from struphy import Output +import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index 75867f644..4af6d3508 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -2,6 +2,7 @@ from pathlib import Path from struphy import Output +import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index 86aeaff11..a3382b235 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -2,6 +2,8 @@ from pathlib import Path from struphy import Output +import struphy_plots +from struphy_plots.plotting import save_all_scalars DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" @@ -10,7 +12,7 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # table and figures of every scalar: post_processing/report/ - run.save_report() + save_all_scalars(run.scalars, run.path_pproc / "report", run_label=run.label) # electric field growth against the analytical rate (0.2845 in units of m/c) energy = run.scalars.electric_energy diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index e320744e7..0807c8389 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -4,6 +4,7 @@ import cunumpy as xp from struphy import Output +import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 9c1df37f7..5509bec9b 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -5,6 +5,7 @@ from matplotlib import pyplot as plt from struphy import Output +import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/src/struphy/diagnostics/analysis.py b/src/struphy/diagnostics/analysis.py deleted file mode 100644 index 3615e2321..000000000 --- a/src/struphy/diagnostics/analysis.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Numerical diagnostics returning values and labeled arrays, without rendering.""" - -from dataclasses import dataclass - -import numpy as np -import xarray as xr - -from struphy.post_processing.arrays import validate_array - - -def _label(data): - return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" - - -@dataclass(frozen=True) -class GrowthFit: - """Configuration for an exponential growth-rate fit.""" - - window: tuple[float | None, float | None] = (None, None) - amplitude_from_quadratic: bool = False - - -@dataclass(frozen=True) -class FitResult: - rate: float - intercept: float - time: np.ndarray - fitted: np.ndarray - - -def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: - """Fit ``exp(rate*t + intercept)`` using only finite, positive samples.""" - validate_array(data, required_dims=("t",)) - if data.dims != ("t",): - raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") - fit = fit or GrowthFit() - time, values = np.asarray(data.t), np.asarray(data) - if len(time) < 2: - return None - lo = time[0] if fit.window[0] is None else fit.window[0] - hi = time[-1] if fit.window[1] is None else fit.window[1] - lo, hi = sorted((lo, hi)) - valid = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) - if np.count_nonzero(valid) < 2: - return None - selected_time = time[valid] - signal = np.log(np.sqrt(values[valid])) if fit.amplitude_from_quadratic else np.log(values[valid]) - rate, intercept = np.polyfit(selected_time, signal, 1) - scale = 2.0 if fit.amplitude_from_quadratic else 1.0 - fitted = np.exp(scale * (rate * selected_time + intercept)) - return FitResult(float(rate), float(intercept), selected_time, fitted) - - -def envelope(data: xr.DataArray) -> xr.DataArray: - """Local maxima of a time series: the interior samples not smaller than their neighbours.""" - validate_array(data, required_dims=("t",)) - if data.dims != ("t",): - raise ValueError(f"envelope input must have dims ('t',), got {data.dims}") - values = np.asarray(data) - peak = np.zeros(len(values), dtype=bool) - peak[1:-1] = (values[1:-1] > values[:-2]) & (values[1:-1] >= values[2:]) - return data.isel(t=np.flatnonzero(peak)) - - -def damping_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: - """Fit ``exp(rate*t + intercept)`` to the envelope of an oscillating time series. - - Use this for signals such as the field energy in Landau damping, where :func:`growth_rate` on - the raw series would fit the oscillation. ``fit.window`` restricts the peaks that are used. - The rate is negative for damping. - """ - return growth_rate(envelope(data), fit) - - -def norm(data: xr.DataArray, *, dims=None, squared: bool = False) -> xr.DataArray: - """L2 norm over ``dims`` (default: every dimension except ``t``), as a function of the rest.""" - validate_array(data) - dims = [dim for dim in data.dims if dim != "t"] if dims is None else list(dims) - total = (data**2).sum(dims) - out = total if squared else np.sqrt(total) - out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - label = _label(data) - out.attrs["label"] = f"squared norm of {label}".strip() if squared else f"norm of {label}".strip() - return out - - -def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: - """Signed deviation from an explicit reference or the first time sample.""" - validate_array(data, required_dims=("t",)) - reference = data.isel(t=0) if ref is None else ref - out = data - reference - out.attrs = dict(data.attrs) - out.attrs["label"] = f"{_label(data)} drift".strip() - return out - - -SPATIAL_DIMS = ("e1", "e2", "e3") -VELOCITY_DIMS = ("v1", "v2", "v3") - - -def _provenance(data: xr.DataArray) -> dict: - return {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - - -def _select_dims(data: xr.DataArray, dims, default) -> list[str]: - if dims is None: - selected = [dim for dim in default if dim in data.dims] - if not selected: - raise ValueError(f"{data.name!r} has none of the dimensions {default}; its dimensions are {data.dims}") - return selected - selected = [dims] if isinstance(dims, str) else list(dims) - missing = [dim for dim in selected if dim not in data.dims] - if missing: - raise ValueError(f"{data.name!r} has no dimensions {missing}; its dimensions are {data.dims}") - return selected - - -def spatial_average(data: xr.DataArray, *, dims=None) -> xr.DataArray: - """Mean over the logical space dimensions, e.g. a binned f(t, e1, v1) becomes f(t, v1). - - ``dims`` defaults to every one of ``e1``, ``e2``, ``e3`` that ``data`` has. The mean is - uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a - mapped domain it is not weighted by the Jacobian. Physical ``X``, ``Y``, ``Z`` coordinates - that depend on the averaged dimensions are dropped. - """ - validate_array(data) - averaged = _select_dims(data, dims, SPATIAL_DIMS) - out = data.mean(averaged, keep_attrs=True) - out.attrs["label"] = f"average of {_label(data)}".strip() - out.attrs.pop("long_name", None) - return out - - -def _bin_widths(data: xr.DataArray, dim: str) -> xr.DataArray: - coordinate = np.asarray(data.coords[dim]) if dim in data.coords else None - if coordinate is None or len(coordinate) < 2: - raise ValueError(f"dimension {dim!r} needs a coordinate with at least two bins") - return xr.DataArray(np.gradient(coordinate), dims=(dim,), coords={dim: data.coords[dim]}) - - -def velocity_moments(f: xr.DataArray, *, dims=None) -> xr.Dataset: - """Moments of a binned distribution function over its velocity dimensions. - - ``dims`` defaults to every one of ``v1``, ``v2``, ``v3`` that ``f`` has; the moments are - functions of the remaining dimensions, for example ``(t, e1)`` for an ``e1_v1`` product. - The integrals are sums over the bins, weighted by the bin widths. - - Returns a Dataset with - - * ``density``: the zeroth moment, :math:`\\int f\\,\\mathrm{d}v`. - * ``mean_``: the mean velocity :math:`u = \\int v f\\,\\mathrm{d}v / n` along each dimension. - * ``variance_``: :math:`\\int (v-u)^2 f\\,\\mathrm{d}v / n`. In normalized units this is the - temperature over the particle mass along that direction, :math:`T/m`. - - Where the density is not positive, the mean and variance are NaN. A ``delta_f`` product has - only the density, which is then the density perturbation, because its mean and variance are - not defined. The values keep the normalization of the run; see ``Output.to_si``. - """ - validate_array(f) - integrated = _select_dims(f, dims, VELOCITY_DIMS) - volume = 1.0 - for dim in integrated: - volume = volume * _bin_widths(f, dim) - density = (f * volume).sum(integrated) - - label = _label(f) - variables = {"density": (density, f"density of {label}")} - if f.name != "delta_f": - weight = density.where(density > 0) - for dim in integrated: - mean = (f * f[dim] * volume).sum(integrated) / weight - variance = (f * (f[dim] - mean) ** 2 * volume).sum(integrated) / weight - variables[f"mean_{dim}"] = (mean, f"mean {dim}") - variables[f"variance_{dim}"] = (variance, f"variance of {dim}") - - provenance = _provenance(f) - out = {} - for name, (values, description) in variables.items(): - values.attrs = {**provenance, "label": description.strip()} - out[name] = values.rename(name) - return xr.Dataset(out, attrs=provenance) - - -def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: - """Absolute relative deviation from an explicit reference or first sample.""" - validate_array(data, required_dims=("t",)) - reference = data.isel(t=0) if ref is None else ref - if np.any(np.asarray(reference) == 0): - raise ValueError("cannot take a relative error against a reference of zero") - out = abs(data - reference) / abs(reference) - out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - out.attrs.update(label=f"relative error of {_label(data)}".strip(), units="") - return out.isel(t=slice(1, None)) if skip_first else out diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 187625d8d..ed335f9d4 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Spectral diagnostics and deprecated plotting helpers for legacy output. -Use ``Output(path)`` and array ``.struphy.plot`` accessors for new plotting code. +Use the optional ``struphy-plots`` package for new plotting code. The legacy distribution/video helpers read the old NPY layout, not output.nc. ``power_spectrum_2d`` remains supported by the analysis accessor. """ @@ -69,7 +69,7 @@ def wrapped(field, *args, **kwargs): f"diagn_tools.{function.__name__}(values, name, grids, ...) is deprecated; pass a field of an Output.\n" "How to update your script, with out = sim.output:\n" " power_spectrum_2d(E_of_t, 'e_field_log', grids=sim.grids_log, grids_mapped=sim.grids_phy, ...)\n" - " -> out.fields.em_fields.e_field_log.struphy.analysis.dispersion(physical=True, ...)\n" + " -> struphy_plots analysis helpers applied to out.fields.em_fields.e_field_log\n" "'physical=True' replaces 'grids_mapped'; 'grids' and 'name' are read from the field itself. " "Take the field from out.with_time_units('normalized') if you compare with normalized dispersion relations.", DeprecationWarning, @@ -294,7 +294,7 @@ def fun(k): return omega, kvec, dispersion, coeffs -@_legacy_plot("out.plot.scalars() or scalar.struphy.plot.timeseries()") +@_legacy_plot("struphy_plots.plotting.plot_scalars()") def plot_scalars( time, scalar_quantities, @@ -490,7 +490,7 @@ def plot_scalars( plt.show() -@_legacy_plot("array.struphy.plot.slice()") +@_legacy_plot("struphy_plots.plotting.plot_slice()") def plot_distr_fun( path, time_idx, @@ -625,7 +625,7 @@ def plot_distr_fun( del delta_f -@_legacy_plot("array.struphy.plot.view(...).animation() or .panels()") +@_legacy_plot("struphy_plots.plotting.animate_slices() or plot_panels()") def plots_videos_2d( t_grid, grid_slices, @@ -785,7 +785,7 @@ def plots_videos_2d( raise NotImplementedError(f"{output=} is not implemented!") -@_legacy_plot("array.struphy.plot.view(...).animation().save(path)") +@_legacy_plot("struphy_plots.plotting.animate_slices().save(path)") def video_2d(slc, diagn_path, images_path): """Create a video of all 2D slices of the distribution function over time. @@ -860,7 +860,7 @@ def video_2d(slc, diagn_path, images_path): video.release() -@_legacy_plot("array.struphy.plot.view(...).animation()") +@_legacy_plot("struphy_plots.plotting.animate_slices()") def plots_2d_video( t_grid, grid_1_mesh, @@ -942,7 +942,7 @@ def plots_2d_video( plt.close("all") -@_legacy_plot("array.struphy.plot.panels()") +@_legacy_plot("struphy_plots.plotting.plot_panels()") def plots_2d_overview( t_grid, grid_1_mesh, diff --git a/src/struphy/diagnostics/plotting.py b/src/struphy/diagnostics/plotting.py deleted file mode 100644 index 4fa07aa4b..000000000 --- a/src/struphy/diagnostics/plotting.py +++ /dev/null @@ -1,664 +0,0 @@ -"""Small, composable plotting functions for labeled Struphy output. - -Users reach these through ``run.plot`` (see :class:`struphy.post_processing.output_accessors.OutputPlots`); -they remain importable for plotting arbitrary labeled arrays. -""" - -from __future__ import annotations - -import logging -from dataclasses import dataclass, field -from pathlib import Path -from typing import Literal - -import matplotlib.pyplot as plt -import numpy as np -import xarray as xr -from matplotlib.widgets import Slider - -from struphy.diagnostics.analysis import ( - FitResult, - GrowthFit, - drift, - growth_rate, - relative_error, -) -from struphy.post_processing.arrays import ( - SCALARS_EXCLUDE, - axis_label, - save_scalars, - scalar_names, - validate_array, - value_label, -) - -logger = logging.getLogger("struphy") - -STRUPHY_STYLE = { - "figure.figsize": (8.0, 5.0), - "figure.dpi": 110, - "axes.grid": True, - "grid.alpha": 0.3, - "axes.titlesize": "medium", - "legend.frameon": False, - "image.cmap": "viridis", -} - -PLANES = { - "XY": ("X", "Y", "X", "Y"), - "XZ": ("X", "Z", "X", "Z"), - "YZ": ("Y", "Z", "Y", "Z"), - "RZ": ("R", "Z", "R", "Z"), -} - - -@dataclass(frozen=True) -class View: - """A reusable selection and rendering recipe for an N-dimensional product.""" - - x: str | None = None - y: str | None = None - sweep: str = "t" - select: dict[str, float] = field(default_factory=dict) - isel: dict[str, int] = field(default_factory=dict) - coordinates: Literal["logical", "physical"] = "logical" - plane: Literal["XY", "XZ", "YZ", "RZ"] = "XY" - - -@dataclass -class PlotResult: - """Already-rendered Matplotlib objects; saving never redraws them. - - As the last expression of a notebook cell it displays its figure once; there is no need - to write ``.fig``. - """ - - fig: object - ax: object - artists: list = field(default_factory=list) - fit_results: list[FitResult | None] = field(default_factory=list) - data: dict = field(default_factory=dict) - _shown: bool = field(default=False, init=False, repr=False, compare=False) - - def save(self, path, *, close=False, **kwargs): - kwargs.setdefault("bbox_inches", "tight") - self.fig.savefig(path, **kwargs) - if close: - plt.close(self.fig) - return str(path) - - def show(self): - plt.show() - self._shown = True - return self - - def __repr__(self): - return f"{type(self).__name__}(fig={self.fig!r})" - - def _ipython_display_(self): - if not self._shown: - _display_figure(self.fig) - - -def _detach_figure(fig): - """Take a figure out of pyplot under the inline backend, which would show it as a still image.""" - import matplotlib - - if "inline" in matplotlib.get_backend(): - plt.close(fig) - - -def _display_figure(fig): - """Display a figure as a notebook cell result, exactly once. - - The inline backend shows every open figure again at the end of the cell, so the displayed - figure is closed. Interactive backends (e.g. ipympl) already show the figure when it is - created, so nothing is displayed twice there either. - """ - import matplotlib - - if "inline" not in matplotlib.get_backend(): - return - from IPython.display import display - - display(fig) - plt.close(fig) - - -def _label(data): - return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" - - -def _items(data): - return [data] if isinstance(data, (xr.DataArray, xr.Dataset)) else list(data) - - -def shared_run_label(data, default="") -> str: - """The run description shared by all arrays (``attrs["run"]``), or ``default``. - - Arrays loaded from a :class:`~struphy.Output` carry it; arrays from different runs share none. - """ - runs = {item.attrs.get("run") for item in _items(data)} - if len(runs - {None, ""}) > 1: - return "" - runs.discard(None) - runs.discard("") - return runs.pop() if runs else default - - -def _finish(fig, *, run_label="", tight=True): - if run_label: - fig.suptitle(run_label, fontsize="small") - if tight: - fig.tight_layout() - - -def _select(data: xr.DataArray, view: View, *, keep_sweep=True): - validate_array(data) - overlap = set(view.select) & set(view.isel) - if overlap: - raise ValueError(f"dimensions cannot appear in both select and isel: {sorted(overlap)}") - selected = data - if view.select: - selected = selected.sel(view.select, method="nearest") - if view.isel: - selected = selected.isel(view.isel) - if not keep_sweep and view.sweep in selected.dims: - selected = selected.isel({view.sweep: 0}) - return selected - - -def logical_grids(data: xr.DataArray, *, x=None, y=None): - """Return 2-D logical coordinate grids and their labels.""" - if x is None or y is None: - if data.ndim != 2: - raise ValueError(f"x and y are required unless data is two-dimensional; got {data.dims}") - x, y = data.dims - if set(data.dims) != {x, y}: - raise ValueError(f"selected data must contain exactly {x!r} and {y!r}; got {data.dims}") - xgrid, ygrid = np.meshgrid(np.asarray(data.coords[x]), np.asarray(data.coords[y]), indexing="ij") - return xgrid, ygrid, axis_label(data, x), axis_label(data, y) - - -def physical_grids(data: xr.DataArray, *, plane="XY"): - """Return physical auxiliary coordinates already attached to a selected field.""" - if plane not in PLANES: - raise ValueError(f"unknown plane {plane!r}; expected one of {tuple(PLANES)}") - xname, yname, xlabel, ylabel = PLANES[plane] - missing = [name for name in ("X", "Y", "Z") if name not in data.coords] - if missing: - raise ValueError(f"physical coordinates are not attached to {data.name!r}: missing {missing}") - xcoord = np.sqrt(data.X**2 + data.Y**2) if xname == "R" else data.coords[xname] - ycoord = data.coords[yname] - if xcoord.ndim != 2 or ycoord.ndim != 2: - raise ValueError("select all but two spatial dimensions before requesting a physical grid") - return np.asarray(xcoord), np.asarray(ycoord), xlabel, ylabel - - -def _slice_data(data, view): - selected = _select(data, view) - if view.sweep in selected.dims and view.sweep not in (view.x, view.y): - raise ValueError(f"select one {view.sweep!r} value before drawing a static slice, or display it as x or y") - if view.x is None or view.y is None: - if selected.ndim != 2: - raise ValueError(f"view.x and view.y are required for remaining dims {selected.dims}") - x, y = selected.dims - else: - x, y = view.x, view.y - if set(selected.dims) != {x, y}: - raise ValueError(f"selection leaves dimensions {selected.dims}; expected only {x!r}, {y!r}") - selected = selected.transpose(x, y) - grids = ( - physical_grids(selected, plane=view.plane) - if view.coordinates == "physical" - else logical_grids(selected, x=x, y=y) - ) - return selected, grids - - -def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=None): - """Plot one or more time series, each on its own time grid; series of different runs are labeled by run.""" - series = _items(data) - if not series: - raise ValueError("at least one time series is required") - for item in series: - validate_array(item, required_dims=("t",)) - if item.dims != ("t",): - raise ValueError(f"time series must have dims ('t',), got {item.dims}") - label_of = _label - if len({item.attrs.get("run_name") for item in series}) > 1: - - def label_of(item): - return " ".join( - filter(None, (_label(item), f"({item.attrs['run_name']})" if item.attrs.get("run_name") else "")) - ) - - run_label = shared_run_label(series) if run_label is None else run_label - own_figure = ax is None - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - artists, fits = [], [] - for item in series: - (line,) = ax.plot(item.t, item, label=label_of(item) or None) - artists.append(line) - result = growth_rate(item, fit) if fit is not None else None - fits.append(result) - if result is not None: - (fitted,) = ax.plot( - result.time, - result.fitted, - "--", - color=line.get_color(), - label=rf"fit: $\gamma$ = {result.rate:.4e}", - ) - ax.axvspan(result.time[0], result.time[-1], alpha=0.12, color="grey") - artists.append(fitted) - if logy: - ax.set_yscale("log") - ax.set_xlabel(axis_label(series[0], "t")) - ax.set_ylabel(value_label(series[0])) - ax.set_title(title if title is not None else _label(series[0])) - if any(label_of(item) for item in series) or fit is not None: - ax.legend() - _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) - return PlotResult(fig, ax, artists, fits) - - -class _SliceRenderer: - """Shared selection, color limits and mesh rendering for every slice presentation.""" - - def __init__(self, data, view, *, vmin=None, vmax=None, shared_clim=True, cmap=None, equal_aspect=None, title=None): - self.data = _select(data, view) - self.view = View(x=view.x, y=view.y, sweep=view.sweep, coordinates=view.coordinates, plane=view.plane) - self.vmin, self.vmax = vmin, vmax - self.shared_clim = shared_clim - self.cmap = cmap or STRUPHY_STYLE["image.cmap"] - self.equal_aspect = view.coordinates == "physical" if equal_aspect is None else equal_aspect - self.title = _label(data) if title is None else title - self.limits = self._limits(self.data) if shared_clim else None - - def _limits(self, data): - if self.vmin is not None and self.vmax is not None: - return self.vmin, self.vmax - values = np.asarray(data) - finite = values[np.isfinite(values)] - if not finite.size: - raise ValueError("cannot determine color limits from data without finite values; provide vmin and vmax") - return ( - float(finite.min()) if self.vmin is None else self.vmin, - float(finite.max()) if self.vmax is None else self.vmax, - ) - - def draw(self, ax, data): - values, (xg, yg, xlabel, ylabel) = _slice_data(data, self.view) - lo, hi = self.limits if self.shared_clim else self._limits(values) - mesh = ax.pcolormesh(xg, yg, values, shading="auto", vmin=lo, vmax=hi, cmap=self.cmap) - ax.set(xlabel=xlabel, ylabel=ylabel, aspect="equal" if self.equal_aspect else "auto") - ax.grid(False) - return mesh - - def frame_title(self, index): - return f"{self.title} at {self.view.sweep} = {float(self.data[self.view.sweep][index]):.3e}" - - def indices(self, step): - if not isinstance(step, (int, np.integer)) or step < 1: - raise ValueError("step must be a positive integer") - validate_array(self.data, required_dims=(self.view.sweep,)) - if not self.data.sizes[self.view.sweep]: - raise ValueError("cannot render an empty sweep") - return range(0, self.data.sizes[self.view.sweep], step) - - -def plot_slice( - data: xr.DataArray, - *, - view=None, - ax=None, - vmin=None, - vmax=None, - equal_aspect=None, - title=None, - run_label=None, - cmap=None, - shared_clim=True, -): - """Render one selected two-dimensional slice.""" - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - shared_clim=shared_clim, - ) - run_label = shared_run_label(data) if run_label is None else run_label - own_figure = ax is None - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - mesh = renderer.draw(ax, renderer.data) - fig.colorbar(mesh, ax=ax, label=value_label(data)) - ax.set_title(renderer.title) - _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) - return PlotResult(fig, ax, [mesh]) - - -def plot_panels( - data: xr.DataArray, - *, - view=None, - nrows=3, - ncols=4, - shared_clim=True, - title=None, - run_label=None, - vmin=None, - vmax=None, - cmap=None, - equal_aspect=None, -): - """Plot snapshots with common color limits over the entire selected sweep by default.""" - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - ) - renderer.indices(1) - if nrows < 1 or ncols < 1: - raise ValueError("nrows and ncols must be positive") - sweep = renderer.view.sweep - indices = np.linspace(0, renderer.data.sizes[sweep] - 1, nrows * ncols).astype(int) - run_label = shared_run_label(data) if run_label is None else run_label - with plt.rc_context(STRUPHY_STYLE): - fig, axes = plt.subplots( - nrows, - ncols, - figsize=(3.5 * ncols, 2.8 * nrows), - sharex=True, - sharey=True, - squeeze=False, - layout="constrained", - ) - meshes = [] - for ax, index in zip(axes.ravel(), indices): - mesh = renderer.draw(ax, renderer.data.isel({sweep: int(index)})) - meshes.append(mesh) - ax.set_title(f"{sweep} = {float(renderer.data[sweep][index]):.3e}") - if not shared_clim: - fig.colorbar(mesh, ax=ax, label=value_label(data)) - if shared_clim: - fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) - fig.suptitle(" — ".join(filter(None, (renderer.title, run_label)))) - return PlotResult(fig, axes, meshes) - - -class InteractiveSliceViewer: - """Slider view with the same rendering options as static and exported slices.""" - - def __init__( - self, - data: xr.DataArray, - *, - view=None, - vmin=None, - vmax=None, - run_label=None, - shared_clim=True, - cmap=None, - equal_aspect=None, - title=None, - ): - self.data = validate_array(data) - self.view = view or View() - self.options = dict( - vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title - ) - self.run_label = shared_run_label(data) if run_label is None else run_label - self.result = None - self.sliders = {} - - def show(self): - (self.result or self.draw()).show() - return self - - def _ipython_display_(self): - (self.result or self.draw())._ipython_display_() - - def draw(self): - if self.result is not None: - return self.result - renderer = _SliceRenderer(self.data, self.view, **self.options) - base = renderer.data - x, y = self.view.x, self.view.y - if x is None or y is None: - candidates = [dim for dim in base.dims if dim != self.view.sweep] - if len(candidates) < 2: - raise ValueError("viewer needs two display dimensions") - x, y = candidates[:2] - renderer.view = View(x=x, y=y, coordinates=self.view.coordinates, plane=self.view.plane) - controls = [dim for dim in base.dims if dim not in {x, y}] - indices = {dim: 0 for dim in controls} - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() - fig.subplots_adjust(bottom=0.13 + 0.05 * len(controls)) - mesh = renderer.draw(ax, base.isel(indices)) - colorbar = fig.colorbar(mesh, ax=ax, label=value_label(self.data)) - self.result = PlotResult(fig, ax, [mesh]) - - def update(_=None): - for dim, slider in self.sliders.items(): - indices[dim] = int(slider.val) - self.result.artists[0].remove() - mesh = renderer.draw(ax, base.isel(indices)) - self.result.artists[:] = [mesh] - colorbar.update_normal(mesh) - values = ", ".join(f"{dim}={float(base[dim][index]):.3e}" for dim, index in indices.items()) - ax.set_title(" at ".join(filter(None, (renderer.title, values)))) - fig.canvas.draw_idle() - - for row, dim in enumerate(controls): - if base.sizes[dim] == 1: - continue - slider_ax = fig.add_axes([0.20, 0.05 + 0.05 * row, 0.60, 0.025]) - slider = Slider(slider_ax, dim, 0, base.sizes[dim] - 1, valstep=1) - slider.on_changed(update) - self.sliders[dim] = slider - update() - _finish(fig, run_label=self.run_label, tight=False) - # Keep widget callbacks alive even if only the PlotResult is retained. - self.result.data["viewer"] = self - return self.result - - -def animate_slices( - data: xr.DataArray, - *, - view=None, - interval=100, - step=1, - vmin=None, - vmax=None, - shared_clim=True, - cmap=None, - equal_aspect=None, - title=None, -): - """Animate slices with fixed color limits over the selected sweep by default.""" - from matplotlib.animation import FuncAnimation - - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - ) - frames = renderer.indices(step) - sweep = renderer.view.sweep - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() - mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) - colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) - _finish(fig, run_label=shared_run_label(data)) - - def update(index): - nonlocal mesh - mesh.remove() - mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) - colorbar.update_normal(mesh) - ax.set_title(renderer.frame_title(index)) - return (mesh,) - - animation = FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) - _detach_figure(fig) - return animation - - -def save_frames( - data: xr.DataArray, - directory, - *, - view=None, - step=1, - prefix="frame", - dpi=110, - vmin=None, - vmax=None, - shared_clim=True, - cmap=None, - equal_aspect=None, - title=None, -): - """Export the configured sweep as PNGs, sharing color limits by default.""" - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - ) - frames = renderer.indices(step) - directory = Path(directory) - directory.mkdir(parents=True, exist_ok=True) - paths = [] - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() - try: - sweep = renderer.view.sweep - mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) - colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) - _finish(fig, run_label=shared_run_label(data)) - for frame, index in enumerate(frames): - mesh.remove() - mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) - colorbar.update_normal(mesh) - ax.set_title(renderer.frame_title(index)) - path = directory / f"{prefix}_{frame:04d}.png" - fig.savefig(path, dpi=dpi, bbox_inches="tight") - paths.append(str(path)) - finally: - plt.close(fig) - return paths - - -def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, logy=False, run_label=None): - """Plot every scalar time series in one axes.""" - selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: - raise ValueError("no scalars to plot") - run_label = shared_run_label([scalars[name] for name in selected]) if run_label is None else run_label - fig, ax = plt.subplots(layout="constrained") - for name in selected: - values = scalars[name] / scalars[relative_to] if relative_to else scalars[name] - ax.plot(values.t, values, label=name) - if logy: - ax.set_yscale("log") - units = {scalars[name].attrs.get("units", "") for name in selected} - ylabel = f"quantity / {relative_to}" if relative_to else (f"[{units.pop()}]" if len(units) == 1 else "[a.u.]") - ax.set(xlabel=axis_label(scalars[selected[0]], "t"), ylabel=ylabel, title="Scalars") - ax.legend(fontsize="small") - if run_label: - fig.suptitle(run_label, fontsize="small") - return PlotResult(fig, ax, list(ax.lines)) - - -def save_all_scalars( - scalars, - directory, - *, - names=None, - exclude=SCALARS_EXCLUDE, - logy=False, - run_label=None, - table="csv", - file_format="png", - dpi=110, -): - """Write a table, scalar overview and one figure per scalar.""" - selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: - return [] - directory = Path(directory) - directory.mkdir(parents=True, exist_ok=True) - paths = [] - if table: - paths.append(save_scalars(scalars, str(directory / f"scalars.{table}"), names=selected, fmt=table)) - overview = plot_scalars(scalars, names=selected, logy=logy, run_label=run_label) - path = directory / f"scalars.{file_format}" - overview.save(path, dpi=dpi, close=True) - paths.append(str(path)) - for name in selected: - result = plot_timeseries(scalars[name], logy=logy, title=name, run_label=run_label) - path = directory / f"{name}.{file_format}" - result.save(path, dpi=dpi, close=True) - paths.append(str(path)) - return paths - - -def plot_marker_trajectories(orbits: xr.DataArray, *, ax=None, max_markers=200, show_paths=None): - """Plot a static 3-D trajectory overview; interactive marker UI is intentionally separate.""" - validate_array(orbits, required_dims=("t", "marker", "quantity")) - count = min(orbits.sizes["marker"], max_markers) - positions = np.asarray(orbits.isel(marker=slice(0, count)).sel(quantity=["x", "y", "z"])) - fig = plt.figure() if ax is None else ax.figure - ax = fig.add_subplot(111, projection="3d") if ax is None else ax - show_paths = count <= 200 if show_paths is None else show_paths - artists = [] - if show_paths: - for marker in range(count): - artists.extend(ax.plot(*positions[:, marker].T, lw=0.8, alpha=0.5)) - artists.append(ax.scatter(*positions[-1].T, s=8)) - ax.set(xlabel="X", ylabel="Y", zlabel="Z", title="Marker trajectories") - return PlotResult(fig, ax, artists) - - -def plot_equilibrium_profile(path_out, *, ax=None): - """Plot radial equilibrium profiles from ``geometry.vts``.""" - import pyvista as pv - - equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) - shape = equilibrium.dimensions - grid = np.reshape(equilibrium.points, shape + (3,)) - radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2) - pressure = np.reshape(equilibrium.point_data["p0"], shape) - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - ax.plot(radius[0, 0], pressure[0, 0], label=r"$p_0$") - if "n0" in equilibrium.point_data: - density = np.reshape(equilibrium.point_data["n0"], shape) - ax.plot(radius[0, 0], density[0, 0], label=r"$n_0$") - ax.plot(radius[0, 0], pressure[0, 0] / density[0, 0], label=r"$T_0$") - ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") - ax.legend() - return PlotResult(fig, ax, list(ax.lines)) diff --git a/src/struphy/diagnostics/tests/test_plotting.py b/src/struphy/diagnostics/tests/test_plotting.py deleted file mode 100644 index c02c269d7..000000000 --- a/src/struphy/diagnostics/tests/test_plotting.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Tests for functional plotting and the shared view recipe.""" - -import matplotlib -import numpy as np -import pytest -import xarray as xr - -matplotlib.use("Agg") -from matplotlib import pyplot as plt # noqa: E402 - -from struphy.diagnostics.plotting import ( # noqa: E402 - GrowthFit, - InteractiveSliceViewer, - View, - animate_slices, - drift, - growth_rate, - logical_grids, - physical_grids, - plot_panels, - plot_scalars, - plot_slice, - plot_timeseries, - relative_error, - save_all_scalars, - save_frames, -) -from struphy.post_processing.arrays import data_array # noqa: E402 - -pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") - - -@pytest.fixture(autouse=True) -def close_figures(): - yield - plt.close("all") - - -def phase_space(nt=6): - return data_array( - np.arange(nt * 4 * 5).reshape(nt, 4, 5), - ("t", "e1", "v1"), - {"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, 4), "v1": np.linspace(-2, 2, 5)}, - name="f", - label="$f$", - coord_units={"t": "s"}, - ) - - -def physical_field(): - coords = {"t": [0, 1], "e1": range(3), "e2": range(4), "e3": range(5)} - grids = np.meshgrid(coords["e1"], coords["e2"], coords["e3"], indexing="ij") - coords.update({name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), grids)}) - return data_array(np.ones((2, 3, 4, 5)), ("t", "e1", "e2", "e3"), coords, name="phi") - - -def scalar_dataset(): - t = np.linspace(0, 1, 6) - return xr.Dataset({"en_tot": ("t", 2 + 0.02 * t), "en_e": ("t", 1 + 0.1 * t)}, coords={"t": t}) - - -def test_growth_rate_uses_only_valid_samples_inside_window(): - data = data_array([1, 0, 4, np.nan, 16], ("t",), {"t": range(5)}) - result = growth_rate(data, GrowthFit((0, 4))) - assert result is not None and np.isfinite(result.rate) - np.testing.assert_array_equal(result.time, [0, 2, 4]) - - -def test_growth_rate_does_not_fall_back_outside_requested_window(): - data = data_array(np.exp(np.arange(5)), ("t",), {"t": range(5)}) - assert growth_rate(data, GrowthFit((1.1, 1.2))) is None - - -def test_growth_rate_of_quadratic_reports_amplitude_rate(): - t = np.linspace(0, 4, 20) - result = growth_rate(data_array(np.exp(0.6 * t), ("t",), {"t": t}), GrowthFit(amplitude_from_quadratic=True)) - assert result.rate == pytest.approx(0.3) - - -def test_diagnostics_preserve_time_coordinates(): - data = data_array([2, 2.2, 1.8], ("t",), {"t": [0, 1, 2]}, label="E") - np.testing.assert_allclose(drift(data), [0, 0.2, -0.2]) - np.testing.assert_allclose(relative_error(data), [0.1, 0.1]) - np.testing.assert_array_equal(relative_error(data).t, [1, 2]) - - -def test_logical_and_physical_grids_follow_selected_dimensions(): - logical = phase_space().isel(t=0) - assert logical_grids(logical)[0].shape == (4, 5) - physical = physical_field().isel(t=0, e3=2) - assert physical_grids(physical, plane="XY")[0].shape == (3, 4) - assert physical_grids(physical, plane="RZ")[0].shape == (3, 4) - - -def test_plot_timeseries_renders_once_and_save_does_not_redraw(tmp_path): - data = data_array(np.exp(np.arange(4)), ("t",), {"t": range(4)}, label="energy", coord_units={"t": "s"}) - result = plot_timeseries(data, fit=GrowthFit(), run_label="dt=.1") - lines = len(result.ax.lines) - result.save(tmp_path / "energy.png") - assert len(result.ax.lines) == lines - assert len(plt.get_fignums()) == 1 - assert result.fig._suptitle.get_text() == "dt=.1" - - -def test_plot_slice_accepts_named_value_and_index_selection(): - result = plot_slice(phase_space(), view=View(x="e1", y="v1", select={"t": 0.52})) - assert result.ax.get_xlabel() == r"$\eta_1$" - assert len(result.artists) == 1 - - -def test_plot_slice_physical_coordinates_are_intrinsic(): - result = plot_slice( - physical_field(), view=View(x="e1", y="e2", isel={"t": 0, "e3": 2}, coordinates="physical", plane="XY") - ) - assert result.ax.get_xlabel() == "X" - assert result.ax.get_aspect() == 1.0 - - -def test_plot_slice_rejects_underspecified_selection(): - with pytest.raises(ValueError, match="selection leaves"): - plot_slice(physical_field(), view=View(x="e1", y="e2", isel={"t": 0})) - - -def test_panels_use_one_recipe_and_keep_full_title(): - result = plot_panels( - phase_space(), view=View(x="e1", y="v1"), nrows=1, ncols=2, title="Distribution", run_label="dt=.1" - ) - assert result.fig._suptitle.get_text() == "Distribution — dt=.1" - assert len(result.artists) == 2 - - -def test_viewer_builds_controls_for_every_non_display_dimension(): - viewer = InteractiveSliceViewer(physical_field(), view=View(x="e1", y="e2", coordinates="physical")) - result = viewer.draw() - assert set(viewer.sliders) == {"t", "e3"} - viewer.sliders["e3"].set_val(3) - assert result.fig is not None - - -def test_animation_and_frames_share_the_view(tmp_path): - data = phase_space(nt=7) - view = View(x="e1", y="v1") - animation = animate_slices(data, view=view, step=3) - assert len(list(animation.new_frame_seq())) == 3 - paths = save_frames(data, tmp_path, view=view, step=3) - assert len(paths) == 3 - assert all(__import__("pathlib").Path(path).exists() for path in paths) - - -def test_scalar_overview_and_export(tmp_path): - result = plot_scalars(scalar_dataset(), run_label="run") - assert sorted(line.get_label() for line in result.artists) == ["en_e", "en_tot"] - assert result.fig._suptitle.get_text() == "run" - paths = save_all_scalars(scalar_dataset(), tmp_path) - assert sorted(__import__("os").path.basename(path) for path in paths) == [ - "en_e.png", - "en_tot.png", - "scalars.csv", - "scalars.png", - ] - assert plt.get_fignums() == [result.fig.number] - - -@pytest.mark.parametrize("shown", [False, True]) -def test_notebook_display_shows_the_figure_once(monkeypatch, shown): - import IPython.display - - displayed = [] - monkeypatch.setattr(matplotlib, "get_backend", lambda: "module://matplotlib_inline.backend_inline") - monkeypatch.setattr(IPython.display, "display", displayed.append) - monkeypatch.setattr(plt, "show", lambda *args, **kwargs: None) - result = plot_timeseries(scalar_dataset().en_tot, logy=False) - if shown: - result.show() - result._ipython_display_() - assert displayed == ([] if shown else [result.fig]) - assert (result.fig.number in plt.get_fignums()) == shown, "the inline backend must not show it again" - - -def test_slice_can_display_the_sweep_dimension(): - data = phase_space() - result = plot_slice(data.isel(v1=slice(None)), view=View(x="t", y="e1", isel={"v1": 0})) - assert result.ax.get_xlabel() == "$t$ [s]" - with pytest.raises(ValueError, match="display it as x or y"): - plot_slice(data, view=View(x="e1", y="v1")) - - -def test_every_presentation_uses_the_full_selected_color_range(tmp_path, monkeypatch): - from matplotlib.figure import Figure - - import struphy.post_processing.xarray_accessors # noqa: F401 - - data = phase_space(nt=3).astype(float) - data[1] = data[1] * 100 # extrema in a frame omitted by panels and export - view = data.struphy.plot.view(x="e1", y="v1", cmap="plasma", equal_aspect=True) - limits = (float(data.min()), float(data.max())) - assert plt.get_fignums() == [] - snapshot = view.slice(t="last") - panels = view.panels(nrows=1, ncols=2) - viewer = view.viewer() - result = viewer.draw() - viewer.sliders["t"].set_val(2) - animation = view.animation(step=2) - mesh = animation._func(2)[0] - for artist in [snapshot.artists[0], *panels.artists, result.artists[0], mesh]: - assert artist.get_clim() == limits - assert artist.get_cmap().name == "plasma" - assert artist.axes.get_aspect() == 1.0 - captured = [] - original = Figure.savefig - - def capture(fig, *args, **kwargs): - captured.append(fig.axes[0].collections[0].get_clim()) - return original(fig, *args, **kwargs) - - monkeypatch.setattr(Figure, "savefig", capture) - before = plt.get_fignums() - assert len(view.save_frames(tmp_path, step=2)) == 2 - assert captured == [limits, limits] - assert plt.get_fignums() == before - - -@pytest.mark.parametrize("shared_clim", [True, False]) -def test_explicit_color_limits_work_for_all_renderers(tmp_path, monkeypatch, shared_clim): - from matplotlib.figure import Figure - - import struphy.post_processing.xarray_accessors # noqa: F401 - - data = phase_space(nt=2) - options = dict(x="e1", y="v1", vmin=-5, vmax=100, shared_clim=shared_clim, cmap="coolwarm") - panels = data.struphy.plot.panels(nrows=1, ncols=2, **options) - animation = data.struphy.plot.animation(**options) - viewer = data.struphy.plot.viewer(**options) - viewer.draw() - viewer.sliders["t"].set_val(1) - for mesh in [*panels.artists, animation._func(1)[0], viewer.result.artists[0]]: - assert mesh.get_clim() == (-5, 100) - assert mesh.get_cmap().name == "coolwarm" - captured = [] - monkeypatch.setattr( - Figure, "savefig", lambda fig, *args, **kwargs: captured.append(fig.axes[0].collections[0].get_clim()) - ) - data.struphy.plot.frames(tmp_path, **options) - assert captured == [(-5, 100), (-5, 100)] - - -def test_per_frame_scaling_is_explicit_and_supports_a_fixed_lower_limit(): - import struphy.post_processing.xarray_accessors # noqa: F401 - - data = phase_space(nt=2) - view = data.struphy.plot.view(x="e1", y="v1", shared_clim=False, vmin=-1) - panels = view.panels(nrows=1, ncols=2) - animation = view.animation() - for index in range(2): - limits = (-1, float(data.isel(t=index).max())) - assert panels.artists[index].get_clim() == limits - assert animation._func(index)[0].get_clim() == limits - - -def test_viewer_show_retains_controls_and_does_not_redraw(monkeypatch): - viewer = InteractiveSliceViewer(phase_space(), view=View(x="e1", y="v1")) - result = viewer.draw() - monkeypatch.setattr(plt, "show", lambda: None) - assert viewer.show() is viewer - assert viewer.draw() is result - assert len(plt.get_fignums()) == 1 - viewer.sliders["t"].set_val(2) - assert result.artists[0] is result.ax.collections[0] - - -@pytest.mark.parametrize("step", [0, -1]) -def test_sweep_rejects_invalid_step(tmp_path, step): - with pytest.raises(ValueError, match="positive integer"): - animate_slices(phase_space(), view=View(x="e1", y="v1"), step=step) - with pytest.raises(ValueError, match="positive integer"): - save_frames(phase_space(), tmp_path, view=View(x="e1", y="v1"), step=step) - - -def test_legacy_scalar_plot_warns_and_still_renders(monkeypatch): - from struphy.diagnostics import diagn_tools - - monkeypatch.setattr(plt, "show", lambda: None) - with pytest.deprecated_call(match="out.plot.scalars"): - diagn_tools.plot_scalars(np.arange(3), {"en_tot": np.array([1.0, 2.0, 3.0])}) - assert plt.get_fignums() diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index b122393ea..878999e90 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -18,6 +18,7 @@ perturbations, set_logging_level, ) +from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import LinearMHD set_logging_level() @@ -87,7 +88,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} - _1, _2, _3, coeffs = run.evaluate("mhd/velocity").struphy.analysis.dispersion( + _1, _2, _3, coeffs = power_spectrum_2d( + run.evaluate("mhd/velocity"), physical=True, component=0, slice_at=[0, 0, None], @@ -107,7 +109,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 # second fft - _1, _2, _3, coeffs = run.evaluate("mhd/pressure").struphy.analysis.dispersion( + _1, _2, _3, coeffs = power_spectrum_2d( + run.evaluate("mhd/pressure"), physical=True, component=0, slice_at=[0, 0, None], diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index cd8dbea16..23f8f5644 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -19,6 +19,7 @@ grids, perturbations, ) +from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import Maxwell logger = logging.getLogger("struphy") @@ -72,7 +73,8 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: # fft - _1, _2, _3, coeffs = run.evaluate("em_fields/e_field").struphy.analysis.dispersion( + _1, _2, _3, coeffs = power_spectrum_2d( + run.evaluate("em_fields/e_field"), physical=True, component=0, slice_at=[0, 0, None], diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 425aa5857..37cbe7200 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -31,7 +31,6 @@ BINNED_LABELS, data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits, ) from struphy.post_processing.orbits import orbits_tools -from struphy.post_processing.output_accessors import OutputPlots from struphy.post_processing.manifest import MANIFEST_SCHEMA_VERSION, is_processed, normalize_options, source_fingerprint from struphy.post_processing.profiling import Profile from struphy.post_processing.si import to_si @@ -186,7 +185,7 @@ class Output: Call :meth:`evaluate` to obtain one product as an :class:`xarray.DataArray`. It materializes post-processing products on demand; call :meth:`pproc` explicitly to choose its options. The :attr:`xarray` property exposes the complete post-processed product tree. - Render products through this object too, e.g. ``out.viewer("em_fields/E", x="e1", y="e2")``. + Optional plotting is provided by the separate ``struphy-plots`` package. * :attr:`scalars` are read directly from the raw HDF5 output. * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` are retained @@ -480,45 +479,6 @@ def _product(self, name: str) -> xr.DataArray: ) raise KeyError(f"{name!r} not found; available products: {available}") - def growth_rate(self, product: str | xr.DataArray, *, window=(None, None), amplitude: bool = False): - """Fit exponential growth of a scalar product and return a ``FitResult``.""" - from struphy.diagnostics.analysis import GrowthFit, growth_rate - - return growth_rate(self._array(product), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def damping_rate(self, product: str | xr.DataArray, *, window=(None, None), amplitude: bool = False): - """Fit exponential decay to the envelope of an oscillating scalar; returns a ``FitResult``.""" - from struphy.diagnostics.analysis import GrowthFit, damping_rate - - return damping_rate(self._array(product), GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def envelope(self, product: str | xr.DataArray) -> xr.DataArray: - """Return the local maxima of a time series, e.g. to overlay on the signal.""" - from struphy.diagnostics.analysis import envelope - - return envelope(self._array(product)) - - def norm(self, product: str | xr.DataArray, *, dims=None, squared: bool = False) -> xr.DataArray: - """Return the L2 norm over ``dims`` (default: all but ``t``), as a function of time.""" - from struphy.diagnostics.analysis import norm - - return norm(self._array(product), dims=dims, squared=squared) - - def spatial_average(self, product: str | xr.DataArray, *, dims=None) -> xr.DataArray: - """Mean of a product over ``e1``, ``e2``, ``e3`` (or ``dims``), e.g. f(t, v1) from f(t, e1, v1).""" - from struphy.diagnostics.analysis import spatial_average - - return spatial_average(self._array(product), dims=dims) - - def velocity_moments(self, product: str | xr.DataArray, *, dims=None) -> xr.Dataset: - """Density, mean velocity and variance of a binned distribution, as functions of the other dimensions. - - See :func:`struphy.diagnostics.analysis.velocity_moments`. - """ - from struphy.diagnostics.analysis import velocity_moments - - return velocity_moments(self._array(product), dims=dims) - def with_physical_coords(self, product: str | xr.DataArray) -> xr.DataArray: """Attach mapped ``X``, ``Y``, ``Z`` coordinates to a product on a logical grid. @@ -544,18 +504,6 @@ def with_physical_coords(self, product: str | xr.DataArray) -> xr.DataArray: array = array.assign_coords({name: (dims, values[keep])}) return array - def drift(self, product: str | xr.DataArray, *, ref=None) -> xr.DataArray: - """Return the deviation of a time series from a reference or its initial value.""" - from struphy.diagnostics.analysis import drift - - return drift(self._array(product), ref=ref) - - def relative_error(self, product: str | xr.DataArray, *, ref=None, skip_first: bool = True) -> xr.DataArray: - """Return the absolute relative deviation of a time series.""" - from struphy.diagnostics.analysis import relative_error - - return relative_error(self._array(product), ref=ref, skip_first=skip_first) - def keys(self) -> tuple[str, ...]: """Return the names accepted by :meth:`evaluate`, without loading their arrays. @@ -600,71 +548,6 @@ def _array(self, product: str | xr.DataArray) -> xr.DataArray: return product raise TypeError(f"product must be a product name or xarray.DataArray, got {type(product).__name__}") - def timeseries(self, product: str | xr.DataArray, *others: str | xr.DataArray, **kwargs): - """Plot one or more scalar products; see :meth:`ArrayPlots.timeseries`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - result = ArrayPlots(self._array(product)).timeseries(*(self._array(other) for other in others), **kwargs) - return result.fig, result.ax - - def view(self, product: str | xr.DataArray, **kwargs): - """Configure a reusable slice view of one product; see :meth:`ArrayPlots.view`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - return ArrayPlots(self._array(product)).view(**kwargs) - - def slice(self, product: str | xr.DataArray, *, ax=None, **kwargs): - """Render one two-dimensional slice; see :meth:`ArrayPlots.slice`.""" - result = self.view(product, **kwargs).slice(ax=ax) - return result.fig, result.ax - - def panels(self, product: str | xr.DataArray, **kwargs): - """Render evenly spaced snapshots; see :meth:`ArrayPlots.panels`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - result = ArrayPlots(self._array(product)).panels(**kwargs) - return result.fig, result.ax - - def viewer(self, product: str | xr.DataArray, **kwargs): - """Create an interactive slice viewer; see :meth:`ArrayPlots.viewer`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - viewer = ArrayPlots(self._array(product)).viewer(**kwargs) - result = viewer.draw() - result.fig._struphy_viewer = viewer - return result.fig, result.ax - - def animation(self, product: str | xr.DataArray, **kwargs): - """Create a slice animation; see :meth:`ArrayPlots.animation`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - animation = ArrayPlots(self._array(product)).animation(**kwargs) - animation._fig._struphy_animation = animation - return animation._fig, animation._fig.axes[0] - - def frames(self, product: str | xr.DataArray, directory, **kwargs): - """Export slice frames; see :meth:`ArrayPlots.frames`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - return ArrayPlots(self._array(product)).frames(directory, **kwargs) - - def trajectories(self, product: str | xr.DataArray, **kwargs): - """Plot saved marker trajectories; see :meth:`ArrayPlots.trajectories`.""" - from struphy.post_processing.xarray_accessors import ArrayPlots - - result = ArrayPlots(self._array(product)).trajectories(**kwargs) - return result.fig, result.ax - - def plot_scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): - """Plot an overview of the scalar time series of this run.""" - result = OutputPlots(self).scalars(names=names, relative_to=relative_to, logy=logy) - return result.fig, result.ax - - def equilibrium(self, ax=None): - """Plot the radial equilibrium profiles saved with this run.""" - result = OutputPlots(self).equilibrium(ax=ax) - return result.fig, result.ax - @property def units(self): """The units of the run's normalization, in SI; see :class:`struphy.physics.physics.Units`.""" @@ -2038,15 +1921,6 @@ def density_catalog(self) -> ProductMapping: def orbit_catalog(self) -> ProductMapping: return self._product_mappings()["orbits"] - @property - def plot(self) -> OutputPlots: - """Compatibility namespace for whole-run plots. - - Prefer :meth:`plot_scalars` and :meth:`equilibrium`; product plots are direct methods - of :class:`Output`, such as :meth:`viewer` and :meth:`timeseries`. - """ - return OutputPlots(self) - @property def f(self) -> DistributionProducts: """Deprecated alias of :attr:`distributions`.""" @@ -2308,7 +2182,7 @@ def info(self) -> None: "- Use out.initial_conditions for reconstructed backgrounds, perturbations, and distributions.", "- Saved Python initial conditions are reconstructed from source; unsupported definitions remain in out.metadata.", "- Use out.keys(), out.fields, out.distributions, out.densities, and out.orbits to discover products.", - "- Use out.evaluate(key), out.pproc(...), and array.struphy.plot.* to load and plot products.", + "- Use out.evaluate(key) and out.pproc(...) to load and process products.", "", f"{'Key':<{key_width}} Description", f"{'-' * key_width} -----------", @@ -2370,16 +2244,6 @@ def save_scalars(self, path=None, **kwargs) -> str: path = Path(path) if path else self.path_pproc / "scalars.csv" return save_scalars(self.scalars, str(path), **kwargs) - def save_report(self, directory=None, **kwargs) -> list[str]: - """Write the standard report: a scalar table, the scalar overview and one figure per scalar. - - Files go to ``post_processing/report/`` by default; returns their paths. - """ - from struphy.diagnostics.plotting import save_all_scalars - - directory = Path(directory) if directory else self.path_pproc / "report" - return save_all_scalars(self.scalars, directory, run_label=self.label, **kwargs) - def report(self, directory=None, *, products=(), format: str = "markdown", max_scalar_rows: int = 200) -> str: """Write a compact, reproducible data report and return its path. diff --git a/src/struphy/post_processing/output_accessors.py b/src/struphy/post_processing/output_accessors.py deleted file mode 100644 index 3e2684d55..000000000 --- a/src/struphy/post_processing/output_accessors.py +++ /dev/null @@ -1,52 +0,0 @@ -"""``out.plot``: plots that need a whole run. - -Plots and diagnostics of a single array live on the array, see -:class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`: -``out.em_fields.phi_log.struphy.plot.slice(...)``, or by name -``out.evaluate("em_fields/phi_log").struphy.plot.slice(...)``. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import struphy.post_processing.xarray_accessors # noqa: F401 (registers array.struphy) - -if TYPE_CHECKING: - from struphy.post_processing.output import Output - - -class OutputPlots: - """Plots of a whole run, as ``out.plot.(...)``. - - They return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` - and ``.save(path)``, titled with the run's numerical parameters. Plots of one product are - methods of that product, e.g. ``out.kinetic_ions.orbits.struphy.plot.trajectories()``. - """ - - def __init__(self, output: "Output"): - self._output = output - - def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): - """Overview of the scalar time series in one axes. - - Parameters - ---------- - names: - Scalars to show; all by default. - relative_to: - Show every scalar divided by this one. - logy: - Logarithmic value axis. - """ - from struphy.diagnostics.plotting import plot_scalars - - return plot_scalars( - self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label - ) - - def equilibrium(self, ax=None): - """Radial equilibrium profiles, from the geometry written at the start of the run.""" - from struphy.diagnostics.plotting import plot_equilibrium_profile - - return plot_equilibrium_profile(self._output.path_out, ax=ax) diff --git a/src/struphy/post_processing/tests/test_derived_products.py b/src/struphy/post_processing/tests/test_derived_products.py deleted file mode 100644 index 5eeb33a1f..000000000 --- a/src/struphy/post_processing/tests/test_derived_products.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Tests for distribution reductions, SI conversion and profiling access.""" - -import os -import time - -import numpy as np -import pytest -import xarray as xr - -from struphy.diagnostics.analysis import spatial_average, velocity_moments -from struphy.post_processing.arrays import data_array -from struphy.post_processing.output import Output -from struphy.post_processing.tests.test_output import write_tree - -F = "kinetic_ions/e1_v1_density/f" - - -def gaussian(v, density, mean, variance): - return density * np.exp(-((v - mean) ** 2) / (2 * variance)) / np.sqrt(2 * np.pi * variance) - - -def binned(values, dims, coords, name="f"): - return data_array(values, dims, coords, name=name, label="$f$") - - -@pytest.fixture -def run(tmp_path): - return Output(write_tree(str(tmp_path))) - - -# --- reductions --------------------------------------------------------------------------------- - - -def test_moments_of_a_maxwellian_recover_its_parameters(): - v = np.linspace(-8, 8, 321) - density = np.array([1.0, 2.0])[:, None, None] # depends on t - mean = np.array([-0.5, 0.0, 0.5])[None, :, None] # depends on e1 - f = binned( - gaussian(v[None, None, :], density, mean, 0.64), - ("t", "e1", "v1"), - {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": v}, - ) - moments = velocity_moments(f) - assert moments.density.dims == ("t", "e1") - np.testing.assert_allclose(moments.density, np.broadcast_to(density[:, :, 0], (2, 3)), rtol=1e-8) - np.testing.assert_allclose(moments.mean_v1, np.broadcast_to(mean[:, :, 0], (2, 3)), atol=1e-8) - np.testing.assert_allclose(moments.variance_v1, 0.64, rtol=1e-8) - - -def test_moments_over_two_velocity_dimensions_are_taken_per_direction(): - v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) - f = binned( - (gaussian(v1[:, None], 1.0, 1.0, 0.5) * gaussian(v2[None, :], 3.0, -0.5, 0.25))[None], - ("t", "v1", "v2"), - {"t": [0.0], "v1": v1, "v2": v2}, - ) - moments = velocity_moments(f) - assert set(moments.data_vars) == {"density", "mean_v1", "variance_v1", "mean_v2", "variance_v2"} - np.testing.assert_allclose(moments.density, 3.0, rtol=1e-8) - np.testing.assert_allclose(moments.mean_v1, 1.0, atol=1e-8) - np.testing.assert_allclose(moments.variance_v1, 0.5, rtol=1e-8) - np.testing.assert_allclose(moments.mean_v2, -0.5, atol=1e-8) - np.testing.assert_allclose(moments.variance_v2, 0.25, rtol=1e-8) - - -def test_one_velocity_dimension_can_be_selected(): - v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) - f = binned(np.ones((1, 181, 121)), ("t", "v1", "v2"), {"t": [0.0], "v1": v1, "v2": v2}) - moments = velocity_moments(f, dims="v2") - assert moments.density.dims == ("t", "v1") - assert "mean_v1" not in moments - - -def test_delta_f_has_only_a_density(): - v = np.linspace(-3, 3, 7) - delta_f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}, name="delta_f") - assert tuple(velocity_moments(delta_f).data_vars) == ("density",) - - -def test_mean_and_variance_are_nan_without_particles(): - v = np.linspace(-3, 3, 7) - f = binned(np.zeros((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}) - moments = velocity_moments(f) - assert moments.density.item() == 0.0 - assert np.isnan(moments.mean_v1.item()) and np.isnan(moments.variance_v1.item()) - - -def test_moments_carry_the_run_and_a_label(): - f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": np.linspace(-3, 3, 7)}) - f.attrs.update(run="dt=0.1", run_name="sim_1") - moments = velocity_moments(f) - assert moments.attrs["run_name"] == "sim_1" - assert moments.mean_v1.attrs["run"] == "dt=0.1" - assert moments.density.attrs["label"] == "density of $f$" - - -def test_moments_reject_missing_velocity_dimensions_and_single_bins(): - no_velocity = binned(np.ones((2, 3)), ("t", "e1"), {"t": [0.0, 1.0], "e1": [0.1, 0.2, 0.3]}) - with pytest.raises(ValueError, match="none of the dimensions"): - velocity_moments(no_velocity) - with pytest.raises(ValueError, match="no dimensions"): - velocity_moments(no_velocity, dims="v1") - one_bin = binned(np.ones((1, 1)), ("t", "v1"), {"t": [0.0], "v1": [0.0]}) - with pytest.raises(ValueError, match="at least two bins"): - velocity_moments(one_bin) - - -def test_spatial_average_removes_the_space_dimensions_only(): - values = np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4) - f = binned(values, ("t", "e1", "v1"), {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": np.arange(4.0)}) - f.attrs["run_name"] = "sim_1" - mean = spatial_average(f) - assert mean.dims == ("t", "v1") - np.testing.assert_allclose(mean, values.mean(axis=1)) - assert mean.attrs["run_name"] == "sim_1" - assert mean.attrs["label"] == "average of $f$" - assert spatial_average(f, dims="e1").dims == ("t", "v1") - - -def test_spatial_average_drops_physical_coordinates_it_averaged_over(): - logical = {f"e{i + 1}": np.linspace(0, 1, n) for i, n in enumerate((3, 4, 1))} - mapped = np.meshgrid(*logical.values(), indexing="ij") - field = data_array( - np.ones((2, 3, 4, 1)), - ("t", "e1", "e2", "e3"), - {"t": [0.0, 1.0], **logical, "X": (("e1", "e2", "e3"), mapped[0])}, - name="E", - ) - mean = spatial_average(field) - assert mean.dims == ("t",) - assert "X" not in mean.coords - - -def test_spatial_average_needs_space_dimensions(): - series = data_array(np.ones(3), ("t",), {"t": [0.0, 1.0, 2.0]}, name="energy") - with pytest.raises(ValueError, match="none of the dimensions"): - spatial_average(series) - - -def test_reductions_are_available_from_the_run_and_the_accessor(run): - moments = run.velocity_moments(F) - # write_tree has f = 1 on v = -3..3 in unit bins: n = 7, u = 0 and = 4 - np.testing.assert_allclose(moments.density, 7.0) - np.testing.assert_allclose(moments.mean_v1, 0.0, atol=1e-12) - np.testing.assert_allclose(moments.variance_v1, 4.0) - assert moments.density.attrs["run_name"] == run.path_out.name - - average = run.spatial_average(F) - assert average.dims == ("t", "v1") - xr.testing.assert_identical(average, run.evaluate(F).struphy.analysis.spatial_average()) - xr.testing.assert_identical(moments, run.evaluate(F).struphy.analysis.velocity_moments()) - - -# --- SI units --------------------------------------------------------------------------------- - - -def test_coordinates_are_converted_and_values_left_alone(run): - units = run.units - f = run.to_si(F) - np.testing.assert_allclose(f.v1, run.evaluate(F).v1 * units.v) - assert f.v1.attrs["units"] == "m/s" - np.testing.assert_allclose(f.t, run.evaluate(F).t * units.t) - assert f.t.attrs["units"] == "s" - assert "t_seconds" not in f.coords - np.testing.assert_array_equal(f.e1, run.evaluate(F).e1) # logical coordinates are dimensionless - np.testing.assert_array_equal(f, run.evaluate(F)) - assert "units" not in f.attrs - assert run.evaluate(F).v1.attrs.get("units") is None # the run's own product is untouched - assert "t_seconds" in run.evaluate(F).coords - - -def test_mapped_coordinates_are_scaled_by_the_length_unit(run): - assert run.units.x == 2.0 - field = run.to_si("em_fields/E") - np.testing.assert_allclose(field.X, run.evaluate("em_fields/E").X * 2.0) - assert field.X.attrs["units"] == "m" - - -def test_values_are_converted_with_a_named_unit(run): - field = run.to_si("em_fields/E", "B") - np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.B) - assert field.attrs["units"] == "T" - assert field.name == "E" - assert field.attrs["run_name"] == run.path_out.name - - -def test_values_are_converted_with_a_composite_unit(run): - field = run.to_si("em_fields/E", run.units.v * run.units.B, label="V/m") - np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.v * run.units.B) - assert field.attrs["units"] == "V/m" - - -def test_conversion_is_idempotent_for_coordinates_and_refuses_values_twice(run): - once = run.to_si(F) - np.testing.assert_array_equal(run.to_si(once).v1, once.v1) - converted = run.to_si("em_fields/E", "B") - with pytest.raises(ValueError, match="already has units"): - run.to_si(converted, "B") - - -def test_unknown_units_are_rejected(run): - with pytest.raises(ValueError, match="unknown unit"): - run.to_si("em_fields/E", "furlong") - - -def test_physical_time_units_are_not_converted_twice(tmp_path): - physical = Output(write_tree(str(tmp_path))).with_time_units("physical") - np.testing.assert_allclose(physical.to_si(F).t, physical[F].t) - - -# --- profiling -------------------------------------------------------------------------------- - - -def write_profile(path_out, *, calls=3, setup=True): - from scope_profiler import ProfileManager, ProfilingOptions - - with ProfileManager.session( - options=ProfilingOptions(), - deactivate_profiling=False, - file_path=os.path.join(path_out, "profiling_data.h5"), - ): - if setup: - with ProfileManager.profile_region("setup: total"): - time.sleep(0.001) - for _ in range(calls): - with ProfileManager.profile_region("prop: A"): - with ProfileManager.profile_region("kernel: k"): - time.sleep(0.01) - - -@pytest.fixture -def profiled(tmp_path, capfd): - write_profile(write_tree(str(tmp_path))) - return Output(str(tmp_path)) - - -def test_a_run_without_profiling_says_how_to_enable_it(run): - with pytest.raises(FileNotFoundError, match="profiling_activated=True"): - run.profile - - -def test_summary_lists_every_region_with_times(profiled): - summary = profiled.profile.summary() - assert summary.sizes == {"region": 4} - assert list(summary.region.values[:1]) == ["scope_profiler.session"] - assert summary.region.values[-1] == "setup: total" - assert summary.calls.sel(region="prop: A").item() == 3 - assert summary.calls.sel(region="setup: total").item() == 1 - assert summary.total_time.sel(region="kernel: k").item() >= 0.03 - assert summary.mean_time.sel(region="kernel: k").item() >= 0.01 - assert summary.fraction.sel(region="scope_profiler.session").item() == pytest.approx(1.0) - assert 0 < summary.fraction.sel(region="prop: A").item() <= 1.0 - assert summary.attrs["run"] == profiled.label - assert summary.attrs["num_ranks"] == 1 - assert summary.total_time.attrs["units"] == "s" - - -def test_summary_filters_and_sorts(profiled): - profile = profiled.profile - assert list(profile.summary(prefix="kernel:").region.values) == ["kernel: k"] - assert len(profile.summary(top=2).region) == 2 - by_calls = profile.summary(sort_by="calls") - assert by_calls.region.values[0] == "prop: A" - with pytest.raises(ValueError, match="cannot sort by"): - profile.summary(sort_by="size") - - -def test_the_profile_is_cached_and_reset_with_the_output(profiled): - assert profiled.profile is profiled.profile - first = profiled.profile - profiled.clear_cache() - assert profiled.profile is not first - - -def test_table_is_readable_text(profiled): - table = profiled.profile.table(top=3) - lines = table.splitlines() - assert lines[0].startswith("Profile: ") and "1 rank(s)" in lines[0] - assert "Total [s]" in lines[2] - assert len(lines) == 4 + 3 - assert "scope_profiler.session" in table and "100.0%" in table - - -def test_runs_are_compared_side_by_side(tmp_path, capfd): - first = Output(write_tree(str(tmp_path / "a"))) - second = Output(write_tree(str(tmp_path / "b"))) - write_profile(first.path_out, calls=3) - write_profile(second.path_out, calls=2, setup=False) - - table = first.profile.compare(second, metric="calls") - assert set(table.dims) == {"region", "run"} - assert list(table.run.values) == [f"{first.label} [a]", f"{second.label} [b]"] # identical labels are told apart - assert table.sel(run=table.run.values[0], region="prop: A").item() == 3 - assert table.sel(run=table.run.values[1], region="prop: A").item() == 2 - assert np.isnan(table.sel(run=table.run.values[1], region="setup: total").item()) - assert table.name == "calls" - assert first.profile.compare(second, prefix="kernel:").region.values.tolist() == ["kernel: k"] - assert first.profile.compare(second.profile).shape == table.shape # a Profile works as well as an Output - - with pytest.raises(ValueError, match="cannot compare"): - first.profile.compare(second, metric="size") diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index c9af575cb..812c485c8 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -197,11 +197,9 @@ def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): np.testing.assert_allclose(run.scalars.en_tot.t, run.time) -def test_saving_scalars_and_plotting_a_product(run, tmp_path): +def test_saving_scalars(run, tmp_path): path = run.save_scalars(tmp_path / "scalars.csv") assert os.path.exists(path) - result = run.scalars.en_tot.struphy.plot.timeseries(logy=False) - assert result.ax.get_xlabel() == "$t$" def test_open_output_needs_an_output_folder(tmp_path): @@ -492,9 +490,9 @@ def test_iter_spline_coefficients_reads_one_raw_snapshot_at_a_time(run): snapshots = list(run.iter_spline_coefficients(stride=2)) assert [time for time, _ in snapshots] == [0.0, 1.0] - assert np.array_equal(snapshots[1]["em_fields"]["phi"], np.array([4, 5])) - assert len(snapshots[0]["em_fields"]["e_field"]) == 2 - assert np.array_equal(snapshots[0]["em_fields"]["e_field"][1], np.array([2.0, 2.0])) + assert np.array_equal(snapshots[1][1]["em_fields"]["phi"], np.array([4, 5])) + assert len(snapshots[0][1]["em_fields"]["e_field"]) == 2 + assert np.array_equal(snapshots[0][1]["em_fields"]["e_field"][1], np.array([2.0, 2.0])) def test_normalized_time_carries_seconds_as_a_coordinate(run): diff --git a/src/struphy/post_processing/tests/test_output_accessors.py b/src/struphy/post_processing/tests/test_output_accessors.py deleted file mode 100644 index 3df900681..000000000 --- a/src/struphy/post_processing/tests/test_output_accessors.py +++ /dev/null @@ -1,249 +0,0 @@ -"""Tests for run.plot, run.analysis and product lookup by name.""" - -import os - -import h5py -import matplotlib - -matplotlib.use("Agg") - -import numpy as np # noqa: E402 -import pytest # noqa: E402 -from matplotlib import pyplot as plt # noqa: E402 - -from struphy.post_processing.output import Output # noqa: E402 -from struphy.post_processing.tests.test_output import write_manifest, write_tree # noqa: E402 - -RATE = 2.0 - - -def make_run(root, name="sim_1"): - path = os.path.join(root, name) - os.makedirs(path) - write_tree(path) - with h5py.File(os.path.join(path, "data", "data_proc0.hdf5"), "a") as file: - time = np.asarray(file["time/value"]) - file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) - write_manifest(path) - return Output(path) - - -@pytest.fixture -def run(tmp_path): - return make_run(str(tmp_path)) - - -@pytest.fixture(autouse=True) -def close_figures(): - yield - plt.close("all") - - -def test_products_are_found_by_name(run): - assert run.evaluate("en_tot").dims == ("t",) - assert run.evaluate("em_fields/E").dims[:2] == ("t", "component") - assert run.evaluate("kinetic_ions/e1_v1_density/f").dims == ("t", "e1", "v1") - assert run.evaluate("kinetic_ions/view_0/n").dims == ("t", "e1", "e2", "e3") - assert run.evaluate("kinetic_ions").dims == ("t", "marker", "quantity") - with pytest.raises(KeyError, match="available products"): - run.evaluate("t") - - -def test_every_array_carries_its_run(run): - for array in (run.scalars.en_tot, run.fields.em_fields.E, run.evaluate("kinetic_ions")): - assert array.attrs["run"] == run.label - assert array.attrs["run_name"] == "sim_1" - assert run.scalars.en_tot.isel(t=slice(1, None)).attrs["run_name"] == "sim_1" - - -def test_timeseries_by_name_with_growth_fit(run): - result = run.evaluate("en_phi").struphy.plot.timeseries(fit=True) - assert result.fit_results[0].rate == pytest.approx(RATE) - assert result.fig._suptitle.get_text() == run.label - - -def test_output_owns_product_plotting(run): - fig, ax = run.timeseries("en_phi", fit=True) - assert fig is ax.figure - - phase_space = run.evaluate("kinetic_ions/e1_v1_density/f").isel(t=-1) - _, ax = run.slice(phase_space, x="e1", y="v1") - assert ax.get_xlabel() == r"$\eta_1$" - - fig, _ = run.viewer("em_fields/E", x="e1", y="e2", component=0) - assert set(fig._struphy_viewer.sliders) == {"t", "e3"} - - _, ax = run.trajectories("kinetic_ions", max_markers=2) - assert ax.name == "3d" - - -def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): - first, second = make_run(str(tmp_path), "sim_1"), make_run(str(tmp_path), "sim_2") - result = first.scalars.en_phi.struphy.plot.timeseries(second.scalars.en_phi) - labels = [text.get_text() for text in result.ax.get_legend().get_texts()] - assert labels == ["en phi (sim_1)", "en phi (sim_2)"] - - -def test_timeseries_into_given_axes_keeps_the_figure_layout(run): - fig, ax = plt.subplots() - fig.suptitle("mine") - run.evaluate("en_tot").struphy.plot.timeseries(ax=ax, logy=False) - assert fig._suptitle.get_text() == "mine" - - -def test_scalar_overview_draws_every_scalar_in_one_axes(run): - fig, ax = run.plot_scalars() - assert sorted(line.get_label() for line in ax.lines) == ["en_phi", "en_tot"] - assert fig.axes == [ax] - - -def test_slices_panels_and_viewer_take_keyword_views(run): - name = "kinetic_ions/e1_v1_density/f" - assert run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(run.evaluate(name).struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - viewer = run.evaluate("em_fields/E").struphy.plot.viewer(x="e1", y="e2", component=0) - viewer.draw() - assert set(viewer.sliders) == {"t", "e3"} - - -def test_orbits_plot_their_trajectories(run): - assert run.kinetic_ions.orbits.struphy.plot.trajectories().ax.name == "3d" - - -def test_report_is_written_below_post_processing(run): - paths = run.save_report() - assert all(path.startswith(str(run.path_pproc / "report")) for path in paths) - assert {os.path.basename(path) for path in paths} >= {"scalars.csv", "scalars.png", "en_phi.png"} - - -def test_analysis_by_name(run): - assert run.evaluate("en_phi").struphy.analysis.growth_rate(window=(0.0, None)).rate == pytest.approx(RATE) - assert run.evaluate("en_phi").struphy.analysis.growth_rate(amplitude=True).rate == pytest.approx(RATE / 2) - np.testing.assert_allclose(run.evaluate("en_tot").struphy.analysis.relative_error(), 0.0) - np.testing.assert_allclose(run.evaluate("en_phi").struphy.analysis.drift().isel(t=0), 0.0) - - -def test_dispersion_rejects_fields_in_seconds(run): - physical = run.with_time_units("physical") - with pytest.raises(ValueError, match="normalized"): - physical.fields.em_fields.E.struphy.analysis.dispersion() - - -def test_selection_keywords_take_positions_values_and_ends(run): - name = "kinetic_ions/e1_v1_density/f" - times = run.evaluate(name).t.values - - by_position = run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t=-1) - by_value = run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t=float(times[-1])) - by_end = run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t="last") - for result in (by_value, by_end): - np.testing.assert_allclose(result.artists[0].get_array(), by_position.artists[0].get_array()) - - with pytest.raises(TypeError, match="not a dimension"): - run.evaluate(name).struphy.plot.slice(x="e1", y="v1", time=-1) - with pytest.raises(TypeError, match="use a number"): - run.evaluate(name).struphy.plot.slice(x="e1", y="v1", t="final") - - -def test_products_of_one_species_sit_on_the_output(run): - assert run.kinetic_ions.e1_v1_density.f.dims == ("t", "e1", "v1") - assert run.kinetic_ions.view_0.n.dims == ("t", "e1", "e2", "e3") - assert run.kinetic_ions.orbits.dims == ("t", "marker", "quantity") - assert run.em_fields.E.dims[:2] == ("t", "component") - assert {"kinetic_ions", "em_fields"} <= set(dir(run)) - with pytest.raises(AttributeError, match="available species"): - run.electrons - - -def test_product_namespaces_expose_a_scoped_lazy_catalog(run): - products = run.kinetic_ions - assert tuple(sorted(products.catalog)) == ("e1_v1_density/delta_f", "e1_v1_density/f", "orbits", "view_0/n") - assert "e1_v1_density/f" in products.catalog - assert "em_fields/E" not in products.catalog - assert "e1_v1_density/f" in repr(products) - assert run.distribution_catalog._cache == {} - assert products["e1_v1_density/f"].dims == ("t", "e1", "v1") - assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f"] is products.catalog["e1_v1_density/f"] - - -def test_arrays_plot_themselves(run): - phase_space = run.kinetic_ions.e1_v1_density.f - assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(phase_space.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - assert set(phase_space.struphy.plot.viewer(x="e1", y="v1").sliders) == set() - assert run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=2).ax.name == "3d" - - -def test_the_accessor_works_on_derived_arrays(run): - energy = run.scalars.en_phi - assert energy.isel(t=slice(1, None)).struphy.analysis.growth_rate().rate == pytest.approx(RATE) - error = energy.struphy.analysis.relative_error() - assert error.struphy.plot.timeseries(logy=False).fig._suptitle.get_text() == run.label - - -def test_products_by_name_and_by_attribute_agree(run): - by_output = run.evaluate("kinetic_ions/e1_v1_density/f").struphy.plot.slice(x="e1", y="v1", t="last") - by_attribute = run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", t="last") - np.testing.assert_allclose(by_output.artists[0].get_array(), by_attribute.artists[0].get_array()) - assert by_output.fig._suptitle.get_text() == by_attribute.fig._suptitle.get_text() == run.label - - -def test_selection_rejects_unknown_dimensions(run): - with pytest.raises(TypeError, match="not a dimension"): - run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", time=-1) - - -def oscillating_energy(rate=-0.3, omega=3.0): - import xarray as xr - - time = np.linspace(0.0, 20.0, 4001) - values = np.exp(2 * rate * time) * np.cos(omega * time) ** 2 + 1e-12 - return xr.DataArray(values, dims="t", coords={"t": time}, name="energy") - - -def test_damping_rate_fits_the_envelope_not_the_oscillation(run): - energy = oscillating_energy(rate=-0.3) - fit = run.damping_rate(energy, amplitude=True) - assert fit.rate == pytest.approx(-0.3, rel=1e-2) - assert energy.struphy.analysis.damping_rate(window=(2.0, 10.0), amplitude=True).rate == pytest.approx( - -0.3, rel=1e-2 - ) - - peaks = run.envelope(energy) - assert 0 < peaks.sizes["t"] < energy.sizes["t"] // 10 - assert np.all(peaks > 1e-3 * np.exp(-0.6 * peaks.t)) - - -def test_damping_rate_without_peaks_is_none(run): - assert run.damping_rate(run.evaluate("en_phi")) is None - - -def test_norm_reduces_all_but_time(run): - e_field = run.evaluate("em_fields/E") - squared = run.norm(e_field, squared=True) - assert squared.dims == ("t",) - np.testing.assert_allclose(squared, (np.asarray(e_field) ** 2).sum(axis=(1, 2, 3, 4))) - np.testing.assert_allclose(run.norm("em_fields/E") ** 2, squared) - assert e_field.struphy.analysis.norm(dims=["e1"]).dims == ("t", "component", "e2", "e3") - assert run.growth_rate(run.norm("em_fields/E", squared=True), amplitude=True) is not None - - -def test_physical_coords_are_attached_to_products_without_them(run): - density = run.evaluate("kinetic_ions/view_0/n") - assert "X" not in density.coords - mapped = run.with_physical_coords(density) - expected = run.domain(*(np.asarray(density[dim]) for dim in ("e1", "e2", "e3"))) - for name, values in zip(("X", "Y", "Z"), expected): - assert mapped[name].dims == ("e1", "e2", "e3") - np.testing.assert_allclose(mapped[name], values) - - plane = run.with_physical_coords(density.isel(e3=0, drop=True)) - assert plane.X.dims == ("e1", "e2") - - phase_space = run.with_physical_coords("kinetic_ions/e1_v1_density/f") - assert phase_space.X.dims == ("e1",) - - field = run.evaluate("em_fields/E") - assert run.with_physical_coords(field) is field - with pytest.raises(ValueError, match="no logical dimensions"): - run.with_physical_coords("en_tot") diff --git a/src/struphy/post_processing/xarray_accessors.py b/src/struphy/post_processing/xarray_accessors.py deleted file mode 100644 index 1f8fd81d6..000000000 --- a/src/struphy/post_processing/xarray_accessors.py +++ /dev/null @@ -1,450 +0,0 @@ -"""Compatibility accessors for plots and diagnostics of a single labeled array. - -Every product of an :class:`~struphy.Output` carries this accessor, and so does every array -derived from one. New code should use the direct methods of ``Output`` instead, for example -``out.timeseries("en_phi")`` or ``out.slice(array, x="e1", y="v1")``. - -Dimensions that are neither displayed nor swept are selected by naming them: an integer is a -position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest -coordinate value (``t=0.35``). -""" - -from __future__ import annotations - -from typing import Literal - -import numpy as np -import xarray as xr - -Coordinates = Literal["logical", "physical"] -Plane = Literal["XY", "XZ", "YZ", "RZ"] - - -@xr.register_dataarray_accessor("struphy") -class StruphyAccessor: - """Struphy diagnostics of one array: ``array.struphy.plot`` and ``array.struphy.analysis``.""" - - def __init__(self, array: xr.DataArray): - self._array = array - - @property - def plot(self) -> "ArrayPlots": - """Plots of this array, e.g. ``array.struphy.plot.slice(x="e1", y="v1", t="last")``.""" - return ArrayPlots(self._array) - - @property - def analysis(self) -> "ArrayAnalysis": - """Diagnostics of this array, e.g. ``array.struphy.analysis.growth_rate()``.""" - return ArrayAnalysis(self._array) - - -class _ArrayAccessor: - def __init__(self, array: xr.DataArray): - self._array = array - - -class ArrayPlots(_ArrayAccessor): - """Plots of one array, as ``array.struphy.plot.(...)``. - - Dimensions that are neither displayed nor swept are selected by naming them: an integer is a - position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest - coordinate value (``t=0.35``). - """ - - def _view(self, x, y, sweep, coords, plane, selection): - from struphy.diagnostics.plotting import View - - select, index = {}, {} - for dim, value in selection.items(): - if dim not in self._array.dims: - raise TypeError( - f"{dim!r} is not a dimension of {self._array.name!r}; its dimensions are {self._array.dims}" - ) - if value == "first": - index[dim] = 0 - elif value == "last": - index[dim] = -1 - elif isinstance(value, (bool, str)): - raise TypeError(f'cannot select {dim}={value!r}; use a number, or "first"/"last"') - elif isinstance(value, (int, np.integer)): - index[dim] = int(value) - else: - select[dim] = float(value) - return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) - - def timeseries( - self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, title: str | None = None, ax=None - ): - """This time series, and any others given, in one axes. - - Parameters - ---------- - *others: - Further arrays with the single dimension ``t``; they may come from other runs and - need not share this array's time grid. - logy: - Logarithmic value axis. - fit: - Time window ``(t0, t1)`` of an exponential fit per series (``None`` for an open end), - or ``True`` for the whole series. Rates are in ``result.fit_results``. - fit_amplitude: - The series is quadratic in an amplitude (e.g. an energy); fit the amplitude's rate. - """ - from struphy.diagnostics.plotting import GrowthFit, plot_timeseries - - growth = None - if fit is not None and fit is not False: - window = (None, None) if fit is True else tuple(fit) - growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) - return plot_timeseries([self._array, *others], ax=ax, logy=logy, fit=growth, title=title) - - def view( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - **selection, - ) -> "SliceView": - """Configure a reusable slice view without rendering a figure. - - Use xarray's ``.sel()``/``.isel()`` for general selection, or pass remaining - dimensions here (integers are positions, floats nearest coordinates, - ``"first"``/``"last"`` select an end). - - ``shared_clim=True`` fixes color limits over all selected data, including - frames omitted by a panel layout or export step. False rescales each frame. - Explicit ``vmin``/``vmax`` override either limit in both modes. ``cmap``, - ``equal_aspect`` and ``title`` apply to every presentation of this view. - - Examples - -------- - >>> view = f.struphy.plot.view(x="e1", y="v1", cmap="RdBu_r") - >>> view.slice(t="last") - >>> view.panels(nrows=2, ncols=3) - >>> view.save_frames("frames") - """ - self._view(x, y, sweep, coords, plane, selection) # validate selections now - return SliceView( - self._array, - dict(x=x, y=y, sweep=sweep, coords=coords, plane=plane), - selection, - dict(vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title), - ) - - def slice( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - ax=None, - **selection, - ): - """Render one 2-D slice; see :meth:`view` for shared options.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).slice(ax=ax) - - def panels( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - nrows: int = 3, - ncols: int = 4, - **selection, - ): - """Render evenly spaced snapshots; see :meth:`view` for shared options.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).panels(nrows=nrows, ncols=ncols) - - def viewer( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - **selection, - ): - """Create an interactive slider view; retain the returned viewer.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).viewer() - - def animation( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - interval: int = 100, - step: int = 1, - **selection, - ): - """Animate the sweep; retain the returned Matplotlib animation.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).animation(interval=interval, step=step) - - def frames( - self, - directory, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - step: int = 1, - prefix: str = "frame", - dpi: int = 110, - **selection, - ): - """Export PNGs; equivalent to ``plot.view(...).save_frames(directory)``.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).save_frames(directory, step=step, prefix=prefix, dpi=dpi) - - def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): - """Three-dimensional paths of saved markers; for an orbit product.""" - from struphy.diagnostics.plotting import plot_marker_trajectories - - return plot_marker_trajectories(self._array, ax=ax, max_markers=max_markers, show_paths=show_paths) - - -class SliceView: - """A configured array view, shared by static, interactive and exported plots. - - Construct with ``array.struphy.plot.view(...)``. Configuration does not create - figures or copy the underlying array. - """ - - def __init__(self, array, coordinates, selection, options): - self._array = array - self._coordinates = dict(coordinates) - self._selection = dict(selection) - self._options = dict(options) - - def _view(self, **selection): - return ArrayPlots(self._array)._view(**self._coordinates, selection={**self._selection, **selection}) - - def slice(self, *, ax=None, **selection): - """Draw a snapshot, e.g. ``view.slice(t="last")``; return a PlotResult.""" - # Resolve shared limits before selecting a single snapshot, so it uses - # the same scale as panels, animation and export of this configured view. - from struphy.diagnostics.plotting import _SliceRenderer, plot_slice - - options = dict(self._options) - if options["shared_clim"]: - renderer = _SliceRenderer(self._array, self._view(), **options) - options.update(zip(("vmin", "vmax"), renderer.limits)) - return plot_slice(self._array, view=self._view(**selection), ax=ax, **options) - - def panels(self, *, nrows=3, ncols=4): - """Draw snapshots spread along the sweep; return a PlotResult.""" - from struphy.diagnostics.plotting import plot_panels - - return plot_panels(self._array, view=self._view(), nrows=nrows, ncols=ncols, **self._options) - - def viewer(self): - """Create a viewer with sliders for unselected dimensions.""" - from struphy.diagnostics.plotting import InteractiveSliceViewer - - return InteractiveSliceViewer(self._array, view=self._view(), **self._options) - - def animation(self, *, interval=100, step=1): - """Create a Matplotlib animation using this view's rendering options.""" - from struphy.diagnostics.plotting import animate_slices - - return animate_slices(self._array, view=self._view(), interval=interval, step=step, **self._options) - - def save_frames(self, directory, *, step=1, prefix="frame", dpi=110): - """Export PNG frames using this view's rendering options; return paths.""" - from struphy.diagnostics.plotting import save_frames - - return save_frames( - self._array, directory, view=self._view(), step=step, prefix=prefix, dpi=dpi, **self._options - ) - - -class ArrayAnalysis(_ArrayAccessor): - """Quantitative diagnostics of one array, as ``array.struphy.analysis.(...)``.""" - - def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): - """Fit ``exp(rate * t + intercept)`` to this time series within ``window``. - - With ``amplitude=True`` the series is quadratic in an amplitude (e.g. an energy) and the - amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, - ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. - """ - from struphy.diagnostics.analysis import GrowthFit, growth_rate - - return growth_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def damping_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): - """Fit exponential decay to the envelope of this oscillating time series; see ``growth_rate``.""" - from struphy.diagnostics.analysis import GrowthFit, damping_rate - - return damping_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def envelope(self) -> xr.DataArray: - """Local maxima of this time series.""" - from struphy.diagnostics.analysis import envelope - - return envelope(self._array) - - def norm(self, *, dims=None, squared: bool = False) -> xr.DataArray: - """L2 norm over ``dims`` (default: every dimension except ``t``).""" - from struphy.diagnostics.analysis import norm - - return norm(self._array, dims=dims, squared=squared) - - def drift(self, *, ref=None) -> xr.DataArray: - """Signed deviation of this time series from ``ref`` or from its first sample.""" - from struphy.diagnostics.analysis import drift - - return drift(self._array, ref=ref) - - def relative_error(self, *, ref=None, skip_first: bool = True) -> xr.DataArray: - """Absolute relative deviation from ``ref`` or from this series' first sample.""" - from struphy.diagnostics.analysis import relative_error - - return relative_error(self._array, ref=ref, skip_first=skip_first) - - def spatial_average(self, *, dims=None) -> xr.DataArray: - """Mean over the logical space dimensions ``e1``, ``e2``, ``e3`` (or ``dims``). - - For a binned ``e1_v1`` distribution this is f(v1, t) averaged over space; see - :func:`struphy.diagnostics.analysis.spatial_average`. - """ - from struphy.diagnostics.analysis import spatial_average - - return spatial_average(self._array, dims=dims) - - def velocity_moments(self, *, dims=None) -> xr.Dataset: - """Density, mean velocity and variance of a binned distribution over its velocity dimensions. - - See :func:`struphy.diagnostics.analysis.velocity_moments` for the definitions. - """ - from struphy.diagnostics.analysis import velocity_moments - - return velocity_moments(self._array, dims=dims) - - def dispersion(self, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs): - """Space-time power spectrum of this field and fitted dispersion branches. - - The time coordinate must be normalized, see :meth:`struphy.Output.with_time_units`. See - :func:`struphy.diagnostics.diagn_tools.power_spectrum_2d` for ``slice_at``, the fit options - and ``do_plot``. Returns ``(omega, kvec, spectrum, coeffs)``. - """ - from struphy.diagnostics.diagn_tools import power_spectrum_2d - - if self._array.t.attrs.get("units") == "s": - raise ValueError( - "the spectrum needs normalized time; take the field from out.with_time_units('normalized')" - ) - return power_spectrum_2d(self._array, component=component, slice_at=slice_at, physical=physical, **kwargs) From 986d9580238f2f6c3fc050d39a7c4eab299b687e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 18:59:32 +0200 Subject: [PATCH 120/193] Removed the legacy postprocessing code --- doc/sections/userguide.rst | 28 ++-- .../diocotron_instability/pproc_diocotron.py | 2 +- src/struphy/models/tests/utils_testing.py | 2 +- ...est_verif_IncompressibleNavierStokesSPH.py | 6 +- .../verification/test_verif_LinearMHD.py | 2 +- .../tests/verification/test_verif_Maxwell.py | 4 +- .../tests/verification/test_verif_Poisson.py | 2 +- .../test_verif_ViscousEulerSPH.py | 10 +- src/struphy/post_processing/legacy.py | 67 --------- src/struphy/post_processing/manifest.py | 2 +- src/struphy/post_processing/output.py | 62 ++------ .../post_processing/tests/test_eval_grids.py | 2 +- .../tests/test_eval_grids_mpi.py | 2 +- .../post_processing/tests/test_legacy.py | 80 ----------- .../post_processing/tests/test_output.py | 11 +- .../post_processing/tests/test_pproc.py | 4 +- src/struphy/simulation/sim.py | 135 ++---------------- src/struphy/simulation/tests/test_output.py | 88 +----------- 18 files changed, 64 insertions(+), 445 deletions(-) delete mode 100644 src/struphy/post_processing/legacy.py delete mode 100644 src/struphy/post_processing/tests/test_legacy.py diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 080b973b2..b1847ff6e 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -499,20 +499,18 @@ After initial conditions are set, launch the run: 11. Post-processing and visualization ------------------------------------- -A serial postprocessor can be reconstructed from a saved output folder, including -one moved to another location: +A postprocessor can be reconstructed from a saved output folder, including one +moved to another location: .. code-block:: python - from struphy.post_processing.post_processing_tools import PostProcessor + from struphy import Output - processor = PostProcessor.from_output("./runs/my_run") - processor.process(physical=True) + output = Output("./runs/my_run") + output.pproc(physical=True) -This reads ``run_metadata.json``, falling back to legacy ``config.json`` when needed. -The original MPI rank count comes from ``run_metadata.json`` or legacy ``meta.yml``. -Under MPI, call this factory on one rank only; use ``Output.process`` for automatic -rank handling. +This reads the ``run_metadata.json`` written by the simulation. Under MPI, call +``pproc`` on every rank; serial processing runs on rank 0 while the other ranks wait. The output of a simulation is a :class:`~struphy.Output`. ``sim.run()`` returns it, and it stays available as ``sim.output``: @@ -549,18 +547,18 @@ not configuration applied to the model afterwards, such as backgrounds or pertur out.domain, out.model.units -Choosing post-processing options: ``out.process()`` +Choosing post-processing options: ``out.pproc()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Scalars need no post-processing. Fields, binned distribution functions, SPH densities and orbits are evaluated from the raw HDF5 data and written to a ``post_processing/`` sub-folder of the output directory. Without an explicit call this happens with default options the first time such a product is accessed. To -choose the options, call ``process`` first: +choose the options, call ``pproc`` first: .. code-block:: python - out.process( + out.pproc( step=1, # evaluate every N-th saved time step celldivide=1, # sub-divide each grid cell for smoother output physical=False, # also evaluate fields in physical coordinates (*_xyz) @@ -573,7 +571,7 @@ choose the options, call ``process`` first: All arguments are optional and default to the values shown above. Products that were already made from the same raw output with the same options are reused, so a -plotting script can be re-run cheaply. Under MPI, call ``process`` on every rank: +plotting script can be re-run cheaply. Under MPI, call ``pproc`` on every rank: serial processing runs on rank 0 while the other ranks wait, and ``parallel=True`` uses the allocated simulation on all ranks. @@ -669,7 +667,7 @@ trajectories are available under ``out.orbits``: VTK output for ParaView and PyVista ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you call ``out.process(create_vtk=True)``, Struphy writes structured-grid VTK +If you call ``out.pproc(create_vtk=True)``, Struphy writes structured-grid VTK files (``.vts``) inside the post-processing folder, grouped by species. Typical locations are: @@ -933,7 +931,7 @@ Gantt charts and flame graphs. Note that ``profiling_data.h5`` is a plain ``scope-profiler`` output file, so it is post-processed with ``scope-profiler`` itself rather than with -``out.process()`` — the two are independent post-processing paths. +``out.pproc()`` — the two are independent post-processing paths. Post-processing with the ``scope-profiler`` CLI diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 838dd6730..db9e136fc 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -27,7 +27,7 @@ def main(paths=(DEFAULT_OUTPUT,)): - runs = [Output(path).process(physical=True) for path in paths] + runs = [Output(path).pproc(physical=True) for path in paths] run = runs[0] # growth rate of the electrostatic energy, one curve per run diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index f0053fead..6c44293ee 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -128,7 +128,7 @@ def call_test(model: StruphyModel, test_profiling: bool = False): if comm is not None: comm.Barrier() - run.process(create_vtk=True) + run.pproc(create_vtk=True) if rank == 0: # discover (but do not load) every product for catalog in (run.field_catalog, run.distribution_catalog, run.density_catalog, run.orbit_catalog): diff --git a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py index 5d55ea0e5..6c1b9074a 100644 --- a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py +++ b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py @@ -100,7 +100,7 @@ def test_chorin_projection_periodic_1d(nx: int, do_plot: bool = False): ) run = sim.run() - run.process() + run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: e1_grid = run.distributions.fluid.e1_current_1.f.e1.values.flatten() @@ -206,7 +206,7 @@ def test_chorin_projection_reflect_1d(nx: int, do_plot: bool = False): ) run = sim.run() - run.process() + run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: e1_grid = run.distributions.fluid.e1_current_1.f.e1.values.flatten() @@ -317,7 +317,7 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): ) run = sim.run() - run.process() + run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: e2_grid = run.distributions.fluid.e2_current_1.f.e2.values.flatten() diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 878999e90..bfc06db48 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -78,7 +78,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): run = sim.run().with_time_units("normalized") # post processing - run.process() + run.pproc() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 23f8f5644..20d8415c4 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -68,7 +68,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): run = sim.run().with_time_units("normalized") # post processing - run.process() + run.pproc() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: @@ -147,7 +147,7 @@ def test_coaxial(do_plot: bool = False): run = sim.run().with_time_units("normalized") # post processing - run.process(physical=True) + run.pproc(physical=True) # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 1a1e185d5..4d188ac61 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -79,7 +79,7 @@ def test_poisson_1d(do_plot=False): run = sim.run().with_time_units("normalized") # post processing - run.process() + run.pproc() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index 1ffa4fe88..3451f945b 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -110,7 +110,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): # run run = sim.run() - run.process() + run.pproc() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: @@ -241,7 +241,7 @@ def test_damped_sound_wave(nx: int, plot_pts: int, do_plot: bool = False): # run run = sim.run() - run.process() + run.pproc() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: @@ -447,7 +447,7 @@ def test_velocity_diffusion(nx: int, plot_pts: int, do_plot: bool = False): # run run = sim.run() - run.process() + run.pproc() # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: @@ -659,7 +659,7 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ ) run = sim.run() - run.process() + run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: e2_grid = run.distributions.euler_fluid.e2_current_1.f.e2.values # logical y in [0, 1] @@ -919,7 +919,7 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo ) run = sim.run() - run.process() + run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: import numpy as np diff --git a/src/struphy/post_processing/legacy.py b/src/struphy/post_processing/legacy.py deleted file mode 100644 index 6a9cddb76..000000000 --- a/src/struphy/post_processing/legacy.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Views of an :class:`~struphy.post_processing.output.Output` in the shapes of earlier versions. - -They back the deprecated :meth:`Simulation.load_plotting_data`, so that code written for -``sim.orbits``, ``sim.f``, ``sim.spline_values`` and ``sim.n_sph`` keeps working. New code should use -the :class:`xarray.DataArray` products of :class:`~struphy.post_processing.output.Output` directly. -""" - -from types import SimpleNamespace - -import numpy as np - - -def _set(namespace: SimpleNamespace, path: tuple[str, ...], value): - """Set ``namespace.....`` to ``value``, creating the levels in between.""" - for name in path[:-1]: - if not hasattr(namespace, name): - setattr(namespace, name, SimpleNamespace()) - namespace = getattr(namespace, name) - setattr(namespace, path[-1], value) - - -def _field_data(array) -> dict[float, list[np.ndarray]]: - """Time -> list of components, each a 3d array, as stored for the fields of earlier versions.""" - values = np.asarray(array) - if "component" not in array.dims: - values = values[:, None] - return {float(t): list(comps) for t, comps in zip(np.asarray(array["t"]), values)} - - -def legacy_views(output) -> SimpleNamespace: - """The products of ``output`` as ``orbits``, ``f``, ``spline_values`` and ``n_sph``. - - * ``orbits.``: array of shape ``(time, marker, quantity)``. - * ``f..``: ``f_binned``, ``delta_f_binned`` and one ``grid_`` per bin axis. - * ``spline_values.._log`` (``_phy``): ``.data``, see :func:`_field_data`. - * ``n_sph..``: ``n_sph`` and its meshgrid ``grid_n_sph``. - """ - views = SimpleNamespace( - orbits=SimpleNamespace(), - f=SimpleNamespace(), - spline_values=SimpleNamespace(), - n_sph=SimpleNamespace(), - ) - - for species, array in output.orbit_catalog.items(): - setattr(views.orbits, species, np.asarray(array)) - - for key, array in output.field_catalog.items(): - species, name = key.split("/", 1) - # the fields in logical coordinates were saved as ``_log``, the pushed-forward ones as ``_phy`` - name = f"{name.removesuffix('_xyz')}_phy" if name.endswith("_xyz") else f"{name}_log" - _set(views.spline_values, (species, name), SimpleNamespace(data=_field_data(array))) - - for key, array in output.distribution_catalog.items(): - species, slice_name, name = key.split("/") - _set(views.f, (species, slice_name, f"{name}_binned"), np.asarray(array)) - for dim in array.dims: - if dim != "t": - _set(views.f, (species, slice_name, f"grid_{dim}"), np.asarray(array[dim])) - - for key, array in output.density_catalog.items(): - species, view, _ = key.split("/") - grid = np.meshgrid(*(np.asarray(array[dim]) for dim in ("e1", "e2", "e3")), indexing="ij") - _set(views.n_sph, (species, view, "n_sph"), np.asarray(array)) - _set(views.n_sph, (species, view, "grid_n_sph"), tuple(grid)) - - return views diff --git a/src/struphy/post_processing/manifest.py b/src/struphy/post_processing/manifest.py index 7615aa033..009c0b49b 100644 --- a/src/struphy/post_processing/manifest.py +++ b/src/struphy/post_processing/manifest.py @@ -11,7 +11,7 @@ def source_fingerprint(path_out: str) -> str: """Fingerprint the raw run files that determine post-processing products.""" digest = hashlib.sha256() - for name in ("config.json", "run_metadata.json", "meta.yml", "data/data_proc0.hdf5"): + for name in ("run_metadata.json", "data/data_proc0.hdf5"): path = os.path.join(path_out, name) if not os.path.exists(path): continue diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 37cbe7200..208b4df5e 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -6,7 +6,6 @@ import logging import os import shutil -import warnings from collections.abc import Callable, Iterator, Mapping, Sequence from contextlib import ExitStack from functools import cached_property @@ -188,8 +187,8 @@ class Output: Optional plotting is provided by the separate ``struphy-plots`` package. * :attr:`scalars` are read directly from the raw HDF5 output. - * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` are retained - as compatibility views over the post-processed products. + * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` group the + post-processed products. * :attr:`model`, :attr:`initial_conditions`, :attr:`domain` and numerical options are reconstructed lazily from saved metadata. No simulation object is created or retained. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. @@ -599,13 +598,12 @@ def path_pproc(self) -> Path: @cached_property def metadata(self) -> dict: - """Saved run metadata, with legacy ``config.json`` supported as a fallback.""" - for name in ("run_metadata.json", "config.json"): - path = self.path_out / name - if path.is_file(): - with path.open() as stream: - return json.load(stream) - raise FileNotFoundError(f"Neither run_metadata.json nor config.json exists in {self.path_out}") + """Saved run metadata.""" + path = self.path_out / "run_metadata.json" + if not path.is_file(): + raise FileNotFoundError(f"run_metadata.json does not exist in {self.path_out}") + with path.open() as stream: + return json.load(stream) def _restore(self, key, cls): # Constructors expect tuples where JSON encodes sequences as lists. @@ -654,9 +652,7 @@ def initial_conditions(self) -> dict: """ from struphy.simulation.sim import Simulation - version = self.metadata.get("model", {}).get( - "initial_conditions_schema_version", self.metadata.get("initial_conditions_schema_version", 1) - ) + version = self.metadata.get("model", {}).get("initial_conditions_schema_version", 1) if version != 1: raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") result = {} @@ -673,10 +669,7 @@ def initial_conditions(self) -> dict: return result def _initial_condition_metadata(self) -> dict: - """Read variable definitions, including the layout of older output folders.""" - legacy = self.metadata.get("initial_conditions") - if legacy is not None: - return legacy + """Read variable definitions from the current run metadata schema.""" return { species_name: { name: variable["initial_conditions"] @@ -724,12 +717,7 @@ def time_opts(self): @cached_property def mpi_ranks(self) -> int: """Number of ranks that wrote the raw output (not the current communicator).""" - if "mpi_ranks" in self.metadata: - return int(self.metadata["mpi_ranks"]) - import yaml - - with (self.path_out / "meta.yml").open() as stream: - return int(yaml.safe_load(stream)["MPI processes"]) + return int(self.metadata["mpi_ranks"]) @property def is_processed(self) -> bool: @@ -817,10 +805,6 @@ def _setup_processing(self, parallel: bool): self.grid, self.derham_opts, comm=self._pproc_comm if parallel else None, domain=self.domain, ) - def process(self, **options) -> "Output": - """Compatibility alias for :meth:`pproc`.""" - return self.pproc(**options) - def _write_manifest(self, status, *, options=None, error=None): if self._pproc_rank != 0: return @@ -1921,30 +1905,6 @@ def density_catalog(self) -> ProductMapping: def orbit_catalog(self) -> ProductMapping: return self._product_mappings()["orbits"] - @property - def f(self) -> DistributionProducts: - """Deprecated alias of :attr:`distributions`.""" - warnings.warn("Output.f is deprecated; use out.distributions instead.", DeprecationWarning, stacklevel=2) - return self.distributions - - @property - def spline_values(self) -> FieldProducts: - """Deprecated alias of :attr:`fields`.""" - warnings.warn("Output.spline_values is deprecated; use out.fields instead.", DeprecationWarning, stacklevel=2) - return self.fields - - @property - def n_sph(self) -> DensityProducts: - """Deprecated alias of :attr:`densities`.""" - warnings.warn("Output.n_sph is deprecated; use out.densities instead.", DeprecationWarning, stacklevel=2) - return self.densities - - @property - def t_grid(self): - """Deprecated alias of :attr:`time`.""" - warnings.warn("Output.t_grid is deprecated; use out.time instead.", DeprecationWarning, stacklevel=2) - return self.time - @property def time_scale(self) -> float: """Factor from Struphy time units to :attr:`time_units`.""" diff --git a/src/struphy/post_processing/tests/test_eval_grids.py b/src/struphy/post_processing/tests/test_eval_grids.py index a3a49ba3d..84ab36f4e 100644 --- a/src/struphy/post_processing/tests/test_eval_grids.py +++ b/src/struphy/post_processing/tests/test_eval_grids.py @@ -17,7 +17,7 @@ def make_pproc(num_elements, domain_array): """A Output stub that only knows about its Derham decomposition. - ``__init__`` is bypassed on purpose (it creates output folders and reads meta.yml). + ``__init__`` is bypassed on purpose because it creates output folders and reads metadata. """ pproc = Output.__new__(Output) pproc._pproc_derham = SimpleNamespace( diff --git a/src/struphy/post_processing/tests/test_eval_grids_mpi.py b/src/struphy/post_processing/tests/test_eval_grids_mpi.py index 74f469778..29deb89d4 100644 --- a/src/struphy/post_processing/tests/test_eval_grids_mpi.py +++ b/src/struphy/post_processing/tests/test_eval_grids_mpi.py @@ -58,7 +58,7 @@ def split_eta1(num_elements, n_parts): def make_mpi_pproc(comm, num_elements=NUM_ELEMENTS): """An Output stub in parallel mode, decomposed over ``comm``. - ``__init__`` is bypassed on purpose (it creates output folders and reads meta.yml); + ``__init__`` is bypassed on purpose because it creates output folders and reads metadata; ``_create_eval_grids`` and ``_collect_on_root`` only need the attributes set here. """ pproc = Output.__new__(Output) diff --git a/src/struphy/post_processing/tests/test_legacy.py b/src/struphy/post_processing/tests/test_legacy.py deleted file mode 100644 index 385541d6a..000000000 --- a/src/struphy/post_processing/tests/test_legacy.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Tests for the views of an Output in the shapes of earlier versions.""" - -from types import SimpleNamespace - -import numpy as np -import xarray as xr - -from struphy.post_processing.legacy import legacy_views - -t = np.array([0.0, 0.1, 0.2]) -e1, e2, e3 = np.linspace(0, 1, 4), np.linspace(0, 1, 5), np.array([0.0]) - - -def make_output(): - scalar_field = xr.DataArray( - np.random.rand(3, 4, 5, 1), dims=("t", "e1", "e2", "e3"), coords={"t": t, "e1": e1, "e2": e2, "e3": e3} - ) - vector_field = xr.DataArray( - np.random.rand(3, 3, 4, 5, 1), - dims=("t", "component", "e1", "e2", "e3"), - coords={"t": t, "component": [0, 1, 2], "e1": e1, "e2": e2, "e3": e3}, - ) - binned = xr.DataArray( - np.random.rand(3, 6, 7), dims=("t", "e1", "v1"), coords={"t": t, "e1": np.arange(6), "v1": np.arange(7)} - ) - return SimpleNamespace( - orbit_catalog={"ions": xr.DataArray(np.random.rand(3, 8, 8), dims=("t", "marker", "quantity"))}, - field_catalog={ - "em_fields/phi": scalar_field, - "em_fields/e_field": vector_field, - "em_fields/e_field_xyz": vector_field, - }, - distribution_catalog={"ions/e1_v1_density/f": binned, "ions/e1_v1_density/delta_f": binned * 2}, - density_catalog={"fluid/view_0/n": scalar_field}, - ) - - -def test_orbits_are_arrays(): - out = make_output() - orbits = legacy_views(out).orbits.ions - assert isinstance(orbits, np.ndarray) - assert orbits.shape == (3, 8, 8) - - -def test_fields_map_time_to_a_list_of_components(): - out = make_output() - fields = legacy_views(out).spline_values.em_fields - - assert sorted(vars(fields)) == ["e_field_log", "e_field_phy", "phi_log"] - data = fields.phi_log.data - assert list(data) == [0.0, 0.1, 0.2] - assert len(data[0.2]) == 1 - np.testing.assert_array_equal(data[0.2][0], out.field_catalog["em_fields/phi"].values[2]) - - data = fields.e_field_log.data - assert len(data[0.1]) == 3 - np.testing.assert_array_equal(data[0.1][2], out.field_catalog["em_fields/e_field"].values[1, 2]) - assert max(fields.phi_log.data) == 0.2 - - -def test_binned_distributions_carry_their_grids(): - out = make_output() - sli = legacy_views(out).f.ions.e1_v1_density - - np.testing.assert_array_equal(sli.f_binned, out.distribution_catalog["ions/e1_v1_density/f"].values) - np.testing.assert_array_equal(sli.delta_f_binned, 2 * sli.f_binned) - np.testing.assert_array_equal(sli.grid_e1, np.arange(6)) - np.testing.assert_array_equal(sli.grid_v1, np.arange(7)) - assert not hasattr(sli, "grid_t") - - -def test_sph_density_comes_with_its_meshgrid(): - out = make_output() - view = legacy_views(out).n_sph.fluid.view_0 - - assert view.n_sph.shape == (3, 4, 5, 1) - ee1, ee2, ee3 = view.grid_n_sph - assert ee1.shape == ee2.shape == ee3.shape == (4, 5, 1) - np.testing.assert_array_equal(ee1[:, 0, 0], e1) - np.testing.assert_array_equal(ee2[0, :, 0], e2) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 812c485c8..a57733c43 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -410,8 +410,7 @@ def test_processing_options_are_part_of_the_manifest(tmp_path): def test_manifest_is_stale_when_raw_output_changes(tmp_path): root = write_tree(str(tmp_path)) - with open(os.path.join(root, "meta.yml"), "w") as stream: - stream.write("MPI processes: 1\n") + os.utime(os.path.join(root, "data", "data_proc0.hdf5"), None) assert not is_processed(root) @@ -422,7 +421,7 @@ def test_serial_process_runs_on_rank_zero_only(tmp_path, monkeypatch, rank): monkeypatch.setattr(Output, "_process_raw", lambda self, **options: calls.append(("process", options))) comm = FakeComm(rank=rank, size=2) run = output_with_comm(monkeypatch, write_tree(str(tmp_path)), comm) - assert run.process(physical=True) is run + assert run.pproc(physical=True) is run expected = [ ("setup", False), ( @@ -440,7 +439,7 @@ def test_parallel_process_runs_on_every_rank(tmp_path, monkeypatch): calls = [] monkeypatch.setattr(Output, "_setup_processing", lambda self, parallel: calls.append(parallel)) monkeypatch.setattr(Output, "_process_raw", lambda self, **options: None) - output_with_comm(monkeypatch, write_tree(str(tmp_path)), FakeComm(rank=3, size=4)).process(parallel=True) + output_with_comm(monkeypatch, write_tree(str(tmp_path)), FakeComm(rank=3, size=4)).pproc(parallel=True) assert calls == [True] @@ -449,7 +448,7 @@ def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): os.remove(os.path.join(root, "post_processing", "manifest.json")) run = Output(root) calls = [] - monkeypatch.setattr(Output, "process", lambda self, **options: calls.append(options)) + monkeypatch.setattr(Output, "pproc", lambda self, **options: calls.append(options)) with pytest.raises(AttributeError, match="available species"): run.typo_here @@ -525,7 +524,7 @@ def test_a_failing_property_reports_its_own_error(tmp_path): def test_parallel_processing_rejects_a_different_rank_count(tmp_path, monkeypatch): run = output_with_comm(monkeypatch, write_tree(str(tmp_path)), FakeComm(size=2)) with pytest.raises(ValueError, match="same number of MPI ranks"): - run.process(parallel=True) + run.pproc(parallel=True) def test_saved_rank_count_does_not_block_serial_implicit_processing(tmp_path, monkeypatch): diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index b7e8a42bb..4f8159258 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -50,12 +50,12 @@ def do_plotting(run: Output, from_parallel=False): run = Output(sim.env.path_out) # serial pproc - run.process(create_vtk=True) + run.pproc(create_vtk=True) if sim.rank == 0: serial = do_plotting(run) # parallel pproc - run.process(create_vtk=True, parallel=True, force=True) + run.pproc(create_vtk=True, parallel=True, force=True) # plot and compare results from serial and parallel pproc if sim.rank == 0: diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 2a001e0ab..958c4e67a 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -8,10 +8,8 @@ import logging import os import shutil -import sysconfig import textwrap import time -import warnings from pathlib import Path import cunumpy as xp @@ -71,13 +69,12 @@ from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.physics.physics import Units from struphy.pic.base import Particles -from struphy.post_processing.legacy import legacy_views from struphy.post_processing.output import Output from struphy.propagators.base import Propagator from struphy.simulation.base import SimulationBase from struphy.utils.clone_config import CloneConfig from struphy.utils.progress import tqdm -from struphy.utils.utils import dict_to_yaml, ruff_autofix_and_format +from struphy.utils.utils import ruff_autofix_and_format logger = logging.getLogger("struphy") @@ -868,23 +865,6 @@ def run(self, one_time_step: bool = False, profiling_activated: bool | None = No self.Barrier() - if self.rank == 0: - # save meta-data - meta = { - "platform": sysconfig.get_platform(), - "python version": sysconfig.get_python_version(), - "model name": self.model_name, - "parameter file": self.params_path, - "output folder": self.env.path_out, - "MPI processes": self.comm_size, - "use MPI.COMM_WORLD": self.comm is not None, - "number of domain clones": self.env.num_clones, - "restart": self.env.restart, - "max wall-clock [min]": self.env.max_runtime, - "save interval [steps]": self.env.save_step, - "wall-clock time[min]": (end_time - self.start_time) / 60, - } - dict_to_yaml(meta, os.path.join(self.env.path_out, "meta.yml")) logger.info("Struphy run finished.") if self.clone_config is not None: @@ -903,89 +883,6 @@ def output(self) -> Output: self._output = Output(self.env.path_out) return self._output - # ------------------------------------------------------------------ - # Deprecated post-processing entry points, superseded by self.output - # ------------------------------------------------------------------ - - def pproc( - self, - step: int = 1, - celldivide: int | tuple[int, int, int] = 1, - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - create_vtk: bool = True, - parallel_pproc: bool = False, - force: bool = True, - load: bool = False, - ) -> Output | None: - """Deprecated, use ``sim.output.pproc(...)``, see :meth:`struphy.Output.pproc`.""" - warnings.warn( - "Simulation.pproc() is deprecated; use sim.output.pproc(...) instead.\n" - "How to update your script: replace 'sim.pproc(physical=True)' by 'out = sim.output' and " - "'out.pproc(physical=True)' (same options). Post-processing also runs on first access of a product, " - "so the call can be dropped if the default options suffice; drop 'sim.load_plotting_data()' as well " - "and read the products from 'out', see the warning of load_plotting_data().", - DeprecationWarning, - stacklevel=2, - ) - self.output.pproc( - step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, - classify=classify, - create_vtk=create_vtk, - parallel=parallel_pproc, - force=force, - ) - return self.load_plotting_data() if load else None - - def load_plotting_data(self) -> Output | None: - """Deprecated, use :attr:`output`; attaches its products as attributes of the simulation. - - They have the shapes of earlier versions, see :func:`struphy.post_processing.legacy.legacy_views`. - - Returns the :class:`struphy.Output` on rank 0 and ``None`` on the other ranks. - """ - warnings.warn( - "Simulation.load_plotting_data() is deprecated; use sim.output (a struphy.Output) instead.\n" - "How to update your script, with out = sim.output:\n" - " sim.orbits. -> out.orbits.\n" - " sim.f...f_binned -> out.distributions...f\n" - " sim.f...grid_e1 -> out.distributions...e1\n" - " sim.spline_values.._log.data -> out.fields..\n" - " sim.spline_values.._phy.data -> out.fields.._xyz\n" - " sim.n_sph...n_sph -> out.densities...n\n" - " sim.grids_log / sim.grids_phy / sim.t_grid -> out.grids_log / out.grids_phy / out.time\n" - "The products are xarray.DataArrays with named dimensions and coordinates (e.g. arr.t, arr.e1).", - DeprecationWarning, - stacklevel=2, - ) - if self.rank != 0: - return None - output = self.output - views = legacy_views(output) - self.orbits = views.orbits - self.f = views.f - self.spline_values = views.spline_values - self.n_sph = views.n_sph - self.grids_log = output.grids_log - self.grids_phy = output.grids_phy - self.t_grid = xp.array(output.time) - return output - - @property - def plotting_data(self) -> Output: - """Deprecated alias of :attr:`output`.""" - warnings.warn( - "Simulation.plotting_data is deprecated; use sim.output instead " - "(e.g. 'sim.plotting_data.orbits' -> 'sim.output.orbits').", - DeprecationWarning, - stacklevel=2, - ) - return self.output - # --------------------- # Code specific methods # --------------------- @@ -1830,22 +1727,18 @@ def _deserialize_initial_condition(value): def _restore_initial_conditions(self, metadata: dict): """Attach metadata initial conditions to the reconstructed model variables.""" - version = metadata.get("model", {}).get( - "initial_conditions_schema_version", metadata.get("initial_conditions_schema_version", 1) - ) + version = metadata.get("model", {}).get("initial_conditions_schema_version", 1) if version != 1: raise ValueError(f"Unsupported initial-conditions metadata schema version: {version}.") model_species = metadata.get("model", {}).get("species", {}) - definitions = metadata.get("initial_conditions") - if definitions is None: - definitions = { - species_name: { - name: variable["initial_conditions"] - for name, variable in species.get("variables", {}).items() - if "initial_conditions" in variable - } - for species_name, species in model_species.items() + definitions = { + species_name: { + name: variable["initial_conditions"] + for name, variable in species.get("variables", {}).items() + if "initial_conditions" in variable } + for species_name, species in model_species.items() + } for species_name, variables in definitions.items(): species = self.model.species.get(species_name) if species is None: @@ -1964,8 +1857,7 @@ def convert_lists_to_tuples(obj): def from_output(cls, path_out: str) -> "Simulation": """Restore the simulation that wrote the output folder ``path_out``. - The configuration is read from the ``run_metadata.json`` written by :meth:`run`, - falling back to legacy ``config.json`` if absent; a copied + The configuration is read from the ``run_metadata.json`` written by :meth:`run`; a copied parameter file is never executed. The metadata holds the options objects and the arguments of the model (and thus its units), which is all that post-processing and plotting need. Initial conditions are restored when present, including @@ -1975,12 +1867,7 @@ def from_output(cls, path_out: str) -> "Simulation": path_out = os.path.abspath(path_out) config_path = os.path.join(path_out, "run_metadata.json") if not os.path.exists(config_path): - config_path = os.path.join(path_out, "config.json") - if not os.path.exists(config_path): - raise FileNotFoundError( - f"Neither config.json nor run_metadata.json exists in {path_out}; is it a Struphy output folder? Outputs of older " - "versions can get one with sim.to_run_metadata(os.path.join(path_out, 'run_metadata.json')) from their parameter file." - ) + raise FileNotFoundError(f"run_metadata.json does not exist in {path_out}; is it a Struphy output folder?") sim = cls.from_file(config_path) sim.env = dataclasses.replace( sim.env, out_folders=os.path.dirname(path_out), sim_folder=os.path.basename(path_out) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 1442f08ea..bbb96aed8 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -3,7 +3,6 @@ import json import os from pathlib import Path -from types import SimpleNamespace import h5py import numpy as np @@ -175,27 +174,6 @@ def test_run_metadata_embeds_user_function_source(tmp_path): assert len(density["source_sha256"]) == 64 -def test_legacy_initial_conditions_metadata_can_still_be_restored(tmp_path): - path_out = tmp_path / "sim_1" - path_out.mkdir() - sim = make_sim(tmp_path) - sim.model.em_fields.b_field.add_background(FieldsBackground(values=(1.0, 2.0, 3.0))) - metadata = json.loads(sim.to_run_metadata()) - metadata["initial_conditions_schema_version"] = metadata["model"].pop("initial_conditions_schema_version") - metadata["initial_conditions"] = { - species_name: { - variable_name: variable.pop("initial_conditions") - for variable_name, variable in species["variables"].items() - } - for species_name, species in metadata["model"]["species"].items() - } - (path_out / "run_metadata.json").write_text(json.dumps(metadata)) - - restored = Simulation.from_output(path_out) - assert restored.model.em_fields.b_field.backgrounds.values == (1.0, 2.0, 3.0) - output = Output(path_out) - assert output.initial_conditions["em_fields"]["b_field"]["backgrounds"].values == (1.0, 2.0, 3.0) - def test_from_output_restores_embedded_initial_condition_source(tmp_path, monkeypatch): path_out = tmp_path / "sim_1" @@ -311,21 +289,15 @@ def test_from_output_never_executes_the_parameter_file(tmp_path): assert Simulation.from_output(sim.env.path_out).time_opts.dt == 0.123 -def test_from_output_requires_a_configuration(tmp_path): - with pytest.raises(FileNotFoundError, match="config.json"): +def test_from_output_requires_run_metadata(tmp_path): + with pytest.raises(FileNotFoundError, match="run_metadata.json"): Simulation.from_output(tmp_path) -@pytest.mark.parametrize("metadata_only", [False, True]) -def test_processing_from_moved_output(tmp_path, metadata_only): +def test_processing_from_moved_output(tmp_path): sim = make_sim(tmp_path, grid=None, derham_opts=None, time_opts=Time(dt=0.123)) os.makedirs(os.path.join(sim.env.path_out, "data")) - if metadata_only: - sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json"), mpi_ranks=3) - else: - sim.export(os.path.join(sim.env.path_out, "config.json")) - with open(os.path.join(sim.env.path_out, "meta.yml"), "w") as stream: - stream.write("MPI processes: 3\n") + sim.to_run_metadata(os.path.join(sim.env.path_out, "run_metadata.json"), mpi_ranks=3) with h5py.File(os.path.join(sim.env.path_out, "data", "data_proc0.hdf5"), "w") as data: data.create_dataset("time/value", data=[0.0, 0.123]) moved = tmp_path / "moved" @@ -343,55 +315,5 @@ def test_processing_from_moved_output(tmp_path, metadata_only): assert processor.mpi_ranks == 3 assert sentinel.read_text() == "keep until processing" assert Output(moved).time_opts.dt == 0.123 - assert processor.process(create_vtk=False) + assert processor.pproc(create_vtk=False) assert is_processed(moved) - - -def test_from_output_prefers_metadata_over_legacy_config(tmp_path): - sim = make_sim(tmp_path, time_opts=Time(dt=0.123)) - os.makedirs(sim.env.path_out) - sim.export(os.path.join(sim.env.path_out, "config.json")) - sim.time_opts = Time(dt=0.456) - sim._write_run_metadata() - assert Simulation.from_output(sim.env.path_out).time_opts.dt == 0.456 - - -def test_deprecated_pproc_delegates_to_the_output(tmp_path, monkeypatch): - sim = make_sim(tmp_path) - calls = [] - monkeypatch.setattr(type(sim.output), "process", lambda self, **options: calls.append(options)) - monkeypatch.setattr(type(sim), "load_plotting_data", lambda self: "loaded") - - with pytest.deprecated_call(): - assert sim.pproc(physical=True) is None - assert calls == [ - dict( - step=1, - celldivide=1, - physical=True, - guiding_center=False, - classify=False, - create_vtk=True, - parallel=False, - force=True, - ) - ] - with pytest.deprecated_call(): - assert sim.pproc(load=True) == "loaded" - - -def test_deprecated_load_plotting_data_attaches_the_products(tmp_path, monkeypatch): - sim = make_sim(tmp_path) - output = sim.output - views = SimpleNamespace(orbits="o", f="f", spline_values="s", n_sph="n") - monkeypatch.setattr("struphy.simulation.sim.legacy_views", lambda out: views if out is output else None) - for name, value in (("grids_log", "gl"), ("grids_phy", "gp"), ("time", np.array([0.0, 0.5]))): - monkeypatch.setattr(type(output), name, property(lambda self, value=value: value)) - - with pytest.deprecated_call(): - assert sim.load_plotting_data() is output - assert (sim.orbits, sim.f, sim.spline_values, sim.n_sph) == ("o", "f", "s", "n") - assert (sim.grids_log, sim.grids_phy) == ("gl", "gp") - assert sim.t_grid.tolist() == [0.0, 0.5] - with pytest.deprecated_call(): - assert sim.plotting_data is output From 09ce9fa57ac1941c6a0c7258ce840c1a28752b0d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 20:17:59 +0200 Subject: [PATCH 121/193] Update tutorials --- tutorials/tutorial_post_processing.ipynb | 62 ++++++++++++++---------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index e3d9aa22e..d60692035 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -27,6 +27,9 @@ "\n", "from IPython.display import HTML\n", "\n", + "# import struphy_plots # registers the optional xarray .struphy accessor\n", + "# from struphy_plots.output_accessors import OutputPlots\n", + "\n", "from struphy import (\n", " BinningPlot,\n", " BoundaryParameters,\n", @@ -256,9 +259,9 @@ "id": "15", "metadata": {}, "source": [ - "### Evaluate a saved spline directly\n", + "### Evaluate saved splines directly: 1-D, 2-D, and 3-D\n", "\n", - "For a small number of logical points, `evaluate()` can read the saved FEEC coefficients and evaluate the spline directly, without creating a full post-processing field. Supply all three logical coordinates, each in the unit interval. Scalars, lists, NumPy arrays, and `range` objects can be mixed; non-scalar inputs form a tensor-product grid. The result remains an xarray array with a `t` coordinate.\n", + "For a small number of logical points, `evaluate()` can read the saved FEEC coefficients and evaluate the spline directly, without creating a full post-processing field. Supply all three logical coordinates, each in the unit interval. Scalars, lists, NumPy arrays, and `range` objects can be mixed; every non-scalar input becomes a dimension of the tensor-product result. Thus one varying coordinate makes a 1-D line, two make a 2-D plane, and three make a 3-D volume. The result is always an xarray array with named logical coordinates.\n", "\n", "A representation conversion is applied after spline evaluation. Its input is inferred from the saved FEEC space, so `representation=` specifies only the target: `\"0\"`, `\"1\"`, `\"2\"`, `\"3\"`, `\"v\"`, or `\"norm\"`. Scalars default to `\"0\"`; vectors default to `\"norm\"`." ] @@ -270,16 +273,17 @@ "metadata": {}, "outputs": [], "source": [ + "# 1-D: a field line at fixed eta2 and eta3, from the final saved time.\n", "eta1_line = np.linspace(0.0, 1.0, 128)\n", - "e_last = out.evaluate(\n", - " \"em_fields/e_field\",\n", + "phi_line = out.evaluate(\n", + " \"em_fields/phi\",\n", " eta1=eta1_line,\n", " eta2=0.5,\n", " eta3=0.5,\n", " t=-1,\n", - " representation=\"norm\", # the default for vector spline fields\n", ")\n", - "e_last\n" + "phi_line.plot()\n", + "print(phi_line.dims, phi_line.shape)\n" ] }, { @@ -287,13 +291,7 @@ "id": "17", "metadata": {}, "source": [ - "The `representation` argument names the target representation:\n", - "\n", - "- The saved field's FEEC space supplies the source (`0`, `1`, `2`, `3`, or `v`).\n", - "- `representation=\"norm\"` on an H(curl) field uses `1_to_norm`; `representation=\"2\"` uses `1_to_2`.\n", - "- `representation=\"1\"` below keeps this electric field in its native H(curl) representation.\n", - "\n", - "Below, `e_last` is in the normalized-vector representation, while `e_one_form` retains the native 1-form representation." + "For a 2-D plane, vary two coordinates and hold the third fixed. This is useful for a cross-section of a 3-D field even when the simulation was run on a coarser grid: the spline is evaluated at the requested points. For a 3-D volume, vary all three coordinates. Keep volume grids modest, then take a plane or line from the labeled result for plotting or further analysis." ] }, { @@ -303,16 +301,28 @@ "metadata": {}, "outputs": [], "source": [ - "# The native 1-form representation at a two-dimensional logical grid.\n", - "e_one_form = out.evaluate(\n", - " \"em_fields/e_field\",\n", + "# 2-D: a logical eta1--eta2 plane.\n", + "phi_plane = out.evaluate(\n", + " \"em_fields/phi\",\n", " eta1=np.linspace(0.0, 1.0, 64),\n", - " eta2=range(2),\n", + " eta2=np.linspace(0.0, 1.0, 48),\n", " eta3=0.5,\n", " t=-1,\n", - " representation=\"1\",\n", ")\n", - "e_one_form\n" + "phi_plane.plot(x=\"e1\", y=\"e2\")\n", + "\n", + "# 3-D: a modest logical volume. The result stays a labeled xarray DataArray.\n", + "phi_volume = out.evaluate(\n", + " \"em_fields/phi\",\n", + " eta1=np.linspace(0.0, 1.0, 32),\n", + " eta2=np.linspace(0.0, 1.0, 24),\n", + " eta3=np.linspace(0.0, 1.0, 16),\n", + " t=-1,\n", + ")\n", + "print(phi_volume.dims, phi_volume.shape)\n", + "\n", + "# xarray plots a 2-D slice of the volume; choose the mid-plane by coordinate index.\n", + "phi_volume.isel(e3=phi_volume.sizes[\"e3\"] // 2).plot(x=\"e1\", y=\"e2\")\n" ] }, { @@ -390,7 +400,7 @@ "id": "26", "metadata": {}, "source": [ - "Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`. Everything below shows those." + "The optional `struphy-plots` package provides the custom plotting and analysis helpers used below. Install it separately (for this checkout: `pip install -e postprocessing_external`) and import `struphy_plots` once to register its xarray accessor. Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`." ] }, { @@ -400,9 +410,9 @@ "source": [ "## Scalar overview and time series\n", "\n", - "Products plot themselves: every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f.struphy.plot.slice(...)` needs no imports and completes as you type. `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks the same product up by name, which suits scripts and loops. The plots that need a whole run, `out.plot.scalars()` and `out.plot.equilibrium()`, stay on the run.\n", + "After importing `struphy_plots`, every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f.struphy.plot.slice(...)` is available on demand. `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks the same product up by name, which suits scripts and loops. Whole-run plots use `OutputPlots(out)`.\n", "\n", - "`out.plot.scalars()` gives a quick overview of every recorded scalar. `.struphy.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." + "`OutputPlots(out).scalars()` gives a quick overview of every recorded scalar. `.struphy.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." ] }, { @@ -412,7 +422,7 @@ "metadata": {}, "outputs": [], "source": [ - "out.plot.scalars()" + "OutputPlots(out).scalars()" ] }, { @@ -493,7 +503,7 @@ "source": [ "## Interactive plots\n", "\n", - "`.struphy.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `out.plot.animation()` and `out.plot.frames()` sweep the same way." + "`.struphy.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same way." ] }, { @@ -560,7 +570,7 @@ "id": "41", "metadata": {}, "source": [ - "For a run with a fluid equilibrium, `out.plot.equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `out.plot.scalars()` and `out.save_report()`." + "For a run with a fluid equilibrium, `OutputPlots(out).equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `OutputPlots(out).scalars()`." ] }, { @@ -570,7 +580,7 @@ "metadata": {}, "outputs": [], "source": [ - "out.plot.equilibrium()" + "OutputPlots(out).equilibrium()" ] }, { From d09fb7ed192f94cb466b002d2925b84152e11854 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 21:30:52 +0200 Subject: [PATCH 122/193] Update docs --- doc/markdown/output-api.md | 18 +-- doc/sections/userguide.rst | 31 +++-- src/struphy/diagnostics/diagn_tools.py | 16 +-- src/struphy/post_processing/output.py | 2 +- tutorials/tutorial_post_processing.ipynb | 140 +++++++++-------------- 5 files changed, 84 insertions(+), 123 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index f5297d8f6..45982f4b0 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -139,7 +139,7 @@ averages over the logical space dimensions `e1`, `e2` and `e3` (or the ones pass so an `e1_v1` product becomes f(v1, t). The mean is uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a mapped domain it is not weighted by the Jacobian. -`struphy_plots.analysis.velocity_moments` integrates over the velocity dimensions instead and returns a dataset with the +The velocity moments are readily computed with xarray reductions over the velocity dimensions; the result is a dataset with the `density`, and the mean `mean_v1` and variance `variance_v1` along every velocity direction, as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. Mean and variance are NaN where the density is not positive, and a `delta_f` product @@ -148,15 +148,17 @@ has only the density (its perturbation). ```python f = "kinetic_ions/e1_v1_density/f" -from struphy_plots.analysis import spatial_average, velocity_moments - -f_of_v = spatial_average(out.evaluate(f)) # dimensions (t, v1) -moments = velocity_moments(out.evaluate(f)) # density, mean_v1, variance_v1 over (t, e1) -temperature_over_mass = spatial_average(moments.variance_v1) +data = out.evaluate(f) +f_of_v = data.mean(("e1", "e2", "e3"), missing_dims="ignore") +velocity_grid = data.v1 +bin_width = velocity_grid.differentiate("v1") +density = (data * bin_width).sum("v1") +mean_v1 = (data * velocity_grid * bin_width).sum("v1") / density +temperature_over_mass = ((data * (velocity_grid - mean_v1) ** 2 * bin_width).sum("v1") / density).mean( + ("e1", "e2", "e3"), missing_dims="ignore" +) ``` -Importing `struphy_plots` additionally registers the optional -`array.struphy.analysis` accessor. ## Convert to SI units diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index b1847ff6e..331defc96 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -579,29 +579,27 @@ serial processing runs on rank 0 while the other ranks wait, and Standard plots and analysis ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Plotting and derived diagnostics are optional and live in the separate -``struphy-plots`` package. Install it separately, then import it to register its -optional xarray accessor: +Products are standard xarray objects. Use xarray's plotting methods for the +ordinary one- and two-dimensional cases: .. code-block:: python - import struphy_plots - f = out.kinetic_ions.e1_v1_density.f - f.struphy.plot.slice(x="e1", y="v1", t="last") - f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4) - f.struphy.plot.viewer(x="e1", y="v1").show() - out.em_fields.phi_xyz.struphy.plot.slice(x="e1", y="e2", t="last", coords="physical") - out.kinetic_ions.orbits.struphy.plot.trajectories() - out.scalars.en_phi.struphy.plot.timeseries(fit=(0.0, 40.0)) # exponential fit in a window - out.scalars.en_phi.struphy.analysis.growth_rate(window=(0.0, 40.0)).rate + f.isel(t=-1).plot(x="e1", y="v1") + out.scalars.en_phi.plot.line(x="t") + out.em_fields.phi_xyz.isel(t=-1, e3=0).plot(x="e1", y="e2") -Plots return a ``PlotResult`` with ``.show()`` and ``.save(path)``. Time series of -several runs are labeled by run: +For a comparison across runs, use a Matplotlib axes and plot the labeled arrays +onto it: .. code-block:: python - out_a.scalars.en_phi.struphy.plot.timeseries(out_b.scalars.en_phi, fit=(0.0, 40.0)) + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + out_a.scalars.en_phi.plot(ax=ax, label="run A") + out_b.scalars.en_phi.plot(ax=ax, label="run B") + ax.legend() The sections below access the arrays directly for custom Matplotlib plots. @@ -638,8 +636,7 @@ perturbation with respect to the background: .. code-block:: python f = out.distributions.kinetic_ions.e1_v1_density.f # dims (t, e1, v1) - import struphy_plots - f.struphy.plot.slice(x="e1", y="v1", t="last").show() + f.isel(t=-1).plot(x="e1", y="v1") Plotting particle orbits diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index ed335f9d4..18da5785f 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Spectral diagnostics and deprecated plotting helpers for legacy output. -Use the optional ``struphy-plots`` package for new plotting code. +Use xarray and Matplotlib for new plotting code. The legacy distribution/video helpers read the old NPY layout, not output.nc. ``power_spectrum_2d`` remains supported by the analysis accessor. """ @@ -69,7 +69,7 @@ def wrapped(field, *args, **kwargs): f"diagn_tools.{function.__name__}(values, name, grids, ...) is deprecated; pass a field of an Output.\n" "How to update your script, with out = sim.output:\n" " power_spectrum_2d(E_of_t, 'e_field_log', grids=sim.grids_log, grids_mapped=sim.grids_phy, ...)\n" - " -> struphy_plots analysis helpers applied to out.fields.em_fields.e_field_log\n" + " -> apply the diagnostic directly to out.fields.em_fields.e_field_log\n" "'physical=True' replaces 'grids_mapped'; 'grids' and 'name' are read from the field itself. " "Take the field from out.with_time_units('normalized') if you compare with normalized dispersion relations.", DeprecationWarning, @@ -294,7 +294,7 @@ def fun(k): return omega, kvec, dispersion, coeffs -@_legacy_plot("struphy_plots.plotting.plot_scalars()") +@_legacy_plot("xarray.DataArray.plot()") def plot_scalars( time, scalar_quantities, @@ -490,7 +490,7 @@ def plot_scalars( plt.show() -@_legacy_plot("struphy_plots.plotting.plot_slice()") +@_legacy_plot("xarray.DataArray.plot() after selection") def plot_distr_fun( path, time_idx, @@ -625,7 +625,7 @@ def plot_distr_fun( del delta_f -@_legacy_plot("struphy_plots.plotting.animate_slices() or plot_panels()") +@_legacy_plot("xarray faceting after selection") def plots_videos_2d( t_grid, grid_slices, @@ -785,7 +785,7 @@ def plots_videos_2d( raise NotImplementedError(f"{output=} is not implemented!") -@_legacy_plot("struphy_plots.plotting.animate_slices().save(path)") +@_legacy_plot("Matplotlib figure saving") def video_2d(slc, diagn_path, images_path): """Create a video of all 2D slices of the distribution function over time. @@ -860,7 +860,7 @@ def video_2d(slc, diagn_path, images_path): video.release() -@_legacy_plot("struphy_plots.plotting.animate_slices()") +@_legacy_plot("xarray faceting after selection") def plots_2d_video( t_grid, grid_1_mesh, @@ -942,7 +942,7 @@ def plots_2d_video( plt.close("all") -@_legacy_plot("struphy_plots.plotting.plot_panels()") +@_legacy_plot("xarray faceting after selection") def plots_2d_overview( t_grid, grid_1_mesh, diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 208b4df5e..9edf0ae08 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -184,7 +184,7 @@ class Output: Call :meth:`evaluate` to obtain one product as an :class:`xarray.DataArray`. It materializes post-processing products on demand; call :meth:`pproc` explicitly to choose its options. The :attr:`xarray` property exposes the complete post-processed product tree. - Optional plotting is provided by the separate ``struphy-plots`` package. + Products are standard xarray objects and can be plotted with their xarray methods. * :attr:`scalars` are read directly from the raw HDF5 output. * :attr:`fields`, :attr:`distributions`, :attr:`densities` and :attr:`orbits` group the diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index d60692035..f2791391b 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -27,8 +27,6 @@ "\n", "from IPython.display import HTML\n", "\n", - "# import struphy_plots # registers the optional xarray .struphy accessor\n", - "# from struphy_plots.output_accessors import OutputPlots\n", "\n", "from struphy import (\n", " BinningPlot,\n", @@ -400,7 +398,7 @@ "id": "26", "metadata": {}, "source": [ - "The optional `struphy-plots` package provides the custom plotting and analysis helpers used below. Install it separately (for this checkout: `pip install -e postprocessing_external`) and import `struphy_plots` once to register its xarray accessor. Use `.struphy.plot` when xarray has nothing to offer: physical coordinates on a mapped domain, panels, the slider viewer, animations, growth-rate fits, and selections like `t=\"last\"`." + "Use xarray's plotting methods for ordinary one- and two-dimensional output. Select a saved time or spatial slice with `.isel()` or `.sel()` first, then call `.plot()` or `.plot.line()`." ] }, { @@ -410,9 +408,9 @@ "source": [ "## Scalar overview and time series\n", "\n", - "After importing `struphy_plots`, every array has a `.struphy` accessor holding `.plot` and `.analysis`, so `out.kinetic_ions.e1_v1_density.f.struphy.plot.slice(...)` is available on demand. `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks the same product up by name, which suits scripts and loops. Whole-run plots use `OutputPlots(out)`.\n", + "Every product is an xarray `DataArray`. `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks a product up by name, which suits scripts and loops; attribute access is convenient interactively.\n", "\n", - "`OutputPlots(out).scalars()` gives a quick overview of every recorded scalar. `.struphy.plot.timeseries()` shows individual series on linear or logarithmic axes; `fit=(t0, t1)` adds an exponential fit restricted to that time window. Plots return an already-rendered `PlotResult`, which a notebook displays by itself; calling `.save()` never draws a second figure." + "Call `.plot()` for a scalar time series. Use a Matplotlib axes when combining several series or setting plot options." ] }, { @@ -422,7 +420,7 @@ "metadata": {}, "outputs": [], "source": [ - "OutputPlots(out).scalars()" + "out.scalars.electric_energy.plot.line(x=\"t\")" ] }, { @@ -433,17 +431,11 @@ "outputs": [], "source": [ "t_fit = 2.0 # Struphy time units, like every time coordinate of this run\n", - "energy_plot = out.scalars.electric_energy.struphy.plot.timeseries(\n", - " fit=(0.0, t_fit),\n", - " #fit_amplitude=True,\n", - " title=\"Electric-field energy\",\n", - ")\n", - "print(\"growth rate of the energy:\", energy_plot.fit_results[0].rate)\n", - "\n", - "# the same fit without drawing; amplitude=True fits the amplitude of a quadratic quantity,\n", - "# so the rate comes out half as large for an energy\n", - "rate = out.scalars.electric_energy.struphy.analysis.growth_rate(window=(0.0, t_fit), amplitude=True).rate\n", - "print(\"growth rate of the amplitude:\", rate)" + "energy = out.scalars.electric_energy.sel(t=slice(0.0, t_fit))\n", + "fig, ax = plt.subplots()\n", + "energy.plot.line(ax=ax, label=\"electric energy\")\n", + "ax.set_yscale(\"log\")\n", + "ax.legend()" ] }, { @@ -463,13 +455,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_space.struphy.plot.slice(\n", - " x=\"e1\",\n", - " y=\"v1\",\n", - " t=\"last\",\n", - " equal_aspect=False,\n", - " title=\"Final phase-space distribution\",\n", - ")" + "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" ] }, { @@ -477,7 +463,7 @@ "id": "32", "metadata": {}, "source": [ - "For a compact view of the evolution, `.struphy.plot.panels()` chooses evenly spaced snapshots in time. `shared_clim=True` makes panel colors directly comparable." + "For a compact view of the evolution, select saved times and use xarray faceting." ] }, { @@ -487,12 +473,8 @@ "metadata": {}, "outputs": [], "source": [ - "phase_space.struphy.plot.panels(\n", - " x=\"e1\",\n", - " y=\"v1\",\n", - " nrows=1,\n", - " ncols=5,\n", - " title=\"Phase-space evolution\",\n", + "phase_space.isel(t=np.linspace(0, phase_space.sizes[\"t\"] - 1, 5, dtype=int)).plot(\n", + " x=\"e1\", y=\"v1\", col=\"t\", col_wrap=5\n", ")" ] }, @@ -501,9 +483,9 @@ "id": "34", "metadata": {}, "source": [ - "## Interactive plots\n", + "## Selecting saved snapshots\n", "\n", - "`.struphy.plot.viewer()` adds one slider for every dimension not assigned to the display axes. In JupyterLab, run `%matplotlib widget` before this cell if `ipympl` is installed; the default inline backend still displays the initial frame. Keep the viewer alive so its callbacks remain connected. `.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same way." + "Use `.isel()` for index-based selection and `.sel()` for coordinate-based selection. This keeps selection explicit and works with every xarray operation." ] }, { @@ -513,8 +495,8 @@ "metadata": {}, "outputs": [], "source": [ - "phase_viewer = phase_space.struphy.plot.viewer(x=\"e1\", y=\"v1\")\n", - "phase_viewer" + "final_phase_space = phase_space.isel(t=-1)\n", + "final_phase_space.plot(x=\"e1\", y=\"v1\")" ] }, { @@ -522,7 +504,7 @@ "id": "36", "metadata": {}, "source": [ - "Saved marker orbits sit under their species. `.struphy.plot.trajectories()` draws their three-dimensional paths, while `max_markers` limits rendering cost for large production runs." + "Saved marker orbits sit under their species. Select one marker and plot its coordinates or quantities over time." ] }, { @@ -532,7 +514,8 @@ "metadata": {}, "outputs": [], "source": [ - "out.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=12, show_paths=True)" + "orbit = out.kinetic_ions.orbits.isel(marker=0)\n", + "orbit.sel(quantity=[\"x\", \"y\", \"z\"]).plot.line(x=\"t\", hue=\"quantity\")" ] }, { @@ -540,7 +523,7 @@ "id": "38", "metadata": {}, "source": [ - "`.struphy.plot.animation()` and `.struphy.plot.frames()` sweep the same data as the viewer. The animation is a Matplotlib `FuncAnimation`, displayed here as JavaScript; `frames()` writes one PNG per step and returns the paths." + "A small collection of explicit snapshots is often more useful in a reproducible notebook than an interactive widget or animation." ] }, { @@ -550,8 +533,7 @@ "metadata": {}, "outputs": [], "source": [ - "animation = phase_space.struphy.plot.animation(x=\"e1\", y=\"v1\", step=4)\n", - "HTML(animation.to_jshtml())" + "phase_space.isel(t=[0, -1]).plot(x=\"e1\", y=\"v1\", col=\"t\")" ] }, { @@ -561,8 +543,9 @@ "metadata": {}, "outputs": [], "source": [ - "frames = phase_space.struphy.plot.frames(os.path.join(demo_root, \"frames\"), x=\"e1\", y=\"v1\", step=10)\n", - "print(\"Wrote:\", [os.path.basename(path) for path in frames])" + "fig, ax = plt.subplots()\n", + "phase_space.isel(t=-1).plot(ax=ax, x=\"e1\", y=\"v1\")\n", + "fig.savefig(os.path.join(demo_root, \"phase_space_final.png\"), bbox_inches=\"tight\")" ] }, { @@ -570,7 +553,7 @@ "id": "41", "metadata": {}, "source": [ - "For a run with a fluid equilibrium, `OutputPlots(out).equilibrium()` plots its radial profiles; it needs the run rather than a single array, like `OutputPlots(out).scalars()`." + "The reconstructed equilibrium is available directly on the output handle for inspection and for model-specific analysis." ] }, { @@ -580,7 +563,7 @@ "metadata": {}, "outputs": [], "source": [ - "OutputPlots(out).equilibrium()" + "print(out.equil)" ] }, { @@ -590,7 +573,7 @@ "source": [ "## Derived quantities\n", "\n", - "`.struphy.analysis` computes without drawing, and every result is an array that plots itself. `drift()` subtracts the first sample, `relative_error()` gives the deviation relative to it, which is the usual way to inspect energy conservation." + "xarray arithmetic computes derived quantities without special APIs. For example, subtract the first sample to obtain a drift and divide by it to obtain a relative error." ] }, { @@ -601,11 +584,11 @@ "outputs": [], "source": [ "total_energy = out.scalars.total_energy\n", - "energy_error = total_energy.struphy.analysis.relative_error()\n", - "energy_drift = total_energy.struphy.analysis.drift()\n", + "energy_drift = total_energy - total_energy.isel(t=0)\n", + "energy_error = abs(energy_drift) / abs(total_energy.isel(t=0))\n", "print(f\"largest drift of the total energy: {abs(energy_drift).max().item():.3e}\")\n", "\n", - "energy_error.struphy.plot.timeseries(title=\"Conservation of the total energy\")" + "energy_error.plot.line(x=\"t\")" ] }, { @@ -613,7 +596,7 @@ "id": "45", "metadata": {}, "source": [ - "`.struphy.analysis.dispersion()` takes the space-time Fourier transform of a field along one direction and draws the spectrum. `slice_at` picks the direction of the transform (`None`) and the indices of the other two. Pass `disp_name` to overlay an analytic dispersion relation from `struphy.dispersion_relations.analytic`, and `fit_branches` to fit the dominant branches." + "For custom diagnostics, first select the labeled subset needed for the calculation. Here a space-time field line is retained as an xarray object, ready for NumPy, SciPy, or another analysis package." ] }, { @@ -623,11 +606,10 @@ "metadata": {}, "outputs": [], "source": [ - "omega, kvec, spectrum, _ = out.em_fields.e_field.struphy.analysis.dispersion(\n", - " slice_at=(None, 0, 0),\n", - " do_plot=True,\n", + "space_time_line = out.evaluate(\n", + " \"em_fields/phi\", eta1=np.linspace(0.0, 1.0, 64), eta2=0.5, eta3=0.5\n", ")\n", - "print(\"spectrum:\", spectrum.shape)" + "print(space_time_line.dims, space_time_line.shape)" ] }, { @@ -637,7 +619,7 @@ "source": [ "## Reducing distribution functions\n", "\n", - "A binned distribution usually has more dimensions than the question needs. `.struphy.analysis.spatial_average()` averages over `e1`, `e2` and `e3`, so the $(\\eta_1, v_1)$ product becomes $f(v_1, t)$: how the velocity distribution of the whole plasma evolves, without the spatial structure. The mean is uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a mapped domain it is not weighted by the Jacobian." + "A binned distribution usually has more dimensions than the question needs. Xarray reductions retain the remaining named dimensions, so averaging over `e1`, `e2` and `e3` turns the $(\\eta_1, v_1)$ product into $f(v_1, t)$. The mean is uniform in logical coordinates, which is a volume average on a Cartesian domain." ] }, { @@ -647,7 +629,7 @@ "metadata": {}, "outputs": [], "source": [ - "f_of_v = phase_space.struphy.analysis.spatial_average()\n", + "f_of_v = phase_space.mean((\"e1\", \"e2\", \"e3\"), missing_dims=\"ignore\")\n", "print(f_of_v.dims)\n", "f_of_v.plot(x=\"t\", y=\"v1\")" ] @@ -657,7 +639,7 @@ "id": "49", "metadata": {}, "source": [ - "`.struphy.analysis.velocity_moments()` integrates over the velocity dimensions instead and returns a dataset with the `density`, and the mean velocity `mean_v1` and the variance `variance_v1` along each velocity direction, all as functions of the remaining dimensions. In normalized units the variance is the temperature divided by the mass. For a `delta_f` product only the density (its perturbation) is returned, because a mean and variance of a perturbation are not defined. Where the density is not positive, mean and variance are NaN." + "Velocity moments are weighted xarray reductions. The bin widths and velocity coordinate remain labeled, making the density, mean velocity, and variance explicit." ] }, { @@ -667,14 +649,13 @@ "metadata": {}, "outputs": [], "source": [ - "moments = phase_space.struphy.analysis.velocity_moments()\n", - "print(moments)\n", - "\n", - "mean_density = moments.density.struphy.analysis.spatial_average()\n", - "mean_density.struphy.plot.timeseries(logy=False, title=\"Mean density of the binned distribution\")\n", - "\n", - "mean_variance = moments.variance_v1.struphy.analysis.spatial_average()\n", - "mean_variance.struphy.plot.timeseries(logy=False, title=\"Mean velocity variance\")" + "dv1 = phase_space.v1.differentiate(\"v1\")\n", + "density = (phase_space * dv1).sum(\"v1\")\n", + "mean_v1 = (phase_space * phase_space.v1 * dv1).sum(\"v1\") / density\n", + "variance_v1 = (phase_space * (phase_space.v1 - mean_v1) ** 2 * dv1).sum(\"v1\") / density\n", + "mean_density = density.mean((\"e1\", \"e2\", \"e3\"), missing_dims=\"ignore\")\n", + "mean_density.plot.line(x=\"t\")\n", + "variance_v1.mean((\"e1\", \"e2\", \"e3\"), missing_dims=\"ignore\").plot.line(x=\"t\")" ] }, { @@ -752,10 +733,10 @@ ")\n", "out_coarse = sim_coarse.run(profiling_activated=True)\n", "\n", - "out.scalars.electric_energy.struphy.plot.timeseries(\n", - " out_coarse.scalars.electric_energy,\n", - " title=\"Electric energy: dt = 0.05 against dt = 0.1\",\n", - ")" + "fig, ax = plt.subplots()\n", + "out.scalars.electric_energy.plot.line(ax=ax, label=\"dt = 0.05\")\n", + "out_coarse.scalars.electric_energy.plot.line(ax=ax, label=\"dt = 0.1\")\n", + "ax.legend()" ] }, { @@ -944,16 +925,7 @@ "metadata": {}, "outputs": [], "source": [ - "out_coaxial.em_fields.b_field_xyz.struphy.plot.slice(\n", - " x=\"e1\",\n", - " y=\"e2\",\n", - " t=\"last\",\n", - " component=2,\n", - " e3=0,\n", - " coords=\"physical\",\n", - " plane=\"XY\",\n", - " title=\"$B_z$ of the coaxial mode\",\n", - ")" + "out_coaxial.em_fields.b_field_xyz.isel(t=-1, component=2, e3=0).plot(x=\"e1\", y=\"e2\")" ] }, { @@ -963,17 +935,7 @@ "metadata": {}, "outputs": [], "source": [ - "out_coaxial.em_fields.b_field_xyz.struphy.plot.panels(\n", - " x=\"e1\",\n", - " y=\"e2\",\n", - " component=2,\n", - " e3=0,\n", - " coords=\"physical\",\n", - " plane=\"XY\",\n", - " nrows=1,\n", - " ncols=4,\n", - " title=\"$B_z$ over time\",\n", - ")" + "out_coaxial.em_fields.b_field_xyz.isel(component=2, e3=0).plot(x=\"e1\", y=\"e2\", col=\"t\", col_wrap=4)" ] }, { From 7f5e080b228c74465e5c1f262cdbf46ea1065058 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 21:32:32 +0200 Subject: [PATCH 123/193] update claude skill --- .claude/skills/setup-simulation/SKILL.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 6ff3cd6a2..02038f820 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -139,7 +139,7 @@ from struphy import Output path_out = Path(__file__).resolve().parent / "sim_data" out = Output(path_out) -out.process(physical=True) # optional; products are otherwise processed with defaults on first access +out.pproc(physical=True) # optional; products are otherwise processed with defaults on first access out.scalars. # xarray time series, no post-processing needed out.fields.. # dims (t, [component,] e1, e2, e3) @@ -156,21 +156,19 @@ explicit output path. `Output` does not retain or construct a `Simulation`; there is no `out.sim`. Do not import the parameter module for post-processing. Metadata comes from -`run_metadata.json`, with legacy `config.json` supported only as a fallback. -`sim.pproc()` remains supported for existing callers. Under MPI, call -`out.process()` on every rank; `parallel=True` requires the saved run's rank count. +`run_metadata.json`. Under MPI, call `out.pproc()` on every rank; `parallel=True` +requires the saved run's rank count. -Products sit under their species and plot themselves, no imports needed: +Products are xarray objects. Use xarray's plotting and selection methods: ```python -out...f.struphy.plot.slice(x="e1", y="v1", t="last") # also .panels/.viewer/.animation/.frames -out..orbits.struphy.plot.trajectories() -out.scalars..struphy.plot.timeseries(fit=(t0, t1)) # also .growth_rate/.drift/.relative_error -out.plot.scalars(), out.plot.equilibrium(), out.save_report() # whole-run plots +out...f.isel(t=-1).plot(x="e1", y="v1") +out.scalars..plot.line(x="t") +out..orbits.isel(marker=0).sel(quantity=["x", "y", "z"]).plot.line(x="t", hue="quantity") ``` -Name any dimension to select it: `t="last"`, `t=-1` (position), `t=0.35` (nearest value), `component=0`. -See `examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py` for a complete script. +Select by position with `.isel(t=-1, component=0)` or by coordinate with +`.sel(t=0.35, method="nearest")`. See the post-processing tutorial for a complete script. ## Common pitfalls From cc8cba9b227e2601bd3c76c1ee616d124c70c744 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 22:21:50 +0200 Subject: [PATCH 124/193] Set default etato 0.5 in evaluate --- src/struphy/post_processing/output.py | 50 +++++++++++++++---- .../post_processing/tests/test_output.py | 37 ++++++++++++++ tutorials/tutorial_post_processing.ipynb | 19 ++----- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 9edf0ae08..e5ff49ada 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -292,12 +292,14 @@ def evaluate( A float ``t`` selects a time coordinate. Other keyword arguments select named coordinates, for example ``component=2`` or ``e1=0.5``. - Supplying all of ``eta1``, ``eta2`` and ``eta3`` instead evaluates a raw FEEC - spline field directly on that logical grid. Each eta can be a scalar, a list, - a one-dimensional array, or a ``range``; mixed inputs form their tensor-product - mesh internally. This reads coefficients one saved snapshot at a time and does - not materialize a spatial post-processing product. Use a raw field name such as - ``"em_fields/e_field"``. ``representation`` selects the output representation + Supplying an ``eta`` evaluates a raw FEEC spline field directly on that logical + grid. Each eta can be a scalar, a list, a one-dimensional array, or a ``range``; + mixed inputs form their tensor-product mesh internally. Omitted directions in a + line or plane cut use the logical midpoint, ``0.5``. When ``name`` identifies a + raw FEEC field and all three etas are omitted, the field is evaluated at the + cell centres of the full simulation grid. This reads coefficients one saved + snapshot at a time and does not materialize a spatial post-processing product. + Use a raw field name such as ``"em_fields/e_field"``. ``representation`` selects the output representation after spline evaluation: one of ``"0"``, ``"1"``, ``"2"``, ``"3"``, ``"v"``, or ``"norm"``. The input representation is inferred from the field's FEEC space. Scalars default to ``"0"`` and vectors to ``"norm"``. @@ -308,16 +310,23 @@ def evaluate( eta = (eta1, eta2, eta3) has_eta = any(value is not None for value in eta) + is_raw_spline_field = not has_eta and self._is_raw_spline_field(name) if has_eta: - if any(value is None for value in eta): - raise ValueError("eta1, eta2, and eta3 must be supplied together") + eta = tuple(0.5 if value is None else value for value in eta) array = self._evaluate_spline_field(name, *eta, t=t, method=method, representation=representation) t = None method = None + elif is_raw_spline_field: + array = self._evaluate_spline_field( + name, *self._default_logical_grid(), t=t, method=method, representation=representation + ) + t = None + method = None if not has_eta: - if representation is not None: - raise ValueError("representation requires eta1, eta2, and eta3") - array = self._product(name) + if representation is not None and not is_raw_spline_field: + raise ValueError("representation requires FEEC evaluation") + if not is_raw_spline_field: + array = self._product(name) if t is not None: if isinstance(t, (int, np.integer)): array = array.isel(t=[int(t)], drop=drop) @@ -340,6 +349,25 @@ def evaluate( raise ValueError("method requires a direct coordinate selector") return array.to_numpy() if as_numpy else array + def _is_raw_spline_field(self, name: str) -> bool: + """Whether ``name`` is a raw FEEC field saved in the primary output file.""" + try: + species, variable = name.split("/") + except ValueError: + return False + path = self.path_out / "data" / "data_proc0.hdf5" + with h5py.File(path) as file: + return f"feec/{species}/{variable}" in file + + def _default_logical_grid(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Return one logical cell-centre coordinate per simulation-grid cell.""" + if self.grid is None: + raise ValueError("default FEEC evaluation requires saved grid metadata") + num_elements = np.asarray(self.grid.num_elements, dtype=int) + if num_elements.shape != (3,) or np.any(num_elements <= 0): + raise ValueError("saved grid must define three positive num_elements values") + return tuple((np.arange(n, dtype=float) + 0.5) / n for n in num_elements) + def _evaluate_spline_field( self, name: str, eta1: Any, eta2: Any, eta3: Any, *, t: int | float | slice | Sequence[int] | None, method: str | None, representation: Representation | None, diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index a57733c43..319090447 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -3,6 +3,7 @@ import json import os from pathlib import Path +from types import SimpleNamespace import h5py import numpy as np @@ -327,6 +328,42 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): np.testing.assert_allclose(values[0], [[75.25, 85.25], [75.5, 85.5]]) +def test_evaluate_raw_spline_field_defaults_to_simulation_grid_cell_centres(run, monkeypatch): + class Field: + space_id = "H1" + + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): + return np.ones((len(eta1), len(eta2), len(eta3))) + + with h5py.File(run.path_out / "data" / "data_proc0.hdf5", "a") as file: + file["feec/em_fields"].create_dataset("phi", data=np.empty(0)) + run.grid = SimpleNamespace(num_elements=(2, 3, 4)) + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) + + values = run.evaluate("em_fields/phi", t=0) + + assert values.dims == ("t", "e1", "e2", "e3") + assert values.shape == (1, 2, 3, 4) + np.testing.assert_allclose(values.e1, [0.25, 0.75]) + np.testing.assert_allclose(values.e2, [1 / 6, 0.5, 5 / 6]) + np.testing.assert_allclose(values.e3, [0.125, 0.375, 0.625, 0.875]) + + +def test_evaluate_raw_spline_field_defaults_omitted_cut_coordinates_to_midpoint(run, monkeypatch): + class Field: + space_id = "H1" + + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): + return np.asarray(eta1)[:, None, None] + eta2 + eta3 + + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) + + values = run.evaluate("em_fields/phi", eta1=[0.25, 0.75], t=0) + + assert values.dims == ("t", "e1") + np.testing.assert_allclose(values[0], [1.25, 1.75]) + + def test_evaluate_raw_spline_field_rejects_coordinates_outside_unit_cube(run): with pytest.raises(ValueError, match="logical unit interval"): run.evaluate("em_fields/phi", eta1=-0.01, eta2=0.5, eta3=0.5) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index f2791391b..ce4b52a36 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -259,7 +259,7 @@ "source": [ "### Evaluate saved splines directly: 1-D, 2-D, and 3-D\n", "\n", - "For a small number of logical points, `evaluate()` can read the saved FEEC coefficients and evaluate the spline directly, without creating a full post-processing field. Supply all three logical coordinates, each in the unit interval. Scalars, lists, NumPy arrays, and `range` objects can be mixed; every non-scalar input becomes a dimension of the tensor-product result. Thus one varying coordinate makes a 1-D line, two make a 2-D plane, and three make a 3-D volume. The result is always an xarray array with named logical coordinates.\n", + "`evaluate()` reads saved FEEC coefficients and evaluates the spline directly, without creating a post-processing field. With no `eta` arguments, it evaluates the full simulation grid at its cell centres. For cuts, provide only the logical coordinates that should vary; omitted directions use the midpoint, `0.5`. Scalars, lists, NumPy arrays, and `range` objects can be mixed; every non-scalar input becomes a dimension of the tensor-product result. Thus one varying coordinate makes a 1-D line, two make a 2-D plane, and no coordinates makes a 3-D volume. The result is always an xarray array with named logical coordinates.\n", "\n", "A representation conversion is applied after spline evaluation. Its input is inferred from the saved FEEC space, so `representation=` specifies only the target: `\"0\"`, `\"1\"`, `\"2\"`, `\"3\"`, `\"v\"`, or `\"norm\"`. Scalars default to `\"0\"`; vectors default to `\"norm\"`." ] @@ -271,13 +271,11 @@ "metadata": {}, "outputs": [], "source": [ - "# 1-D: a field line at fixed eta2 and eta3, from the final saved time.\n", + "# 1-D: a field line; eta2 and eta3 default to their midpoints.\n", "eta1_line = np.linspace(0.0, 1.0, 128)\n", "phi_line = out.evaluate(\n", " \"em_fields/phi\",\n", " eta1=eta1_line,\n", - " eta2=0.5,\n", - " eta3=0.5,\n", " t=-1,\n", ")\n", "phi_line.plot()\n", @@ -299,24 +297,17 @@ "metadata": {}, "outputs": [], "source": [ - "# 2-D: a logical eta1--eta2 plane.\n", + "# 2-D: a logical eta1--eta2 plane; eta3 defaults to its midpoint.\n", "phi_plane = out.evaluate(\n", " \"em_fields/phi\",\n", " eta1=np.linspace(0.0, 1.0, 64),\n", " eta2=np.linspace(0.0, 1.0, 48),\n", - " eta3=0.5,\n", " t=-1,\n", ")\n", "phi_plane.plot(x=\"e1\", y=\"e2\")\n", "\n", - "# 3-D: a modest logical volume. The result stays a labeled xarray DataArray.\n", - "phi_volume = out.evaluate(\n", - " \"em_fields/phi\",\n", - " eta1=np.linspace(0.0, 1.0, 32),\n", - " eta2=np.linspace(0.0, 1.0, 24),\n", - " eta3=np.linspace(0.0, 1.0, 16),\n", - " t=-1,\n", - ")\n", + "# 3-D: no eta arguments uses the complete simulation grid at cell centres.\n", + "phi_volume = out.evaluate(\"em_fields/phi\", t=-1)\n", "print(phi_volume.dims, phi_volume.shape)\n", "\n", "# xarray plots a 2-D slice of the volume; choose the mid-plane by coordinate index.\n", From 450c671eefef730e741f6abbdc16aedc07d6f929 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 22:34:10 +0200 Subject: [PATCH 125/193] Include mapped physical X, Y, and Z coordinates --- src/struphy/post_processing/output.py | 17 ++++++++++++++++- .../post_processing/tests/test_output.py | 14 ++++++++++++++ tutorials/tutorial_post_processing.ipynb | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index e5ff49ada..5c03f99d0 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -410,7 +410,22 @@ def _evaluate_spline_field( coords: dict[str, Any] = {"t": times[indices], **grid_coords} if is_vector: coords["component"] = np.arange(data.shape[1]) - return self._stamp(xr.DataArray(data, dims=dims, coords=coords, name=variable)) + array = self._stamp(xr.DataArray(data, dims=dims, coords=coords, name=variable)) + return self._attach_physical_coords(array, etas, grid_dims) + + def _attach_physical_coords( + self, array: xr.DataArray, etas: tuple[Any, Any, Any], dims: tuple[str, ...] + ) -> xr.DataArray: + """Map a raw FEEC evaluation grid and attach its Cartesian coordinates.""" + grids = [np.atleast_1d(np.asarray(eta, dtype=float)) for eta in etas] + mapped = self.domain(*grids) + shape = tuple(len(grid) for grid in grids) + keep = tuple(slice(None) if dim in dims else 0 for dim in ("e1", "e2", "e3")) + coordinates = {} + for name, values in zip(("X", "Y", "Z"), mapped): + values = np.asarray(values).reshape(shape)[keep] + coordinates[name] = (dims, values) if dims else values.item() + return array.assign_coords(coordinates) def _apply_representation( self, value: Any, etas: tuple[Any, Any, Any], source: str, representation: Representation | None, diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 319090447..f0431fecb 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -337,7 +337,14 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): with h5py.File(run.path_out / "data" / "data_proc0.hdf5", "a") as file: file["feec/em_fields"].create_dataset("phi", data=np.empty(0)) + + class Domain: + def __call__(self, eta1, eta2, eta3): + e1, e2, e3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + return e1 + 1.0, e2 + 2.0, e3 + 3.0 + run.grid = SimpleNamespace(num_elements=(2, 3, 4)) + run.domain = Domain() monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) values = run.evaluate("em_fields/phi", t=0) @@ -347,6 +354,10 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): np.testing.assert_allclose(values.e1, [0.25, 0.75]) np.testing.assert_allclose(values.e2, [1 / 6, 0.5, 5 / 6]) np.testing.assert_allclose(values.e3, [0.125, 0.375, 0.625, 0.875]) + assert values.X.dims == values.Y.dims == values.Z.dims == ("e1", "e2", "e3") + np.testing.assert_allclose(values.X[:, 0, 0], [1.25, 1.75]) + np.testing.assert_allclose(values.Y[0, :, 0], [2 + 1 / 6, 2.5, 2 + 5 / 6]) + np.testing.assert_allclose(values.Z[0, 0, :], [3.125, 3.375, 3.625, 3.875]) def test_evaluate_raw_spline_field_defaults_omitted_cut_coordinates_to_midpoint(run, monkeypatch): @@ -383,6 +394,9 @@ def transform(self, value, *etas, kind, squeeze_out): calls.append((kind, etas, squeeze_out)) return value + def __call__(self, eta1, eta2, eta3): + return np.meshgrid(eta1, eta2, eta3, indexing="ij") + monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) monkeypatch.setattr(run, "domain", Domain()) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index ce4b52a36..cf9eec428 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -259,7 +259,7 @@ "source": [ "### Evaluate saved splines directly: 1-D, 2-D, and 3-D\n", "\n", - "`evaluate()` reads saved FEEC coefficients and evaluates the spline directly, without creating a post-processing field. With no `eta` arguments, it evaluates the full simulation grid at its cell centres. For cuts, provide only the logical coordinates that should vary; omitted directions use the midpoint, `0.5`. Scalars, lists, NumPy arrays, and `range` objects can be mixed; every non-scalar input becomes a dimension of the tensor-product result. Thus one varying coordinate makes a 1-D line, two make a 2-D plane, and no coordinates makes a 3-D volume. The result is always an xarray array with named logical coordinates.\n", + "`evaluate()` reads saved FEEC coefficients and evaluates the spline directly, without creating a post-processing field. With no `eta` arguments, it evaluates the full simulation grid at its cell centres. For cuts, provide only the logical coordinates that should vary; omitted directions use the midpoint, `0.5`. Scalars, lists, NumPy arrays, and `range` objects can be mixed; every non-scalar input becomes a dimension of the tensor-product result. Thus one varying coordinate makes a 1-D line, two make a 2-D plane, and no coordinates makes a 3-D volume. The result is always an xarray array with logical `e1`/`e2`/`e3` coordinates and mapped physical `X`/`Y`/`Z` coordinates.\n", "\n", "A representation conversion is applied after spline evaluation. Its input is inferred from the saved FEEC space, so `representation=` specifies only the target: `\"0\"`, `\"1\"`, `\"2\"`, `\"3\"`, `\"v\"`, or `\"norm\"`. Scalars default to `\"0\"`; vectors default to `\"norm\"`." ] From a1ecdb7c6c404f2df354e11298d312dd4e592de9 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 22:58:35 +0200 Subject: [PATCH 126/193] Removed numpy return, added scalars evaluation, pic/sph binning defaults --- doc/markdown/output-api.md | 92 +++++----- src/struphy/post_processing/output.py | 163 ++++++++++++++++-- .../post_processing/tests/test_output.py | 49 +++++- tutorials/tutorial_post_processing.ipynb | 4 +- 4 files changed, 233 insertions(+), 75 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 45982f4b0..84b0d2097 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -17,8 +17,9 @@ out = sim.run() ## Discover available data -Use `keys()` to list the names accepted by `evaluate()`, or `info()` for the same names with -short descriptions. +Use `keys()` to list discovered products, or `info()` for their short descriptions. Pass fields +and particle data to `evaluate()` as `species/variable`; use the reserved `"scalars"` name for +scalar histories. ```python out.info() @@ -51,47 +52,55 @@ missing non-scalar product when running serially. ## Evaluate data -`evaluate()` returns an ordinary `xarray.DataArray`. Use xarray for selections, arithmetic, -reductions, and interoperability with other scientific Python packages. +`evaluate()` always returns an xarray object: an `xarray.DataArray` for one field or particle +product, and an `xarray.Dataset` for `"scalars"`. Use xarray for selections, arithmetic, +reductions, plotting, and interoperability with other scientific Python packages. ```python rho = out.evaluate("diagnostics/rho_xyz") -phi = out.evaluate("phi_integral") +scalars = out.evaluate("scalars") +electric_energy = out.evaluate("scalars", variables="electric_energy") # Last saved time and one vector component, selected by integer position -electric_field = out.evaluate("em_fields/E", isel={"t": -1, "component": 2}) +electric_field = out.evaluate("em_fields/E", t=-1, component=2) # Select the nearest logical-coordinate plane -midplane = out.evaluate( - "diagnostics/rho_xyz", - sel={"e3": 0.5}, - method="nearest", - drop=True, -) +midplane = out.evaluate("diagnostics/rho_xyz", e3=0.5, method="nearest", drop=True) # Select a time range -history = out.evaluate("phi_integral", isel={"t": slice(100, None)}) +history = out.evaluate("scalars", variables="electric_energy", t=slice(100, None)).electric_energy ``` -`isel` uses integer positions and `sel` uses named dimension-coordinate values. Selections are -applied in that order. Physical `X`, `Y`, and `Z` coordinates describe the mapped logical grid; -evaluating at an arbitrary physical point requires interpolation or an inverse-coordinate map. +Integer `t` values select saved snapshot positions; a floating-point `t` selects a saved time +coordinate. Other keyword arguments select named xarray coordinates. Physical `X`, `Y`, and `Z` +coordinates describe the mapped logical grid; evaluating at an arbitrary physical point requires +interpolation or an inverse-coordinate map. -To return only values, without xarray coordinates and attributes, use `as_numpy=True`. +For raw FEEC fields, use the `species/variable` name. With no `eta` coordinates, evaluation uses +the full simulation grid at cell centres and includes `X`, `Y`, and `Z` coordinates. Providing +one or two eta coordinates makes a line or plane cut; unspecified directions use `0.5`. ```python -rho_values = out.evaluate("diagnostics/rho_xyz", isel={"t": -1}, as_numpy=True) +import numpy as np + +phi = out.evaluate("em_fields/phi", t=-1) # full 3-D grid +line = out.evaluate("em_fields/phi", eta1=np.linspace(0, 1, 200), t=-1) ``` -For domains with an analytical inverse map, evaluate a field at a physical point directly: +Particle products use the same `species/variable` form. The default is the first matching binned +result, followed by a density/KDE result and then orbits. `info()` exposes the available choices; +`dataset=` selects one explicitly. ```python -value = out.evaluate( - "em_fields/phi_xyz", - physical={"X": 1.0, "Y": 0.0, "Z": 0.2}, -) +out.info("kinetic_ions/f") +distribution = out.evaluate("kinetic_ions/f") +delta_f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") ``` +For a quick inspection, `out.plot(array)` chooses the last time, first vector component, and +midpoint slices as needed. For controlled figures, select dimensions explicitly and call xarray's +native `.plot()` methods. + ## Analyze and report data Numerical helpers stay on `Output` and return values or xarray arrays rather than figures. @@ -146,9 +155,9 @@ by the mass. Mean and variance are NaN where the density is not positive, and a has only the density (its perturbation). ```python -f = "kinetic_ions/e1_v1_density/f" +f = "kinetic_ions/f" -data = out.evaluate(f) +data = out.evaluate(f, dataset="e1_v1_density/f") f_of_v = data.mean(("e1", "e2", "e3"), missing_dims="ignore") velocity_grid = data.v1 bin_width = velocity_grid.differentiate("v1") @@ -212,41 +221,22 @@ For anything not covered here, `out.profile.results` is the full `scope_profiler ## Plot data -Struphy-aware plotting is performed by `Output`, not by modifying xarray arrays. Rendering -methods return `(fig, ax)` (or `(fig, axes)` for panels), so normal Matplotlib controls display, -saving, and further customization. +Use the native xarray plotting methods after making the intended selection. ```python -from matplotlib import pyplot as plt - -fig, ax = out.timeseries("phi_integral", fit=(0.0, None), fit_amplitude=True) -ax.set_title("Potential growth") +out.evaluate("scalars", variables="electric_energy").electric_energy.plot.line(x="t") -fig, ax = out.viewer( - "diagnostics/rho_xyz", - x="e1", - y="e2", - coords="physical", - plane="RZ", -) - -fig, axes = out.panels("kinetic_ions/e1_v1_density/f", x="e1", y="v1") -fig, ax = out.trajectories("kinetic_ions", max_markers=1000) - -plt.show() +rho = out.evaluate("diagnostics/rho_xyz", t=-1) +rho.isel(e3=rho.sizes["e3"] // 2).plot(x="e1", y="e2") ``` -Plotting methods also accept a derived `DataArray` instead of a saved-product name. +For an intentionally simple inspection plot, pass an xarray object to `Output.plot()`. +It chooses the last time, first vector component, and midpoint slices until xarray can plot it. ```python -rho_last = out.evaluate("diagnostics/rho_xyz", isel={"t": -1}) -fig, ax = out.slice(rho_last, x="e1", y="e2", coords="physical", plane="RZ") +out.plot(out.evaluate("em_fields/E")) ``` -The available product plotting methods are `timeseries`, `slice`, `panels`, `viewer`, -`animation`, and `trajectories`. `view` creates a reusable view configuration, while `frames` -writes PNG files and returns their paths. Whole-run plots are `plot_scalars` and `equilibrium`. - ## MPI post-processing `Output` always uses `MPI.COMM_WORLD`; no communicator is passed to its constructor. diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 5c03f99d0..d16bd66c8 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -246,8 +246,8 @@ def __exit__(self, *_): @staticmethod def compare(first: "Output", second: "Output", product: str, *, method: str = "linear") -> xr.Dataset: """Align one product from two runs and return both values, their difference and ratio.""" - left = first.evaluate(product) - right = second.evaluate(product) + left = first._array(product) + right = second._array(product) right = right.interp_like(left, method=method) difference = left - right ratio = xr.where(right != 0, left / right, np.nan) @@ -270,27 +270,31 @@ def evaluate( *, method: str | None = None, drop: bool = False, - as_numpy: bool = False, t: int | float | slice | Sequence[int] | None = None, eta1: Any | None = None, eta2: Any | None = None, eta3: Any | None = None, representation: Representation | None = None, + dataset: str | None = None, + variables: str | Sequence[str] | None = None, **coordinates: Any, - ) -> xr.DataArray | np.ndarray: - """Return a named simulation product as an :class:`xarray.DataArray`. + ) -> xr.DataArray | xr.Dataset: + """Return a named simulation product as an xarray object. Scalars are read directly from raw output. Other products are materialized with :meth:`pproc` on first use when no complete post-processing output exists. The returned array is an ordinary xarray object, so use xarray for selection, arithmetic and further - analysis. Set ``as_numpy=True`` to return only the selected values as a - :class:`numpy.ndarray`. + analysis. ``evaluate("scalars")`` returns an :class:`xarray.Dataset` containing + all scalar histories; use ``variables=`` to select scalar names. Common selections can be passed directly: ``t`` selects saved snapshots by index (an integer, list of integers, or slice); omit it for every saved timestep. The returned array always retains its ``t`` dimension. A float ``t`` selects a time coordinate. Other keyword arguments select - named coordinates, for example ``component=2`` or ``e1=0.5``. + named coordinates, for example ``component=2`` or ``e1=0.5``. For particle + products, a ``"species/variable"`` name selects the first matching binned + product, then density/KDE product, then orbits. Pass ``dataset=`` to select a + particular discovered product; use ``out.info("species/variable")`` to list them. Supplying an ``eta`` evaluates a raw FEEC spline field directly on that logical grid. Each eta can be a scalar, a list, a one-dimensional array, or a ``range``; @@ -307,11 +311,32 @@ def evaluate( selectors = dict(coordinates) if "physical" in selectors: raise TypeError("physical is no longer supported; use eta1, eta2, eta3 and representation") + if "as_numpy" in selectors: + raise TypeError("evaluate() always returns xarray; call .to_numpy() on its result when needed") + if name != "scalars" and name.count("/") != 1: + raise ValueError("evaluate() names must use the 'species/variable' form, or be 'scalars'") eta = (eta1, eta2, eta3) has_eta = any(value is not None for value in eta) - is_raw_spline_field = not has_eta and self._is_raw_spline_field(name) - if has_eta: + if has_eta and dataset is not None: + raise ValueError("dataset= cannot be combined with direct FEEC eta evaluation") + if name == "scalars": + if has_eta or representation is not None or dataset is not None: + raise ValueError("'scalars' accepts only time, variables, and coordinate selections") + if variables is None: + array: xr.DataArray | xr.Dataset = self.scalars + else: + names = [variables] if isinstance(variables, str) else list(variables) + unknown = set(names) - set(self.scalars.data_vars) + if unknown: + raise KeyError(f"unknown scalar variables: {tuple(sorted(unknown))}") + array = self.scalars[names] + elif variables is not None: + raise ValueError("variables= is only valid with evaluate('scalars')") + is_raw_spline_field = not has_eta and dataset is None and self._is_raw_spline_field(name) + if name == "scalars": + is_raw_spline_field = False + elif has_eta: eta = tuple(0.5 if value is None else value for value in eta) array = self._evaluate_spline_field(name, *eta, t=t, method=method, representation=representation) t = None @@ -325,8 +350,8 @@ def evaluate( if not has_eta: if representation is not None and not is_raw_spline_field: raise ValueError("representation requires FEEC evaluation") - if not is_raw_spline_field: - array = self._product(name) + if not is_raw_spline_field and name != "scalars": + array = self._product(name, dataset=dataset) if t is not None: if isinstance(t, (int, np.integer)): array = array.isel(t=[int(t)], drop=drop) @@ -347,7 +372,7 @@ def evaluate( array = array.sel(selectors, **options) elif method is not None: raise ValueError("method requires a direct coordinate selector") - return array.to_numpy() if as_numpy else array + return array def _is_raw_spline_field(self, name: str) -> bool: """Whether ``name`` is a raw FEEC field saved in the primary output file.""" @@ -505,13 +530,18 @@ def _snapshot_indices( raise IndexError("t is outside the saved snapshot range") return indices - def _product(self, name: str) -> xr.DataArray: + def _product(self, name: str, *, dataset: str | None = None) -> xr.DataArray: """Resolve one saved product for :meth:`evaluate`.""" + if dataset is not None: + name = self._dataset_key(name, dataset) if name in self.scalars.data_vars: return self.scalars[name] for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): if name in catalog: return catalog[name] + candidates = self._particle_candidates(name) + if candidates: + return candidates[0][1] available = ( *self.scalars.data_vars, *self.field_catalog, @@ -521,6 +551,60 @@ def _product(self, name: str) -> xr.DataArray: ) raise KeyError(f"{name!r} not found; available products: {available}") + def _dataset_key(self, name: str, dataset: str) -> str: + """Resolve a species-relative explicit particle dataset name.""" + if dataset in self.keys(): + return dataset + try: + species, _ = name.split("/") + except ValueError as error: + raise ValueError("dataset= requires a 'species/variable' name") from error + key = species if dataset == "orbits" else f"{species}/{dataset}" + if key not in self.keys(): + raise KeyError(f"{dataset!r} is not a dataset for {species!r}; choices: {self._candidate_keys(name)}") + return key + + def _particle_candidates(self, name: str) -> list[tuple[str, xr.DataArray]]: + """Particle products matching ``species/variable``, in public default order.""" + try: + species, variable = name.split("/") + except ValueError: + return [] + candidates = [] + for catalog in (self.distribution_catalog, self.density_catalog): + for key in catalog: + if key.startswith(f"{species}/") and key.rsplit("/", 1)[-1] == variable: + candidates.append((key, catalog[key])) + if candidates: + return candidates + if variable == "orbits" and species in self.orbit_catalog: + return [(species, self.orbit_catalog[species])] + for catalog in (self.distribution_catalog, self.density_catalog): + keys = [key for key in catalog if key.startswith(f"{species}/")] + if keys: + return [(key, catalog[key]) for key in keys] + if species in self.orbit_catalog: + candidates.append((species, self.orbit_catalog[species])) + return candidates + + def _candidate_keys(self, name: str) -> tuple[str, ...]: + """Discovered particle datasets that can be selected for a species/variable name.""" + try: + species, variable = name.split("/") + except ValueError: + return () + keys = [key for key, _ in self._particle_candidates(name)] + if not keys and variable == "*": + keys = [ + key + for catalog in (self.distribution_catalog, self.density_catalog) + for key in sorted(catalog) + if key.startswith(f"{species}/") + ] + if species in self.orbit_catalog: + keys.append(species) + return tuple(keys) + def with_physical_coords(self, product: str | xr.DataArray) -> xr.DataArray: """Attach mapped ``X``, ``Y``, ``Z`` coordinates to a product on a logical grid. @@ -567,7 +651,7 @@ def catalog(self, *, details: bool = False) -> xr.Dataset: "description": ("product", [self._product_description(key) for key in keys]), } if details: - arrays = [self.evaluate(key) for key in keys] + arrays = [self._array(key) for key in keys] data["dimensions"] = ("product", [", ".join(array.dims) for array in arrays]) data["units"] = ("product", [str(array.attrs.get("units", "")) for array in arrays]) return xr.Dataset(data, coords={"product": list(keys)}) @@ -585,11 +669,40 @@ def provenance(self, product: str | None = None) -> dict: def _array(self, product: str | xr.DataArray) -> xr.DataArray: """Resolve a saved product name or accept an already-derived xarray array.""" if isinstance(product, str): + if product in self.scalars.data_vars: + return self.scalars[product] + for catalog in (self.field_catalog, self.distribution_catalog, self.density_catalog, self.orbit_catalog): + if product in catalog: + return catalog[product] return self.evaluate(product) if isinstance(product, xr.DataArray): return product raise TypeError(f"product must be a product name or xarray.DataArray, got {type(product).__name__}") + def plot(self, array: xr.DataArray | xr.Dataset, **kwargs: Any) -> Any: + """Make a small xarray quick-look plot. + + Time-dependent spatial data are shown at the last saved time, vector data use + the first component, and remaining dimensions beyond two are sliced at their + midpoint. For publication plots, select dimensions explicitly and call xarray's + plotting methods directly. + """ + if isinstance(array, xr.Dataset): + if len(array.data_vars) != 1: + raise ValueError("plot() needs a DataArray or a Dataset containing exactly one variable") + array = array[next(iter(array.data_vars))] + if not isinstance(array, xr.DataArray): + raise TypeError(f"plot() needs an xarray DataArray or Dataset, got {type(array).__name__}") + view = array + for dim, index in (("t", -1), ("component", 0)): + if dim in view.dims and view.ndim > 2: + view = view.isel({dim: index}) + while view.ndim > 2: + view = view.isel({view.dims[-1]: view.sizes[view.dims[-1]] // 2}) + if view.ndim == 1 and "t" in view.dims: + return view.plot.line(x="t", **kwargs) + return view.plot(**kwargs) + @property def units(self): """The units of the run's normalization, in SI; see :class:`struphy.physics.physics.Units`.""" @@ -2133,14 +2246,30 @@ def label(self) -> str: self._label = ", ".join(values) or self.path_out.name return self._label - def info(self) -> None: + def info(self, name: str | None = None) -> None: """Print a concise run summary, configuration reference, and product catalog. Use ``out.info()`` interactively. The summary includes model parameters, species variables, propagator options, and initial-condition definitions saved in metadata. As with :meth:`keys`, the product catalog materializes default post-processing when needed; call :meth:`pproc` first to choose its options. + ``out.info("species/variable")`` instead lists the particle datasets that can + satisfy that request, in the default selection order. """ + if name is not None: + if name == "scalars": + rows = [(key, self._product_description(key)) for key in self.scalars.data_vars] + else: + rows = [(key, self._product_description(key)) for key in self._candidate_keys(name)] + if not rows: + print(f"No alternative datasets for {name!r}.") + return + width = max(len(key) for key, _ in rows) + print("Dataset choices (default first):") + print(f"{'Dataset':<{width}} Description") + print(f"{'-' * width} -----------") + print("\n".join(f"{key:<{width}} {description}" for key, description in rows)) + return rows = [(key, self._product_description(key)) for key in self.keys()] key_width = max((len(key) for key, _ in rows), default=3) model = self.metadata.get("model", {}) @@ -2257,7 +2386,7 @@ def report(self, directory=None, *, products=(), format: str = "markdown", max_s if format not in {"markdown", "html"}: raise ValueError("format must be 'markdown' or 'html'") catalog = self.catalog(details=False) - requested = [self.evaluate(key) for key in products] + requested = [self._array(key) for key in products] directory = Path(directory) if directory else self.path_pproc / "report" directory.mkdir(parents=True, exist_ok=True) csv_path = save_scalars(self.scalars, str(directory / "scalars.csv")) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index f0431fecb..4df6b0e1d 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -269,19 +269,58 @@ def test_evaluate_selects_positions_coordinates_and_slices(run): assert every_second.sizes["t"] == 2 phase_space = run.evaluate( - "kinetic_ions/e1_v1_density/f", + "kinetic_ions/f", + dataset="e1_v1_density/f", e1=0.49, method="nearest", drop=True, ) assert phase_space.dims == ("t", "v1") - history = run.evaluate("en_tot").isel(t=slice(1, None)) + history = run.evaluate("scalars", variables="en_tot").en_tot.isel(t=slice(1, None)) assert history.sizes["t"] == NT - 1 - values = run.evaluate("en_tot", t=-1, as_numpy=True) - assert isinstance(values, np.ndarray) - np.testing.assert_allclose(values, 2.0) + with pytest.raises(TypeError, match="always returns xarray"): + run.evaluate("scalars", variables="en_tot", t=-1, as_numpy=True) + + with pytest.raises(ValueError, match="species/variable"): + run.evaluate("en_tot") + + +def test_evaluate_scalars_and_particle_defaults(run): + scalars = run.evaluate("scalars", variables="en_tot", t=-1) + assert isinstance(scalars, xr.Dataset) + assert list(scalars.data_vars) == ["en_tot"] + assert scalars.sizes["t"] == 1 + + distribution = run.evaluate("kinetic_ions/f") + assert distribution.name == "f" + assert distribution.dims == ("t", "e1", "v1") + + fallback = run.evaluate("kinetic_ions/any_variable") + assert fallback.name == "f" + + density = run.evaluate("kinetic_ions/n") + assert density.name == "n" + assert density.dims == ("t", "e1", "e2", "e3") + + selected = run.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") + assert selected.name == "delta_f" + + orbits = run.evaluate("kinetic_ions/orbits") + assert orbits.name == "orbits" + + +def test_info_lists_particle_dataset_choices_in_default_order(run, capsys): + run.info("kinetic_ions/f") + report = capsys.readouterr().out + assert "Dataset choices (default first):" in report + assert "kinetic_ions/e1_v1_density/f" in report + + +def test_plot_makes_a_quick_xarray_view(run): + artist = run.plot(run.evaluate("em_fields/E")) + assert artist is not None def test_evaluate_raw_spline_field_at_logical_point(run, monkeypatch): diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index cf9eec428..5e14cf893 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -158,7 +158,7 @@ "id": "7", "metadata": {}, "source": [ - "Products sit on the run under the species that produced them, so VS Code and interactive shells complete them as you type: `out.kinetic_ions.e1_v1_density.f`, `out.kinetic_ions.orbits`, `out.em_fields.phi_log`. To discover the products a particular namespace contains, evaluate it or inspect its flat, lazy `.catalog`: `list(out.kinetic_ions.catalog)`. The grouped views `out.fields`, `out.distributions`, `out.densities` and `out.orbits` show the same products by kind, and `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks one up by name, which is handy in scripts and loops." + "Products sit on the run under the species that produced them, so VS Code and interactive shells complete them as you type: `out.kinetic_ions.e1_v1_density.f`, `out.kinetic_ions.orbits`, `out.em_fields.phi_log`. To discover the products a particular namespace contains, inspect its flat, lazy `.catalog`: `list(out.kinetic_ions.catalog)`. The grouped views `out.fields`, `out.distributions`, `out.densities` and `out.orbits` show the same products by kind. In scripts, `out.evaluate(\"kinetic_ions/f\", dataset=\"e1_v1_density/f\")` looks up one explicitly." ] }, { @@ -399,7 +399,7 @@ "source": [ "## Scalar overview and time series\n", "\n", - "Every product is an xarray `DataArray`. `out.evaluate(\"kinetic_ions/e1_v1_density/f\")` looks a product up by name, which suits scripts and loops; attribute access is convenient interactively.\n", + "Field and particle products are xarray `DataArray` objects; `out.evaluate(\"scalars\")` returns an xarray `Dataset`. `out.evaluate(\"kinetic_ions/f\", dataset=\"e1_v1_density/f\")` looks a product up by name, which suits scripts and loops; attribute access is convenient interactively.\n", "\n", "Call `.plot()` for a scalar time series. Use a Matplotlib axes when combining several series or setting plot options." ] From 24ad50cf462b162fcefde4337ae0bb1243fc9c92 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Sep 2026 23:51:39 +0200 Subject: [PATCH 127/193] Temporary: add the postprocesssing scripts in postprocessing_external/ --- postprocessing_external/README.md | 17 + postprocessing_external/pyproject.toml | 16 + .../src/struphy_plots/__init__.py | 5 + .../src/struphy_plots/accessors.py | 489 +++++++++++ .../src/struphy_plots/analysis.py | 193 +++++ .../src/struphy_plots/arrays.py | 78 ++ .../src/struphy_plots/output_accessors.py | 58 ++ .../src/struphy_plots/plotting.py | 768 ++++++++++++++++++ .../tests/test_analysis_and_core_output.py | 302 +++++++ .../tests/test_output_accessors.py | 259 ++++++ .../tests/test_plotting.py | 308 +++++++ 11 files changed, 2493 insertions(+) create mode 100644 postprocessing_external/README.md create mode 100644 postprocessing_external/pyproject.toml create mode 100644 postprocessing_external/src/struphy_plots/__init__.py create mode 100644 postprocessing_external/src/struphy_plots/accessors.py create mode 100644 postprocessing_external/src/struphy_plots/analysis.py create mode 100644 postprocessing_external/src/struphy_plots/arrays.py create mode 100644 postprocessing_external/src/struphy_plots/output_accessors.py create mode 100644 postprocessing_external/src/struphy_plots/plotting.py create mode 100644 postprocessing_external/tests/test_analysis_and_core_output.py create mode 100644 postprocessing_external/tests/test_output_accessors.py create mode 100644 postprocessing_external/tests/test_plotting.py diff --git a/postprocessing_external/README.md b/postprocessing_external/README.md new file mode 100644 index 000000000..6e9b7e801 --- /dev/null +++ b/postprocessing_external/README.md @@ -0,0 +1,17 @@ +# struphy-plots (staging) + +This directory contains the optional plotting layer extracted from Struphy. It +is deliberately a standalone source package so it can be moved to its own +repository without changing the Struphy runtime package. + +For development, install it with `pip install -e postprocessing_external`. +Import `struphy_plots` after installing it to register the optional +`xarray.DataArray.struphy` accessor on Struphy output arrays. For example, +`out.evaluate("em_fields/phi").struphy.plot.slice(x="e1", y="e2", t="last")`. +Direct plotting functions are available from +`struphy_plots.plotting`; analysis functions are in `struphy_plots.analysis`. + +The accessor provides time-series, lineout, slice, panel, vector, comparison, +animation, and marker-trajectory plots. For three-dimensional scalar fields, +install the optional PyVista dependency (`pip install struphy-plots[pyvista]`) and +use `field.struphy.plot.volume(t=-1)`, then call `show()` on the returned plotter. diff --git a/postprocessing_external/pyproject.toml b/postprocessing_external/pyproject.toml new file mode 100644 index 000000000..425fa21f6 --- /dev/null +++ b/postprocessing_external/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "struphy-plots" +version = "0.0.0" +description = "Optional plotting and diagnostics for labeled Struphy output" +requires-python = ">=3.10" +dependencies = ["matplotlib", "numpy", "xarray", "ipywidgets"] + +[project.optional-dependencies] +pyvista = ["pyvista"] + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/postprocessing_external/src/struphy_plots/__init__.py b/postprocessing_external/src/struphy_plots/__init__.py new file mode 100644 index 000000000..4c7511748 --- /dev/null +++ b/postprocessing_external/src/struphy_plots/__init__.py @@ -0,0 +1,5 @@ +"""Optional plotting and diagnostic helpers for Struphy xarray output.""" + +from .accessors import StruphyAccessor + +__all__ = ["StruphyAccessor"] diff --git a/postprocessing_external/src/struphy_plots/accessors.py b/postprocessing_external/src/struphy_plots/accessors.py new file mode 100644 index 000000000..e67e49b00 --- /dev/null +++ b/postprocessing_external/src/struphy_plots/accessors.py @@ -0,0 +1,489 @@ +"""Optional accessors for plots and diagnostics of a single labeled array. + +Every product of an :class:`~struphy.Output` carries this accessor after importing +``struphy_plots``, and so does every array derived from one. + +Dimensions that are neither displayed nor swept are selected by naming them: an integer is a +position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest +coordinate value (``t=0.35``). +""" + +from __future__ import annotations + +from typing import Literal + +import numpy as np +import xarray as xr + +Coordinates = Literal["logical", "physical"] +Plane = Literal["XY", "XZ", "YZ", "RZ"] + + +@xr.register_dataarray_accessor("struphy") +class StruphyAccessor: + """Struphy diagnostics of one array: ``array.struphy.plot`` and ``array.struphy.analysis``.""" + + def __init__(self, array: xr.DataArray): + self._array = array + + @property + def plot(self) -> "ArrayPlots": + """Plots of this array, e.g. ``array.struphy.plot.slice(x="e1", y="v1", t="last")``.""" + return ArrayPlots(self._array) + + @property + def analysis(self) -> "ArrayAnalysis": + """Diagnostics of this array, e.g. ``array.struphy.analysis.growth_rate()``.""" + return ArrayAnalysis(self._array) + + +class _ArrayAccessor: + def __init__(self, array: xr.DataArray): + self._array = array + + +class ArrayPlots(_ArrayAccessor): + """Plots of one array, as ``array.struphy.plot.(...)``. + + Dimensions that are neither displayed nor swept are selected by naming them: an integer is a + position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest + coordinate value (``t=0.35``). + """ + + def _view(self, x, y, sweep, coords, plane, selection): + from .plotting import View + + select, index = {}, {} + for dim, value in selection.items(): + if dim not in self._array.dims: + raise TypeError( + f"{dim!r} is not a dimension of {self._array.name!r}; its dimensions are {self._array.dims}" + ) + if value == "first": + index[dim] = 0 + elif value == "last": + index[dim] = -1 + elif isinstance(value, (bool, str)): + raise TypeError(f'cannot select {dim}={value!r}; use a number, or "first"/"last"') + elif isinstance(value, (int, np.integer)): + index[dim] = int(value) + else: + select[dim] = float(value) + return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) + + def timeseries( + self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, title: str | None = None, ax=None + ): + """This time series, and any others given, in one axes. + + Parameters + ---------- + *others: + Further arrays with the single dimension ``t``; they may come from other runs and + need not share this array's time grid. + logy: + Logarithmic value axis. + fit: + Time window ``(t0, t1)`` of an exponential fit per series (``None`` for an open end), + or ``True`` for the whole series. Rates are in ``result.fit_results``. + fit_amplitude: + The series is quadratic in an amplitude (e.g. an energy); fit the amplitude's rate. + """ + from .analysis import GrowthFit + from .plotting import plot_timeseries + + growth = None + if fit is not None and fit is not False: + window = (None, None) if fit is True else tuple(fit) + growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) + return plot_timeseries([self._array, *others], ax=ax, logy=logy, fit=growth, title=title) + + def lineout(self, *, x: str | None = None, ax=None, title: str | None = None, **selection): + """Plot a one-dimensional profile after selecting every other dimension.""" + from .plotting import _select, plot_lineout + + view = self._view(None, None, "t", "logical", "XY", selection) + return plot_lineout(_select(self._array, view), x=x, ax=ax, title=title) + + def vector( + self, *, x: str, y: str, components: tuple[int, int] = (0, 1), stride: int = 1, + coordinates: Coordinates = "logical", ax=None, **selection, + ): + """Plot two vector components after selecting time and remaining dimensions.""" + from .plotting import _select, plot_vector + + view = self._view(None, None, "t", coordinates, "XY", selection) + return plot_vector( + _select(self._array, view), x=x, y=y, components=components, stride=stride, coordinates=coordinates, ax=ax + ) + + def volume_slices(self, *, indices: dict[str, int] | None = None, cmap=None, **selection): + """Render three orthogonal slices of a selected scalar volume.""" + from .plotting import _select, plot_volume_slices + + view = self._view(None, None, "t", "logical", "XY", selection) + return plot_volume_slices(_select(self._array, view), indices=indices, cmap=cmap) + + def volume(self, *, name: str | None = None, cmap="viridis", opacity="linear", **selection): + """Create a PyVista volume plotter for a selected scalar field.""" + from .plotting import _select, pyvista_volume + + view = self._view(None, None, "t", "logical", "XY", selection) + return pyvista_volume(_select(self._array, view), name=name, cmap=cmap, opacity=opacity) + + def compare(self, other: xr.DataArray, *, mode: Literal["difference", "ratio"] = "difference", ax=None): + """Plot a one-dimensional aligned difference or ratio against another array.""" + from .plotting import plot_compare + + return plot_compare(self._array, other, mode=mode, ax=ax) + + def view( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + **selection, + ) -> "SliceView": + """Configure a reusable slice view without rendering a figure. + + Use xarray's ``.sel()``/``.isel()`` for general selection, or pass remaining + dimensions here (integers are positions, floats nearest coordinates, + ``"first"``/``"last"`` select an end). + + ``shared_clim=True`` fixes color limits over all selected data, including + frames omitted by a panel layout or export step. False rescales each frame. + Explicit ``vmin``/``vmax`` override either limit in both modes. ``cmap``, + ``equal_aspect`` and ``title`` apply to every presentation of this view. + + Examples + -------- + >>> view = f.struphy.plot.view(x="e1", y="v1", cmap="RdBu_r") + >>> view.slice(t="last") + >>> view.panels(nrows=2, ncols=3) + >>> view.save_frames("frames") + """ + self._view(x, y, sweep, coords, plane, selection) # validate selections now + return SliceView( + self._array, + dict(x=x, y=y, sweep=sweep, coords=coords, plane=plane), + selection, + dict(vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title), + ) + + def slice( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + ax=None, + **selection, + ): + """Render one 2-D slice; see :meth:`view` for shared options.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).slice(ax=ax) + + def panels( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + nrows: int = 3, + ncols: int = 4, + **selection, + ): + """Render evenly spaced snapshots; see :meth:`view` for shared options.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).panels(nrows=nrows, ncols=ncols) + + def viewer( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + **selection, + ): + """Create an interactive slider view; retain the returned viewer.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).viewer() + + def animation( + self, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + interval: int = 100, + step: int = 1, + **selection, + ): + """Animate the sweep; retain the returned Matplotlib animation.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).animation(interval=interval, step=step) + + def frames( + self, + directory, + *, + x: str | None = None, + y: str | None = None, + sweep: str = "t", + coords: Coordinates = "logical", + plane: Plane = "XY", + vmin=None, + vmax=None, + shared_clim: bool = True, + cmap: str | None = None, + equal_aspect: bool | None = None, + title: str | None = None, + step: int = 1, + prefix: str = "frame", + dpi: int = 110, + **selection, + ): + """Export PNGs; equivalent to ``plot.view(...).save_frames(directory)``.""" + return self.view( + x=x, + y=y, + sweep=sweep, + coords=coords, + plane=plane, + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + **selection, + ).save_frames(directory, step=step, prefix=prefix, dpi=dpi) + + def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): + """Three-dimensional paths of saved markers; for an orbit product.""" + from .plotting import plot_marker_trajectories + + return plot_marker_trajectories(self._array, ax=ax, max_markers=max_markers, show_paths=show_paths) + + +class SliceView: + """A configured array view, shared by static, interactive and exported plots. + + Construct with ``array.struphy.plot.view(...)``. Configuration does not create + figures or copy the underlying array. + """ + + def __init__(self, array, coordinates, selection, options): + self._array = array + self._coordinates = dict(coordinates) + self._selection = dict(selection) + self._options = dict(options) + + def _view(self, **selection): + return ArrayPlots(self._array)._view(**self._coordinates, selection={**self._selection, **selection}) + + def slice(self, *, ax=None, **selection): + """Draw a snapshot, e.g. ``view.slice(t="last")``; return a PlotResult.""" + # Resolve shared limits before selecting a single snapshot, so it uses + # the same scale as panels, animation and export of this configured view. + from .plotting import _SliceRenderer, plot_slice + + options = dict(self._options) + if options["shared_clim"]: + renderer = _SliceRenderer(self._array, self._view(), **options) + options.update(zip(("vmin", "vmax"), renderer.limits)) + return plot_slice(self._array, view=self._view(**selection), ax=ax, **options) + + def panels(self, *, nrows=3, ncols=4): + """Draw snapshots spread along the sweep; return a PlotResult.""" + from .plotting import plot_panels + + return plot_panels(self._array, view=self._view(), nrows=nrows, ncols=ncols, **self._options) + + def viewer(self): + """Create a viewer with sliders for unselected dimensions.""" + from .plotting import InteractiveSliceViewer + + return InteractiveSliceViewer(self._array, view=self._view(), **self._options) + + def animation(self, *, interval=100, step=1): + """Create a Matplotlib animation using this view's rendering options.""" + from .plotting import animate_slices + + return animate_slices(self._array, view=self._view(), interval=interval, step=step, **self._options) + + def save_frames(self, directory, *, step=1, prefix="frame", dpi=110): + """Export PNG frames using this view's rendering options; return paths.""" + from .plotting import save_frames + + return save_frames( + self._array, directory, view=self._view(), step=step, prefix=prefix, dpi=dpi, **self._options + ) + + +class ArrayAnalysis(_ArrayAccessor): + """Quantitative diagnostics of one array, as ``array.struphy.analysis.(...)``.""" + + def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): + """Fit ``exp(rate * t + intercept)`` to this time series within ``window``. + + With ``amplitude=True`` the series is quadratic in an amplitude (e.g. an energy) and the + amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, + ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. + """ + from .analysis import GrowthFit, growth_rate + + return growth_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def damping_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): + """Fit exponential decay to the envelope of this oscillating time series; see ``growth_rate``.""" + from .analysis import GrowthFit, damping_rate + + return damping_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) + + def envelope(self) -> xr.DataArray: + """Local maxima of this time series.""" + from .analysis import envelope + + return envelope(self._array) + + def norm(self, *, dims=None, squared: bool = False) -> xr.DataArray: + """L2 norm over ``dims`` (default: every dimension except ``t``).""" + from .analysis import norm + + return norm(self._array, dims=dims, squared=squared) + + def drift(self, *, ref=None) -> xr.DataArray: + """Signed deviation of this time series from ``ref`` or from its first sample.""" + from .analysis import drift + + return drift(self._array, ref=ref) + + def relative_error(self, *, ref=None, skip_first: bool = True) -> xr.DataArray: + """Absolute relative deviation from ``ref`` or from this series' first sample.""" + from .analysis import relative_error + + return relative_error(self._array, ref=ref, skip_first=skip_first) + + def spatial_average(self, *, dims=None) -> xr.DataArray: + """Mean over the logical space dimensions ``e1``, ``e2``, ``e3`` (or ``dims``). + + For a binned ``e1_v1`` distribution this is f(v1, t) averaged over space; see + :func:`struphy.diagnostics.analysis.spatial_average`. + """ + from .analysis import spatial_average + + return spatial_average(self._array, dims=dims) + + def velocity_moments(self, *, dims=None) -> xr.Dataset: + """Density, mean velocity and variance of a binned distribution over its velocity dimensions. + + See :func:`struphy.diagnostics.analysis.velocity_moments` for the definitions. + """ + from .analysis import velocity_moments + + return velocity_moments(self._array, dims=dims) + + def dispersion(self, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs): + """Space-time power spectrum of this field and fitted dispersion branches. + + The time coordinate must be normalized, see :meth:`struphy.Output.with_time_units`. See + :func:`struphy.diagnostics.diagn_tools.power_spectrum_2d` for ``slice_at``, the fit options + and ``do_plot``. Returns ``(omega, kvec, spectrum, coeffs)``. + """ + from struphy.diagnostics.diagn_tools import power_spectrum_2d + + if self._array.t.attrs.get("units") == "s": + raise ValueError( + "the spectrum needs normalized time; take the field from out.with_time_units('normalized')" + ) + return power_spectrum_2d(self._array, component=component, slice_at=slice_at, physical=physical, **kwargs) diff --git a/postprocessing_external/src/struphy_plots/analysis.py b/postprocessing_external/src/struphy_plots/analysis.py new file mode 100644 index 000000000..91b6c8b0b --- /dev/null +++ b/postprocessing_external/src/struphy_plots/analysis.py @@ -0,0 +1,193 @@ +"""Numerical diagnostics returning values and labeled arrays, without rendering.""" + +from dataclasses import dataclass + +import numpy as np +import xarray as xr + +from .arrays import validate_array + + +def _label(data): + return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" + + +@dataclass(frozen=True) +class GrowthFit: + """Configuration for an exponential growth-rate fit.""" + + window: tuple[float | None, float | None] = (None, None) + amplitude_from_quadratic: bool = False + + +@dataclass(frozen=True) +class FitResult: + rate: float + intercept: float + time: np.ndarray + fitted: np.ndarray + + +def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: + """Fit ``exp(rate*t + intercept)`` using only finite, positive samples.""" + validate_array(data, required_dims=("t",)) + if data.dims != ("t",): + raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") + fit = fit or GrowthFit() + time, values = np.asarray(data.t), np.asarray(data) + if len(time) < 2: + return None + lo = time[0] if fit.window[0] is None else fit.window[0] + hi = time[-1] if fit.window[1] is None else fit.window[1] + lo, hi = sorted((lo, hi)) + valid = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) + if np.count_nonzero(valid) < 2: + return None + selected_time = time[valid] + signal = np.log(np.sqrt(values[valid])) if fit.amplitude_from_quadratic else np.log(values[valid]) + rate, intercept = np.polyfit(selected_time, signal, 1) + scale = 2.0 if fit.amplitude_from_quadratic else 1.0 + fitted = np.exp(scale * (rate * selected_time + intercept)) + return FitResult(float(rate), float(intercept), selected_time, fitted) + + +def envelope(data: xr.DataArray) -> xr.DataArray: + """Local maxima of a time series: the interior samples not smaller than their neighbours.""" + validate_array(data, required_dims=("t",)) + if data.dims != ("t",): + raise ValueError(f"envelope input must have dims ('t',), got {data.dims}") + values = np.asarray(data) + peak = np.zeros(len(values), dtype=bool) + peak[1:-1] = (values[1:-1] > values[:-2]) & (values[1:-1] >= values[2:]) + return data.isel(t=np.flatnonzero(peak)) + + +def damping_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: + """Fit ``exp(rate*t + intercept)`` to the envelope of an oscillating time series. + + Use this for signals such as the field energy in Landau damping, where :func:`growth_rate` on + the raw series would fit the oscillation. ``fit.window`` restricts the peaks that are used. + The rate is negative for damping. + """ + return growth_rate(envelope(data), fit) + + +def norm(data: xr.DataArray, *, dims=None, squared: bool = False) -> xr.DataArray: + """L2 norm over ``dims`` (default: every dimension except ``t``), as a function of the rest.""" + validate_array(data) + dims = [dim for dim in data.dims if dim != "t"] if dims is None else list(dims) + total = (data**2).sum(dims) + out = total if squared else np.sqrt(total) + out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + label = _label(data) + out.attrs["label"] = f"squared norm of {label}".strip() if squared else f"norm of {label}".strip() + return out + + +def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: + """Signed deviation from an explicit reference or the first time sample.""" + validate_array(data, required_dims=("t",)) + reference = data.isel(t=0) if ref is None else ref + out = data - reference + out.attrs = dict(data.attrs) + out.attrs["label"] = f"{_label(data)} drift".strip() + return out + + +SPATIAL_DIMS = ("e1", "e2", "e3") +VELOCITY_DIMS = ("v1", "v2", "v3") + + +def _provenance(data: xr.DataArray) -> dict: + return {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + + +def _select_dims(data: xr.DataArray, dims, default) -> list[str]: + if dims is None: + selected = [dim for dim in default if dim in data.dims] + if not selected: + raise ValueError(f"{data.name!r} has none of the dimensions {default}; its dimensions are {data.dims}") + return selected + selected = [dims] if isinstance(dims, str) else list(dims) + missing = [dim for dim in selected if dim not in data.dims] + if missing: + raise ValueError(f"{data.name!r} has no dimensions {missing}; its dimensions are {data.dims}") + return selected + + +def spatial_average(data: xr.DataArray, *, dims=None) -> xr.DataArray: + """Mean over the logical space dimensions, e.g. a binned f(t, e1, v1) becomes f(t, v1). + + ``dims`` defaults to every one of ``e1``, ``e2``, ``e3`` that ``data`` has. The mean is + uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a + mapped domain it is not weighted by the Jacobian. Physical ``X``, ``Y``, ``Z`` coordinates + that depend on the averaged dimensions are dropped. + """ + validate_array(data) + averaged = _select_dims(data, dims, SPATIAL_DIMS) + out = data.mean(averaged, keep_attrs=True) + out.attrs["label"] = f"average of {_label(data)}".strip() + out.attrs.pop("long_name", None) + return out + + +def _bin_widths(data: xr.DataArray, dim: str) -> xr.DataArray: + coordinate = np.asarray(data.coords[dim]) if dim in data.coords else None + if coordinate is None or len(coordinate) < 2: + raise ValueError(f"dimension {dim!r} needs a coordinate with at least two bins") + return xr.DataArray(np.gradient(coordinate), dims=(dim,), coords={dim: data.coords[dim]}) + + +def velocity_moments(f: xr.DataArray, *, dims=None) -> xr.Dataset: + """Moments of a binned distribution function over its velocity dimensions. + + ``dims`` defaults to every one of ``v1``, ``v2``, ``v3`` that ``f`` has; the moments are + functions of the remaining dimensions, for example ``(t, e1)`` for an ``e1_v1`` product. + The integrals are sums over the bins, weighted by the bin widths. + + Returns a Dataset with + + * ``density``: the zeroth moment, :math:`\\int f\\,\\mathrm{d}v`. + * ``mean_``: the mean velocity :math:`u = \\int v f\\,\\mathrm{d}v / n` along each dimension. + * ``variance_``: :math:`\\int (v-u)^2 f\\,\\mathrm{d}v / n`. In normalized units this is the + temperature over the particle mass along that direction, :math:`T/m`. + + Where the density is not positive, the mean and variance are NaN. A ``delta_f`` product has + only the density, which is then the density perturbation, because its mean and variance are + not defined. The values keep the normalization of the run; see ``Output.to_si``. + """ + validate_array(f) + integrated = _select_dims(f, dims, VELOCITY_DIMS) + volume = 1.0 + for dim in integrated: + volume = volume * _bin_widths(f, dim) + density = (f * volume).sum(integrated) + + label = _label(f) + variables = {"density": (density, f"density of {label}")} + if f.name != "delta_f": + weight = density.where(density > 0) + for dim in integrated: + mean = (f * f[dim] * volume).sum(integrated) / weight + variance = (f * (f[dim] - mean) ** 2 * volume).sum(integrated) / weight + variables[f"mean_{dim}"] = (mean, f"mean {dim}") + variables[f"variance_{dim}"] = (variance, f"variance of {dim}") + + provenance = _provenance(f) + out = {} + for name, (values, description) in variables.items(): + values.attrs = {**provenance, "label": description.strip()} + out[name] = values.rename(name) + return xr.Dataset(out, attrs=provenance) + + +def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: + """Absolute relative deviation from an explicit reference or first sample.""" + validate_array(data, required_dims=("t",)) + reference = data.isel(t=0) if ref is None else ref + if np.any(np.asarray(reference) == 0): + raise ValueError("cannot take a relative error against a reference of zero") + out = abs(data - reference) / abs(reference) + out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} + out.attrs.update(label=f"relative error of {_label(data)}".strip(), units="") + return out.isel(t=slice(1, None)) if skip_first else out diff --git a/postprocessing_external/src/struphy_plots/arrays.py b/postprocessing_external/src/struphy_plots/arrays.py new file mode 100644 index 000000000..9fbf4fdb0 --- /dev/null +++ b/postprocessing_external/src/struphy_plots/arrays.py @@ -0,0 +1,78 @@ +"""Small xarray metadata helpers used by :mod:`struphy_plots`. + +They intentionally live here rather than in Struphy so the plotting package can +operate on labeled xarray data from any producer. +""" + +from __future__ import annotations + +import os +from collections.abc import Mapping, Sequence + +import numpy as np +import xarray as xr + +DIM_LABELS = { + "t": r"$t$", "e1": r"$\eta_1$", "e2": r"$\eta_2$", "e3": r"$\eta_3$", + "v1": r"$v_1$", "v2": r"$v_2$", "v3": r"$v_3$", "x": r"$x$", "y": r"$y$", + "z": r"$z$", "R": r"$R$", "Z": r"$Z$", "component": "component", + "marker": "marker", "quantity": "quantity", +} +SCALARS_EXCLUDE = ("time",) + + +def validate_array(data: xr.DataArray, *, required_dims: Sequence[str] = ()) -> xr.DataArray: + if not isinstance(data, xr.DataArray): + raise TypeError(f"expected xarray.DataArray, got {type(data).__name__}") + missing = tuple(dim for dim in required_dims if dim not in data.dims) + if missing: + raise ValueError(f"missing dimensions {missing}; available dimensions are {data.dims}") + return data + + +def axis_label(data: xr.DataArray, dim: str) -> str: + if dim not in data.dims: + raise KeyError(f"dimension {dim!r} not found in {data.dims}") + coord = data.coords.get(dim) + label = ("" if coord is None else coord.attrs.get("long_name", "")) or DIM_LABELS.get(dim, dim) + unit = "" if coord is None else coord.attrs.get("units", "") + return f"{label} [{unit}]" if unit else label + + +def value_label(data: xr.DataArray) -> str: + label = data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" + unit = data.attrs.get("units", "") or "a.u." + return f"{label} [{unit}]" if label else f"[{unit}]" + + +def scalar_names(scalars: xr.Dataset | Mapping, *, names=None, exclude=SCALARS_EXCLUDE) -> list[str]: + available = tuple(scalars.data_vars if isinstance(scalars, xr.Dataset) else scalars.keys()) + if names is not None: + missing = [name for name in names if name not in available] + if missing: + raise KeyError(f"no scalars {missing}, available: {available}") + return list(names) + return [name for name in available if name not in exclude] + + +def save_scalars(scalars: xr.Dataset | Mapping, path: str, *, names=None, exclude=SCALARS_EXCLUDE, fmt=None) -> str: + selected = scalar_names(scalars, names=names, exclude=exclude) + arrays = [scalars[name] for name in selected] + for array in arrays: + validate_array(array, required_dims=("t",)) + if array.dims != ("t",): + raise ValueError(f"scalar {array.name!r} must have only the 't' dimension, got {array.dims}") + if arrays: + arrays = xr.align(*arrays, join="exact") + time = np.asarray(arrays[0].coords["t"]) + values = np.column_stack([np.asarray(array) for array in arrays]) + else: + time, values = np.zeros(0), np.zeros((0, 0)) + fmt = (fmt or os.path.splitext(path)[1].lstrip(".") or "csv").lower() + if fmt == "npz": + np.savez(path, t=time, **{name: values[:, i] for i, name in enumerate(selected)}) + elif fmt == "csv": + np.savetxt(path, np.column_stack((time, values)), delimiter=",", header=",".join(("t", *selected)), comments="") + else: + raise ValueError(f"unknown format {fmt!r}, expected 'csv' or 'npz'") + return path diff --git a/postprocessing_external/src/struphy_plots/output_accessors.py b/postprocessing_external/src/struphy_plots/output_accessors.py new file mode 100644 index 000000000..406a2a7d2 --- /dev/null +++ b/postprocessing_external/src/struphy_plots/output_accessors.py @@ -0,0 +1,58 @@ +"""Optional plots that need a whole run. + +Plots and diagnostics of a single array live on the array, see +:class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`: +``out.em_fields.phi_log.struphy.plot.slice(...)``, or from a value returned by +``out.evaluate("em_fields/phi_log")``. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from . import accessors # noqa: F401 (registers array.struphy) + +if TYPE_CHECKING: + from struphy.post_processing.output import Output + + +class OutputPlots: + """Plots of a whole run, constructed as ``OutputPlots(out)``. + + They return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` + and ``.save(path)``, titled with the run's numerical parameters. Plots of one product are + methods of that product, e.g. ``out.kinetic_ions.orbits.struphy.plot.trajectories()``. + """ + + def __init__(self, output: "Output"): + self._output = output + + def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): + """Overview of the scalar time series in one axes. + + Parameters + ---------- + names: + Scalars to show; all by default. + relative_to: + Show every scalar divided by this one. + logy: + Logarithmic value axis. + """ + from .plotting import plot_scalars + + return plot_scalars( + self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label + ) + + def equilibrium(self, ax=None): + """Radial equilibrium profiles, from the geometry written at the start of the run.""" + from .plotting import plot_equilibrium_profile + + return plot_equilibrium_profile(self._output.path_out, ax=ax) + + def equilibrium_3d(self, *, scalars: str = "p0", cmap="viridis"): + """Create a PyVista equilibrium view; call ``.show()`` on the returned plotter.""" + from .plotting import show_equilibrium + + return show_equilibrium(self._output.path_out, scalars=scalars, cmap=cmap) diff --git a/postprocessing_external/src/struphy_plots/plotting.py b/postprocessing_external/src/struphy_plots/plotting.py new file mode 100644 index 000000000..b12556330 --- /dev/null +++ b/postprocessing_external/src/struphy_plots/plotting.py @@ -0,0 +1,768 @@ +"""Small, composable plotting functions for labeled Struphy output. + +They remain importable for plotting arbitrary labeled arrays. The optional xarray +accessor exposes them as ``array.struphy.plot.*``. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +from matplotlib.widgets import Slider + +from .analysis import ( + FitResult, + GrowthFit, + drift, + growth_rate, + relative_error, +) +from .arrays import ( + SCALARS_EXCLUDE, + axis_label, + save_scalars, + scalar_names, + validate_array, + value_label, +) + +logger = logging.getLogger("struphy") + +STRUPHY_STYLE = { + "figure.figsize": (8.0, 5.0), + "figure.dpi": 110, + "axes.grid": True, + "grid.alpha": 0.3, + "axes.titlesize": "medium", + "legend.frameon": False, + "image.cmap": "viridis", +} + +PLANES = { + "XY": ("X", "Y", "X", "Y"), + "XZ": ("X", "Z", "X", "Z"), + "YZ": ("Y", "Z", "Y", "Z"), + "RZ": ("R", "Z", "R", "Z"), +} + + +@dataclass(frozen=True) +class View: + """A reusable selection and rendering recipe for an N-dimensional product.""" + + x: str | None = None + y: str | None = None + sweep: str = "t" + select: dict[str, float] = field(default_factory=dict) + isel: dict[str, int] = field(default_factory=dict) + coordinates: Literal["logical", "physical"] = "logical" + plane: Literal["XY", "XZ", "YZ", "RZ"] = "XY" + + +@dataclass +class PlotResult: + """Already-rendered Matplotlib objects; saving never redraws them. + + As the last expression of a notebook cell it displays its figure once; there is no need + to write ``.fig``. + """ + + fig: object + ax: object + artists: list = field(default_factory=list) + fit_results: list[FitResult | None] = field(default_factory=list) + data: dict = field(default_factory=dict) + _shown: bool = field(default=False, init=False, repr=False, compare=False) + + def save(self, path, *, close=False, **kwargs): + kwargs.setdefault("bbox_inches", "tight") + self.fig.savefig(path, **kwargs) + if close: + plt.close(self.fig) + return str(path) + + def show(self): + plt.show() + self._shown = True + return self + + def __repr__(self): + return f"{type(self).__name__}(fig={self.fig!r})" + + def _ipython_display_(self): + if not self._shown: + _display_figure(self.fig) + + +def _detach_figure(fig): + """Take a figure out of pyplot under the inline backend, which would show it as a still image.""" + import matplotlib + + if "inline" in matplotlib.get_backend(): + plt.close(fig) + + +def _display_figure(fig): + """Display a figure as a notebook cell result, exactly once. + + The inline backend shows every open figure again at the end of the cell, so the displayed + figure is closed. Interactive backends (e.g. ipympl) already show the figure when it is + created, so nothing is displayed twice there either. + """ + import matplotlib + + if "inline" not in matplotlib.get_backend(): + return + from IPython.display import display + + display(fig) + plt.close(fig) + + +def _label(data): + return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" + + +def _items(data): + return [data] if isinstance(data, (xr.DataArray, xr.Dataset)) else list(data) + + +def shared_run_label(data, default="") -> str: + """The run description shared by all arrays (``attrs["run"]``), or ``default``. + + Arrays loaded from a :class:`~struphy.Output` carry it; arrays from different runs share none. + """ + runs = {item.attrs.get("run") for item in _items(data)} + if len(runs - {None, ""}) > 1: + return "" + runs.discard(None) + runs.discard("") + return runs.pop() if runs else default + + +def _finish(fig, *, run_label="", tight=True): + if run_label: + fig.suptitle(run_label, fontsize="small") + if tight: + fig.tight_layout() + + +def _select(data: xr.DataArray, view: View, *, keep_sweep=True): + validate_array(data) + overlap = set(view.select) & set(view.isel) + if overlap: + raise ValueError(f"dimensions cannot appear in both select and isel: {sorted(overlap)}") + selected = data + if view.select: + selected = selected.sel(view.select, method="nearest") + if view.isel: + selected = selected.isel(view.isel) + if not keep_sweep and view.sweep in selected.dims: + selected = selected.isel({view.sweep: 0}) + return selected + + +def logical_grids(data: xr.DataArray, *, x=None, y=None): + """Return 2-D logical coordinate grids and their labels.""" + if x is None or y is None: + if data.ndim != 2: + raise ValueError(f"x and y are required unless data is two-dimensional; got {data.dims}") + x, y = data.dims + if set(data.dims) != {x, y}: + raise ValueError(f"selected data must contain exactly {x!r} and {y!r}; got {data.dims}") + xgrid, ygrid = np.meshgrid(np.asarray(data.coords[x]), np.asarray(data.coords[y]), indexing="ij") + return xgrid, ygrid, axis_label(data, x), axis_label(data, y) + + +def physical_grids(data: xr.DataArray, *, plane="XY"): + """Return physical auxiliary coordinates already attached to a selected field.""" + if plane not in PLANES: + raise ValueError(f"unknown plane {plane!r}; expected one of {tuple(PLANES)}") + xname, yname, xlabel, ylabel = PLANES[plane] + missing = [name for name in ("X", "Y", "Z") if name not in data.coords] + if missing: + raise ValueError(f"physical coordinates are not attached to {data.name!r}: missing {missing}") + xcoord = np.sqrt(data.X**2 + data.Y**2) if xname == "R" else data.coords[xname] + ycoord = data.coords[yname] + if xcoord.ndim != 2 or ycoord.ndim != 2: + raise ValueError("select all but two spatial dimensions before requesting a physical grid") + return np.asarray(xcoord), np.asarray(ycoord), xlabel, ylabel + + +def _slice_data(data, view): + selected = _select(data, view) + if view.sweep in selected.dims and view.sweep not in (view.x, view.y): + raise ValueError(f"select one {view.sweep!r} value before drawing a static slice, or display it as x or y") + if view.x is None or view.y is None: + if selected.ndim != 2: + raise ValueError(f"view.x and view.y are required for remaining dims {selected.dims}") + x, y = selected.dims + else: + x, y = view.x, view.y + if set(selected.dims) != {x, y}: + raise ValueError(f"selection leaves dimensions {selected.dims}; expected only {x!r}, {y!r}") + selected = selected.transpose(x, y) + grids = ( + physical_grids(selected, plane=view.plane) + if view.coordinates == "physical" + else logical_grids(selected, x=x, y=y) + ) + return selected, grids + + +def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=None): + """Plot one or more time series, each on its own time grid; series of different runs are labeled by run.""" + series = _items(data) + if not series: + raise ValueError("at least one time series is required") + for item in series: + validate_array(item, required_dims=("t",)) + if item.dims != ("t",): + raise ValueError(f"time series must have dims ('t',), got {item.dims}") + label_of = _label + if len({item.attrs.get("run_name") for item in series}) > 1: + + def label_of(item): + return " ".join( + filter(None, (_label(item), f"({item.attrs['run_name']})" if item.attrs.get("run_name") else "")) + ) + + run_label = shared_run_label(series) if run_label is None else run_label + own_figure = ax is None + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + artists, fits = [], [] + for item in series: + (line,) = ax.plot(item.t, item, label=label_of(item) or None) + artists.append(line) + result = growth_rate(item, fit) if fit is not None else None + fits.append(result) + if result is not None: + (fitted,) = ax.plot( + result.time, + result.fitted, + "--", + color=line.get_color(), + label=rf"fit: $\gamma$ = {result.rate:.4e}", + ) + ax.axvspan(result.time[0], result.time[-1], alpha=0.12, color="grey") + artists.append(fitted) + if logy: + ax.set_yscale("log") + ax.set_xlabel(axis_label(series[0], "t")) + ax.set_ylabel(value_label(series[0])) + ax.set_title(title if title is not None else _label(series[0])) + if any(label_of(item) for item in series) or fit is not None: + ax.legend() + _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) + return PlotResult(fig, ax, artists, fits) + + +def plot_lineout(data: xr.DataArray, *, x: str | None = None, ax=None, title=None): + """Plot a selected one-dimensional profile using one named coordinate.""" + validate_array(data) + if data.ndim != 1: + raise ValueError(f"lineout needs exactly one remaining dimension, got {data.dims}") + x = data.dims[0] if x is None else x + if x != data.dims[0]: + raise ValueError(f"lineout coordinate {x!r} is not the remaining dimension {data.dims[0]!r}") + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + (line,) = ax.plot(data[x], data) + ax.set(xlabel=axis_label(data, x), ylabel=value_label(data), title=_label(data) if title is None else title) + _finish(fig, run_label=shared_run_label(data) if line.axes.figure is fig else "") + return PlotResult(fig, ax, [line]) + + +def plot_vector( + data: xr.DataArray, *, x: str, y: str, components: tuple[int, int] = (0, 1), component_dim: str = "component", ax=None, + stride: int = 1, coordinates: Literal["logical", "physical"] = "logical", +): + """Render two components of a selected vector field with Matplotlib quivers.""" + validate_array(data, required_dims=(component_dim, x, y)) + if set(data.dims) != {component_dim, x, y}: + raise ValueError(f"select every dimension except {component_dim!r}, {x!r}, and {y!r}; got {data.dims}") + if stride < 1: + raise ValueError("stride must be positive") + vector = data.transpose(component_dim, x, y).isel({component_dim: list(components), x: slice(None, None, stride), y: slice(None, None, stride)}) + if coordinates == "physical": + planes = {frozenset(("e1", "e2")): "XY", frozenset(("e1", "e3")): "XZ", frozenset(("e2", "e3")): "YZ"} + plane = planes.get(frozenset((x, y))) + if plane is None: + raise ValueError("physical vector plots require two logical spatial dimensions") + xg, yg, xlabel, ylabel = physical_grids(vector.isel({component_dim: 0}), plane=plane) + else: + xg, yg, xlabel, ylabel = logical_grids(vector.isel({component_dim: 0}), x=x, y=y) + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + quiver = ax.quiver(xg, yg, vector.isel({component_dim: 0}), vector.isel({component_dim: 1})) + ax.set(xlabel=xlabel, ylabel=ylabel, title=_label(data), aspect="equal" if coordinates == "physical" else "auto") + _finish(fig, run_label=shared_run_label(data)) + return PlotResult(fig, ax, [quiver]) + + +def plot_volume_slices(data: xr.DataArray, *, indices: dict[str, int] | None = None, cmap=None): + """Show three orthogonal midpoint slices of a selected scalar volume.""" + validate_array(data, required_dims=("e1", "e2", "e3")) + if set(data.dims) != {"e1", "e2", "e3"}: + raise ValueError(f"select every non-spatial dimension before volume_slices(); got {data.dims}") + indices = {dim: data.sizes[dim] // 2 for dim in data.dims} | (indices or {}) + fig, axes = plt.subplots(1, 3, figsize=(12, 3.6), layout="constrained") + artists = [] + for ax, normal, x, y in zip(axes, ("e3", "e2", "e1"), ("e1", "e1", "e2"), ("e2", "e3", "e3")): + plane = data.isel({normal: indices[normal]}).transpose(x, y) + mesh = ax.pcolormesh(plane[x], plane[y], np.asarray(plane).T, shading="auto", cmap=cmap) + ax.set(xlabel=axis_label(plane, x), ylabel=axis_label(plane, y), title=f"{normal} index {indices[normal]}") + fig.colorbar(mesh, ax=ax, label=value_label(data)) + artists.append(mesh) + fig.suptitle(" — ".join(filter(None, (_label(data), shared_run_label(data)))) ) + return PlotResult(fig, axes, artists) + + +def plot_compare(first: xr.DataArray, second: xr.DataArray, *, mode: Literal["difference", "ratio"] = "difference", ax=None): + """Plot a one-dimensional aligned difference or ratio of two arrays.""" + first, second = xr.align(first, second, join="inner") + result = first - second if mode == "difference" else xr.where(second != 0, first / second, np.nan) + result.name = f"{_label(first)} {mode}" + return plot_lineout(result, ax=ax) + + +def pyvista_volume(data: xr.DataArray, *, name: str | None = None, cmap="viridis", opacity="linear"): + """Create a PyVista volume view from a selected scalar field with ``X/Y/Z`` coordinates. + + The returned plotter is not shown automatically; call ``plotter.show()`` in an + interactive session or use PyVista's off-screen rendering options in batch jobs. + """ + import pyvista as pv + + validate_array(data, required_dims=("e1", "e2", "e3")) + if set(data.dims) != {"e1", "e2", "e3"}: + raise ValueError(f"select every non-spatial dimension before pyvista_volume(); got {data.dims}") + if any(coord not in data.coords for coord in ("X", "Y", "Z")): + raise ValueError("pyvista_volume() requires mapped X, Y, and Z coordinates") + grid = pv.StructuredGrid( + np.asarray(data.X, dtype=float), np.asarray(data.Y, dtype=float), np.asarray(data.Z, dtype=float) + ) + name = name or _label(data) or "value" + grid.point_data[name] = np.asarray(data).ravel(order="F") + plotter = pv.Plotter() + plotter.add_volume(grid, scalars=name, cmap=cmap, opacity=opacity) + plotter.show_axes() + return plotter + + +def show_equilibrium(path_out, *, scalars: str = "p0", cmap="viridis"): + """Create a PyVista view of ``geometry.vts`` and its equilibrium scalar field.""" + import pyvista as pv + + grid = pv.read(str(Path(path_out) / "geometry.vts")) + if scalars not in grid.point_data: + raise KeyError(f"{scalars!r} is not available; choices: {tuple(grid.point_data)}") + plotter = pv.Plotter() + plotter.add_mesh(grid, scalars=scalars, cmap=cmap, show_edges=False) + plotter.show_axes() + return plotter + + +class _SliceRenderer: + """Shared selection, color limits and mesh rendering for every slice presentation.""" + + def __init__(self, data, view, *, vmin=None, vmax=None, shared_clim=True, cmap=None, equal_aspect=None, title=None): + self.data = _select(data, view) + self.view = View(x=view.x, y=view.y, sweep=view.sweep, coordinates=view.coordinates, plane=view.plane) + self.vmin, self.vmax = vmin, vmax + self.shared_clim = shared_clim + self.cmap = cmap or STRUPHY_STYLE["image.cmap"] + self.equal_aspect = view.coordinates == "physical" if equal_aspect is None else equal_aspect + self.title = _label(data) if title is None else title + self.limits = self._limits(self.data) if shared_clim else None + + def _limits(self, data): + if self.vmin is not None and self.vmax is not None: + return self.vmin, self.vmax + values = np.asarray(data) + finite = values[np.isfinite(values)] + if not finite.size: + raise ValueError("cannot determine color limits from data without finite values; provide vmin and vmax") + return ( + float(finite.min()) if self.vmin is None else self.vmin, + float(finite.max()) if self.vmax is None else self.vmax, + ) + + def draw(self, ax, data): + values, (xg, yg, xlabel, ylabel) = _slice_data(data, self.view) + lo, hi = self.limits if self.shared_clim else self._limits(values) + mesh = ax.pcolormesh(xg, yg, values, shading="auto", vmin=lo, vmax=hi, cmap=self.cmap) + ax.set(xlabel=xlabel, ylabel=ylabel, aspect="equal" if self.equal_aspect else "auto") + ax.grid(False) + return mesh + + def frame_title(self, index): + return f"{self.title} at {self.view.sweep} = {float(self.data[self.view.sweep][index]):.3e}" + + def indices(self, step): + if not isinstance(step, (int, np.integer)) or step < 1: + raise ValueError("step must be a positive integer") + validate_array(self.data, required_dims=(self.view.sweep,)) + if not self.data.sizes[self.view.sweep]: + raise ValueError("cannot render an empty sweep") + return range(0, self.data.sizes[self.view.sweep], step) + + +def plot_slice( + data: xr.DataArray, + *, + view=None, + ax=None, + vmin=None, + vmax=None, + equal_aspect=None, + title=None, + run_label=None, + cmap=None, + shared_clim=True, +): + """Render one selected two-dimensional slice.""" + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + shared_clim=shared_clim, + ) + run_label = shared_run_label(data) if run_label is None else run_label + own_figure = ax is None + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + mesh = renderer.draw(ax, renderer.data) + fig.colorbar(mesh, ax=ax, label=value_label(data)) + ax.set_title(renderer.title) + _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) + return PlotResult(fig, ax, [mesh]) + + +def plot_panels( + data: xr.DataArray, + *, + view=None, + nrows=3, + ncols=4, + shared_clim=True, + title=None, + run_label=None, + vmin=None, + vmax=None, + cmap=None, + equal_aspect=None, +): + """Plot snapshots with common color limits over the entire selected sweep by default.""" + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + ) + renderer.indices(1) + if nrows < 1 or ncols < 1: + raise ValueError("nrows and ncols must be positive") + sweep = renderer.view.sweep + indices = np.linspace(0, renderer.data.sizes[sweep] - 1, nrows * ncols).astype(int) + run_label = shared_run_label(data) if run_label is None else run_label + with plt.rc_context(STRUPHY_STYLE): + fig, axes = plt.subplots( + nrows, + ncols, + figsize=(3.5 * ncols, 2.8 * nrows), + sharex=True, + sharey=True, + squeeze=False, + layout="constrained", + ) + meshes = [] + for ax, index in zip(axes.ravel(), indices): + mesh = renderer.draw(ax, renderer.data.isel({sweep: int(index)})) + meshes.append(mesh) + ax.set_title(f"{sweep} = {float(renderer.data[sweep][index]):.3e}") + if not shared_clim: + fig.colorbar(mesh, ax=ax, label=value_label(data)) + if shared_clim: + fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) + fig.suptitle(" — ".join(filter(None, (renderer.title, run_label)))) + return PlotResult(fig, axes, meshes) + + +class InteractiveSliceViewer: + """Slider view with the same rendering options as static and exported slices.""" + + def __init__( + self, + data: xr.DataArray, + *, + view=None, + vmin=None, + vmax=None, + run_label=None, + shared_clim=True, + cmap=None, + equal_aspect=None, + title=None, + ): + self.data = validate_array(data) + self.view = view or View() + self.options = dict( + vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title + ) + self.run_label = shared_run_label(data) if run_label is None else run_label + self.result = None + self.sliders = {} + + def show(self): + (self.result or self.draw()).show() + return self + + def _ipython_display_(self): + (self.result or self.draw())._ipython_display_() + + def draw(self): + if self.result is not None: + return self.result + renderer = _SliceRenderer(self.data, self.view, **self.options) + base = renderer.data + x, y = self.view.x, self.view.y + if x is None or y is None: + candidates = [dim for dim in base.dims if dim != self.view.sweep] + if len(candidates) < 2: + raise ValueError("viewer needs two display dimensions") + x, y = candidates[:2] + renderer.view = View(x=x, y=y, coordinates=self.view.coordinates, plane=self.view.plane) + controls = [dim for dim in base.dims if dim not in {x, y}] + indices = {dim: 0 for dim in controls} + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() + fig.subplots_adjust(bottom=0.13 + 0.05 * len(controls)) + mesh = renderer.draw(ax, base.isel(indices)) + colorbar = fig.colorbar(mesh, ax=ax, label=value_label(self.data)) + self.result = PlotResult(fig, ax, [mesh]) + + def update(_=None): + for dim, slider in self.sliders.items(): + indices[dim] = int(slider.val) + self.result.artists[0].remove() + mesh = renderer.draw(ax, base.isel(indices)) + self.result.artists[:] = [mesh] + colorbar.update_normal(mesh) + values = ", ".join(f"{dim}={float(base[dim][index]):.3e}" for dim, index in indices.items()) + ax.set_title(" at ".join(filter(None, (renderer.title, values)))) + fig.canvas.draw_idle() + + for row, dim in enumerate(controls): + if base.sizes[dim] == 1: + continue + slider_ax = fig.add_axes([0.20, 0.05 + 0.05 * row, 0.60, 0.025]) + slider = Slider(slider_ax, dim, 0, base.sizes[dim] - 1, valstep=1) + slider.on_changed(update) + self.sliders[dim] = slider + update() + _finish(fig, run_label=self.run_label, tight=False) + # Keep widget callbacks alive even if only the PlotResult is retained. + self.result.data["viewer"] = self + return self.result + + +def animate_slices( + data: xr.DataArray, + *, + view=None, + interval=100, + step=1, + vmin=None, + vmax=None, + shared_clim=True, + cmap=None, + equal_aspect=None, + title=None, +): + """Animate slices with fixed color limits over the selected sweep by default.""" + from matplotlib.animation import FuncAnimation + + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + ) + frames = renderer.indices(step) + sweep = renderer.view.sweep + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() + mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) + colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) + _finish(fig, run_label=shared_run_label(data)) + + def update(index): + nonlocal mesh + mesh.remove() + mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) + colorbar.update_normal(mesh) + ax.set_title(renderer.frame_title(index)) + return (mesh,) + + animation = FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) + _detach_figure(fig) + return animation + + +def save_frames( + data: xr.DataArray, + directory, + *, + view=None, + step=1, + prefix="frame", + dpi=110, + vmin=None, + vmax=None, + shared_clim=True, + cmap=None, + equal_aspect=None, + title=None, +): + """Export the configured sweep as PNGs, sharing color limits by default.""" + renderer = _SliceRenderer( + data, + view or View(), + vmin=vmin, + vmax=vmax, + shared_clim=shared_clim, + cmap=cmap, + equal_aspect=equal_aspect, + title=title, + ) + frames = renderer.indices(step) + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + paths = [] + with plt.rc_context(STRUPHY_STYLE): + fig, ax = plt.subplots() + try: + sweep = renderer.view.sweep + mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) + colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) + _finish(fig, run_label=shared_run_label(data)) + for frame, index in enumerate(frames): + mesh.remove() + mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) + colorbar.update_normal(mesh) + ax.set_title(renderer.frame_title(index)) + path = directory / f"{prefix}_{frame:04d}.png" + fig.savefig(path, dpi=dpi, bbox_inches="tight") + paths.append(str(path)) + finally: + plt.close(fig) + return paths + + +def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, logy=False, run_label=None): + """Plot every scalar time series in one axes.""" + selected = scalar_names(scalars, names=names, exclude=exclude) + if not selected: + raise ValueError("no scalars to plot") + run_label = shared_run_label([scalars[name] for name in selected]) if run_label is None else run_label + fig, ax = plt.subplots(layout="constrained") + for name in selected: + values = scalars[name] / scalars[relative_to] if relative_to else scalars[name] + ax.plot(values.t, values, label=name) + if logy: + ax.set_yscale("log") + units = {scalars[name].attrs.get("units", "") for name in selected} + ylabel = f"quantity / {relative_to}" if relative_to else (f"[{units.pop()}]" if len(units) == 1 else "[a.u.]") + ax.set(xlabel=axis_label(scalars[selected[0]], "t"), ylabel=ylabel, title="Scalars") + ax.legend(fontsize="small") + if run_label: + fig.suptitle(run_label, fontsize="small") + return PlotResult(fig, ax, list(ax.lines)) + + +def save_all_scalars( + scalars, + directory, + *, + names=None, + exclude=SCALARS_EXCLUDE, + logy=False, + run_label=None, + table="csv", + file_format="png", + dpi=110, +): + """Write a table, scalar overview and one figure per scalar.""" + selected = scalar_names(scalars, names=names, exclude=exclude) + if not selected: + return [] + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + paths = [] + if table: + paths.append(save_scalars(scalars, str(directory / f"scalars.{table}"), names=selected, fmt=table)) + overview = plot_scalars(scalars, names=selected, logy=logy, run_label=run_label) + path = directory / f"scalars.{file_format}" + overview.save(path, dpi=dpi, close=True) + paths.append(str(path)) + for name in selected: + result = plot_timeseries(scalars[name], logy=logy, title=name, run_label=run_label) + path = directory / f"{name}.{file_format}" + result.save(path, dpi=dpi, close=True) + paths.append(str(path)) + return paths + + +def plot_marker_trajectories(orbits: xr.DataArray, *, ax=None, max_markers=200, show_paths=None): + """Plot a static 3-D trajectory overview; interactive marker UI is intentionally separate.""" + validate_array(orbits, required_dims=("t", "marker", "quantity")) + count = min(orbits.sizes["marker"], max_markers) + positions = np.asarray(orbits.isel(marker=slice(0, count)).sel(quantity=["x", "y", "z"])) + fig = plt.figure() if ax is None else ax.figure + ax = fig.add_subplot(111, projection="3d") if ax is None else ax + show_paths = count <= 200 if show_paths is None else show_paths + artists = [] + if show_paths: + for marker in range(count): + artists.extend(ax.plot(*positions[:, marker].T, lw=0.8, alpha=0.5)) + artists.append(ax.scatter(*positions[-1].T, s=8)) + ax.set(xlabel="X", ylabel="Y", zlabel="Z", title="Marker trajectories") + return PlotResult(fig, ax, artists) + + +def plot_equilibrium_profile(path_out, *, ax=None): + """Plot radial equilibrium profiles from ``geometry.vts``.""" + import pyvista as pv + + equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) + shape = equilibrium.dimensions + grid = np.reshape(equilibrium.points, shape + (3,)) + radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2) + pressure = np.reshape(equilibrium.point_data["p0"], shape) + fig, ax = plt.subplots() if ax is None else (ax.figure, ax) + ax.plot(radius[0, 0], pressure[0, 0], label=r"$p_0$") + if "n0" in equilibrium.point_data: + density = np.reshape(equilibrium.point_data["n0"], shape) + ax.plot(radius[0, 0], density[0, 0], label=r"$n_0$") + ax.plot(radius[0, 0], pressure[0, 0] / density[0, 0], label=r"$T_0$") + ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") + ax.legend() + return PlotResult(fig, ax, list(ax.lines)) diff --git a/postprocessing_external/tests/test_analysis_and_core_output.py b/postprocessing_external/tests/test_analysis_and_core_output.py new file mode 100644 index 000000000..8c4ad0314 --- /dev/null +++ b/postprocessing_external/tests/test_analysis_and_core_output.py @@ -0,0 +1,302 @@ +"""Tests for distribution reductions, SI conversion and profiling access.""" + +import os +import time + +import numpy as np +import pytest +import xarray as xr + +from struphy_plots.analysis import spatial_average, velocity_moments +from struphy.post_processing.arrays import data_array +from struphy.post_processing.output import Output +from struphy.post_processing.tests.test_output import write_tree + +F = "kinetic_ions/f" + + +def gaussian(v, density, mean, variance): + return density * np.exp(-((v - mean) ** 2) / (2 * variance)) / np.sqrt(2 * np.pi * variance) + + +def binned(values, dims, coords, name="f"): + return data_array(values, dims, coords, name=name, label="$f$") + + +@pytest.fixture +def run(tmp_path): + return Output(write_tree(str(tmp_path))) + + +# --- reductions --------------------------------------------------------------------------------- + + +def test_moments_of_a_maxwellian_recover_its_parameters(): + v = np.linspace(-8, 8, 321) + density = np.array([1.0, 2.0])[:, None, None] # depends on t + mean = np.array([-0.5, 0.0, 0.5])[None, :, None] # depends on e1 + f = binned( + gaussian(v[None, None, :], density, mean, 0.64), + ("t", "e1", "v1"), + {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": v}, + ) + moments = velocity_moments(f) + assert moments.density.dims == ("t", "e1") + np.testing.assert_allclose(moments.density, np.broadcast_to(density[:, :, 0], (2, 3)), rtol=1e-8) + np.testing.assert_allclose(moments.mean_v1, np.broadcast_to(mean[:, :, 0], (2, 3)), atol=1e-8) + np.testing.assert_allclose(moments.variance_v1, 0.64, rtol=1e-8) + + +def test_moments_over_two_velocity_dimensions_are_taken_per_direction(): + v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) + f = binned( + (gaussian(v1[:, None], 1.0, 1.0, 0.5) * gaussian(v2[None, :], 3.0, -0.5, 0.25))[None], + ("t", "v1", "v2"), + {"t": [0.0], "v1": v1, "v2": v2}, + ) + moments = velocity_moments(f) + assert set(moments.data_vars) == {"density", "mean_v1", "variance_v1", "mean_v2", "variance_v2"} + np.testing.assert_allclose(moments.density, 3.0, rtol=1e-8) + np.testing.assert_allclose(moments.mean_v1, 1.0, atol=1e-8) + np.testing.assert_allclose(moments.variance_v1, 0.5, rtol=1e-8) + np.testing.assert_allclose(moments.mean_v2, -0.5, atol=1e-8) + np.testing.assert_allclose(moments.variance_v2, 0.25, rtol=1e-8) + + +def test_one_velocity_dimension_can_be_selected(): + v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) + f = binned(np.ones((1, 181, 121)), ("t", "v1", "v2"), {"t": [0.0], "v1": v1, "v2": v2}) + moments = velocity_moments(f, dims="v2") + assert moments.density.dims == ("t", "v1") + assert "mean_v1" not in moments + + +def test_delta_f_has_only_a_density(): + v = np.linspace(-3, 3, 7) + delta_f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}, name="delta_f") + assert tuple(velocity_moments(delta_f).data_vars) == ("density",) + + +def test_mean_and_variance_are_nan_without_particles(): + v = np.linspace(-3, 3, 7) + f = binned(np.zeros((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}) + moments = velocity_moments(f) + assert moments.density.item() == 0.0 + assert np.isnan(moments.mean_v1.item()) and np.isnan(moments.variance_v1.item()) + + +def test_moments_carry_the_run_and_a_label(): + f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": np.linspace(-3, 3, 7)}) + f.attrs.update(run="dt=0.1", run_name="sim_1") + moments = velocity_moments(f) + assert moments.attrs["run_name"] == "sim_1" + assert moments.mean_v1.attrs["run"] == "dt=0.1" + assert moments.density.attrs["label"] == "density of $f$" + + +def test_moments_reject_missing_velocity_dimensions_and_single_bins(): + no_velocity = binned(np.ones((2, 3)), ("t", "e1"), {"t": [0.0, 1.0], "e1": [0.1, 0.2, 0.3]}) + with pytest.raises(ValueError, match="none of the dimensions"): + velocity_moments(no_velocity) + with pytest.raises(ValueError, match="no dimensions"): + velocity_moments(no_velocity, dims="v1") + one_bin = binned(np.ones((1, 1)), ("t", "v1"), {"t": [0.0], "v1": [0.0]}) + with pytest.raises(ValueError, match="at least two bins"): + velocity_moments(one_bin) + + +def test_spatial_average_removes_the_space_dimensions_only(): + values = np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4) + f = binned(values, ("t", "e1", "v1"), {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": np.arange(4.0)}) + f.attrs["run_name"] = "sim_1" + mean = spatial_average(f) + assert mean.dims == ("t", "v1") + np.testing.assert_allclose(mean, values.mean(axis=1)) + assert mean.attrs["run_name"] == "sim_1" + assert mean.attrs["label"] == "average of $f$" + assert spatial_average(f, dims="e1").dims == ("t", "v1") + + +def test_spatial_average_drops_physical_coordinates_it_averaged_over(): + logical = {f"e{i + 1}": np.linspace(0, 1, n) for i, n in enumerate((3, 4, 1))} + mapped = np.meshgrid(*logical.values(), indexing="ij") + field = data_array( + np.ones((2, 3, 4, 1)), + ("t", "e1", "e2", "e3"), + {"t": [0.0, 1.0], **logical, "X": (("e1", "e2", "e3"), mapped[0])}, + name="E", + ) + mean = spatial_average(field) + assert mean.dims == ("t",) + assert "X" not in mean.coords + + +def test_spatial_average_needs_space_dimensions(): + series = data_array(np.ones(3), ("t",), {"t": [0.0, 1.0, 2.0]}, name="energy") + with pytest.raises(ValueError, match="none of the dimensions"): + spatial_average(series) + + +def test_reductions_are_available_from_external_helpers_and_the_accessor(run): + product = run.evaluate(F) + moments = velocity_moments(product) + # write_tree has f = 1 on v = -3..3 in unit bins: n = 7, u = 0 and = 4 + np.testing.assert_allclose(moments.density, 7.0) + np.testing.assert_allclose(moments.mean_v1, 0.0, atol=1e-12) + np.testing.assert_allclose(moments.variance_v1, 4.0) + assert moments.density.attrs["run_name"] == run.path_out.name + + average = spatial_average(product) + assert average.dims == ("t", "v1") + xr.testing.assert_identical(average, product.struphy.analysis.spatial_average()) + xr.testing.assert_identical(moments, product.struphy.analysis.velocity_moments()) + + +# --- SI units --------------------------------------------------------------------------------- + + +def test_coordinates_are_converted_and_values_left_alone(run): + units = run.units + f = run.to_si(F) + np.testing.assert_allclose(f.v1, run.evaluate(F).v1 * units.v) + assert f.v1.attrs["units"] == "m/s" + np.testing.assert_allclose(f.t, run.evaluate(F).t * units.t) + assert f.t.attrs["units"] == "s" + assert "t_seconds" not in f.coords + np.testing.assert_array_equal(f.e1, run.evaluate(F).e1) # logical coordinates are dimensionless + np.testing.assert_array_equal(f, run.evaluate(F)) + assert "units" not in f.attrs + assert run.evaluate(F).v1.attrs.get("units") is None # the run's own product is untouched + assert "t_seconds" in run.evaluate(F).coords + + +def test_mapped_coordinates_are_scaled_by_the_length_unit(run): + assert run.units.x == 2.0 + field = run.to_si("em_fields/E") + np.testing.assert_allclose(field.X, run.evaluate("em_fields/E").X * 2.0) + assert field.X.attrs["units"] == "m" + + +def test_values_are_converted_with_a_named_unit(run): + field = run.to_si("em_fields/E", "B") + np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.B) + assert field.attrs["units"] == "T" + assert field.name == "E" + assert field.attrs["run_name"] == run.path_out.name + + +def test_values_are_converted_with_a_composite_unit(run): + field = run.to_si("em_fields/E", run.units.v * run.units.B, label="V/m") + np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.v * run.units.B) + assert field.attrs["units"] == "V/m" + + +def test_conversion_is_idempotent_for_coordinates_and_refuses_values_twice(run): + once = run.to_si(F) + np.testing.assert_array_equal(run.to_si(once).v1, once.v1) + converted = run.to_si("em_fields/E", "B") + with pytest.raises(ValueError, match="already has units"): + run.to_si(converted, "B") + + +def test_unknown_units_are_rejected(run): + with pytest.raises(ValueError, match="unknown unit"): + run.to_si("em_fields/E", "furlong") + + +def test_physical_time_units_are_not_converted_twice(tmp_path): + physical = Output(write_tree(str(tmp_path))).with_time_units("physical") + np.testing.assert_allclose(physical.to_si(F).t, physical.evaluate(F).t) + + +# --- profiling -------------------------------------------------------------------------------- + + +def write_profile(path_out, *, calls=3, setup=True): + from scope_profiler import ProfileManager, ProfilingOptions + + with ProfileManager.session( + options=ProfilingOptions(), + deactivate_profiling=False, + file_path=os.path.join(path_out, "profiling_data.h5"), + ): + if setup: + with ProfileManager.profile_region("setup: total"): + time.sleep(0.001) + for _ in range(calls): + with ProfileManager.profile_region("prop: A"): + with ProfileManager.profile_region("kernel: k"): + time.sleep(0.01) + + +@pytest.fixture +def profiled(tmp_path, capfd): + write_profile(write_tree(str(tmp_path))) + return Output(str(tmp_path)) + + +def test_a_run_without_profiling_says_how_to_enable_it(run): + with pytest.raises(FileNotFoundError, match="profiling_activated=True"): + run.profile + + +def test_summary_lists_every_region_with_times(profiled): + summary = profiled.profile.summary() + assert summary.sizes == {"region": 4} + assert list(summary.region.values[:1]) == ["scope_profiler.session"] + assert summary.region.values[-1] == "setup: total" + assert summary.calls.sel(region="prop: A").item() == 3 + assert summary.calls.sel(region="setup: total").item() == 1 + assert summary.total_time.sel(region="kernel: k").item() >= 0.03 + assert summary.mean_time.sel(region="kernel: k").item() >= 0.01 + assert summary.fraction.sel(region="scope_profiler.session").item() == pytest.approx(1.0) + assert 0 < summary.fraction.sel(region="prop: A").item() <= 1.0 + assert summary.attrs["run"] == profiled.label + assert summary.attrs["num_ranks"] == 1 + assert summary.total_time.attrs["units"] == "s" + + +def test_summary_filters_and_sorts(profiled): + profile = profiled.profile + assert list(profile.summary(prefix="kernel:").region.values) == ["kernel: k"] + assert len(profile.summary(top=2).region) == 2 + by_calls = profile.summary(sort_by="calls") + assert by_calls.region.values[0] == "prop: A" + with pytest.raises(ValueError, match="cannot sort by"): + profile.summary(sort_by="size") + + +def test_the_profile_is_cached_and_reset_with_the_output(profiled): + assert profiled.profile is profiled.profile + first = profiled.profile + profiled.clear_cache() + assert profiled.profile is not first + + +def test_table_is_readable_text(profiled): + table = profiled.profile.table(top=3) + lines = table.splitlines() + assert lines[0].startswith("Profile: ") and "1 rank(s)" in lines[0] + assert "Total [s]" in lines[2] + assert len(lines) == 4 + 3 + assert "scope_profiler.session" in table and "100.0%" in table + + +def test_runs_are_compared_side_by_side(tmp_path, capfd): + first = Output(write_tree(str(tmp_path / "a"))) + second = Output(write_tree(str(tmp_path / "b"))) + write_profile(first.path_out, calls=3) + write_profile(second.path_out, calls=2, setup=False) + + table = first.profile.compare(second, metric="calls") + assert set(table.dims) == {"region", "run"} + assert list(table.run.values) == [f"{first.label} [a]", f"{second.label} [b]"] # identical labels are told apart + assert table.sel(run=table.run.values[0], region="prop: A").item() == 3 + assert table.sel(run=table.run.values[1], region="prop: A").item() == 2 + assert np.isnan(table.sel(run=table.run.values[1], region="setup: total").item()) + assert table.name == "calls" + assert first.profile.compare(second, prefix="kernel:").region.values.tolist() == ["kernel: k"] + assert first.profile.compare(second.profile).shape == table.shape # a Profile works as well as an Output + + with pytest.raises(ValueError, match="cannot compare"): + first.profile.compare(second, metric="size") diff --git a/postprocessing_external/tests/test_output_accessors.py b/postprocessing_external/tests/test_output_accessors.py new file mode 100644 index 000000000..0e211e4dc --- /dev/null +++ b/postprocessing_external/tests/test_output_accessors.py @@ -0,0 +1,259 @@ +"""Tests for run.plot, run.analysis and product lookup by name.""" + +import os + +import h5py +import matplotlib + +matplotlib.use("Agg") + +import numpy as np # noqa: E402 +import pytest # noqa: E402 +from matplotlib import pyplot as plt # noqa: E402 + +import struphy_plots # noqa: F401, E402 +from struphy_plots.analysis import damping_rate, envelope, growth_rate, norm +from struphy_plots.output_accessors import OutputPlots +from struphy_plots.plotting import save_all_scalars +from struphy.post_processing.output import Output # noqa: E402 +from struphy.post_processing.tests.test_output import write_manifest, write_tree # noqa: E402 + +RATE = 2.0 + + +def make_run(root, name="sim_1"): + path = os.path.join(root, name) + os.makedirs(path) + write_tree(path) + with h5py.File(os.path.join(path, "data", "data_proc0.hdf5"), "a") as file: + time = np.asarray(file["time/value"]) + file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) + write_manifest(path) + return Output(path) + + +@pytest.fixture +def run(tmp_path): + return make_run(str(tmp_path)) + + +@pytest.fixture(autouse=True) +def close_figures(): + yield + plt.close("all") + + +def scalar(run, name): + return run.evaluate("scalars", variables=name)[name] + + +def distribution(run): + return run.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") + + +def density(run): + return run.evaluate("kinetic_ions/n", dataset="view_0/n") + + +def orbits(run): + return run.evaluate("kinetic_ions/orbits") + + +def test_products_are_found_by_name(run): + assert scalar(run, "en_tot").dims == ("t",) + assert run.evaluate("em_fields/E").dims[:2] == ("t", "component") + assert distribution(run).dims == ("t", "e1", "v1") + assert density(run).dims == ("t", "e1", "e2", "e3") + assert orbits(run).dims == ("t", "marker", "quantity") + with pytest.raises(ValueError, match="species/variable"): + run.evaluate("t") + + +def test_every_array_carries_its_run(run): + for array in (run.scalars.en_tot, run.fields.em_fields.E, orbits(run)): + assert array.attrs["run"] == run.label + assert array.attrs["run_name"] == "sim_1" + assert run.scalars.en_tot.isel(t=slice(1, None)).attrs["run_name"] == "sim_1" + + +def test_timeseries_by_name_with_growth_fit(run): + result = scalar(run, "en_phi").struphy.plot.timeseries(fit=True) + assert result.fit_results[0].rate == pytest.approx(RATE) + assert result.fig._suptitle.get_text() == run.label + + +def test_output_keeps_only_the_core_quick_plot(run): + assert not hasattr(run, "timeseries") + assert callable(run.plot) + + +def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): + first, second = make_run(str(tmp_path), "sim_1"), make_run(str(tmp_path), "sim_2") + result = first.scalars.en_phi.struphy.plot.timeseries(second.scalars.en_phi) + labels = [text.get_text() for text in result.ax.get_legend().get_texts()] + assert labels == ["en phi (sim_1)", "en phi (sim_2)"] + + +def test_timeseries_into_given_axes_keeps_the_figure_layout(run): + fig, ax = plt.subplots() + fig.suptitle("mine") + scalar(run, "en_tot").struphy.plot.timeseries(ax=ax, logy=False) + assert fig._suptitle.get_text() == "mine" + + +def test_scalar_overview_draws_every_scalar_in_one_axes(run): + result = OutputPlots(run).scalars() + fig, ax = result.fig, result.ax + assert sorted(line.get_label() for line in ax.lines) == ["en_phi", "en_tot"] + assert fig.axes == [ax] + + +def test_slices_panels_and_viewer_take_keyword_views(run): + product = distribution(run) + assert product.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" + assert len(product.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + viewer = run.evaluate("em_fields/E").struphy.plot.viewer(x="e1", y="e2", component=0) + viewer.draw() + assert set(viewer.sliders) == {"t", "e3"} + + +def test_orbits_plot_their_trajectories(run): + assert run.kinetic_ions.orbits.struphy.plot.trajectories().ax.name == "3d" + + +def test_report_is_written_below_post_processing(run): + paths = save_all_scalars(run.scalars, run.path_pproc / "report", run_label=run.label) + assert all(path.startswith(str(run.path_pproc / "report")) for path in paths) + assert {os.path.basename(path) for path in paths} >= {"scalars.csv", "scalars.png", "en_phi.png"} + + +def test_analysis_by_name(run): + assert scalar(run, "en_phi").struphy.analysis.growth_rate(window=(0.0, None)).rate == pytest.approx(RATE) + assert scalar(run, "en_phi").struphy.analysis.growth_rate(amplitude=True).rate == pytest.approx(RATE / 2) + np.testing.assert_allclose(scalar(run, "en_tot").struphy.analysis.relative_error(), 0.0) + np.testing.assert_allclose(scalar(run, "en_phi").struphy.analysis.drift().isel(t=0), 0.0) + + +def test_dispersion_rejects_fields_in_seconds(run): + physical = run.with_time_units("physical") + with pytest.raises(ValueError, match="normalized"): + physical.fields.em_fields.E.struphy.analysis.dispersion() + + +def test_selection_keywords_take_positions_values_and_ends(run): + product = distribution(run) + times = product.t.values + + by_position = product.struphy.plot.slice(x="e1", y="v1", t=-1) + by_value = product.struphy.plot.slice(x="e1", y="v1", t=float(times[-1])) + by_end = product.struphy.plot.slice(x="e1", y="v1", t="last") + for result in (by_value, by_end): + np.testing.assert_allclose(result.artists[0].get_array(), by_position.artists[0].get_array()) + + with pytest.raises(TypeError, match="not a dimension"): + product.struphy.plot.slice(x="e1", y="v1", time=-1) + with pytest.raises(TypeError, match="use a number"): + product.struphy.plot.slice(x="e1", y="v1", t="final") + + +def test_products_of_one_species_sit_on_the_output(run): + assert run.kinetic_ions.e1_v1_density.f.dims == ("t", "e1", "v1") + assert run.kinetic_ions.view_0.n.dims == ("t", "e1", "e2", "e3") + assert run.kinetic_ions.orbits.dims == ("t", "marker", "quantity") + assert run.em_fields.E.dims[:2] == ("t", "component") + assert {"kinetic_ions", "em_fields"} <= set(dir(run)) + with pytest.raises(AttributeError, match="available species"): + run.electrons + + +def test_product_namespaces_expose_a_scoped_lazy_catalog(run): + products = run.kinetic_ions + assert tuple(sorted(products.catalog)) == ("e1_v1_density/delta_f", "e1_v1_density/f", "orbits", "view_0/n") + assert "e1_v1_density/f" in products.catalog + assert "em_fields/E" not in products.catalog + assert "e1_v1_density/f" in repr(products) + assert run.distribution_catalog._cache == {} + assert products["e1_v1_density/f"].dims == ("t", "e1", "v1") + assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f"] is products.catalog["e1_v1_density/f"] + + +def test_arrays_plot_themselves(run): + phase_space = run.kinetic_ions.e1_v1_density.f + assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" + assert len(phase_space.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 + assert set(phase_space.struphy.plot.viewer(x="e1", y="v1").sliders) == set() + assert run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=2).ax.name == "3d" + + +def test_the_accessor_works_on_derived_arrays(run): + energy = run.scalars.en_phi + assert energy.isel(t=slice(1, None)).struphy.analysis.growth_rate().rate == pytest.approx(RATE) + error = energy.struphy.analysis.relative_error() + assert error.struphy.plot.timeseries(logy=False).fig._suptitle.get_text() == run.label + + +def test_products_by_name_and_by_attribute_agree(run): + by_output = distribution(run).struphy.plot.slice(x="e1", y="v1", t="last") + by_attribute = run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", t="last") + np.testing.assert_allclose(by_output.artists[0].get_array(), by_attribute.artists[0].get_array()) + assert by_output.fig._suptitle.get_text() == by_attribute.fig._suptitle.get_text() == run.label + + +def test_selection_rejects_unknown_dimensions(run): + with pytest.raises(TypeError, match="not a dimension"): + run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", time=-1) + + +def oscillating_energy(rate=-0.3, omega=3.0): + import xarray as xr + + time = np.linspace(0.0, 20.0, 4001) + values = np.exp(2 * rate * time) * np.cos(omega * time) ** 2 + 1e-12 + return xr.DataArray(values, dims="t", coords={"t": time}, name="energy") + + +def test_damping_rate_fits_the_envelope_not_the_oscillation(run): + energy = oscillating_energy(rate=-0.3) + fit = energy.struphy.analysis.damping_rate(amplitude=True) + assert fit.rate == pytest.approx(-0.3, rel=1e-2) + assert energy.struphy.analysis.damping_rate(window=(2.0, 10.0), amplitude=True).rate == pytest.approx( + -0.3, rel=1e-2 + ) + + peaks = energy.struphy.analysis.envelope() + assert 0 < peaks.sizes["t"] < energy.sizes["t"] // 10 + assert np.all(peaks > 1e-3 * np.exp(-0.6 * peaks.t)) + + +def test_damping_rate_without_peaks_is_none(run): + assert scalar(run, "en_phi").struphy.analysis.damping_rate() is None + + +def test_norm_reduces_all_but_time(run): + e_field = run.evaluate("em_fields/E") + squared = e_field.struphy.analysis.norm(squared=True) + assert squared.dims == ("t",) + np.testing.assert_allclose(squared, (np.asarray(e_field) ** 2).sum(axis=(1, 2, 3, 4))) + assert e_field.struphy.analysis.norm(dims=["e1"]).dims == ("t", "component", "e2", "e3") + assert growth_rate(squared, fit=None) is not None + + +def test_physical_coords_are_attached_to_products_without_them(run): + density_data = density(run) + assert "X" not in density_data.coords + mapped = run.with_physical_coords(density_data) + expected = run.domain(*(np.asarray(density_data[dim]) for dim in ("e1", "e2", "e3"))) + for name, values in zip(("X", "Y", "Z"), expected): + assert mapped[name].dims == ("e1", "e2", "e3") + np.testing.assert_allclose(mapped[name], values) + + plane = run.with_physical_coords(density_data.isel(e3=0, drop=True)) + assert plane.X.dims == ("e1", "e2") + + phase_space = run.with_physical_coords(distribution(run)) + assert phase_space.X.dims == ("e1",) + + field = run.evaluate("em_fields/E") + assert run.with_physical_coords(field) is field + with pytest.raises(ValueError, match="no logical dimensions"): + run.with_physical_coords(scalar(run, "en_tot")) diff --git a/postprocessing_external/tests/test_plotting.py b/postprocessing_external/tests/test_plotting.py new file mode 100644 index 000000000..2775a7659 --- /dev/null +++ b/postprocessing_external/tests/test_plotting.py @@ -0,0 +1,308 @@ +"""Tests for functional plotting and the shared view recipe.""" + +import matplotlib +import numpy as np +import pytest +import xarray as xr + +matplotlib.use("Agg") +from matplotlib import pyplot as plt # noqa: E402 + +from struphy_plots.plotting import ( # noqa: E402 + GrowthFit, + InteractiveSliceViewer, + View, + animate_slices, + drift, + growth_rate, + logical_grids, + physical_grids, + plot_panels, + plot_lineout, + plot_scalars, + plot_vector, + plot_volume_slices, + pyvista_volume, + plot_slice, + plot_timeseries, + relative_error, + save_all_scalars, + save_frames, +) +from struphy.post_processing.arrays import data_array # noqa: E402 + +pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") + + +@pytest.fixture(autouse=True) +def close_figures(): + yield + plt.close("all") + + +def phase_space(nt=6): + return data_array( + np.arange(nt * 4 * 5).reshape(nt, 4, 5), + ("t", "e1", "v1"), + {"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, 4), "v1": np.linspace(-2, 2, 5)}, + name="f", + label="$f$", + coord_units={"t": "s"}, + ) + + +def physical_field(): + coords = {"t": [0, 1], "e1": range(3), "e2": range(4), "e3": range(5)} + grids = np.meshgrid(coords["e1"], coords["e2"], coords["e3"], indexing="ij") + coords.update({name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), grids)}) + return data_array(np.ones((2, 3, 4, 5)), ("t", "e1", "e2", "e3"), coords, name="phi") + + +def scalar_dataset(): + t = np.linspace(0, 1, 6) + return xr.Dataset({"en_tot": ("t", 2 + 0.02 * t), "en_e": ("t", 1 + 0.1 * t)}, coords={"t": t}) + + +def test_lineout_vector_and_orthogonal_volume_slices_render(): + line = plot_lineout(phase_space().isel(t=0, e1=0)) + assert len(line.artists) == 1 + assert len(phase_space().struphy.plot.lineout(x="v1", t=0, e1=0).artists) == 1 + + vector = data_array( + np.ones((2, 3, 4)), ("component", "e1", "e2"), {"component": [0, 1], "e1": range(3), "e2": range(4)} + ) + assert len(plot_vector(vector, x="e1", y="e2").artists) == 1 + assert len(vector.struphy.plot.vector(x="e1", y="e2").artists) == 1 + assert len(plot_volume_slices(physical_field().isel(t=0)).artists) == 3 + + +def test_pyvista_volume_uses_mapped_coordinates(): + plotter = pyvista_volume(physical_field().isel(t=0)) + assert plotter.renderer is not None + plotter.close() + + +def test_growth_rate_uses_only_valid_samples_inside_window(): + data = data_array([1, 0, 4, np.nan, 16], ("t",), {"t": range(5)}) + result = growth_rate(data, GrowthFit((0, 4))) + assert result is not None and np.isfinite(result.rate) + np.testing.assert_array_equal(result.time, [0, 2, 4]) + + +def test_growth_rate_does_not_fall_back_outside_requested_window(): + data = data_array(np.exp(np.arange(5)), ("t",), {"t": range(5)}) + assert growth_rate(data, GrowthFit((1.1, 1.2))) is None + + +def test_growth_rate_of_quadratic_reports_amplitude_rate(): + t = np.linspace(0, 4, 20) + result = growth_rate(data_array(np.exp(0.6 * t), ("t",), {"t": t}), GrowthFit(amplitude_from_quadratic=True)) + assert result.rate == pytest.approx(0.3) + + +def test_diagnostics_preserve_time_coordinates(): + data = data_array([2, 2.2, 1.8], ("t",), {"t": [0, 1, 2]}, label="E") + np.testing.assert_allclose(drift(data), [0, 0.2, -0.2]) + np.testing.assert_allclose(relative_error(data), [0.1, 0.1]) + np.testing.assert_array_equal(relative_error(data).t, [1, 2]) + + +def test_logical_and_physical_grids_follow_selected_dimensions(): + logical = phase_space().isel(t=0) + assert logical_grids(logical)[0].shape == (4, 5) + physical = physical_field().isel(t=0, e3=2) + assert physical_grids(physical, plane="XY")[0].shape == (3, 4) + assert physical_grids(physical, plane="RZ")[0].shape == (3, 4) + + +def test_plot_timeseries_renders_once_and_save_does_not_redraw(tmp_path): + data = data_array(np.exp(np.arange(4)), ("t",), {"t": range(4)}, label="energy", coord_units={"t": "s"}) + result = plot_timeseries(data, fit=GrowthFit(), run_label="dt=.1") + lines = len(result.ax.lines) + result.save(tmp_path / "energy.png") + assert len(result.ax.lines) == lines + assert len(plt.get_fignums()) == 1 + assert result.fig._suptitle.get_text() == "dt=.1" + + +def test_plot_slice_accepts_named_value_and_index_selection(): + result = plot_slice(phase_space(), view=View(x="e1", y="v1", select={"t": 0.52})) + assert result.ax.get_xlabel() == r"$\eta_1$" + assert len(result.artists) == 1 + + +def test_plot_slice_physical_coordinates_are_intrinsic(): + result = plot_slice( + physical_field(), view=View(x="e1", y="e2", isel={"t": 0, "e3": 2}, coordinates="physical", plane="XY") + ) + assert result.ax.get_xlabel() == "X" + assert result.ax.get_aspect() == 1.0 + + +def test_plot_slice_rejects_underspecified_selection(): + with pytest.raises(ValueError, match="selection leaves"): + plot_slice(physical_field(), view=View(x="e1", y="e2", isel={"t": 0})) + + +def test_panels_use_one_recipe_and_keep_full_title(): + result = plot_panels( + phase_space(), view=View(x="e1", y="v1"), nrows=1, ncols=2, title="Distribution", run_label="dt=.1" + ) + assert result.fig._suptitle.get_text() == "Distribution — dt=.1" + assert len(result.artists) == 2 + + +def test_viewer_builds_controls_for_every_non_display_dimension(): + viewer = InteractiveSliceViewer(physical_field(), view=View(x="e1", y="e2", coordinates="physical")) + result = viewer.draw() + assert set(viewer.sliders) == {"t", "e3"} + viewer.sliders["e3"].set_val(3) + assert result.fig is not None + + +def test_animation_and_frames_share_the_view(tmp_path): + data = phase_space(nt=7) + view = View(x="e1", y="v1") + animation = animate_slices(data, view=view, step=3) + assert len(list(animation.new_frame_seq())) == 3 + paths = save_frames(data, tmp_path, view=view, step=3) + assert len(paths) == 3 + assert all(__import__("pathlib").Path(path).exists() for path in paths) + + +def test_scalar_overview_and_export(tmp_path): + result = plot_scalars(scalar_dataset(), run_label="run") + assert sorted(line.get_label() for line in result.artists) == ["en_e", "en_tot"] + assert result.fig._suptitle.get_text() == "run" + paths = save_all_scalars(scalar_dataset(), tmp_path) + assert sorted(__import__("os").path.basename(path) for path in paths) == [ + "en_e.png", + "en_tot.png", + "scalars.csv", + "scalars.png", + ] + assert plt.get_fignums() == [result.fig.number] + + +@pytest.mark.parametrize("shown", [False, True]) +def test_notebook_display_shows_the_figure_once(monkeypatch, shown): + import IPython.display + + displayed = [] + monkeypatch.setattr(matplotlib, "get_backend", lambda: "module://matplotlib_inline.backend_inline") + monkeypatch.setattr(IPython.display, "display", displayed.append) + monkeypatch.setattr(plt, "show", lambda *args, **kwargs: None) + result = plot_timeseries(scalar_dataset().en_tot, logy=False) + if shown: + result.show() + result._ipython_display_() + assert displayed == ([] if shown else [result.fig]) + assert (result.fig.number in plt.get_fignums()) == shown, "the inline backend must not show it again" + + +def test_slice_can_display_the_sweep_dimension(): + data = phase_space() + result = plot_slice(data.isel(v1=slice(None)), view=View(x="t", y="e1", isel={"v1": 0})) + assert result.ax.get_xlabel() == "$t$ [s]" + with pytest.raises(ValueError, match="display it as x or y"): + plot_slice(data, view=View(x="e1", y="v1")) + + +def test_every_presentation_uses_the_full_selected_color_range(tmp_path, monkeypatch): + from matplotlib.figure import Figure + + import struphy_plots # noqa: F401 + + data = phase_space(nt=3).astype(float) + data[1] = data[1] * 100 # extrema in a frame omitted by panels and export + view = data.struphy.plot.view(x="e1", y="v1", cmap="plasma", equal_aspect=True) + limits = (float(data.min()), float(data.max())) + assert plt.get_fignums() == [] + snapshot = view.slice(t="last") + panels = view.panels(nrows=1, ncols=2) + viewer = view.viewer() + result = viewer.draw() + viewer.sliders["t"].set_val(2) + animation = view.animation(step=2) + mesh = animation._func(2)[0] + for artist in [snapshot.artists[0], *panels.artists, result.artists[0], mesh]: + assert artist.get_clim() == limits + assert artist.get_cmap().name == "plasma" + assert artist.axes.get_aspect() == 1.0 + captured = [] + original = Figure.savefig + + def capture(fig, *args, **kwargs): + captured.append(fig.axes[0].collections[0].get_clim()) + return original(fig, *args, **kwargs) + + monkeypatch.setattr(Figure, "savefig", capture) + before = plt.get_fignums() + assert len(view.save_frames(tmp_path, step=2)) == 2 + assert captured == [limits, limits] + assert plt.get_fignums() == before + + +@pytest.mark.parametrize("shared_clim", [True, False]) +def test_explicit_color_limits_work_for_all_renderers(tmp_path, monkeypatch, shared_clim): + from matplotlib.figure import Figure + + import struphy_plots # noqa: F401 + + data = phase_space(nt=2) + options = dict(x="e1", y="v1", vmin=-5, vmax=100, shared_clim=shared_clim, cmap="coolwarm") + panels = data.struphy.plot.panels(nrows=1, ncols=2, **options) + animation = data.struphy.plot.animation(**options) + viewer = data.struphy.plot.viewer(**options) + viewer.draw() + viewer.sliders["t"].set_val(1) + for mesh in [*panels.artists, animation._func(1)[0], viewer.result.artists[0]]: + assert mesh.get_clim() == (-5, 100) + assert mesh.get_cmap().name == "coolwarm" + captured = [] + monkeypatch.setattr( + Figure, "savefig", lambda fig, *args, **kwargs: captured.append(fig.axes[0].collections[0].get_clim()) + ) + data.struphy.plot.frames(tmp_path, **options) + assert captured == [(-5, 100), (-5, 100)] + + +def test_per_frame_scaling_is_explicit_and_supports_a_fixed_lower_limit(): + import struphy_plots # noqa: F401 + + data = phase_space(nt=2) + view = data.struphy.plot.view(x="e1", y="v1", shared_clim=False, vmin=-1) + panels = view.panels(nrows=1, ncols=2) + animation = view.animation() + for index in range(2): + limits = (-1, float(data.isel(t=index).max())) + assert panels.artists[index].get_clim() == limits + assert animation._func(index)[0].get_clim() == limits + + +def test_viewer_show_retains_controls_and_does_not_redraw(monkeypatch): + viewer = InteractiveSliceViewer(phase_space(), view=View(x="e1", y="v1")) + result = viewer.draw() + monkeypatch.setattr(plt, "show", lambda: None) + assert viewer.show() is viewer + assert viewer.draw() is result + assert len(plt.get_fignums()) == 1 + viewer.sliders["t"].set_val(2) + assert result.artists[0] is result.ax.collections[0] + + +@pytest.mark.parametrize("step", [0, -1]) +def test_sweep_rejects_invalid_step(tmp_path, step): + with pytest.raises(ValueError, match="positive integer"): + animate_slices(phase_space(), view=View(x="e1", y="v1"), step=step) + with pytest.raises(ValueError, match="positive integer"): + save_frames(phase_space(), tmp_path, view=View(x="e1", y="v1"), step=step) + + +def test_legacy_scalar_plot_warns_and_still_renders(monkeypatch): + from struphy.diagnostics import diagn_tools + + monkeypatch.setattr(plt, "show", lambda: None) + with pytest.deprecated_call(match="diagn_tools.plot_scalars"): + diagn_tools.plot_scalars(np.arange(3), {"en_tot": np.array([1.0, 2.0, 3.0])}) + assert plt.get_fignums() From d97fe4156bc5f0bab42ae86a1548eff20aa18207 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 00:00:33 +0200 Subject: [PATCH 128/193] formatting --- doc/conf.py | 3 +- .../cyclone/pproc_cyclone.py | 4 +- .../itg_cylindre/pproc_drift_kinetic.py | 4 +- .../diocotron_instability/pproc_diocotron.py | 3 +- .../bump_on/pproc_bump_on.py | 2 +- .../pproc_strong_Landau_damping.py | 3 +- .../two_stream/pproc_two_stream.py | 3 +- .../pproc_weak_Landau_damping.py | 2 +- .../pproc_weibel_instability.py | 2 +- params_LinearMHDDriftkineticCC.py | 47 ++++++------ .../src/struphy_plots/accessors.py | 11 ++- .../src/struphy_plots/arrays.py | 19 ++++- .../src/struphy_plots/plotting.py | 21 ++++-- .../tests/test_analysis_and_core_output.py | 2 +- .../tests/test_output_accessors.py | 4 +- .../tests/test_plotting.py | 14 ++-- src/struphy/post_processing/manifest.py | 1 - src/struphy/post_processing/output.py | 75 +++++++++++++++---- .../post_processing/tests/test_output.py | 22 +++++- src/struphy/simulation/sim.py | 33 ++++---- src/struphy/simulation/tests/test_output.py | 34 ++++++--- 21 files changed, 211 insertions(+), 98 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 72f335fbb..38e9e3e01 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -53,6 +53,7 @@ "sphinx_collections", ] + def _struphy_is_compiled(): """Whether `struphy compile` has been run (state.yml exists in the installed package).""" try: @@ -65,7 +66,7 @@ def _struphy_is_compiled(): # Notebooks are slow to run and many depend on compiled Struphy kernels, # so only execute them when Struphy has been compiled; otherwise reuse stored outputs. nbsphinx_execute = "auto" if _struphy_is_compiled() else "never" -nbsphinx_kernel_name = 'local-env' # This is just for Stefan's local machine, where the system kernel does not work. +nbsphinx_kernel_name = "local-env" # This is just for Stefan's local machine, where the system kernel does not work. napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 93b203d10..11c9180f3 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,11 +1,11 @@ import sys from pathlib import Path +import struphy_plots from matplotlib import pyplot as plt +from struphy_plots.output_accessors import OutputPlots from struphy import Output -import struphy_plots -from struphy_plots.output_accessors import OutputPlots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index 4cd9ce032..c8232a73d 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,11 +1,11 @@ import sys from pathlib import Path +import struphy_plots from matplotlib import pyplot as plt +from struphy_plots.output_accessors import OutputPlots from struphy import Output -import struphy_plots -from struphy_plots.output_accessors import OutputPlots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index db9e136fc..20f3fc721 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -7,10 +7,11 @@ import sys from pathlib import Path -from struphy import Output import struphy_plots from struphy_plots.output_accessors import OutputPlots +from struphy import Output + DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" FIT_QUANTITY = "en_phi" diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 96c1822b4..4a77ee18b 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -1,10 +1,10 @@ import argparse from pathlib import Path +import struphy_plots from matplotlib import pyplot as plt from struphy import Output -import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index 4af6d3508..7be8dc2b8 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,9 +1,10 @@ import argparse from pathlib import Path -from struphy import Output import struphy_plots +from struphy import Output + DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index a3382b235..c6e076a46 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,10 +1,11 @@ import argparse from pathlib import Path -from struphy import Output import struphy_plots from struphy_plots.plotting import save_all_scalars +from struphy import Output + DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 0807c8389..075150bb9 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -2,9 +2,9 @@ from pathlib import Path import cunumpy as xp +import struphy_plots from struphy import Output -import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 5509bec9b..0126c9984 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -2,10 +2,10 @@ from pathlib import Path import cunumpy as xp +import struphy_plots from matplotlib import pyplot as plt from struphy import Output -import struphy_plots DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" diff --git a/params_LinearMHDDriftkineticCC.py b/params_LinearMHDDriftkineticCC.py index 5a69f8847..517c10207 100644 --- a/params_LinearMHDDriftkineticCC.py +++ b/params_LinearMHDDriftkineticCC.py @@ -1,7 +1,7 @@ # ----------------------------- # Description of the simulation # ----------------------------- -# Please fill in a verbal description of the simulation. +# Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. name = "Default LinearMHDDriftkineticCC" @@ -15,45 +15,44 @@ """ import logging + import numpy as np + from struphy import set_logging_level from struphy.initial.base import Perturbation + set_logging_level(logging.WARNING) # ------------------ # Import Struphy API # ------------------ +# For particles: from struphy import ( BaseUnits, + BinningPlot, + BoundaryParameters, DerhamOptions, EnvironmentOptions, FieldsBackground, + KernelDensityPlot, + LoadingParameters, ProfilingOptions, + SavingParameters, Simulation, + SortingParameters, Time, + WeightsParameters, domains, equils, grids, - perturbations, -) - -# For particles: -from struphy import ( - BinningPlot, - BoundaryParameters, - KernelDensityPlot, - LoadingParameters, - WeightsParameters, - SortingParameters, - SavingParameters, maxwellians, + perturbations, ) # --------------------- # Instance of the model # --------------------- - from struphy.models import LinearMHDDriftkineticCC # Units @@ -118,12 +117,13 @@ boundary_params = BoundaryParameters() sorting_params = SortingParameters() saving_params = SavingParameters() -model.energetic_ions.set_markers(loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, - ) +model.energetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + sorting_params=sorting_params, + saving_params=saving_params, +) # ------------------ # Propagator options @@ -164,9 +164,9 @@ def __call__(self, eta1, eta2, eta3, flat_eval=False): model.mhd.velocity.add_background(FieldsBackground()) # Perturbations for (some) FEEC variables -model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=0)) -model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=1)) -model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=2)) +model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=0)) +model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=1)) +model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=2)) # A custom perturbation class can be combined with built-in perturbations. model.mhd.velocity.add_perturbation(RadialVelocityPerturbation(amplitude=0.02, comp=0)) @@ -174,6 +174,7 @@ def __call__(self, eta1, eta2, eta3, flat_eval=False): # For kinetic species, if add_initial_condition() is not called, the background is taken as the kinetic initial condition. # For kinetic species the perturbations are added to the moments of the distribution function (defined as tuples). + # User-defined profiles can be used anywhere a kinetic Maxwellian accepts a # callable. They are written inline to run_metadata.json and restored with # Simulation.from_output(..., trust_initial_condition_source=True). diff --git a/postprocessing_external/src/struphy_plots/accessors.py b/postprocessing_external/src/struphy_plots/accessors.py index e67e49b00..805ec68ce 100644 --- a/postprocessing_external/src/struphy_plots/accessors.py +++ b/postprocessing_external/src/struphy_plots/accessors.py @@ -106,8 +106,15 @@ def lineout(self, *, x: str | None = None, ax=None, title: str | None = None, ** return plot_lineout(_select(self._array, view), x=x, ax=ax, title=title) def vector( - self, *, x: str, y: str, components: tuple[int, int] = (0, 1), stride: int = 1, - coordinates: Coordinates = "logical", ax=None, **selection, + self, + *, + x: str, + y: str, + components: tuple[int, int] = (0, 1), + stride: int = 1, + coordinates: Coordinates = "logical", + ax=None, + **selection, ): """Plot two vector components after selecting time and remaining dimensions.""" from .plotting import _select, plot_vector diff --git a/postprocessing_external/src/struphy_plots/arrays.py b/postprocessing_external/src/struphy_plots/arrays.py index 9fbf4fdb0..64a0430b4 100644 --- a/postprocessing_external/src/struphy_plots/arrays.py +++ b/postprocessing_external/src/struphy_plots/arrays.py @@ -13,10 +13,21 @@ import xarray as xr DIM_LABELS = { - "t": r"$t$", "e1": r"$\eta_1$", "e2": r"$\eta_2$", "e3": r"$\eta_3$", - "v1": r"$v_1$", "v2": r"$v_2$", "v3": r"$v_3$", "x": r"$x$", "y": r"$y$", - "z": r"$z$", "R": r"$R$", "Z": r"$Z$", "component": "component", - "marker": "marker", "quantity": "quantity", + "t": r"$t$", + "e1": r"$\eta_1$", + "e2": r"$\eta_2$", + "e3": r"$\eta_3$", + "v1": r"$v_1$", + "v2": r"$v_2$", + "v3": r"$v_3$", + "x": r"$x$", + "y": r"$y$", + "z": r"$z$", + "R": r"$R$", + "Z": r"$Z$", + "component": "component", + "marker": "marker", + "quantity": "quantity", } SCALARS_EXCLUDE = ("time",) diff --git a/postprocessing_external/src/struphy_plots/plotting.py b/postprocessing_external/src/struphy_plots/plotting.py index b12556330..3d7778091 100644 --- a/postprocessing_external/src/struphy_plots/plotting.py +++ b/postprocessing_external/src/struphy_plots/plotting.py @@ -280,8 +280,15 @@ def plot_lineout(data: xr.DataArray, *, x: str | None = None, ax=None, title=Non def plot_vector( - data: xr.DataArray, *, x: str, y: str, components: tuple[int, int] = (0, 1), component_dim: str = "component", ax=None, - stride: int = 1, coordinates: Literal["logical", "physical"] = "logical", + data: xr.DataArray, + *, + x: str, + y: str, + components: tuple[int, int] = (0, 1), + component_dim: str = "component", + ax=None, + stride: int = 1, + coordinates: Literal["logical", "physical"] = "logical", ): """Render two components of a selected vector field with Matplotlib quivers.""" validate_array(data, required_dims=(component_dim, x, y)) @@ -289,7 +296,9 @@ def plot_vector( raise ValueError(f"select every dimension except {component_dim!r}, {x!r}, and {y!r}; got {data.dims}") if stride < 1: raise ValueError("stride must be positive") - vector = data.transpose(component_dim, x, y).isel({component_dim: list(components), x: slice(None, None, stride), y: slice(None, None, stride)}) + vector = data.transpose(component_dim, x, y).isel( + {component_dim: list(components), x: slice(None, None, stride), y: slice(None, None, stride)} + ) if coordinates == "physical": planes = {frozenset(("e1", "e2")): "XY", frozenset(("e1", "e3")): "XZ", frozenset(("e2", "e3")): "YZ"} plane = planes.get(frozenset((x, y))) @@ -319,11 +328,13 @@ def plot_volume_slices(data: xr.DataArray, *, indices: dict[str, int] | None = N ax.set(xlabel=axis_label(plane, x), ylabel=axis_label(plane, y), title=f"{normal} index {indices[normal]}") fig.colorbar(mesh, ax=ax, label=value_label(data)) artists.append(mesh) - fig.suptitle(" — ".join(filter(None, (_label(data), shared_run_label(data)))) ) + fig.suptitle(" — ".join(filter(None, (_label(data), shared_run_label(data))))) return PlotResult(fig, axes, artists) -def plot_compare(first: xr.DataArray, second: xr.DataArray, *, mode: Literal["difference", "ratio"] = "difference", ax=None): +def plot_compare( + first: xr.DataArray, second: xr.DataArray, *, mode: Literal["difference", "ratio"] = "difference", ax=None +): """Plot a one-dimensional aligned difference or ratio of two arrays.""" first, second = xr.align(first, second, join="inner") result = first - second if mode == "difference" else xr.where(second != 0, first / second, np.nan) diff --git a/postprocessing_external/tests/test_analysis_and_core_output.py b/postprocessing_external/tests/test_analysis_and_core_output.py index 8c4ad0314..48bfdf46f 100644 --- a/postprocessing_external/tests/test_analysis_and_core_output.py +++ b/postprocessing_external/tests/test_analysis_and_core_output.py @@ -6,8 +6,8 @@ import numpy as np import pytest import xarray as xr - from struphy_plots.analysis import spatial_average, velocity_moments + from struphy.post_processing.arrays import data_array from struphy.post_processing.output import Output from struphy.post_processing.tests.test_output import write_tree diff --git a/postprocessing_external/tests/test_output_accessors.py b/postprocessing_external/tests/test_output_accessors.py index 0e211e4dc..1e2cce492 100644 --- a/postprocessing_external/tests/test_output_accessors.py +++ b/postprocessing_external/tests/test_output_accessors.py @@ -9,12 +9,12 @@ import numpy as np # noqa: E402 import pytest # noqa: E402 -from matplotlib import pyplot as plt # noqa: E402 - import struphy_plots # noqa: F401, E402 +from matplotlib import pyplot as plt # noqa: E402 from struphy_plots.analysis import damping_rate, envelope, growth_rate, norm from struphy_plots.output_accessors import OutputPlots from struphy_plots.plotting import save_all_scalars + from struphy.post_processing.output import Output # noqa: E402 from struphy.post_processing.tests.test_output import write_manifest, write_tree # noqa: E402 diff --git a/postprocessing_external/tests/test_plotting.py b/postprocessing_external/tests/test_plotting.py index 2775a7659..9fb79bf36 100644 --- a/postprocessing_external/tests/test_plotting.py +++ b/postprocessing_external/tests/test_plotting.py @@ -7,7 +7,6 @@ matplotlib.use("Agg") from matplotlib import pyplot as plt # noqa: E402 - from struphy_plots.plotting import ( # noqa: E402 GrowthFit, InteractiveSliceViewer, @@ -17,18 +16,19 @@ growth_rate, logical_grids, physical_grids, - plot_panels, plot_lineout, + plot_panels, plot_scalars, + plot_slice, + plot_timeseries, plot_vector, plot_volume_slices, pyvista_volume, - plot_slice, - plot_timeseries, relative_error, save_all_scalars, save_frames, ) + from struphy.post_processing.arrays import data_array # noqa: E402 pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") @@ -209,9 +209,8 @@ def test_slice_can_display_the_sweep_dimension(): def test_every_presentation_uses_the_full_selected_color_range(tmp_path, monkeypatch): - from matplotlib.figure import Figure - import struphy_plots # noqa: F401 + from matplotlib.figure import Figure data = phase_space(nt=3).astype(float) data[1] = data[1] * 100 # extrema in a frame omitted by panels and export @@ -245,9 +244,8 @@ def capture(fig, *args, **kwargs): @pytest.mark.parametrize("shared_clim", [True, False]) def test_explicit_color_limits_work_for_all_renderers(tmp_path, monkeypatch, shared_clim): - from matplotlib.figure import Figure - import struphy_plots # noqa: F401 + from matplotlib.figure import Figure data = phase_space(nt=2) options = dict(x="e1", y="v1", vmin=-5, vmax=100, shared_clim=shared_clim, cmap="coolwarm") diff --git a/src/struphy/post_processing/manifest.py b/src/struphy/post_processing/manifest.py index 009c0b49b..12e1721d3 100644 --- a/src/struphy/post_processing/manifest.py +++ b/src/struphy/post_processing/manifest.py @@ -4,7 +4,6 @@ import json import os - MANIFEST_SCHEMA_VERSION = 1 diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index d16bd66c8..b24dec8c4 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -13,8 +13,8 @@ from pathlib import Path from typing import Any, Literal -import h5py import cunumpy as xp +import h5py import numpy as np import xarray as xr from feectools.ddm.mpi import MockComm @@ -27,10 +27,20 @@ from struphy.pic.base import Particles from struphy.post_processing import store from struphy.post_processing.arrays import ( - BINNED_LABELS, data_array, save_scalars, wrap_binned_data, wrap_field_data, wrap_orbits, + BINNED_LABELS, + data_array, + save_scalars, + wrap_binned_data, + wrap_field_data, + wrap_orbits, +) +from struphy.post_processing.manifest import ( + MANIFEST_SCHEMA_VERSION, + is_processed, + normalize_options, + source_fingerprint, ) from struphy.post_processing.orbits import orbits_tools -from struphy.post_processing.manifest import MANIFEST_SCHEMA_VERSION, is_processed, normalize_options, source_fingerprint from struphy.post_processing.profiling import Profile from struphy.post_processing.si import to_si from struphy.utils.progress import tqdm @@ -394,8 +404,15 @@ def _default_logical_grid(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: return tuple((np.arange(n, dtype=float) + 0.5) / n for n in num_elements) def _evaluate_spline_field( - self, name: str, eta1: Any, eta2: Any, eta3: Any, *, t: int | float | slice | Sequence[int] | None, - method: str | None, representation: Representation | None, + self, + name: str, + eta1: Any, + eta2: Any, + eta3: Any, + *, + t: int | float | slice | Sequence[int] | None, + method: str | None, + representation: Representation | None, ) -> xr.DataArray: """Evaluate one raw FEEC field on a tensor-product logical grid.""" try: @@ -453,7 +470,11 @@ def _attach_physical_coords( return array.assign_coords(coordinates) def _apply_representation( - self, value: Any, etas: tuple[Any, Any, Any], source: str, representation: Representation | None, + self, + value: Any, + etas: tuple[Any, Any, Any], + source: str, + representation: Representation | None, ) -> Any: """Transform a field from its FEEC-space representation to the requested target.""" target = representation or ("norm" if source in {"1", "2", "v"} else "0") @@ -502,7 +523,10 @@ def _reshape_spline_value(value: Any, shape: tuple[int, ...]) -> Any: @staticmethod def _snapshot_indices( - selection: int | float | slice | Sequence[int] | None, times: np.ndarray, *, method: str | None, + selection: int | float | slice | Sequence[int] | None, + times: np.ndarray, + *, + method: str | None, ) -> np.ndarray: """Turn the public ``t`` selector into non-negative saved-snapshot indices.""" count = len(times) @@ -940,9 +964,17 @@ def pproc( finally: self._reset() for name in ( - "_pproc_derham", "_pproc_comm", "_pproc_rank", "_pproc_ranks", "_pproc_parallel", - "_pproc_t_grid", "_pproc_exist_fields", "_pproc_exist_particles", - "_pproc_kinetic_species", "_pproc_kinetic_kinds", "_collect_recv_bufs", + "_pproc_derham", + "_pproc_comm", + "_pproc_rank", + "_pproc_ranks", + "_pproc_parallel", + "_pproc_t_grid", + "_pproc_exist_fields", + "_pproc_exist_particles", + "_pproc_kinetic_species", + "_pproc_kinetic_kinds", + "_collect_recv_bufs", ): self.__dict__.pop(name, None) return self @@ -958,7 +990,10 @@ def _setup_processing(self, parallel: bool): self._pproc_derham = None if self.grid is not None and self.derham_opts is not None: self._pproc_derham = Derham( - self.grid, self.derham_opts, comm=self._pproc_comm if parallel else None, domain=self.domain, + self.grid, + self.derham_opts, + comm=self._pproc_comm if parallel else None, + domain=self.domain, ) def _write_manifest(self, status, *, options=None, error=None): @@ -2288,10 +2323,20 @@ def info(self, name: str | None = None) -> None: key: value for key, value in species.items() if key - not in {"class", "variables", "loading_params", "weights_params", "boundary_params", "sorting_params", "saving_params"} + not in { + "class", + "variables", + "loading_params", + "weights_params", + "boundary_params", + "sorting_params", + "saving_params", + } and value is not None } - lines.append(f" {species_name} ({species.get('class', 'Species')}): {json.dumps(parameters, sort_keys=True)}") + lines.append( + f" {species_name} ({species.get('class', 'Species')}): {json.dumps(parameters, sort_keys=True)}" + ) for variable_name, variable in species.get("variables", {}).items(): lines.append( f" {variable_name}: {variable.get('class', 'Variable')} " @@ -2303,7 +2348,9 @@ def info(self, name: str | None = None) -> None: lines.append("Initial conditions:") for species_name, variables in self._initial_condition_metadata().items(): for variable_name, definition in variables.items(): - parts = ", ".join(f"{key}={self._initial_condition_description(value)}" for key, value in definition.items()) + parts = ", ".join( + f"{key}={self._initial_condition_description(value)}" for key, value in definition.items() + ) lines.append(f" {species_name}.{variable_name}: {parts}") lines = [ *lines, diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 4df6b0e1d..a5eef8d05 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -15,8 +15,8 @@ from struphy.post_processing import output as output_module from struphy.post_processing import store from struphy.post_processing.arrays import orbit_quantities -from struphy.post_processing.output import Output, open_output from struphy.post_processing.manifest import is_processed, normalize_options, source_fingerprint +from struphy.post_processing.output import Output, open_output NT, N1, N2, N3, NV, N_MARKERS = 3, 4, 5, 6, 7, 10 @@ -472,10 +472,24 @@ def __call__(self, e1, e2, e3, *, squeeze_out=False): source = field(eta1, eta2, eta3) for target in ("1", "2", "v", "norm"): result = run.evaluate( - "em_fields/e_field", eta1=eta1, eta2=eta2, eta3=eta3, t=0, representation=target, + "em_fields/e_field", + eta1=eta1, + eta2=eta2, + eta3=eta3, + t=0, + representation=target, ) - expected = source if target == "1" else domain.transform( - source, eta1, eta2, eta3, kind=f"1_to_{target}", squeeze_out=True, + expected = ( + source + if target == "1" + else domain.transform( + source, + eta1, + eta2, + eta3, + kind=f"1_to_{target}", + squeeze_out=True, + ) ) expected = np.squeeze(np.asarray(expected)) np.testing.assert_allclose(result.isel(t=0), expected) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 958c4e67a..f9a882cfe 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,6 +1,6 @@ # third party imports -import dataclasses import copy +import dataclasses import glob import hashlib import inspect @@ -55,8 +55,8 @@ ProjectedMHDequilibrium, ) from struphy.geometry.base import Domain -from struphy.io.output_handling import DataContainer from struphy.initial.base import Perturbation +from struphy.io.output_handling import DataContainer from struphy.models import Maxwell from struphy.models.base import StruphyModel from struphy.models.species import ( @@ -1574,13 +1574,16 @@ def _serialize_initial_condition(value): return {str(key): Simulation._serialize_initial_condition(item) for key, item in value.items()} if isinstance(value, (list, tuple)): return [Simulation._serialize_initial_condition(item) for item in value] - if type(value).__module__ not in { - "struphy.initial.perturbations", - "struphy.kinetic_background.maxwellians", - "struphy.kinetic_background.base", - "struphy.io.options", - } and not inspect.isfunction(value) and ( - isinstance(value, Perturbation) or callable(value) + if ( + type(value).__module__ + not in { + "struphy.initial.perturbations", + "struphy.kinetic_background.maxwellians", + "struphy.kinetic_background.base", + "struphy.io.options", + } + and not inspect.isfunction(value) + and (isinstance(value, Perturbation) or callable(value)) ): cls = type(value) if "" in cls.__qualname__: @@ -1666,8 +1669,10 @@ def _deserialize_initial_condition(value): return value if isinstance(value, list): return tuple(Simulation._deserialize_initial_condition(item) for item in value) - if not isinstance(value, dict) or "type" not in value or ( - "params" not in value and value["type"] not in {"python_function", "python_class", "callable"} + if ( + not isinstance(value, dict) + or "type" not in value + or ("params" not in value and value["type"] not in {"python_function", "python_class", "callable"}) ): return {key: Simulation._deserialize_initial_condition(item) for key, item in value.items()} @@ -1678,8 +1683,8 @@ def _deserialize_initial_condition(value): source = value["source"] if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: raise ValueError("Initial-condition function source hash does not match its metadata.") - import numpy as np import cunumpy as xp + import numpy as np namespace = {"np": np, "numpy": np, "xp": xp, "cp": xp, "cupy": xp} exec(source, namespace) # noqa: S102 -- reconstruct saved Python function @@ -1690,8 +1695,8 @@ def _deserialize_initial_condition(value): source = value["source"] if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: raise ValueError("Initial-condition class source hash does not match its metadata.") - import numpy as np import cunumpy as xp + import numpy as np namespace = { "np": np, @@ -1715,8 +1720,8 @@ def _deserialize_initial_condition(value): return FieldsBackground(**Simulation._deserialize_initial_condition(value["params"])) from struphy.initial import perturbations - from struphy.kinetic_background import maxwellians from struphy.kinetic_background import base as kinetic_background_base + from struphy.kinetic_background import maxwellians params = Simulation._deserialize_initial_condition(value["params"]) for module in (equils, perturbations, maxwellians, kinetic_background_base): diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index bbb96aed8..b12f4719b 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -8,14 +8,24 @@ import numpy as np import pytest -from struphy import BaseUnits, EnvironmentOptions, FieldsBackground, Output, Simulation, Time, equils, maxwellians, perturbations +from struphy import ( + BaseUnits, + EnvironmentOptions, + FieldsBackground, + Output, + Simulation, + Time, + equils, + maxwellians, + perturbations, +) +from struphy.initial.base import Perturbation from struphy.linear_algebra.solver import SolverParameters from struphy.models import ColdPlasmaVlasov, LinearMHD, Maxwell, Poisson, VlasovAmpereOneSpecies from struphy.ode.utils import ButcherTableau from struphy.particles.parameters import LoadingParameters from struphy.pic.accumulation.filter import FilterParameters from struphy.post_processing.manifest import is_processed -from struphy.initial.base import Perturbation def user_density_profile(eta1, eta2, eta3): @@ -115,7 +125,11 @@ def test_run_metadata_contains_variables_and_propagator_options(tmp_path): assert metadata["model"] == sim.model.to_dict(initial_condition_serializer=sim._serialize_initial_condition) assert "species" not in metadata assert "propagator_options" not in metadata - assert {key: value for key, value in metadata["model"]["species"]["em_fields"]["variables"]["e_field"].items() if key != "initial_conditions"} == { + assert { + key: value + for key, value in metadata["model"]["species"]["em_fields"]["variables"]["e_field"].items() + if key != "initial_conditions" + } == { "class": "FEECVariable", "space": "Hcurl", "save_data": False, @@ -155,7 +169,9 @@ def test_run_metadata_contains_serialized_initial_conditions(tmp_path): } assert b_field["perturbations"]["type"] == "TorusModesCos" - kinetic = json.loads(kinetic_sim.to_run_metadata())["model"]["species"]["kinetic_ions"]["variables"]["var"]["initial_conditions"] + kinetic = json.loads(kinetic_sim.to_run_metadata())["model"]["species"]["kinetic_ions"]["variables"]["var"][ + "initial_conditions" + ] assert kinetic["backgrounds"]["type"] == "Maxwellian3D" assert kinetic["initial_condition"]["type"] == "SumKineticBackground" assert kinetic["initial_condition"]["params"]["f1"]["params"]["n"][1]["type"] == "TorusModesCos" @@ -165,16 +181,15 @@ def test_run_metadata_embeds_user_function_source(tmp_path): sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(user_density_profile, None))) - density = json.loads(sim.to_run_metadata())["model"]["species"]["kinetic_ions"]["variables"]["var"]["initial_conditions"]["backgrounds"][ - "params" - ]["n"][0] + density = json.loads(sim.to_run_metadata())["model"]["species"]["kinetic_ions"]["variables"]["var"][ + "initial_conditions" + ]["backgrounds"]["params"]["n"][0] assert density["type"] == "python_function" assert density["name"] == "user_density_profile" assert "def user_density_profile" in density["source"] assert len(density["source_sha256"]) == 64 - def test_from_output_restores_embedded_initial_condition_source(tmp_path, monkeypatch): path_out = tmp_path / "sim_1" path_out.mkdir() @@ -273,7 +288,8 @@ def test_cold_plasma_vlasov_species_and_variables_own_their_metadata(tmp_path): assert species["thermal_elec"]["alpha"] == 3.0 assert species["thermal_elec"]["epsilon"] == 0.5 assert species["thermal_elec"]["variables"]["current"]["initial_conditions"] == { - "backgrounds": None, "perturbations": None + "backgrounds": None, + "perturbations": None, } assert species["hot_elec"]["loading_params"]["Np"] == 1234 assert species["hot_elec"]["variables"]["var"]["initial_conditions"]["initial_condition"] is None From e1cc5cd77cc341f1ae8fba810eee3ac450965b45 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 07:34:26 +0200 Subject: [PATCH 129/193] Removed legacy plots --- .../tests/test_plotting.py | 8 - pyproject.toml | 1 - src/struphy/diagnostics/console_diagn.py | 411 -------- src/struphy/diagnostics/diagn_tools.py | 907 +----------------- .../diagnostics/tests/test_diagn_tools.py | 25 - 5 files changed, 1 insertion(+), 1351 deletions(-) delete mode 100644 src/struphy/diagnostics/console_diagn.py diff --git a/postprocessing_external/tests/test_plotting.py b/postprocessing_external/tests/test_plotting.py index 9fb79bf36..4608f95ef 100644 --- a/postprocessing_external/tests/test_plotting.py +++ b/postprocessing_external/tests/test_plotting.py @@ -296,11 +296,3 @@ def test_sweep_rejects_invalid_step(tmp_path, step): with pytest.raises(ValueError, match="positive integer"): save_frames(phase_space(), tmp_path, view=View(x="e1", y="v1"), step=step) - -def test_legacy_scalar_plot_warns_and_still_renders(monkeypatch): - from struphy.diagnostics import diagn_tools - - monkeypatch.setattr(plt, "show", lambda: None) - with pytest.deprecated_call(match="diagn_tools.plot_scalars"): - diagn_tools.plot_scalars(np.arange(3), {"en_tot": np.array([1.0, 2.0, 3.0])}) - assert plt.get_fignums() diff --git a/pyproject.toml b/pyproject.toml index b83b22640..955937c55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,6 @@ changelog = "https://github.com/struphy-hub/struphy/blob/devel/CHANGELOG.md" [project.scripts] struphy = "struphy.console.main:struphy" -kinetic-diagnostics = "struphy.diagnostics.console_diagn:main" [tool.setuptools.package-data] "struphy.fields_background.mhd_equil.eqdsk" = [ diff --git a/src/struphy/diagnostics/console_diagn.py b/src/struphy/diagnostics/console_diagn.py deleted file mode 100644 index fe2530ef4..000000000 --- a/src/struphy/diagnostics/console_diagn.py +++ /dev/null @@ -1,411 +0,0 @@ -"""An executable for quick access to the diagnostic tools in diagn_tools.py""" - -#!/usr/bin/env python3 -import argparse -import logging -import os -import subprocess - -import cunumpy as xp -import h5py -import yaml - -import struphy -import struphy.utils.utils as utils -from struphy.diagnostics.diagn_tools import plot_distr_fun, plot_scalars, plots_videos_2d - -logger = logging.getLogger("struphy") - - -def main(): - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "actions", - nargs="+", - type=str, - default=[None], - help="""which actions to perform:\ - \n - plot_scalars : plots the scalar quantities that were saved during the simulation\ - \n - plot_distr : plots the distribution function and delta-f (if available)\ - \n set points for slicing with options below (default is middle of the space)\ - \n - 2d_video : make a video of the distribution function (minus the background) in a 2D slice of phase space\ - \n - 2d_plots : plots an overview of the distribution function (minus the background) in a 2D slice of phase space\ - \n for up to 8 different points in time. - """, - ) - parser.add_argument( - "-f", - nargs=1, - type=str, - default=["sim_1"], - help="name of the folder for the simulation data (in io/out)", - ) - parser.add_argument( - "-scalars", - nargs="+", - action="append", - default=[], - help="(for plot_scalars) which quantities to plot", - ) - parser.add_argument( - "--full-f", - action="store_true", - help="whether to plot full-f instead of delta-f data", - ) - parser.add_argument( - "-slices", - nargs="+", - action="append", - default=[], - help="(for 2d_plots & 2d_video) which slices to plot / make a video for", - ) - parser.add_argument( - "--log", - action="store_true", - help="(for plot_scalars) if logarithmic y-axis should be used", - ) - parser.add_argument( - "--show", - action="store_true", - help="(for plot_scalars and 2d_plots) if the plot should be shown", - ) - parser.add_argument( - "-times", - nargs=1, - type=int, - default=[6], - help="(for 2_plots) at how many points in time should be plotted (default=6)", - ) - parser.add_argument( - "--nosave", - action="store_true", - help="(for plot_scalars) if the plot should not be displayed", - ) - parser.add_argument( - "--fit", - action="store_true", - help="(for plot_scalars) if a fit should be done (using maxima)", - ) - parser.add_argument( - "--minfit", - action="store_true", - help="(for plot_scalars) if a fit should be done using minima", - ) - parser.add_argument( - "-degree", - nargs=1, - type=int, - default=[1], - help="(for plot_scalars --fit) the degree of the fit curve (default=1)", - ) - parser.add_argument( - "-extrema", - nargs=1, - type=int, - default=[4], - help="(for plot_scalars --fit) how many extrema should be used for the fit (default=4)", - ) - parser.add_argument( - "-startextr", - nargs=1, - type=int, - default=[0], - help="(for plot_scalars --fit) which extremum should be used first for the fit (0 = first)", - ) - parser.add_argument( - "-order", - nargs=1, - type=int, - default=[4], - help="(for plot_scalars --fit) how many neighbouring points should be used for determining the extrema", - ) - parser.add_argument( - "-t", - nargs=1, - type=float, - default=[0.0], - help="(for plot_distr) at which time to plot the distribution function", - ) - parser.add_argument( - "-e1", - nargs=1, - type=float, - default=[0.5], - help="(for plot_distr) at which position in eta1 direction to plot", - ) - parser.add_argument( - "-e2", - nargs=1, - type=float, - default=[0.5], - help="(for plot_distr) at which position in eta2 direction to plot", - ) - parser.add_argument( - "-e3", - nargs=1, - type=float, - default=[0.5], - help="(for plot_distr) at which position in eta3 direction to plot", - ) - parser.add_argument( - "-v1", - nargs=1, - type=float, - default=[None], - help="(for plot_distr) at which point in v1 direction to plot", - ) - parser.add_argument( - "-v2", - nargs=1, - type=float, - default=[None], - help="(for plot_distr) at which point in v2 direction to plot", - ) - parser.add_argument( - "-v3", - nargs=1, - type=float, - default=[None], - help="(for plot_distr) at which point in v3 direction to plot", - ) - - # Parse the arguments - args = parser.parse_args() - actions = args.actions - foldername = args.f[0] - time = args.t[0] - plot_full_f = args.full_f - do_log = args.log - show = args.show - n_times = args.times[0] - nosave = args.nosave - if len(args.scalars) != 0: - scalars_plot = args.scalars[0] - else: - scalars_plot = args.scalars - if len(args.slices) != 0: - slices_plot = args.slices[0] - else: - slices_plot = args.slices - - # Arguments for fitting - do_fit = args.fit - fit_minima = args.minfit - if fit_minima and do_fit: - do_fit = False - no_extrema = args.extrema[0] - order = args.order[0] - degree = args.degree[0] - start_extremum = args.startextr[0] - - # Read struphy state file - state = utils.read_state() - - o_path = state["o_path"] - - path = os.path.join(o_path, foldername) - - grid_slices = { - "e1": args.e1[0], - "e2": args.e2[0], - "e3": args.e3[0], - "v1": args.v1[0], - "v2": args.v2[0], - "v3": args.v3[0], - } - - # Get fields - file = h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") - saved_scalars = file["scalar"] - saved_time = file["time"]["value"][:] - - # read in parameters - with open(path + "/parameters.yml") as file: - params = yaml.load(file, Loader=yaml.FullLoader) - - # Get model name - with open(path + "/meta.txt", "r") as file: - for line in file.readlines(): - if line[0:10] == "model_name": - model_name = line.split(":")[1].strip() - - if "plot_scalars" in actions: - plot_scalars( - time=saved_time, - scalar_quantities=saved_scalars, - scalars_plot=scalars_plot, - do_log=do_log, - do_fit=do_fit, - fit_minima=fit_minima, - order=order, - no_extrema=no_extrema, - degree=degree, - show_plot=show, - start_extremum=start_extremum, - save_plot=not nosave, - savedir=path, - ) - - if ("plot_distr" in actions) or ("2d_video" in actions) or ("2d_plots" in actions): - # Do post-processing if it wasn't done before - if not os.path.exists(os.path.join(path, "post_processing")): - logger.info("This folder hasn't been post-processed yet. Starting post-processing..") - subprocess.run(["struphy", "pproc", "-d", foldername]) - - # iterate over species - for species in params["kinetic"].keys(): - # Get model class - import struphy.models as models - - try: - model_class = getattr(models, model_name) - except AttributeError: - raise ModuleNotFoundError(f"{model_name} not found!") - - # get particles class name - species_dict = model_class.species() - particles_class_name = species_dict["kinetic"][species] - - # Get default background of particles class - from struphy.pic import particles - - default_bckgr_type = getattr( - particles, - particles_class_name, - ).default_bckgr_params() - - # Get default background parameters - from struphy.kinetic_background import maxwellians - - bckgr_fun = None - if "background" in params["kinetic"][species]: - bckgr_type = params["kinetic"][species]["background"] - - for fi, maxw_params in bckgr_type.items(): - if fi[-2] == "_": - fi_type = fi[:-2] - else: - fi_type = fi - - if bckgr_fun is None: - bckgr_fun = getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - ) - else: - bckgr_fun = bckgr_fun + getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - ) - else: - bckgr_fun = getattr(maxwellians, default_bckgr_type)() - - # Get values of background shifts in velocity space - positions = [xp.array([grid_slices["e" + str(k)]]) for k in range(1, 4)] - u = bckgr_fun.u(*positions) - eval_params = {"u" + str(k + 1): u[k][0] for k in range(3)} - - # Set velocity point of evaluation to velocity shift if not given by input - for k in range(1, 4): - if grid_slices["v" + str(k)] is None: - key = "u" + str(k) - if key in eval_params.keys(): - grid_slices["v" + str(k)] = eval_params[key] - - # Plot the distribution function - if "plot_distr" in actions: - # Get index of where to plot in time - time_idx = xp.argmin(xp.abs(time - saved_time)) - - plot_distr_fun( - path=os.path.join( - path, - "post_processing", - "kinetic_data", - species, - ), - time_idx=time_idx, - grid_slices=grid_slices, - save_plot=True, - savepath=path, - ) - - # Create a video of the phase space - if ("2d_video" in actions) or ("2d_plots" in actions): - for slice_name in os.listdir( - os.path.join( - path, - "post_processing", - "kinetic_data", - species, - "distribution_function", - ), - ): - for action in actions: - output = None - if action == "2d_video": - output = "video" - elif action == "2d_plots": - output = "overview" - else: - continue - - slice_name_given, polar_params = do_plot_and_if_polar( - slices_plot=slices_plot, - slice_name=slice_name, - geometry_params=params["geometry"], - ) - - if slice_name_given: - plots_videos_2d( - t_grid=saved_time, - grid_slices=grid_slices, - slice_name=slice_name, - plot_full_f=plot_full_f, - species=species, - path=path, - model_name=model_name, - output=output, - background_params=params["kinetic"][species]["background"], - n_times=n_times, - show_plot=show, - save_plot=not nosave, - polar_params=polar_params, - ) - - file.close() - - -def do_plot_and_if_polar(slices_plot, slice_name, geometry_params): - """Helper function to determine if a given slice should be plotted, and if yes, wether in polar coords. - - Parameters - ---------- - """ - slice_name_given = False - if slices_plot != []: - if slice_name in slices_plot: - slice_name_given = True - else: - slice_name_given = True - - polar_params = {} - - do_polar = False - geom_type = geometry_params["type"] - if geom_type == "HollowCylinder": - if slice_name == "e1_e2": - do_polar = True - polar_params["radial_coord"] = "e1" - polar_params["r_min"] = geometry_params[geom_type]["a1"] - polar_params["r_max"] = geometry_params[geom_type]["a2"] - polar_params["angular_coord"] = "e2" - - polar_params["do_polar"] = do_polar - - return slice_name_given, polar_params - - -if __name__ == "__main__": - main() diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 18da5785f..373f7c224 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -1,17 +1,7 @@ #!/usr/bin/env python3 -"""Spectral diagnostics and deprecated plotting helpers for legacy output. - -Use xarray and Matplotlib for new plotting code. -The legacy distribution/video helpers read the old NPY layout, not output.nc. -``power_spectrum_2d`` remains supported by the analysis accessor. -""" +"""Spectral diagnostics for labeled xarray output.""" import logging -import os -import shutil -import subprocess -import warnings -from functools import wraps import cunumpy as xp import matplotlib.colors as colors @@ -21,66 +11,9 @@ from scipy.signal import argrelextrema from struphy.dispersion_relations import analytic -from struphy.utils.progress import tqdm logger = logging.getLogger("struphy") - -def _legacy_plot(replacement): - def decorate(function): - @wraps(function) - def wrapped(*args, **kwargs): - warnings.warn( - f"diagn_tools.{function.__name__} is deprecated; use {replacement}. " - "Open new output with Output(path); legacy file-based helpers require the old NPY layout.", - DeprecationWarning, - stacklevel=2, - ) - return function(*args, **kwargs) - - return wrapped - - return decorate - - -def _accept_legacy_values(function): - """Also accept the call ``function(values, name, grids, grids_mapped=None, ...)`` of earlier versions. - - ``values`` maps time to the list of components of a field, as ``sim.spline_values...data`` - of :meth:`Simulation.load_plotting_data`. It is converted to a field with a logical and, if - ``grids_mapped`` is given, a physical fft coordinate. - """ - - def from_legacy_values(values, name, grids, grids_mapped=None, **kwargs): - times = sorted(values) - data = xp.stack([xp.stack([xp.asarray(comp) for comp in values[t]]) for t in times]) - coords = {"t": times, "component": xp.arange(data.shape[1])} - coords.update({f"e{n}": xp.asarray(grid) for n, grid in enumerate(grids, 1)}) - if grids_mapped is not None: - coords.update({X: (("e1", "e2", "e3"), xp.asarray(grid)) for X, grid in zip("XYZ", grids_mapped)}) - field = xr.DataArray(data, dims=("t", "component", "e1", "e2", "e3"), coords=coords, name=name) - return function(field, physical=grids_mapped is not None, **kwargs) - - @wraps(function) - def wrapped(field, *args, **kwargs): - if not isinstance(field, dict): - return function(field, *args, **kwargs) - warnings.warn( - f"diagn_tools.{function.__name__}(values, name, grids, ...) is deprecated; pass a field of an Output.\n" - "How to update your script, with out = sim.output:\n" - " power_spectrum_2d(E_of_t, 'e_field_log', grids=sim.grids_log, grids_mapped=sim.grids_phy, ...)\n" - " -> apply the diagnostic directly to out.fields.em_fields.e_field_log\n" - "'physical=True' replaces 'grids_mapped'; 'grids' and 'name' are read from the field itself. " - "Take the field from out.with_time_units('normalized') if you compare with normalized dispersion relations.", - DeprecationWarning, - stacklevel=2, - ) - return from_legacy_values(field, *args, **kwargs) - - return wrapped - - -@_accept_legacy_values def power_spectrum_2d( field: xr.DataArray, component: int = 0, @@ -293,841 +226,3 @@ def fun(k): return omega, kvec, dispersion, coeffs - -@_legacy_plot("xarray.DataArray.plot()") -def plot_scalars( - time, - scalar_quantities, - scalars_plot=None, - do_log=False, - do_fit=False, - fit_minima=False, - order=4, - no_extrema=4, - start_extremum=0, - degree=1, - show_plot=False, - save_plot=False, - savedir=None, - file_format="png", -): - """Plot the scalar quantities and the relative error in the total energy for a simulation. - - Parameters - ---------- - scalar_quantities : dict - HDF5 dictionary dataset containing the scalar quantities that were saved during the simulation - - scalars_plot : list | tuple - list of names of scalars that should be plotted. If empty then all are plotted - - do_log : boolean - Do a logarithmic plot in the y-axis if True. - - do_fit : boolean - Do a fit to maxima if True. - - fit_minima : boolean - Do a fit to minima if True. Will set do_fit to False if True. - - order : int - How many neighbouring points should be used for finding extrema. - - no_extrema : int - How many extrema should be used for the fit. - - start_extremum : int - Which extremum should be used first for the fit. - - show_plot : boolean - Display the figure if True. - - save_plot : boolean - Save the figure if True. Then a path has to be given. - - savedir : str - Name of the folder in which the plot of the result should be saved. - - file_format : str - Type of file which the plot of the result should be saved. - """ - - # Only have one of the two as True - if fit_minima and do_fit: - do_fit = False - - if "en_tot" in scalar_quantities.keys(): - en_tot = scalar_quantities["en_tot"][:] - - plt.figure("en_tot") - if do_log: - plt.semilogy(time, en_tot) - else: - plt.plot(time, en_tot) - - if save_plot: - assert savedir is not None, "When wanting to save the plot a path has to be given!" - plt.savefig(os.path.join(savedir, "en_tot" + "." + file_format)) - else: - plt.show() - - plt.figure("en_tot_rel_err") - plt.plot( - time[1:], - xp.divide( - xp.abs(en_tot[1:] - en_tot[0]), - en_tot[0], - ), - ) - - if save_plot: - assert savedir is not None, "When wanting to save the plot a path has to be given!" - plt.savefig( - os.path.join( - savedir, - "en_tot_rel_err" + "." + file_format, - ), - ) - if show_plot: - plt.show() - - # Dict with label as key and time series as value - plot_quantities = {} - if scalars_plot is None: - for key, quantity in scalar_quantities.items(): - if key not in ["time", "en_tot"]: - plot_quantities[key] = quantity[:] - else: - for key in scalars_plot: - plot_quantities[key] = scalar_quantities[key][:] - - # Make the figure - plt.figure("scalars") - for key, plot_quantity in plot_quantities.items(): - # Get the indices of the extrema - if do_fit: - inds_exs = argrelextrema(plot_quantity, xp.greater, order=order) - elif fit_minima: - inds_exs = argrelextrema(plot_quantity, xp.less, order=order) - else: - inds_exs = None - - if inds_exs is not None: - # Get x-values and y-values of data to fit to - quantity_extrema = plot_quantity[inds_exs][start_extremum : start_extremum + no_extrema] - times_extrema = time[inds_exs][start_extremum : start_extremum + no_extrema] - - # for plotting take a bit more time at start and end - if len(inds_exs[0]) >= 2: - time_start_idx = xp.max( - [0, 2 * inds_exs[0][start_extremum] - inds_exs[0][start_extremum + 1]], - ) - time_end_idx = xp.min( - [ - len(time) - 1, - 2 * inds_exs[0][start_extremum + no_extrema - 1] - inds_exs[0][start_extremum + no_extrema - 2], - ], - ) - time_cut = time[time_start_idx:time_end_idx] - else: - time_cut = time - - if do_log: - # plot quantity, extrema, and fit - plt.semilogy(time, plot_quantity[:], ".", label=key, markersize=2) - - if inds_exs is not None: - # do the fitting - coeffs = xp.polyfit( - times_extrema, - xp.log( - quantity_extrema, - ), - deg=degree, - ) - plt.plot( - times_extrema, - quantity_extrema, - "r*", - label="local extrema", - ) - plt.plot( - time_cut, - xp.exp(coeffs[0] * time_cut + coeffs[1]), - label=r"$a * \exp(m x)$ with" + f"\na={xp.round(xp.exp(coeffs[1]), 3)} m={xp.round(coeffs[0], 3)}", - ) - else: - plt.plot(time, plot_quantity[:], ".", label=key, markersize=2) - - if inds_exs is not None: - # do the fitting - coeffs = xp.polyfit( - times_extrema, - quantity_extrema, - deg=degree, - ) - - # plot quantity, extrema, and fit - plt.plot( - times_extrema, - quantity_extrema, - "r*", - label="local extrema", - ) - plt.plot( - time_cut, - xp.exp(coeffs[0] * time_cut + coeffs[1]), - label=r"$a x + b$ with" + f"\na={xp.round(coeffs[1], 3)} b={xp.round(coeffs[0], 3)}", - ) - - plt.legend() - plt.xlabel("time") - - if save_plot: - assert savedir is not None, "When wanting to save the plot a path has to be given!" - plt.savefig(os.path.join(savedir, "scalars" + "." + file_format)) - if show_plot: - plt.show() - - -@_legacy_plot("xarray.DataArray.plot() after selection") -def plot_distr_fun( - path, - time_idx, - grid_slices, - save_plot=False, - savepath=None, - file_format="png", -): - """Plot the binned distribution function at given slices of the phase space. - - Parameters - ---------- - path : str - Path to the kinetic data of the species. - - time : float - at which point in time to plot - - grid_slices : dict - dictionary with keys e and v that hold dictionaries with directions and values - that indicate which slices of the data should be plotted - - save_plot : boolean - Save figure if True. Then a path has to be given. - - savepath : str - Path under which the plot of the result should be saved. - - file_format : str - Type of file which the plot of the result should be saved. - """ - - species = str(path.split("/")[-1]) - path = os.path.join(path, "distribution_function") - - # Loop over folders and plot for each of them - for folder in os.listdir(path): - grids = [] - f = None - delta_f = None - - subpath = os.path.join(path, folder) - - # Loop over the files in this subdirectory - for filename in os.listdir(subpath): - filepath = os.path.join(subpath, filename) - - # load full distribution functions - if filename == "f_binned.npy": - f = xp.load(filepath) - - # load delta f - elif filename == "delta_f_binned.npy": - delta_f = xp.load(filepath) - - assert f is not None, "No distribution function file found!" - - # Load grid - directions = folder.split("_") - for direction in directions: - grids += [ - xp.load( - os.path.join( - subpath, - "grid_" + direction + ".npy", - ), - ), - ] - - # Get indices of where to plot in other directions - grid_idxs = {} - for k in range(f.ndim - 1): - grid_idxs[directions[k]] = xp.argmin( - xp.abs(grids[k] - grid_slices[directions[k]]), - ) - - for k in range(f.ndim - 1): - # Prepare slicing - f_slicing = [0] * f.ndim - # time index - f_slicing[0] = time_idx - # direction in which to plot - f_slicing[k + 1] = slice(None) - # directions in which f is evaluated at a point - for j in range(1, f.ndim): - if j == k + 1: - continue - f_slicing[j] = grid_idxs[directions[k]] - - # plot delta_f - if delta_f is not None: - plt.figure("delta_f") - plt.plot(grids[k], delta_f[tuple(f_slicing)].squeeze()) - plt.xlabel(directions[k]) - plt.ylabel(r"$\delta f$") - plt.title(f"time step n={time_idx}") - logger.info(f"Created plot for delta_f in {directions[k]}") - - if save_plot: - assert savepath is not None, "When wanting to save the plot a path has to be given!" - savename = os.path.join( - savepath, - species + "_delta_f_" + directions[k] + "." + file_format, - ) - plt.savefig(savename) - else: - plt.show() - plt.close() - - # plot full f - if f is not None: - plt.figure("f") - plt.plot(grids[k], f[tuple(f_slicing)].squeeze()) - plt.xlabel(directions[k]) - plt.ylabel(r"$f$") - plt.title(f"time step n={time_idx}") - logger.info(f"Created plot for f in {directions[k]}") - - if save_plot: - assert savepath is not None, "When wanting to save the plot a path has to be given!" - savename = os.path.join( - savepath, - species + "_f_" + directions[k] + "." + file_format, - ) - plt.savefig(savename) - else: - plt.show() - plt.close() - - del grids - del f - del delta_f - - -@_legacy_plot("xarray faceting after selection") -def plots_videos_2d( - t_grid, - grid_slices, - slice_name, - plot_full_f, - species, - path, - model_name, - output: str = "overview", - background_params=None, - n_times=6, - show_plot=False, - save_plot=True, - polar_params={}, -): - """TODO""" - choices = ["overview", "video"] - assert output in choices, f"Can only do one of {choices=} but got {output=}" - - # Make sure that the slice that was saved during the simulation is at least 2D - if "_" not in slice_name: - return - - if polar_params == {}: - do_polar = False - else: - do_polar = polar_params["do_polar"] - - data_path = os.path.join( - path, - "post_processing", - "kinetic_data", - species, - "distribution_function", - slice_name, - ) - - # Create a folder for the diagnostics - diagn_path = os.path.join(path, "diagnostics") - if (output == "overview" and save_plot) or output == "video": - if not os.path.exists(diagn_path): - os.mkdir(diagn_path) - - slices_2d, grids, directions, df_data = get_slices_grids_directions_and_df_data( - plot_full_f=plot_full_f, - background_params=background_params, - grid_slices=grid_slices, - data_path=data_path, - slice_name=slice_name, - ) - - # Make plot series for each 2D slice - for slc in slices_2d: - # Assign some nicer names - label_1 = slc[:2] - label_2 = slc[-2:] - - # Only needed for "video" option - images_path = None - if output == "video": - # Create folder for saving the images series - images_path = os.path.join( - diagn_path, - "video_frames_" + slc, - ) - if os.path.exists(images_path): - shutil.rmtree(images_path) - - os.mkdir(images_path) - - # Get indices of where to plot in other directions - grid_idxs = {} - for k in range(df_data.ndim - 1): - direc = directions[k] - grid_idxs[direc] = xp.argmin( - xp.abs(grids[direc] - grid_slices[direc]), - ) - - grid_1 = xp.load( - os.path.join( - data_path, - "grid_" + label_1 + ".npy", - ), - ) - grid_2 = xp.load( - os.path.join( - data_path, - "grid_" + label_2 + ".npy", - ), - ) - - # Prepare slicing - f_slicing = [0] * df_data.ndim - for k in range(df_data.ndim): - # directions in which f is evaluated at a point - if directions[k - 1] in slc: - f_slicing[k] = slice(None) - else: - f_slicing[k] = grid_idxs[directions[k - 1]] - - df_binned = df_data[tuple(f_slicing)].squeeze() - - assert t_grid.ndim == grid_1.ndim == grid_2.ndim == 1, "Input arrays must be 1D!" - assert df_binned.shape[0] == t_grid.size, f"{df_binned.shape =}, {t_grid.shape =}" - assert df_binned.shape[1] == grid_1.size, f"{df_binned.shape =}, {grid_1.shape =}" - assert df_binned.shape[2] == grid_2.size, f"{df_binned.shape =}, {grid_2.shape =}" - - # Scale the coordinates to cartesian sizes for plot to be more obvious - if do_polar: - for sl, var in zip([label_1, label_2], [grid_1, grid_2]): - if sl in polar_params.values(): - if polar_params["radial_coord"] == sl: - var *= polar_params["r_max"] - polar_params["r_min"] - var += polar_params["r_min"] - elif polar_params["angular_coord"] == sl: - var *= 2 * xp.pi - - grid_1_mesh, grid_2_mesh = xp.meshgrid(grid_1, grid_2, indexing="ij") - - if output == "video": - plots_2d_video( - t_grid=t_grid, - grid_1_mesh=grid_1_mesh, - grid_2_mesh=grid_2_mesh, - df_binned=df_binned, - model_name=model_name, - label_1=label_1, - label_2=label_2, - do_polar=do_polar, - images_path=images_path, - ) - - video_2d( - slc=slc, - diagn_path=diagn_path, - images_path=images_path, - ) - - elif output == "overview": - plots_2d_overview( - t_grid=t_grid, - grid_1_mesh=grid_1_mesh, - grid_2_mesh=grid_2_mesh, - slc=slc, - df_binned=df_binned, - save_path=diagn_path, - model_name=model_name, - label_1=label_1, - label_2=label_2, - do_polar=do_polar, - n_times=n_times, - show_plot=show_plot, - save_plot=save_plot, - ) - - else: - raise NotImplementedError(f"{output=} is not implemented!") - - -@_legacy_plot("Matplotlib figure saving") -def video_2d(slc, diagn_path, images_path): - """Create a video of all 2D slices of the distribution function over time. - - Parameters - ---------- - t_grid : xp.ndarray - 1D-array containing all the times - - grid_slices : dict - holds the names of the directions as keys and the values at where the function should - be evaluated as values - - slice_name : str - The name of the slicing, e.g. e2_v1_v2 - - plot_full_f : bool - whether to plot full-f instead of delta-f data - - species : str - the name of the species - - path : str - the path to the data of which the videos should be created - - model_name : str - name of the model that was run - - background_params : dict [optional] - parameters of the maxwellian background type if a full_f method was used - """ - - try: - import cv2 - except: - yn = input( - "It seems like cv2 is not installed. Would you like to install it now (Y/n)?", - ) - - if yn in ("", "Y", "y", "yes", "Yes"): - subprocess.run( - ["python3", "-m", "pip", "install", "opencv-python"], - ) - else: - return - - images = [ - img - for img in sorted( - os.listdir(images_path), - ) - if img.endswith(".png") - ] - frame = cv2.imread(os.path.join(images_path, images[0])) - height, width, _ = frame.shape - - fps = 15 - video = cv2.VideoWriter( - os.path.join( - diagn_path, - "video_" + slc + ".avi", - ), - 0, - fps, - (width, height), - ) - - logger.info("Creating video now") - for image in tqdm(images): - video.write(cv2.imread(os.path.join(images_path, image))) - - cv2.destroyAllWindows() - video.release() - - -@_legacy_plot("xarray faceting after selection") -def plots_2d_video( - t_grid, - grid_1_mesh, - grid_2_mesh, - df_binned, - model_name, - label_1=None, - label_2=None, - do_polar=False, - images_path=None, -): - # Best color scheme - cmap = "seismic" - - vmin = [] - vmax = [] - - # Get parameters for time and labelling for it - nt = len(t_grid) - log_nt = int(xp.log10(nt)) + 1 - len_dt = len(str(t_grid[1]).split(".")[1]) - - # Get the correct scale for the plots - vmin += [xp.min(df_binned[:]) / 3] - vmax += [xp.max(df_binned[:]) / 3] - vmin = xp.min(vmin) - vmax = xp.max(vmax) - vscale = xp.max(xp.abs([vmin, vmax])) - - # Set up the figure and axis once - if do_polar: - fig, ax = plt.subplots(figsize=(9, 9), subplot_kw=dict(projection="polar")) - im = ax.pcolormesh(grid_2_mesh, grid_1_mesh, df_binned[0], cmap=cmap, vmin=-vscale, vmax=vscale) - else: - fig, ax = plt.subplots(figsize=(9, 9)) - im = ax.pcolormesh(grid_1_mesh, grid_2_mesh, df_binned[0], cmap=cmap, vmin=-vscale, vmax=vscale) - - # Create the colorbar once - fig.colorbar(im, ax=ax) - - for k in tqdm(range(nt)): - obj = plt - t = f"%.{len_dt}f" % t_grid[k] - - # Set the title including the time - fig.suptitle(rf"Struphy model '{model_name}', $t=${t}") - - # Update the plot data. pcolormesh returns a QuadMesh; update its array. - # Note: set_array expects a 1D array, so we flatten the data. - im.set_array(df_binned[k].ravel()) - - # Force a re-draw of the canvas - fig.canvas.draw_idle() - - # Only add axis labels for non-polar plots since it confuses - if not do_polar: - if label_1[0] == "e": - obj.xlabel(rf"$\eta_{label_1[-1]}$") - else: - obj.xlabel(rf"$v_{label_1[-1]}$") - if label_2[0] == "e": - obj.ylabel(rf"$\eta_{label_2[-1]}$") - else: - obj.ylabel(rf"$v_{label_2[-1]}$") - - # Save the current frame - plt.savefig( - os.path.join( - images_path, - "step_{0:0{1}d}.png".format(k, log_nt), - ), - bbox_inches="tight", - dpi=150, - ) - - # Clear the figure - plt.clf() - - plt.close("all") - - -@_legacy_plot("xarray faceting after selection") -def plots_2d_overview( - t_grid, - grid_1_mesh, - grid_2_mesh, - slc, - df_binned, - save_path, - model_name, - label_1=None, - label_2=None, - do_polar=False, - n_times=1, - show_plot=False, - save_plot=True, -): - # Best color scheme - cmap = "seismic" - - times = [] - for k in range(n_times): - times += [int((len(t_grid) - 1) * k / n_times)] - - # Get parameters for time and labelling for it - len_dt = len(str(t_grid[1]).split(".")[1]) - - # Assign some values and change them below - vmin = [] - vmax = [] - n_rows = 1 - n_cols = 1 - fig_size = (1, 1) - fig_height = 1 - - # Make nice layout for subplots - if n_times in [1, 2, 3]: - n_cols = n_times - n_rows = 1 - fig_height = 4.5 - elif n_times == 4: - n_cols = 2 - n_rows = 2 - fig_height = 8.5 - else: - n_cols = 3 - n_rows = int(xp.ceil(n_times / n_cols)) - fig_height = 4 * n_rows - - fig_size = (4 * n_cols, fig_height) - - # Get the correct scale for the plots - for time in times: - vmin += [xp.min(df_binned[time]) / 3] - vmax += [xp.max(df_binned[time]) / 3] - vmin = xp.min(vmin) - vmax = xp.max(vmax) - vscale = xp.max(xp.abs([vmin, vmax])) - - # Plot options for polar plots - subplot_kw = dict(projection="polar") if do_polar else None - - # Create figure - fig, axes = plt.subplots(n_rows, n_cols, figsize=fig_size, subplot_kw=subplot_kw) - - # So we an use .flatten() even for just 1 plot - if not isinstance(axes, xp.ndarray): - axes = xp.array([axes]) - - # fig.tight_layout(h_pad=5.0, w_pad=5.0) - # fig.tight_layout(pad=5.0) - plt.subplots_adjust( - left=0.05, - bottom=0.1, - right=0.85, - top=0.9, - wspace=0.3, - hspace=0.35, - ) - - # Set the suptitle - fig.suptitle(f"Struphy model '{model_name}'") - - for k in xp.arange(n_times): - obj = axes.flatten()[k] - n = times[k] - t = f"%.{len_dt}f" % t_grid[n] - - obj.title.set_text(rf"$t=${t}") - - # Plot the data - if not do_polar: - im = obj.pcolor(grid_1_mesh, grid_2_mesh, df_binned[n], cmap=cmap, vmin=-vscale, vmax=vscale) - else: - im = obj.pcolor(grid_2_mesh, grid_1_mesh, df_binned[n], cmap=cmap, vmin=-vscale, vmax=vscale) - - # Only add axis labels for non-polar plots since it confuses - if not do_polar: - if label_1[0] == "e": - obj.set_xlabel(rf"$\eta_{label_1[-1]}$") - else: - obj.set_xlabel(rf"$v_{label_1[-1]}$") - if label_2[0] == "e": - obj.set_ylabel(rf"$\eta_{label_2[-1]}$") - else: - obj.set_ylabel(rf"$v_{label_2[-1]}$") - - # Add global colorbar - cbar_ax = fig.add_axes([0.9, 0.1, 0.02, 0.7]) - plt.colorbar(im, cax=cbar_ax) - - if save_plot: - plt.savefig( - os.path.join( - save_path, - "overview_" + slc + ".png", - ), - dpi=150, - ) - - if show_plot: - plt.show() - - # Clear the figure - plt.clf() - - plt.close("all") - - -def get_slices_grids_directions_and_df_data(plot_full_f, grid_slices, data_path, slice_name, background_params=None): - """Prepare the lists of slices, grids, and directions from the given data and extract the delta-f data. - - Parameters - ---------- - plot_full_f : bool - whether to plot full-f instead of delta-f data - - grid_slices : dict - holds the names of the directions as keys and the values at where the function should - be evaluated as values - - data_path : str - the path to the data which should be prepared - - slice_name : str - The name of the slicing, e.g. e2_v1_v2 - - background_params : dict [optional] - parameters of the maxwellian background type if a full_f method was used - - Returns - ------- - slices_2d : list[string] - A list of all the slicings - - grids : list[xp.ndarray] - A list of all grids according to the slices - - directions : list[string] - A list of the directions that appear in all slices - - df_data : xp.ndarray - The data of delta-f (in case of full-f: distribution function minus background) - """ - - directions = slice_name.split("_") - - # Load all the grids - grids = {} - for direction in directions: - grids[direction] = xp.load( - os.path.join(data_path, "grid_" + direction + ".npy"), - ) - - # If simulation was for full-f subtract the background function - if plot_full_f: - _name = "f_binned.npy" - else: - _name = "delta_f_binned.npy" - _data = xp.load(os.path.join(data_path, _name)) - - # Check how many slicings have been given and make slices_2d for all - # combinations of spatial and velocity dimensions - slices_2d = [] - for direc1 in directions: - for direc2 in directions: - if (direc1 != direc2) and (direc2 + "_" + direc1) not in slices_2d: - slices_2d += [direc1 + "_" + direc2] - - return slices_2d, grids, directions, _data diff --git a/src/struphy/diagnostics/tests/test_diagn_tools.py b/src/struphy/diagnostics/tests/test_diagn_tools.py index a5e5c52a5..d1146287b 100644 --- a/src/struphy/diagnostics/tests/test_diagn_tools.py +++ b/src/struphy/diagnostics/tests/test_diagn_tools.py @@ -45,28 +45,3 @@ def test_fitted_phase_speed(physical): def test_needs_exactly_one_fft_direction(): with pytest.raises(AssertionError, match="slice_at"): power_spectrum_2d(standing_waves(tend=1.0), slice_at=(None, None, 0)) - - -def test_legacy_call_with_a_dict_of_time_snapshots(): - field = standing_waves() - values = {float(t): list(snapshot) for t, snapshot in zip(field.t.values, field.values)} - grids_log = [field[dim].values for dim in ("e1", "e2", "e3")] - grids_phy = [field[dim].values for dim in ("X", "Y", "Z")] - - with pytest.deprecated_call(): - omega, kvec, dispersion, coeffs = power_spectrum_2d( - values, - "e_field_log", - grids=grids_log, - grids_mapped=grids_phy, - component=1, - slice_at=[0, 0, None], - fit_branches=1, - noise_level=0.5, - ) - assert coeffs[0][0] == pytest.approx(SPEED, rel=0.02) - with pytest.deprecated_call(): - *_, coeffs = power_spectrum_2d( - values, "e_field_log", grids_log, component=1, slice_at=[0, 0, None], fit_branches=1, noise_level=0.5 - ) - assert coeffs[0][0] == pytest.approx(SPEED / LENGTH, rel=0.02) From 11b41ac8a822897f549999475bd07bb9991f0310 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 07:41:14 +0200 Subject: [PATCH 130/193] Add plot of spectrum = out.dispersion(...); spectrum.power.plot(...) --- doc/markdown/output-api.md | 8 +++ src/struphy/post_processing/output.py | 52 +++++++++++++++++++ .../post_processing/tests/test_output.py | 7 +++ 3 files changed, 67 insertions(+) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 84b0d2097..6bab64cb8 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -122,6 +122,14 @@ peaks = out.envelope("electric_energy") growth = out.growth_rate(out.norm("diagnostics/rho", squared=True), amplitude=True) ``` +For a saved field, `dispersion()` computes a labeled space-time power spectrum without +creating a figure: + +```python +spectrum = out.dispersion("em_fields/e_field", component=0, slice_at=(0, 0, None)) +spectrum.power.plot(x="k", y="omega") +``` + Fields carry mapped `X`, `Y`, `Z` coordinates; binned products (such as `e1_e2_density`) do not. `with_physical_coords` attaches them by evaluating the run's domain on the array's logical grid. diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index b24dec8c4..9b2ce3d7e 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -263,6 +263,58 @@ def compare(first: "Output", second: "Output", product: str, *, method: str = "l ratio = xr.where(right != 0, left / right, np.nan) return xr.Dataset({"first": left, "second": right, "difference": difference, "ratio": ratio}) + def dispersion( + self, + name: str, + *, + dataset: str | None = None, + component: int = 0, + slice_at: tuple = (None, 0, 0), + physical: bool = False, + fit_branches: int = 0, + noise_level: float = 0.1, + extr_order: int = 10, + fit_degree: tuple[int, ...] = (1,), + ) -> xr.Dataset: + """Compute a space-time dispersion spectrum for one saved field. + + The returned dataset has ``power(omega, k)`` and angular-frequency/wave-number + coordinates. Optional polynomial branch fits are stored as ``branch_coefficients``. + Plotting is intentionally left to the optional xarray plotting package. + """ + from struphy.diagnostics.diagn_tools import power_spectrum_2d + + field = self._product(name, dataset=dataset) if dataset is not None else self._array(name) + omega, kvec, power, coefficients = power_spectrum_2d( + field, + component=component, + slice_at=slice_at, + physical=physical, + fit_branches=fit_branches, + noise_level=noise_level, + extr_order=extr_order, + fit_degree=fit_degree, + ) + result = xr.Dataset( + { + "power": (("omega", "k"), np.asarray(power)), + }, + coords={"omega": np.asarray(omega), "k": np.asarray(kvec)}, + attrs={"run": self.label, "run_name": self.path_out.name, "source": name}, + ) + result["omega"].attrs["long_name"] = "angular frequency" + result["k"].attrs["long_name"] = "wave number" + result["power"].attrs["long_name"] = "space-time power spectrum" + if coefficients: + width = max(len(np.asarray(values).ravel()) for values in coefficients) + fitted = np.full((len(coefficients), width), np.nan) + for index, values in enumerate(coefficients): + values = np.asarray(values).ravel() + fitted[index, : values.size] = values + result["branch_coefficients"] = (("branch", "coefficient"), fitted) + result = result.assign_coords(branch=np.arange(len(coefficients))) + return result + def _reset(self): if getattr(self, "_tree", None) is not None: self._tree.close() # an open store would block the next process() from writing it diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index a5eef8d05..379b3824c 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -311,6 +311,13 @@ def test_evaluate_scalars_and_particle_defaults(run): assert orbits.name == "orbits" +def test_output_dispersion_returns_labeled_dataset(run): + spectrum = run.dispersion("em_fields/E", component=0, slice_at=(None, 0, 0)) + assert set(spectrum.data_vars) == {"power"} + assert spectrum.power.dims == ("omega", "k") + assert spectrum.attrs["source"] == "em_fields/E" + + def test_info_lists_particle_dataset_choices_in_default_order(run, capsys): run.info("kinetic_ions/f") report = capsys.readouterr().out From 42ccfea4df122397a295d384ad8e6e54a304423c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 08:01:51 +0200 Subject: [PATCH 131/193] Moved the power spectrum logic to Output --- .../src/struphy_plots/accessors.py | 12 +- .../src/struphy_plots/output_accessors.py | 2 +- src/struphy/diagnostics/diagn_tools.py | 228 ------------------ .../diagnostics/tests/test_diagn_tools.py | 47 ---- .../verification/test_verif_LinearMHD.py | 23 +- .../tests/verification/test_verif_Maxwell.py | 9 +- src/struphy/post_processing/output.py | 22 +- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 21 +- tutorials/tutorial_maxwell.ipynb | 10 +- 9 files changed, 30 insertions(+), 344 deletions(-) delete mode 100644 src/struphy/diagnostics/diagn_tools.py delete mode 100644 src/struphy/diagnostics/tests/test_diagn_tools.py diff --git a/postprocessing_external/src/struphy_plots/accessors.py b/postprocessing_external/src/struphy_plots/accessors.py index 805ec68ce..4a6264024 100644 --- a/postprocessing_external/src/struphy_plots/accessors.py +++ b/postprocessing_external/src/struphy_plots/accessors.py @@ -465,7 +465,7 @@ def spatial_average(self, *, dims=None) -> xr.DataArray: """Mean over the logical space dimensions ``e1``, ``e2``, ``e3`` (or ``dims``). For a binned ``e1_v1`` distribution this is f(v1, t) averaged over space; see - :func:`struphy.diagnostics.analysis.spatial_average`. + :func:`struphy_plots.analysis.spatial_average`. """ from .analysis import spatial_average @@ -474,7 +474,7 @@ def spatial_average(self, *, dims=None) -> xr.DataArray: def velocity_moments(self, *, dims=None) -> xr.Dataset: """Density, mean velocity and variance of a binned distribution over its velocity dimensions. - See :func:`struphy.diagnostics.analysis.velocity_moments` for the definitions. + See :func:`struphy_plots.analysis.velocity_moments` for the definitions. """ from .analysis import velocity_moments @@ -484,13 +484,13 @@ def dispersion(self, *, component: int = 0, slice_at: tuple = (None, 0, 0), phys """Space-time power spectrum of this field and fitted dispersion branches. The time coordinate must be normalized, see :meth:`struphy.Output.with_time_units`. See - :func:`struphy.diagnostics.diagn_tools.power_spectrum_2d` for ``slice_at``, the fit options - and ``do_plot``. Returns ``(omega, kvec, spectrum, coeffs)``. + :func:`struphy.post_processing.spectral.compute_dispersion` for ``slice_at`` and fit options. + Returns an xarray Dataset with ``power(omega, k)``. """ - from struphy.diagnostics.diagn_tools import power_spectrum_2d + from struphy.post_processing.spectral import compute_dispersion if self._array.t.attrs.get("units") == "s": raise ValueError( "the spectrum needs normalized time; take the field from out.with_time_units('normalized')" ) - return power_spectrum_2d(self._array, component=component, slice_at=slice_at, physical=physical, **kwargs) + return compute_dispersion(self._array, component=component, slice_at=slice_at, physical=physical, **kwargs) diff --git a/postprocessing_external/src/struphy_plots/output_accessors.py b/postprocessing_external/src/struphy_plots/output_accessors.py index 406a2a7d2..4bc415474 100644 --- a/postprocessing_external/src/struphy_plots/output_accessors.py +++ b/postprocessing_external/src/struphy_plots/output_accessors.py @@ -19,7 +19,7 @@ class OutputPlots: """Plots of a whole run, constructed as ``OutputPlots(out)``. - They return a rendered :class:`~struphy.diagnostics.plotting.PlotResult` with ``.show()`` + They return plotting-library objects with ``.show()`` and ``.save(path)``, titled with the run's numerical parameters. Plots of one product are methods of that product, e.g. ``out.kinetic_ions.orbits.struphy.plot.trajectories()``. """ diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py deleted file mode 100644 index 373f7c224..000000000 --- a/src/struphy/diagnostics/diagn_tools.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -"""Spectral diagnostics for labeled xarray output.""" - -import logging - -import cunumpy as xp -import matplotlib.colors as colors -import matplotlib.pyplot as plt -import xarray as xr -from scipy.fft import fftfreq, fftn -from scipy.signal import argrelextrema - -from struphy.dispersion_relations import analytic - -logger = logging.getLogger("struphy") - -def power_spectrum_2d( - field: xr.DataArray, - component: int = 0, - slice_at: tuple = (None, 0, 0), - physical: bool = False, - do_plot: bool = False, - disp_name: str = None, - disp_params: dict = {}, - fit_branches: int = 0, - noise_level: float = 0.1, - extr_order: int = 10, - fit_degree: tuple = (1,), - save_plot: bool = False, - save_name: str = None, - file_format: str = "png", -): - """Perform fft in space-time, (t, x) -> (omega, k), where x can be a logical or physical coordinate. - Returns values if plot=False. - - Parameters - ---------- - field : xarray.DataArray - An evaluated FEEC field of a :class:`~struphy.Output`, with dims ``(t, [component,] e1, e2, e3)``, - e.g. ``run.fields.em_fields.e_field_log``. Its time coordinate must be uniform; use - ``run.with_time_units("normalized")`` to compare with normalized dispersion relations. - - component : int - Which component of the field to consider; ignored for fields without a component dimension. - - slice_at : 3-tuple - At which indices i, j the 1d slice data (t, eta)_(i, j) should be obtained. - One entry must be "None"; this is the direction of the fft. - Default: [None, 0, 0] performs the eta1-fft at (eta2[0], eta3[0]). - - physical : boolean - Perform the fft on the physical coordinate (X, Y or Z) along the fft direction instead of - on the logical one. The field must carry physical coordinates. - - do_plot : boolean - Plot result if True, otherwise return things. - - disp_name : str - The name of the dispersion relation class in struphy.dispersion_relations.analytic to be used for analytic - comparison. If None, only the computed spectrum is drawn. - - disp_params : dict - Parameters needed for analytical dispersion relation, see struphy.dispersion_relations.analytic. - - fit_branches: int - How many branches to fit in the dispersion relation. - Default=0 means no fits are made. - - noise_level: float - Sets the threshold above which local maxima in the power spectrum are taken into account. - Computed as threshold = max(spectrum) * noise_level. - - extr_oder: int - Order given to argrelextrema. - - fit_degree: tuple[int] - Degree of fitting polynomial for each branch (fit_branches) of power spectrum. - - save_plot : boolean - Save figure if True. Then a path has to be given. - - save_name : str - Name under which the plot of the result should be saved. - - file_format : str - Type of file which the plot of the result should be saved. - - Returns - ------- - omega : xp.array - 1d array of angular frequency. - - kvec : xp.array - 1d array of wave vector. - - dispersion : xp.array - 2d array of shape (omega.size, kvec.size) holding the fft. - - coeffs : list[list] - List of fitting coefficients (lenght is fit_branches). - """ - assert list(slice_at).count(None) == 1, 'Exactly one entry of slice_at must be "None".' - name = str(field.name) - if "component" in field.dims: - field = field.isel(component=component) - - # extract 2d data (t, eta) for fft - axis = list(slice_at).index(None) - along = ("e1", "e2", "e3")[axis] - fixed = {dim: index for dim, index in zip(("e1", "e2", "e3"), slice_at) if index is not None} - sliced = field.isel(fixed).transpose("t", along) - data = xp.asarray(sliced) - - # check uniform grid in time - time = xp.asarray(sliced.t) - dt = time[1] - time[0] - assert xp.allclose(time[1:] - time[:-1], dt, rtol=0.0, atol=1e-12 * max(1.0, abs(dt))), "time grid is not uniform" - - if physical: - grid = xp.asarray(sliced[("X", "Y", "Z")[axis]]) - else: - grid = xp.asarray(sliced[along]) - - # extract uniform grid in space - Nt = data.shape[0] - Nx = grid.size - dx = grid[1] - grid[0] - assert xp.allclose(grid[1:] - grid[:-1], dx * xp.ones_like(grid[:-1])) - - dispersion = (2.0 / Nt) * (2.0 / Nx) * xp.abs(fftn(data))[: Nt // 2, : Nx // 2] - kvec = 2 * xp.pi * fftfreq(Nx, dx)[: Nx // 2] - omega = 2 * xp.pi * fftfreq(Nt, dt)[: Nt // 2] - - coeffs = None - if fit_branches > 0: - assert len(fit_degree) == fit_branches - # determine maxima for each k - k_start = kvec.size // 8 # take only first half of k-vector - k_end = kvec.size // 2 # take only first half of k-vector - k_fit = [] - omega_fit = {} - for n in range(fit_branches): - omega_fit[n] = [] - for k, f_of_omega in zip(kvec[k_start:k_end], dispersion[:, k_start:k_end].T): - threshold = xp.max(f_of_omega) * noise_level - extrms = argrelextrema(f_of_omega, xp.greater, order=extr_order)[0] - above_noise = xp.nonzero(f_of_omega > threshold)[0] - intersec = list(set(extrms) & set(above_noise)) - # intersec = list(set(extrms)) - if not intersec: - continue - intersec.sort() - # logger.info(f"{intersec = }") - # logger.info(f"{[omega[intersec[n]] for n in range(fit_branches)]}") - assert len(intersec) == fit_branches, ( - f"Number of found branches {len(intersec)} is not {fit_branches =}! \ - Try to lower 'noise_level' or increase 'extr_order'." - ) - k_fit += [k] - for n in range(fit_branches): - omega_fit[n] += [omega[intersec[n]]] - - # fit - coeffs = [] - for m, om in omega_fit.items(): - coeffs += [xp.polyfit(k_fit, om, deg=fit_degree[n])] - logger.info(f"\nFitted {coeffs =}") - - if do_plot: - _, ax = plt.subplots(1, 1, figsize=(10, 10)) - colormap = "plasma" - K, W = xp.meshgrid(kvec, omega) - lvls = xp.logspace(-15, -1, 27) - disp_plot = ax.contourf( - K, - W, - dispersion**2 / (dispersion**2).max(), - cmap=colormap, - norm=colors.LogNorm(), - levels=lvls, - ) - plt.colorbar( - ticks=[1e-12, 1e-9, 1e-6, 1e-3], - mappable=disp_plot, - format="%.0e", - ) - title = name + ", component " + str(component + 1) - ax.set_title(title) - ax.set_xlabel("$k$ [a.u.]") - ax.set_ylabel(r"$\omega$ [a.u.]") - - if fit_branches > 0: - for n, cs in enumerate(coeffs): - - def fun(k): - out = k * 0.0 - for i, c in enumerate(xp.flip(cs)): - out += c * k**i - return out - - ax.plot(kvec, fun(kvec), "r:", label=f"fit_{n + 1}") - - # analytic solution, when a dispersion relation is given - set_min = set_max = 0.0 - if disp_name is not None: - disp = getattr(analytic, disp_name)(**disp_params) - - branches = disp(kvec) - for key, branch in branches.items(): - vals = xp.real(branch) - ax.plot(kvec, vals, "--", label=key) - set_min = min(set_min, xp.min(vals)) - set_max = max(set_max, xp.max(vals)) - else: - set_min, set_max = 0.0, omega[-1] - - ax.legend() - ax.set_xlim(0, kvec[-1]) - ax.set_ylim(set_min * 1.1, set_max * 1.1) - - if save_plot: - assert save_name is not None, "When wanting to save the plot a path has to be given!" - plt.savefig(save_name + "." + file_format) - else: - plt.show() - - return omega, kvec, dispersion, coeffs - diff --git a/src/struphy/diagnostics/tests/test_diagn_tools.py b/src/struphy/diagnostics/tests/test_diagn_tools.py deleted file mode 100644 index d1146287b..000000000 --- a/src/struphy/diagnostics/tests/test_diagn_tools.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Tests for the dispersion analysis on labeled field data.""" - -import numpy as np -import pytest - -from struphy.diagnostics.diagn_tools import power_spectrum_2d -from struphy.post_processing.arrays import data_array - -LENGTH, SPEED = 20.0, 1.0 - - -def standing_waves(dt=0.05, tend=2 * LENGTH, nx=128): - """Standing waves of all resolved wavenumbers with phase speed SPEED along eta3, as a field of a Output. - - The time window holds whole periods of every wave, so the spectrum has no leakage. - """ - t = np.arange(0.0, tend, dt) - eta = np.linspace(0.0, 1.0, nx, endpoint=False) - z = eta * LENGTH - rng = np.random.default_rng(0) - values = np.zeros((t.size, eta.size)) - for n in range(1, nx // 2): - k = 2 * np.pi * n / LENGTH - values += np.cos(k * z[None, :] + rng.uniform(0, 2 * np.pi)) * np.cos(k * SPEED * t[:, None]) - field = np.zeros((t.size, 2, 2, 1, nx)) - field[:, 1, :, 0, :] = values[:, None, :] - mesh = np.meshgrid(np.zeros(2), np.zeros(1), z, indexing="ij") - coords = {"t": t, "component": [0, 1], "e1": [0.0, 0.5], "e2": [0.0], "e3": eta} - coords.update({name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), mesh)}) - return data_array(field, ("t", "component", "e1", "e2", "e3"), coords, name="e_field_log") - - -@pytest.mark.parametrize("physical", [True, False]) -def test_fitted_phase_speed(physical): - field = standing_waves() - omega, kvec, dispersion, coeffs = power_spectrum_2d( - field, component=1, slice_at=(0, 0, None), physical=physical, fit_branches=1, noise_level=0.5 - ) - assert dispersion.shape == (omega.size, kvec.size) - # on the logical grid, wavenumbers are scaled by the domain length - expected = SPEED if physical else SPEED / LENGTH - assert coeffs[0][0] == pytest.approx(expected, rel=0.02) - - -def test_needs_exactly_one_fft_direction(): - with pytest.raises(AssertionError, match="slice_at"): - power_spectrum_2d(standing_waves(tend=1.0), slice_at=(None, None, 0)) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index bfc06db48..3dc0cb48c 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -18,7 +18,6 @@ perturbations, set_logging_level, ) -from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import LinearMHD set_logging_level() @@ -86,16 +85,11 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 - disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} - - _1, _2, _3, coeffs = power_spectrum_2d( - run.evaluate("mhd/velocity"), + spectrum = run.dispersion( + "mhd/velocity", physical=True, component=0, slice_at=[0, 0, None], - do_plot=do_plot, - disp_name="MHDhomogenSlab", - disp_params=disp_params, fit_branches=1, noise_level=0.5, extr_order=10, @@ -106,17 +100,14 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): vA = xp.sqrt(Bsquare / n0) v_alfven = vA * B0z / xp.sqrt(Bsquare) logger.info(f"{v_alfven =}") - assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 + assert xp.abs(spectrum.branch_coefficients.values[0, 0] - v_alfven) < 0.07 # second fft - _1, _2, _3, coeffs = power_spectrum_2d( - run.evaluate("mhd/pressure"), + spectrum = run.dispersion( + "mhd/pressure", physical=True, component=0, slice_at=[0, 0, None], - do_plot=do_plot, - disp_name="MHDhomogenSlab", - disp_params=disp_params, fit_branches=2, noise_level=0.4, extr_order=10, @@ -132,8 +123,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): v_fast = xp.sqrt(1 / 2 * (cS**2 + vA**2) * (1 + xp.sqrt(1 - delta))) logger.info(f"{v_slow =}") logger.info(f"{v_fast =}") - assert xp.abs(coeffs[0][0] - v_slow) < 0.05 - assert xp.abs(coeffs[1][0] - v_fast) < 0.19 + assert xp.abs(spectrum.branch_coefficients.values[0, 0] - v_slow) < 0.05 + assert xp.abs(spectrum.branch_coefficients.values[1, 0] - v_fast) < 0.19 shutil.rmtree(test_folder) diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 20d8415c4..6d9068828 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -19,7 +19,6 @@ grids, perturbations, ) -from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import Maxwell logger = logging.getLogger("struphy") @@ -73,13 +72,11 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: # fft - _1, _2, _3, coeffs = power_spectrum_2d( - run.evaluate("em_fields/e_field"), + spectrum = run.dispersion( + "em_fields/e_field", physical=True, component=0, slice_at=[0, 0, None], - do_plot=do_plot, - disp_name="Maxwell1D", fit_branches=1, noise_level=0.5, extr_order=10, @@ -88,7 +85,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # assert c_light_speed = 1.0 - assert xp.abs(coeffs[0][0] - c_light_speed) < 0.02 + assert xp.abs(spectrum.branch_coefficients.values[0, 0] - c_light_speed) < 0.02 shutil.rmtree(test_folder) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 9b2ce3d7e..aa7cb6af2 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -282,10 +282,10 @@ def dispersion( coordinates. Optional polynomial branch fits are stored as ``branch_coefficients``. Plotting is intentionally left to the optional xarray plotting package. """ - from struphy.diagnostics.diagn_tools import power_spectrum_2d - field = self._product(name, dataset=dataset) if dataset is not None else self._array(name) - omega, kvec, power, coefficients = power_spectrum_2d( + from struphy.post_processing.spectral import compute_dispersion + + result = compute_dispersion( field, component=component, slice_at=slice_at, @@ -295,24 +295,10 @@ def dispersion( extr_order=extr_order, fit_degree=fit_degree, ) - result = xr.Dataset( - { - "power": (("omega", "k"), np.asarray(power)), - }, - coords={"omega": np.asarray(omega), "k": np.asarray(kvec)}, - attrs={"run": self.label, "run_name": self.path_out.name, "source": name}, - ) + result.attrs.update(run=self.label, run_name=self.path_out.name, source=name) result["omega"].attrs["long_name"] = "angular frequency" result["k"].attrs["long_name"] = "wave number" result["power"].attrs["long_name"] = "space-time power spectrum" - if coefficients: - width = max(len(np.asarray(values).ravel()) for values in coefficients) - fitted = np.full((len(coefficients), width), np.nan) - for index, values in enumerate(coefficients): - values = np.asarray(values).ravel() - fitted[index, : values.size] = values - result["branch_coefficients"] = (("branch", "coefficient"), fitted) - result = result.assign_coords(branch=np.arange(len(coefficients))) return result def _reset(self): diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index 277e47e47..add221640 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -63,7 +63,6 @@ " grids,\n", " perturbations,\n", ")\n", - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", "from struphy.models import LinearMHD\n", "\n", "logger = logging.getLogger(\"struphy\")" @@ -260,14 +259,10 @@ "# physical=True uses the mapped X/Y/Z grid along the fft direction, matching\n", "# the domain's physical z-extent\n", "print(\"\\n=== Shear Alfvén Wave Analysis ===\")\n", - "_1, _2, _3, coeffs_alfven = power_spectrum_2d(\n", - " u_of_t,\n", + "spectrum_alfven = out.dispersion(\"mhd/velocity\",\n", " component=0,\n", " slice_at=[0, 0, None],\n", " physical=True,\n", - " do_plot=True,\n", - " disp_name=\"MHDhomogenSlab\",\n", - " disp_params=disp_params,\n", " fit_branches=1,\n", " noise_level=0.5,\n", " extr_order=10,\n", @@ -277,13 +272,13 @@ "# Theoretical Alfvén speed\n", "vA = xp.sqrt(Bsquare / n0)\n", "v_alfven_theory = vA * B0z / xp.sqrt(Bsquare)\n", - "v_alfven_fit = float(coeffs_alfven[0][0])\n", + "v_alfven_fit = float(spectrum_alfven.branch_coefficients.values[0, 0])\n", "\n", "print(f\"Théoretical Alfvén speed: {v_alfven_theory:.6f}\")\n", "print(f\"Fitted Alfvén speed: {v_alfven_fit:.6f}\")\n", "print(f\"Relative error: {abs(v_alfven_fit - v_alfven_theory) / v_alfven_theory * 100:.2f}%\")\n", "\n", - "error_alfven = xp.abs(coeffs_alfven[0][0] - v_alfven_theory)\n", + "error_alfven = xp.abs(v_alfven_fit - v_alfven_theory)\n", "assert error_alfven < 0.07, f\"Alfvén wave speed error {error_alfven:.4f} exceeds tolerance\"\n", "print(\"✓ Alfvén wave verification passed.\\n\")" ] @@ -296,14 +291,10 @@ "source": [ "# 2. Magnetosonic waves analysis from pressure\n", "print(\"=== Slow and Fast Magnetosonic Wave Analysis ===\")\n", - "_1, _2, _3, coeffs_sonic = power_spectrum_2d(\n", - " p_of_t,\n", + "spectrum_sonic = out.dispersion(\"mhd/pressure\",\n", " component=0,\n", " slice_at=[0, 0, None],\n", " physical=True,\n", - " do_plot=True,\n", - " disp_name=\"MHDhomogenSlab\",\n", - " disp_params=disp_params,\n", " fit_branches=2,\n", " noise_level=0.4,\n", " extr_order=10,\n", @@ -316,8 +307,8 @@ "v_slow_theory = xp.sqrt(0.5 * (cS**2 + vA**2) * (1.0 - xp.sqrt(1.0 - delta)))\n", "v_fast_theory = xp.sqrt(0.5 * (cS**2 + vA**2) * (1.0 + xp.sqrt(1.0 - delta)))\n", "\n", - "v_slow_fit = float(coeffs_sonic[0][0])\n", - "v_fast_fit = float(coeffs_sonic[1][0])\n", + "v_slow_fit = float(spectrum_sonic.branch_coefficients.values[0, 0])\n", + "v_fast_fit = float(spectrum_sonic.branch_coefficients.values[1, 0])\n", "\n", "print(\"\\nSlow Magnetosonic Wave:\")\n", "print(f\" Théoretical speed: {v_slow_theory:.6f}\")\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index d27e6fee4..8e56ffb31 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -111,7 +111,6 @@ " grids,\n", " perturbations,\n", ")\n", - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", "from struphy.models import Maxwell\n", "\n", "logger = logging.getLogger(\"struphy\")" @@ -265,13 +264,10 @@ "# Compute power spectrum and fit dispersion relation (physical=True uses the\n", "# mapped X/Y/Z grid along the fft direction, matching the domain's physical z-extent)\n", "print(\"\\n=== Light Wave Dispersion Analysis ===\")\n", - "_1, _2, _3, coeffs = power_spectrum_2d(\n", - " E_of_t,\n", + "spectrum = out.dispersion(\"em_fields/e_field\",\n", " component=0,\n", " slice_at=[0, 0, None],\n", " physical=True,\n", - " do_plot=True,\n", - " disp_name=\"Maxwell1D\",\n", " fit_branches=1,\n", " noise_level=0.5,\n", " extr_order=10,\n", @@ -280,7 +276,7 @@ "\n", "# Extract fitted wave speed\n", "c_light_speed = 1.0 # Theoretical\n", - "c_fit = float(coeffs[0][0])\n", + "c_fit = float(spectrum.branch_coefficients.values[0, 0])\n", "\n", "print(f\"\\nTheoretical wave speed (c): {c_light_speed:.6f}\")\n", "print(f\"Fitted wave speed: {c_fit:.6f}\")\n", @@ -288,7 +284,7 @@ "print(f\"Relative error: {abs(c_fit - c_light_speed) / c_light_speed * 100:.3f}%\")\n", "\n", "# Verify against tolerance\n", - "error = xp.abs(coeffs[0][0] - c_light_speed)\n", + "error = xp.abs(c_fit - c_light_speed)\n", "tolerance = 0.02\n", "assert error < tolerance, f\"Wave speed error {error:.4f} exceeds tolerance {tolerance}\"\n", "print(f\"\\n✓ Light wave verification passed (error < {tolerance}).\")" From f1cf9ef76b9edd2e43a1810dd654ae3352e437e3 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 08:37:29 +0200 Subject: [PATCH 132/193] Removed legacy plotting scripts --- src/struphy/diagnostics/diagnostics_pic.ipynb | 237 ------------- src/struphy/diagnostics/paraview/__init__.py | 0 .../diagnostics/paraview/mesh_creator.py | 323 ------------------ .../diagnostics/paraview/vtk_writer.py | 88 ----- 4 files changed, 648 deletions(-) delete mode 100644 src/struphy/diagnostics/diagnostics_pic.ipynb delete mode 100644 src/struphy/diagnostics/paraview/__init__.py delete mode 100644 src/struphy/diagnostics/paraview/mesh_creator.py delete mode 100644 src/struphy/diagnostics/paraview/vtk_writer.py diff --git a/src/struphy/diagnostics/diagnostics_pic.ipynb b/src/struphy/diagnostics/diagnostics_pic.ipynb deleted file mode 100644 index f41425141..000000000 --- a/src/struphy/diagnostics/diagnostics_pic.ipynb +++ /dev/null @@ -1,237 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "import numpy as np\n", - "from matplotlib import pyplot as plt\n", - "\n", - "import struphy\n", - "\n", - "path_out = os.path.join(struphy.__path__[0], \"io/out\", \"sim_1\")\n", - "\n", - "print(path_out)\n", - "os.listdir(path_out)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "data_path = os.path.join(path_out, \"post_processing\")\n", - "\n", - "os.listdir(data_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "t_grid = np.load(os.path.join(data_path, \"t_grid.npy\"))\n", - "t_grid" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "f_path = os.path.join(data_path, \"kinetic_data\", \"ions\", \"distribution_function\")\n", - "\n", - "print(os.listdir(f_path))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path = os.path.join(f_path, \"e1\")\n", - "print(os.listdir(path))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grid = np.load(os.path.join(f_path, \"e1/\", \"grid_e1.npy\"))\n", - "f_binned = np.load(os.path.join(f_path, \"e1/\", \"f_binned.npy\"))\n", - "delta_f_e1_binned = np.load(os.path.join(f_path, \"e1/\", \"delta_f_binned.npy\"))\n", - "\n", - "print(grid.shape)\n", - "print(f_binned.shape)\n", - "print(delta_f_e1_binned.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps = list(np.arange(10))\n", - "\n", - "plt.figure(figsize=(12, 5 * len(steps)))\n", - "for n, step in enumerate(steps):\n", - " plt.subplot(len(steps), 2, 2 * n + 1)\n", - " plt.plot(grid, f_binned[step], label=f\"time = {t_grid[step]}\")\n", - " plt.xlabel(\"e1\")\n", - " # plt.ylim([.5, 1.5])\n", - " plt.title(\"full-f\")\n", - " plt.subplot(len(steps), 2, 2 * n + 2)\n", - " plt.plot(grid, delta_f_e1_binned[step], label=f\"time = {t_grid[step]}\")\n", - " plt.xlabel(\"e1\")\n", - " # plt.ylim([-3e-3, 3e-3])\n", - " plt.title(r\"$\\delta f$\")\n", - " plt.legend()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path = os.path.join(f_path, \"e1_v1\")\n", - "print(os.listdir(path))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grid_e1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_e1.npy\"))\n", - "grid_v1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_v1.npy\"))\n", - "f_binned = np.load(os.path.join(f_path, \"e1_v1/\", \"f_binned.npy\"))\n", - "delta_f_binned = np.load(os.path.join(f_path, \"e1_v1/\", \"delta_f_binned.npy\"))\n", - "\n", - "print(grid_e1.shape)\n", - "print(grid_v1.shape)\n", - "print(f_binned.shape)\n", - "print(delta_f_binned.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps = list(np.arange(10))\n", - "\n", - "plt.figure(figsize=(12, 5 * len(steps)))\n", - "for n, step in enumerate(steps):\n", - " plt.subplot(len(steps), 2, 2 * n + 1)\n", - " plt.pcolor(grid_e1, grid_v1, f_binned[step].T, label=f\"time = {t_grid[step]}\")\n", - " plt.xlabel(\"$e1$\")\n", - " plt.ylabel(r\"$v_\\parallel$\")\n", - " plt.title(\"full-f\")\n", - " plt.legend()\n", - " plt.colorbar()\n", - " plt.subplot(len(steps), 2, 2 * n + 2)\n", - " plt.pcolor(grid_e1, grid_v1, delta_f_binned[step].T, label=f\"time = {t_grid[step]}\")\n", - " plt.xlabel(\"$e1$\")\n", - " plt.ylabel(r\"$v_\\parallel$\")\n", - " plt.title(r\"$\\delta f$\")\n", - " plt.legend()\n", - " plt.colorbar()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fields_path = os.path.join(data_path, \"fields_data\")\n", - "\n", - "print(os.listdir(fields_path))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pickle\n", - "\n", - "with open(os.path.join(fields_path, \"grids_phy.bin\"), \"rb\") as file:\n", - " x_grid, y_grid, z_grid = pickle.load(file)\n", - "\n", - "print(type(x_grid))\n", - "print(x_grid.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open(os.path.join(fields_path, \"em_fields\", \"phi_phy.bin\"), \"rb\") as file:\n", - " phi = pickle.load(file)\n", - "\n", - "plt.figure(figsize=(12, 12))\n", - "\n", - "steps = [0, 20, 40, -1]\n", - "for n, step in enumerate(steps):\n", - " t = t_grid[step]\n", - " print(phi[t][0].shape)\n", - " plt.subplot(2, 2, n + 1)\n", - " plt.plot(x_grid[:, 0, 0], phi[t][0][:, 0, 0], label=f\"time = {t}\")\n", - " plt.xlabel(\"x\")\n", - " plt.ylabel(r\"$\\phi$(x)\")\n", - " plt.legend()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/src/struphy/diagnostics/paraview/__init__.py b/src/struphy/diagnostics/paraview/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/struphy/diagnostics/paraview/mesh_creator.py b/src/struphy/diagnostics/paraview/mesh_creator.py deleted file mode 100644 index fdb722577..000000000 --- a/src/struphy/diagnostics/paraview/mesh_creator.py +++ /dev/null @@ -1,323 +0,0 @@ -# from tqdm import tqdm -import logging - -import cunumpy as xp -import vtkmodules.all as vtk -from vtkmodules.util.numpy_support import numpy_to_vtk as np2vtk -from vtkmodules.util.numpy_support import vtk_to_numpy as vtk2np -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid - -logger = logging.getLogger("struphy") - - -def make_ugrid_and_write_vtu(filename: str, writer, vtk_dir, gvec, s_range, u_range, v_range, periodic): - """A helper function to orchestrate operations to run many test cases. - - This is not needed in practice. - - Parameters - ---------- - filename : str - Filename to write the ParaView file. - writer : vtkWriter - A `vtkWriter` class from `writer.paraview.vtk_writer`. - vtk_dir : str - Directory to store the output ParaView files. - gvec : gvec_to_python.GVEC_functions.GVEC - A wrapper class that maps logical coordinates (s,u,v) to Cartesian (x,y,z), among other things, such as computing MHD variables. - s_range : numpy.ndarray - Range of logical radial coordinates to transform into Cartesian vertices. - u_range : numpy.ndarray - Range of logical poloidal coordinates to transform into Cartesian vertices. - v_range : numpy.ndarray - Range of logical toroidal coordinates to transform into Cartesian vertices. - periodic : boolean - Whether the mesh is a periodic structure. - """ - - # Generate one set of data, then write them in ParaView files as using different graphics primitives. - num_pts = s_range.shape[0] * u_range.shape[0] * v_range.shape[0] - logger.info(f"Number of points: {num_pts}") - point_data = {} - cell_data = {} - vtk_points, suv_points, xyz_points, point_indices = gen_vtk_points( - gvec, - s_range, - u_range, - v_range, - point_data, - cell_data, - ) - logger.info(f"vtk_points.GetNumberOfPoints() {vtk_points.GetNumberOfPoints()}") - - ugrid = setup_ugrid(vtk_points, num_pts) - connect_cell(s_range, u_range, v_range, point_indices, ugrid, point_data, cell_data, periodic) - set_data(ugrid, point_data, cell_data) - writer.write(vtk_dir, filename, ugrid) - # vtk_render(ugrid) - - -def gen_vtk_points(gvec, s_range, u_range, v_range, point_data, cell_data): - """Generate vertices for `vtkUnstructuredGrid`. - - Parameters - ---------- - gvec : gvec_to_python.GVEC_functions.GVEC - A wrapper class that maps logical coordinates (s,u,v) to Cartesian (x,y,z), among other things, such as computing MHD variables. - s_range : numpy.ndarray - Range of logical radial coordinates to transform into Cartesian vertices. - u_range : numpy.ndarray - Range of logical poloidal coordinates to transform into Cartesian vertices. - v_range : numpy.ndarray - Range of logical toroidal coordinates to transform into Cartesian vertices. - point_data : dict - A dictionary of arrays to store data assoicated with each point/vertex. - cell_data : dict - A dictionary of arrays to store data assoicated with each cell in the mesh. - - Returns - ------- - vtk_points : vtk.vtkPoints - Vertices. - suv_points : numpy.ndarray - Associated (s,u,v) coordinate, indexed with the index of the (s,u,v) coordinate that generated that point. - xyz_points : numpy.ndarray - Associated Cartesian coordinate of each (s,u,v), indexed with the index of the (s,u,v) coordinate that generated that point. - point_indices : numpy.ndarray - Associated index of each `vtk_points`, indexed with the index of the (s,u,v) coordinate that generated that point. - """ - - pt_idx = 0 - vtk_points = vtk.vtkPoints() - suv_points = xp.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0], 3)) - xyz_points = xp.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0], 3)) - point_indices = xp.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0]), dtype=xp.int_) - - # Add metadata to grid. - num_pts = s_range.shape[0] * u_range.shape[0] * v_range.shape[0] - point_data["s"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["u"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["v"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["x"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["y"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["z"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["theta"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["zeta"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["Point ID"] = xp.zeros(num_pts, dtype=xp.int_) - point_data["pressure"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["phi"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["chi"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["iota"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["q"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["det"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["det/(2pi)^2"] = xp.zeros(num_pts, dtype=xp.float_) - point_data["A"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["A_vec"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["A_1"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["A_2"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["B"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["B_vec"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["B_1"] = xp.zeros((num_pts, 3), dtype=xp.float_) - point_data["B_2"] = xp.zeros((num_pts, 3), dtype=xp.float_) - - # pbar = tqdm(total=num_pts) - for s_idx, s in enumerate(s_range): - for u_idx, u in enumerate(u_range): - for v_idx, v in enumerate(v_range): - point = gvec.f(s, u, v) - suv_points[s_idx, u_idx, v_idx, :] = xp.array([s, u, v]) - xyz_points[s_idx, u_idx, v_idx, :] = point - point_indices[s_idx, u_idx, v_idx] = pt_idx - vtk_points.InsertPoint(pt_idx, point) - # vtk_points.InsertNextPoint(i, i, i) - - # Coordinates that correspond to each point. - point_data["s"][pt_idx] = s - point_data["u"][pt_idx] = u - point_data["v"][pt_idx] = v - point_data["x"][pt_idx] = point[0] - point_data["y"][pt_idx] = point[1] - point_data["z"][pt_idx] = point[2] - point_data["Point ID"][pt_idx] = pt_idx - point_data["pressure"][pt_idx] = gvec.P(s, u, v) - point_data["phi"][pt_idx] = gvec.PHI(s, u, v) - point_data["chi"][pt_idx] = gvec.CHI(s, u, v) - point_data["iota"][pt_idx] = gvec.IOTA(s, u, v) - point_data["det"][pt_idx] = gvec.df_det(s, u, v) - point_data["A"][pt_idx] = gvec.A(s, u, v) - point_data["A_vec"][pt_idx] = gvec.A_vec(s, u, v) - point_data["A_1"][pt_idx] = gvec.A_1(s, u, v) - point_data["A_2"][pt_idx] = gvec.A_2(s, u, v) - point_data["B"][pt_idx] = gvec.B(s, u, v) # TODO: if s > 1e-4: ... - point_data["B_vec"][pt_idx] = gvec.B_vec(s, u, v) - point_data["B_1"][pt_idx] = gvec.B_1(s, u, v) - point_data["B_2"][pt_idx] = gvec.B_2(s, u, v) - - # pbar.update(1) - pt_idx += 1 - - # pbar.close() - point_data["theta"] = 2 * xp.pi * point_data["u"] - point_data["zeta"] = 2 * xp.pi * point_data["v"] - point_data["q"] = 1 / point_data["iota"] - point_data["det/(2pi)^2"] = point_data["det"] / (2 * xp.pi) ** 2 - - return vtk_points, suv_points, xyz_points, point_indices - - -def setup_ugrid(pts, num_pts): - """Associate vertices/points with a new `vtkUnstructuredGrid`. - - Parameters - ---------- - pts : vtk.vtkPoints - Cartesian coordinates of each vertex that is used to construct an unstructured grid. - num_pts : int - Number of vertices. - - Returns - ------- - ugrid : vtk.vtkUnstructuredGrid - An unstructured grid with vertices associated. - """ - - ugrid = vtk.vtkUnstructuredGrid() - ugrid.SetPoints(pts) - ugrid.Allocate(num_pts) - - return ugrid - - -def set_data(ugrid, point_data, cell_data): - """Associate point and cell data with an `vtkUnstructuredGrid`. - - Parameters - ---------- - ugrid : vtk.vtkUnstructuredGrid - An unstructured grid. - point_data : dict - A dictionary of arrays to store data assoicated with each point/vertex. - cell_data : dict - A dictionary of arrays to store data assoicated with each cell in the mesh. - """ - - # Getting the VTK data storage object. - vtk_point_data = ugrid.GetPointData() - vtk_cell_data = ugrid.GetCellData() - - # For each numpy data array, convert it into VTK formay, set its name, and add to grid. - for i, (k, v) in enumerate(point_data.items()): - vtk_array = np2vtk(v) - vtk_array.SetName(k) - vtk_point_data.AddArray(vtk_array) - - for i, (k, v) in enumerate(cell_data.items()): - vtk_array = np2vtk(v) - vtk_array.SetName(k) - vtk_cell_data.AddArray(vtk_array) - - -def vtk_render(ugrid): # pragma: no cover - """Opens an interactive window that renders the current `vtkUnstructuredGrid`. - - Parameters - ---------- - ugrid : vtk.vtkUnstructuredGrid - An unstructured grid. - """ - - colors = vtk.vtkNamedColors() - - renderer = vtk.vtkRenderer() - - renWin = vtk.vtkRenderWindow() - renWin.AddRenderer(renderer) - iren = vtk.vtkRenderWindowInteractor() - iren.SetRenderWindow(renWin) - - ugridMapper = vtk.vtkDataSetMapper() - ugridMapper.SetInputData(ugrid) - - ugridActor = vtk.vtkActor() - ugridActor.SetMapper(ugridMapper) - ugridActor.GetProperty().SetColor(colors.GetColor3d("Peacock")) - ugridActor.GetProperty().EdgeVisibilityOn() - ugridActor.GetProperty().SetOpacity(0.8) - - renderer.AddActor(ugridActor) - renderer.SetBackground(colors.GetColor3d("Beige")) - - renderer.ResetCamera() - renderer.GetActiveCamera().Elevation(60.0) - renderer.GetActiveCamera().Azimuth(30.0) - renderer.GetActiveCamera().Dolly(1.0) - - renWin.SetSize(640, 480) - renWin.SetWindowName("UGrid") - - # Interact with the data. - renWin.Render() - - iren.Start() - - -# ============================================================ -# Connect vertices to form primitives -# e.g. points, lines, quads, cells. -# ============================================================ - - -def connect_cell(s_range, u_range, v_range, point_indices, ugrid, point_data, cell_data, periodic): - """Create (initialize) cells of a `vtkUnstructuredGrid` using connectivity of its vertices. - - Inserted cells are of type `vtk.VTK_HEXAHEDRON`. Connected cells form the volume of a torus. - - Parameters - ---------- - s_range : numpy.ndarray - Range of logical radial coordinates that was used to transform into Cartesian vertices. - u_range : numpy.ndarray - Range of logical poloidal coordinates that was used to transform into Cartesian vertices. - v_range : numpy.ndarray - Range of logical toroidal coordinates that was used to transform into Cartesian vertices. - point_indices : numpy.ndarray - Associated index of each `vtk_points`, indexed with the index of the (s,u,v) coordinate that generated that point. - ugrid : vtk.vtkUnstructuredGrid - An unstructured grid. - point_data : dict - (Unused) A dictionary of arrays to store data assoicated with each point/vertex. - cell_data : dict - (Unused) A dictionary of arrays to store data assoicated with each cell in the mesh. - periodic : 3-tuple of bool - Whether each direction is periodic. - e.g. Connect a torus in poloidal and toroidal directions if periodic==[False,True,True]. - """ - - cell_idx = 0 - cell_data["Cell ID"] = [] - - len_s, len_u, len_v = s_range.shape[0], u_range.shape[0], v_range.shape[0] - - for s_idx, s in enumerate(s_range): - for u_idx, u in enumerate(u_range): - for v_idx, v in enumerate(v_range): - if ( - (periodic[0] or s_idx + 1 < len_s) - and (periodic[1] or u_idx + 1 < len_u) - and (periodic[2] or v_idx + 1 < len_v) - ): - vertex1 = point_indices[s_idx, u_idx, v_idx] - vertex2 = point_indices[s_idx, (u_idx + 1) % len_u, v_idx] - vertex3 = point_indices[s_idx, (u_idx + 1) % len_u, (v_idx + 1) % len_v] - vertex4 = point_indices[s_idx, u_idx, (v_idx + 1) % len_v] - vertex5 = point_indices[(s_idx + 1), u_idx, v_idx] - vertex6 = point_indices[(s_idx + 1), (u_idx + 1) % len_u, v_idx] - vertex7 = point_indices[(s_idx + 1), (u_idx + 1) % len_u, (v_idx + 1) % len_v] - vertex8 = point_indices[(s_idx + 1), u_idx, (v_idx + 1) % len_v] - - connected_idx = [vertex1, vertex2, vertex3, vertex4, vertex5, vertex6, vertex7, vertex8] - ugrid.InsertNextCell(vtk.VTK_HEXAHEDRON, len(connected_idx), connected_idx) - cell_data["Cell ID"].append(cell_idx) - cell_idx += 1 - - cell_data["Cell ID"] = xp.array(cell_data["Cell ID"], dtype=xp.int_) diff --git a/src/struphy/diagnostics/paraview/vtk_writer.py b/src/struphy/diagnostics/paraview/vtk_writer.py deleted file mode 100644 index 6d0c051d4..000000000 --- a/src/struphy/diagnostics/paraview/vtk_writer.py +++ /dev/null @@ -1,88 +0,0 @@ -import os - -import vtkmodules.all as vtk -from vtkmodules.util.numpy_support import numpy_to_vtk as np2vtk -from vtkmodules.util.numpy_support import vtk_to_numpy as vtk2np -from vtkmodules.vtkIOXML import vtkXMLRectilinearGridWriter, vtkXMLUnstructuredGridWriter - -""" -Some useful resources: -- https://vtk.org/Wiki/VTK/Tutorials/DataStorage -- https://stackoverflow.com/questions/59301207/save-and-write-a-vtk-polydata-file -- https://stackoverflow.com/questions/54603267/how-to-show-vtkunstructuredgrid-in-python-script-based-on-paraview -- https://kitware.github.io/vtk-examples/site/Python/ -- https://kitware.github.io/vtk-examples/site/Python/UnstructuredGrid/UGrid/ -- https://github.com/Kitware/VTK/tree/master/Wrapping/Python -- https://pypi.org/project/meshio/ -- https://stackoverflow.com/questions/59651524/writing-vtk-file-from-python-for-use-in-paraview -- https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf - -Outdated resources: -- https://vtk.org/Wiki/VTK/Writing_VTK_files_using_python -- https://github.com/pearu/pyvtk -- https://github.com/paulo-herrera/PyEVTK -- https://shocksolution.com/microfluidics-and-biotechnology/visualization/python-vtk-paraview/ -""" - - -class vtkWriter: - """Usage `from struphy.io.out.paraview.vtk_writer import vtkWriter`""" - - def __init__(self, format: str = "vtu"): - """Initialize vtkWriter object. - - Parameters - ---------- - format : str - Type of XML data that is to be written, denoted by its file extension. - """ - - self.format = format - - # Writes .vtu - self.vtu_writer = vtkXMLUnstructuredGridWriter() - - # Writes .vtr - self.vtr_writer = vtkXMLRectilinearGridWriter() - - # Writes .vti - self.vti_writer = vtk.vtkXMLImageDataWriter() - - # Writes .vtp - self.vtp_writer = vtk.vtkXMLPolyDataWriter() - - if format == "vtu": - self.writer = self.vtu_writer - elif format == "vtr": - self.writer = self.vtr_writer - # Others not implemented. - else: - raise NotImplementedError(".{} ParaView file format not implemented.".format(format)) - - def write(self, directory: str, filename: str, ugrid): - """Write the `vtkUnstructuredGrid` object into a `.vtu` file. - - Parameters - ---------- - directory : str - Output directory. - filename : str - Output filename, WITHOUT file extension. - ugrid : vtk.vtkUnstructuredGrid - A `vtkUnstructuredGrid` object. - - Returns - ------- - success : bool - Whether file write is successful. - """ - - writer = self.writer - writer.SetInputDataObject(ugrid) - - filepath = os.path.join(directory, filename + "." + writer.GetDefaultFileExtension()) - os.makedirs(directory, exist_ok=True) # Make sure directory exists. - writer.SetFileName(filepath) - success = writer.Write() - - return success == 1 From d4e1d2b4b34a73d4c8550badd35cee76ca3c40cc Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 25 Sep 2026 14:17:37 +0200 Subject: [PATCH 133/193] Added quickstart.py --- quickstart.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 quickstart.py diff --git a/quickstart.py b/quickstart.py new file mode 100644 index 000000000..252cf74db --- /dev/null +++ b/quickstart.py @@ -0,0 +1,55 @@ +import numpy as np +from struphy import Simulation, domains, grids, perturbations +from struphy.models import Poisson +from matplotlib import pyplot as plt +model = Poisson() + +stab_eps = 1e-8 + +model.propagators.poisson.options = model.propagators.poisson.Options( + stab_eps=stab_eps, +) + +Lx = 2.0 * np.pi +Ly = 4.0 * np.pi +mode = 2 +k = mode * 2.0 * np.pi / Lx +source_amp = k**2 + stab_eps + +fun = perturbations.ModesCos(ls=(mode,), amps=(source_amp,)) + +model.em_fields.source.add_perturbation(fun) + +domain = domains.Cuboid(r1=Lx, l2=-Ly / 2, r2=Ly / 2) +grid = grids.TensorProductGrid(num_elements=(64, 64, 1)) + +sim = Simulation(model=model, domain=domain, grid=grid) +out = sim.run() + +# Plot phi in 1d along eta1 and along physical coordinate X. +# The evaluate command returns an xarray DataContainer object, +# which can be indexed like a dictionary to access the data arrays. +fig, axs = plt.subplots(1, 2, figsize=(12, 4)) + +eta1 = np.linspace(0, 1, 100) +phi_1d = out.evaluate("em_fields/phi", eta1=eta1, t = -1) + +x = phi_1d["X"] +phi_exact = np.cos(k * x) +phi_exact_logical = np.cos(Lx * k * eta1) + +phi_1d.plot(ax=axs[0], label="Struphy") # Plot along eta1 +phi_1d.plot(x="X", ax=axs[1], label="Struphy") # Plot along the physical coordinate X +axs[0].plot(eta1, phi_exact_logical, "k--", lw=1.8, label="exact") +axs[1].plot(x, phi_exact, "k--", lw=1.8, label="exact") + +for i in range(2): + axs[i].legend() + axs[i].grid(alpha=0.3) +fig.savefig("quickstart_poisson_phi.png", dpi=150) +plt.show() + +# Plot phi in 2d in physical coordinates +phi_2d = out.evaluate("em_fields/phi", eta1=np.linspace(0,1,100), eta2=np.linspace(0,1,100), t = -1) +phi_2d.plot(x="X", y="Y") +plt.show() \ No newline at end of file From c991514c44caa459207abf87dd20410ad4794bd7 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 14:20:11 +0200 Subject: [PATCH 134/193] Deleted postprocessing_external --- postprocessing_external/README.md | 17 - postprocessing_external/pyproject.toml | 16 - .../src/struphy_plots/__init__.py | 5 - .../src/struphy_plots/accessors.py | 496 ----------- .../src/struphy_plots/analysis.py | 193 ----- .../src/struphy_plots/arrays.py | 89 -- .../src/struphy_plots/output_accessors.py | 58 -- .../src/struphy_plots/plotting.py | 779 ------------------ .../tests/test_analysis_and_core_output.py | 302 ------- .../tests/test_output_accessors.py | 259 ------ .../tests/test_plotting.py | 298 ------- 11 files changed, 2512 deletions(-) delete mode 100644 postprocessing_external/README.md delete mode 100644 postprocessing_external/pyproject.toml delete mode 100644 postprocessing_external/src/struphy_plots/__init__.py delete mode 100644 postprocessing_external/src/struphy_plots/accessors.py delete mode 100644 postprocessing_external/src/struphy_plots/analysis.py delete mode 100644 postprocessing_external/src/struphy_plots/arrays.py delete mode 100644 postprocessing_external/src/struphy_plots/output_accessors.py delete mode 100644 postprocessing_external/src/struphy_plots/plotting.py delete mode 100644 postprocessing_external/tests/test_analysis_and_core_output.py delete mode 100644 postprocessing_external/tests/test_output_accessors.py delete mode 100644 postprocessing_external/tests/test_plotting.py diff --git a/postprocessing_external/README.md b/postprocessing_external/README.md deleted file mode 100644 index 6e9b7e801..000000000 --- a/postprocessing_external/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# struphy-plots (staging) - -This directory contains the optional plotting layer extracted from Struphy. It -is deliberately a standalone source package so it can be moved to its own -repository without changing the Struphy runtime package. - -For development, install it with `pip install -e postprocessing_external`. -Import `struphy_plots` after installing it to register the optional -`xarray.DataArray.struphy` accessor on Struphy output arrays. For example, -`out.evaluate("em_fields/phi").struphy.plot.slice(x="e1", y="e2", t="last")`. -Direct plotting functions are available from -`struphy_plots.plotting`; analysis functions are in `struphy_plots.analysis`. - -The accessor provides time-series, lineout, slice, panel, vector, comparison, -animation, and marker-trajectory plots. For three-dimensional scalar fields, -install the optional PyVista dependency (`pip install struphy-plots[pyvista]`) and -use `field.struphy.plot.volume(t=-1)`, then call `show()` on the returned plotter. diff --git a/postprocessing_external/pyproject.toml b/postprocessing_external/pyproject.toml deleted file mode 100644 index 425fa21f6..000000000 --- a/postprocessing_external/pyproject.toml +++ /dev/null @@ -1,16 +0,0 @@ -[build-system] -requires = ["setuptools>=61"] -build-backend = "setuptools.build_meta" - -[project] -name = "struphy-plots" -version = "0.0.0" -description = "Optional plotting and diagnostics for labeled Struphy output" -requires-python = ">=3.10" -dependencies = ["matplotlib", "numpy", "xarray", "ipywidgets"] - -[project.optional-dependencies] -pyvista = ["pyvista"] - -[tool.setuptools.packages.find] -where = ["src"] diff --git a/postprocessing_external/src/struphy_plots/__init__.py b/postprocessing_external/src/struphy_plots/__init__.py deleted file mode 100644 index 4c7511748..000000000 --- a/postprocessing_external/src/struphy_plots/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Optional plotting and diagnostic helpers for Struphy xarray output.""" - -from .accessors import StruphyAccessor - -__all__ = ["StruphyAccessor"] diff --git a/postprocessing_external/src/struphy_plots/accessors.py b/postprocessing_external/src/struphy_plots/accessors.py deleted file mode 100644 index 4a6264024..000000000 --- a/postprocessing_external/src/struphy_plots/accessors.py +++ /dev/null @@ -1,496 +0,0 @@ -"""Optional accessors for plots and diagnostics of a single labeled array. - -Every product of an :class:`~struphy.Output` carries this accessor after importing -``struphy_plots``, and so does every array derived from one. - -Dimensions that are neither displayed nor swept are selected by naming them: an integer is a -position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest -coordinate value (``t=0.35``). -""" - -from __future__ import annotations - -from typing import Literal - -import numpy as np -import xarray as xr - -Coordinates = Literal["logical", "physical"] -Plane = Literal["XY", "XZ", "YZ", "RZ"] - - -@xr.register_dataarray_accessor("struphy") -class StruphyAccessor: - """Struphy diagnostics of one array: ``array.struphy.plot`` and ``array.struphy.analysis``.""" - - def __init__(self, array: xr.DataArray): - self._array = array - - @property - def plot(self) -> "ArrayPlots": - """Plots of this array, e.g. ``array.struphy.plot.slice(x="e1", y="v1", t="last")``.""" - return ArrayPlots(self._array) - - @property - def analysis(self) -> "ArrayAnalysis": - """Diagnostics of this array, e.g. ``array.struphy.analysis.growth_rate()``.""" - return ArrayAnalysis(self._array) - - -class _ArrayAccessor: - def __init__(self, array: xr.DataArray): - self._array = array - - -class ArrayPlots(_ArrayAccessor): - """Plots of one array, as ``array.struphy.plot.(...)``. - - Dimensions that are neither displayed nor swept are selected by naming them: an integer is a - position (``t=-1``), ``"first"`` and ``"last"`` are the ends, and a float is the nearest - coordinate value (``t=0.35``). - """ - - def _view(self, x, y, sweep, coords, plane, selection): - from .plotting import View - - select, index = {}, {} - for dim, value in selection.items(): - if dim not in self._array.dims: - raise TypeError( - f"{dim!r} is not a dimension of {self._array.name!r}; its dimensions are {self._array.dims}" - ) - if value == "first": - index[dim] = 0 - elif value == "last": - index[dim] = -1 - elif isinstance(value, (bool, str)): - raise TypeError(f'cannot select {dim}={value!r}; use a number, or "first"/"last"') - elif isinstance(value, (int, np.integer)): - index[dim] = int(value) - else: - select[dim] = float(value) - return View(x=x, y=y, sweep=sweep, select=select, isel=index, coordinates=coords, plane=plane) - - def timeseries( - self, *others, logy: bool = True, fit=None, fit_amplitude: bool = False, title: str | None = None, ax=None - ): - """This time series, and any others given, in one axes. - - Parameters - ---------- - *others: - Further arrays with the single dimension ``t``; they may come from other runs and - need not share this array's time grid. - logy: - Logarithmic value axis. - fit: - Time window ``(t0, t1)`` of an exponential fit per series (``None`` for an open end), - or ``True`` for the whole series. Rates are in ``result.fit_results``. - fit_amplitude: - The series is quadratic in an amplitude (e.g. an energy); fit the amplitude's rate. - """ - from .analysis import GrowthFit - from .plotting import plot_timeseries - - growth = None - if fit is not None and fit is not False: - window = (None, None) if fit is True else tuple(fit) - growth = GrowthFit(window=window, amplitude_from_quadratic=fit_amplitude) - return plot_timeseries([self._array, *others], ax=ax, logy=logy, fit=growth, title=title) - - def lineout(self, *, x: str | None = None, ax=None, title: str | None = None, **selection): - """Plot a one-dimensional profile after selecting every other dimension.""" - from .plotting import _select, plot_lineout - - view = self._view(None, None, "t", "logical", "XY", selection) - return plot_lineout(_select(self._array, view), x=x, ax=ax, title=title) - - def vector( - self, - *, - x: str, - y: str, - components: tuple[int, int] = (0, 1), - stride: int = 1, - coordinates: Coordinates = "logical", - ax=None, - **selection, - ): - """Plot two vector components after selecting time and remaining dimensions.""" - from .plotting import _select, plot_vector - - view = self._view(None, None, "t", coordinates, "XY", selection) - return plot_vector( - _select(self._array, view), x=x, y=y, components=components, stride=stride, coordinates=coordinates, ax=ax - ) - - def volume_slices(self, *, indices: dict[str, int] | None = None, cmap=None, **selection): - """Render three orthogonal slices of a selected scalar volume.""" - from .plotting import _select, plot_volume_slices - - view = self._view(None, None, "t", "logical", "XY", selection) - return plot_volume_slices(_select(self._array, view), indices=indices, cmap=cmap) - - def volume(self, *, name: str | None = None, cmap="viridis", opacity="linear", **selection): - """Create a PyVista volume plotter for a selected scalar field.""" - from .plotting import _select, pyvista_volume - - view = self._view(None, None, "t", "logical", "XY", selection) - return pyvista_volume(_select(self._array, view), name=name, cmap=cmap, opacity=opacity) - - def compare(self, other: xr.DataArray, *, mode: Literal["difference", "ratio"] = "difference", ax=None): - """Plot a one-dimensional aligned difference or ratio against another array.""" - from .plotting import plot_compare - - return plot_compare(self._array, other, mode=mode, ax=ax) - - def view( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - **selection, - ) -> "SliceView": - """Configure a reusable slice view without rendering a figure. - - Use xarray's ``.sel()``/``.isel()`` for general selection, or pass remaining - dimensions here (integers are positions, floats nearest coordinates, - ``"first"``/``"last"`` select an end). - - ``shared_clim=True`` fixes color limits over all selected data, including - frames omitted by a panel layout or export step. False rescales each frame. - Explicit ``vmin``/``vmax`` override either limit in both modes. ``cmap``, - ``equal_aspect`` and ``title`` apply to every presentation of this view. - - Examples - -------- - >>> view = f.struphy.plot.view(x="e1", y="v1", cmap="RdBu_r") - >>> view.slice(t="last") - >>> view.panels(nrows=2, ncols=3) - >>> view.save_frames("frames") - """ - self._view(x, y, sweep, coords, plane, selection) # validate selections now - return SliceView( - self._array, - dict(x=x, y=y, sweep=sweep, coords=coords, plane=plane), - selection, - dict(vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title), - ) - - def slice( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - ax=None, - **selection, - ): - """Render one 2-D slice; see :meth:`view` for shared options.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).slice(ax=ax) - - def panels( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - nrows: int = 3, - ncols: int = 4, - **selection, - ): - """Render evenly spaced snapshots; see :meth:`view` for shared options.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).panels(nrows=nrows, ncols=ncols) - - def viewer( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - **selection, - ): - """Create an interactive slider view; retain the returned viewer.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).viewer() - - def animation( - self, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - interval: int = 100, - step: int = 1, - **selection, - ): - """Animate the sweep; retain the returned Matplotlib animation.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).animation(interval=interval, step=step) - - def frames( - self, - directory, - *, - x: str | None = None, - y: str | None = None, - sweep: str = "t", - coords: Coordinates = "logical", - plane: Plane = "XY", - vmin=None, - vmax=None, - shared_clim: bool = True, - cmap: str | None = None, - equal_aspect: bool | None = None, - title: str | None = None, - step: int = 1, - prefix: str = "frame", - dpi: int = 110, - **selection, - ): - """Export PNGs; equivalent to ``plot.view(...).save_frames(directory)``.""" - return self.view( - x=x, - y=y, - sweep=sweep, - coords=coords, - plane=plane, - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - **selection, - ).save_frames(directory, step=step, prefix=prefix, dpi=dpi) - - def trajectories(self, *, max_markers: int = 200, show_paths: bool | None = None, ax=None): - """Three-dimensional paths of saved markers; for an orbit product.""" - from .plotting import plot_marker_trajectories - - return plot_marker_trajectories(self._array, ax=ax, max_markers=max_markers, show_paths=show_paths) - - -class SliceView: - """A configured array view, shared by static, interactive and exported plots. - - Construct with ``array.struphy.plot.view(...)``. Configuration does not create - figures or copy the underlying array. - """ - - def __init__(self, array, coordinates, selection, options): - self._array = array - self._coordinates = dict(coordinates) - self._selection = dict(selection) - self._options = dict(options) - - def _view(self, **selection): - return ArrayPlots(self._array)._view(**self._coordinates, selection={**self._selection, **selection}) - - def slice(self, *, ax=None, **selection): - """Draw a snapshot, e.g. ``view.slice(t="last")``; return a PlotResult.""" - # Resolve shared limits before selecting a single snapshot, so it uses - # the same scale as panels, animation and export of this configured view. - from .plotting import _SliceRenderer, plot_slice - - options = dict(self._options) - if options["shared_clim"]: - renderer = _SliceRenderer(self._array, self._view(), **options) - options.update(zip(("vmin", "vmax"), renderer.limits)) - return plot_slice(self._array, view=self._view(**selection), ax=ax, **options) - - def panels(self, *, nrows=3, ncols=4): - """Draw snapshots spread along the sweep; return a PlotResult.""" - from .plotting import plot_panels - - return plot_panels(self._array, view=self._view(), nrows=nrows, ncols=ncols, **self._options) - - def viewer(self): - """Create a viewer with sliders for unselected dimensions.""" - from .plotting import InteractiveSliceViewer - - return InteractiveSliceViewer(self._array, view=self._view(), **self._options) - - def animation(self, *, interval=100, step=1): - """Create a Matplotlib animation using this view's rendering options.""" - from .plotting import animate_slices - - return animate_slices(self._array, view=self._view(), interval=interval, step=step, **self._options) - - def save_frames(self, directory, *, step=1, prefix="frame", dpi=110): - """Export PNG frames using this view's rendering options; return paths.""" - from .plotting import save_frames - - return save_frames( - self._array, directory, view=self._view(), step=step, prefix=prefix, dpi=dpi, **self._options - ) - - -class ArrayAnalysis(_ArrayAccessor): - """Quantitative diagnostics of one array, as ``array.struphy.analysis.(...)``.""" - - def growth_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): - """Fit ``exp(rate * t + intercept)`` to this time series within ``window``. - - With ``amplitude=True`` the series is quadratic in an amplitude (e.g. an energy) and the - amplitude's rate is returned. Returns a ``FitResult`` (``.rate``, ``.intercept``, - ``.time``, ``.fitted``), or ``None`` with fewer than two valid samples. - """ - from .analysis import GrowthFit, growth_rate - - return growth_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def damping_rate(self, *, window: tuple[float | None, float | None] = (None, None), amplitude: bool = False): - """Fit exponential decay to the envelope of this oscillating time series; see ``growth_rate``.""" - from .analysis import GrowthFit, damping_rate - - return damping_rate(self._array, GrowthFit(window=tuple(window), amplitude_from_quadratic=amplitude)) - - def envelope(self) -> xr.DataArray: - """Local maxima of this time series.""" - from .analysis import envelope - - return envelope(self._array) - - def norm(self, *, dims=None, squared: bool = False) -> xr.DataArray: - """L2 norm over ``dims`` (default: every dimension except ``t``).""" - from .analysis import norm - - return norm(self._array, dims=dims, squared=squared) - - def drift(self, *, ref=None) -> xr.DataArray: - """Signed deviation of this time series from ``ref`` or from its first sample.""" - from .analysis import drift - - return drift(self._array, ref=ref) - - def relative_error(self, *, ref=None, skip_first: bool = True) -> xr.DataArray: - """Absolute relative deviation from ``ref`` or from this series' first sample.""" - from .analysis import relative_error - - return relative_error(self._array, ref=ref, skip_first=skip_first) - - def spatial_average(self, *, dims=None) -> xr.DataArray: - """Mean over the logical space dimensions ``e1``, ``e2``, ``e3`` (or ``dims``). - - For a binned ``e1_v1`` distribution this is f(v1, t) averaged over space; see - :func:`struphy_plots.analysis.spatial_average`. - """ - from .analysis import spatial_average - - return spatial_average(self._array, dims=dims) - - def velocity_moments(self, *, dims=None) -> xr.Dataset: - """Density, mean velocity and variance of a binned distribution over its velocity dimensions. - - See :func:`struphy_plots.analysis.velocity_moments` for the definitions. - """ - from .analysis import velocity_moments - - return velocity_moments(self._array, dims=dims) - - def dispersion(self, *, component: int = 0, slice_at: tuple = (None, 0, 0), physical: bool = False, **kwargs): - """Space-time power spectrum of this field and fitted dispersion branches. - - The time coordinate must be normalized, see :meth:`struphy.Output.with_time_units`. See - :func:`struphy.post_processing.spectral.compute_dispersion` for ``slice_at`` and fit options. - Returns an xarray Dataset with ``power(omega, k)``. - """ - from struphy.post_processing.spectral import compute_dispersion - - if self._array.t.attrs.get("units") == "s": - raise ValueError( - "the spectrum needs normalized time; take the field from out.with_time_units('normalized')" - ) - return compute_dispersion(self._array, component=component, slice_at=slice_at, physical=physical, **kwargs) diff --git a/postprocessing_external/src/struphy_plots/analysis.py b/postprocessing_external/src/struphy_plots/analysis.py deleted file mode 100644 index 91b6c8b0b..000000000 --- a/postprocessing_external/src/struphy_plots/analysis.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Numerical diagnostics returning values and labeled arrays, without rendering.""" - -from dataclasses import dataclass - -import numpy as np -import xarray as xr - -from .arrays import validate_array - - -def _label(data): - return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" - - -@dataclass(frozen=True) -class GrowthFit: - """Configuration for an exponential growth-rate fit.""" - - window: tuple[float | None, float | None] = (None, None) - amplitude_from_quadratic: bool = False - - -@dataclass(frozen=True) -class FitResult: - rate: float - intercept: float - time: np.ndarray - fitted: np.ndarray - - -def growth_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: - """Fit ``exp(rate*t + intercept)`` using only finite, positive samples.""" - validate_array(data, required_dims=("t",)) - if data.dims != ("t",): - raise ValueError(f"growth-rate input must have dims ('t',), got {data.dims}") - fit = fit or GrowthFit() - time, values = np.asarray(data.t), np.asarray(data) - if len(time) < 2: - return None - lo = time[0] if fit.window[0] is None else fit.window[0] - hi = time[-1] if fit.window[1] is None else fit.window[1] - lo, hi = sorted((lo, hi)) - valid = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) - if np.count_nonzero(valid) < 2: - return None - selected_time = time[valid] - signal = np.log(np.sqrt(values[valid])) if fit.amplitude_from_quadratic else np.log(values[valid]) - rate, intercept = np.polyfit(selected_time, signal, 1) - scale = 2.0 if fit.amplitude_from_quadratic else 1.0 - fitted = np.exp(scale * (rate * selected_time + intercept)) - return FitResult(float(rate), float(intercept), selected_time, fitted) - - -def envelope(data: xr.DataArray) -> xr.DataArray: - """Local maxima of a time series: the interior samples not smaller than their neighbours.""" - validate_array(data, required_dims=("t",)) - if data.dims != ("t",): - raise ValueError(f"envelope input must have dims ('t',), got {data.dims}") - values = np.asarray(data) - peak = np.zeros(len(values), dtype=bool) - peak[1:-1] = (values[1:-1] > values[:-2]) & (values[1:-1] >= values[2:]) - return data.isel(t=np.flatnonzero(peak)) - - -def damping_rate(data: xr.DataArray, fit: GrowthFit | None = None) -> FitResult | None: - """Fit ``exp(rate*t + intercept)`` to the envelope of an oscillating time series. - - Use this for signals such as the field energy in Landau damping, where :func:`growth_rate` on - the raw series would fit the oscillation. ``fit.window`` restricts the peaks that are used. - The rate is negative for damping. - """ - return growth_rate(envelope(data), fit) - - -def norm(data: xr.DataArray, *, dims=None, squared: bool = False) -> xr.DataArray: - """L2 norm over ``dims`` (default: every dimension except ``t``), as a function of the rest.""" - validate_array(data) - dims = [dim for dim in data.dims if dim != "t"] if dims is None else list(dims) - total = (data**2).sum(dims) - out = total if squared else np.sqrt(total) - out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - label = _label(data) - out.attrs["label"] = f"squared norm of {label}".strip() if squared else f"norm of {label}".strip() - return out - - -def drift(data: xr.DataArray, *, ref=None) -> xr.DataArray: - """Signed deviation from an explicit reference or the first time sample.""" - validate_array(data, required_dims=("t",)) - reference = data.isel(t=0) if ref is None else ref - out = data - reference - out.attrs = dict(data.attrs) - out.attrs["label"] = f"{_label(data)} drift".strip() - return out - - -SPATIAL_DIMS = ("e1", "e2", "e3") -VELOCITY_DIMS = ("v1", "v2", "v3") - - -def _provenance(data: xr.DataArray) -> dict: - return {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - - -def _select_dims(data: xr.DataArray, dims, default) -> list[str]: - if dims is None: - selected = [dim for dim in default if dim in data.dims] - if not selected: - raise ValueError(f"{data.name!r} has none of the dimensions {default}; its dimensions are {data.dims}") - return selected - selected = [dims] if isinstance(dims, str) else list(dims) - missing = [dim for dim in selected if dim not in data.dims] - if missing: - raise ValueError(f"{data.name!r} has no dimensions {missing}; its dimensions are {data.dims}") - return selected - - -def spatial_average(data: xr.DataArray, *, dims=None) -> xr.DataArray: - """Mean over the logical space dimensions, e.g. a binned f(t, e1, v1) becomes f(t, v1). - - ``dims`` defaults to every one of ``e1``, ``e2``, ``e3`` that ``data`` has. The mean is - uniform in the logical coordinates, which is the volume average on a Cartesian domain; on a - mapped domain it is not weighted by the Jacobian. Physical ``X``, ``Y``, ``Z`` coordinates - that depend on the averaged dimensions are dropped. - """ - validate_array(data) - averaged = _select_dims(data, dims, SPATIAL_DIMS) - out = data.mean(averaged, keep_attrs=True) - out.attrs["label"] = f"average of {_label(data)}".strip() - out.attrs.pop("long_name", None) - return out - - -def _bin_widths(data: xr.DataArray, dim: str) -> xr.DataArray: - coordinate = np.asarray(data.coords[dim]) if dim in data.coords else None - if coordinate is None or len(coordinate) < 2: - raise ValueError(f"dimension {dim!r} needs a coordinate with at least two bins") - return xr.DataArray(np.gradient(coordinate), dims=(dim,), coords={dim: data.coords[dim]}) - - -def velocity_moments(f: xr.DataArray, *, dims=None) -> xr.Dataset: - """Moments of a binned distribution function over its velocity dimensions. - - ``dims`` defaults to every one of ``v1``, ``v2``, ``v3`` that ``f`` has; the moments are - functions of the remaining dimensions, for example ``(t, e1)`` for an ``e1_v1`` product. - The integrals are sums over the bins, weighted by the bin widths. - - Returns a Dataset with - - * ``density``: the zeroth moment, :math:`\\int f\\,\\mathrm{d}v`. - * ``mean_``: the mean velocity :math:`u = \\int v f\\,\\mathrm{d}v / n` along each dimension. - * ``variance_``: :math:`\\int (v-u)^2 f\\,\\mathrm{d}v / n`. In normalized units this is the - temperature over the particle mass along that direction, :math:`T/m`. - - Where the density is not positive, the mean and variance are NaN. A ``delta_f`` product has - only the density, which is then the density perturbation, because its mean and variance are - not defined. The values keep the normalization of the run; see ``Output.to_si``. - """ - validate_array(f) - integrated = _select_dims(f, dims, VELOCITY_DIMS) - volume = 1.0 - for dim in integrated: - volume = volume * _bin_widths(f, dim) - density = (f * volume).sum(integrated) - - label = _label(f) - variables = {"density": (density, f"density of {label}")} - if f.name != "delta_f": - weight = density.where(density > 0) - for dim in integrated: - mean = (f * f[dim] * volume).sum(integrated) / weight - variance = (f * (f[dim] - mean) ** 2 * volume).sum(integrated) / weight - variables[f"mean_{dim}"] = (mean, f"mean {dim}") - variables[f"variance_{dim}"] = (variance, f"variance of {dim}") - - provenance = _provenance(f) - out = {} - for name, (values, description) in variables.items(): - values.attrs = {**provenance, "label": description.strip()} - out[name] = values.rename(name) - return xr.Dataset(out, attrs=provenance) - - -def relative_error(data: xr.DataArray, *, ref=None, skip_first=True) -> xr.DataArray: - """Absolute relative deviation from an explicit reference or first sample.""" - validate_array(data, required_dims=("t",)) - reference = data.isel(t=0) if ref is None else ref - if np.any(np.asarray(reference) == 0): - raise ValueError("cannot take a relative error against a reference of zero") - out = abs(data - reference) / abs(reference) - out.attrs = {key: value for key, value in data.attrs.items() if key in ("run", "run_name")} - out.attrs.update(label=f"relative error of {_label(data)}".strip(), units="") - return out.isel(t=slice(1, None)) if skip_first else out diff --git a/postprocessing_external/src/struphy_plots/arrays.py b/postprocessing_external/src/struphy_plots/arrays.py deleted file mode 100644 index 64a0430b4..000000000 --- a/postprocessing_external/src/struphy_plots/arrays.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Small xarray metadata helpers used by :mod:`struphy_plots`. - -They intentionally live here rather than in Struphy so the plotting package can -operate on labeled xarray data from any producer. -""" - -from __future__ import annotations - -import os -from collections.abc import Mapping, Sequence - -import numpy as np -import xarray as xr - -DIM_LABELS = { - "t": r"$t$", - "e1": r"$\eta_1$", - "e2": r"$\eta_2$", - "e3": r"$\eta_3$", - "v1": r"$v_1$", - "v2": r"$v_2$", - "v3": r"$v_3$", - "x": r"$x$", - "y": r"$y$", - "z": r"$z$", - "R": r"$R$", - "Z": r"$Z$", - "component": "component", - "marker": "marker", - "quantity": "quantity", -} -SCALARS_EXCLUDE = ("time",) - - -def validate_array(data: xr.DataArray, *, required_dims: Sequence[str] = ()) -> xr.DataArray: - if not isinstance(data, xr.DataArray): - raise TypeError(f"expected xarray.DataArray, got {type(data).__name__}") - missing = tuple(dim for dim in required_dims if dim not in data.dims) - if missing: - raise ValueError(f"missing dimensions {missing}; available dimensions are {data.dims}") - return data - - -def axis_label(data: xr.DataArray, dim: str) -> str: - if dim not in data.dims: - raise KeyError(f"dimension {dim!r} not found in {data.dims}") - coord = data.coords.get(dim) - label = ("" if coord is None else coord.attrs.get("long_name", "")) or DIM_LABELS.get(dim, dim) - unit = "" if coord is None else coord.attrs.get("units", "") - return f"{label} [{unit}]" if unit else label - - -def value_label(data: xr.DataArray) -> str: - label = data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" - unit = data.attrs.get("units", "") or "a.u." - return f"{label} [{unit}]" if label else f"[{unit}]" - - -def scalar_names(scalars: xr.Dataset | Mapping, *, names=None, exclude=SCALARS_EXCLUDE) -> list[str]: - available = tuple(scalars.data_vars if isinstance(scalars, xr.Dataset) else scalars.keys()) - if names is not None: - missing = [name for name in names if name not in available] - if missing: - raise KeyError(f"no scalars {missing}, available: {available}") - return list(names) - return [name for name in available if name not in exclude] - - -def save_scalars(scalars: xr.Dataset | Mapping, path: str, *, names=None, exclude=SCALARS_EXCLUDE, fmt=None) -> str: - selected = scalar_names(scalars, names=names, exclude=exclude) - arrays = [scalars[name] for name in selected] - for array in arrays: - validate_array(array, required_dims=("t",)) - if array.dims != ("t",): - raise ValueError(f"scalar {array.name!r} must have only the 't' dimension, got {array.dims}") - if arrays: - arrays = xr.align(*arrays, join="exact") - time = np.asarray(arrays[0].coords["t"]) - values = np.column_stack([np.asarray(array) for array in arrays]) - else: - time, values = np.zeros(0), np.zeros((0, 0)) - fmt = (fmt or os.path.splitext(path)[1].lstrip(".") or "csv").lower() - if fmt == "npz": - np.savez(path, t=time, **{name: values[:, i] for i, name in enumerate(selected)}) - elif fmt == "csv": - np.savetxt(path, np.column_stack((time, values)), delimiter=",", header=",".join(("t", *selected)), comments="") - else: - raise ValueError(f"unknown format {fmt!r}, expected 'csv' or 'npz'") - return path diff --git a/postprocessing_external/src/struphy_plots/output_accessors.py b/postprocessing_external/src/struphy_plots/output_accessors.py deleted file mode 100644 index 4bc415474..000000000 --- a/postprocessing_external/src/struphy_plots/output_accessors.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Optional plots that need a whole run. - -Plots and diagnostics of a single array live on the array, see -:class:`~struphy.post_processing.xarray_accessors.StruphyAccessor`: -``out.em_fields.phi_log.struphy.plot.slice(...)``, or from a value returned by -``out.evaluate("em_fields/phi_log")``. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from . import accessors # noqa: F401 (registers array.struphy) - -if TYPE_CHECKING: - from struphy.post_processing.output import Output - - -class OutputPlots: - """Plots of a whole run, constructed as ``OutputPlots(out)``. - - They return plotting-library objects with ``.show()`` - and ``.save(path)``, titled with the run's numerical parameters. Plots of one product are - methods of that product, e.g. ``out.kinetic_ions.orbits.struphy.plot.trajectories()``. - """ - - def __init__(self, output: "Output"): - self._output = output - - def scalars(self, names=None, *, relative_to: str | None = None, logy: bool = False): - """Overview of the scalar time series in one axes. - - Parameters - ---------- - names: - Scalars to show; all by default. - relative_to: - Show every scalar divided by this one. - logy: - Logarithmic value axis. - """ - from .plotting import plot_scalars - - return plot_scalars( - self._output.scalars, names=names, relative_to=relative_to, logy=logy, run_label=self._output.label - ) - - def equilibrium(self, ax=None): - """Radial equilibrium profiles, from the geometry written at the start of the run.""" - from .plotting import plot_equilibrium_profile - - return plot_equilibrium_profile(self._output.path_out, ax=ax) - - def equilibrium_3d(self, *, scalars: str = "p0", cmap="viridis"): - """Create a PyVista equilibrium view; call ``.show()`` on the returned plotter.""" - from .plotting import show_equilibrium - - return show_equilibrium(self._output.path_out, scalars=scalars, cmap=cmap) diff --git a/postprocessing_external/src/struphy_plots/plotting.py b/postprocessing_external/src/struphy_plots/plotting.py deleted file mode 100644 index 3d7778091..000000000 --- a/postprocessing_external/src/struphy_plots/plotting.py +++ /dev/null @@ -1,779 +0,0 @@ -"""Small, composable plotting functions for labeled Struphy output. - -They remain importable for plotting arbitrary labeled arrays. The optional xarray -accessor exposes them as ``array.struphy.plot.*``. -""" - -from __future__ import annotations - -import logging -from dataclasses import dataclass, field -from pathlib import Path -from typing import Literal - -import matplotlib.pyplot as plt -import numpy as np -import xarray as xr -from matplotlib.widgets import Slider - -from .analysis import ( - FitResult, - GrowthFit, - drift, - growth_rate, - relative_error, -) -from .arrays import ( - SCALARS_EXCLUDE, - axis_label, - save_scalars, - scalar_names, - validate_array, - value_label, -) - -logger = logging.getLogger("struphy") - -STRUPHY_STYLE = { - "figure.figsize": (8.0, 5.0), - "figure.dpi": 110, - "axes.grid": True, - "grid.alpha": 0.3, - "axes.titlesize": "medium", - "legend.frameon": False, - "image.cmap": "viridis", -} - -PLANES = { - "XY": ("X", "Y", "X", "Y"), - "XZ": ("X", "Z", "X", "Z"), - "YZ": ("Y", "Z", "Y", "Z"), - "RZ": ("R", "Z", "R", "Z"), -} - - -@dataclass(frozen=True) -class View: - """A reusable selection and rendering recipe for an N-dimensional product.""" - - x: str | None = None - y: str | None = None - sweep: str = "t" - select: dict[str, float] = field(default_factory=dict) - isel: dict[str, int] = field(default_factory=dict) - coordinates: Literal["logical", "physical"] = "logical" - plane: Literal["XY", "XZ", "YZ", "RZ"] = "XY" - - -@dataclass -class PlotResult: - """Already-rendered Matplotlib objects; saving never redraws them. - - As the last expression of a notebook cell it displays its figure once; there is no need - to write ``.fig``. - """ - - fig: object - ax: object - artists: list = field(default_factory=list) - fit_results: list[FitResult | None] = field(default_factory=list) - data: dict = field(default_factory=dict) - _shown: bool = field(default=False, init=False, repr=False, compare=False) - - def save(self, path, *, close=False, **kwargs): - kwargs.setdefault("bbox_inches", "tight") - self.fig.savefig(path, **kwargs) - if close: - plt.close(self.fig) - return str(path) - - def show(self): - plt.show() - self._shown = True - return self - - def __repr__(self): - return f"{type(self).__name__}(fig={self.fig!r})" - - def _ipython_display_(self): - if not self._shown: - _display_figure(self.fig) - - -def _detach_figure(fig): - """Take a figure out of pyplot under the inline backend, which would show it as a still image.""" - import matplotlib - - if "inline" in matplotlib.get_backend(): - plt.close(fig) - - -def _display_figure(fig): - """Display a figure as a notebook cell result, exactly once. - - The inline backend shows every open figure again at the end of the cell, so the displayed - figure is closed. Interactive backends (e.g. ipympl) already show the figure when it is - created, so nothing is displayed twice there either. - """ - import matplotlib - - if "inline" not in matplotlib.get_backend(): - return - from IPython.display import display - - display(fig) - plt.close(fig) - - -def _label(data): - return data.attrs.get("label") or data.attrs.get("long_name") or data.name or "" - - -def _items(data): - return [data] if isinstance(data, (xr.DataArray, xr.Dataset)) else list(data) - - -def shared_run_label(data, default="") -> str: - """The run description shared by all arrays (``attrs["run"]``), or ``default``. - - Arrays loaded from a :class:`~struphy.Output` carry it; arrays from different runs share none. - """ - runs = {item.attrs.get("run") for item in _items(data)} - if len(runs - {None, ""}) > 1: - return "" - runs.discard(None) - runs.discard("") - return runs.pop() if runs else default - - -def _finish(fig, *, run_label="", tight=True): - if run_label: - fig.suptitle(run_label, fontsize="small") - if tight: - fig.tight_layout() - - -def _select(data: xr.DataArray, view: View, *, keep_sweep=True): - validate_array(data) - overlap = set(view.select) & set(view.isel) - if overlap: - raise ValueError(f"dimensions cannot appear in both select and isel: {sorted(overlap)}") - selected = data - if view.select: - selected = selected.sel(view.select, method="nearest") - if view.isel: - selected = selected.isel(view.isel) - if not keep_sweep and view.sweep in selected.dims: - selected = selected.isel({view.sweep: 0}) - return selected - - -def logical_grids(data: xr.DataArray, *, x=None, y=None): - """Return 2-D logical coordinate grids and their labels.""" - if x is None or y is None: - if data.ndim != 2: - raise ValueError(f"x and y are required unless data is two-dimensional; got {data.dims}") - x, y = data.dims - if set(data.dims) != {x, y}: - raise ValueError(f"selected data must contain exactly {x!r} and {y!r}; got {data.dims}") - xgrid, ygrid = np.meshgrid(np.asarray(data.coords[x]), np.asarray(data.coords[y]), indexing="ij") - return xgrid, ygrid, axis_label(data, x), axis_label(data, y) - - -def physical_grids(data: xr.DataArray, *, plane="XY"): - """Return physical auxiliary coordinates already attached to a selected field.""" - if plane not in PLANES: - raise ValueError(f"unknown plane {plane!r}; expected one of {tuple(PLANES)}") - xname, yname, xlabel, ylabel = PLANES[plane] - missing = [name for name in ("X", "Y", "Z") if name not in data.coords] - if missing: - raise ValueError(f"physical coordinates are not attached to {data.name!r}: missing {missing}") - xcoord = np.sqrt(data.X**2 + data.Y**2) if xname == "R" else data.coords[xname] - ycoord = data.coords[yname] - if xcoord.ndim != 2 or ycoord.ndim != 2: - raise ValueError("select all but two spatial dimensions before requesting a physical grid") - return np.asarray(xcoord), np.asarray(ycoord), xlabel, ylabel - - -def _slice_data(data, view): - selected = _select(data, view) - if view.sweep in selected.dims and view.sweep not in (view.x, view.y): - raise ValueError(f"select one {view.sweep!r} value before drawing a static slice, or display it as x or y") - if view.x is None or view.y is None: - if selected.ndim != 2: - raise ValueError(f"view.x and view.y are required for remaining dims {selected.dims}") - x, y = selected.dims - else: - x, y = view.x, view.y - if set(selected.dims) != {x, y}: - raise ValueError(f"selection leaves dimensions {selected.dims}; expected only {x!r}, {y!r}") - selected = selected.transpose(x, y) - grids = ( - physical_grids(selected, plane=view.plane) - if view.coordinates == "physical" - else logical_grids(selected, x=x, y=y) - ) - return selected, grids - - -def plot_timeseries(data, *, ax=None, logy=True, fit: GrowthFit | None = None, title=None, run_label=None): - """Plot one or more time series, each on its own time grid; series of different runs are labeled by run.""" - series = _items(data) - if not series: - raise ValueError("at least one time series is required") - for item in series: - validate_array(item, required_dims=("t",)) - if item.dims != ("t",): - raise ValueError(f"time series must have dims ('t',), got {item.dims}") - label_of = _label - if len({item.attrs.get("run_name") for item in series}) > 1: - - def label_of(item): - return " ".join( - filter(None, (_label(item), f"({item.attrs['run_name']})" if item.attrs.get("run_name") else "")) - ) - - run_label = shared_run_label(series) if run_label is None else run_label - own_figure = ax is None - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - artists, fits = [], [] - for item in series: - (line,) = ax.plot(item.t, item, label=label_of(item) or None) - artists.append(line) - result = growth_rate(item, fit) if fit is not None else None - fits.append(result) - if result is not None: - (fitted,) = ax.plot( - result.time, - result.fitted, - "--", - color=line.get_color(), - label=rf"fit: $\gamma$ = {result.rate:.4e}", - ) - ax.axvspan(result.time[0], result.time[-1], alpha=0.12, color="grey") - artists.append(fitted) - if logy: - ax.set_yscale("log") - ax.set_xlabel(axis_label(series[0], "t")) - ax.set_ylabel(value_label(series[0])) - ax.set_title(title if title is not None else _label(series[0])) - if any(label_of(item) for item in series) or fit is not None: - ax.legend() - _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) - return PlotResult(fig, ax, artists, fits) - - -def plot_lineout(data: xr.DataArray, *, x: str | None = None, ax=None, title=None): - """Plot a selected one-dimensional profile using one named coordinate.""" - validate_array(data) - if data.ndim != 1: - raise ValueError(f"lineout needs exactly one remaining dimension, got {data.dims}") - x = data.dims[0] if x is None else x - if x != data.dims[0]: - raise ValueError(f"lineout coordinate {x!r} is not the remaining dimension {data.dims[0]!r}") - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - (line,) = ax.plot(data[x], data) - ax.set(xlabel=axis_label(data, x), ylabel=value_label(data), title=_label(data) if title is None else title) - _finish(fig, run_label=shared_run_label(data) if line.axes.figure is fig else "") - return PlotResult(fig, ax, [line]) - - -def plot_vector( - data: xr.DataArray, - *, - x: str, - y: str, - components: tuple[int, int] = (0, 1), - component_dim: str = "component", - ax=None, - stride: int = 1, - coordinates: Literal["logical", "physical"] = "logical", -): - """Render two components of a selected vector field with Matplotlib quivers.""" - validate_array(data, required_dims=(component_dim, x, y)) - if set(data.dims) != {component_dim, x, y}: - raise ValueError(f"select every dimension except {component_dim!r}, {x!r}, and {y!r}; got {data.dims}") - if stride < 1: - raise ValueError("stride must be positive") - vector = data.transpose(component_dim, x, y).isel( - {component_dim: list(components), x: slice(None, None, stride), y: slice(None, None, stride)} - ) - if coordinates == "physical": - planes = {frozenset(("e1", "e2")): "XY", frozenset(("e1", "e3")): "XZ", frozenset(("e2", "e3")): "YZ"} - plane = planes.get(frozenset((x, y))) - if plane is None: - raise ValueError("physical vector plots require two logical spatial dimensions") - xg, yg, xlabel, ylabel = physical_grids(vector.isel({component_dim: 0}), plane=plane) - else: - xg, yg, xlabel, ylabel = logical_grids(vector.isel({component_dim: 0}), x=x, y=y) - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - quiver = ax.quiver(xg, yg, vector.isel({component_dim: 0}), vector.isel({component_dim: 1})) - ax.set(xlabel=xlabel, ylabel=ylabel, title=_label(data), aspect="equal" if coordinates == "physical" else "auto") - _finish(fig, run_label=shared_run_label(data)) - return PlotResult(fig, ax, [quiver]) - - -def plot_volume_slices(data: xr.DataArray, *, indices: dict[str, int] | None = None, cmap=None): - """Show three orthogonal midpoint slices of a selected scalar volume.""" - validate_array(data, required_dims=("e1", "e2", "e3")) - if set(data.dims) != {"e1", "e2", "e3"}: - raise ValueError(f"select every non-spatial dimension before volume_slices(); got {data.dims}") - indices = {dim: data.sizes[dim] // 2 for dim in data.dims} | (indices or {}) - fig, axes = plt.subplots(1, 3, figsize=(12, 3.6), layout="constrained") - artists = [] - for ax, normal, x, y in zip(axes, ("e3", "e2", "e1"), ("e1", "e1", "e2"), ("e2", "e3", "e3")): - plane = data.isel({normal: indices[normal]}).transpose(x, y) - mesh = ax.pcolormesh(plane[x], plane[y], np.asarray(plane).T, shading="auto", cmap=cmap) - ax.set(xlabel=axis_label(plane, x), ylabel=axis_label(plane, y), title=f"{normal} index {indices[normal]}") - fig.colorbar(mesh, ax=ax, label=value_label(data)) - artists.append(mesh) - fig.suptitle(" — ".join(filter(None, (_label(data), shared_run_label(data))))) - return PlotResult(fig, axes, artists) - - -def plot_compare( - first: xr.DataArray, second: xr.DataArray, *, mode: Literal["difference", "ratio"] = "difference", ax=None -): - """Plot a one-dimensional aligned difference or ratio of two arrays.""" - first, second = xr.align(first, second, join="inner") - result = first - second if mode == "difference" else xr.where(second != 0, first / second, np.nan) - result.name = f"{_label(first)} {mode}" - return plot_lineout(result, ax=ax) - - -def pyvista_volume(data: xr.DataArray, *, name: str | None = None, cmap="viridis", opacity="linear"): - """Create a PyVista volume view from a selected scalar field with ``X/Y/Z`` coordinates. - - The returned plotter is not shown automatically; call ``plotter.show()`` in an - interactive session or use PyVista's off-screen rendering options in batch jobs. - """ - import pyvista as pv - - validate_array(data, required_dims=("e1", "e2", "e3")) - if set(data.dims) != {"e1", "e2", "e3"}: - raise ValueError(f"select every non-spatial dimension before pyvista_volume(); got {data.dims}") - if any(coord not in data.coords for coord in ("X", "Y", "Z")): - raise ValueError("pyvista_volume() requires mapped X, Y, and Z coordinates") - grid = pv.StructuredGrid( - np.asarray(data.X, dtype=float), np.asarray(data.Y, dtype=float), np.asarray(data.Z, dtype=float) - ) - name = name or _label(data) or "value" - grid.point_data[name] = np.asarray(data).ravel(order="F") - plotter = pv.Plotter() - plotter.add_volume(grid, scalars=name, cmap=cmap, opacity=opacity) - plotter.show_axes() - return plotter - - -def show_equilibrium(path_out, *, scalars: str = "p0", cmap="viridis"): - """Create a PyVista view of ``geometry.vts`` and its equilibrium scalar field.""" - import pyvista as pv - - grid = pv.read(str(Path(path_out) / "geometry.vts")) - if scalars not in grid.point_data: - raise KeyError(f"{scalars!r} is not available; choices: {tuple(grid.point_data)}") - plotter = pv.Plotter() - plotter.add_mesh(grid, scalars=scalars, cmap=cmap, show_edges=False) - plotter.show_axes() - return plotter - - -class _SliceRenderer: - """Shared selection, color limits and mesh rendering for every slice presentation.""" - - def __init__(self, data, view, *, vmin=None, vmax=None, shared_clim=True, cmap=None, equal_aspect=None, title=None): - self.data = _select(data, view) - self.view = View(x=view.x, y=view.y, sweep=view.sweep, coordinates=view.coordinates, plane=view.plane) - self.vmin, self.vmax = vmin, vmax - self.shared_clim = shared_clim - self.cmap = cmap or STRUPHY_STYLE["image.cmap"] - self.equal_aspect = view.coordinates == "physical" if equal_aspect is None else equal_aspect - self.title = _label(data) if title is None else title - self.limits = self._limits(self.data) if shared_clim else None - - def _limits(self, data): - if self.vmin is not None and self.vmax is not None: - return self.vmin, self.vmax - values = np.asarray(data) - finite = values[np.isfinite(values)] - if not finite.size: - raise ValueError("cannot determine color limits from data without finite values; provide vmin and vmax") - return ( - float(finite.min()) if self.vmin is None else self.vmin, - float(finite.max()) if self.vmax is None else self.vmax, - ) - - def draw(self, ax, data): - values, (xg, yg, xlabel, ylabel) = _slice_data(data, self.view) - lo, hi = self.limits if self.shared_clim else self._limits(values) - mesh = ax.pcolormesh(xg, yg, values, shading="auto", vmin=lo, vmax=hi, cmap=self.cmap) - ax.set(xlabel=xlabel, ylabel=ylabel, aspect="equal" if self.equal_aspect else "auto") - ax.grid(False) - return mesh - - def frame_title(self, index): - return f"{self.title} at {self.view.sweep} = {float(self.data[self.view.sweep][index]):.3e}" - - def indices(self, step): - if not isinstance(step, (int, np.integer)) or step < 1: - raise ValueError("step must be a positive integer") - validate_array(self.data, required_dims=(self.view.sweep,)) - if not self.data.sizes[self.view.sweep]: - raise ValueError("cannot render an empty sweep") - return range(0, self.data.sizes[self.view.sweep], step) - - -def plot_slice( - data: xr.DataArray, - *, - view=None, - ax=None, - vmin=None, - vmax=None, - equal_aspect=None, - title=None, - run_label=None, - cmap=None, - shared_clim=True, -): - """Render one selected two-dimensional slice.""" - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - shared_clim=shared_clim, - ) - run_label = shared_run_label(data) if run_label is None else run_label - own_figure = ax is None - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - mesh = renderer.draw(ax, renderer.data) - fig.colorbar(mesh, ax=ax, label=value_label(data)) - ax.set_title(renderer.title) - _finish(fig, run_label=run_label if own_figure else "", tight=own_figure) - return PlotResult(fig, ax, [mesh]) - - -def plot_panels( - data: xr.DataArray, - *, - view=None, - nrows=3, - ncols=4, - shared_clim=True, - title=None, - run_label=None, - vmin=None, - vmax=None, - cmap=None, - equal_aspect=None, -): - """Plot snapshots with common color limits over the entire selected sweep by default.""" - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - ) - renderer.indices(1) - if nrows < 1 or ncols < 1: - raise ValueError("nrows and ncols must be positive") - sweep = renderer.view.sweep - indices = np.linspace(0, renderer.data.sizes[sweep] - 1, nrows * ncols).astype(int) - run_label = shared_run_label(data) if run_label is None else run_label - with plt.rc_context(STRUPHY_STYLE): - fig, axes = plt.subplots( - nrows, - ncols, - figsize=(3.5 * ncols, 2.8 * nrows), - sharex=True, - sharey=True, - squeeze=False, - layout="constrained", - ) - meshes = [] - for ax, index in zip(axes.ravel(), indices): - mesh = renderer.draw(ax, renderer.data.isel({sweep: int(index)})) - meshes.append(mesh) - ax.set_title(f"{sweep} = {float(renderer.data[sweep][index]):.3e}") - if not shared_clim: - fig.colorbar(mesh, ax=ax, label=value_label(data)) - if shared_clim: - fig.colorbar(meshes[-1], ax=list(axes.ravel()), label=value_label(data)) - fig.suptitle(" — ".join(filter(None, (renderer.title, run_label)))) - return PlotResult(fig, axes, meshes) - - -class InteractiveSliceViewer: - """Slider view with the same rendering options as static and exported slices.""" - - def __init__( - self, - data: xr.DataArray, - *, - view=None, - vmin=None, - vmax=None, - run_label=None, - shared_clim=True, - cmap=None, - equal_aspect=None, - title=None, - ): - self.data = validate_array(data) - self.view = view or View() - self.options = dict( - vmin=vmin, vmax=vmax, shared_clim=shared_clim, cmap=cmap, equal_aspect=equal_aspect, title=title - ) - self.run_label = shared_run_label(data) if run_label is None else run_label - self.result = None - self.sliders = {} - - def show(self): - (self.result or self.draw()).show() - return self - - def _ipython_display_(self): - (self.result or self.draw())._ipython_display_() - - def draw(self): - if self.result is not None: - return self.result - renderer = _SliceRenderer(self.data, self.view, **self.options) - base = renderer.data - x, y = self.view.x, self.view.y - if x is None or y is None: - candidates = [dim for dim in base.dims if dim != self.view.sweep] - if len(candidates) < 2: - raise ValueError("viewer needs two display dimensions") - x, y = candidates[:2] - renderer.view = View(x=x, y=y, coordinates=self.view.coordinates, plane=self.view.plane) - controls = [dim for dim in base.dims if dim not in {x, y}] - indices = {dim: 0 for dim in controls} - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() - fig.subplots_adjust(bottom=0.13 + 0.05 * len(controls)) - mesh = renderer.draw(ax, base.isel(indices)) - colorbar = fig.colorbar(mesh, ax=ax, label=value_label(self.data)) - self.result = PlotResult(fig, ax, [mesh]) - - def update(_=None): - for dim, slider in self.sliders.items(): - indices[dim] = int(slider.val) - self.result.artists[0].remove() - mesh = renderer.draw(ax, base.isel(indices)) - self.result.artists[:] = [mesh] - colorbar.update_normal(mesh) - values = ", ".join(f"{dim}={float(base[dim][index]):.3e}" for dim, index in indices.items()) - ax.set_title(" at ".join(filter(None, (renderer.title, values)))) - fig.canvas.draw_idle() - - for row, dim in enumerate(controls): - if base.sizes[dim] == 1: - continue - slider_ax = fig.add_axes([0.20, 0.05 + 0.05 * row, 0.60, 0.025]) - slider = Slider(slider_ax, dim, 0, base.sizes[dim] - 1, valstep=1) - slider.on_changed(update) - self.sliders[dim] = slider - update() - _finish(fig, run_label=self.run_label, tight=False) - # Keep widget callbacks alive even if only the PlotResult is retained. - self.result.data["viewer"] = self - return self.result - - -def animate_slices( - data: xr.DataArray, - *, - view=None, - interval=100, - step=1, - vmin=None, - vmax=None, - shared_clim=True, - cmap=None, - equal_aspect=None, - title=None, -): - """Animate slices with fixed color limits over the selected sweep by default.""" - from matplotlib.animation import FuncAnimation - - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - ) - frames = renderer.indices(step) - sweep = renderer.view.sweep - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() - mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) - colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) - _finish(fig, run_label=shared_run_label(data)) - - def update(index): - nonlocal mesh - mesh.remove() - mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) - colorbar.update_normal(mesh) - ax.set_title(renderer.frame_title(index)) - return (mesh,) - - animation = FuncAnimation(fig, update, frames=frames, interval=interval, blit=False) - _detach_figure(fig) - return animation - - -def save_frames( - data: xr.DataArray, - directory, - *, - view=None, - step=1, - prefix="frame", - dpi=110, - vmin=None, - vmax=None, - shared_clim=True, - cmap=None, - equal_aspect=None, - title=None, -): - """Export the configured sweep as PNGs, sharing color limits by default.""" - renderer = _SliceRenderer( - data, - view or View(), - vmin=vmin, - vmax=vmax, - shared_clim=shared_clim, - cmap=cmap, - equal_aspect=equal_aspect, - title=title, - ) - frames = renderer.indices(step) - directory = Path(directory) - directory.mkdir(parents=True, exist_ok=True) - paths = [] - with plt.rc_context(STRUPHY_STYLE): - fig, ax = plt.subplots() - try: - sweep = renderer.view.sweep - mesh = renderer.draw(ax, renderer.data.isel({sweep: 0})) - colorbar = fig.colorbar(mesh, ax=ax, label=value_label(data)) - _finish(fig, run_label=shared_run_label(data)) - for frame, index in enumerate(frames): - mesh.remove() - mesh = renderer.draw(ax, renderer.data.isel({sweep: index})) - colorbar.update_normal(mesh) - ax.set_title(renderer.frame_title(index)) - path = directory / f"{prefix}_{frame:04d}.png" - fig.savefig(path, dpi=dpi, bbox_inches="tight") - paths.append(str(path)) - finally: - plt.close(fig) - return paths - - -def plot_scalars(scalars, *, names=None, exclude=SCALARS_EXCLUDE, relative_to=None, logy=False, run_label=None): - """Plot every scalar time series in one axes.""" - selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: - raise ValueError("no scalars to plot") - run_label = shared_run_label([scalars[name] for name in selected]) if run_label is None else run_label - fig, ax = plt.subplots(layout="constrained") - for name in selected: - values = scalars[name] / scalars[relative_to] if relative_to else scalars[name] - ax.plot(values.t, values, label=name) - if logy: - ax.set_yscale("log") - units = {scalars[name].attrs.get("units", "") for name in selected} - ylabel = f"quantity / {relative_to}" if relative_to else (f"[{units.pop()}]" if len(units) == 1 else "[a.u.]") - ax.set(xlabel=axis_label(scalars[selected[0]], "t"), ylabel=ylabel, title="Scalars") - ax.legend(fontsize="small") - if run_label: - fig.suptitle(run_label, fontsize="small") - return PlotResult(fig, ax, list(ax.lines)) - - -def save_all_scalars( - scalars, - directory, - *, - names=None, - exclude=SCALARS_EXCLUDE, - logy=False, - run_label=None, - table="csv", - file_format="png", - dpi=110, -): - """Write a table, scalar overview and one figure per scalar.""" - selected = scalar_names(scalars, names=names, exclude=exclude) - if not selected: - return [] - directory = Path(directory) - directory.mkdir(parents=True, exist_ok=True) - paths = [] - if table: - paths.append(save_scalars(scalars, str(directory / f"scalars.{table}"), names=selected, fmt=table)) - overview = plot_scalars(scalars, names=selected, logy=logy, run_label=run_label) - path = directory / f"scalars.{file_format}" - overview.save(path, dpi=dpi, close=True) - paths.append(str(path)) - for name in selected: - result = plot_timeseries(scalars[name], logy=logy, title=name, run_label=run_label) - path = directory / f"{name}.{file_format}" - result.save(path, dpi=dpi, close=True) - paths.append(str(path)) - return paths - - -def plot_marker_trajectories(orbits: xr.DataArray, *, ax=None, max_markers=200, show_paths=None): - """Plot a static 3-D trajectory overview; interactive marker UI is intentionally separate.""" - validate_array(orbits, required_dims=("t", "marker", "quantity")) - count = min(orbits.sizes["marker"], max_markers) - positions = np.asarray(orbits.isel(marker=slice(0, count)).sel(quantity=["x", "y", "z"])) - fig = plt.figure() if ax is None else ax.figure - ax = fig.add_subplot(111, projection="3d") if ax is None else ax - show_paths = count <= 200 if show_paths is None else show_paths - artists = [] - if show_paths: - for marker in range(count): - artists.extend(ax.plot(*positions[:, marker].T, lw=0.8, alpha=0.5)) - artists.append(ax.scatter(*positions[-1].T, s=8)) - ax.set(xlabel="X", ylabel="Y", zlabel="Z", title="Marker trajectories") - return PlotResult(fig, ax, artists) - - -def plot_equilibrium_profile(path_out, *, ax=None): - """Plot radial equilibrium profiles from ``geometry.vts``.""" - import pyvista as pv - - equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) - shape = equilibrium.dimensions - grid = np.reshape(equilibrium.points, shape + (3,)) - radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2) - pressure = np.reshape(equilibrium.point_data["p0"], shape) - fig, ax = plt.subplots() if ax is None else (ax.figure, ax) - ax.plot(radius[0, 0], pressure[0, 0], label=r"$p_0$") - if "n0" in equilibrium.point_data: - density = np.reshape(equilibrium.point_data["n0"], shape) - ax.plot(radius[0, 0], density[0, 0], label=r"$n_0$") - ax.plot(radius[0, 0], pressure[0, 0] / density[0, 0], label=r"$T_0$") - ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") - ax.legend() - return PlotResult(fig, ax, list(ax.lines)) diff --git a/postprocessing_external/tests/test_analysis_and_core_output.py b/postprocessing_external/tests/test_analysis_and_core_output.py deleted file mode 100644 index 48bfdf46f..000000000 --- a/postprocessing_external/tests/test_analysis_and_core_output.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Tests for distribution reductions, SI conversion and profiling access.""" - -import os -import time - -import numpy as np -import pytest -import xarray as xr -from struphy_plots.analysis import spatial_average, velocity_moments - -from struphy.post_processing.arrays import data_array -from struphy.post_processing.output import Output -from struphy.post_processing.tests.test_output import write_tree - -F = "kinetic_ions/f" - - -def gaussian(v, density, mean, variance): - return density * np.exp(-((v - mean) ** 2) / (2 * variance)) / np.sqrt(2 * np.pi * variance) - - -def binned(values, dims, coords, name="f"): - return data_array(values, dims, coords, name=name, label="$f$") - - -@pytest.fixture -def run(tmp_path): - return Output(write_tree(str(tmp_path))) - - -# --- reductions --------------------------------------------------------------------------------- - - -def test_moments_of_a_maxwellian_recover_its_parameters(): - v = np.linspace(-8, 8, 321) - density = np.array([1.0, 2.0])[:, None, None] # depends on t - mean = np.array([-0.5, 0.0, 0.5])[None, :, None] # depends on e1 - f = binned( - gaussian(v[None, None, :], density, mean, 0.64), - ("t", "e1", "v1"), - {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": v}, - ) - moments = velocity_moments(f) - assert moments.density.dims == ("t", "e1") - np.testing.assert_allclose(moments.density, np.broadcast_to(density[:, :, 0], (2, 3)), rtol=1e-8) - np.testing.assert_allclose(moments.mean_v1, np.broadcast_to(mean[:, :, 0], (2, 3)), atol=1e-8) - np.testing.assert_allclose(moments.variance_v1, 0.64, rtol=1e-8) - - -def test_moments_over_two_velocity_dimensions_are_taken_per_direction(): - v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) - f = binned( - (gaussian(v1[:, None], 1.0, 1.0, 0.5) * gaussian(v2[None, :], 3.0, -0.5, 0.25))[None], - ("t", "v1", "v2"), - {"t": [0.0], "v1": v1, "v2": v2}, - ) - moments = velocity_moments(f) - assert set(moments.data_vars) == {"density", "mean_v1", "variance_v1", "mean_v2", "variance_v2"} - np.testing.assert_allclose(moments.density, 3.0, rtol=1e-8) - np.testing.assert_allclose(moments.mean_v1, 1.0, atol=1e-8) - np.testing.assert_allclose(moments.variance_v1, 0.5, rtol=1e-8) - np.testing.assert_allclose(moments.mean_v2, -0.5, atol=1e-8) - np.testing.assert_allclose(moments.variance_v2, 0.25, rtol=1e-8) - - -def test_one_velocity_dimension_can_be_selected(): - v1, v2 = np.linspace(-9, 9, 181), np.linspace(-6, 6, 121) - f = binned(np.ones((1, 181, 121)), ("t", "v1", "v2"), {"t": [0.0], "v1": v1, "v2": v2}) - moments = velocity_moments(f, dims="v2") - assert moments.density.dims == ("t", "v1") - assert "mean_v1" not in moments - - -def test_delta_f_has_only_a_density(): - v = np.linspace(-3, 3, 7) - delta_f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}, name="delta_f") - assert tuple(velocity_moments(delta_f).data_vars) == ("density",) - - -def test_mean_and_variance_are_nan_without_particles(): - v = np.linspace(-3, 3, 7) - f = binned(np.zeros((1, 7)), ("t", "v1"), {"t": [0.0], "v1": v}) - moments = velocity_moments(f) - assert moments.density.item() == 0.0 - assert np.isnan(moments.mean_v1.item()) and np.isnan(moments.variance_v1.item()) - - -def test_moments_carry_the_run_and_a_label(): - f = binned(np.ones((1, 7)), ("t", "v1"), {"t": [0.0], "v1": np.linspace(-3, 3, 7)}) - f.attrs.update(run="dt=0.1", run_name="sim_1") - moments = velocity_moments(f) - assert moments.attrs["run_name"] == "sim_1" - assert moments.mean_v1.attrs["run"] == "dt=0.1" - assert moments.density.attrs["label"] == "density of $f$" - - -def test_moments_reject_missing_velocity_dimensions_and_single_bins(): - no_velocity = binned(np.ones((2, 3)), ("t", "e1"), {"t": [0.0, 1.0], "e1": [0.1, 0.2, 0.3]}) - with pytest.raises(ValueError, match="none of the dimensions"): - velocity_moments(no_velocity) - with pytest.raises(ValueError, match="no dimensions"): - velocity_moments(no_velocity, dims="v1") - one_bin = binned(np.ones((1, 1)), ("t", "v1"), {"t": [0.0], "v1": [0.0]}) - with pytest.raises(ValueError, match="at least two bins"): - velocity_moments(one_bin) - - -def test_spatial_average_removes_the_space_dimensions_only(): - values = np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4) - f = binned(values, ("t", "e1", "v1"), {"t": [0.0, 1.0], "e1": [0.1, 0.5, 0.9], "v1": np.arange(4.0)}) - f.attrs["run_name"] = "sim_1" - mean = spatial_average(f) - assert mean.dims == ("t", "v1") - np.testing.assert_allclose(mean, values.mean(axis=1)) - assert mean.attrs["run_name"] == "sim_1" - assert mean.attrs["label"] == "average of $f$" - assert spatial_average(f, dims="e1").dims == ("t", "v1") - - -def test_spatial_average_drops_physical_coordinates_it_averaged_over(): - logical = {f"e{i + 1}": np.linspace(0, 1, n) for i, n in enumerate((3, 4, 1))} - mapped = np.meshgrid(*logical.values(), indexing="ij") - field = data_array( - np.ones((2, 3, 4, 1)), - ("t", "e1", "e2", "e3"), - {"t": [0.0, 1.0], **logical, "X": (("e1", "e2", "e3"), mapped[0])}, - name="E", - ) - mean = spatial_average(field) - assert mean.dims == ("t",) - assert "X" not in mean.coords - - -def test_spatial_average_needs_space_dimensions(): - series = data_array(np.ones(3), ("t",), {"t": [0.0, 1.0, 2.0]}, name="energy") - with pytest.raises(ValueError, match="none of the dimensions"): - spatial_average(series) - - -def test_reductions_are_available_from_external_helpers_and_the_accessor(run): - product = run.evaluate(F) - moments = velocity_moments(product) - # write_tree has f = 1 on v = -3..3 in unit bins: n = 7, u = 0 and = 4 - np.testing.assert_allclose(moments.density, 7.0) - np.testing.assert_allclose(moments.mean_v1, 0.0, atol=1e-12) - np.testing.assert_allclose(moments.variance_v1, 4.0) - assert moments.density.attrs["run_name"] == run.path_out.name - - average = spatial_average(product) - assert average.dims == ("t", "v1") - xr.testing.assert_identical(average, product.struphy.analysis.spatial_average()) - xr.testing.assert_identical(moments, product.struphy.analysis.velocity_moments()) - - -# --- SI units --------------------------------------------------------------------------------- - - -def test_coordinates_are_converted_and_values_left_alone(run): - units = run.units - f = run.to_si(F) - np.testing.assert_allclose(f.v1, run.evaluate(F).v1 * units.v) - assert f.v1.attrs["units"] == "m/s" - np.testing.assert_allclose(f.t, run.evaluate(F).t * units.t) - assert f.t.attrs["units"] == "s" - assert "t_seconds" not in f.coords - np.testing.assert_array_equal(f.e1, run.evaluate(F).e1) # logical coordinates are dimensionless - np.testing.assert_array_equal(f, run.evaluate(F)) - assert "units" not in f.attrs - assert run.evaluate(F).v1.attrs.get("units") is None # the run's own product is untouched - assert "t_seconds" in run.evaluate(F).coords - - -def test_mapped_coordinates_are_scaled_by_the_length_unit(run): - assert run.units.x == 2.0 - field = run.to_si("em_fields/E") - np.testing.assert_allclose(field.X, run.evaluate("em_fields/E").X * 2.0) - assert field.X.attrs["units"] == "m" - - -def test_values_are_converted_with_a_named_unit(run): - field = run.to_si("em_fields/E", "B") - np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.B) - assert field.attrs["units"] == "T" - assert field.name == "E" - assert field.attrs["run_name"] == run.path_out.name - - -def test_values_are_converted_with_a_composite_unit(run): - field = run.to_si("em_fields/E", run.units.v * run.units.B, label="V/m") - np.testing.assert_allclose(field, run.evaluate("em_fields/E") * run.units.v * run.units.B) - assert field.attrs["units"] == "V/m" - - -def test_conversion_is_idempotent_for_coordinates_and_refuses_values_twice(run): - once = run.to_si(F) - np.testing.assert_array_equal(run.to_si(once).v1, once.v1) - converted = run.to_si("em_fields/E", "B") - with pytest.raises(ValueError, match="already has units"): - run.to_si(converted, "B") - - -def test_unknown_units_are_rejected(run): - with pytest.raises(ValueError, match="unknown unit"): - run.to_si("em_fields/E", "furlong") - - -def test_physical_time_units_are_not_converted_twice(tmp_path): - physical = Output(write_tree(str(tmp_path))).with_time_units("physical") - np.testing.assert_allclose(physical.to_si(F).t, physical.evaluate(F).t) - - -# --- profiling -------------------------------------------------------------------------------- - - -def write_profile(path_out, *, calls=3, setup=True): - from scope_profiler import ProfileManager, ProfilingOptions - - with ProfileManager.session( - options=ProfilingOptions(), - deactivate_profiling=False, - file_path=os.path.join(path_out, "profiling_data.h5"), - ): - if setup: - with ProfileManager.profile_region("setup: total"): - time.sleep(0.001) - for _ in range(calls): - with ProfileManager.profile_region("prop: A"): - with ProfileManager.profile_region("kernel: k"): - time.sleep(0.01) - - -@pytest.fixture -def profiled(tmp_path, capfd): - write_profile(write_tree(str(tmp_path))) - return Output(str(tmp_path)) - - -def test_a_run_without_profiling_says_how_to_enable_it(run): - with pytest.raises(FileNotFoundError, match="profiling_activated=True"): - run.profile - - -def test_summary_lists_every_region_with_times(profiled): - summary = profiled.profile.summary() - assert summary.sizes == {"region": 4} - assert list(summary.region.values[:1]) == ["scope_profiler.session"] - assert summary.region.values[-1] == "setup: total" - assert summary.calls.sel(region="prop: A").item() == 3 - assert summary.calls.sel(region="setup: total").item() == 1 - assert summary.total_time.sel(region="kernel: k").item() >= 0.03 - assert summary.mean_time.sel(region="kernel: k").item() >= 0.01 - assert summary.fraction.sel(region="scope_profiler.session").item() == pytest.approx(1.0) - assert 0 < summary.fraction.sel(region="prop: A").item() <= 1.0 - assert summary.attrs["run"] == profiled.label - assert summary.attrs["num_ranks"] == 1 - assert summary.total_time.attrs["units"] == "s" - - -def test_summary_filters_and_sorts(profiled): - profile = profiled.profile - assert list(profile.summary(prefix="kernel:").region.values) == ["kernel: k"] - assert len(profile.summary(top=2).region) == 2 - by_calls = profile.summary(sort_by="calls") - assert by_calls.region.values[0] == "prop: A" - with pytest.raises(ValueError, match="cannot sort by"): - profile.summary(sort_by="size") - - -def test_the_profile_is_cached_and_reset_with_the_output(profiled): - assert profiled.profile is profiled.profile - first = profiled.profile - profiled.clear_cache() - assert profiled.profile is not first - - -def test_table_is_readable_text(profiled): - table = profiled.profile.table(top=3) - lines = table.splitlines() - assert lines[0].startswith("Profile: ") and "1 rank(s)" in lines[0] - assert "Total [s]" in lines[2] - assert len(lines) == 4 + 3 - assert "scope_profiler.session" in table and "100.0%" in table - - -def test_runs_are_compared_side_by_side(tmp_path, capfd): - first = Output(write_tree(str(tmp_path / "a"))) - second = Output(write_tree(str(tmp_path / "b"))) - write_profile(first.path_out, calls=3) - write_profile(second.path_out, calls=2, setup=False) - - table = first.profile.compare(second, metric="calls") - assert set(table.dims) == {"region", "run"} - assert list(table.run.values) == [f"{first.label} [a]", f"{second.label} [b]"] # identical labels are told apart - assert table.sel(run=table.run.values[0], region="prop: A").item() == 3 - assert table.sel(run=table.run.values[1], region="prop: A").item() == 2 - assert np.isnan(table.sel(run=table.run.values[1], region="setup: total").item()) - assert table.name == "calls" - assert first.profile.compare(second, prefix="kernel:").region.values.tolist() == ["kernel: k"] - assert first.profile.compare(second.profile).shape == table.shape # a Profile works as well as an Output - - with pytest.raises(ValueError, match="cannot compare"): - first.profile.compare(second, metric="size") diff --git a/postprocessing_external/tests/test_output_accessors.py b/postprocessing_external/tests/test_output_accessors.py deleted file mode 100644 index 1e2cce492..000000000 --- a/postprocessing_external/tests/test_output_accessors.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Tests for run.plot, run.analysis and product lookup by name.""" - -import os - -import h5py -import matplotlib - -matplotlib.use("Agg") - -import numpy as np # noqa: E402 -import pytest # noqa: E402 -import struphy_plots # noqa: F401, E402 -from matplotlib import pyplot as plt # noqa: E402 -from struphy_plots.analysis import damping_rate, envelope, growth_rate, norm -from struphy_plots.output_accessors import OutputPlots -from struphy_plots.plotting import save_all_scalars - -from struphy.post_processing.output import Output # noqa: E402 -from struphy.post_processing.tests.test_output import write_manifest, write_tree # noqa: E402 - -RATE = 2.0 - - -def make_run(root, name="sim_1"): - path = os.path.join(root, name) - os.makedirs(path) - write_tree(path) - with h5py.File(os.path.join(path, "data", "data_proc0.hdf5"), "a") as file: - time = np.asarray(file["time/value"]) - file.create_dataset("scalar/en_phi", data=np.exp(RATE * time)) - write_manifest(path) - return Output(path) - - -@pytest.fixture -def run(tmp_path): - return make_run(str(tmp_path)) - - -@pytest.fixture(autouse=True) -def close_figures(): - yield - plt.close("all") - - -def scalar(run, name): - return run.evaluate("scalars", variables=name)[name] - - -def distribution(run): - return run.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") - - -def density(run): - return run.evaluate("kinetic_ions/n", dataset="view_0/n") - - -def orbits(run): - return run.evaluate("kinetic_ions/orbits") - - -def test_products_are_found_by_name(run): - assert scalar(run, "en_tot").dims == ("t",) - assert run.evaluate("em_fields/E").dims[:2] == ("t", "component") - assert distribution(run).dims == ("t", "e1", "v1") - assert density(run).dims == ("t", "e1", "e2", "e3") - assert orbits(run).dims == ("t", "marker", "quantity") - with pytest.raises(ValueError, match="species/variable"): - run.evaluate("t") - - -def test_every_array_carries_its_run(run): - for array in (run.scalars.en_tot, run.fields.em_fields.E, orbits(run)): - assert array.attrs["run"] == run.label - assert array.attrs["run_name"] == "sim_1" - assert run.scalars.en_tot.isel(t=slice(1, None)).attrs["run_name"] == "sim_1" - - -def test_timeseries_by_name_with_growth_fit(run): - result = scalar(run, "en_phi").struphy.plot.timeseries(fit=True) - assert result.fit_results[0].rate == pytest.approx(RATE) - assert result.fig._suptitle.get_text() == run.label - - -def test_output_keeps_only_the_core_quick_plot(run): - assert not hasattr(run, "timeseries") - assert callable(run.plot) - - -def test_timeseries_of_several_runs_are_labeled_by_run(tmp_path): - first, second = make_run(str(tmp_path), "sim_1"), make_run(str(tmp_path), "sim_2") - result = first.scalars.en_phi.struphy.plot.timeseries(second.scalars.en_phi) - labels = [text.get_text() for text in result.ax.get_legend().get_texts()] - assert labels == ["en phi (sim_1)", "en phi (sim_2)"] - - -def test_timeseries_into_given_axes_keeps_the_figure_layout(run): - fig, ax = plt.subplots() - fig.suptitle("mine") - scalar(run, "en_tot").struphy.plot.timeseries(ax=ax, logy=False) - assert fig._suptitle.get_text() == "mine" - - -def test_scalar_overview_draws_every_scalar_in_one_axes(run): - result = OutputPlots(run).scalars() - fig, ax = result.fig, result.ax - assert sorted(line.get_label() for line in ax.lines) == ["en_phi", "en_tot"] - assert fig.axes == [ax] - - -def test_slices_panels_and_viewer_take_keyword_views(run): - product = distribution(run) - assert product.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(product.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - viewer = run.evaluate("em_fields/E").struphy.plot.viewer(x="e1", y="e2", component=0) - viewer.draw() - assert set(viewer.sliders) == {"t", "e3"} - - -def test_orbits_plot_their_trajectories(run): - assert run.kinetic_ions.orbits.struphy.plot.trajectories().ax.name == "3d" - - -def test_report_is_written_below_post_processing(run): - paths = save_all_scalars(run.scalars, run.path_pproc / "report", run_label=run.label) - assert all(path.startswith(str(run.path_pproc / "report")) for path in paths) - assert {os.path.basename(path) for path in paths} >= {"scalars.csv", "scalars.png", "en_phi.png"} - - -def test_analysis_by_name(run): - assert scalar(run, "en_phi").struphy.analysis.growth_rate(window=(0.0, None)).rate == pytest.approx(RATE) - assert scalar(run, "en_phi").struphy.analysis.growth_rate(amplitude=True).rate == pytest.approx(RATE / 2) - np.testing.assert_allclose(scalar(run, "en_tot").struphy.analysis.relative_error(), 0.0) - np.testing.assert_allclose(scalar(run, "en_phi").struphy.analysis.drift().isel(t=0), 0.0) - - -def test_dispersion_rejects_fields_in_seconds(run): - physical = run.with_time_units("physical") - with pytest.raises(ValueError, match="normalized"): - physical.fields.em_fields.E.struphy.analysis.dispersion() - - -def test_selection_keywords_take_positions_values_and_ends(run): - product = distribution(run) - times = product.t.values - - by_position = product.struphy.plot.slice(x="e1", y="v1", t=-1) - by_value = product.struphy.plot.slice(x="e1", y="v1", t=float(times[-1])) - by_end = product.struphy.plot.slice(x="e1", y="v1", t="last") - for result in (by_value, by_end): - np.testing.assert_allclose(result.artists[0].get_array(), by_position.artists[0].get_array()) - - with pytest.raises(TypeError, match="not a dimension"): - product.struphy.plot.slice(x="e1", y="v1", time=-1) - with pytest.raises(TypeError, match="use a number"): - product.struphy.plot.slice(x="e1", y="v1", t="final") - - -def test_products_of_one_species_sit_on_the_output(run): - assert run.kinetic_ions.e1_v1_density.f.dims == ("t", "e1", "v1") - assert run.kinetic_ions.view_0.n.dims == ("t", "e1", "e2", "e3") - assert run.kinetic_ions.orbits.dims == ("t", "marker", "quantity") - assert run.em_fields.E.dims[:2] == ("t", "component") - assert {"kinetic_ions", "em_fields"} <= set(dir(run)) - with pytest.raises(AttributeError, match="available species"): - run.electrons - - -def test_product_namespaces_expose_a_scoped_lazy_catalog(run): - products = run.kinetic_ions - assert tuple(sorted(products.catalog)) == ("e1_v1_density/delta_f", "e1_v1_density/f", "orbits", "view_0/n") - assert "e1_v1_density/f" in products.catalog - assert "em_fields/E" not in products.catalog - assert "e1_v1_density/f" in repr(products) - assert run.distribution_catalog._cache == {} - assert products["e1_v1_density/f"].dims == ("t", "e1", "v1") - assert run.distribution_catalog._cache["kinetic_ions/e1_v1_density/f"] is products.catalog["e1_v1_density/f"] - - -def test_arrays_plot_themselves(run): - phase_space = run.kinetic_ions.e1_v1_density.f - assert phase_space.struphy.plot.slice(x="e1", y="v1", t="last").ax.get_xlabel() == r"$\eta_1$" - assert len(phase_space.struphy.plot.panels(x="e1", y="v1", nrows=1, ncols=2).artists) == 2 - assert set(phase_space.struphy.plot.viewer(x="e1", y="v1").sliders) == set() - assert run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=2).ax.name == "3d" - - -def test_the_accessor_works_on_derived_arrays(run): - energy = run.scalars.en_phi - assert energy.isel(t=slice(1, None)).struphy.analysis.growth_rate().rate == pytest.approx(RATE) - error = energy.struphy.analysis.relative_error() - assert error.struphy.plot.timeseries(logy=False).fig._suptitle.get_text() == run.label - - -def test_products_by_name_and_by_attribute_agree(run): - by_output = distribution(run).struphy.plot.slice(x="e1", y="v1", t="last") - by_attribute = run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", t="last") - np.testing.assert_allclose(by_output.artists[0].get_array(), by_attribute.artists[0].get_array()) - assert by_output.fig._suptitle.get_text() == by_attribute.fig._suptitle.get_text() == run.label - - -def test_selection_rejects_unknown_dimensions(run): - with pytest.raises(TypeError, match="not a dimension"): - run.kinetic_ions.e1_v1_density.f.struphy.plot.slice(x="e1", y="v1", time=-1) - - -def oscillating_energy(rate=-0.3, omega=3.0): - import xarray as xr - - time = np.linspace(0.0, 20.0, 4001) - values = np.exp(2 * rate * time) * np.cos(omega * time) ** 2 + 1e-12 - return xr.DataArray(values, dims="t", coords={"t": time}, name="energy") - - -def test_damping_rate_fits_the_envelope_not_the_oscillation(run): - energy = oscillating_energy(rate=-0.3) - fit = energy.struphy.analysis.damping_rate(amplitude=True) - assert fit.rate == pytest.approx(-0.3, rel=1e-2) - assert energy.struphy.analysis.damping_rate(window=(2.0, 10.0), amplitude=True).rate == pytest.approx( - -0.3, rel=1e-2 - ) - - peaks = energy.struphy.analysis.envelope() - assert 0 < peaks.sizes["t"] < energy.sizes["t"] // 10 - assert np.all(peaks > 1e-3 * np.exp(-0.6 * peaks.t)) - - -def test_damping_rate_without_peaks_is_none(run): - assert scalar(run, "en_phi").struphy.analysis.damping_rate() is None - - -def test_norm_reduces_all_but_time(run): - e_field = run.evaluate("em_fields/E") - squared = e_field.struphy.analysis.norm(squared=True) - assert squared.dims == ("t",) - np.testing.assert_allclose(squared, (np.asarray(e_field) ** 2).sum(axis=(1, 2, 3, 4))) - assert e_field.struphy.analysis.norm(dims=["e1"]).dims == ("t", "component", "e2", "e3") - assert growth_rate(squared, fit=None) is not None - - -def test_physical_coords_are_attached_to_products_without_them(run): - density_data = density(run) - assert "X" not in density_data.coords - mapped = run.with_physical_coords(density_data) - expected = run.domain(*(np.asarray(density_data[dim]) for dim in ("e1", "e2", "e3"))) - for name, values in zip(("X", "Y", "Z"), expected): - assert mapped[name].dims == ("e1", "e2", "e3") - np.testing.assert_allclose(mapped[name], values) - - plane = run.with_physical_coords(density_data.isel(e3=0, drop=True)) - assert plane.X.dims == ("e1", "e2") - - phase_space = run.with_physical_coords(distribution(run)) - assert phase_space.X.dims == ("e1",) - - field = run.evaluate("em_fields/E") - assert run.with_physical_coords(field) is field - with pytest.raises(ValueError, match="no logical dimensions"): - run.with_physical_coords(scalar(run, "en_tot")) diff --git a/postprocessing_external/tests/test_plotting.py b/postprocessing_external/tests/test_plotting.py deleted file mode 100644 index 4608f95ef..000000000 --- a/postprocessing_external/tests/test_plotting.py +++ /dev/null @@ -1,298 +0,0 @@ -"""Tests for functional plotting and the shared view recipe.""" - -import matplotlib -import numpy as np -import pytest -import xarray as xr - -matplotlib.use("Agg") -from matplotlib import pyplot as plt # noqa: E402 -from struphy_plots.plotting import ( # noqa: E402 - GrowthFit, - InteractiveSliceViewer, - View, - animate_slices, - drift, - growth_rate, - logical_grids, - physical_grids, - plot_lineout, - plot_panels, - plot_scalars, - plot_slice, - plot_timeseries, - plot_vector, - plot_volume_slices, - pyvista_volume, - relative_error, - save_all_scalars, - save_frames, -) - -from struphy.post_processing.arrays import data_array # noqa: E402 - -pytestmark = pytest.mark.filterwarnings("ignore:Animation was deleted") - - -@pytest.fixture(autouse=True) -def close_figures(): - yield - plt.close("all") - - -def phase_space(nt=6): - return data_array( - np.arange(nt * 4 * 5).reshape(nt, 4, 5), - ("t", "e1", "v1"), - {"t": np.linspace(0, 1, nt), "e1": np.linspace(0, 1, 4), "v1": np.linspace(-2, 2, 5)}, - name="f", - label="$f$", - coord_units={"t": "s"}, - ) - - -def physical_field(): - coords = {"t": [0, 1], "e1": range(3), "e2": range(4), "e3": range(5)} - grids = np.meshgrid(coords["e1"], coords["e2"], coords["e3"], indexing="ij") - coords.update({name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), grids)}) - return data_array(np.ones((2, 3, 4, 5)), ("t", "e1", "e2", "e3"), coords, name="phi") - - -def scalar_dataset(): - t = np.linspace(0, 1, 6) - return xr.Dataset({"en_tot": ("t", 2 + 0.02 * t), "en_e": ("t", 1 + 0.1 * t)}, coords={"t": t}) - - -def test_lineout_vector_and_orthogonal_volume_slices_render(): - line = plot_lineout(phase_space().isel(t=0, e1=0)) - assert len(line.artists) == 1 - assert len(phase_space().struphy.plot.lineout(x="v1", t=0, e1=0).artists) == 1 - - vector = data_array( - np.ones((2, 3, 4)), ("component", "e1", "e2"), {"component": [0, 1], "e1": range(3), "e2": range(4)} - ) - assert len(plot_vector(vector, x="e1", y="e2").artists) == 1 - assert len(vector.struphy.plot.vector(x="e1", y="e2").artists) == 1 - assert len(plot_volume_slices(physical_field().isel(t=0)).artists) == 3 - - -def test_pyvista_volume_uses_mapped_coordinates(): - plotter = pyvista_volume(physical_field().isel(t=0)) - assert plotter.renderer is not None - plotter.close() - - -def test_growth_rate_uses_only_valid_samples_inside_window(): - data = data_array([1, 0, 4, np.nan, 16], ("t",), {"t": range(5)}) - result = growth_rate(data, GrowthFit((0, 4))) - assert result is not None and np.isfinite(result.rate) - np.testing.assert_array_equal(result.time, [0, 2, 4]) - - -def test_growth_rate_does_not_fall_back_outside_requested_window(): - data = data_array(np.exp(np.arange(5)), ("t",), {"t": range(5)}) - assert growth_rate(data, GrowthFit((1.1, 1.2))) is None - - -def test_growth_rate_of_quadratic_reports_amplitude_rate(): - t = np.linspace(0, 4, 20) - result = growth_rate(data_array(np.exp(0.6 * t), ("t",), {"t": t}), GrowthFit(amplitude_from_quadratic=True)) - assert result.rate == pytest.approx(0.3) - - -def test_diagnostics_preserve_time_coordinates(): - data = data_array([2, 2.2, 1.8], ("t",), {"t": [0, 1, 2]}, label="E") - np.testing.assert_allclose(drift(data), [0, 0.2, -0.2]) - np.testing.assert_allclose(relative_error(data), [0.1, 0.1]) - np.testing.assert_array_equal(relative_error(data).t, [1, 2]) - - -def test_logical_and_physical_grids_follow_selected_dimensions(): - logical = phase_space().isel(t=0) - assert logical_grids(logical)[0].shape == (4, 5) - physical = physical_field().isel(t=0, e3=2) - assert physical_grids(physical, plane="XY")[0].shape == (3, 4) - assert physical_grids(physical, plane="RZ")[0].shape == (3, 4) - - -def test_plot_timeseries_renders_once_and_save_does_not_redraw(tmp_path): - data = data_array(np.exp(np.arange(4)), ("t",), {"t": range(4)}, label="energy", coord_units={"t": "s"}) - result = plot_timeseries(data, fit=GrowthFit(), run_label="dt=.1") - lines = len(result.ax.lines) - result.save(tmp_path / "energy.png") - assert len(result.ax.lines) == lines - assert len(plt.get_fignums()) == 1 - assert result.fig._suptitle.get_text() == "dt=.1" - - -def test_plot_slice_accepts_named_value_and_index_selection(): - result = plot_slice(phase_space(), view=View(x="e1", y="v1", select={"t": 0.52})) - assert result.ax.get_xlabel() == r"$\eta_1$" - assert len(result.artists) == 1 - - -def test_plot_slice_physical_coordinates_are_intrinsic(): - result = plot_slice( - physical_field(), view=View(x="e1", y="e2", isel={"t": 0, "e3": 2}, coordinates="physical", plane="XY") - ) - assert result.ax.get_xlabel() == "X" - assert result.ax.get_aspect() == 1.0 - - -def test_plot_slice_rejects_underspecified_selection(): - with pytest.raises(ValueError, match="selection leaves"): - plot_slice(physical_field(), view=View(x="e1", y="e2", isel={"t": 0})) - - -def test_panels_use_one_recipe_and_keep_full_title(): - result = plot_panels( - phase_space(), view=View(x="e1", y="v1"), nrows=1, ncols=2, title="Distribution", run_label="dt=.1" - ) - assert result.fig._suptitle.get_text() == "Distribution — dt=.1" - assert len(result.artists) == 2 - - -def test_viewer_builds_controls_for_every_non_display_dimension(): - viewer = InteractiveSliceViewer(physical_field(), view=View(x="e1", y="e2", coordinates="physical")) - result = viewer.draw() - assert set(viewer.sliders) == {"t", "e3"} - viewer.sliders["e3"].set_val(3) - assert result.fig is not None - - -def test_animation_and_frames_share_the_view(tmp_path): - data = phase_space(nt=7) - view = View(x="e1", y="v1") - animation = animate_slices(data, view=view, step=3) - assert len(list(animation.new_frame_seq())) == 3 - paths = save_frames(data, tmp_path, view=view, step=3) - assert len(paths) == 3 - assert all(__import__("pathlib").Path(path).exists() for path in paths) - - -def test_scalar_overview_and_export(tmp_path): - result = plot_scalars(scalar_dataset(), run_label="run") - assert sorted(line.get_label() for line in result.artists) == ["en_e", "en_tot"] - assert result.fig._suptitle.get_text() == "run" - paths = save_all_scalars(scalar_dataset(), tmp_path) - assert sorted(__import__("os").path.basename(path) for path in paths) == [ - "en_e.png", - "en_tot.png", - "scalars.csv", - "scalars.png", - ] - assert plt.get_fignums() == [result.fig.number] - - -@pytest.mark.parametrize("shown", [False, True]) -def test_notebook_display_shows_the_figure_once(monkeypatch, shown): - import IPython.display - - displayed = [] - monkeypatch.setattr(matplotlib, "get_backend", lambda: "module://matplotlib_inline.backend_inline") - monkeypatch.setattr(IPython.display, "display", displayed.append) - monkeypatch.setattr(plt, "show", lambda *args, **kwargs: None) - result = plot_timeseries(scalar_dataset().en_tot, logy=False) - if shown: - result.show() - result._ipython_display_() - assert displayed == ([] if shown else [result.fig]) - assert (result.fig.number in plt.get_fignums()) == shown, "the inline backend must not show it again" - - -def test_slice_can_display_the_sweep_dimension(): - data = phase_space() - result = plot_slice(data.isel(v1=slice(None)), view=View(x="t", y="e1", isel={"v1": 0})) - assert result.ax.get_xlabel() == "$t$ [s]" - with pytest.raises(ValueError, match="display it as x or y"): - plot_slice(data, view=View(x="e1", y="v1")) - - -def test_every_presentation_uses_the_full_selected_color_range(tmp_path, monkeypatch): - import struphy_plots # noqa: F401 - from matplotlib.figure import Figure - - data = phase_space(nt=3).astype(float) - data[1] = data[1] * 100 # extrema in a frame omitted by panels and export - view = data.struphy.plot.view(x="e1", y="v1", cmap="plasma", equal_aspect=True) - limits = (float(data.min()), float(data.max())) - assert plt.get_fignums() == [] - snapshot = view.slice(t="last") - panels = view.panels(nrows=1, ncols=2) - viewer = view.viewer() - result = viewer.draw() - viewer.sliders["t"].set_val(2) - animation = view.animation(step=2) - mesh = animation._func(2)[0] - for artist in [snapshot.artists[0], *panels.artists, result.artists[0], mesh]: - assert artist.get_clim() == limits - assert artist.get_cmap().name == "plasma" - assert artist.axes.get_aspect() == 1.0 - captured = [] - original = Figure.savefig - - def capture(fig, *args, **kwargs): - captured.append(fig.axes[0].collections[0].get_clim()) - return original(fig, *args, **kwargs) - - monkeypatch.setattr(Figure, "savefig", capture) - before = plt.get_fignums() - assert len(view.save_frames(tmp_path, step=2)) == 2 - assert captured == [limits, limits] - assert plt.get_fignums() == before - - -@pytest.mark.parametrize("shared_clim", [True, False]) -def test_explicit_color_limits_work_for_all_renderers(tmp_path, monkeypatch, shared_clim): - import struphy_plots # noqa: F401 - from matplotlib.figure import Figure - - data = phase_space(nt=2) - options = dict(x="e1", y="v1", vmin=-5, vmax=100, shared_clim=shared_clim, cmap="coolwarm") - panels = data.struphy.plot.panels(nrows=1, ncols=2, **options) - animation = data.struphy.plot.animation(**options) - viewer = data.struphy.plot.viewer(**options) - viewer.draw() - viewer.sliders["t"].set_val(1) - for mesh in [*panels.artists, animation._func(1)[0], viewer.result.artists[0]]: - assert mesh.get_clim() == (-5, 100) - assert mesh.get_cmap().name == "coolwarm" - captured = [] - monkeypatch.setattr( - Figure, "savefig", lambda fig, *args, **kwargs: captured.append(fig.axes[0].collections[0].get_clim()) - ) - data.struphy.plot.frames(tmp_path, **options) - assert captured == [(-5, 100), (-5, 100)] - - -def test_per_frame_scaling_is_explicit_and_supports_a_fixed_lower_limit(): - import struphy_plots # noqa: F401 - - data = phase_space(nt=2) - view = data.struphy.plot.view(x="e1", y="v1", shared_clim=False, vmin=-1) - panels = view.panels(nrows=1, ncols=2) - animation = view.animation() - for index in range(2): - limits = (-1, float(data.isel(t=index).max())) - assert panels.artists[index].get_clim() == limits - assert animation._func(index)[0].get_clim() == limits - - -def test_viewer_show_retains_controls_and_does_not_redraw(monkeypatch): - viewer = InteractiveSliceViewer(phase_space(), view=View(x="e1", y="v1")) - result = viewer.draw() - monkeypatch.setattr(plt, "show", lambda: None) - assert viewer.show() is viewer - assert viewer.draw() is result - assert len(plt.get_fignums()) == 1 - viewer.sliders["t"].set_val(2) - assert result.artists[0] is result.ax.collections[0] - - -@pytest.mark.parametrize("step", [0, -1]) -def test_sweep_rejects_invalid_step(tmp_path, step): - with pytest.raises(ValueError, match="positive integer"): - animate_slices(phase_space(), view=View(x="e1", y="v1"), step=step) - with pytest.raises(ValueError, match="positive integer"): - save_frames(phase_space(), tmp_path, view=View(x="e1", y="v1"), step=step) - From 239e3d65c0da6a336ce3a7c32d0f5f41d4fe254e Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 25 Sep 2026 14:26:44 +0200 Subject: [PATCH 135/193] adapt quickstart --- doc/sections/quickstart.rst | 173 ++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index fa1765770..37ced1f91 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -13,26 +13,26 @@ Solve Poisson In A Few Steps ---------------------------- Make sure that Struphy is installed and compiled (see :ref:`install_modes`). -Save the code below as ``params_poisson.py``. Output is written beside that script, -regardless of the directory from which you launch it. In a notebook, replace -``Path(__file__).resolve().parent`` with an explicit directory such as ``Path.cwd()`` -and omit ``params_path=__file__`` from the simulation constructor. +Save the code below as ``params_poisson.py``. By default, output is written to +``sim_1/`` in the directory from which you launch the script (change this with +:class:`~struphy.EnvironmentOptions`). -We search for a potential :math:`\phi(x)` satisfying the Poisson equation +We search for a potential :math:`\phi(x, y)` satisfying the Poisson equation .. math:: -\Delta \phi = \rho -for given source term :math:`\rho(x)` on a periodic 1D domain. +for a given source term :math:`\rho(x)` on a doubly periodic 2D domain. 1. Import the API and choose a model. .. code-block:: python - from pathlib import Path + import numpy as np + from matplotlib import pyplot as plt - from struphy import EnvironmentOptions, Output, Simulation, domains, grids, perturbations + from struphy import Simulation, domains, grids, perturbations from struphy.models import Poisson 2. Create the :class:`~struphy.models.poisson.Poisson` model. @@ -56,9 +56,8 @@ For periodic boundary conditions we will stabilize via ``options``. .. code-block:: python - import numpy as np - Lx = 2.0 * np.pi + Ly = 4.0 * np.pi mode = 2 k = mode * 2.0 * np.pi / Lx source_amp = k**2 + stab_eps @@ -67,94 +66,90 @@ For periodic boundary conditions we will stabilize via ``options``. model.em_fields.source.add_perturbation(fun) -5. Set the output folder, build domain and grid, then instantiate a simulation. +5. Build domain and grid, then instantiate a simulation. .. code-block:: python - script_dir = Path(__file__).resolve().parent - path_out = script_dir / "sim_data" - env = EnvironmentOptions(out_folders=str(script_dir), sim_folder=path_out.name) + domain = domains.Cuboid(r1=Lx, l2=-Ly / 2, r2=Ly / 2) + grid = grids.TensorProductGrid(num_elements=(64, 64, 1)) - domain = domains.Cuboid(l1=0.0, r1=Lx) - grid = grids.TensorProductGrid(num_elements=(64, 1, 1)) + sim = Simulation(model=model, domain=domain, grid=grid) - sim = Simulation( - model=model, - params_path=__file__, - env=env, - domain=domain, - grid=grid, - ) - -6. Run one step (enough for this stationary solve). +6. Run the simulation. ``sim.run()`` returns an :class:`~struphy.Output` object, + the entry point for all post-processing. .. code-block:: python - sim.run(one_time_step=True) + out = sim.run() -7. Open the output folder directly. Fields are post-processed when first accessed - and come as labeled :class:`xarray.DataArray` objects. +7. Evaluate the potential on a line along :math:`\eta_1` and compare to the exact solution. + ``out.evaluate()`` takes a ``"species/variable"`` name, evaluates the saved spline + field on the given logical coordinates, and returns a labeled :class:`xarray.DataArray`. + Omitted directions (here :math:`\eta_2, \eta_3`) default to the midpoint ``0.5``, + and ``t=-1`` selects the last saved snapshot. The array also carries the physical + coordinates ``X, Y, Z`` so that xarray's ``.plot()`` can use either coordinate system. .. code-block:: python - out = Output(path_out) - phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) - -``Output`` reconstructs the model, domain and numerical options lazily from -``run_metadata.json``, using their ``from_dict()`` methods. Access them as -``out.model``, ``out.domain`` or ``out.time_opts``; there is no ``out.sim``. A separate -post-processing script can use the same path without importing the parameter file. + fig, axs = plt.subplots(1, 2, figsize=(12, 4)) -8. Compare to the exact solution, and save the figure in the output folder. + eta1 = np.linspace(0, 1, 100) + phi_1d = out.evaluate("em_fields/phi", eta1=eta1, t=-1) -.. code-block:: python + x = phi_1d["X"] + phi_exact = np.cos(k * x) + phi_exact_logical = np.cos(Lx * k * eta1) - import matplotlib.pyplot as plt + phi_1d.plot(ax=axs[0], label="Struphy") # plot along eta1 (default) + phi_1d.plot(x="X", ax=axs[1], label="Struphy") # plot along physical X + axs[0].plot(eta1, phi_exact_logical, "k--", lw=1.8, label="exact") + axs[1].plot(x, phi_exact, "k--", lw=1.8, label="exact") - x = phi.X.values - phi_num = phi.values - phi_exact = np.cos(k * x) - err_max = np.max(np.abs(phi_num - phi_exact)) - - plt.figure(figsize=(7, 3.8)) - plt.plot(x, phi_exact, "k--", lw=1.8, label="exact") - plt.plot(x, phi_num, "o", ms=3.5, label="Struphy") - plt.xlabel("x") - plt.ylabel("phi") - plt.title("Struphy quickstart: Poisson solution") - plt.legend() - plt.grid(alpha=0.3) - plt.tight_layout() - plt.savefig(path_out / "quickstart_poisson_phi.png", dpi=150) + for ax in axs: + ax.legend() + ax.grid(alpha=0.3) + fig.savefig("quickstart_poisson_phi.png", dpi=150) plt.show() - print(f"max error = {err_max:.3e}") - .. figure:: ../pics/quickstart_poisson_phi.png :figwidth: 85% :alt: Poisson quickstart comparison of exact and numerical solution - Exact (dashed) and Struphy (markers) solutions from Step 8. + Exact (dashed) and Struphy solutions from Step 7, along :math:`\eta_1` (left) and :math:`x` (right). -Full script (save as ``params_poisson.py`` and run with ``python params_poisson.py``): +8. Evaluate on a 2D logical grid and plot in physical coordinates. .. code-block:: python - from pathlib import Path + eta = np.linspace(0, 1, 100) + phi_2d = out.evaluate("em_fields/phi", eta1=eta, eta2=eta, t=-1) + phi_2d.plot(x="X", y="Y") + plt.show() + +``out`` can also be created later from the output folder alone, e.g. in a separate +post-processing script, via ``out = Output("sim_1")``; the model, domain and numerical +options are reconstructed from ``run_metadata.json``. + +Full script (save as ``params_poisson.py`` and run with ``python params_poisson.py``): + +.. code-block:: python import numpy as np - from struphy import EnvironmentOptions, Output, Simulation, domains, grids, perturbations + from matplotlib import pyplot as plt + + from struphy import Simulation, domains, grids, perturbations from struphy.models import Poisson model = Poisson() stab_eps = 1e-8 - + model.propagators.poisson.options = model.propagators.poisson.Options( stab_eps=stab_eps, ) Lx = 2.0 * np.pi + Ly = 4.0 * np.pi mode = 2 k = mode * 2.0 * np.pi / Lx source_amp = k**2 + stab_eps @@ -163,38 +158,44 @@ Full script (save as ``params_poisson.py`` and run with ``python params_poisson. model.em_fields.source.add_perturbation(fun) - script_dir = Path(__file__).resolve().parent - path_out = script_dir / "sim_data" - env = EnvironmentOptions(out_folders=str(script_dir), sim_folder=path_out.name) + domain = domains.Cuboid(r1=Lx, l2=-Ly / 2, r2=Ly / 2) + grid = grids.TensorProductGrid(num_elements=(64, 64, 1)) - domain = domains.Cuboid(l1=0.0, r1=Lx) - grid = grids.TensorProductGrid(num_elements=(64, 1, 1)) + sim = Simulation(model=model, domain=domain, grid=grid) - sim = Simulation(model=model, params_path=__file__, env=env, domain=domain, grid=grid) if __name__ == "__main__": - sim.run(one_time_step=True) + # sim.run() returns an Output object for post-processing + out = sim.run() + + # out.evaluate() evaluates a saved field on logical coordinates (eta1, eta2, eta3) + # and returns a labeled xarray.DataArray; omitted etas default to 0.5, + # t=-1 selects the last snapshot. Physical coordinates X, Y, Z are attached. + fig, axs = plt.subplots(1, 2, figsize=(12, 4)) - out = Output(path_out) - phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0) - x = phi.X.values - phi_num = phi.values + eta1 = np.linspace(0, 1, 100) + phi_1d = out.evaluate("em_fields/phi", eta1=eta1, t=-1) + + x = phi_1d["X"] phi_exact = np.cos(k * x) - err_max = np.max(np.abs(phi_num - phi_exact)) - - import matplotlib.pyplot as plt - - plt.figure(figsize=(7, 3.8)) - plt.plot(x, phi_exact, "k--", lw=1.8, label="exact") - plt.plot(x, phi_num, "o", ms=3.5, label="Struphy") - plt.xlabel("x") - plt.ylabel("phi") - plt.title("Struphy quickstart: Poisson solution") - plt.legend() - plt.grid(alpha=0.3) - plt.tight_layout() - plt.savefig(path_out / "quickstart_poisson_phi.png", dpi=150) + phi_exact_logical = np.cos(Lx * k * eta1) + + # xarray plotting: along eta1 by default, or along any attached coordinate + phi_1d.plot(ax=axs[0], label="Struphy") + phi_1d.plot(x="X", ax=axs[1], label="Struphy") + axs[0].plot(eta1, phi_exact_logical, "k--", lw=1.8, label="exact") + axs[1].plot(x, phi_exact, "k--", lw=1.8, label="exact") + + for ax in axs: + ax.legend() + ax.grid(alpha=0.3) + fig.savefig("quickstart_poisson_phi.png", dpi=150) + plt.show() + + # 2D evaluation on a logical tensor-product grid, plotted in physical coordinates + eta = np.linspace(0, 1, 100) + phi_2d = out.evaluate("em_fields/phi", eta1=eta, eta2=eta, t=-1) + phi_2d.plot(x="X", y="Y") plt.show() - print(f"max error = {err_max:.3e}") Same Workflow For All Models From 73c030e22c0b7305418eb9f2550ce0c9ce3c3b93 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 14:32:03 +0200 Subject: [PATCH 136/193] Fix the to_dict() and from dict for the marker params --- src/struphy/models/base.py | 66 ++++++++++++++++++++++++++++++++++- src/struphy/simulation/sim.py | 3 ++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index f00bc649a..4a20cfa51 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -994,13 +994,77 @@ def _serialize_propagator_option(self, value): def from_dict(cls, dct) -> "StruphyModel": """Deserialize a model from :meth:`to_dict`.""" from struphy.models.utils import get_model_by_name + from struphy.particles.parameters import ( + BoundaryParameters, + LoadingParameters, + SavingParameters, + SortingParameters, + WeightsParameters, + ) params = {} for key, value in dct.get("params", {}).items(): if isinstance(value, dict) and set(value) == {"BaseUnits"}: value = BaseUnits.from_dict(value["BaseUnits"]) params[key] = value - return get_model_by_name(dct["model"])(**params) + model = get_model_by_name(dct["model"])(**params) + + parameter_types = { + "loading_params": LoadingParameters, + "weights_params": WeightsParameters, + "boundary_params": BoundaryParameters, + "sorting_params": SortingParameters, + "saving_params": SavingParameters, + } + for species_name, species_data in dct.get("species", {}).items(): + species = model.species.get(species_name) + if species is None: + continue + for variable_name, variable_data in species_data.get("variables", {}).items(): + variable = species.variables.get(variable_name) + if variable is None: + continue + if "save_data" in variable_data: + variable.save_data = variable_data["save_data"] + if isinstance(variable, PICVariable) and "n_as_volume_form" in variable_data: + variable._n_as_volume_form = variable_data["n_as_volume_form"] + + if isinstance(species, ParticleSpecies) and "loading_params" in species_data: + marker_params = { + name: parameter_types[name](**species_data[name]) + for name in parameter_types + if name in species_data + } + marker_params["bufsize"] = species_data.get("bufsize", 1.0) + species.set_markers(**marker_params) + + def restore_option(value, template): + if is_dataclass(template) and isinstance(value, dict): + for field in fields(template): + if field.init and field.name in value: + setattr(template, field.name, restore_option(value[field.name], getattr(template, field.name))) + return template + if isinstance(template, dict) and isinstance(value, dict): + return { + restore_option(key, key): restore_option(item, template.get(key)) + for key, item in value.items() + } + if isinstance(value, list): + template_item = template[0] if isinstance(template, (list, tuple)) and template else None + return [restore_option(item, template_item) for item in value] + if isinstance(value, str) and "." in value: + species_name, variable_name = value.split(".", 1) + species = model.species.get(species_name) + if species is not None and variable_name in species.variables: + return species.variables[variable_name] + return value + + for prop_name, options in dct.get("propagator_options", {}).items(): + propagator = getattr(model.propagators, prop_name, None) + if propagator is not None: + restore_option(options, propagator.options) + + return model @classmethod def from_name(cls, name: str) -> "StruphyModel": diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index f9a882cfe..92b1dec66 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1967,7 +1967,10 @@ def generate_script( sim_class_def += ")\n" + # Rebuild from the serialized configuration so post-construction model + # settings (particle parameters and propagator options) are preserved. script += sim_setup + "\n" + sim_class_def + script += f"\n# Restore the complete model configuration\nsim = Simulation.from_dict({self.to_dict()!r})\n" if include_main_guard: script += """ if __name__ == "__main__": From a379a0727595d74e67575630598968deef3db5bd Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 25 Sep 2026 14:34:58 +0200 Subject: [PATCH 137/193] adapt particel tracing notebook --- tutorials/tutorial_particle_tracing.ipynb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tutorials/tutorial_particle_tracing.ipynb b/tutorials/tutorial_particle_tracing.ipynb index c87421399..7ed57a1fc 100644 --- a/tutorials/tutorial_particle_tracing.ipynb +++ b/tutorials/tutorial_particle_tracing.ipynb @@ -325,11 +325,17 @@ "\n", "fig = plt.figure(figsize=(10, 6))\n", "\n", - "orbits = out.orbits.kinetic_ions.values\n", - "orbits_uni = out_2.orbits.kinetic_ions.values\n", + "# out.evaluate(\"/orbits\") returns an xarray.DataArray with dims (t, marker, quantity);\n", + "# the quantity coordinate labels the saved columns (\"x\", \"y\", \"z\", \"v1\", ..., \"weight\", \"id\")\n", + "orbits = out.evaluate(\"kinetic_ions/orbits\")\n", + "orbits_uni = out_2.evaluate(\"kinetic_ions/orbits\")\n", + "\n", + "# initial marker positions (first saved time step)\n", + "pos = orbits.isel(t=0)\n", + "pos_uni = orbits_uni.isel(t=0)\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", + "plt.scatter(pos.sel(quantity=\"x\"), pos.sel(quantity=\"y\"), s=2.0)\n", "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", @@ -339,7 +345,7 @@ "plt.title(\"sim_1: draw uniform in logical space\")\n", "\n", "plt.subplot(1, 2, 2)\n", - "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.0)\n", + "plt.scatter(pos_uni.sel(quantity=\"x\"), pos_uni.sel(quantity=\"y\"), s=2.0)\n", "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", @@ -1372,7 +1378,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.3)", + "display_name": "env (3.12.3.final.0)", "language": "python", "name": "python3" }, From 847eb6f2a289442b1e96fa4a427702c1bef20ffe Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 14:37:00 +0200 Subject: [PATCH 138/193] Removed Output.dispersion() and Output.plot() --- doc/markdown/output-api.md | 8 --- src/struphy/post_processing/output.py | 62 ------------------- .../post_processing/tests/test_output.py | 12 ---- 3 files changed, 82 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 6bab64cb8..84b0d2097 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -122,14 +122,6 @@ peaks = out.envelope("electric_energy") growth = out.growth_rate(out.norm("diagnostics/rho", squared=True), amplitude=True) ``` -For a saved field, `dispersion()` computes a labeled space-time power spectrum without -creating a figure: - -```python -spectrum = out.dispersion("em_fields/e_field", component=0, slice_at=(0, 0, None)) -spectrum.power.plot(x="k", y="omega") -``` - Fields carry mapped `X`, `Y`, `Z` coordinates; binned products (such as `e1_e2_density`) do not. `with_physical_coords` attaches them by evaluating the run's domain on the array's logical grid. diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index aa7cb6af2..b5713c86c 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -263,44 +263,6 @@ def compare(first: "Output", second: "Output", product: str, *, method: str = "l ratio = xr.where(right != 0, left / right, np.nan) return xr.Dataset({"first": left, "second": right, "difference": difference, "ratio": ratio}) - def dispersion( - self, - name: str, - *, - dataset: str | None = None, - component: int = 0, - slice_at: tuple = (None, 0, 0), - physical: bool = False, - fit_branches: int = 0, - noise_level: float = 0.1, - extr_order: int = 10, - fit_degree: tuple[int, ...] = (1,), - ) -> xr.Dataset: - """Compute a space-time dispersion spectrum for one saved field. - - The returned dataset has ``power(omega, k)`` and angular-frequency/wave-number - coordinates. Optional polynomial branch fits are stored as ``branch_coefficients``. - Plotting is intentionally left to the optional xarray plotting package. - """ - field = self._product(name, dataset=dataset) if dataset is not None else self._array(name) - from struphy.post_processing.spectral import compute_dispersion - - result = compute_dispersion( - field, - component=component, - slice_at=slice_at, - physical=physical, - fit_branches=fit_branches, - noise_level=noise_level, - extr_order=extr_order, - fit_degree=fit_degree, - ) - result.attrs.update(run=self.label, run_name=self.path_out.name, source=name) - result["omega"].attrs["long_name"] = "angular frequency" - result["k"].attrs["long_name"] = "wave number" - result["power"].attrs["long_name"] = "space-time power spectrum" - return result - def _reset(self): if getattr(self, "_tree", None) is not None: self._tree.close() # an open store would block the next process() from writing it @@ -741,30 +703,6 @@ def _array(self, product: str | xr.DataArray) -> xr.DataArray: return product raise TypeError(f"product must be a product name or xarray.DataArray, got {type(product).__name__}") - def plot(self, array: xr.DataArray | xr.Dataset, **kwargs: Any) -> Any: - """Make a small xarray quick-look plot. - - Time-dependent spatial data are shown at the last saved time, vector data use - the first component, and remaining dimensions beyond two are sliced at their - midpoint. For publication plots, select dimensions explicitly and call xarray's - plotting methods directly. - """ - if isinstance(array, xr.Dataset): - if len(array.data_vars) != 1: - raise ValueError("plot() needs a DataArray or a Dataset containing exactly one variable") - array = array[next(iter(array.data_vars))] - if not isinstance(array, xr.DataArray): - raise TypeError(f"plot() needs an xarray DataArray or Dataset, got {type(array).__name__}") - view = array - for dim, index in (("t", -1), ("component", 0)): - if dim in view.dims and view.ndim > 2: - view = view.isel({dim: index}) - while view.ndim > 2: - view = view.isel({view.dims[-1]: view.sizes[view.dims[-1]] // 2}) - if view.ndim == 1 and "t" in view.dims: - return view.plot.line(x="t", **kwargs) - return view.plot(**kwargs) - @property def units(self): """The units of the run's normalization, in SI; see :class:`struphy.physics.physics.Units`.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 379b3824c..9bf4c4a03 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -311,13 +311,6 @@ def test_evaluate_scalars_and_particle_defaults(run): assert orbits.name == "orbits" -def test_output_dispersion_returns_labeled_dataset(run): - spectrum = run.dispersion("em_fields/E", component=0, slice_at=(None, 0, 0)) - assert set(spectrum.data_vars) == {"power"} - assert spectrum.power.dims == ("omega", "k") - assert spectrum.attrs["source"] == "em_fields/E" - - def test_info_lists_particle_dataset_choices_in_default_order(run, capsys): run.info("kinetic_ions/f") report = capsys.readouterr().out @@ -325,11 +318,6 @@ def test_info_lists_particle_dataset_choices_in_default_order(run, capsys): assert "kinetic_ions/e1_v1_density/f" in report -def test_plot_makes_a_quick_xarray_view(run): - artist = run.plot(run.evaluate("em_fields/E")) - assert artist is not None - - def test_evaluate_raw_spline_field_at_logical_point(run, monkeypatch): calls = [] From bf8b5c03be0a52b52a12d6a355447cd284aa7620 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 14:39:06 +0200 Subject: [PATCH 139/193] formatting --- quickstart.py | 14 ++++++++------ src/struphy/models/base.py | 3 +-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/quickstart.py b/quickstart.py index 252cf74db..c9c0012e0 100644 --- a/quickstart.py +++ b/quickstart.py @@ -1,7 +1,9 @@ import numpy as np +from matplotlib import pyplot as plt + from struphy import Simulation, domains, grids, perturbations from struphy.models import Poisson -from matplotlib import pyplot as plt + model = Poisson() stab_eps = 1e-8 @@ -32,14 +34,14 @@ fig, axs = plt.subplots(1, 2, figsize=(12, 4)) eta1 = np.linspace(0, 1, 100) -phi_1d = out.evaluate("em_fields/phi", eta1=eta1, t = -1) +phi_1d = out.evaluate("em_fields/phi", eta1=eta1, t=-1) x = phi_1d["X"] phi_exact = np.cos(k * x) phi_exact_logical = np.cos(Lx * k * eta1) -phi_1d.plot(ax=axs[0], label="Struphy") # Plot along eta1 -phi_1d.plot(x="X", ax=axs[1], label="Struphy") # Plot along the physical coordinate X +phi_1d.plot(ax=axs[0], label="Struphy") # Plot along eta1 +phi_1d.plot(x="X", ax=axs[1], label="Struphy") # Plot along the physical coordinate X axs[0].plot(eta1, phi_exact_logical, "k--", lw=1.8, label="exact") axs[1].plot(x, phi_exact, "k--", lw=1.8, label="exact") @@ -50,6 +52,6 @@ plt.show() # Plot phi in 2d in physical coordinates -phi_2d = out.evaluate("em_fields/phi", eta1=np.linspace(0,1,100), eta2=np.linspace(0,1,100), t = -1) +phi_2d = out.evaluate("em_fields/phi", eta1=np.linspace(0, 1, 100), eta2=np.linspace(0, 1, 100), t=-1) phi_2d.plot(x="X", y="Y") -plt.show() \ No newline at end of file +plt.show() diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 4a20cfa51..ceee69cb9 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -1046,8 +1046,7 @@ def restore_option(value, template): return template if isinstance(template, dict) and isinstance(value, dict): return { - restore_option(key, key): restore_option(item, template.get(key)) - for key, item in value.items() + restore_option(key, key): restore_option(item, template.get(key)) for key, item in value.items() } if isinstance(value, list): template_item = template[0] if isinstance(template, (list, tuple)) and template else None From 5c714d72fec681113ecbaf9cb431491ff954d6af Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 14:39:15 +0200 Subject: [PATCH 140/193] remove quickstart.py --- quickstart.py | 57 --------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 quickstart.py diff --git a/quickstart.py b/quickstart.py deleted file mode 100644 index c9c0012e0..000000000 --- a/quickstart.py +++ /dev/null @@ -1,57 +0,0 @@ -import numpy as np -from matplotlib import pyplot as plt - -from struphy import Simulation, domains, grids, perturbations -from struphy.models import Poisson - -model = Poisson() - -stab_eps = 1e-8 - -model.propagators.poisson.options = model.propagators.poisson.Options( - stab_eps=stab_eps, -) - -Lx = 2.0 * np.pi -Ly = 4.0 * np.pi -mode = 2 -k = mode * 2.0 * np.pi / Lx -source_amp = k**2 + stab_eps - -fun = perturbations.ModesCos(ls=(mode,), amps=(source_amp,)) - -model.em_fields.source.add_perturbation(fun) - -domain = domains.Cuboid(r1=Lx, l2=-Ly / 2, r2=Ly / 2) -grid = grids.TensorProductGrid(num_elements=(64, 64, 1)) - -sim = Simulation(model=model, domain=domain, grid=grid) -out = sim.run() - -# Plot phi in 1d along eta1 and along physical coordinate X. -# The evaluate command returns an xarray DataContainer object, -# which can be indexed like a dictionary to access the data arrays. -fig, axs = plt.subplots(1, 2, figsize=(12, 4)) - -eta1 = np.linspace(0, 1, 100) -phi_1d = out.evaluate("em_fields/phi", eta1=eta1, t=-1) - -x = phi_1d["X"] -phi_exact = np.cos(k * x) -phi_exact_logical = np.cos(Lx * k * eta1) - -phi_1d.plot(ax=axs[0], label="Struphy") # Plot along eta1 -phi_1d.plot(x="X", ax=axs[1], label="Struphy") # Plot along the physical coordinate X -axs[0].plot(eta1, phi_exact_logical, "k--", lw=1.8, label="exact") -axs[1].plot(x, phi_exact, "k--", lw=1.8, label="exact") - -for i in range(2): - axs[i].legend() - axs[i].grid(alpha=0.3) -fig.savefig("quickstart_poisson_phi.png", dpi=150) -plt.show() - -# Plot phi in 2d in physical coordinates -phi_2d = out.evaluate("em_fields/phi", eta1=np.linspace(0, 1, 100), eta2=np.linspace(0, 1, 100), t=-1) -phi_2d.plot(x="X", y="Y") -plt.show() From 4927248c0e74eb1c58551e949b357e77037a703b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 25 Sep 2026 14:40:34 +0200 Subject: [PATCH 141/193] improve out.info() to show keys only --- src/struphy/post_processing/output.py | 112 +++++------------- .../post_processing/tests/test_output.py | 18 ++- tutorials/tutorial_post_processing.ipynb | 2 +- 3 files changed, 45 insertions(+), 87 deletions(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index aa7cb6af2..a2eb61067 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -2320,14 +2320,12 @@ def label(self) -> str: return self._label def info(self, name: str | None = None) -> None: - """Print a concise run summary, configuration reference, and product catalog. - - Use ``out.info()`` interactively. The summary includes model parameters, - species variables, propagator options, and initial-condition definitions saved in - metadata. As with :meth:`keys`, the product catalog materializes default - post-processing when needed; call :meth:`pproc` first to choose its options. - ``out.info("species/variable")`` instead lists the particle datasets that can - satisfy that request, in the default selection order. + """Print the evaluable keys and how to load each one with :meth:`evaluate`. + + As with :meth:`keys`, listing materializes default post-processing when needed; + call :meth:`pproc` first to choose its options. ``out.info("species/variable")`` + instead lists the particle datasets that can satisfy that request, in the default + selection order. """ if name is not None: if name == "scalars": @@ -2343,87 +2341,37 @@ def info(self, name: str | None = None) -> None: print(f"{'-' * width} -----------") print("\n".join(f"{key:<{width}} {description}" for key, description in rows)) return - rows = [(key, self._product_description(key)) for key in self.keys()] - key_width = max((len(key) for key, _ in rows), default=3) - model = self.metadata.get("model", {}) + rows = [(key, self._product_description(key), self._evaluate_call(key)) for key in self.keys()] + key_width = max((len(key) for key, _, _ in rows), default=3) + description_width = max((len(description) for _, description, _ in rows), default=11) lines = [ f"Output: {self.path_out}", - self.label, - "", - "Configuration", - "-------------", - f"Model: {model.get('model', self.metadata.get('model_name', 'unknown'))}", - f"Model parameters: {json.dumps(model.get('params', {}), sort_keys=True)}", - "Species and variables:", - ] - for species_name, species in model.get("species", {}).items(): - parameters = { - key: value - for key, value in species.items() - if key - not in { - "class", - "variables", - "loading_params", - "weights_params", - "boundary_params", - "sorting_params", - "saving_params", - } - and value is not None - } - lines.append( - f" {species_name} ({species.get('class', 'Species')}): {json.dumps(parameters, sort_keys=True)}" - ) - for variable_name, variable in species.get("variables", {}).items(): - lines.append( - f" {variable_name}: {variable.get('class', 'Variable')} " - f"[{variable.get('space', 'unknown')}], save_data={variable.get('save_data', True)}" - ) - lines.append("Propagator options:") - for name, options in model.get("propagator_options", {}).items(): - lines.append(f" {name}: {json.dumps(options, sort_keys=True)}") - lines.append("Initial conditions:") - for species_name, variables in self._initial_condition_metadata().items(): - for variable_name, definition in variables.items(): - parts = ", ".join( - f"{key}={self._initial_condition_description(value)}" for key, value in definition.items() - ) - lines.append(f" {species_name}.{variable_name}: {parts}") - lines = [ - *lines, "", - "Help", - "----", - "- Use out.model for the reconstructed model and its variables.", - "- Use out.initial_conditions for reconstructed backgrounds, perturbations, and distributions.", - "- Saved Python initial conditions are reconstructed from source; unsupported definitions remain in out.metadata.", - "- Use out.keys(), out.fields, out.distributions, out.densities, and out.orbits to discover products.", - "- Use out.evaluate(key) and out.pproc(...) to load and process products.", + f"{'Key':<{key_width}} {'Description':<{description_width}} Load with", + f"{'-' * key_width} {'-' * description_width} ---------", + *(f"{key:<{key_width}} {description:<{description_width}} {call}" for key, description, call in rows), "", - f"{'Key':<{key_width}} Description", - f"{'-' * key_width} -----------", + "Hints", + "-----", + "- t=-1 (index), t=slice(...) or t=0.5 (time value) selects snapshots; the t dimension is kept.", + "- Other keyword arguments select named coordinates, e.g. component=0 or quantity='x'.", + "- Fields: pass eta1=, eta2=, eta3= (scalars or 1D arrays) to evaluate on a logical grid;", + " omitted directions default to 0.5. The result carries physical coordinates X, Y, Z.", + "- Particles: out.info('species/variable') lists alternative datasets for dataset=.", + "- Results are xarray objects: use .sel/.isel, .plot(x='X'), or .values for NumPy.", ] - lines.extend(f"{key:<{key_width}} {description}" for key, description in rows) print("\n".join(lines)) - @staticmethod - def _initial_condition_description(value) -> str: - """Short, source-free description of one serialized initial condition.""" - if value is None: - return "none" - if isinstance(value, list): - return "[" + ", ".join(Output._initial_condition_description(item) for item in value) + "]" - if not isinstance(value, dict): - return repr(value) - kind = value.get("type") - if kind is None: - return "mapping" - if kind in {"python_function", "python_class"}: - return f"{kind}({value.get('name', value.get('serialization', 'unknown'))})" - if kind == "callable": - return f"callable({value.get('serialization', 'unknown')})" - return kind + def _evaluate_call(self, key: str) -> str: + """The :meth:`evaluate` call that loads ``key``.""" + if key in self.scalars.data_vars: + return f"out.evaluate('scalars', variables='{key}')" + if key in self.field_catalog: + return f"out.evaluate('{key}')" + if key in self.distribution_catalog or key in self.density_catalog: + species, *_, variable = key.split("/") + return f"out.evaluate('{species}/{variable}', dataset='{key}')" + return f"out.evaluate('{key}/orbits')" def _product_description(self, key: str) -> str: """A stable description for a key, without loading its data array.""" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 379b3824c..72eb35618 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -571,10 +571,11 @@ def test_unknown_species_never_starts_processing(tmp_path, monkeypatch): def test_info_lists_evaluable_products_with_descriptions(run, capsys): assert run.info() is None text = capsys.readouterr().out - assert "Configuration" in text and "Species and variables" in text - assert "Propagator options" in text and "Initial conditions" in text - assert "Help" in text and "out.initial_conditions" in text - assert "Key" in text and "Description" in text + assert "Configuration" not in text and "Propagator options" not in text + assert "Key" in text and "Description" in text and "Load with" in text and "Hints" in text + assert "out.evaluate('scalars', variables='en_tot')" in text + assert "out.evaluate('kinetic_ions/f', dataset='kinetic_ions/e1_v1_density/f')" in text + assert "out.evaluate('kinetic_ions/orbits')" in text assert "en_tot" in text and "scalar time series" in text assert "kinetic_ions/e1_v1_density/f" in text and "particle distribution" in text assert "kinetic_ions" in text and "marker trajectories" in text @@ -582,6 +583,15 @@ def test_info_lists_evaluable_products_with_descriptions(run, capsys): assert run.field_catalog._cache == {}, "listing must not load arrays" +def test_info_evaluate_calls_load_their_keys(run): + for key in run.keys(): + call = run._evaluate_call(key) + array = eval(call, {"out": run}) + if key in run.scalars.data_vars: + array = array[key] + xr.testing.assert_identical(array, run._product(key)) + + def test_info_labels_distribution_and_density_symbols(run, capsys): run.info() text = capsys.readouterr().out diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 5e14cf893..445ec65b3 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -188,7 +188,7 @@ "source": [ "### Reconstructed setup and initial conditions\n", "\n", - "`out.info()` prints the saved model parameters, species variables, propagator options, initial-condition summary, and a short API guide. In `out.metadata`, serialized initial conditions live on each variable under `model → species → variables → initial_conditions`. `out.initial_conditions` gives reconstructed objects, including saved Python functions and classes; `out.model` restores them onto the model variables. Unsupported definitions remain available in `out.metadata`." + "`out.info()` prints every evaluable key with a short description and the exact `out.evaluate(...)` call that loads it, followed by a few hints on selecting times and coordinates. The run configuration is not part of `info()`: in `out.metadata`, serialized initial conditions live on each variable under `model → species → variables → initial_conditions`. `out.initial_conditions` gives reconstructed objects, including saved Python functions and classes; `out.model` restores them onto the model variables. Unsupported definitions remain available in `out.metadata`." ] }, { From cb68f990ce60a8c6cf2e4c9e288861715f9abefc Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 14:46:46 +0200 Subject: [PATCH 142/193] Fixed serialization, include derived or legacy keys not accepted by their constructors such as ColdPlasma --- src/struphy/simulation/sim.py | 23 ++++++++++++++++-- src/struphy/simulation/tests/test_output.py | 27 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 92b1dec66..00496348d 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1621,12 +1621,31 @@ def _serialize_initial_condition(value): if dataclasses.is_dataclass(value) and not isinstance(value, type): return { "type": type(value).__name__, - "params": Simulation._serialize_initial_condition(value.to_dict()), + "params": Simulation._serialize_initial_condition( + { + field.name: getattr(value, field.name) + for field in dataclasses.fields(value) + if field.init + } + ), } if hasattr(value, "params"): + parameters = value.params + try: + signature = inspect.signature(type(value)) + except (TypeError, ValueError): + signature = None + if signature is not None and not any( + parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values() + ): + parameters = { + name: parameter_value + for name, parameter_value in parameters.items() + if name in signature.parameters + } return { "type": type(value).__name__, - "params": Simulation._serialize_initial_condition(value.params), + "params": Simulation._serialize_initial_condition(parameters), } if inspect.isfunction(value): # Top-level functions are fully captured in metadata. Their source is diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index b12f4719b..36d912c6a 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -1,6 +1,7 @@ """Tests for the link between a Simulation and its output.""" import json +import inspect import os from pathlib import Path @@ -20,6 +21,8 @@ perturbations, ) from struphy.initial.base import Perturbation +from struphy.initial import perturbations +from struphy.kinetic_background import maxwellians from struphy.linear_algebra.solver import SolverParameters from struphy.models import ColdPlasmaVlasov, LinearMHD, Maxwell, Poisson, VlasovAmpereOneSpecies from struphy.ode.utils import ButcherTableau @@ -177,6 +180,30 @@ def test_run_metadata_contains_serialized_initial_conditions(tmp_path): assert kinetic["initial_condition"]["params"]["f1"]["params"]["n"][1]["type"] == "TorusModesCos" +def test_all_builtin_initial_conditions_round_trip(): + initial_conditions = [ + FieldsBackground(values=(1.0, 2.0, 3.0)), + equils.HomogenSlab(), + maxwellians.Maxwellian3D(n=(1.0, perturbations.Noise(seed=7))), + maxwellians.Maxwellian3D() + maxwellians.Maxwellian3D(n=(0.2, None)), + 2.0 * maxwellians.GyroMaxwellian2D(), + ] + + for module in (perturbations, maxwellians): + for name, initial_condition_class in vars(module).items(): + if not inspect.isclass(initial_condition_class) or initial_condition_class.__module__ != module.__name__: + continue + if name == "CanonicalMaxwellian2D": + initial_conditions.append(initial_condition_class(equil=equils.AdhocTorus())) + else: + initial_conditions.append(initial_condition_class()) + + for initial_condition in initial_conditions: + serialized = Simulation._serialize_initial_condition(initial_condition) + restored = Simulation._deserialize_initial_condition(serialized) + assert Simulation._serialize_initial_condition(restored) == serialized, type(initial_condition).__name__ + + def test_run_metadata_embeds_user_function_source(tmp_path): sim = Simulation(model=VlasovAmpereOneSpecies(), env=EnvironmentOptions(out_folders=str(tmp_path))) sim.model.kinetic_ions.var.add_background(maxwellians.Maxwellian3D(n=(user_density_profile, None))) From 920721cd4299b25385d60cf8345e429b07ee3b8f Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 25 Sep 2026 15:20:39 +0200 Subject: [PATCH 143/193] changed the orbits logic for Output: dataset insted of dataarray --- .claude/skills/setup-simulation/SKILL.md | 4 +- doc/sections/userguide.rst | 11 ++- src/struphy/pic/base.py | 13 ++++ src/struphy/pic/particles.py | 36 ++++++++- src/struphy/post_processing/arrays.py | 68 ++++++++--------- src/struphy/post_processing/output.py | 44 ++++++----- src/struphy/post_processing/store.py | 2 +- .../post_processing/tests/test_arrays.py | 13 ++-- .../post_processing/tests/test_output.py | 45 +++++++---- tutorials/tutorial_beltrami_sph.ipynb | 14 ++-- tutorials/tutorial_dam_break_sph.ipynb | 24 +++--- tutorials/tutorial_gas_expansion_sph.ipynb | 10 +-- tutorials/tutorial_particle_tracing.ipynb | 75 +++++++++---------- tutorials/tutorial_post_processing.ipynb | 4 +- 14 files changed, 219 insertions(+), 144 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index 02038f820..e8353b0e9 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -144,7 +144,7 @@ out.pproc(physical=True) # optional; products are otherwise processed wi out.scalars. # xarray time series, no post-processing needed out.fields.. # dims (t, [component,] e1, e2, e3) out.distributions...f # dims (t, ) -out.orbits. # dims (t, marker, quantity) +out.orbits. # Dataset: one (t, marker) variable per quantity (x, y, z, v1, ..., weight) out.model.units # model reconstructed from metadata out.domain, out.grid, out.time_opts, out.derham_opts # reconstructed lazily with from_dict() ``` @@ -164,7 +164,7 @@ Products are xarray objects. Use xarray's plotting and selection methods: ```python out...f.isel(t=-1).plot(x="e1", y="v1") out.scalars..plot.line(x="t") -out..orbits.isel(marker=0).sel(quantity=["x", "y", "z"]).plot.line(x="t", hue="quantity") +out..orbits.isel(marker=0)[["x", "y", "z"]].to_dataarray("quantity").plot.line(x="t", hue="quantity") ``` Select by position with `.isel(t=-1, component=0)` or by coordinate with diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 331defc96..109f01b60 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -644,17 +644,22 @@ Plotting particle orbits If ``n_markers > 0`` was set in :class:`~struphy.particles.parameters.SavingParameters`, individual marker -trajectories are available under ``out.orbits``: +trajectories are available as an :class:`xarray.Dataset` with one ``(t, marker)`` +variable per saved quantity. Positions ``x, y, z`` are physical; the remaining +quantities (velocities, ``weight``, ...) depend on the particle class, see +:attr:`~struphy.pic.base.Particles.orbit_quantities`. Each variable's +``description`` attribute says what it is. .. code-block:: python import matplotlib.pyplot as plt - orbits = out.orbits.kinetic_ions # dims (t, marker, attribute) + orbits = out.evaluate("kinetic_ions/orbits") # or out.orbits.kinetic_ions + print(orbits) # lists x, y, z, v1, v2, v3, weight marker = orbits.isel(marker=0) plt.figure() - plt.plot(marker.isel(attribute=0), marker.isel(attribute=2)) # position x vs z + plt.plot(marker.x, marker.z) # position x vs z plt.xlabel("x") plt.ylabel("z") plt.title("Marker orbit (particle 0)") diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index fe21ac046..cb8e5aa7b 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -77,6 +77,14 @@ def _to_numpy_for_kernel(value): return value +ORBIT_POSITIONS = ( + (0, "x", "$x$", "physical position x"), + (1, "y", "$y$", "physical position y"), + (2, "z", "$z$", "physical position z"), +) +"""Orbit quantities of the position columns, which post-processing maps to physical coordinates.""" + + class Particles(metaclass=ABCMeta): r""" Base class for particle species. @@ -428,6 +436,11 @@ def coordinate_labels(self) -> tuple[str]: Length must be 3 + vdim, where the first 3 are the spatial coordinates and the last vdim are the velocity coordinates.""" pass + orbit_quantities: tuple[tuple[int, str, str, str], ...] = () + """Marker columns saved in the ``orbits`` post-processing product, as + ``(column, name, long_name, description)``. The first three are :data:`ORBIT_POSITIONS`; + the marker index becomes the ``marker`` coordinate of the product.""" + @property @abstractmethod def mu_idx(self): diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 86e37ab78..2d475ce73 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -11,7 +11,7 @@ from struphy.kinetic_background import maxwellians from struphy.kinetic_background.base import Maxwellian, SumKineticBackground from struphy.pic import utilities_kernels -from struphy.pic.base import Particles +from struphy.pic.base import ORBIT_POSITIONS, Particles class Particles6D(Particles): @@ -31,6 +31,14 @@ class Particles6D(Particles): """Dimension of the (Cartesian) velocity space, here 3.""" coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_x$", "$v_y$", "$v_z$") """Labels for the coordinates in the phase space. Length is 6, with the first 3 being the spatial coordinates and the last 3 being the velocity coordinates.""" + orbit_quantities = ( + *ORBIT_POSITIONS, + (3, "v1", "$v_x$", "Cartesian velocity x"), + (4, "v2", "$v_y$", "Cartesian velocity y"), + (5, "v3", "$v_z$", "Cartesian velocity z"), + (6, "weight", "$w$", "marker weight"), + ) + """Marker columns saved as orbits, see :attr:`~struphy.pic.base.Particles.orbit_quantities`.""" default_background = maxwellians.Maxwellian3D() """Default kinetic background is a 3D Cartesian Maxwellian.""" default_n_cols = {"diagnostics": 0, "aux": 5} @@ -262,6 +270,14 @@ class Particles5D(Particles): """Dimension of the velocity space, here 2 (:math:`v_\\parallel, \\mu`).""" coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_\\parallel$", "$\\mu$") """Labels for the coordinates in the phase space. Length is 5, with the first 3 being the spatial coordinates and the last 2 being the velocity coordinates.""" + orbit_quantities = ( + *ORBIT_POSITIONS, + (3, "v_par", "$v_\\parallel$", "parallel velocity"), + (4, "mu", "$\\mu$", "magnetic moment"), + (5, "weight", "$w$", "marker weight"), + (9, "p_phi", "$p_\\phi$", "canonical toroidal momentum (set by save_constants_of_motion)"), + ) + """Marker columns saved as orbits, see :attr:`~struphy.pic.base.Particles.orbit_quantities`.""" mu_idx = 4 """Column index of particle magnetic moment.""" default_background = maxwellians.GyroMaxwellian2D() @@ -520,6 +536,14 @@ class Particles5Dvperp(Particles): """Dimension of the velocity space, here 2 (:math:`v_\\parallel, v_\\perp`).""" coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_\\parallel$", "$v_\\perp$") """Labels for the coordinates in the phase space. Length is 5, with the first 3 being the spatial coordinates and the last 2 being the velocity coordinates.""" + orbit_quantities = ( + *ORBIT_POSITIONS, + (3, "v_par", "$v_\\parallel$", "parallel velocity"), + (4, "v_perp", "$v_\\perp$", "perpendicular velocity"), + (5, "weight", "$w$", "marker weight"), + (10, "p_phi", "$p_\\phi$", "canonical toroidal momentum (set by save_constants_of_motion)"), + ) + """Marker columns saved as orbits, see :attr:`~struphy.pic.base.Particles.orbit_quantities`.""" default_background = maxwellians.GyroMaxwellian2Dvperp() """Default kinetic background is a gyrotropic Maxwellian in :math:`(v_\\parallel, v_\\perp)`.""" default_n_cols = {"diagnostics": 3, "aux": 12} @@ -813,6 +837,8 @@ class Particles3D(Particles): """Dimension of the velocity space, here 0 (no velocity coordinates).""" coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$") """Labels for the coordinates in the phase space. Length is 3, with all being spatial coordinates.""" + orbit_quantities = (*ORBIT_POSITIONS, (3, "weight", "$w$", "marker weight")) + """Marker columns saved as orbits, see :attr:`~struphy.pic.base.Particles.orbit_quantities`.""" default_background = maxwellians.ColdPlasma() """Default kinetic background is a cold-plasma (velocity-independent) density.""" default_n_cols = {"diagnostics": 0, "aux": 5} @@ -890,6 +916,14 @@ class ParticlesSPH(Particles): """Dimension of the per-marker Cartesian velocity attribute, here 3 (not a sampled coordinate, see class docstring).""" coordinate_labels = ("$\\eta_1$", "$\\eta_2$", "$\\eta_3$", "$v_x$", "$v_y$", "$v_z$") """Labels for the coordinates in the phase space. Length is 6, with the first 3 being the spatial coordinates and the last 3 being the velocity coordinates.""" + orbit_quantities = ( + *ORBIT_POSITIONS, + (3, "v1", "$v_x$", "Cartesian velocity x"), + (4, "v2", "$v_y$", "Cartesian velocity y"), + (5, "v3", "$v_z$", "Cartesian velocity z"), + (6, "weight", "$w$", "marker weight"), + ) + """Marker columns saved as orbits, see :attr:`~struphy.pic.base.Particles.orbit_quantities`.""" default_background = equils.ConstantVelocity() """Default fluid background is a spatially constant velocity field.""" default_n_cols = {"diagnostics": 0, "aux": 24} diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index b495c991d..8221f1c45 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -26,7 +26,6 @@ "Z": r"$Z$", "component": "component", "marker": "marker", - "quantity": "quantity", } BINNED_LABELS = {"f": "$f$", "delta_f": r"$\delta f$", "n": "$n$"} SCALARS_EXCLUDE = ("time",) @@ -140,46 +139,41 @@ def save_scalars(scalars: xr.Dataset | Mapping, path: str, *, names=None, exclud return path -def orbit_columns(n_columns: int) -> dict: - columns = {"position": slice(0, 3), "id": n_columns - 1} - if n_columns == 8: - columns.update(velocity=slice(3, 6), weight=6) - elif n_columns == 5: - columns["velocity"] = 3 - else: - columns["velocity"] = slice(3, n_columns - 1) - return columns - - -def orbit_quantities(n_columns: int) -> list[str]: - """Name every saved marker column, so that orbits are self-describing.""" - columns = orbit_columns(n_columns) - names = [""] * n_columns - for axis, name in enumerate(("x", "y", "z")): - names[axis] = name - velocity = columns["velocity"] - indices = range(*velocity.indices(n_columns)) if isinstance(velocity, slice) else [velocity] - for number, index in enumerate(indices, 1): - names[index] = f"v{number}" - if "weight" in columns: - names[columns["weight"]] = "weight" - names[columns["id"]] = "id" - return [name or f"column_{index}" for index, name in enumerate(names)] - - -def wrap_orbits(values, time, *, time_unit="") -> xr.DataArray: - """Label marker orbits with time, marker and named quantity dimensions.""" +def wrap_orbits(values, time, quantities, *, time_unit="") -> xr.Dataset: + """Marker orbits as one ``(t, marker)`` variable per saved quantity. + + ``quantities`` are the ``(column, name, long_name, description)`` entries of + :attr:`~struphy.pic.base.Particles.orbit_quantities`, in the order of the last axis of + ``values``. + """ values = np.asarray(values) - return data_array( - values, - ("t", "marker", "quantity"), - {"t": time, "marker": np.arange(values.shape[1]), "quantity": orbit_quantities(values.shape[2])}, - name="orbits", - label="marker orbits", - coord_units={"t": time_unit}, + coords = {"t": time, "marker": np.arange(values.shape[1])} + return xr.Dataset( + { + name: data_array( + values[..., index], + ("t", "marker"), + coords, + name=name, + label=long_name, + coord_units={"t": time_unit}, + attrs={"description": description}, + ) + for index, (_, name, long_name, description) in enumerate(quantities) + }, + attrs={"product": "orbits", "label": "marker orbits"}, ) +def orbits_from_legacy(array: xr.DataArray) -> xr.Dataset: + """Convert a ``(t, marker, quantity)`` orbits array of an earlier store to :func:`wrap_orbits` form.""" + dataset = array.to_dataset(dim="quantity").drop_vars("id", errors="ignore") + for name in dataset.data_vars: + dataset[name].attrs = {"label": name, "long_name": DIM_LABELS.get(name, name)} + dataset.attrs.update(product="orbits", label="marker orbits") + return dataset + + def wrap_field_data( values_by_time: Mapping, grids_log=None, diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index a2eb61067..e3eb7dba5 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -29,6 +29,7 @@ from struphy.post_processing.arrays import ( BINNED_LABELS, data_array, + orbits_from_legacy, save_scalars, wrap_binned_data, wrap_field_data, @@ -181,7 +182,8 @@ class DensityProducts(ProductNamespace): class OrbitProducts(ProductNamespace): - """Marker trajectories grouped by species.""" + """Marker trajectories grouped by species, each an :class:`xarray.Dataset` with one + ``(t, marker)`` variable per quantity of :attr:`~struphy.pic.base.Particles.orbit_quantities`.""" class Output: @@ -1724,24 +1726,18 @@ def _post_process_markers( # get number of time steps and markers nt, n_markers, n_cols = file_0["kinetic/" + species + "/markers"].shape - # get velocity dimension from one of the variables of the species + # get the saved orbit quantities from the particle class of the species for _, var in species_obj.variables.items(): assert isinstance(var, PICVariable | SPHVariable) cls: Particles = var.particles_class - vdim = cls.vdim + quantities = cls.orbit_quantities break log_nt = int(xp.log10(int(((nt - 1) / step)))) + 1 - # directory for .txt files and marker index which will be saved + # directory for .txt files and marker columns which will be saved (marker index last) path_orbits = os.path.join(path_kinetic_species, "orbits") - - if vdim == 2: - save_index = list(range(0, 6)) + [10] + [-1] - elif vdim == 3: - save_index = list(range(0, 7)) + [-1] - else: - save_index = list(range(0, 4)) + [-1] + save_index = [column for column, *_ in quantities] + [-1] if self._pproc_rank == 0: try: @@ -1815,8 +1811,9 @@ def _post_process_markers( self._pproc_comm.Barrier() if self._pproc_rank == 0: - values = wrap_orbits(xp.stack(orbits), self._pproc_t_grid[: len(orbits)]) - store.write_group(store.store_path(self.path_pproc), f"/{species}", xr.Dataset({"orbits": values})) + # the marker index (last column) equals the position along the marker axis + values = wrap_orbits(xp.stack(orbits)[..., :-1], self._pproc_t_grid[: len(orbits)], quantities) + store.write_group(store.store_path(self.path_pproc), f"/{species}/orbits", values) def _post_process_f( self, @@ -2354,10 +2351,12 @@ def info(self, name: str | None = None) -> None: "Hints", "-----", "- t=-1 (index), t=slice(...) or t=0.5 (time value) selects snapshots; the t dimension is kept.", - "- Other keyword arguments select named coordinates, e.g. component=0 or quantity='x'.", + "- Other keyword arguments select named coordinates, e.g. component=0 or marker=[0, 1, 2].", "- Fields: pass eta1=, eta2=, eta3= (scalars or 1D arrays) to evaluate on a logical grid;", " omitted directions default to 0.5. The result carries physical coordinates X, Y, Z.", "- Particles: out.info('species/variable') lists alternative datasets for dataset=.", + "- Orbits are an xarray.Dataset with one (t, marker) variable per quantity, e.g. orbits.x;", + " each variable's 'description' attribute says what it is.", "- Results are xarray objects: use .sel/.isel, .plot(x='X'), or .values for NumPy.", ] print("\n".join(lines)) @@ -2385,7 +2384,7 @@ def _product_description(self, key: str) -> str: if key in self.density_catalog: label = BINNED_LABELS.get(key.rsplit("/", 1)[-1], key.rsplit("/", 1)[-1]) return f"SPH density ({label})" - return "marker trajectories" + return f"marker trajectories ({', '.join(self.orbit_catalog[key].data_vars)})" def _product_kind(self, key: str) -> str: if key in self.scalars.data_vars: @@ -2553,11 +2552,17 @@ def _discover(self, kind: str) -> dict: """Loaders for one kind of product, keyed as ``[/]/``.""" loaders = {} for group, dataset in self._groups().items(): + if dataset.attrs.get("product") == "orbits": + if kind == "orbits": + loaders[group.rsplit("/", 1)[0]] = lambda group=group: self._load(group) + continue for name in dataset.data_vars: if self._kind(group, name) != kind: continue - key = group if name == "orbits" else f"{group}/{name}" - loaders[key] = lambda group=group, name=name: self._load(group, name) + if name == "orbits": # one (t, marker, quantity) array in stores of earlier versions + loaders[group] = lambda group=group: orbits_from_legacy(self._load(group, "orbits")) + else: + loaders[f"{group}/{name}"] = lambda group=group, name=name: self._load(group, name) return loaders @staticmethod @@ -2569,8 +2574,9 @@ def _kind(group: str, name: str) -> str: return "fields" return "densities" if name == "n" else "distributions" - def _load(self, group: str, name: str) -> xr.DataArray: - array = self.tree[group].ds[name] + def _load(self, group: str, name: str | None = None) -> xr.DataArray | xr.Dataset: + """One variable of a store group, or the whole group dataset when ``name`` is None.""" + array = self.tree[group].to_dataset() if name is None else self.tree[group].ds[name] if self.time_units == "physical" and "t" in array.dims: array = array.assign_coords(t=array.t * self.time_scale) array.coords["t"].attrs["units"] = "s" diff --git a/src/struphy/post_processing/store.py b/src/struphy/post_processing/store.py index 216489f81..6fd04b9d2 100644 --- a/src/struphy/post_processing/store.py +++ b/src/struphy/post_processing/store.py @@ -4,7 +4,7 @@ that group's dataset:: /em_fields e_field, phi, and e_field_xyz, phi_xyz with physical=True - /kinetic_ions orbits + /kinetic_ions/orbits x, y, z, v1, ... (one variable per quantity, see Particles.orbit_quantities) /kinetic_ions/e1_v1_density f, delta_f /kinetic_ions/view_0 n diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index bb1700dd9..17d635aef 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -74,11 +74,14 @@ def test_binned_wrapper_keeps_memory_mappable_values(): assert data.attrs["label"] == "$f$" -def test_orbits_name_their_columns(): - data = wrap_orbits(np.zeros((2, 5, 8)), [0, 1]) - assert list(data.quantity.values) == ["x", "y", "z", "v1", "v2", "v3", "weight", "id"] - assert data.sel(marker=2).dims == ("t", "quantity") - assert data.sel(quantity="weight").dims == ("t", "marker") +def test_orbits_are_one_variable_per_quantity(): + quantities = ((0, "x", "$x$", "position x"), (6, "weight", "$w$", "marker weight")) + data = wrap_orbits(np.arange(20.0).reshape(2, 5, 2), [0, 1], quantities) + assert list(data.data_vars) == ["x", "weight"] + assert data.weight.dims == ("t", "marker") + assert data.weight.attrs["description"] == "marker weight" + np.testing.assert_array_equal(data.weight.isel(t=0), [1, 3, 5, 7, 9]) + assert data.sel(marker=2).x.dims == ("t",) def test_scalar_alignment_is_exact(): diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 72eb35618..25d8af654 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -14,7 +14,8 @@ from struphy.models import Maxwell from struphy.post_processing import output as output_module from struphy.post_processing import store -from struphy.post_processing.arrays import orbit_quantities +from struphy.pic.particles import Particles6D +from struphy.post_processing.arrays import wrap_orbits from struphy.post_processing.manifest import is_processed, normalize_options, source_fingerprint from struphy.post_processing.output import Output, open_output @@ -66,14 +67,8 @@ def write_tree(root): coords={"t": t, "e1": logical["e1"], "e2": logical["e2"], "e3": np.zeros(1)}, ), ) - store.write_group( - path, - "/kinetic_ions", - xr.Dataset( - {"orbits": (("t", "marker", "quantity"), np.stack([np.full((N_MARKERS, 8), step) for step in range(NT)]))}, - coords={"t": t, "marker": np.arange(N_MARKERS), "quantity": orbit_quantities(8)}, - ), - ) + orbits = np.stack([np.full((N_MARKERS, 7), step) for step in range(NT)]) + store.write_group(path, "/kinetic_ions/orbits", wrap_orbits(orbits, t, Particles6D.orbit_quantities)) data_dir = os.path.join(root, "data") os.makedirs(data_dir) @@ -187,10 +182,34 @@ def test_sph_density_views_take_dimensions_from_their_grids(run): np.testing.assert_allclose(data.e2, np.linspace(0, 1, N2)) -def test_orbit_product_keeps_column_semantics(run): +def test_orbit_product_is_a_dataset_of_named_quantities(run): data = run.orbits["kinetic_ions"] - assert data.dims == ("t", "marker", "quantity") - assert list(data.quantity.values) == ["x", "y", "z", "v1", "v2", "v3", "weight", "id"] + assert isinstance(data, xr.Dataset) + assert list(data.data_vars) == ["x", "y", "z", "v1", "v2", "v3", "weight"] + assert dict(data.sizes) == {"t": NT, "marker": N_MARKERS} + assert data.x.attrs["description"] == "physical position x" + assert data.v1.attrs["long_name"] == "$v_x$" + np.testing.assert_array_equal(data.marker, np.arange(N_MARKERS)) + + +def test_orbits_of_earlier_stores_are_converted_on_read(tmp_path): + root = write_tree(str(tmp_path)) + path = store.store_path(os.path.join(root, "post_processing")) + t = np.linspace(0, 1, NT) + legacy = np.stack([np.full((N_MARKERS, 8), step) for step in range(NT)]) + names = ["x", "y", "z", "v1", "v2", "v3", "weight", "id"] + store.write_group( + path, + "/electrons", + xr.Dataset( + {"orbits": (("t", "marker", "quantity"), legacy)}, + coords={"t": t, "marker": np.arange(N_MARKERS), "quantity": names}, + ), + ) + data = Output(root).orbits["electrons"] + assert isinstance(data, xr.Dataset) + assert list(data.data_vars) == names[:-1] + assert data.x.dims == ("t", "marker") def test_scalar_time_uses_the_same_policy_as_postprocessed_products(run): @@ -308,7 +327,7 @@ def test_evaluate_scalars_and_particle_defaults(run): assert selected.name == "delta_f" orbits = run.evaluate("kinetic_ions/orbits") - assert orbits.name == "orbits" + assert isinstance(orbits, xr.Dataset) and "weight" in orbits.data_vars def test_output_dispersion_returns_labeled_dataset(run): diff --git a/tutorials/tutorial_beltrami_sph.ipynb b/tutorials/tutorial_beltrami_sph.ipynb index cff47397c..51f494133 100644 --- a/tutorials/tutorial_beltrami_sph.ipynb +++ b/tutorials/tutorial_beltrami_sph.ipynb @@ -334,10 +334,11 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = np.asarray(out.orbits.cold_fluid)\n", + "# one (t, marker) variable per saved quantity: orbits.x, orbits.y, ...\n", + "orbits = out.evaluate(\"cold_fluid/orbits\")\n", "\n", "coloring = np.select(\n", - " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + " [orbits.x[0] <= -0.2, np.abs(orbits.x[0]) < +0.2, orbits.x[0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", @@ -350,7 +351,7 @@ " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", " ax = plt.gca()\n", - " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", + " plt.scatter(orbits.x[i], orbits.y[i], c=coloring)\n", " plt.axis(\"square\")\n", " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", @@ -468,10 +469,11 @@ "\n", "plt.figure(figsize=(12, 28))\n", "\n", - "orbits = np.asarray(out_tess.orbits.cold_fluid)\n", + "# one (t, marker) variable per saved quantity: orbits.x, orbits.y, ...\n", + "orbits = out_tess.evaluate(\"cold_fluid/orbits\")\n", "\n", "coloring = np.select(\n", - " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + " [orbits.x[0] <= -0.2, np.abs(orbits.x[0]) < +0.2, orbits.x[0] >= 0.2], [-1.0, 0.0, +1.0]\n", ")\n", "\n", "dt = time_opts.dt\n", @@ -484,7 +486,7 @@ " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", " ax = plt.gca()\n", - " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", + " plt.scatter(orbits.x[i], orbits.y[i], c=coloring)\n", " plt.axis(\"square\")\n", " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", diff --git a/tutorials/tutorial_dam_break_sph.ipynb b/tutorials/tutorial_dam_break_sph.ipynb index d6a0ea2fa..b4170e716 100644 --- a/tutorials/tutorial_dam_break_sph.ipynb +++ b/tutorials/tutorial_dam_break_sph.ipynb @@ -285,13 +285,13 @@ "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", "n_sph = density\n", "\n", - "# Marker orbits: shape (Nt_orb, n_markers, n_attrs)\n", - "# attrs for vdim=2: [x, y, z, v1, v2, w, diag, id]\n", - "orbits = np.asarray(out.orbits.euler_fluid)\n", + "# Marker orbits: xarray.Dataset with one (t, marker) variable per saved quantity\n", + "# (x, y, z, v1, v2, v3, weight); print(orbits) lists them\n", + "orbits = out.evaluate(\"euler_fluid/orbits\")\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", - "Nt_orb = orbits.shape[0]\n", + "Nt_orb = orbits.sizes[\"t\"]\n", "t_orbit = np.linspace(0.0, Tend, Nt_orb)\n", "\n", "X = np.asarray(ee1)[:, :, 0] * r1 # physical x, shape (pts_e1, pts_e2)\n", @@ -299,11 +299,11 @@ "n_arr = np.asarray(n_sph) # (Nt+1, pts_e1, pts_e2, 1)\n", "\n", "# Colour each marker by its initial x position within the column\n", - "x_init = orbits[0, :, 0]\n", + "x_init = orbits.x[0].values\n", "c_val = x_init / (r1 / 4.0) # 0 = left wall, 1 = dam face\n", "\n", "print(f\"KDE field shape: {n_arr.shape}\")\n", - "print(f\"Marker orbits: {orbits.shape} [{Nt_orb} snapshots, {orbits.shape[1]} markers]\")" + "print(f\"Marker orbits: {Nt_orb} snapshots, {orbits.sizes['marker']} markers\")" ] }, { @@ -333,8 +333,8 @@ " n_2d = n_arr[idx, :, :, 0]\n", " im = ax.pcolormesh(X, Y, n_2d, vmin=0.0, vmax=vmax_plot, cmap=\"Blues\", shading=\"auto\")\n", " ax.scatter(\n", - " orbits[oidx, :, 0],\n", - " orbits[oidx, :, 1],\n", + " orbits.x[oidx],\n", + " orbits.y[oidx],\n", " c=c_val, cmap=\"autumn\", s=3,\n", " vmin=0.0, vmax=1.0, alpha=0.7,\n", " )\n", @@ -376,8 +376,8 @@ "source": [ "fig, ax = plt.subplots(figsize=(8, 6))\n", "sc = ax.scatter(\n", - " orbits[-1, :, 0],\n", - " orbits[-1, :, 1],\n", + " orbits.x[-1],\n", + " orbits.y[-1],\n", " c=c_val, cmap=\"autumn\", s=8, vmin=0.0, vmax=1.0,\n", ")\n", "ax.set_xlim(0.0, r1)\n", @@ -408,8 +408,8 @@ "metadata": {}, "outputs": [], "source": [ - "x_all = orbits[:, :, 0]\n", - "y_all = orbits[:, :, 1]\n", + "x_all = orbits.x\n", + "y_all = orbits.y\n", "\n", "x_min, x_max = float(np.min(x_all)), float(np.max(x_all))\n", "y_min, y_max = float(np.min(y_all)), float(np.max(y_all))\n", diff --git a/tutorials/tutorial_gas_expansion_sph.ipynb b/tutorials/tutorial_gas_expansion_sph.ipynb index 7080b7f9a..0e68e5a37 100644 --- a/tutorials/tutorial_gas_expansion_sph.ipynb +++ b/tutorials/tutorial_gas_expansion_sph.ipynb @@ -404,10 +404,10 @@ "bc_x = np.asarray(f_e1e2[\"e1\"])\n", "bc_y = np.asarray(f_e1e2[\"e2\"])\n", "\n", - "# markers\n", - "orbits = np.asarray(out.orbits.euler_fluid)\n", - "positions = orbits[0, :, :3]\n", - "weights = orbits[0, :, 6]\n", + "# markers: one (t, marker) variable per saved quantity (x, y, z, v1, v2, v3, weight)\n", + "orbits = out.evaluate(\"euler_fluid/orbits\")\n", + "positions = np.stack([orbits.x[0], orbits.y[0], orbits.z[0]], axis=-1)\n", + "weights = orbits.weight[0].values\n", "\n", "# binning and sph eval\n", "n_sph = np.asarray(density)[0]\n", @@ -483,7 +483,7 @@ "dt = time_opts.dt\n", "Nt = out.time.size - 1\n", "\n", - "positions = orbits[:, :, :3]\n", + "positions = np.stack([orbits.x, orbits.y, orbits.z], axis=-1)\n", "\n", "interval = Nt / 10\n", "plot_ct = 0\n", diff --git a/tutorials/tutorial_particle_tracing.ipynb b/tutorials/tutorial_particle_tracing.ipynb index 7ed57a1fc..eb57145c3 100644 --- a/tutorials/tutorial_particle_tracing.ipynb +++ b/tutorials/tutorial_particle_tracing.ipynb @@ -325,8 +325,8 @@ "\n", "fig = plt.figure(figsize=(10, 6))\n", "\n", - "# out.evaluate(\"/orbits\") returns an xarray.DataArray with dims (t, marker, quantity);\n", - "# the quantity coordinate labels the saved columns (\"x\", \"y\", \"z\", \"v1\", ..., \"weight\", \"id\")\n", + "# out.evaluate(\"/orbits\") returns an xarray.Dataset with one (t, marker) variable per\n", + "# saved quantity, e.g. orbits.x, orbits.v1, orbits.weight; print(orbits) lists them all\n", "orbits = out.evaluate(\"kinetic_ions/orbits\")\n", "orbits_uni = out_2.evaluate(\"kinetic_ions/orbits\")\n", "\n", @@ -335,7 +335,7 @@ "pos_uni = orbits_uni.isel(t=0)\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.scatter(pos.sel(quantity=\"x\"), pos.sel(quantity=\"y\"), s=2.0)\n", + "plt.scatter(pos.x, pos.y, s=2.0)\n", "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", @@ -345,7 +345,7 @@ "plt.title(\"sim_1: draw uniform in logical space\")\n", "\n", "plt.subplot(1, 2, 2)\n", - "plt.scatter(pos_uni.sel(quantity=\"x\"), pos_uni.sel(quantity=\"y\"), s=2.0)\n", + "plt.scatter(pos_uni.x, pos_uni.y, s=2.0)\n", "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", @@ -399,10 +399,10 @@ "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = out_3.orbits.kinetic_ions.values\n", + "orbits_standard = out_3.evaluate(\"kinetic_ions/orbits\")\n", "\n", "plt.subplot(1, 3, 1)\n", - "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", + "plt.scatter(orbits.x[0], orbits.y[0], s=2.0)\n", "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", @@ -412,7 +412,7 @@ "plt.title(\"sim_1: draw uniform in logical space\")\n", "\n", "plt.subplot(1, 3, 2)\n", - "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.0)\n", + "plt.scatter(orbits_uni.x[0], orbits_uni.y[0], s=2.0)\n", "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", @@ -422,7 +422,7 @@ "plt.title(\"sim_2: draw uniform on disc\")\n", "\n", "plt.subplot(1, 3, 3)\n", - "plt.scatter(orbits_standard[0, :, 0], orbits_standard[0, :, 1], s=2.0)\n", + "plt.scatter(orbits_standard.x[0], orbits_standard.y[0], s=2.0)\n", "circle3 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle3)\n", @@ -474,10 +474,10 @@ "\n", "fig = plt.figure(figsize=(15, 6))\n", "\n", - "orbits_standard = out_3.orbits.kinetic_ions.values\n", + "orbits_standard = out_3.evaluate(\"kinetic_ions/orbits\")\n", "\n", "plt.subplot(1, 3, 1)\n", - "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", + "plt.scatter(orbits.x[0], orbits.y[0], s=2.0)\n", "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", @@ -487,7 +487,7 @@ "plt.title(\"sim_1: draw uniform in logical space\")\n", "\n", "plt.subplot(1, 3, 2)\n", - "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.0)\n", + "plt.scatter(orbits_uni.x[0], orbits_uni.y[0], s=2.0)\n", "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", @@ -497,7 +497,7 @@ "plt.title(\"sim_2: draw uniform on disc\")\n", "\n", "plt.subplot(1, 3, 3)\n", - "plt.scatter(orbits_standard[0, :, 0], orbits_standard[0, :, 1], s=2.0)\n", + "plt.scatter(orbits_standard.x[0], orbits_standard.y[0], s=2.0)\n", "circle3 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle3)\n", @@ -614,13 +614,13 @@ "id": "35", "metadata": {}, "source": [ - "Under `out.orbits.`, Struphy stores orbit data in a 3D array (indexable as NumPy via `.values`):\n", + "`out.evaluate(\"/orbits\")` (or `out.orbits.`) returns the saved marker orbits as an `xarray.Dataset` with dimensions `t` (saved time step) and `marker` (particle index). Each saved quantity is one variable of this dataset:\n", "\n", - "- axis 0: time step,\n", - "- axis 1: particle index,\n", - "- axis 2: particle attributes.\n", + "- `x`, `y`, `z`: physical positions,\n", + "- `v1`, `v2`, `v3`: Cartesian velocities (for `Particles6D`; other particle classes save their own velocity coordinates),\n", + "- `weight`: time-dependent marker weight.\n", "\n", - "The first three attributes are particle positions, followed by velocities and then weights (initial and time-dependent)." + "Each variable carries a `description` attribute, e.g. `orbits.x.attrs[\"description\"]`. Use `orbits.x.values` for a NumPy array of shape `(Nt, Np)`." ] }, { @@ -630,11 +630,10 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = out.orbits.kinetic_ions.values\n", + "orbits = out.evaluate(\"kinetic_ions/orbits\")\n", "\n", - "Nt = orbits.shape[0]\n", - "Np = orbits.shape[1]\n", - "Nattr = orbits.shape[2]" + "Nt = orbits.sizes[\"t\"]\n", + "Np = orbits.sizes[\"marker\"]" ] }, { @@ -657,7 +656,7 @@ "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", - " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", + " ax.scatter(orbits.x[:, i], orbits.y[:, i], c=colors[i % 4], alpha=alpha)\n", "\n", "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", @@ -790,10 +789,10 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = out_withB.orbits.kinetic_ions.values\n", + "orbits = out_withB.evaluate(\"kinetic_ions/orbits\")\n", "\n", - "Nt = orbits.shape[0]\n", - "Np = orbits.shape[1]" + "Nt = orbits.sizes[\"t\"]\n", + "Np = orbits.sizes[\"marker\"]" ] }, { @@ -814,7 +813,7 @@ "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", - " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", + " ax.scatter(orbits.x[:, i], orbits.y[:, i], c=colors[i % 4], alpha=alpha)\n", "\n", "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", @@ -1157,10 +1156,10 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = out_asdex.orbits.kinetic_ions.values\n", + "orbits = out_asdex.evaluate(\"kinetic_ions/orbits\")\n", "\n", - "Nt = orbits.shape[0]\n", - "Np = orbits.shape[1]" + "Nt = orbits.sizes[\"t\"]\n", + "Np = orbits.sizes[\"marker\"]" ] }, { @@ -1178,11 +1177,11 @@ "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", + " r = np.sqrt(orbits.x[:, i] ** 2 + orbits.y[:, i] ** 2)\n", " # poloidal\n", - " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", + " ax.scatter(r, orbits.z[:, i], c=colors[i % 4], s=1)\n", " # top view\n", - " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", + " ax_top.scatter(orbits.x[:, i], orbits.y[:, i], c=colors[i % 4], s=1)\n", "\n", "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", @@ -1343,10 +1342,10 @@ "metadata": {}, "outputs": [], "source": [ - "orbits = out_gc.orbits.kinetic_ions.values\n", + "orbits = out_gc.evaluate(\"kinetic_ions/orbits\")\n", "\n", - "Nt = orbits.shape[0]\n", - "Np = orbits.shape[1]" + "Nt = orbits.sizes[\"t\"]\n", + "Np = orbits.sizes[\"marker\"]" ] }, { @@ -1364,11 +1363,11 @@ "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", + " r = np.sqrt(orbits.x[:, i] ** 2 + orbits.y[:, i] ** 2)\n", " # poloidal\n", - " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", + " ax.scatter(r, orbits.z[:, i], c=colors[i % 4], s=1)\n", " # top view\n", - " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", + " ax_top.scatter(orbits.x[:, i], orbits.y[:, i], c=colors[i % 4], s=1)\n", "\n", "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 445ec65b3..1829c0bec 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -495,7 +495,7 @@ "id": "36", "metadata": {}, "source": [ - "Saved marker orbits sit under their species. Select one marker and plot its coordinates or quantities over time." + "Saved marker orbits sit under their species as an `xarray.Dataset` with one `(t, marker)` variable per quantity (`x`, `y`, `z`, velocities, `weight`); each variable's `description` attribute says what it is. Select one marker and plot its positions over time." ] }, { @@ -506,7 +506,7 @@ "outputs": [], "source": [ "orbit = out.kinetic_ions.orbits.isel(marker=0)\n", - "orbit.sel(quantity=[\"x\", \"y\", \"z\"]).plot.line(x=\"t\", hue=\"quantity\")" + "orbit[[\"x\", \"y\", \"z\"]].to_dataarray(\"quantity\").plot.line(x=\"t\", hue=\"quantity\")" ] }, { From 1c4ec1c95a453307f26de5896beed2a933606957 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 15:40:15 +0200 Subject: [PATCH 144/193] formatting --- src/struphy/post_processing/tests/test_output.py | 2 +- src/struphy/simulation/sim.py | 6 +----- src/struphy/simulation/tests/test_output.py | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index b46c6019b..a2532e7bf 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -12,9 +12,9 @@ from struphy import BaseUnits, Time, domains from struphy.models import Maxwell +from struphy.pic.particles import Particles6D from struphy.post_processing import output as output_module from struphy.post_processing import store -from struphy.pic.particles import Particles6D from struphy.post_processing.arrays import wrap_orbits from struphy.post_processing.manifest import is_processed, normalize_options, source_fingerprint from struphy.post_processing.output import Output, open_output diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 00496348d..773b50f30 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1622,11 +1622,7 @@ def _serialize_initial_condition(value): return { "type": type(value).__name__, "params": Simulation._serialize_initial_condition( - { - field.name: getattr(value, field.name) - for field in dataclasses.fields(value) - if field.init - } + {field.name: getattr(value, field.name) for field in dataclasses.fields(value) if field.init} ), } if hasattr(value, "params"): diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 36d912c6a..d939b050a 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -1,7 +1,7 @@ """Tests for the link between a Simulation and its output.""" -import json import inspect +import json import os from pathlib import Path @@ -20,8 +20,8 @@ maxwellians, perturbations, ) -from struphy.initial.base import Perturbation from struphy.initial import perturbations +from struphy.initial.base import Perturbation from struphy.kinetic_background import maxwellians from struphy.linear_algebra.solver import SolverParameters from struphy.models import ColdPlasmaVlasov, LinearMHD, Maxwell, Poisson, VlasovAmpereOneSpecies From 88523ac71969438a3f64420d607156356a4f436b Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:17:25 +0200 Subject: [PATCH 145/193] updated examples to the new API --- .../cyclone/pproc_cyclone.py | 86 ++++++++++++++----- .../itg_cylindre/pproc_drift_kinetic.py | 86 ++++++++++++++----- .../diocotron_instability/pproc_diocotron.py | 74 ++++++++++------ .../bump_on/pproc_bump_on.py | 20 ++++- .../pproc_strong_Landau_damping.py | 19 +++- .../two_stream/pproc_two_stream.py | 35 +++++--- .../pproc_weak_Landau_damping.py | 31 ++++--- .../pproc_weibel_instability.py | 16 +++- 8 files changed, 263 insertions(+), 104 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index 11c9180f3..f409679ce 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -1,47 +1,87 @@ import sys from pathlib import Path -import struphy_plots +import numpy as np from matplotlib import pyplot as plt -from struphy_plots.output_accessors import OutputPlots from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" -# quantity whose exponential growth rate is fitted +# scalar whose exponential growth rate is fitted, and the fit window in Struphy time units FIT_QUANTITY = "phi_integral" FIT_WINDOW = (0.0, None) -SHOW_EQUIL_PROFILE = False - -# products to sweep interactively, as (name, displayed component or None, physical plane) -SWEEPS = [ - ("kinetic_ions/e1_e2_density/delta_f", None, "RZ"), - ("em_fields/phi_xyz", None, "RZ"), - ("diagnostics/rho_xyz", None, "RZ"), - ("diagnostics/rho_xyz", None, "XY"), +# products shown at the last saved time, as (product, physical plane, logical coordinate held fixed) +SNAPSHOTS = [ + ("kinetic_ions/e1_e2_density/delta_f", "RZ", {}), + ("em_fields/phi_xyz", "RZ", {"e3": 0}), + ("diagnostics/rho_xyz", "RZ", {"e3": 0}), + ("diagnostics/rho_xyz", "XY", {"e2": 0}), ] +def product(run, name): + """Look up a saved product such as ``"kinetic_ions/e1_e2_density/f"`` by attribute access.""" + data = run + for part in name.split("/"): + data = getattr(data, part) + return data + + +def plot_growth(series, window=(None, None)): + """Plot a positive time series on a log axis with a fitted exponential ``exp(rate * t)``.""" + time, values = series.t.values, series.values + lo = time[0] if window[0] is None else window[0] + hi = time[-1] if window[1] is None else window[1] + fig, ax = plt.subplots() + ax.plot(time, values, label=series.name) + + mask = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) + if np.count_nonzero(mask) >= 2: + rate, intercept = np.polyfit(time[mask], np.log(values[mask]), 1) + ax.plot(time[mask], np.exp(rate * time[mask] + intercept), "--", label=f"fit, rate = {rate:.4g}") + print(f"{series.name}: growth rate = {rate:.6g}") + + ax.set(xlabel="time", yscale="log", title=f"Evolution of {series.name}") + ax.legend() + return fig, ax + + +def plot_plane(data, plane, fixed): + """Pseudocolor plot of the last saved time in the physical RZ or XY plane.""" + snapshot = data.isel(t=-1, **fixed) + if plane == "RZ": + snapshot = snapshot.assign_coords(R=np.hypot(snapshot.X, snapshot.Y)) + x, y = "R", "Z" + else: + x, y = "X", "Y" + fig, ax = plt.subplots() + snapshot.plot(x=x, y=y, ax=ax) + ax.set_aspect("equal") + ax.set_title(f"{data.name}, t = {float(snapshot.t):.3g}") + return fig, ax + + +def plot_trajectories(orbits, max_markers=1000): + """Marker paths in the physical XY plane.""" + selected = orbits.isel(marker=slice(0, max_markers)) + fig, ax = plt.subplots() + ax.plot(selected.x, selected.y, lw=0.5) + ax.set(xlabel="$x$", ylabel="$y$", title="Marker trajectories", aspect="equal") + return fig, ax + + def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( - fit=FIT_WINDOW, - fit_amplitude=True, - title=f"Evolution of {FIT_QUANTITY}", - ) - - if SHOW_EQUIL_PROFILE: - OutputPlots(run).equilibrium() + plot_growth(run.scalars[FIT_QUANTITY], window=FIT_WINDOW) - for name, component, plane in SWEEPS: - selection = {} if component is None else {"component": component} - run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection) + for name, plane, fixed in SNAPSHOTS: + plot_plane(product(run, name), plane, fixed) - run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000) + plot_trajectories(run.kinetic_ions.orbits, max_markers=1000) plt.show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index c8232a73d..cecee3204 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -1,47 +1,87 @@ import sys from pathlib import Path -import struphy_plots +import numpy as np from matplotlib import pyplot as plt -from struphy_plots.output_accessors import OutputPlots from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" -# quantity whose exponential growth rate is fitted +# scalar whose exponential growth rate is fitted, and the fit window in Struphy time units FIT_QUANTITY = "phi_integral" FIT_WINDOW = (0.0, None) -SHOW_EQUIL_PROFILE = False - -# products to sweep interactively, as (name, displayed component or None, physical plane) -SWEEPS = [ - ("kinetic_ions/e1_e2_density/f", None, "XY"), - ("kinetic_ions/e1_e2_density/delta_f", None, "XY"), - ("em_fields/phi_xyz", None, "XY"), - ("diagnostics/rho_xyz", None, "XY"), +# products shown at the last saved time, as (product, physical plane, logical coordinate held fixed) +SNAPSHOTS = [ + ("kinetic_ions/e1_e2_density/f", "XY", {}), + ("kinetic_ions/e1_e2_density/delta_f", "XY", {}), + ("em_fields/phi_xyz", "XY", {"e3": 0}), + ("diagnostics/rho_xyz", "XY", {"e3": 0}), ] +def product(run, name): + """Look up a saved product such as ``"kinetic_ions/e1_e2_density/f"`` by attribute access.""" + data = run + for part in name.split("/"): + data = getattr(data, part) + return data + + +def plot_growth(series, window=(None, None)): + """Plot a positive time series on a log axis with a fitted exponential ``exp(rate * t)``.""" + time, values = series.t.values, series.values + lo = time[0] if window[0] is None else window[0] + hi = time[-1] if window[1] is None else window[1] + fig, ax = plt.subplots() + ax.plot(time, values, label=series.name) + + mask = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) + if np.count_nonzero(mask) >= 2: + rate, intercept = np.polyfit(time[mask], np.log(values[mask]), 1) + ax.plot(time[mask], np.exp(rate * time[mask] + intercept), "--", label=f"fit, rate = {rate:.4g}") + print(f"{series.name}: growth rate = {rate:.6g}") + + ax.set(xlabel="time", yscale="log", title=f"Evolution of {series.name}") + ax.legend() + return fig, ax + + +def plot_plane(data, plane, fixed): + """Pseudocolor plot of the last saved time in the physical RZ or XY plane.""" + snapshot = data.isel(t=-1, **fixed) + if plane == "RZ": + snapshot = snapshot.assign_coords(R=np.hypot(snapshot.X, snapshot.Y)) + x, y = "R", "Z" + else: + x, y = "X", "Y" + fig, ax = plt.subplots() + snapshot.plot(x=x, y=y, ax=ax) + ax.set_aspect("equal") + ax.set_title(f"{data.name}, t = {float(snapshot.t):.3g}") + return fig, ax + + +def plot_trajectories(orbits, max_markers=1000): + """Marker paths in the physical XY plane.""" + selected = orbits.isel(marker=slice(0, max_markers)) + fig, ax = plt.subplots() + ax.plot(selected.x, selected.y, lw=0.5) + ax.set(xlabel="$x$", ylabel="$y$", title="Marker trajectories", aspect="equal") + return fig, ax + + def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - run.evaluate(FIT_QUANTITY).struphy.plot.timeseries( - fit=FIT_WINDOW, - fit_amplitude=True, - title=f"Evolution of {FIT_QUANTITY}", - ) - - if SHOW_EQUIL_PROFILE: - OutputPlots(run).equilibrium() + plot_growth(run.scalars[FIT_QUANTITY], window=FIT_WINDOW) - for name, component, plane in SWEEPS: - selection = {} if component is None else {"component": component} - run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane=plane, **selection) + for name, plane, fixed in SNAPSHOTS: + plot_plane(product(run, name), plane, fixed) - run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000) + plot_trajectories(run.kinetic_ions.orbits, max_markers=1000) plt.show() diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 20f3fc721..260345903 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -7,51 +7,75 @@ import sys from pathlib import Path -import struphy_plots -from struphy_plots.output_accessors import OutputPlots +import numpy as np +from matplotlib import pyplot as plt from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_1" +# scalar whose exponential growth rate is fitted, and the fit window in Struphy time units FIT_QUANTITY = "en_phi" FIT_WINDOW = (0.0, 42.0) -SHOW_EQUIL_PROFILE = True - -# products to sweep interactively in the physical XY plane -SWEEPS = [ - "kinetic_ions/e1_e2_density/f", - "kinetic_ions/e1_e2_density/delta_f", - "em_fields/phi_xyz", +# products shown at the last saved time in the physical XY plane +SNAPSHOTS = [ + ("kinetic_ions", "e1_e2_density", "f"), + ("kinetic_ions", "e1_e2_density", "delta_f"), + ("em_fields", "phi_xyz"), ] +def fit_growth(series, window=(None, None)): + """Fit ``exp(rate * t + intercept)`` to the positive samples inside ``window``.""" + time, values = series.t.values, series.values + lo = time[0] if window[0] is None else window[0] + hi = time[-1] if window[1] is None else window[1] + mask = (time >= lo) & (time <= hi) & np.isfinite(values) & (values > 0) + if np.count_nonzero(mask) < 2: + return None + rate, intercept = np.polyfit(time[mask], np.log(values[mask]), 1) + return rate, intercept, time[mask] + + def main(paths=(DEFAULT_OUTPUT,)): runs = [Output(path).pproc(physical=True) for path in paths] run = runs[0] # growth rate of the electrostatic energy, one curve per run - first, *rest = (each[FIT_QUANTITY] for each in runs) - plot = first.struphy.plot.timeseries( - *rest, - fit=FIT_WINDOW, - title=f"Evolution of {FIT_QUANTITY}", - ).show() - - for each, result in zip(runs, plot.fit_results): - print(f"{each.path_out.name}: growth rate = {None if result is None else result.rate}") + fig, ax = plt.subplots() + for each in runs: + series = each.scalars[FIT_QUANTITY] + (line,) = ax.plot(series.t, series, label=each.path_out.name) + result = fit_growth(series, FIT_WINDOW) + if result is not None: + rate, intercept, time = result + ax.plot(time, np.exp(rate * time + intercept), "--", color=line.get_color()) + print(f"{each.path_out.name}: growth rate = {None if result is None else result[0]}") + ax.set(xlabel="time", yscale="log", title=f"Evolution of {FIT_QUANTITY}") + ax.legend() + plt.show() if len(runs) > 1: return - if SHOW_EQUIL_PROFILE: - OutputPlots(run).equilibrium() - - for name in SWEEPS: - run.evaluate(name).struphy.plot.viewer(x="e1", y="e2", coords="physical", plane="XY").show() - - run.kinetic_ions.orbits.struphy.plot.trajectories(max_markers=1000).show() + for path in SNAPSHOTS: + data = run + for part in path: + data = getattr(data, part) + snapshot = data.isel(t=-1) + if "e3" in snapshot.dims: + snapshot = snapshot.isel(e3=0) + fig, ax = plt.subplots() + snapshot.plot(x="X", y="Y", ax=ax) + ax.set(aspect="equal", title=f"{'/'.join(path)}, t = {float(snapshot.t):.3g}") + plt.show() + + orbits = run.kinetic_ions.orbits.isel(marker=slice(0, 1000)) + fig, ax = plt.subplots() + ax.plot(orbits.x, orbits.y, lw=0.5) + ax.set(xlabel="$x$", ylabel="$y$", title="Marker trajectories", aspect="equal") + plt.show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 4a77ee18b..322990410 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -1,7 +1,7 @@ import argparse from pathlib import Path -import struphy_plots +import numpy as np from matplotlib import pyplot as plt from struphy import Output @@ -9,11 +9,20 @@ DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" +def plot_panels(data, x, y, n_panels, ncols, title=None): + """Snapshots of a binned distribution at evenly spaced saved times, one panel each.""" + times = np.unique(np.linspace(0, data.sizes["t"] - 1, n_panels).round().astype(int)) + grid = data.isel(t=times).plot(x=x, y=y, col="t", col_wrap=min(ncols, len(times))) + if title is not None: + grid.fig.suptitle(title) + return grid + + def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # initial velocity distribution - initial = run.evaluate("kinetic_ions/v1_density/f").isel(t=0) + initial = run.kinetic_ions.v1_density.f.isel(t=0) ax = initial.plot()[0].axes ax.set( xlabel="velocity $v$", @@ -23,10 +32,13 @@ def main(path_out=DEFAULT_OUTPUT): plt.show() # electric field energy - run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() + run.scalars.electric_energy.plot(yscale="log") + plt.title("Electric energy") + plt.show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4, title="full-$f$") + plt.show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index 7be8dc2b8..f53a0b11d 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -1,21 +1,34 @@ import argparse from pathlib import Path -import struphy_plots +import numpy as np +from matplotlib import pyplot as plt from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" +def plot_panels(data, x, y, n_panels, ncols, title=None): + """Snapshots of a binned distribution at evenly spaced saved times, one panel each.""" + times = np.unique(np.linspace(0, data.sizes["t"] - 1, n_panels).round().astype(int)) + grid = data.isel(t=times).plot(x=x, y=y, col="t", col_wrap=min(ncols, len(times))) + if title is not None: + grid.fig.suptitle(title) + return grid + + def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # electric field energy - run.scalars.electric_energy.struphy.plot.timeseries(title="Electric energy").show() + run.scalars.electric_energy.plot(yscale="log") + plt.title("Electric energy") + plt.show() # full f in the e1-v1 plane - run.kinetic_ions.e1_v1_density.f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4, title="full-$f$").show() + plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4, title="full-$f$") + plt.show() if __name__ == "__main__": diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index c6e076a46..e848188f7 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -1,33 +1,42 @@ import argparse from pathlib import Path -import struphy_plots -from struphy_plots.plotting import save_all_scalars +import numpy as np +from matplotlib import pyplot as plt from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" +def plot_panels(data, x, y, n_panels, ncols, title=None): + """Snapshots of a binned distribution at evenly spaced saved times, one panel each.""" + times = np.unique(np.linspace(0, data.sizes["t"] - 1, n_panels).round().astype(int)) + grid = data.isel(t=times).plot(x=x, y=y, col="t", col_wrap=min(ncols, len(times))) + if title is not None: + grid.fig.suptitle(title) + return grid + + def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) - # table and figures of every scalar: post_processing/report/ - save_all_scalars(run.scalars, run.path_pproc / "report", run_label=run.label) + # every scalar time series as CSV: post_processing/scalars.csv + print(f"scalars written to {run.save_scalars()}") # electric field growth against the analytical rate (0.2845 in units of m/c) energy = run.scalars.electric_energy - analytical = energy.copy(data=10 ** (0.2845 * energy.t - 5.3)) # t is in Struphy units - analytical.attrs["label"] = "analytical" - energy.struphy.plot.timeseries(analytical, title="Electric energy").show() + analytical = 10 ** (0.2845 * energy.t - 5.3) # t is in Struphy units + fig, ax = plt.subplots() + ax.plot(energy.t, energy, label="numerical") + ax.plot(energy.t, analytical, "--", label="analytical") + ax.set(xlabel="time", yscale="log", title="Electric energy") + ax.legend() + plt.show() # phase space evolution - f = run.kinetic_ions.e1_v1_density.f - f.struphy.plot.panels(x="e1", y="v1", nrows=3, ncols=4).show() - - # interactive alternative to dumping a frame sequence - f.struphy.plot.viewer(x="e1", y="v1").show() - + plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4) + plt.show() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 075150bb9..1cb32b0d1 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -2,13 +2,23 @@ from pathlib import Path import cunumpy as xp -import struphy_plots +import numpy as np +from matplotlib import pyplot as plt from struphy import Output DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" +def plot_panels(data, x, y, n_panels, ncols, title=None): + """Snapshots of a binned distribution at evenly spaced saved times, one panel each.""" + times = np.unique(np.linspace(0, data.sizes["t"] - 1, n_panels).round().astype(int)) + grid = data.isel(t=times).plot(x=x, y=y, col="t", col_wrap=min(ncols, len(times))) + if title is not None: + grid.fig.suptitle(title) + return grid + + def E_exact(t, eps=0.001): """Analytical electric energy of weak Landau damping, t in normalized units.""" r = 0.3677 @@ -22,18 +32,19 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): run = Output(path_out) # electric field energy against the analytical damping - energy = run.scalars.electric_energy.copy() - energy.attrs["label"] = "numerical" - analytical = energy.copy(data=E_exact(energy.t.values, eps=amplitude)) # t is in Struphy units - analytical.attrs["label"] = "analytical" - energy.struphy.plot.timeseries(analytical, title="Electric energy").show() + energy = run.scalars.electric_energy + analytical = E_exact(energy.t.values, eps=amplitude) # t is in Struphy units + fig, ax = plt.subplots() + ax.plot(energy.t, energy, label="numerical") + ax.plot(energy.t, analytical, "--", label="analytical") + ax.set(xlabel="time", yscale="log", title="Electric energy") + ax.legend() + plt.show() # full f and delta f in the e1-v1 plane at four times for quantity, title in (("f", "full-$f$"), ("delta_f", r"$\delta f$")): - getattr(run.kinetic_ions.e1_v1_density, quantity).struphy.plot.panels( - x="e1", y="v1", nrows=1, ncols=4, title=title - ).show() - + plot_panels(getattr(run.kinetic_ions.e1_v1_density, quantity), x="e1", y="v1", n_panels=4, ncols=4, title=title) + plt.show() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 0126c9984..4bc861b05 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -2,7 +2,7 @@ from pathlib import Path import cunumpy as xp -import struphy_plots +import numpy as np from matplotlib import pyplot as plt from struphy import Output @@ -10,8 +10,17 @@ DEFAULT_OUTPUT = Path(__file__).resolve().parent / "sim_data" +def plot_panels(data, x, y, n_panels, ncols, title=None): + """Snapshots of a binned distribution at evenly spaced saved times, one panel each.""" + times = np.unique(np.linspace(0, data.sizes["t"] - 1, n_panels).round().astype(int)) + grid = data.isel(t=times).plot(x=x, y=y, col="t", col_wrap=min(ncols, len(times))) + if title is not None: + grid.fig.suptitle(title) + return grid + + def main(path_out=DEFAULT_OUTPUT): - run = Output(path_out, time_units="normalized") + run = Output(path_out).with_time_units("normalized") time = run.time Tend = run.time_opts.Tend algo = run.time_opts.split_algo @@ -101,7 +110,8 @@ def field_energy(field): ("v1_v2_density", "v1", "v2"), ): for quantity in ("f", "delta_f"): - getattr(getattr(distributions, bin_name), quantity).struphy.plot.panels(x=x, y=y, nrows=5, ncols=4).show() + plot_panels(getattr(getattr(distributions, bin_name), quantity), x=x, y=y, n_panels=20, ncols=4) + plt.show() # ------------------ # EM field at selected times From 863a15db88b3bf908f7f5a94d30a6e06c3fca58e Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:17:58 +0200 Subject: [PATCH 146/193] More bugfixes for serialization --- src/struphy/initial/base.py | 2 + src/struphy/simulation/sim.py | 131 +++++++++++++++----- src/struphy/simulation/tests/test_output.py | 33 +++++ 3 files changed, 135 insertions(+), 31 deletions(-) diff --git a/src/struphy/initial/base.py b/src/struphy/initial/base.py index 53b6199d4..77c30eb09 100644 --- a/src/struphy/initial/base.py +++ b/src/struphy/initial/base.py @@ -1,3 +1,4 @@ +import copy import logging from abc import ABCMeta, abstractmethod from typing import Callable @@ -226,6 +227,7 @@ def __init__( given_in_basis: LiteralOptions.GivenInBasis = None, comp: int = 0, ): + self.params = copy.deepcopy(locals()) self.fun = fun # use the setters diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 773b50f30..26b0cca8b 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,8 +1,10 @@ # third party imports +import ast import copy import dataclasses import glob import hashlib +import importlib import inspect import json import logging @@ -1560,7 +1562,59 @@ def to_dict(self) -> dict: } @staticmethod - def _serialize_initial_condition(value): + def _serialize_function_globals(func, source: str, seen=None) -> dict: + """Capture the module-level names a user function needs to be re-executed. + + Default arguments (``def f(x, r=r_minus)``) and body references (``R0``, + helper functions) are resolved in the defining module, so the source alone + cannot be restored. Plain values, modules and other top-level functions are + recorded; anything else is left out and fails only if it is actually used. + """ + seen = set() if seen is None else seen + seen = seen | {id(func)} + names = sorted( + { + node.id + for node in ast.walk(ast.parse(source)) + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) + } + ) + + def plain(item): + if item is None or isinstance(item, (bool, int, float, str)): + return True + return isinstance(item, (list, tuple)) and all(plain(each) for each in item) + + referenced = {} + for name in names: + if name == func.__name__ or name not in func.__globals__: + continue + item = func.__globals__[name] + if inspect.ismodule(item): + referenced[name] = {"type": "module", "name": item.__name__} + elif plain(item): + referenced[name] = {"type": "value", "value": Simulation._serialize_initial_condition(item)} + elif inspect.isfunction(item) and id(item) not in seen: + serialized = Simulation._serialize_initial_condition(item, _seen=seen) + if serialized.get("serialization") != "unsupported": + referenced[name] = serialized + return referenced + + @staticmethod + def _deserialize_function_globals(referenced: dict) -> dict: + """Rebuild the namespace recorded by :meth:`_serialize_function_globals`.""" + namespace = {} + for name, item in referenced.items(): + if item["type"] == "module": + namespace[name] = importlib.import_module(item["name"]) + elif item["type"] == "value": + namespace[name] = Simulation._deserialize_initial_condition(item["value"]) + else: + namespace[name] = Simulation._deserialize_initial_condition(item) + return namespace + + @staticmethod + def _serialize_initial_condition(value, _seen=None): """Convert initial-condition definitions into JSON-compatible provenance data. This deliberately captures constructor parameters rather than evaluated FEEC @@ -1592,20 +1646,25 @@ def _serialize_initial_condition(value): "serialization": "unsupported", "reason": "nested classes are not supported", } - try: - source = textwrap.dedent(inspect.getsource(cls)) - except (OSError, TypeError): - return { + if cls.__module__ == "struphy" or cls.__module__.startswith("struphy."): + # Struphy's own classes are importable; re-executing their source would + # lose the names their module imports. + data = {"type": "python_class", "name": cls.__qualname__, "module": cls.__module__} + else: + try: + source = textwrap.dedent(inspect.getsource(cls)) + except (OSError, TypeError): + return { + "type": "python_class", + "serialization": "unsupported", + "reason": "source code is unavailable", + } + data = { "type": "python_class", - "serialization": "unsupported", - "reason": "source code is unavailable", + "name": cls.__name__, + "source": source, + "source_sha256": hashlib.sha256(source.encode()).hexdigest(), } - data = { - "type": "python_class", - "name": cls.__name__, - "source": source, - "source_sha256": hashlib.sha256(source.encode()).hexdigest(), - } if hasattr(value, "params"): data["params"] = Simulation._serialize_initial_condition(value.params) elif hasattr(value, "__dict__"): @@ -1661,12 +1720,16 @@ def _serialize_initial_condition(value): "serialization": "unsupported", "reason": "source code is unavailable", } - return { + data = { "type": "python_function", "name": value.__name__, "source": source, "source_sha256": hashlib.sha256(source.encode()).hexdigest(), } + referenced = Simulation._serialize_function_globals(value, source, _seen) + if referenced: + data["globals"] = referenced + return data if callable(value): return { "type": "callable", @@ -1702,28 +1765,34 @@ def _deserialize_initial_condition(value): import numpy as np namespace = {"np": np, "numpy": np, "xp": xp, "cp": xp, "cupy": xp} + namespace.update(Simulation._deserialize_function_globals(value.get("globals", {}))) exec(source, namespace) # noqa: S102 -- reconstruct saved Python function return namespace[value["name"]] if kind == "python_class": if value.get("serialization") == "unsupported": raise ValueError(f"Cannot restore initial-condition class: {value['reason']}.") - source = value["source"] - if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: - raise ValueError("Initial-condition class source hash does not match its metadata.") - import cunumpy as xp - import numpy as np - - namespace = { - "np": np, - "numpy": np, - "xp": xp, - "cp": xp, - "cupy": xp, - "Perturbation": Perturbation, - "dataclass": dataclasses.dataclass, - } - exec(source, namespace) # noqa: S102 -- reconstruct saved Python class - initial_condition_class = namespace[value["name"]] + if "module" in value: + initial_condition_class = importlib.import_module(value["module"]) + for part in value["name"].split("."): + initial_condition_class = getattr(initial_condition_class, part) + else: + source = value["source"] + if hashlib.sha256(source.encode()).hexdigest() != value["source_sha256"]: + raise ValueError("Initial-condition class source hash does not match its metadata.") + import cunumpy as xp + import numpy as np + + namespace = { + "np": np, + "numpy": np, + "xp": xp, + "cp": xp, + "cupy": xp, + "Perturbation": Perturbation, + "dataclass": dataclasses.dataclass, + } + exec(source, namespace) # noqa: S102 -- reconstruct saved Python class + initial_condition_class = namespace[value["name"]] if "params" in value: return initial_condition_class(**Simulation._deserialize_initial_condition(value["params"])) initial_condition = initial_condition_class.__new__(initial_condition_class) diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index d939b050a..8673154b5 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -35,6 +35,17 @@ def user_density_profile(eta1, eta2, eta3): return 1.0 + eta1 * 0.0 + eta2 * 0.0 + eta3 * 0.0 +PROFILE_INNER, PROFILE_OUTER = 0.25, 0.75 + + +def user_radial_step(eta1): + return np.where((PROFILE_INNER <= eta1) & (eta1 < PROFILE_OUTER), 1.0, 0.0) + + +def user_profile_with_globals(eta1, eta2, eta3, inner=PROFILE_INNER): + return 2.0 * user_radial_step(eta1) + inner + eta2 * 0.0 + eta3 * 0.0 + + class UserCosinePerturbation(Perturbation): def __init__(self, amplitude=0.1): self.params = {"amplitude": amplitude} @@ -217,6 +228,28 @@ def test_run_metadata_embeds_user_function_source(tmp_path): assert len(density["source_sha256"]) == 64 +def test_user_function_referencing_module_globals_round_trips(): + serialized = Simulation._serialize_initial_condition(user_profile_with_globals) + assert set(serialized["globals"]) == {"PROFILE_INNER", "user_radial_step"} + assert set(serialized["globals"]["user_radial_step"]["globals"]) == {"PROFILE_INNER", "PROFILE_OUTER", "np"} + + restored = Simulation._deserialize_initial_condition(json.loads(json.dumps(serialized))) + assert restored(0.5, 0.0, 0.0) == user_profile_with_globals(0.5, 0.0, 0.0) + assert restored(0.9, 0.0, 0.0) == user_profile_with_globals(0.9, 0.0, 0.0) + + +def test_struphy_class_wrapping_user_function_round_trips(): + from struphy.initial.base import GenericPerturbation + + serialized = Simulation._serialize_initial_condition(GenericPerturbation(user_profile_with_globals)) + assert serialized["module"] == "struphy.initial.base" + assert "source" not in serialized + + restored = Simulation._deserialize_initial_condition(json.loads(json.dumps(serialized))) + assert isinstance(restored, GenericPerturbation) + assert restored(0.5, 0.0, 0.0) == user_profile_with_globals(0.5, 0.0, 0.0) + + def test_from_output_restores_embedded_initial_condition_source(tmp_path, monkeypatch): path_out = tmp_path / "sim_1" path_out.mkdir() From bbd38465125eccca986cdfdfdac65570e38648a2 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:21:18 +0200 Subject: [PATCH 147/193] Formatting --- examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py | 1 + .../weak_Landau_damping/pproc_weak_Landau_damping.py | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index e848188f7..d4466a8b2 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -38,6 +38,7 @@ def main(path_out=DEFAULT_OUTPUT): plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4) plt.show() + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") parser.add_argument( diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 1cb32b0d1..61457c23e 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -46,6 +46,7 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): plot_panels(getattr(run.kinetic_ions.e1_v1_density, quantity), x="e1", y="v1", n_panels=4, ncols=4, title=title) plt.show() + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot a saved simulation run.") parser.add_argument( From af88d171d202727d7570109f8cfe4271b571eb6f Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:28:24 +0200 Subject: [PATCH 148/193] Optionally plot equil --- .../cyclone/pproc_cyclone.py | 24 ++++++++++++++++++ .../itg_cylindre/pproc_drift_kinetic.py | 24 ++++++++++++++++++ .../diocotron_instability/pproc_diocotron.py | 25 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index f409679ce..c81847c87 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -2,6 +2,7 @@ from pathlib import Path import numpy as np +import pyvista as pv from matplotlib import pyplot as plt from struphy import Output @@ -12,6 +13,8 @@ FIT_QUANTITY = "phi_integral" FIT_WINDOW = (0.0, None) +SHOW_EQUIL_PROFILE = False + # products shown at the last saved time, as (product, physical plane, logical coordinate held fixed) SNAPSHOTS = [ ("kinetic_ions/e1_e2_density/delta_f", "RZ", {}), @@ -63,6 +66,24 @@ def plot_plane(data, plane, fixed): return fig, ax +def plot_equilibrium(path_out): + """Radial equilibrium profiles from the geometry written at the start of the run.""" + equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) + shape = equilibrium.dimensions + grid = np.reshape(equilibrium.points, shape + (3,)) + radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2)[0, 0] + pressure = np.reshape(equilibrium.point_data["p0"], shape)[0, 0] + fig, ax = plt.subplots() + ax.plot(radius, pressure, label=r"$p_0$") + if "n0" in equilibrium.point_data: + density = np.reshape(equilibrium.point_data["n0"], shape)[0, 0] + ax.plot(radius, density, label=r"$n_0$") + ax.plot(radius, pressure / density, label=r"$T_0$") + ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") + ax.legend() + return fig, ax + + def plot_trajectories(orbits, max_markers=1000): """Marker paths in the physical XY plane.""" selected = orbits.isel(marker=slice(0, max_markers)) @@ -78,6 +99,9 @@ def main(path_out=DEFAULT_OUTPUT): # growth rate of the electrostatic potential plot_growth(run.scalars[FIT_QUANTITY], window=FIT_WINDOW) + if SHOW_EQUIL_PROFILE: + plot_equilibrium(run.path_out) + for name, plane, fixed in SNAPSHOTS: plot_plane(product(run, name), plane, fixed) diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index cecee3204..e92b8c5e3 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -2,6 +2,7 @@ from pathlib import Path import numpy as np +import pyvista as pv from matplotlib import pyplot as plt from struphy import Output @@ -12,6 +13,8 @@ FIT_QUANTITY = "phi_integral" FIT_WINDOW = (0.0, None) +SHOW_EQUIL_PROFILE = False + # products shown at the last saved time, as (product, physical plane, logical coordinate held fixed) SNAPSHOTS = [ ("kinetic_ions/e1_e2_density/f", "XY", {}), @@ -63,6 +66,24 @@ def plot_plane(data, plane, fixed): return fig, ax +def plot_equilibrium(path_out): + """Radial equilibrium profiles from the geometry written at the start of the run.""" + equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) + shape = equilibrium.dimensions + grid = np.reshape(equilibrium.points, shape + (3,)) + radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2)[0, 0] + pressure = np.reshape(equilibrium.point_data["p0"], shape)[0, 0] + fig, ax = plt.subplots() + ax.plot(radius, pressure, label=r"$p_0$") + if "n0" in equilibrium.point_data: + density = np.reshape(equilibrium.point_data["n0"], shape)[0, 0] + ax.plot(radius, density, label=r"$n_0$") + ax.plot(radius, pressure / density, label=r"$T_0$") + ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") + ax.legend() + return fig, ax + + def plot_trajectories(orbits, max_markers=1000): """Marker paths in the physical XY plane.""" selected = orbits.isel(marker=slice(0, max_markers)) @@ -78,6 +99,9 @@ def main(path_out=DEFAULT_OUTPUT): # growth rate of the electrostatic potential plot_growth(run.scalars[FIT_QUANTITY], window=FIT_WINDOW) + if SHOW_EQUIL_PROFILE: + plot_equilibrium(run.path_out) + for name, plane, fixed in SNAPSHOTS: plot_plane(product(run, name), plane, fixed) diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index 260345903..eccdc8a33 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -8,6 +8,7 @@ from pathlib import Path import numpy as np +import pyvista as pv from matplotlib import pyplot as plt from struphy import Output @@ -18,6 +19,8 @@ FIT_QUANTITY = "en_phi" FIT_WINDOW = (0.0, 42.0) +SHOW_EQUIL_PROFILE = True + # products shown at the last saved time in the physical XY plane SNAPSHOTS = [ ("kinetic_ions", "e1_e2_density", "f"), @@ -26,6 +29,24 @@ ] +def plot_equilibrium(path_out): + """Radial equilibrium profiles from the geometry written at the start of the run.""" + equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) + shape = equilibrium.dimensions + grid = np.reshape(equilibrium.points, shape + (3,)) + radius = np.sqrt(grid[..., 0] ** 2 + grid[..., 1] ** 2)[0, 0] + pressure = np.reshape(equilibrium.point_data["p0"], shape)[0, 0] + fig, ax = plt.subplots() + ax.plot(radius, pressure, label=r"$p_0$") + if "n0" in equilibrium.point_data: + density = np.reshape(equilibrium.point_data["n0"], shape)[0, 0] + ax.plot(radius, density, label=r"$n_0$") + ax.plot(radius, pressure / density, label=r"$T_0$") + ax.set(xlabel=r"$R$", title="Radial equilibrium profiles") + ax.legend() + return fig, ax + + def fit_growth(series, window=(None, None)): """Fit ``exp(rate * t + intercept)`` to the positive samples inside ``window``.""" time, values = series.t.values, series.values @@ -59,6 +80,10 @@ def main(paths=(DEFAULT_OUTPUT,)): if len(runs) > 1: return + if SHOW_EQUIL_PROFILE: + plot_equilibrium(run.path_out) + plt.show() + for path in SNAPSHOTS: data = run for part in path: From d0925116370fc1437fe7b7c365210b3e3a03d992 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:35:10 +0200 Subject: [PATCH 149/193] fix tutorial --- tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index add221640..6f9486891 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -320,8 +320,8 @@ "print(f\" Fitted speed: {v_fast_fit:.6f}\")\n", "print(f\" Relative error: {abs(v_fast_fit - v_fast_theory) / v_fast_theory * 100:.2f}%\")\n", "\n", - "error_slow = xp.abs(coeffs_sonic[0][0] - v_slow_theory)\n", - "error_fast = xp.abs(coeffs_sonic[1][0] - v_fast_theory)\n", + "error_slow = xp.abs(v_slow_fit - v_slow_theory)\n", + "error_fast = xp.abs(v_fast_fit - v_fast_theory)\n", "\n", "assert error_slow < 0.05, f\"Slow wave speed error {error_slow:.4f} exceeds tolerance\"\n", "assert error_fast < 0.19, f\"Fast wave speed error {error_fast:.4f} exceeds tolerance\"\n", From c08d304ad3bf1cbda55723f6056c8f9e36fc09db Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:37:16 +0200 Subject: [PATCH 150/193] Set default options in two_fluid_quasi_neutral_full.py --- src/struphy/propagators/two_fluid_quasi_neutral_full.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/struphy/propagators/two_fluid_quasi_neutral_full.py b/src/struphy/propagators/two_fluid_quasi_neutral_full.py index 5f55847c8..3a7a84227 100644 --- a/src/struphy/propagators/two_fluid_quasi_neutral_full.py +++ b/src/struphy/propagators/two_fluid_quasi_neutral_full.py @@ -163,7 +163,8 @@ def __post_init__(self): @property def options(self) -> Options: - assert hasattr(self, "_options"), "Options not set." + if not hasattr(self, "_options"): + self._options = self.Options() return self._options @options.setter From b437e24b43b5a97e267901ecb8619e23b142f2e2 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:47:09 +0200 Subject: [PATCH 151/193] Fix imports --- src/struphy/models/drift_kinetic_electrostatic_adiabatic.py | 2 +- src/struphy/simulation/tests/test_output.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 7124e5275..62a8a5d89 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -126,7 +126,7 @@ def __init__( # 2. derive units (must be done after instantiating species to access charge and mass numbers) self.setup_equation_params(base_units=base_units) if epsilon is None: - epsilon = self.params["epsilon"] = self.kinetic_ions.var.species.equation_params.epsilon + epsilon = self.kinetic_ions.var.species.equation_params.epsilon # 3. instantiate all propagators rho = ParticlesToGrid( diff --git a/src/struphy/simulation/tests/test_output.py b/src/struphy/simulation/tests/test_output.py index 8673154b5..ae72be637 100644 --- a/src/struphy/simulation/tests/test_output.py +++ b/src/struphy/simulation/tests/test_output.py @@ -20,9 +20,7 @@ maxwellians, perturbations, ) -from struphy.initial import perturbations from struphy.initial.base import Perturbation -from struphy.kinetic_background import maxwellians from struphy.linear_algebra.solver import SolverParameters from struphy.models import ColdPlasmaVlasov, LinearMHD, Maxwell, Poisson, VlasovAmpereOneSpecies from struphy.ode.utils import ButcherTableau From f2701b20630dccfd2ed13dd0313665c8b098350f Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 16:49:16 +0200 Subject: [PATCH 152/193] Build dispersion relation in the tutorials --- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 126 +++++++++++---- tutorials/tutorial_maxwell.ipynb | 151 +++++++++++++----- 2 files changed, 204 insertions(+), 73 deletions(-) diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index 6f9486891..d38a6f1eb 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -51,6 +51,7 @@ "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import LogNorm\n", "import cunumpy as xp\n", "\n", "from struphy import (\n", @@ -233,6 +234,80 @@ "**Slow and fast magnetosonic waves**: derived from dispersion relation involving both $v_A$ and $c_s$." ] }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "### Computing the Dispersion Relation\n", + "\n", + "A wave $\\propto e^{i(kz - \\omega t)}$ shows up as a peak at $(\\omega, k)$ in the Fourier transform of the field over time and space. We therefore\n", + "\n", + "1. take the field along $z$ at the first $x$ and $y$ grid point, giving data $f(t, z)$ on uniform grids,\n", + "2. compute the space-time power spectrum $|\\hat f(\\omega, k)|$ with a 2D FFT and keep the non-negative frequencies and wave numbers,\n", + "3. at each $k$ (in the band between 1/8 and 1/2 of the resolved wave numbers), locate the local maxima in $\\omega$ that rise above a fraction `noise_level` of that column's maximum; each peak belongs to one wave branch,\n", + "4. fit a line $\\omega = v\\,k + b$ to each branch. The slope $v$ is the phase velocity of that wave." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.fft import fft2, fftfreq\n", + "from scipy.signal import argrelextrema\n", + "\n", + "\n", + "def power_spectrum(field, component=0):\n", + " \"\"\"Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.\"\"\"\n", + " if \"component\" in field.dims:\n", + " field = field.isel(component=component)\n", + " data = field.isel(e1=0, e2=0).transpose(\"t\", \"e3\")\n", + " time, z = data.t.values, data.Z.values\n", + " nt, nz = data.shape\n", + " power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2]\n", + " omega = 2 * np.pi * fftfreq(nt, time[1] - time[0])[: nt // 2]\n", + " k = 2 * np.pi * fftfreq(nz, z[1] - z[0])[: nz // 2]\n", + " return omega, k, power\n", + "\n", + "\n", + "def fit_branches(omega, k, power, n_branches, noise_level, order=10):\n", + " \"\"\"Fit omega = v * k + b to each of the n_branches spectral peaks; returns [(v, b), ...] sorted by omega.\"\"\"\n", + " k_fit, omega_fit = [], [[] for _ in range(n_branches)]\n", + " for i in range(k.size // 8, k.size // 2):\n", + " column = power[:, i]\n", + " maxima = argrelextrema(column, np.greater, order=order)[0]\n", + " peaks = sorted(j for j in maxima if column[j] > noise_level * column.max())\n", + " if not peaks:\n", + " continue\n", + " assert len(peaks) == n_branches, (\n", + " f\"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. \"\n", + " \"Try another noise_level or order.\"\n", + " )\n", + " k_fit.append(k[i])\n", + " for branch, j in zip(omega_fit, peaks):\n", + " branch.append(omega[j])\n", + " return [np.polyfit(k_fit, branch, deg=1) for branch in omega_fit]\n", + "\n", + "\n", + "def plot_spectrum(omega, k, power, fits, theory, title):\n", + " \"\"\"Normalized power spectrum with the fitted branches (dotted) and the theoretical ones, omega = v * k (dashed).\"\"\"\n", + " fig, ax = plt.subplots(figsize=(7, 6))\n", + " normalized = np.maximum(power**2 / (power**2).max(), 1e-15)\n", + " levels = np.logspace(-15, 0, 31)\n", + " mappable = ax.contourf(k, omega, normalized, levels=levels, norm=LogNorm(), cmap=\"plasma\")\n", + " fig.colorbar(mappable, ax=ax, ticks=[1e-12, 1e-9, 1e-6, 1e-3, 1e0], format=\"%.0e\")\n", + " for n, (slope, intercept) in enumerate(fits):\n", + " ax.plot(k, slope * k + intercept, \"w:\", lw=2, label=f\"fit {n + 1}: v = {slope:.4f}\")\n", + " for label, speed in theory.items():\n", + " ax.plot(k, speed * k, \"--\", label=f\"{label}: v = {speed:.4f}\")\n", + " ax.set(xlabel=\"$k$\", ylabel=r\"$\\omega$\", title=title, xlim=(0, k[-1]), ylim=(0, omega[-1]))\n", + " ax.legend(loc=\"upper left\")\n", + " plt.show()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -244,35 +319,19 @@ "u_of_t = out.fields.mhd.velocity\n", "p_of_t = out.fields.mhd.pressure\n", "\n", - "# Dispersion relation parameters\n", "gamma = 5 / 3 # Adiabatic index\n", - "disp_params = {\n", - " \"B0x\": B0x,\n", - " \"B0y\": B0y,\n", - " \"B0z\": B0z,\n", - " \"p0\": p0,\n", - " \"n0\": n0,\n", - " \"gamma\": gamma,\n", - "}\n", - "\n", - "# 1. Shear Alfvén wave analysis from velocity\n", - "# physical=True uses the mapped X/Y/Z grid along the fft direction, matching\n", - "# the domain's physical z-extent\n", + "\n", + "# 1. Shear Alfvén wave analysis from the x-component of the velocity\n", "print(\"\\n=== Shear Alfvén Wave Analysis ===\")\n", - "spectrum_alfven = out.dispersion(\"mhd/velocity\",\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " physical=True,\n", - " fit_branches=1,\n", - " noise_level=0.5,\n", - " extr_order=10,\n", - " fit_degree=(1,),\n", - ")\n", + "omega, k, power = power_spectrum(u_of_t, component=0)\n", + "fits_alfven = fit_branches(omega, k, power, n_branches=1, noise_level=0.5)\n", "\n", "# Theoretical Alfvén speed\n", "vA = xp.sqrt(Bsquare / n0)\n", "v_alfven_theory = vA * B0z / xp.sqrt(Bsquare)\n", - "v_alfven_fit = float(spectrum_alfven.branch_coefficients.values[0, 0])\n", + "v_alfven_fit = float(fits_alfven[0][0])\n", + "\n", + "plot_spectrum(omega, k, power, fits_alfven, {\"shear Alfvén\": v_alfven_theory}, title=\"$u_x$ power spectrum\")\n", "\n", "print(f\"Théoretical Alfvén speed: {v_alfven_theory:.6f}\")\n", "print(f\"Fitted Alfvén speed: {v_alfven_fit:.6f}\")\n", @@ -291,15 +350,8 @@ "source": [ "# 2. Magnetosonic waves analysis from pressure\n", "print(\"=== Slow and Fast Magnetosonic Wave Analysis ===\")\n", - "spectrum_sonic = out.dispersion(\"mhd/pressure\",\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " physical=True,\n", - " fit_branches=2,\n", - " noise_level=0.4,\n", - " extr_order=10,\n", - " fit_degree=(1, 1),\n", - ")\n", + "omega, k, power = power_spectrum(p_of_t)\n", + "fits_sonic = fit_branches(omega, k, power, n_branches=2, noise_level=0.4)\n", "\n", "# Theoretical magnetosonic speeds\n", "cS = xp.sqrt(gamma * p0 / n0)\n", @@ -307,8 +359,14 @@ "v_slow_theory = xp.sqrt(0.5 * (cS**2 + vA**2) * (1.0 - xp.sqrt(1.0 - delta)))\n", "v_fast_theory = xp.sqrt(0.5 * (cS**2 + vA**2) * (1.0 + xp.sqrt(1.0 - delta)))\n", "\n", - "v_slow_fit = float(spectrum_sonic.branch_coefficients.values[0, 0])\n", - "v_fast_fit = float(spectrum_sonic.branch_coefficients.values[1, 0])\n", + "v_slow_fit = float(fits_sonic[0][0])\n", + "v_fast_fit = float(fits_sonic[1][0])\n", + "\n", + "plot_spectrum(\n", + " omega, k, power, fits_sonic,\n", + " {\"slow magnetosonic\": v_slow_theory, \"fast magnetosonic\": v_fast_theory},\n", + " title=\"$p$ power spectrum\",\n", + ")\n", "\n", "print(\"\\nSlow Magnetosonic Wave:\")\n", "print(f\" Théoretical speed: {v_slow_theory:.6f}\")\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index 8e56ffb31..ee9f1b5d9 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -100,6 +100,7 @@ "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import LogNorm\n", "import cunumpy as xp\n", "\n", "from struphy import (\n", @@ -250,10 +251,88 @@ "**Expected result**: The fitted wave speed should be approximately 1.0 (the speed of light in normalized units)." ] }, + { + "cell_type": "markdown", + "id": "15", + "metadata": { + "language": "markdown" + }, + "source": [ + "### Computing the Dispersion Relation\n", + "\n", + "A wave $\\propto e^{i(kz - \\omega t)}$ shows up as a peak at $(\\omega, k)$ in the Fourier transform of the field over time and space. We therefore\n", + "\n", + "1. take the field along $z$ at the first $x$ and $y$ grid point, giving data $f(t, z)$ on uniform grids,\n", + "2. compute the space-time power spectrum $|\\hat f(\\omega, k)|$ with a 2D FFT and keep the non-negative frequencies and wave numbers,\n", + "3. at each $k$ (in the band between 1/8 and 1/2 of the resolved wave numbers), locate the local maxima in $\\omega$ that rise above a fraction `noise_level` of that column's maximum; each peak belongs to one wave branch,\n", + "4. fit a line $\\omega = v\\,k + b$ to each branch. The slope $v$ is the phase velocity of that wave." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", + "metadata": { + "language": "python" + }, + "outputs": [], + "source": [ + "from scipy.fft import fft2, fftfreq\n", + "from scipy.signal import argrelextrema\n", + "\n", + "\n", + "def power_spectrum(field, component=0):\n", + " \"\"\"Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.\"\"\"\n", + " if \"component\" in field.dims:\n", + " field = field.isel(component=component)\n", + " data = field.isel(e1=0, e2=0).transpose(\"t\", \"e3\")\n", + " time, z = data.t.values, data.Z.values\n", + " nt, nz = data.shape\n", + " power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2]\n", + " omega = 2 * np.pi * fftfreq(nt, time[1] - time[0])[: nt // 2]\n", + " k = 2 * np.pi * fftfreq(nz, z[1] - z[0])[: nz // 2]\n", + " return omega, k, power\n", + "\n", + "\n", + "def fit_branches(omega, k, power, n_branches, noise_level, order=10):\n", + " \"\"\"Fit omega = v * k + b to each of the n_branches spectral peaks; returns [(v, b), ...] sorted by omega.\"\"\"\n", + " k_fit, omega_fit = [], [[] for _ in range(n_branches)]\n", + " for i in range(k.size // 8, k.size // 2):\n", + " column = power[:, i]\n", + " maxima = argrelextrema(column, np.greater, order=order)[0]\n", + " peaks = sorted(j for j in maxima if column[j] > noise_level * column.max())\n", + " if not peaks:\n", + " continue\n", + " assert len(peaks) == n_branches, (\n", + " f\"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. \"\n", + " \"Try another noise_level or order.\"\n", + " )\n", + " k_fit.append(k[i])\n", + " for branch, j in zip(omega_fit, peaks):\n", + " branch.append(omega[j])\n", + " return [np.polyfit(k_fit, branch, deg=1) for branch in omega_fit]\n", + "\n", + "\n", + "def plot_spectrum(omega, k, power, fits, theory, title):\n", + " \"\"\"Normalized power spectrum with the fitted branches (dotted) and the theoretical ones, omega = v * k (dashed).\"\"\"\n", + " fig, ax = plt.subplots(figsize=(7, 6))\n", + " normalized = np.maximum(power**2 / (power**2).max(), 1e-15)\n", + " levels = np.logspace(-15, 0, 31)\n", + " mappable = ax.contourf(k, omega, normalized, levels=levels, norm=LogNorm(), cmap=\"plasma\")\n", + " fig.colorbar(mappable, ax=ax, ticks=[1e-12, 1e-9, 1e-6, 1e-3, 1e0], format=\"%.0e\")\n", + " for n, (slope, intercept) in enumerate(fits):\n", + " ax.plot(k, slope * k + intercept, \"w:\", lw=2, label=f\"fit {n + 1}: v = {slope:.4f}\")\n", + " for label, speed in theory.items():\n", + " ax.plot(k, speed * k, \"--\", label=f\"{label}: v = {speed:.4f}\")\n", + " ax.set(xlabel=\"$k$\", ylabel=r\"$\\omega$\", title=title, xlim=(0, k[-1]), ylim=(0, omega[-1]))\n", + " ax.legend(loc=\"upper left\")\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -261,22 +340,16 @@ "# Extract electric field time-series\n", "E_of_t = out.fields.em_fields.e_field\n", "\n", - "# Compute power spectrum and fit dispersion relation (physical=True uses the\n", - "# mapped X/Y/Z grid along the fft direction, matching the domain's physical z-extent)\n", + "# Compute the power spectrum of E_x and fit the light-wave branch\n", "print(\"\\n=== Light Wave Dispersion Analysis ===\")\n", - "spectrum = out.dispersion(\"em_fields/e_field\",\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " physical=True,\n", - " fit_branches=1,\n", - " noise_level=0.5,\n", - " extr_order=10,\n", - " fit_degree=(1,),\n", - ")\n", + "omega, k, power = power_spectrum(E_of_t, component=0)\n", + "fits = fit_branches(omega, k, power, n_branches=1, noise_level=0.5)\n", "\n", "# Extract fitted wave speed\n", "c_light_speed = 1.0 # Theoretical\n", - "c_fit = float(spectrum.branch_coefficients.values[0, 0])\n", + "c_fit = float(fits[0][0])\n", + "\n", + "plot_spectrum(omega, k, power, fits, {\"light wave\": c_light_speed}, title=\"$E_x$ power spectrum\")\n", "\n", "print(f\"\\nTheoretical wave speed (c): {c_light_speed:.6f}\")\n", "print(f\"Fitted wave speed: {c_fit:.6f}\")\n", @@ -292,7 +365,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ "### Conclusion\n", @@ -314,7 +387,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -329,7 +402,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "20", "metadata": {}, "source": [ "## Verification of Electromagnetic Modes in a Coaxial Waveguide\n", @@ -359,7 +432,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -387,7 +460,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "22", "metadata": {}, "source": [ "### Coaxial Waveguide Geometry\n", @@ -398,7 +471,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -424,7 +497,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "24", "metadata": {}, "source": [ "### Model and Propagator Configuration\n", @@ -435,7 +508,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -451,7 +524,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "26", "metadata": {}, "source": [ "### Domain and Discretization\n", @@ -462,7 +535,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -489,7 +562,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "28", "metadata": {}, "source": [ "### Initial Conditions: Coaxial Mode Initialization\n", @@ -500,7 +573,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -522,7 +595,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "30", "metadata": {}, "source": [ "### Simulation Setup and Execution\n", @@ -533,7 +606,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -567,7 +640,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "32", "metadata": {}, "source": [ "### Diagnostics: Mode Field Verification\n", @@ -578,7 +651,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -599,7 +672,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "34", "metadata": {}, "source": [ "### Analytical Bessel Function Solutions\n", @@ -610,7 +683,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -653,7 +726,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "36", "metadata": {}, "source": [ "### Field Component Comparison\n", @@ -664,7 +737,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -711,7 +784,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "38", "metadata": {}, "source": [ "### 2D Slice Visualization of the Final Fields\n", @@ -722,7 +795,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -779,7 +852,7 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "40", "metadata": {}, "source": [ "### Verification: Error Tolerance Check\n", @@ -790,7 +863,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -834,7 +907,7 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "42", "metadata": {}, "source": [ "### Conclusion\n", @@ -852,7 +925,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "43", "metadata": {}, "outputs": [], "source": [ From 69c5ff71ac758f9635451ed4302bab21e916a2b7 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 17:37:25 +0200 Subject: [PATCH 153/193] Fix to_dict() --- src/struphy/models/base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index ceee69cb9..a14baa4b2 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -1045,9 +1045,11 @@ def restore_option(value, template): setattr(template, field.name, restore_option(value[field.name], getattr(template, field.name))) return template if isinstance(template, dict) and isinstance(value, dict): - return { - restore_option(key, key): restore_option(item, template.get(key)) for key, item in value.items() - } + restored = {} + for key, item in value.items(): + restored_key = restore_option(key, key) + restored[restored_key] = restore_option(item, template.get(restored_key)) + return restored if isinstance(value, list): template_item = template[0] if isinstance(template, (list, tuple)) and template else None return [restore_option(item, template_item) for item in value] From bd1e91632eff3ce009684fcfaaef38c06b2e0a24 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 17:37:42 +0200 Subject: [PATCH 154/193] Better explanation on why two sims are not equal --- src/struphy/models/tests/utils_testing.py | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 6c44293ee..9813c36fb 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -16,6 +16,26 @@ logger = logging.getLogger("struphy") +def _describe_dict_diff(a, b, path: str = "") -> list[str]: + """Recursively compare two (nested) dicts/lists and describe every leaf that differs. + + Used to turn a bare ``sim != sim2`` assertion into a readable list of the + exact fields that diverged, which is otherwise very hard to track down. + """ + lines = [] + if isinstance(a, dict) and isinstance(b, dict): + for key in sorted(set(a) | set(b), key=str): + lines += _describe_dict_diff(a.get(key, ""), b.get(key, ""), f"{path}.{key}") + elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + if len(a) != len(b): + lines.append(f" length mismatch at {path}: {len(a)} vs {len(b)}") + for i, (x, y) in enumerate(zip(a, b)): + lines += _describe_dict_diff(x, y, f"{path}[{i}]") + elif a != b: + lines.append(f" {path}: {a!r} != {b!r}") + return lines + + # generic function for calling model tests def call_test(model: StruphyModel, test_profiling: bool = False): model_name = model.name() @@ -79,7 +99,11 @@ def call_test(model: StruphyModel, test_profiling: bool = False): sim_dict = sim.to_dict() # test the to_dict method sim2 = Simulation.from_dict(sim_dict) # test the from_dict method - assert sim == sim2, "Simulation to_dict and from_dict methods are not consistent" + if sim != sim2: + raise AssertionError( + f"Simulation to_dict and from_dict methods are not consistent for {model_name}:\n" + + "\n".join(_describe_dict_diff(sim_dict, sim2.to_dict())) + ) # test the generate_script method sim1_script = sim.generate_script() From c2ad7df3d0037b1de78a52476972ea18fa79f549 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 17:58:14 +0200 Subject: [PATCH 155/193] Fix test_output.py --- src/struphy/post_processing/tests/test_output.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index a2532e7bf..2f566e42e 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -255,7 +255,8 @@ def forbidden(*args, **kwargs): def test_evaluate_triggers_default_processing_when_missing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = Output(root) + # Automatic processing is serial only; the multi-rank refusal is tested separately. + run = output_with_comm(monkeypatch, root, FakeComm()) calls = [] def fake_pproc(self, **options): From 57e7d60f49ef9f107412f5e93985fb913282d2e3 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 18:48:32 +0200 Subject: [PATCH 156/193] Update postprocessing tutorial --- tutorials/tutorial_post_processing.ipynb | 31 +++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index 1829c0bec..a857e47a0 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -306,12 +306,23 @@ ")\n", "phi_plane.plot(x=\"e1\", y=\"e2\")\n", "\n", - "# 3-D: no eta arguments uses the complete simulation grid at cell centres.\n", - "phi_volume = out.evaluate(\"em_fields/phi\", t=-1)\n", + "# 3-D: vary all three coordinates; keep volume grids modest.\n", + "phi_volume = out.evaluate(\n", + " \"em_fields/phi\",\n", + " eta1=np.linspace(0.0, 1.0, 32),\n", + " eta2=np.linspace(0.0, 1.0, 16),\n", + " eta3=np.linspace(0.0, 1.0, 8),\n", + " t=-1,\n", + ")\n", "print(phi_volume.dims, phi_volume.shape)\n", "\n", "# xarray plots a 2-D slice of the volume; choose the mid-plane by coordinate index.\n", - "phi_volume.isel(e3=phi_volume.sizes[\"e3\"] // 2).plot(x=\"e1\", y=\"e2\")\n" + "phi_volume.isel(e3=phi_volume.sizes[\"e3\"] // 2).plot(x=\"e1\", y=\"e2\")\n", + "\n", + "# Without eta arguments the spline is evaluated at the cell centres of the simulation grid,\n", + "# here 16 x 1 x 1 cells, so this run's grid gives a line along e1.\n", + "phi_grid = out.evaluate(\"em_fields/phi\", t=-1)\n", + "print(phi_grid.dims, phi_grid.shape)" ] }, { @@ -620,7 +631,9 @@ "metadata": {}, "outputs": [], "source": [ - "f_of_v = phase_space.mean((\"e1\", \"e2\", \"e3\"), missing_dims=\"ignore\")\n", + "# average over whichever logical space directions the binning kept\n", + "spatial = [dim for dim in (\"e1\", \"e2\", \"e3\") if dim in phase_space.dims]\n", + "f_of_v = phase_space.mean(spatial)\n", "print(f_of_v.dims)\n", "f_of_v.plot(x=\"t\", y=\"v1\")" ] @@ -644,9 +657,9 @@ "density = (phase_space * dv1).sum(\"v1\")\n", "mean_v1 = (phase_space * phase_space.v1 * dv1).sum(\"v1\") / density\n", "variance_v1 = (phase_space * (phase_space.v1 - mean_v1) ** 2 * dv1).sum(\"v1\") / density\n", - "mean_density = density.mean((\"e1\", \"e2\", \"e3\"), missing_dims=\"ignore\")\n", + "mean_density = density.mean(spatial)\n", "mean_density.plot.line(x=\"t\")\n", - "variance_v1.mean((\"e1\", \"e2\", \"e3\"), missing_dims=\"ignore\").plot.line(x=\"t\")" + "variance_v1.mean(spatial).plot.line(x=\"t\")" ] }, { @@ -680,7 +693,7 @@ "source": [ "## Save standard output\n", "\n", - "Every `PlotResult` supports `.save(path)`. For a complete scalar report, `out.save_report()` writes a CSV table, an overview, and one PNG per scalar beneath `post_processing/report/`." + "Products are xarray objects, so figures are saved with Matplotlib's `plt.savefig(path)` and arrays with xarray's own writers, for example `phi.to_netcdf(path)`. For a compact record of the run, `out.report()` writes the full scalar history as `scalars.csv` and a report listing the run metadata, every available product, and the dimensions and units of any products passed with `products=`. By default the files go to `post_processing/report/`; `format` is `\"markdown\"` or `\"html\"`, and the return value is the path of the report file." ] }, { @@ -690,9 +703,9 @@ "metadata": {}, "outputs": [], "source": [ - "written = out.save_report()\n", + "report = out.report(products=[\"em_fields/phi\"], format=\"html\")\n", "print(\"Wrote:\")\n", - "for path in written:\n", + "for path in (report, os.path.join(os.path.dirname(report), \"scalars.csv\")):\n", " print(\" \", os.path.relpath(path, out.path_out))" ] }, From 4a0be827b25e2814da8970b66c3b5d8eb7c6b3c3 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 18:57:20 +0200 Subject: [PATCH 157/193] Fix verification tests --- .../verification/test_verif_LinearMHD.py | 65 ++++++++++++------- .../tests/verification/test_verif_Maxwell.py | 49 ++++++++++---- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 3dc0cb48c..429503a11 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -3,8 +3,11 @@ import shutil import cunumpy as xp +import numpy as np import pytest from feectools.ddm.mpi import mpi as MPI +from scipy.fft import fft2, fftfreq +from scipy.signal import argrelextrema from struphy import ( BaseUnits, @@ -24,6 +27,38 @@ logger = logging.getLogger("struphy") +def _power_spectrum(field, component=0): + """Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.""" + if "component" in field.dims: + field = field.isel(component=component) + data = field.isel(e1=0, e2=0).transpose("t", "e3") + time, z = data.t.values, data.Z.values + nt, nz = data.shape + power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2] + omega = 2 * np.pi * fftfreq(nt, time[1] - time[0])[: nt // 2] + k = 2 * np.pi * fftfreq(nz, z[1] - z[0])[: nz // 2] + return omega, k, power + + +def _fit_branches(omega, k, power, n_branches, noise_level, order=10): + """Fit omega = v * k + b to each of the n_branches spectral peaks; returns [(v, b), ...].""" + k_fit, omega_fit = [], [[] for _ in range(n_branches)] + for i in range(k.size // 8, k.size // 2): + column = power[:, i] + maxima = argrelextrema(column, np.greater, order=order)[0] + peaks = sorted(j for j in maxima if column[j] > noise_level * column.max()) + if not peaks: + continue + assert len(peaks) == n_branches, ( + f"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. " + "Try another noise_level or order." + ) + k_fit.append(k[i]) + for branch, j in zip(omega_fit, peaks): + branch.append(omega[j]) + return [np.polyfit(k_fit, branch, deg=1) for branch in omega_fit] + + @pytest.mark.parametrize("algo", ["implicit", "explicit"]) def test_slab_waves_1d(algo: str, do_plot: bool = False): # light-weight model instance @@ -85,34 +120,18 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 - spectrum = run.dispersion( - "mhd/velocity", - physical=True, - component=0, - slice_at=[0, 0, None], - fit_branches=1, - noise_level=0.5, - extr_order=10, - fit_degree=(1,), - ) + omega, k, power = _power_spectrum(run.fields.mhd.velocity, component=0) + fits = _fit_branches(omega, k, power, n_branches=1, noise_level=0.5) # assert vA = xp.sqrt(Bsquare / n0) v_alfven = vA * B0z / xp.sqrt(Bsquare) logger.info(f"{v_alfven =}") - assert xp.abs(spectrum.branch_coefficients.values[0, 0] - v_alfven) < 0.07 + assert xp.abs(fits[0][0] - v_alfven) < 0.07 # second fft - spectrum = run.dispersion( - "mhd/pressure", - physical=True, - component=0, - slice_at=[0, 0, None], - fit_branches=2, - noise_level=0.4, - extr_order=10, - fit_degree=(1, 1), - ) + omega, k, power = _power_spectrum(run.fields.mhd.pressure) + fits = _fit_branches(omega, k, power, n_branches=2, noise_level=0.4) # assert gamma = 5 / 3 @@ -123,8 +142,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): v_fast = xp.sqrt(1 / 2 * (cS**2 + vA**2) * (1 + xp.sqrt(1 - delta))) logger.info(f"{v_slow =}") logger.info(f"{v_fast =}") - assert xp.abs(spectrum.branch_coefficients.values[0, 0] - v_slow) < 0.05 - assert xp.abs(spectrum.branch_coefficients.values[1, 0] - v_fast) < 0.19 + assert xp.abs(fits[0][0] - v_slow) < 0.05 + assert xp.abs(fits[1][0] - v_fast) < 0.19 shutil.rmtree(test_folder) diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 6d9068828..a1e617f3e 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -3,9 +3,12 @@ import shutil import cunumpy as xp +import numpy as np import pytest from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt +from scipy.fft import fft2, fftfreq +from scipy.signal import argrelextrema from scipy.special import jv, yn from struphy import ( @@ -24,6 +27,38 @@ logger = logging.getLogger("struphy") +def _power_spectrum(field, component=0): + """Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.""" + if "component" in field.dims: + field = field.isel(component=component) + data = field.isel(e1=0, e2=0).transpose("t", "e3") + time, z = data.t.values, data.Z.values + nt, nz = data.shape + power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2] + omega = 2 * np.pi * fftfreq(nt, time[1] - time[0])[: nt // 2] + k = 2 * np.pi * fftfreq(nz, z[1] - z[0])[: nz // 2] + return omega, k, power + + +def _fit_branches(omega, k, power, n_branches, noise_level, order=10): + """Fit omega = v * k + b to each of the n_branches spectral peaks; returns [(v, b), ...].""" + k_fit, omega_fit = [], [[] for _ in range(n_branches)] + for i in range(k.size // 8, k.size // 2): + column = power[:, i] + maxima = argrelextrema(column, np.greater, order=order)[0] + peaks = sorted(j for j in maxima if column[j] > noise_level * column.max()) + if not peaks: + continue + assert len(peaks) == n_branches, ( + f"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. " + "Try another noise_level or order." + ) + k_fit.append(k[i]) + for branch, j in zip(omega_fit, peaks): + branch.append(omega[j]) + return [np.polyfit(k_fit, branch, deg=1) for branch in omega_fit] + + @pytest.mark.parametrize("algo", ["implicit", "explicit"]) def test_light_wave_1d(algo: str, do_plot: bool = False): # light-weight model instance @@ -72,20 +107,12 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: # fft - spectrum = run.dispersion( - "em_fields/e_field", - physical=True, - component=0, - slice_at=[0, 0, None], - fit_branches=1, - noise_level=0.5, - extr_order=10, - fit_degree=(1,), - ) + omega, k, power = _power_spectrum(run.fields.em_fields.e_field, component=0) + fits = _fit_branches(omega, k, power, n_branches=1, noise_level=0.5) # assert c_light_speed = 1.0 - assert xp.abs(spectrum.branch_coefficients.values[0, 0] - c_light_speed) < 0.02 + assert xp.abs(fits[0][0] - c_light_speed) < 0.02 shutil.rmtree(test_folder) From e40a4d1b75362f087c0479e5bed2532f6bf57c6e Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 19:02:28 +0200 Subject: [PATCH 158/193] Update tutorials --- doc/sections/tutorials.rst | 3 ++- tutorials/tutorial_domains.ipynb | 10 +++++++--- tutorials/tutorial_mhd_equilibria.ipynb | 9 ++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/doc/sections/tutorials.rst b/doc/sections/tutorials.rst index 0f2db1cd2..09a21e2b2 100644 --- a/doc/sections/tutorials.rst +++ b/doc/sections/tutorials.rst @@ -40,9 +40,10 @@ It is recommended to use the same Python environment as for Struphy, e.g., by in ../_collections/tutorials/tutorial_beltrami_sph ../_collections/tutorials/tutorial_gas_expansion_sph ../_collections/tutorials/tutorial_viscous_euler_sph - ../_collections/tutorials/tutorial_velocity_diffsusion_sph + ../_collections/tutorials/tutorial_velocity_diffusion_sph ../_collections/tutorials/tutorial_hagen_poiseuille_sph ../_collections/tutorials/tutorial_dam_break_sph + ../_collections/tutorials/tutorial_pressureless_sph_shock .. toctree:: diff --git a/tutorials/tutorial_domains.ipynb b/tutorials/tutorial_domains.ipynb index e912edc03..f8c7c4ab0 100644 --- a/tutorials/tutorial_domains.ipynb +++ b/tutorials/tutorial_domains.ipynb @@ -446,9 +446,13 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture\n", - "desc_equil = equils.DESCequilibrium(use_nfp=False)\n", - "domain = domains.DESCunit(desc_equil)" + "import contextlib\n", + "import io\n", + "\n", + "# DESC is verbose while loading: silence its output, but let errors through.\n", + "with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):\n", + " desc_equil = equils.DESCequilibrium(use_nfp=False)\n", + " domain = domains.DESCunit(desc_equil)" ] }, { diff --git a/tutorials/tutorial_mhd_equilibria.ipynb b/tutorials/tutorial_mhd_equilibria.ipynb index 96e195b3a..eaeace079 100644 --- a/tutorials/tutorial_mhd_equilibria.ipynb +++ b/tutorials/tutorial_mhd_equilibria.ipynb @@ -138,7 +138,7 @@ "\n", "**DESC** (Design of Equilibria with Stellarator Constraints) is a modern stellarator equilibrium optimizer. Like GVEC, it is a `LogicalMHDequilibrium` with built-in geometry.\n", "\n", - "**About the `%%capture` cell above:** DESC initialization can produce verbose output. The `%%capture` magic silences it so the notebook remains clean. The `mhd_equil` object is still created and available for use." + "**About the cell below:** DESC initialization can produce verbose output. Redirecting stdout and stderr silences it so the notebook remains clean, while any error is still raised. The `mhd_equil` object is created and available for use." ] }, { @@ -147,8 +147,11 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture\n", - "mhd_equil = equils.DESCequilibrium(use_nfp=False)" + "import contextlib\n", + "import io\n", + "\n", + "with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):\n", + " mhd_equil = equils.DESCequilibrium(use_nfp=False)" ] }, { From 0adc539d1f0a917abaa11e67e27d311574d664f1 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 19:18:29 +0200 Subject: [PATCH 159/193] formatting --- src/struphy/models/tests/verification/test_verif_LinearMHD.py | 3 +-- src/struphy/models/tests/verification/test_verif_Maxwell.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 429503a11..e42c1c345 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -50,8 +50,7 @@ def _fit_branches(omega, k, power, n_branches, noise_level, order=10): if not peaks: continue assert len(peaks) == n_branches, ( - f"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. " - "Try another noise_level or order." + f"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. Try another noise_level or order." ) k_fit.append(k[i]) for branch, j in zip(omega_fit, peaks): diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index a1e617f3e..cbad33248 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -50,8 +50,7 @@ def _fit_branches(omega, k, power, n_branches, noise_level, order=10): if not peaks: continue assert len(peaks) == n_branches, ( - f"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. " - "Try another noise_level or order." + f"Found {len(peaks)} branches at k={k[i]:.3f}, expected {n_branches}. Try another noise_level or order." ) k_fit.append(k[i]) for branch, j in zip(omega_fit, peaks): From 3a520c6f9fd67854707d9b6eda0d0601e3020b4c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 19:48:23 +0200 Subject: [PATCH 160/193] Fix test_verif_ViscousEulerSPH.py --- .../test_verif_ViscousEulerSPH.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index 3451f945b..113842e6c 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -741,10 +741,11 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ from matplotlib.colors import LinearSegmentedColormap from tqdm import tqdm as _tqdm - orbits = np.asarray(run.orbits.euler_fluid) # (Nt_orb, n_markers, n_attrs) - # attrs for vdim=2: [x, y, z, v1, v2, w, diag, id] + orbits = run.orbits.euler_fluid + x_orb = orbits.x.values # (Nt_orb, n_markers) + y_orb = orbits.y.values - Nt_orb = orbits.shape[0] + Nt_orb = orbits.sizes["t"] t_orbit = np.linspace(0.0, time_opts.Tend, Nt_orb) # colormap: blue at walls (y=0, y=H), red at channel centre (y=H/2) @@ -760,11 +761,11 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ os.makedirs(png_dir, exist_ok=True) for i, idx in _tqdm(enumerate(snap_inds), total=n_snaps, desc="saving PNGs"): - c_val = 1.0 - 2.0 * np.abs(orbits[idx, :, 1] / H - 0.5) + c_val = 1.0 - 2.0 * np.abs(y_orb[idx] / H - 0.5) fig_png, ax_png = plt.subplots(figsize=(8, 6)) sc_png = ax_png.scatter( - orbits[idx, :, 0], - orbits[idx, :, 1], + x_orb[idx], + y_orb[idx], c=c_val, cmap=cmap_pos, norm=norm, @@ -785,10 +786,10 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ # show last snapshot in a new figure fig_last, ax_last = plt.subplots(figsize=(8, 6)) idx_last = snap_inds[-1] - c_val_last = 1.0 - 2.0 * np.abs(orbits[idx_last, :, 1] / H - 0.5) + c_val_last = 1.0 - 2.0 * np.abs(y_orb[idx_last] / H - 0.5) sc_last = ax_last.scatter( - orbits[idx_last, :, 0], - orbits[idx_last, :, 1], + x_orb[idx_last], + y_orb[idx_last], c=c_val_last, cmap=cmap_pos, norm=norm, @@ -937,12 +938,14 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo n_arr = np.asarray(n_sph) # (Nt+1, pts_e1, pts_e2, 1) # orbits needed for both do_plot scatter overlay and create_png - orbits = np.asarray(run.orbits.euler_fluid) # (Nt_orb, n_markers, n_attrs) - Nt_orb = orbits.shape[0] + orbits = run.orbits.euler_fluid + x_orb = orbits.x.values # (Nt_orb, n_markers) + y_orb = orbits.y.values + Nt_orb = orbits.sizes["t"] t_orbit = np.linspace(0.0, time_opts.Tend, Nt_orb) # color each marker by its initial x (gradient across the left column) - x_init = orbits[0, :, 0] + x_init = x_orb[0] c_val = x_init / (r1 / 2) # 0 = left wall, 1 = dam face if do_plot: @@ -958,8 +961,8 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo n_2d = n_arr[idx, :, :, 0] im = ax.pcolormesh(X, Y, n_2d, vmin=0.0, vmax=vmax_plot / 2, cmap="Blues", shading="auto") ax.scatter( - orbits[oidx, :, 0], - orbits[oidx, :, 1], + x_orb[oidx], + y_orb[oidx], c=c_val, cmap="autumn", s=2, @@ -999,8 +1002,8 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo n_2d = n_arr[n_idx, :, :, 0] im = ax_png.pcolormesh(X, Y, n_2d, vmin=0.0, vmax=vmax_plot / 2, cmap="Blues", shading="auto") ax_png.scatter( - orbits[idx, :, 0], - orbits[idx, :, 1], + x_orb[idx], + y_orb[idx], c=c_val, cmap="autumn", s=1, @@ -1021,8 +1024,8 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo plt.close(fig_png) # sanity: no markers should escape the closed box (allow 1% tolerance) - x_all = orbits[:, :, 0] - y_all = orbits[:, :, 1] + x_all = x_orb + y_all = y_orb assert np.all(x_all >= -0.01 * r1) and np.all(x_all <= 1.01 * r1), "Markers escaped x-domain in dam break test" assert np.all(y_all >= -0.01 * r2) and np.all(y_all <= 1.01 * r2), "Markers escaped y-domain in dam break test" logger.info("Dam break domain bounds assertion passed.") From 50fedde29e8d9e13ad020fba5d63e0c6a086fe27 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 20:25:18 +0200 Subject: [PATCH 161/193] update docs --- doc/markdown/output-api.md | 74 +++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 84b0d2097..08f334909 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -97,29 +97,36 @@ distribution = out.evaluate("kinetic_ions/f") delta_f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") ``` -For a quick inspection, `out.plot(array)` chooses the last time, first vector component, and -midpoint slices as needed. For controlled figures, select dimensions explicitly and call xarray's -native `.plot()` methods. +To make a figure, select the dimensions to show and call xarray's native `.plot()` methods (see +[Plot data](#plot-data)). ## Analyze and report data -Numerical helpers stay on `Output` and return values or xarray arrays rather than figures. +`Output` has no fitting or error helpers: products are xarray objects, so such analysis is a few +lines of xarray and NumPy. Relative energy error, and an exponential growth or damping rate +fitted over a time window: ```python -fit = out.growth_rate("phi_integral", window=(20.0, 60.0), amplitude=True) -energy_error = out.relative_error("en_tot") -energy_drift = out.drift("en_tot") +energy = out.evaluate("scalars", variables="en_tot").en_tot +relative_error = (energy - energy.isel(t=0)) / energy.isel(t=0) + +window = out.evaluate("scalars", variables="phi_integral").phi_integral.sel(t=slice(20.0, 60.0)) +rate, log_amplitude = np.polyfit(window.t, np.log(np.abs(window)), 1) ``` -For oscillating signals such as the field energy in Landau damping, `damping_rate` fits the -exponential to the local maxima (`envelope`) instead of the raw series. `norm` reduces a field -to a time series (by default over every dimension except `t`), which can then be fitted. +For an oscillating signal such as the field energy in Landau damping, fit the local maxima rather +than the raw series. A field reduces to a time series with an xarray reduction over every +dimension except `t`. ```python -damping = out.damping_rate("electric_energy", window=(0.0, 8.0), amplitude=True) -peaks = out.envelope("electric_energy") +from scipy.signal import find_peaks + +electric_energy = out.evaluate("scalars", variables="electric_energy").electric_energy +peaks, _ = find_peaks(electric_energy.values) +damping_rate, _ = np.polyfit(electric_energy.t[peaks], np.log(electric_energy[peaks]), 1) -growth = out.growth_rate(out.norm("diagnostics/rho", squared=True), amplitude=True) +rho = out.evaluate("diagnostics/rho") +rho_squared = (rho**2).mean([dim for dim in rho.dims if dim != "t"]) ``` Fields carry mapped `X`, `Y`, `Z` coordinates; binned products (such as `e1_e2_density`) do not. @@ -143,29 +150,26 @@ store is useful. Prefer `evaluate(key)` for normal single-product work. ## Reduce a distribution function -A binned distribution usually has more dimensions than a question needs. `spatial_average` -averages over the logical space dimensions `e1`, `e2` and `e3` (or the ones passed as `dims`), -so an `e1_v1` product becomes f(v1, t). The mean is uniform in the logical coordinates, which is -the volume average on a Cartesian domain; on a mapped domain it is not weighted by the Jacobian. +A binned distribution usually has more dimensions than a question needs. Averaging over the +logical space dimensions it has turns an `e1_v1` product into f(v1, t). A binned product keeps +only the dimensions of its slice, so select them from `data.dims`. The mean is uniform in the +logical coordinates, which is the volume average on a Cartesian domain; on a mapped domain it is +not weighted by the Jacobian. -The velocity moments are readily computed with xarray reductions over the velocity dimensions; the result is a dataset with the -`density`, and the mean `mean_v1` and variance `variance_v1` along every velocity direction, as -functions of the remaining dimensions. In normalized units the variance is the temperature divided -by the mass. Mean and variance are NaN where the density is not positive, and a `delta_f` product -has only the density (its perturbation). +Velocity moments are xarray reductions over a velocity dimension: the density, the mean velocity +and the variance, as functions of the remaining dimensions. In normalized units the variance is +the temperature divided by the mass. Mean and variance are NaN where the density is not positive; +for a `delta_f` product only the density (its perturbation) is meaningful. ```python -f = "kinetic_ions/f" +data = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") +space = [dim for dim in ("e1", "e2", "e3") if dim in data.dims] +f_of_v = data.mean(space) -data = out.evaluate(f, dataset="e1_v1_density/f") -f_of_v = data.mean(("e1", "e2", "e3"), missing_dims="ignore") -velocity_grid = data.v1 -bin_width = velocity_grid.differentiate("v1") +bin_width = data.v1.differentiate("v1") density = (data * bin_width).sum("v1") -mean_v1 = (data * velocity_grid * bin_width).sum("v1") / density -temperature_over_mass = ((data * (velocity_grid - mean_v1) ** 2 * bin_width).sum("v1") / density).mean( - ("e1", "e2", "e3"), missing_dims="ignore" -) +mean_v1 = (data * data.v1 * bin_width).sum("v1") / density +temperature_over_mass = ((data * (data.v1 - mean_v1) ** 2 * bin_width).sum("v1") / density).mean(space) ``` @@ -230,12 +234,8 @@ rho = out.evaluate("diagnostics/rho_xyz", t=-1) rho.isel(e3=rho.sizes["e3"] // 2).plot(x="e1", y="e2") ``` -For an intentionally simple inspection plot, pass an xarray object to `Output.plot()`. -It chooses the last time, first vector component, and midpoint slices until xarray can plot it. - -```python -out.plot(out.evaluate("em_fields/E")) -``` +xarray squeezes size-one dimensions before plotting, so an array that is 2-D on a grid with one +cell in some direction plots as a line. Select until the array has the dimensions the plot needs. ## MPI post-processing From c5cddb58a534658e09f032ed55ac0c2ecb01089f Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 20:25:44 +0200 Subject: [PATCH 162/193] Update tests --- src/struphy/post_processing/tests/test_output.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 2f566e42e..1b9f83e6c 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -665,3 +665,17 @@ def test_saved_rank_count_does_not_block_serial_implicit_processing(tmp_path, mo monkeypatch.setattr(Output, "pproc", lambda self: calls.append(self.path_out)) run._ensure_processed() assert calls == [run.path_out] + + +def test_command_line_lists_keys_and_writes_a_report(tmp_path, monkeypatch, capsys): + import sys + + from struphy.console.main import struphy + + root = write_tree(str(tmp_path / "run")) + for argv in (["struphy", "output", "keys", root], ["struphy", "output", "report", root, "--format", "html"]): + monkeypatch.setattr(sys, "argv", argv) + struphy() + printed = capsys.readouterr().out.splitlines() + assert printed[: len(Output(root).keys())] == list(Output(root).keys()) + assert Path(printed[-1]).name == "report.html" and Path(printed[-1]).exists() From e80ed1938fb02b8e25c6bea1a358d1209d533f59 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 20:26:04 +0200 Subject: [PATCH 163/193] datacontainer bugfixes --- src/struphy/io/output_handling.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/struphy/io/output_handling.py b/src/struphy/io/output_handling.py index 3b03fc4bd..d0b535cbf 100644 --- a/src/struphy/io/output_handling.py +++ b/src/struphy/io/output_handling.py @@ -1,4 +1,3 @@ -import ctypes import logging import os @@ -46,10 +45,10 @@ def __init__(self, path_out, file_name=None, comm=None): # check if file already exists file_exists = os.path.exists(self.file_path) - # dictionary with pairs (dataset key : object ID) + # dictionary with pairs (dataset key : object to save) self._dset_dict = {} - # get dataset keys if file already exists and set None object IDs; time series are + # get dataset keys if file already exists and set None objects; time series are # chunked (see add_data), static datasets such as kinetic backgrounds are not if file_exists: dataset_keys = [] @@ -78,7 +77,7 @@ def file_path(self): @property def dset_dict(self): - """Dictionary with dataset keys and object IDs.""" + """Dictionary with dataset keys and the objects saved to them.""" return self._dset_dict @staticmethod @@ -139,8 +138,8 @@ def add_data(self, data_dict): ) file[key][0] = val_np - # set object ID - self._dset_dict[key] = id(val) + # keep a reference, so the object is alive and current when it is saved + self._dset_dict[key] = val def save_data(self, keys=None): """ @@ -151,18 +150,15 @@ def save_data(self, keys=None): keys : list Keys to the data objects specified when using "add_data". Default saves all specified data objects. """ + if keys is None: + keys = self._dset_dict with h5py.File(self.file_path, "a") as file: - # loop over all keys - if keys is None: - for key in self._dset_dict: - file[key].resize(file[key].shape[0] + 1, axis=0) - file[key][-1] = self._as_numpy_array(ctypes.cast(self._dset_dict[key], ctypes.py_object).value) - - # only loop over given keys - else: - for key in keys: - file[key].resize(file[key].shape[0] + 1, axis=0) - file[key][-1] = self._as_numpy_array(ctypes.cast(self._dset_dict[key], ctypes.py_object).value) + for key in keys: + val = self._dset_dict[key] + if val is None: + raise KeyError(f"Dataset {key!r} exists in {self.file_path} but no data was added for it") + file[key].resize(file[key].shape[0] + 1, axis=0) + file[key][-1] = self._as_numpy_array(val) def info(self): """Print info of data sets to screen.""" From be1bd3cc1535767e851c96a62c4513bb8b92a715 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 20:26:23 +0200 Subject: [PATCH 164/193] Update struphy output cli --- src/struphy/console/main.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/struphy/console/main.py b/src/struphy/console/main.py index e98fa2716..a6fc4c8d9 100644 --- a/src/struphy/console/main.py +++ b/src/struphy/console/main.py @@ -431,19 +431,13 @@ def add_parser_likwid_profile(subparsers): def add_parser_output(subparsers): """Add the lightweight command-line interface for completed simulation output.""" - parser = subparsers.add_parser("output", help="inspect, process, report, or plot a simulation output") - parser.add_argument("action", choices=("info", "keys", "pproc", "report", "plot")) + parser = subparsers.add_parser("output", help="inspect, process, or report a simulation output") + parser.add_argument("action", choices=("info", "keys", "pproc", "report")) parser.add_argument("path", help="simulation output directory") parser.add_argument("--physical", action="store_true", help="materialize physical field components") parser.add_argument("--parallel", action="store_true", help="use MPI.COMM_WORLD for parallel pproc") parser.add_argument("--format", choices=("markdown", "html"), default="markdown", help="report format") parser.add_argument("--directory", help="report directory") - parser.add_argument( - "--kind", choices=("timeseries", "slice", "panels", "viewer", "trajectories"), default="timeseries" - ) - parser.add_argument("--product", help="product key for plot") - parser.add_argument("--x", help="first displayed dimension") - parser.add_argument("--y", help="second displayed dimension") def add_parser_test(subparsers, list_models): From 63691394335c202e901a9d85e90e225954a83dae Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 21:24:12 +0200 Subject: [PATCH 165/193] Add the missing struphy output CLI module and DataContainer tests --- src/struphy/console/output.py | 46 +++++++++++++++++++ src/struphy/io/tests/__init__.py | 0 src/struphy/io/tests/test_output_handling.py | 48 ++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 src/struphy/console/output.py create mode 100644 src/struphy/io/tests/__init__.py create mode 100644 src/struphy/io/tests/test_output_handling.py diff --git a/src/struphy/console/output.py b/src/struphy/console/output.py new file mode 100644 index 000000000..985b63987 --- /dev/null +++ b/src/struphy/console/output.py @@ -0,0 +1,46 @@ +from struphy.post_processing.output import open_output + + +def struphy_output( + action: str, + path: str, + physical: bool = False, + parallel: bool = False, + format: str = "markdown", + directory: str | None = None, +): + """Inspect, post-process or report a completed simulation output. + + Parameters + ---------- + action : {"info", "keys", "pproc", "report"} + ``info`` lists the evaluable products with descriptions, ``keys`` prints one key per line, + ``pproc`` materializes the post-processed products, and ``report`` writes a data report. + + path : str + The simulation output directory. + + physical : bool + With ``pproc``, also create physical field components. + + parallel : bool + With ``pproc``, post-process on all ranks of ``MPI.COMM_WORLD``. + + format : {"markdown", "html"} + With ``report``, the report format. + + directory : str, optional + With ``report``, where to write it (default ``post_processing/report/``). + """ + out = open_output(path) + if action == "info": + out.info() + elif action == "keys": + for key in out.keys(): + print(key) + elif action == "pproc": + out.pproc(physical=physical, parallel=parallel) + elif action == "report": + print(out.report(directory, format=format)) + else: + raise ValueError(f"Unknown action {action!r}") diff --git a/src/struphy/io/tests/__init__.py b/src/struphy/io/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/struphy/io/tests/test_output_handling.py b/src/struphy/io/tests/test_output_handling.py new file mode 100644 index 000000000..169e6a837 --- /dev/null +++ b/src/struphy/io/tests/test_output_handling.py @@ -0,0 +1,48 @@ +import gc +import os + +import h5py +import numpy as np +import pytest + +from struphy.io.output_handling import DataContainer + + +def container(tmp_path): + os.makedirs(tmp_path / "data", exist_ok=True) + return DataContainer(str(tmp_path)) + + +def test_saves_the_current_values_of_the_added_objects(tmp_path): + data = container(tmp_path) + value = np.zeros(1) + field = np.zeros((2, 3)) + data.add_data({"scalar/energy": value, "feec/field": field}) + for step in (1, 2): + value[0] = step + field[:] = step + data.save_data() + with h5py.File(data.file_path) as file: + np.testing.assert_array_equal(file["scalar/energy"][:], [0.0, 1.0, 2.0]) + np.testing.assert_array_equal(file["feec/field"][:, 0, 0], [0.0, 1.0, 2.0]) + + +def test_an_object_added_as_a_temporary_is_kept_alive(tmp_path): + data = container(tmp_path) + data.add_data({"scalar/value": np.full(1, 7.0)}) + gc.collect() + np.full(1, -1.0) # would reuse the freed memory if the container did not hold a reference + data.save_data() + with h5py.File(data.file_path) as file: + np.testing.assert_array_equal(file["scalar/value"][:], [7.0, 7.0]) + + +def test_a_restart_names_a_dataset_that_was_not_added_again(tmp_path): + first = container(tmp_path) + first.add_data({"scalar/energy": np.zeros(1), "scalar/dropped": np.zeros(1)}) + + restarted = DataContainer(str(tmp_path)) + restarted.add_data({"scalar/energy": np.ones(1)}) + restarted.save_data(keys=["scalar/energy"]) + with pytest.raises(KeyError, match="scalar/dropped"): + restarted.save_data() From 7f979f108eeb36e72b36ebeb05868a63576727ac Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Sep 2026 23:27:45 +0200 Subject: [PATCH 166/193] Bugfix for frozen test --- src/struphy/models/tests/test_examples.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/struphy/models/tests/test_examples.py b/src/struphy/models/tests/test_examples.py index 5ca741d77..2961446f2 100644 --- a/src/struphy/models/tests/test_examples.py +++ b/src/struphy/models/tests/test_examples.py @@ -3,9 +3,11 @@ from pathlib import Path import pytest +from feectools.ddm.mpi import MockComm from feectools.ddm.mpi import mpi as MPI from struphy.io.setup import import_parameters_py +from struphy.post_processing import output as output_module logger = logging.getLogger("struphy") @@ -18,7 +20,7 @@ @pytest.mark.examples @pytest.mark.parametrize("params_path", PARAMS_MODULES) -def test_examples(params_path: Path): +def test_examples(params_path: Path, monkeypatch): """Run a full simulation for each example parameter file found in the examples/ directory. The test loads the parameter file, runs the simulation, and then @@ -50,6 +52,9 @@ def test_examples(params_path: Path): MPI.COMM_WORLD.Barrier() if MPI.COMM_WORLD.Get_rank() == 0: + # The scripts run on rank 0 alone, so their Output must not synchronize with the other + # ranks: its barriers would pair with the Barrier below and desynchronize the ranks. + monkeypatch.setattr(output_module, "mpi_comm_world", MockComm) if pproc_path.exists(): pproc_module = import_parameters_py(str(pproc_path), name=pproc_name) pproc_module.main() From d00b30f7bcdfe4b732505a350f03966152a8539d Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 09:16:59 +0200 Subject: [PATCH 167/193] Commented out nbsphinx_kernel_name --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 38e9e3e01..7dcde51b6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -66,7 +66,7 @@ def _struphy_is_compiled(): # Notebooks are slow to run and many depend on compiled Struphy kernels, # so only execute them when Struphy has been compiled; otherwise reuse stored outputs. nbsphinx_execute = "auto" if _struphy_is_compiled() else "never" -nbsphinx_kernel_name = "local-env" # This is just for Stefan's local machine, where the system kernel does not work. +# nbsphinx_kernel_name = "local-env" # This is just for Stefan's local machine, where the system kernel does not work. napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True From b55437d136c4c8bec04f9695fe82f2bd3f216285 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 09:22:33 +0200 Subject: [PATCH 168/193] Removed catalog from api docs --- doc/markdown/output-api.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 08f334909..4c5a0b123 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -26,9 +26,6 @@ out.info() for key in out.keys(): print(key) - -# Machine-readable product metadata -catalog = out.catalog(details=True) ``` `keys()` and `info()` do not load product arrays. If post-processed products do not exist yet, @@ -137,8 +134,8 @@ density = out.with_physical_coords("kinetic_ions/e1_e2_density/f").isel(t=-1) radius = np.hypot(density.X, density.Y) ``` -Write a compact data report with metadata and the product catalog. Add selected products to -record their dimensions and units. +Write a compact data report with metadata. Add selected products to record their dimensions and +units. ```python report = out.report("report", products=["en_tot", "diagnostics/rho_xyz"]) From e1a13a4f945d5b0c7300f42b2062ca9f1a4fd81a Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 09:38:02 +0200 Subject: [PATCH 169/193] Removed out_folders from quickstart --- doc/sections/quickstart.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 37ced1f91..6b57432d0 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -215,9 +215,7 @@ The same Simulation API is reused across models. For example, replace :class:`~s perturbations.ModesCos(ls=(1,), amps=(1e-2,), comp=1) ) - env = EnvironmentOptions( - out_folders=str(Path(__file__).resolve().parent), sim_folder="sim_data" - ) + env = EnvironmentOptions(sim_folder="sim_data") sim = Simulation(model=model, env=env, params_path=__file__) if __name__ == "__main__": sim.run() From 248ea1db15e1dacc93a49315e749478af6225ecc Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 09:44:06 +0200 Subject: [PATCH 170/193] Removed pproc() from output-api.md, added commends on MPI postprocessing --- doc/markdown/output-api.md | 23 +++++------------------ src/struphy/post_processing/output.py | 3 +++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 4c5a0b123..94385f69d 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -29,23 +29,7 @@ for key in out.keys(): ``` `keys()` and `info()` do not load product arrays. If post-processed products do not exist yet, -they materialize them using default post-processing options. Call `pproc()` first when those -options matter. - -## Materialize post-processing products - -Use `pproc()` explicitly to choose how fields and particle diagnostics are generated. - -```python -out.pproc( - physical=True, # also create physical field components and coordinates - step=1, # use every saved time step - celldivide=1, -) -``` - -Matching existing products are reused. `evaluate()` also calls `pproc()` automatically for a -missing non-scalar product when running serially. +they materialize them using default post-processing options. ## Evaluate data @@ -253,4 +237,7 @@ out.pproc(parallel=True, physical=True) ``` Automatic materialization through `evaluate()` is intentionally disabled when more than one MPI -rank is active. Call `pproc()` explicitly first in that case. +rank is active. Call `pproc()` explicitly first in that case. `pproc()` is collective: rank 0 +works while the rest wait at a barrier. `evaluate()` cannot trigger it automatically because it +is not guaranteed to be called on every rank, and doing so could hang ranks that never reach the +call instead of failing fast. diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 40031bf7a..5bb1db8ad 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -1956,6 +1956,9 @@ def _ensure_processed(self): if self.is_processed: return if self.comm.Get_size() > 1: + # pproc() is collective (rank 0 works while the rest wait at a Barrier), but evaluate() + # is not guaranteed to be called on every rank; auto-triggering pproc() here could hang + # ranks that never reach this call instead of failing fast. raise RuntimeError(f"{self.path_out} has no post-processed data; call out.pproc() on all ranks first") logger.warning( "\nNo post-processed data in %s, processing with default options (call out.pproc(...) to choose them)", From 1e3916d857e59b9ce5dad7631af19abc5671e2ba Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 09:54:28 +0200 Subject: [PATCH 171/193] Pass parallel: bool = False to evaluate() --- doc/markdown/output-api.md | 15 ++++++++++----- src/struphy/post_processing/output.py | 8 ++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 94385f69d..ce7242b4d 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -236,8 +236,13 @@ world communicator must have the same number of ranks as the run that wrote the out.pproc(parallel=True, physical=True) ``` -Automatic materialization through `evaluate()` is intentionally disabled when more than one MPI -rank is active. Call `pproc()` explicitly first in that case. `pproc()` is collective: rank 0 -works while the rest wait at a barrier. `evaluate()` cannot trigger it automatically because it -is not guaranteed to be called on every rank, and doing so could hang ranks that never reach the -call instead of failing fast. +Automatic materialization through `evaluate()` is disabled by default when more than one MPI rank +is active, because `pproc()` is collective (rank 0 works while the rest wait at a barrier) and +`evaluate()` is not otherwise guaranteed to be called on every rank; auto-triggering it could hang +ranks that never reach the call instead of failing fast. Call `pproc()` explicitly first in that +case, or pass `parallel=True` to `evaluate()` when calling it collectively on every rank; this +triggers `pproc(parallel=True)` automatically on first use. + +```python +out.evaluate("em_fields/E", parallel=True) +``` diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 5bb1db8ad..e5a5571bd 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -289,6 +289,7 @@ def evaluate( representation: Representation | None = None, dataset: str | None = None, variables: str | Sequence[str] | None = None, + parallel: bool = False, **coordinates: Any, ) -> xr.DataArray | xr.Dataset: """Return a named simulation product as an xarray object. @@ -299,6 +300,11 @@ def evaluate( analysis. ``evaluate("scalars")`` returns an :class:`xarray.Dataset` containing all scalar histories; use ``variables=`` to select scalar names. + Under more than one MPI rank, automatic materialization is disabled unless ``parallel`` + is set: pass ``parallel=True`` only when calling ``evaluate()`` collectively on every + rank, which triggers :meth:`pproc` with ``parallel=True`` on first use. Otherwise call + :meth:`pproc` explicitly first. + Common selections can be passed directly: ``t`` selects saved snapshots by index (an integer, list of integers, or slice); omit it for every saved timestep. The returned array always retains its ``t`` dimension. @@ -363,6 +369,8 @@ def evaluate( if representation is not None and not is_raw_spline_field: raise ValueError("representation requires FEEC evaluation") if not is_raw_spline_field and name != "scalars": + if parallel and not self.is_processed: + self.pproc(parallel=True) array = self._product(name, dataset=dataset) if t is not None: if isinstance(t, (int, np.integer)): From d670e5419e010d5e2650b66958cc3b33cc2e6f6f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 09:57:14 +0200 Subject: [PATCH 172/193] Removed params_LinearMHDDriftkineticCC.py --- params_LinearMHDDriftkineticCC.py | 221 ------------------------------ 1 file changed, 221 deletions(-) delete mode 100644 params_LinearMHDDriftkineticCC.py diff --git a/params_LinearMHDDriftkineticCC.py b/params_LinearMHDDriftkineticCC.py deleted file mode 100644 index 517c10207..000000000 --- a/params_LinearMHDDriftkineticCC.py +++ /dev/null @@ -1,221 +0,0 @@ -# ----------------------------- -# Description of the simulation -# ----------------------------- -# Please fill in a verbal description of the simulation. -# It will be printed at the beginning of the simulation and can be used to keep track of the different runs. - -name = "Default LinearMHDDriftkineticCC" -description = """ -This is the default simulation for the model LinearMHDDriftkineticCC. -It is meant to be a template for users to set up their own simulations with this model. -It contains all the necessary components of a Struphy simulation, including the model, -the environment options, the time stepping options, the geometry, the equilibrium, -the grid, the Derham options, and the initial conditions. -Users can modify this file to set up their own simulations with different parameters and initial conditions. -""" - -import logging - -import numpy as np - -from struphy import set_logging_level -from struphy.initial.base import Perturbation - -set_logging_level(logging.WARNING) - -# ------------------ -# Import Struphy API -# ------------------ - -# For particles: -from struphy import ( - BaseUnits, - BinningPlot, - BoundaryParameters, - DerhamOptions, - EnvironmentOptions, - FieldsBackground, - KernelDensityPlot, - LoadingParameters, - ProfilingOptions, - SavingParameters, - Simulation, - SortingParameters, - Time, - WeightsParameters, - domains, - equils, - grids, - maxwellians, - perturbations, -) - -# --------------------- -# Instance of the model -# --------------------- -from struphy.models import LinearMHDDriftkineticCC - -# Units -base_units = BaseUnits() - -# Model instance -model = LinearMHDDriftkineticCC(base_units=base_units) - -# List all variables and decide whether to save their data -model.em_fields.b_field.save_data = True -model.mhd.density.save_data = True -model.mhd.pressure.save_data = True -model.mhd.velocity.save_data = True -model.energetic_ions.var.save_data = True - -# -------------------------- -# Instance of the simulation -# -------------------------- - -# Environment options -env = EnvironmentOptions() - -# Time stepping -time_opts = Time() - -# Geometry -domain = domains.Cuboid() - -# Fluid equilibrium (can be used as part of initial conditions) -equil = equils.HomogenSlab() - -# Grid -grid = grids.TensorProductGrid() - -# Derham options -derham_opts = DerhamOptions() - -# Profiling options -profiling_opts = ProfilingOptions() - -# Simulation object -sim = Simulation( - model=model, - name=name, - description=description, - params_path=__file__, - env=env, - time_opts=time_opts, - domain=domain, - equil=equil, - grid=grid, - derham_opts=derham_opts, - profiling_opts=profiling_opts, -) - -# ------------------- -# Particle parameters -# ------------------- - -loading_params = LoadingParameters() -weights_params = WeightsParameters() -boundary_params = BoundaryParameters() -sorting_params = SortingParameters() -saving_params = SavingParameters() -model.energetic_ions.set_markers( - loading_params=loading_params, - weights_params=weights_params, - boundary_params=boundary_params, - sorting_params=sorting_params, - saving_params=saving_params, -) - -# ------------------ -# Propagator options -# ------------------ - -model.propagators.push_bxe.options = model.propagators.push_bxe.Options() -model.propagators.push_parallel.options = model.propagators.push_parallel.Options() -model.propagators.shearalfen_cc5d.options = model.propagators.shearalfen_cc5d.Options() -model.propagators.magnetosonic.options = model.propagators.magnetosonic.Options() -model.propagators.cc5d_density.options = model.propagators.cc5d_density.Options() -model.propagators.cc5d_gradb.options = model.propagators.cc5d_gradb.Options() -model.propagators.cc5d_curlb.options = model.propagators.cc5d_curlb.Options() - -# ------------------ -# Initial conditions -# ------------------ -# Initial conditions are the sum of the background(s) and the perturbation(s). -# If backgrounds or perturbations are not specified, they are assumed to be zero. - - -class RadialVelocityPerturbation(Perturbation): - """User-defined vector-component perturbation with a radial envelope. - - This class is saved inline in ``run_metadata.json`` and can be restored with - ``Simulation.from_output(..., trust_initial_condition_source=True)``. - """ - - def __init__(self, amplitude=0.02, comp=0): - self.params = {"amplitude": amplitude, "comp": comp} - self.given_in_basis = "v" - self.comp = comp - - def __call__(self, eta1, eta2, eta3, flat_eval=False): - return self.params["amplitude"] * np.sin(np.pi * eta1) * np.cos(2.0 * np.pi * eta2) - - -# Background for (some) FEEC variables -model.mhd.velocity.add_background(FieldsBackground()) - -# Perturbations for (some) FEEC variables -model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=0)) -model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=1)) -model.mhd.velocity.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=2)) -# A custom perturbation class can be combined with built-in perturbations. -model.mhd.velocity.add_perturbation(RadialVelocityPerturbation(amplitude=0.02, comp=0)) - -# For kinetic species the background is mandatory. -# For kinetic species, if add_initial_condition() is not called, the background is taken as the kinetic initial condition. -# For kinetic species the perturbations are added to the moments of the distribution function (defined as tuples). - - -# User-defined profiles can be used anywhere a kinetic Maxwellian accepts a -# callable. They are written inline to run_metadata.json and restored with -# Simulation.from_output(..., trust_initial_condition_source=True). -# The restoration namespace supplies np/numpy and xp/cp/cupy. -def energetic_ion_density(*etas): - """A weak periodic density modulation.""" - eta1, eta2, eta3 = etas[0].T if len(etas) == 1 else etas - return 1.0 + 0.05 * np.cos(2.0 * np.pi * eta2) - - -def energetic_ion_parallel_flow(*etas): - """A small parallel flow with a radial envelope.""" - eta1, eta2, eta3 = etas[0].T if len(etas) == 1 else etas - return 0.1 * np.sin(np.pi * eta1) * np.sin(2.0 * np.pi * eta3) - - -def energetic_ion_parallel_thermal_speed(*etas): - """A positive, smoothly varying parallel thermal speed.""" - eta1, eta2, eta3 = etas[0].T if len(etas) == 1 else etas - return 1.0 + 0.05 * np.cos(2.0 * np.pi * eta1) - - -# Background for kinetic species -maxwellian_1 = maxwellians.GyroMaxwellian2D( - n=(energetic_ion_density, None), - u_para=(energetic_ion_parallel_flow, None), - vth_para=(energetic_ion_parallel_thermal_speed, None), -) -maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None)) -background = maxwellian_1 + maxwellian_2 -model.energetic_ions.var.add_background(background) - -# Perturbations for (some) kinetic species -perturbation = perturbations.TorusModesCos() -maxwellian_1pt = maxwellians.GyroMaxwellian2D( - n=(energetic_ion_density, perturbation), - u_para=(energetic_ion_parallel_flow, None), - vth_para=(energetic_ion_parallel_thermal_speed, None), -) -init = maxwellian_1pt + maxwellian_2 -model.energetic_ions.var.add_initial_condition(init) - -if __name__ == "__main__": - sim.run() From afd714cbe7678137651e5f0ad8b58f3f3dac521d Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:00:29 +0200 Subject: [PATCH 173/193] Use new api in pproc_weibel_instability.py --- .../weibel_instability/pproc_weibel_instability.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 4bc861b05..87a81421c 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -29,7 +29,7 @@ def main(path_out=DEFAULT_OUTPUT): # Gauss law violation # ------------------ if run.model.measure_gauss_law: - gauss_error = run.scalars.gauss_error + gauss_error = run.evaluate("scalars", variables="gauss_error")["gauss_error"] fig, ax = plt.subplots(1, figsize=(10, 6)) ax.plot(gauss_error.t, gauss_error) @@ -45,8 +45,8 @@ def main(path_out=DEFAULT_OUTPUT): # ------------------ # progression of EM-field energy along different directions # ------------------ - e_field = run.fields.em_fields.e_field - b_field = run.fields.em_fields.b_field + e_field = run.evaluate("em_fields/e_field") + b_field = run.evaluate("em_fields/b_field") spatial = ("e1", "e2", "e3") unit_volume = xp.prod([1 / (e_field.sizes[dim] - 1) for dim in spatial]) @@ -104,13 +104,13 @@ def field_energy(field): # ------------------ # Binning distribution evolution # ------------------ - distributions = run.distributions.kinetic_ions for bin_name, x, y in ( ("e1_v1_density", "e1", "v1"), ("v1_v2_density", "v1", "v2"), ): + binned = run.evaluate(f"kinetic_ions/{bin_name}") for quantity in ("f", "delta_f"): - plot_panels(getattr(getattr(distributions, bin_name), quantity), x=x, y=y, n_panels=20, ncols=4) + plot_panels(binned[quantity], x=x, y=y, n_panels=20, ncols=4) plt.show() # ------------------ @@ -148,7 +148,7 @@ def current_1D(time_step: float): fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True) for i in range(3): for j in range(3): - current = getattr(distributions, f"e{i + 1}_current_{j + 1}").f + current = run.evaluate(f"kinetic_ions/e{i + 1}_current_{j + 1}")["f"] current = current.sel(t=time_step, method="nearest") ax[i, j].axhline(color="red", alpha=0.5) ax[i, j].plot(current[f"e{i + 1}"], current) From 4c6b1743e5d8e79f10bffbe028f24734c807d966 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:01:29 +0200 Subject: [PATCH 174/193] Use new API in test_verif_IncompressibleNavierStokesSPH.py --- ...est_verif_IncompressibleNavierStokesSPH.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py index 6c1b9074a..b51bbc2bc 100644 --- a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py +++ b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py @@ -103,8 +103,9 @@ def test_chorin_projection_periodic_1d(nx: int, do_plot: bool = False): run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: - e1_grid = run.distributions.fluid.e1_current_1.f.e1.values.flatten() - j1_binned = run.distributions.fluid.e1_current_1.f.values # (Nt+1, n_bins) + j1 = run.evaluate("fluid/current_1", dataset="e1_current_1/f") + e1_grid = j1.e1.values.flatten() + j1_binned = j1.values # (Nt+1, n_bins) amp_initial = 0.5 * (np.max(j1_binned[0]) - np.min(j1_binned[0])) amp_final = 0.5 * (np.max(j1_binned[-1]) - np.min(j1_binned[-1])) @@ -209,8 +210,9 @@ def test_chorin_projection_reflect_1d(nx: int, do_plot: bool = False): run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: - e1_grid = run.distributions.fluid.e1_current_1.f.e1.values.flatten() - j1_binned = run.distributions.fluid.e1_current_1.f.values # (Nt+1, n_bins) + j1 = run.evaluate("fluid/current_1", dataset="e1_current_1/f") + e1_grid = j1.e1.values.flatten() + j1_binned = j1.values # (Nt+1, n_bins) amp_initial = np.max(np.abs(j1_binned[0])) amp_final = np.max(np.abs(j1_binned[-1])) @@ -320,9 +322,11 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: - e2_grid = run.distributions.fluid.e2_current_1.f.e2.values.flatten() - j1_binned = run.distributions.fluid.e2_current_1.f.values # (Nt+1, n_bins) - j2_binned = run.distributions.fluid.e2_current_2.f.values # (Nt+1, n_bins) + j1 = run.evaluate("fluid/current_1", dataset="e2_current_1/f") + j2 = run.evaluate("fluid/current_2", dataset="e2_current_2/f") + e2_grid = j1.e2.values.flatten() + j1_binned = j1.values # (Nt+1, n_bins) + j2_binned = j2.values # (Nt+1, n_bins) # Analytische Profile U = 0.5 @@ -357,10 +361,6 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): plt.tight_layout() plt.show() - e2_grid = run.distributions.fluid.e2_current_1.f.e2.values.flatten() - j1_binned = run.distributions.fluid.e2_current_1.f.values # (Nt+1, n_bins) - j2_binned = run.distributions.fluid.e2_current_2.f.values # (Nt+1, n_bins) - # --- DEBUG: Check marker velocities --- markers = model.fluid.density.particles.markers # markers columns: 0:eta1, 1:eta2, 2:eta3, 3:v1, 4:v2, 5:v3, 6:weight, ... From cc91b75a9ec3b59af0ca9f536f2471a6ddf44695 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:11:15 +0200 Subject: [PATCH 175/193] Updated remaining examples to new API --- .../cyclone/pproc_cyclone.py | 14 +++++++------- .../itg_cylindre/pproc_drift_kinetic.py | 14 +++++++------- .../diocotron_instability/pproc_diocotron.py | 16 +++++++++++----- .../bump_on/pproc_bump_on.py | 7 ++++--- .../pproc_strong_Landau_damping.py | 5 +++-- .../two_stream/pproc_two_stream.py | 5 +++-- .../pproc_weak_Landau_damping.py | 5 +++-- .../pproc_weibel_instability.py | 6 +++--- 8 files changed, 41 insertions(+), 31 deletions(-) diff --git a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py index c81847c87..4bccd2f6b 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py +++ b/examples/DriftKineticElectrostaticAdiabatic/cyclone/pproc_cyclone.py @@ -25,11 +25,11 @@ def product(run, name): - """Look up a saved product such as ``"kinetic_ions/e1_e2_density/f"`` by attribute access.""" - data = run - for part in name.split("/"): - data = getattr(data, part) - return data + """Look up a saved product such as ``"kinetic_ions/e1_e2_density/f"`` via evaluate().""" + species, *rest = name.split("/") + if len(rest) == 1: + return run.evaluate(name) + return run.evaluate(f"{species}/{rest[-1]}", dataset="/".join(rest)) def plot_growth(series, window=(None, None)): @@ -97,7 +97,7 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - plot_growth(run.scalars[FIT_QUANTITY], window=FIT_WINDOW) + plot_growth(run.evaluate("scalars", variables=FIT_QUANTITY)[FIT_QUANTITY], window=FIT_WINDOW) if SHOW_EQUIL_PROFILE: plot_equilibrium(run.path_out) @@ -105,7 +105,7 @@ def main(path_out=DEFAULT_OUTPUT): for name, plane, fixed in SNAPSHOTS: plot_plane(product(run, name), plane, fixed) - plot_trajectories(run.kinetic_ions.orbits, max_markers=1000) + plot_trajectories(run.evaluate("kinetic_ions/orbits"), max_markers=1000) plt.show() diff --git a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py index e92b8c5e3..befe20469 100644 --- a/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py +++ b/examples/DriftKineticElectrostaticAdiabatic/itg_cylindre/pproc_drift_kinetic.py @@ -25,11 +25,11 @@ def product(run, name): - """Look up a saved product such as ``"kinetic_ions/e1_e2_density/f"`` by attribute access.""" - data = run - for part in name.split("/"): - data = getattr(data, part) - return data + """Look up a saved product such as ``"kinetic_ions/e1_e2_density/f"`` via evaluate().""" + species, *rest = name.split("/") + if len(rest) == 1: + return run.evaluate(name) + return run.evaluate(f"{species}/{rest[-1]}", dataset="/".join(rest)) def plot_growth(series, window=(None, None)): @@ -97,7 +97,7 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out).pproc(physical=True) # growth rate of the electrostatic potential - plot_growth(run.scalars[FIT_QUANTITY], window=FIT_WINDOW) + plot_growth(run.evaluate("scalars", variables=FIT_QUANTITY)[FIT_QUANTITY], window=FIT_WINDOW) if SHOW_EQUIL_PROFILE: plot_equilibrium(run.path_out) @@ -105,7 +105,7 @@ def main(path_out=DEFAULT_OUTPUT): for name, plane, fixed in SNAPSHOTS: plot_plane(product(run, name), plane, fixed) - plot_trajectories(run.kinetic_ions.orbits, max_markers=1000) + plot_trajectories(run.evaluate("kinetic_ions/orbits"), max_markers=1000) plt.show() diff --git a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py index eccdc8a33..86eb65d05 100644 --- a/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py +++ b/examples/ToyGyrokinetic/diocotron_instability/pproc_diocotron.py @@ -29,6 +29,14 @@ ] +def product(run, parts): + """Look up a saved product such as ``("kinetic_ions", "e1_e2_density", "f")`` via evaluate().""" + species, *rest = parts + if len(rest) == 1: + return run.evaluate("/".join(parts)) + return run.evaluate(f"{species}/{rest[-1]}", dataset="/".join(rest)) + + def plot_equilibrium(path_out): """Radial equilibrium profiles from the geometry written at the start of the run.""" equilibrium = pv.read(str(Path(path_out) / "geometry.vts")) @@ -66,7 +74,7 @@ def main(paths=(DEFAULT_OUTPUT,)): # growth rate of the electrostatic energy, one curve per run fig, ax = plt.subplots() for each in runs: - series = each.scalars[FIT_QUANTITY] + series = each.evaluate("scalars", variables=FIT_QUANTITY)[FIT_QUANTITY] (line,) = ax.plot(series.t, series, label=each.path_out.name) result = fit_growth(series, FIT_WINDOW) if result is not None: @@ -85,9 +93,7 @@ def main(paths=(DEFAULT_OUTPUT,)): plt.show() for path in SNAPSHOTS: - data = run - for part in path: - data = getattr(data, part) + data = product(run, path) snapshot = data.isel(t=-1) if "e3" in snapshot.dims: snapshot = snapshot.isel(e3=0) @@ -96,7 +102,7 @@ def main(paths=(DEFAULT_OUTPUT,)): ax.set(aspect="equal", title=f"{'/'.join(path)}, t = {float(snapshot.t):.3g}") plt.show() - orbits = run.kinetic_ions.orbits.isel(marker=slice(0, 1000)) + orbits = run.evaluate("kinetic_ions/orbits").isel(marker=slice(0, 1000)) fig, ax = plt.subplots() ax.plot(orbits.x, orbits.y, lw=0.5) ax.set(xlabel="$x$", ylabel="$y$", title="Marker trajectories", aspect="equal") diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index 322990410..c062eec77 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -22,7 +22,7 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # initial velocity distribution - initial = run.kinetic_ions.v1_density.f.isel(t=0) + initial = run.evaluate("kinetic_ions/f", dataset="v1_density/f").isel(t=0) ax = initial.plot()[0].axes ax.set( xlabel="velocity $v$", @@ -32,12 +32,13 @@ def main(path_out=DEFAULT_OUTPUT): plt.show() # electric field energy - run.scalars.electric_energy.plot(yscale="log") + run.evaluate("scalars", variables="electric_energy")["electric_energy"].plot(yscale="log") plt.title("Electric energy") plt.show() # full f in the e1-v1 plane - plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4, title="full-$f$") + data = run.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") + plot_panels(data, x="e1", y="v1", n_panels=12, ncols=4, title="full-$f$") plt.show() diff --git a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py index f53a0b11d..fa35809ee 100644 --- a/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/strong_Landau_damping/pproc_strong_Landau_damping.py @@ -22,12 +22,13 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # electric field energy - run.scalars.electric_energy.plot(yscale="log") + run.evaluate("scalars", variables="electric_energy")["electric_energy"].plot(yscale="log") plt.title("Electric energy") plt.show() # full f in the e1-v1 plane - plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4, title="full-$f$") + data = run.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") + plot_panels(data, x="e1", y="v1", n_panels=12, ncols=4, title="full-$f$") plt.show() diff --git a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py index d4466a8b2..0366ebf2f 100644 --- a/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py +++ b/examples/VlasovAmpereOneSpecies/two_stream/pproc_two_stream.py @@ -25,7 +25,7 @@ def main(path_out=DEFAULT_OUTPUT): print(f"scalars written to {run.save_scalars()}") # electric field growth against the analytical rate (0.2845 in units of m/c) - energy = run.scalars.electric_energy + energy = run.evaluate("scalars", variables="electric_energy")["electric_energy"] analytical = 10 ** (0.2845 * energy.t - 5.3) # t is in Struphy units fig, ax = plt.subplots() ax.plot(energy.t, energy, label="numerical") @@ -35,7 +35,8 @@ def main(path_out=DEFAULT_OUTPUT): plt.show() # phase space evolution - plot_panels(run.kinetic_ions.e1_v1_density.f, x="e1", y="v1", n_panels=12, ncols=4) + data = run.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") + plot_panels(data, x="e1", y="v1", n_panels=12, ncols=4) plt.show() diff --git a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py index 61457c23e..1170314b0 100644 --- a/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py +++ b/examples/VlasovAmpereOneSpecies/weak_Landau_damping/pproc_weak_Landau_damping.py @@ -32,7 +32,7 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): run = Output(path_out) # electric field energy against the analytical damping - energy = run.scalars.electric_energy + energy = run.evaluate("scalars", variables="electric_energy")["electric_energy"] analytical = E_exact(energy.t.values, eps=amplitude) # t is in Struphy units fig, ax = plt.subplots() ax.plot(energy.t, energy, label="numerical") @@ -43,7 +43,8 @@ def main(path_out=DEFAULT_OUTPUT, amplitude=0.001): # full f and delta f in the e1-v1 plane at four times for quantity, title in (("f", "full-$f$"), ("delta_f", r"$\delta f$")): - plot_panels(getattr(run.kinetic_ions.e1_v1_density, quantity), x="e1", y="v1", n_panels=4, ncols=4, title=title) + data = run.evaluate(f"kinetic_ions/{quantity}", dataset=f"e1_v1_density/{quantity}") + plot_panels(data, x="e1", y="v1", n_panels=4, ncols=4, title=title) plt.show() diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 87a81421c..4d154da35 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -108,9 +108,9 @@ def field_energy(field): ("e1_v1_density", "e1", "v1"), ("v1_v2_density", "v1", "v2"), ): - binned = run.evaluate(f"kinetic_ions/{bin_name}") for quantity in ("f", "delta_f"): - plot_panels(binned[quantity], x=x, y=y, n_panels=20, ncols=4) + data = run.evaluate(f"kinetic_ions/{quantity}", dataset=f"{bin_name}/{quantity}") + plot_panels(data, x=x, y=y, n_panels=20, ncols=4) plt.show() # ------------------ @@ -148,7 +148,7 @@ def current_1D(time_step: float): fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(9, 9), sharey=True, sharex=True) for i in range(3): for j in range(3): - current = run.evaluate(f"kinetic_ions/e{i + 1}_current_{j + 1}")["f"] + current = run.evaluate("kinetic_ions/f", dataset=f"e{i + 1}_current_{j + 1}/f") current = current.sel(t=time_step, method="nearest") ax[i, j].axhline(color="red", alpha=0.5) ax[i, j].plot(current[f"e{i + 1}"], current) From 7f133d616ba65bea969d66813eff7fc03046a6b1 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:20:21 +0200 Subject: [PATCH 176/193] Update tutorials/tutorial_dam_break_sph.ipynb --- tutorials/tutorial_dam_break_sph.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tutorial_dam_break_sph.ipynb b/tutorials/tutorial_dam_break_sph.ipynb index b4170e716..ac6a9718c 100644 --- a/tutorials/tutorial_dam_break_sph.ipynb +++ b/tutorials/tutorial_dam_break_sph.ipynb @@ -281,7 +281,7 @@ "outputs": [], "source": [ "# KDE density field: shape (Nt+1, pts_e1, pts_e2, 1)\n", - "density = out.densities.euler_fluid.view_0.n\n", + "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", "n_sph = density\n", "\n", From 03440f7629140cc942e0dbc2dcbd3767a7943222 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:22:23 +0200 Subject: [PATCH 177/193] Port tutorials and docs to new API --- doc/sections/userguide.rst | 20 +++++++++---------- .../cube_strong_scaling/params_poisson.py | 6 +++--- tutorials/dev_tutorial_feec_bcs.ipynb | 2 +- tutorials/tutorial_gas_expansion_sph.ipynb | 4 ++-- tutorials/tutorial_hagen_poiseuille_sph.ipynb | 2 +- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 4 ++-- tutorials/tutorial_maxwell.ipynb | 6 +++--- tutorials/tutorial_poisson.ipynb | 10 +++++----- .../tutorial_pressureless_sph_shock.ipynb | 4 ++-- .../tutorial_velocity_diffusion_sph.ipynb | 9 +++++---- tutorials/tutorial_viscous_euler_sph.ipynb | 9 +++++---- 11 files changed, 39 insertions(+), 37 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 109f01b60..199d81a4b 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -519,8 +519,8 @@ and it stays available as ``sim.output``: out = sim.run() - out.scalars.total_energy # scalar time series, straight from the raw output - out.fields.em_fields.e_field # evaluated FEEC field (post-processed on first access) + out.evaluate("scalars", variables="total_energy") # scalar time series, straight from the raw output + out.evaluate("em_fields/e_field") # evaluated FEEC field (post-processed on first access) out.domain, out.model.units # reconstructed from saved metadata Every product is an :class:`xarray.DataArray` with named dimensions @@ -584,10 +584,10 @@ ordinary one- and two-dimensional cases: .. code-block:: python - f = out.kinetic_ions.e1_v1_density.f + f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") f.isel(t=-1).plot(x="e1", y="v1") - out.scalars.en_phi.plot.line(x="t") - out.em_fields.phi_xyz.isel(t=-1, e3=0).plot(x="e1", y="e2") + out.evaluate("scalars", variables="en_phi")["en_phi"].plot.line(x="t") + out.evaluate("em_fields/phi_xyz").isel(t=-1, e3=0).plot(x="e1", y="e2") For a comparison across runs, use a Matplotlib axes and plot the labeled arrays onto it: @@ -597,8 +597,8 @@ onto it: import matplotlib.pyplot as plt fig, ax = plt.subplots() - out_a.scalars.en_phi.plot(ax=ax, label="run A") - out_b.scalars.en_phi.plot(ax=ax, label="run B") + out_a.evaluate("scalars", variables="en_phi")["en_phi"].plot(ax=ax, label="run A") + out_b.evaluate("scalars", variables="en_phi")["en_phi"].plot(ax=ax, label="run B") ax.legend() The sections below access the arrays directly for custom Matplotlib plots. @@ -615,7 +615,7 @@ components) or ``_xyz`` (physical components, with import matplotlib.pyplot as plt - e_field = out.fields.em_fields.e_field # dims (t, component, e1, e2, e3) + e_field = out.evaluate("em_fields/e_field") # dims (t, component, e1, e2, e3) snapshot = e_field.isel(t=-1, component=0, e2=0, e3=0) plt.figure() @@ -635,7 +635,7 @@ perturbation with respect to the background: .. code-block:: python - f = out.distributions.kinetic_ions.e1_v1_density.f # dims (t, e1, v1) + f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") # dims (t, e1, v1) f.isel(t=-1).plot(x="e1", y="v1") @@ -654,7 +654,7 @@ quantities (velocities, ``weight``, ...) depend on the particle class, see import matplotlib.pyplot as plt - orbits = out.evaluate("kinetic_ions/orbits") # or out.orbits.kinetic_ions + orbits = out.evaluate("kinetic_ions/orbits") print(orbits) # lists x, y, z, v1, v2, v3, weight marker = orbits.isel(marker=0) diff --git a/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py b/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py index 7d72a47bc..c87d63c29 100644 --- a/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py +++ b/profiling/examples/Poisson/cube_strong_scaling/params_poisson.py @@ -136,7 +136,7 @@ def rhs_fun(x, y, z): if __name__ == "__main__": run = sim.run(profiling_activated=True, one_time_step=True) - run.process(create_vtk=True, parallel=True) + run.pproc(create_vtk=True, parallel=True) def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): from matplotlib import pyplot as plt @@ -233,11 +233,11 @@ def plot_slices(num, exact, name, slice_pt_x=0, slice_pt_y=0, slice_pt_z=0): return fig if sim.comm.rank == 0: - rhs_data = run.fields.em_fields.source_log + rhs_data = run.evaluate("em_fields/source_log") print(rhs_data) rhs = rhs_data.isel(t=0).values - phi_data = run.fields.em_fields.phi_log + phi_data = run.evaluate("em_fields/phi_log") print(phi_data) phi = phi_data.isel(t=-1).values x, y, z = run.grids_phy diff --git a/tutorials/dev_tutorial_feec_bcs.ipynb b/tutorials/dev_tutorial_feec_bcs.ipynb index 4bc15da8d..2507de4dc 100644 --- a/tutorials/dev_tutorial_feec_bcs.ipynb +++ b/tutorials/dev_tutorial_feec_bcs.ipynb @@ -373,7 +373,7 @@ "metadata": {}, "outputs": [], "source": [ - "phi = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0)\n", + "phi = out.evaluate(\"em_fields/phi\").isel(t=-1, e2=0, e3=0)\n", "print(phi)" ] }, diff --git a/tutorials/tutorial_gas_expansion_sph.ipynb b/tutorials/tutorial_gas_expansion_sph.ipynb index 0e68e5a37..d5462c3b9 100644 --- a/tutorials/tutorial_gas_expansion_sph.ipynb +++ b/tutorials/tutorial_gas_expansion_sph.ipynb @@ -396,11 +396,11 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "density = out.densities.euler_fluid.view_0.n\n", + "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", - "f_e1e2 = out.distributions.euler_fluid.e1_e2_density.f\n", + "f_e1e2 = out.evaluate(\"euler_fluid/f\", dataset=\"e1_e2_density/f\")\n", "bc_x = np.asarray(f_e1e2[\"e1\"])\n", "bc_y = np.asarray(f_e1e2[\"e2\"])\n", "\n", diff --git a/tutorials/tutorial_hagen_poiseuille_sph.ipynb b/tutorials/tutorial_hagen_poiseuille_sph.ipynb index d383acb59..46600be82 100644 --- a/tutorials/tutorial_hagen_poiseuille_sph.ipynb +++ b/tutorials/tutorial_hagen_poiseuille_sph.ipynb @@ -278,7 +278,7 @@ "metadata": {}, "outputs": [], "source": [ - "j1_binned = out.distributions.euler_fluid.e2_current_1.f\n", + "j1_binned = out.evaluate(\"euler_fluid/f\", dataset=\"e2_current_1/f\")\n", "e2_grid = j1_binned[\"e2\"] # logical y in [0, 1]\n", "\n", "Nt = int(Tend / dt)\n", diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index d38a6f1eb..599545e6e 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -316,8 +316,8 @@ "source": [ "# Field data is post-processed lazily on first access through `out`\n", "# Extract velocity and pressure time-series\n", - "u_of_t = out.fields.mhd.velocity\n", - "p_of_t = out.fields.mhd.pressure\n", + "u_of_t = out.evaluate(\"mhd/velocity\")\n", + "p_of_t = out.evaluate(\"mhd/pressure\")\n", "\n", "gamma = 5 / 3 # Adiabatic index\n", "\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index ee9f1b5d9..ed687bf8b 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -338,7 +338,7 @@ "source": [ "# Field data is post-processed lazily on first access through `out`\n", "# Extract electric field time-series\n", - "E_of_t = out.fields.em_fields.e_field\n", + "E_of_t = out.evaluate(\"em_fields/e_field\")\n", "\n", "# Compute the power spectrum of E_x and fit the light-wave branch\n", "print(\"\\n=== Light Wave Dispersion Analysis ===\")\n", @@ -658,8 +658,8 @@ "# Extract time and field data\n", "t_grid = out.time\n", "grids_phy = out.grids_phy\n", - "e_field_phy = out.fields.em_fields.e_field_xyz\n", - "b_field_phy = out.fields.em_fields.b_field_xyz\n", + "e_field_phy = out.evaluate(\"em_fields/e_field_xyz\")\n", + "b_field_phy = out.evaluate(\"em_fields/b_field_xyz\")\n", "\n", "# Extract coordinate arrays in the first (r-θ) plane\n", "X = grids_phy[0][:, :, 0] # Radial coordinate (Cartesian x for plotting)\n", diff --git a/tutorials/tutorial_poisson.ipynb b/tutorials/tutorial_poisson.ipynb index 5199eced9..8e5208d2a 100644 --- a/tutorials/tutorial_poisson.ipynb +++ b/tutorials/tutorial_poisson.ipynb @@ -148,7 +148,7 @@ "# Extract 1D line data and compare to analytic solution\n", "x = out.grids_phy[0][:, 0, 0]\n", "\n", - "phi_num = out.fields.em_fields.phi.isel(t=-1, e2=0, e3=0).values\n", + "phi_num = out.evaluate(\"em_fields/phi\").isel(t=-1, e2=0, e3=0).values\n", "phi_ref = phi_exact(x, 0.0, 0.0)\n", "\n", "err = phi_num - phi_ref\n", @@ -292,7 +292,7 @@ "X = out2.grids_phy[0][:, :, 0]\n", "Y = out2.grids_phy[1][:, :, 0]\n", "\n", - "phi2_num = out2.fields.em_fields.phi.isel(t=-1, e3=0).values\n", + "phi2_num = out2.evaluate(\"em_fields/phi\").isel(t=-1, e3=0).values\n", "phi2_ref = phi2_exact(X, Y, 0.0)\n", "err2 = phi2_num - phi2_ref\n", "err2_max = np.max(np.abs(err2))\n", @@ -425,7 +425,7 @@ "X3 = out3.grids_phy[0][:, :, 0]\n", "Y3 = out3.grids_phy[1][:, :, 0]\n", "\n", - "phi3_num = out3.fields.em_fields.phi.isel(t=-1, e3=0).values\n", + "phi3_num = out3.evaluate(\"em_fields/phi\").isel(t=-1, e3=0).values\n", "phi3_ref = phi3_exact(X3, Y3, 0.0)\n", "err3 = phi3_num - phi3_ref\n", "err3_max = np.max(np.abs(err3))\n", @@ -593,8 +593,8 @@ " return {float(t): list(comps) for t, comps in zip(np.asarray(array[\"t\"]), values)}\n", "\n", "x4 = out4.grids_phy[0][:, 0, 0]\n", - "phi4_log = _field_data(out4.fields.em_fields.phi)\n", - "source4_log = _field_data(out4.fields.em_fields.source)\n", + "phi4_log = _field_data(out4.evaluate(\"em_fields/phi\"))\n", + "source4_log = _field_data(out4.evaluate(\"em_fields/source\"))\n", "t_times = sorted(phi4_log.keys())\n", "\n", "print(f\"Solution saved at {len(t_times)} time points\")\n", diff --git a/tutorials/tutorial_pressureless_sph_shock.ipynb b/tutorials/tutorial_pressureless_sph_shock.ipynb index c78e18d2d..62036d6ab 100644 --- a/tutorials/tutorial_pressureless_sph_shock.ipynb +++ b/tutorials/tutorial_pressureless_sph_shock.ipynb @@ -516,8 +516,8 @@ "outputs": [], "source": [ "# Extract binned outputs\n", - "rho_binned = np.asarray(out.distributions.cold_fluid.e1_density.f)\n", - "current1_binned = np.asarray(out.distributions.cold_fluid.e1_current_1.f)\n", + "rho_binned = np.asarray(out.evaluate(\"cold_fluid/f\", dataset=\"e1_density/f\"))\n", + "current1_binned = np.asarray(out.evaluate(\"cold_fluid/f\", dataset=\"e1_current_1/f\"))\n", "t_grid = out.time\n", "eta1_bins = np.linspace(0, 1, n_bins + 1)[:-1] # bin centers\n", "\n", diff --git a/tutorials/tutorial_velocity_diffusion_sph.ipynb b/tutorials/tutorial_velocity_diffusion_sph.ipynb index 47ebfbf3e..6fa43d5a4 100644 --- a/tutorials/tutorial_velocity_diffusion_sph.ipynb +++ b/tutorials/tutorial_velocity_diffusion_sph.ipynb @@ -266,12 +266,13 @@ "metadata": {}, "outputs": [], "source": [ - "density = out.densities.euler_fluid.view_0.n\n", + "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", "n_sph = np.asarray(density) # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = np.asarray(out.distributions.euler_fluid.e1_current_1.f) # shape (Nt+1, n_bins)\n", - "e1_binned = np.asarray(out.distributions.euler_fluid.e1_current_1.f[\"e1\"]) # logical x in [0, 1]\n", - "n_binned = np.asarray(out.distributions.euler_fluid.e1_density.f) # shape (Nt+1, n_bins)\n", + "j1_current = out.evaluate(\"euler_fluid/f\", dataset=\"e1_current_1/f\")\n", + "j1_binned = np.asarray(j1_current) # shape (Nt+1, n_bins)\n", + "e1_binned = np.asarray(j1_current[\"e1\"]) # logical x in [0, 1]\n", + "n_binned = np.asarray(out.evaluate(\"euler_fluid/f\", dataset=\"e1_density/f\")) # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_viscous_euler_sph.ipynb b/tutorials/tutorial_viscous_euler_sph.ipynb index d053ca328..80cb5b9bb 100644 --- a/tutorials/tutorial_viscous_euler_sph.ipynb +++ b/tutorials/tutorial_viscous_euler_sph.ipynb @@ -279,7 +279,7 @@ "outputs": [], "source": [ "# Extract particle positions and density\n", - "density = out.densities.euler_fluid.view_0.n\n", + "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", "ee1, ee2, ee3 = xp.meshgrid(\n", " xp.asarray(density.e1), xp.asarray(density.e2), xp.asarray(density.e3), indexing=\"ij\"\n", ")\n", @@ -704,11 +704,12 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "density_damp = out_damp.densities.euler_fluid.view_0.n\n", + "density_damp = out_damp.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", "ee1, ee2, ee3 = np.meshgrid(density_damp.e1, density_damp.e2, density_damp.e3, indexing=\"ij\")\n", "n_sph = np.asarray(density_damp) # shape (Nt+1, plot_pts, 1, 1)\n", - "j1_binned = np.asarray(out_damp.distributions.euler_fluid.e1_current_1.f) # shape (Nt+1, n_bins)\n", - "e1_binned = np.asarray(out_damp.distributions.euler_fluid.e1_current_1.f[\"e1\"]) # logical x in [0,1]\n", + "j1_current_damp = out_damp.evaluate(\"euler_fluid/f\", dataset=\"e1_current_1/f\")\n", + "j1_binned = np.asarray(j1_current_damp) # shape (Nt+1, n_bins)\n", + "e1_binned = np.asarray(j1_current_damp[\"e1\"]) # logical x in [0,1]\n", "\n", "Nt = j1_binned.shape[0] - 1\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", From 3f4ee515441f194e1216aa5c7c9dc1d98c856b23 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:25:14 +0200 Subject: [PATCH 178/193] Updated doc/sections/userguide.rst --- doc/sections/userguide.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 199d81a4b..e11af2fca 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -507,10 +507,12 @@ moved to another location: from struphy import Output output = Output("./runs/my_run") - output.pproc(physical=True) -This reads the ``run_metadata.json`` written by the simulation. Under MPI, call -``pproc`` on every rank; serial processing runs on rank 0 while the other ranks wait. +This reads the ``run_metadata.json`` written by the simulation. Products are +materialized with :meth:`~struphy.Output.pproc` on first access, using default +options; call ``pproc`` explicitly first to choose different options (see +below), and under MPI call it on every rank, since serial processing runs on +rank 0 while the other ranks wait. The output of a simulation is a :class:`~struphy.Output`. ``sim.run()`` returns it, and it stays available as ``sim.output``: From 6e603f18899327dc0f528895bdd84a8aa6400b8b Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:26:28 +0200 Subject: [PATCH 179/193] Use t=0 in examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py --- examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py index c062eec77..0df5fc848 100644 --- a/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py +++ b/examples/VlasovAmpereOneSpecies/bump_on/pproc_bump_on.py @@ -22,7 +22,7 @@ def main(path_out=DEFAULT_OUTPUT): run = Output(path_out) # initial velocity distribution - initial = run.evaluate("kinetic_ions/f", dataset="v1_density/f").isel(t=0) + initial = run.evaluate("kinetic_ions/f", dataset="v1_density/f", t=0) ax = initial.plot()[0].axes ax.set( xlabel="velocity $v$", From baad8e423f5d4811e36e2d08720118dc8b292446 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:52:50 +0200 Subject: [PATCH 180/193] bugfix: Assert compared raw diff instead of relative --- tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index 599545e6e..2a0e77720 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -337,8 +337,8 @@ "print(f\"Fitted Alfvén speed: {v_alfven_fit:.6f}\")\n", "print(f\"Relative error: {abs(v_alfven_fit - v_alfven_theory) / v_alfven_theory * 100:.2f}%\")\n", "\n", - "error_alfven = xp.abs(v_alfven_fit - v_alfven_theory)\n", - "assert error_alfven < 0.07, f\"Alfvén wave speed error {error_alfven:.4f} exceeds tolerance\"\n", + "rel_error_alfven = xp.abs(v_alfven_fit - v_alfven_theory) / v_alfven_theory\n", + "assert rel_error_alfven < 0.07, f\"Alfvén wave speed relative error {rel_error_alfven:.4f} exceeds tolerance\"\n", "print(\"✓ Alfvén wave verification passed.\\n\")" ] }, @@ -378,11 +378,11 @@ "print(f\" Fitted speed: {v_fast_fit:.6f}\")\n", "print(f\" Relative error: {abs(v_fast_fit - v_fast_theory) / v_fast_theory * 100:.2f}%\")\n", "\n", - "error_slow = xp.abs(v_slow_fit - v_slow_theory)\n", - "error_fast = xp.abs(v_fast_fit - v_fast_theory)\n", + "rel_error_slow = xp.abs(v_slow_fit - v_slow_theory) / v_slow_theory\n", + "rel_error_fast = xp.abs(v_fast_fit - v_fast_theory) / v_fast_theory\n", "\n", - "assert error_slow < 0.05, f\"Slow wave speed error {error_slow:.4f} exceeds tolerance\"\n", - "assert error_fast < 0.19, f\"Fast wave speed error {error_fast:.4f} exceeds tolerance\"\n", + "assert rel_error_slow < 0.05, f\"Slow wave speed relative error {rel_error_slow:.4f} exceeds tolerance\"\n", + "assert rel_error_fast < 0.19, f\"Fast wave speed relative error {rel_error_fast:.4f} exceeds tolerance\"\n", "print(\"\\n✓ Magnetosonic wave verification passed.\")" ] }, From 3f474b9eab40ad3bb8236f7bb319840499871f67 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 10:54:11 +0200 Subject: [PATCH 181/193] Fixed ZeroDivisionError at pproc_weibel_instability.py, directions have a single periodic element, so their field size is 1, making size - 1 == 0 --- .../weibel_instability/pproc_weibel_instability.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py index 4d154da35..ea4a8069e 100644 --- a/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py +++ b/examples/VlasovMaxwellOneSpecies/weibel_instability/pproc_weibel_instability.py @@ -48,7 +48,9 @@ def main(path_out=DEFAULT_OUTPUT): e_field = run.evaluate("em_fields/e_field") b_field = run.evaluate("em_fields/b_field") spatial = ("e1", "e2", "e3") - unit_volume = xp.prod([1 / (e_field.sizes[dim] - 1) for dim in spatial]) + # a periodic direction with a single element has size 1 (no repeated endpoint); + # its cell then spans the whole unit length, contributing a factor of 1 + unit_volume = xp.prod([1 / max(e_field.sizes[dim] - 1, 1) for dim in spatial]) def field_energy(field): """Energy of each component over space, as array of shape (component, t).""" From fcb2b55fbf2f02d1203d5bca461d21beb0fb2e09 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 11:14:33 +0200 Subject: [PATCH 182/193] Tutorial bugfix --- tutorials/tutorial_maxwell.ipynb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index ed687bf8b..a56821ef0 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -295,7 +295,12 @@ "\n", "\n", "def fit_branches(omega, k, power, n_branches, noise_level, order=10):\n", - " \"\"\"Fit omega = v * k + b to each of the n_branches spectral peaks; returns [(v, b), ...] sorted by omega.\"\"\"\n", + " \"\"\"Fit omega = v * k to each of the n_branches spectral peaks; returns [(v, 0.0), ...] sorted by omega.\n", + "\n", + " The fit is forced through the origin: these branches are non-dispersive at leading\n", + " order (omega(k=0) = 0), and an unconstrained affine fit picks up a spurious intercept\n", + " that biases the slope estimate.\n", + " \"\"\"\n", " k_fit, omega_fit = [], [[] for _ in range(n_branches)]\n", " for i in range(k.size // 8, k.size // 2):\n", " column = power[:, i]\n", @@ -310,7 +315,8 @@ " k_fit.append(k[i])\n", " for branch, j in zip(omega_fit, peaks):\n", " branch.append(omega[j])\n", - " return [np.polyfit(k_fit, branch, deg=1) for branch in omega_fit]\n", + " k_arr = np.asarray(k_fit)\n", + " return [(np.sum(k_arr * np.asarray(branch)) / np.sum(k_arr**2), 0.0) for branch in omega_fit]\n", "\n", "\n", "def plot_spectrum(omega, k, power, fits, theory, title):\n", From 29457ca1e8c5ccff2a0722aaca95bd134069d546 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 18:18:40 +0200 Subject: [PATCH 183/193] Bugfix with new api in tutorial_poisson.ipynb --- tutorials/tutorial_poisson.ipynb | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tutorials/tutorial_poisson.ipynb b/tutorials/tutorial_poisson.ipynb index 8e5208d2a..0a40dcc1f 100644 --- a/tutorials/tutorial_poisson.ipynb +++ b/tutorials/tutorial_poisson.ipynb @@ -146,9 +146,11 @@ "outputs": [], "source": [ "# Extract 1D line data and compare to analytic solution\n", - "x = out.grids_phy[0][:, 0, 0]\n", - "\n", - "phi_num = out.evaluate(\"em_fields/phi\").isel(t=-1, e2=0, e3=0).values\n", + "# x comes from the field's own physical coordinate, matching phi_num's grid exactly\n", + "# (out.grids_phy uses a different, node-centered grid convention).\n", + "phi_slice = out.evaluate(\"em_fields/phi\").isel(t=-1, e2=0, e3=0)\n", + "x = phi_slice.X.values\n", + "phi_num = phi_slice.values\n", "phi_ref = phi_exact(x, 0.0, 0.0)\n", "\n", "err = phi_num - phi_ref\n", @@ -289,10 +291,11 @@ "outputs": [], "source": [ "# 2D diagnostics and plots\n", - "X = out2.grids_phy[0][:, :, 0]\n", - "Y = out2.grids_phy[1][:, :, 0]\n", - "\n", - "phi2_num = out2.evaluate(\"em_fields/phi\").isel(t=-1, e3=0).values\n", + "# X, Y come from the field's own physical coordinates, matching phi2_num's grid exactly\n", + "phi2_slice = out2.evaluate(\"em_fields/phi\").isel(t=-1, e3=0)\n", + "X = phi2_slice.X.values\n", + "Y = phi2_slice.Y.values\n", + "phi2_num = phi2_slice.values\n", "phi2_ref = phi2_exact(X, Y, 0.0)\n", "err2 = phi2_num - phi2_ref\n", "err2_max = np.max(np.abs(err2))\n", @@ -422,10 +425,11 @@ "outputs": [], "source": [ "# Annulus diagnostics and plots in physical coordinates only\n", - "X3 = out3.grids_phy[0][:, :, 0]\n", - "Y3 = out3.grids_phy[1][:, :, 0]\n", - "\n", - "phi3_num = out3.evaluate(\"em_fields/phi\").isel(t=-1, e3=0).values\n", + "# X3, Y3 come from the field's own physical coordinates, matching phi3_num's grid exactly\n", + "phi3_slice = out3.evaluate(\"em_fields/phi\").isel(t=-1, e3=0)\n", + "X3 = phi3_slice.X.values\n", + "Y3 = phi3_slice.Y.values\n", + "phi3_num = phi3_slice.values\n", "phi3_ref = phi3_exact(X3, Y3, 0.0)\n", "err3 = phi3_num - phi3_ref\n", "err3_max = np.max(np.abs(err3))\n", @@ -592,8 +596,10 @@ " values = values[:, None]\n", " return {float(t): list(comps) for t, comps in zip(np.asarray(array[\"t\"]), values)}\n", "\n", - "x4 = out4.grids_phy[0][:, 0, 0]\n", - "phi4_log = _field_data(out4.evaluate(\"em_fields/phi\"))\n", + "# x4 comes from the field's own physical coordinate, matching phi4_log's grid exactly\n", + "phi4_array = out4.evaluate(\"em_fields/phi\")\n", + "x4 = phi4_array.isel(t=0, e2=0, e3=0).X.values\n", + "phi4_log = _field_data(phi4_array)\n", "source4_log = _field_data(out4.evaluate(\"em_fields/source\"))\n", "t_times = sorted(phi4_log.keys())\n", "\n", From 22e97eeac8739169b205525c64b4db4dac0efd23 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 26 Sep 2026 23:08:16 +0200 Subject: [PATCH 184/193] Renamed e1,e2,e3 to eta1,eta2,eta3 --- .claude/skills/setup-simulation/SKILL.md | 4 +- doc/markdown/output-api.md | 6 +- doc/sections/userguide.rst | 12 +-- ...est_verif_IncompressibleNavierStokesSPH.py | 6 +- .../verification/test_verif_LinearMHD.py | 2 +- .../tests/verification/test_verif_Maxwell.py | 6 +- .../tests/verification/test_verif_Poisson.py | 4 +- .../test_verif_ViscousEulerSPH.py | 14 ++-- src/struphy/post_processing/arrays.py | 16 ++-- src/struphy/post_processing/output.py | 49 +++++++---- src/struphy/post_processing/si.py | 2 +- src/struphy/post_processing/store.py | 30 +++++-- .../post_processing/tests/test_arrays.py | 18 ++-- .../post_processing/tests/test_output.py | 84 +++++++++++-------- .../post_processing/tests/test_pproc.py | 6 +- tutorials/tutorial_dam_break_sph.ipynb | 2 +- tutorials/tutorial_gas_expansion_sph.ipynb | 6 +- tutorials/tutorial_hagen_poiseuille_sph.ipynb | 2 +- .../tutorial_linear_mhd_slab_waves_1d.ipynb | 2 +- tutorials/tutorial_maxwell.ipynb | 8 +- tutorials/tutorial_poisson.ipynb | 2 +- tutorials/tutorial_post_processing.ipynb | 30 +++---- .../tutorial_velocity_diffusion_sph.ipynb | 4 +- tutorials/tutorial_viscous_euler_sph.ipynb | 6 +- 24 files changed, 189 insertions(+), 132 deletions(-) diff --git a/.claude/skills/setup-simulation/SKILL.md b/.claude/skills/setup-simulation/SKILL.md index e8353b0e9..a3ee15a53 100644 --- a/.claude/skills/setup-simulation/SKILL.md +++ b/.claude/skills/setup-simulation/SKILL.md @@ -142,7 +142,7 @@ out = Output(path_out) out.pproc(physical=True) # optional; products are otherwise processed with defaults on first access out.scalars. # xarray time series, no post-processing needed -out.fields.. # dims (t, [component,] e1, e2, e3) +out.fields.. # dims (t, [component,] eta1, eta2, eta3) out.distributions...f # dims (t, ) out.orbits. # Dataset: one (t, marker) variable per quantity (x, y, z, v1, ..., weight) out.model.units # model reconstructed from metadata @@ -162,7 +162,7 @@ requires the saved run's rank count. Products are xarray objects. Use xarray's plotting and selection methods: ```python -out...f.isel(t=-1).plot(x="e1", y="v1") +out...f.isel(t=-1).plot(x="eta1", y="v1") out.scalars..plot.line(x="t") out..orbits.isel(marker=0)[["x", "y", "z"]].to_dataarray("quantity").plot.line(x="t", hue="quantity") ``` diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index ce7242b4d..484018b56 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -46,7 +46,7 @@ electric_energy = out.evaluate("scalars", variables="electric_energy") electric_field = out.evaluate("em_fields/E", t=-1, component=2) # Select the nearest logical-coordinate plane -midplane = out.evaluate("diagnostics/rho_xyz", e3=0.5, method="nearest", drop=True) +midplane = out.evaluate("diagnostics/rho_xyz", eta3=0.5, method="nearest", drop=True) # Select a time range history = out.evaluate("scalars", variables="electric_energy", t=slice(100, None)).electric_energy @@ -144,7 +144,7 @@ for a `delta_f` product only the density (its perturbation) is meaningful. ```python data = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") -space = [dim for dim in ("e1", "e2", "e3") if dim in data.dims] +space = [dim for dim in ("eta1", "eta2", "eta3") if dim in data.dims] f_of_v = data.mean(space) bin_width = data.v1.differentiate("v1") @@ -212,7 +212,7 @@ Use the native xarray plotting methods after making the intended selection. out.evaluate("scalars", variables="electric_energy").electric_energy.plot.line(x="t") rho = out.evaluate("diagnostics/rho_xyz", t=-1) -rho.isel(e3=rho.sizes["e3"] // 2).plot(x="e1", y="e2") +rho.isel(eta3=rho.sizes["eta3"] // 2).plot(x="eta1", y="eta2") ``` xarray squeezes size-one dimensions before plotting, so an array that is 2-D on a grid with one diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index e11af2fca..585d47162 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -526,7 +526,7 @@ and it stays available as ``sim.output``: out.domain, out.model.units # reconstructed from saved metadata Every product is an :class:`xarray.DataArray` with named dimensions -(``t``, ``component``, ``e1``, ``e2``, ``e3``, ``v1``, ...), coordinates and units. +(``t``, ``component``, ``eta1``, ``eta2``, ``eta3``, ``v1``, ...), coordinates and units. Arrays are read from disk only when accessed. Time is in Struphy units, in which the models' analytic results are written; seconds come @@ -587,9 +587,9 @@ ordinary one- and two-dimensional cases: .. code-block:: python f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") - f.isel(t=-1).plot(x="e1", y="v1") + f.isel(t=-1).plot(x="eta1", y="v1") out.evaluate("scalars", variables="en_phi")["en_phi"].plot.line(x="t") - out.evaluate("em_fields/phi_xyz").isel(t=-1, e3=0).plot(x="e1", y="e2") + out.evaluate("em_fields/phi_xyz").isel(t=-1, eta3=0).plot(x="eta1", y="eta2") For a comparison across runs, use a Matplotlib axes and plot the labeled arrays onto it: @@ -617,8 +617,8 @@ components) or ``_xyz`` (physical components, with import matplotlib.pyplot as plt - e_field = out.evaluate("em_fields/e_field") # dims (t, component, e1, e2, e3) - snapshot = e_field.isel(t=-1, component=0, e2=0, e3=0) + e_field = out.evaluate("em_fields/e_field") # dims (t, component, eta1, eta2, eta3) + snapshot = e_field.isel(t=-1, component=0, eta2=0, eta3=0) plt.figure() plt.plot(snapshot.X, snapshot) # physical x-coordinate along eta1 @@ -638,7 +638,7 @@ perturbation with respect to the background: .. code-block:: python f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/f") # dims (t, e1, v1) - f.isel(t=-1).plot(x="e1", y="v1") + f.isel(t=-1).plot(x="eta1", y="v1") Plotting particle orbits diff --git a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py index b51bbc2bc..bacfef937 100644 --- a/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py +++ b/src/struphy/models/tests/verification/test_verif_IncompressibleNavierStokesSPH.py @@ -104,7 +104,7 @@ def test_chorin_projection_periodic_1d(nx: int, do_plot: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: j1 = run.evaluate("fluid/current_1", dataset="e1_current_1/f") - e1_grid = j1.e1.values.flatten() + e1_grid = j1.eta1.values.flatten() j1_binned = j1.values # (Nt+1, n_bins) amp_initial = 0.5 * (np.max(j1_binned[0]) - np.min(j1_binned[0])) @@ -211,7 +211,7 @@ def test_chorin_projection_reflect_1d(nx: int, do_plot: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: j1 = run.evaluate("fluid/current_1", dataset="e1_current_1/f") - e1_grid = j1.e1.values.flatten() + e1_grid = j1.eta1.values.flatten() j1_binned = j1.values # (Nt+1, n_bins) amp_initial = np.max(np.abs(j1_binned[0])) @@ -324,7 +324,7 @@ def test_channel_noslip_shear_relaxation(nx: int, do_plot: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: j1 = run.evaluate("fluid/current_1", dataset="e2_current_1/f") j2 = run.evaluate("fluid/current_2", dataset="e2_current_2/f") - e2_grid = j1.e2.values.flatten() + e2_grid = j1.eta2.values.flatten() j1_binned = j1.values # (Nt+1, n_bins) j2_binned = j2.values # (Nt+1, n_bins) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index e42c1c345..c9db80ea6 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -31,7 +31,7 @@ def _power_spectrum(field, component=0): """Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.""" if "component" in field.dims: field = field.isel(component=component) - data = field.isel(e1=0, e2=0).transpose("t", "e3") + data = field.isel(eta1=0, eta2=0).transpose("t", "eta3") time, z = data.t.values, data.Z.values nt, nz = data.shape power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2] diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index cbad33248..6d9849dc9 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -31,7 +31,7 @@ def _power_spectrum(field, component=0): """Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.""" if "component" in field.dims: field = field.isel(component=component) - data = field.isel(e1=0, e2=0).transpose("t", "e3") + data = field.isel(eta1=0, eta2=0).transpose("t", "eta3") time, z = data.t.values, data.Z.values nt, nz = data.shape power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2] @@ -181,8 +181,8 @@ def test_coaxial(do_plot: bool = False): modes = m # load data at the final time in the plane eta3 = 0 - e_field_xyz = run.fields.em_fields.e_field_xyz.isel(t=-1, e3=0) - b_field_xyz = run.fields.em_fields.b_field_xyz.isel(t=-1, e3=0) + e_field_xyz = run.fields.em_fields.e_field_xyz.isel(t=-1, eta3=0) + b_field_xyz = run.fields.em_fields.b_field_xyz.isel(t=-1, eta3=0) t_end = float(e_field_xyz.t) X = e_field_xyz.X.values diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 4d188ac61..2dfe0da4b 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -83,8 +83,8 @@ def test_poisson_1d(do_plot=False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - phi = run.fields.em_fields.phi.isel(e2=0, e3=0) - source = run.fields.em_fields.source.isel(e2=0, e3=0) + phi = run.fields.em_fields.phi.isel(eta2=0, eta3=0) + source = run.fields.em_fields.source.isel(eta2=0, eta3=0) x = phi.X.values interval = 2 diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index 113842e6c..6f537bfcd 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -115,7 +115,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: density = run.densities.euler_fluid.view_0.n - ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(density.eta1.values, density.eta2.values, density.eta3.values, indexing="ij") n_sph = density.values if do_plot: @@ -245,11 +245,11 @@ def test_damped_sound_wave(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - e1_binned = run.distributions.euler_fluid.e1_density.f.e1.values + e1_binned = run.distributions.euler_fluid.e1_density.f.eta1.values n_binned = run.distributions.euler_fluid.e1_density.delta_f.values j1_binned = run.distributions.euler_fluid.e1_current_1.f.values density = run.densities.euler_fluid.view_0.n - ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(density.eta1.values, density.eta2.values, density.eta3.values, indexing="ij") n_sph = density.values print(f"{e1_binned.shape = }") @@ -452,9 +452,9 @@ def test_velocity_diffusion(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: density = run.densities.euler_fluid.view_0.n - ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(density.eta1.values, density.eta2.values, density.eta3.values, indexing="ij") n_sph = density.values - e1_binned = run.distributions.euler_fluid.e1_density.f.e1.values + e1_binned = run.distributions.euler_fluid.e1_density.f.eta1.values n_binned = run.distributions.euler_fluid.e1_density.f.values j1_binned = run.distributions.euler_fluid.e1_current_1.f.values print(f"{e1_binned.shape = }") @@ -662,7 +662,7 @@ def test_hagen_poiseuille(nx: int, plot_pts: int, do_plot: bool = False, create_ run.pproc() if MPI.COMM_WORLD.Get_rank() == 0: - e2_grid = run.distributions.euler_fluid.e2_current_1.f.e2.values # logical y in [0, 1] + e2_grid = run.distributions.euler_fluid.e2_current_1.f.eta2.values # logical y in [0, 1] j1_binned = run.distributions.euler_fluid.e2_current_1.f.values # shape (Nt+1, n_bins) import numpy as np @@ -930,7 +930,7 @@ def test_dam_break(nx: int, plot_pts: int, do_plot: bool = False, create_png: bo times = np.linspace(0.0, time_opts.Tend, Nt + 1) density = run.densities.euler_fluid.view_0.n - ee1, ee2, ee3 = xp.meshgrid(density.e1.values, density.e2.values, density.e3.values, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(density.eta1.values, density.eta2.values, density.eta3.values, indexing="ij") n_sph = density.values # (Nt+1, pts_e1, pts_e2, 1) X = np.asarray(ee1)[:, :, 0] * r1 # physical x, shape (pts_e1, pts_e2) diff --git a/src/struphy/post_processing/arrays.py b/src/struphy/post_processing/arrays.py index 8221f1c45..a4993abdb 100644 --- a/src/struphy/post_processing/arrays.py +++ b/src/struphy/post_processing/arrays.py @@ -13,9 +13,9 @@ DIM_LABELS = { "t": r"$t$", - "e1": r"$\eta_1$", - "e2": r"$\eta_2$", - "e3": r"$\eta_3$", + "eta1": r"$\eta_1$", + "eta2": r"$\eta_2$", + "eta3": r"$\eta_3$", "v1": r"$v_1$", "v2": r"$v_2$", "v3": r"$v_3$", @@ -198,22 +198,22 @@ def wrap_field_data( for t in times ] ) - dims = ("t", "e1", "e2", "e3") + dims = ("t", "eta1", "eta2", "eta3") else: values = np.stack([np.stack([np.asarray(c) for c in values_by_time[t]]) for t in times]) - dims = ("t", "component", "e1", "e2", "e3") + dims = ("t", "component", "eta1", "eta2", "eta3") coords: dict = {"t": np.asarray(times) * time_scale} if "component" in dims: coords["component"] = np.arange(values.shape[1]) if grids_log is not None: for i, grid in enumerate(grids_log, 1): - dim = f"e{i}" + dim = f"eta{i}" if len(grid) == values.shape[dims.index(dim)]: coords[dim] = np.asarray(grid) - spatial_shape = tuple(values.shape[dims.index(dim)] for dim in ("e1", "e2", "e3")) + spatial_shape = tuple(values.shape[dims.index(dim)] for dim in ("eta1", "eta2", "eta3")) if grids_phy is not None and all(np.asarray(grid).shape == spatial_shape for grid in grids_phy): for coordinate, grid in zip(("X", "Y", "Z"), grids_phy): - coords[coordinate] = (("e1", "e2", "e3"), np.asarray(grid)) + coords[coordinate] = (("eta1", "eta2", "eta3"), np.asarray(grid)) return data_array(values, dims, coords, name=name or None, label=name, coord_units={"t": time_unit}) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index e5a5571bd..a7ad76aca 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -309,7 +309,7 @@ def evaluate( by index (an integer, list of integers, or slice); omit it for every saved timestep. The returned array always retains its ``t`` dimension. A float ``t`` selects a time coordinate. Other keyword arguments select - named coordinates, for example ``component=2`` or ``e1=0.5``. For particle + named coordinates, for example ``component=2`` or ``eta1=0.5`` (for a product other than a raw FEEC field). For particle products, a ``"species/variable"`` name selects the first matching binned product, then density/KDE product, then orbits. Pass ``dataset=`` to select a particular discovered product; use ``out.info("species/variable")`` to list them. @@ -336,6 +336,13 @@ def evaluate( eta = (eta1, eta2, eta3) has_eta = any(value is not None for value in eta) + if has_eta and name != "scalars" and (dataset is not None or self._is_kinetic_product(name)): + # Only raw FEEC fields can be evaluated anywhere; for particle products (binned + # distributions, SPH densities, ...) eta1/eta2/eta3 select their saved logical coordinates. + for direction, value in enumerate(eta, 1): + if value is not None: + selectors[f"eta{direction}"] = value + eta, has_eta = (None, None, None), False if has_eta and dataset is not None: raise ValueError("dataset= cannot be combined with direct FEEC eta evaluation") if name == "scalars": @@ -394,6 +401,15 @@ def evaluate( raise ValueError("method requires a direct coordinate selector") return array + def _is_kinetic_product(self, name: str) -> bool: + """Whether ``name`` belongs to a kinetic species of the raw output (and is no FEEC field).""" + species = name.split("/")[0] + path = self.path_out / "data" / "data_proc0.hdf5" + if not path.exists(): + return False + with h5py.File(path) as file: + return f"kinetic/{species}" in file and not self._is_raw_spline_field(name) + def _is_raw_spline_field(self, name: str) -> bool: """Whether ``name`` is a raw FEEC field saved in the primary output file.""" try: @@ -472,7 +488,7 @@ def _attach_physical_coords( grids = [np.atleast_1d(np.asarray(eta, dtype=float)) for eta in etas] mapped = self.domain(*grids) shape = tuple(len(grid) for grid in grids) - keep = tuple(slice(None) if dim in dims else 0 for dim in ("e1", "e2", "e3")) + keep = tuple(slice(None) if dim in dims else 0 for dim in ("eta1", "eta2", "eta3")) coordinates = {} for name, values in zip(("X", "Y", "Z"), mapped): values = np.asarray(values).reshape(shape)[keep] @@ -505,7 +521,7 @@ def _logical_grid(*etas: Any) -> tuple[tuple[Any, Any, Any], tuple[str, ...], di arguments = [] dims = [] coords = {} - for dimension, eta in zip(("e1", "e2", "e3"), etas): + for dimension, eta in zip(("eta1", "eta2", "eta3"), etas): array = np.asarray(eta, dtype=float) if not np.all(np.isfinite(array)) or np.any((array < 0.0) | (array > 1.0)): raise ValueError(f"{dimension} values must be finite and lie in the logical unit interval [0, 1]") @@ -643,24 +659,26 @@ def with_physical_coords(self, product: str | xr.DataArray) -> xr.DataArray: """Attach mapped ``X``, ``Y``, ``Z`` coordinates to a product on a logical grid. Fields already carry them; this is for products that do not, such as binned densities. - The coordinates are evaluated with the run's domain on the array's ``e1``, ``e2``, ``e3`` + The coordinates are evaluated with the run's domain on the array's ``eta1``, ``eta2``, ``eta3`` grid; a missing logical dimension is evaluated at ``0.5``. """ array = self._array(product) if all(name in array.coords for name in ("X", "Y", "Z")): return array - dims = tuple(dim for dim in ("e1", "e2", "e3") if dim in array.dims) + dims = tuple(dim for dim in ("eta1", "eta2", "eta3") if dim in array.dims) if not dims: - raise ValueError(f"{array.name!r} has no logical dimensions e1, e2, e3; its dimensions are {array.dims}") + raise ValueError( + f"{array.name!r} has no logical dimensions eta1, eta2, eta3; its dimensions are {array.dims}" + ) missing = [dim for dim in dims if dim not in array.coords] if missing: raise ValueError(f"{array.name!r} has no coordinate values for {missing}") - grids = [np.asarray(array.coords[dim]) if dim in dims else np.array([0.5]) for dim in ("e1", "e2", "e3")] + grids = [np.asarray(array.coords[dim]) if dim in dims else np.array([0.5]) for dim in ("eta1", "eta2", "eta3")] mapped = self.domain(*grids) shape = tuple(len(grid) for grid in grids) for name, values in zip(("X", "Y", "Z"), mapped): values = np.asarray(values).reshape(shape) - keep = tuple(slice(None) if dim in dims else 0 for dim in ("e1", "e2", "e3")) + keep = tuple(slice(None) if dim in dims else 0 for dim in ("eta1", "eta2", "eta3")) array = array.assign_coords({name: (dims, values[keep])}) return array @@ -1793,7 +1811,8 @@ def _post_process_f( slice_grids = {} with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - dims = [part for part in slice_name.split("_")] + # the slice name keeps struphy's short labels (e1_v1); the dimensions are eta1, v1, ... + dims = [store.LOGICAL_DIMS.get(part, part) for part in slice_name.split("_")] centers = [grid[:] for _, grid in file_0["kinetic/" + species + "/f/" + slice_name].attrs.items()] slice_grids[slice_name] = dict(zip(dims, centers)) slice_names = list(slice_grids) @@ -1883,15 +1902,15 @@ def _binned_dataset(self, grids: dict, variables: dict) -> xr.Dataset: def _mapped_coords(self, grids: dict) -> dict: """``X``, ``Y``, ``Z`` on the logical directions of ``grids``, when there are two or three.""" - logical = tuple(dim for dim in grids if dim in ("e1", "e2", "e3")) + logical = tuple(dim for dim in grids if dim in ("eta1", "eta2", "eta3")) if len(logical) not in (2, 3) or self.domain is None: return {} try: if len(logical) == 2: mesh = xp.meshgrid(*(xp.asarray(grids[dim]) for dim in logical), indexing="ij") - arguments = {"e1": 0.5, "e2": 0.0, "e3": 0.0} + arguments = {"eta1": 0.5, "eta2": 0.0, "eta3": 0.0} arguments.update(dict(zip(logical, mesh))) - mapped = self.domain(arguments["e1"], arguments["e2"], arguments["e3"], squeeze_out=True) + mapped = self.domain(arguments["eta1"], arguments["eta2"], arguments["eta3"], squeeze_out=True) else: mapped = self.domain(*(xp.asarray(grids[dim]) for dim in logical)) except (TypeError, ValueError): @@ -1922,7 +1941,7 @@ def _post_process_n_sph( with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: for view in file_0["kinetic/" + species + "/n_sph"]: attrs = file_0["kinetic/" + species + "/n_sph/" + view].attrs - view_grids[view] = {f"e{direction}": attrs["eta" + direction][:] for direction in ("1", "2", "3")} + view_grids[view] = {f"eta{direction}": attrs["eta" + direction][:] for direction in ("1", "2", "3")} views = list(view_grids) # compute sph density @@ -2100,7 +2119,7 @@ def time(self): def _first_field(self) -> xr.Dataset | None: """The dataset of a field species, which carries the evaluation grids.""" for group, dataset in self._groups().items(): - if "/" not in group and any("e1" in dataset[name].dims for name in dataset.data_vars): + if "/" not in group and any("eta1" in dataset[name].dims for name in dataset.data_vars): return dataset return None @@ -2123,7 +2142,7 @@ def _load_grids(self, name): if dataset is None: return None if name == "grids_log": - return [np.asarray(dataset[dim]) for dim in ("e1", "e2", "e3")] + return [np.asarray(dataset[dim]) for dim in ("eta1", "eta2", "eta3")] if not all(coordinate in dataset.coords for coordinate in ("X", "Y", "Z")): return None return [np.asarray(dataset[coordinate]) for coordinate in ("X", "Y", "Z")] diff --git a/src/struphy/post_processing/si.py b/src/struphy/post_processing/si.py index 9db60533c..deb71d25c 100644 --- a/src/struphy/post_processing/si.py +++ b/src/struphy/post_processing/si.py @@ -33,7 +33,7 @@ def to_si(array: xr.DataArray, units, unit: str | float | None = None, *, label: Coordinates are converted whenever present: time ``t`` to seconds, the mapped ``X``, ``Y``, ``Z`` to meters and the velocities ``v1``, ``v2``, ``v3`` to m/s, each with the matching unit - of ``units``. Logical coordinates ``e1``, ``e2``, ``e3`` are dimensionless and unchanged. + of ``units``. Logical coordinates ``eta1``, ``eta2``, ``eta3`` are dimensionless and unchanged. The values are only converted if ``unit`` is given, because a product does not record which unit its variable was normalized with. Pass the name of that unit, one of ``x``, ``B``, ``n``, diff --git a/src/struphy/post_processing/store.py b/src/struphy/post_processing/store.py index 6fd04b9d2..756fa638e 100644 --- a/src/struphy/post_processing/store.py +++ b/src/struphy/post_processing/store.py @@ -5,10 +5,11 @@ /em_fields e_field, phi, and e_field_xyz, phi_xyz with physical=True /kinetic_ions/orbits x, y, z, v1, ... (one variable per quantity, see Particles.orbit_quantities) - /kinetic_ions/e1_v1_density f, delta_f + /kinetic_ions/e1_v1_density f, delta_f (dimensions t, eta1, v1) /kinetic_ions/view_0 n -Coordinates (time, the logical grids, and the mapped ``X``, ``Y``, ``Z``), units and labels +Coordinates (time, the logical grids ``eta1``, ``eta2``, ``eta3``, and the mapped ``X``, ``Y``, +``Z``), units and labels travel with the data, so reading needs nothing but the file. Groups are written one at a time and read lazily, through xarray's ``h5netcdf`` engine. """ @@ -20,9 +21,13 @@ import xarray as xr STORE_NAME = "output.nc" -SCHEMA_VERSION = 1 +# 2: the logical dimensions are named eta1, eta2, eta3 (they were e1, e2, e3 in version 1) +SCHEMA_VERSION = 2 ENGINE = "h5netcdf" +# struphy's short logical labels (as in binning slice names like "e1_v1") and the dimension names +LOGICAL_DIMS = {"e1": "eta1", "e2": "eta2", "e3": "eta3"} + def store_path(path_pproc) -> str: """Path of the product store inside a post-processing directory.""" @@ -39,6 +44,21 @@ def write_group(path, group: str, dataset: xr.Dataset): dataset.to_netcdf(path, group=group, mode="a", engine=ENGINE) +def _rename_legacy_dims(dataset: xr.Dataset) -> xr.Dataset: + """A version-1 group with its logical dimensions (e1, e2, e3) renamed to eta1, eta2, eta3.""" + names = {old: new for old, new in LOGICAL_DIMS.items() if old in dataset.dims or old in dataset.coords} + return dataset.rename(names) if names else dataset + + def open_tree(path) -> xr.DataTree: - """Open the store lazily; arrays are read from disk when they are used.""" - return xr.open_datatree(path, engine=ENGINE) + """Open the store lazily; arrays are read from disk when they are used. + + Stores written before schema version 2 name the logical dimensions e1, e2, e3; they are + renamed to eta1, eta2, eta3 on reading, so older post-processing output keeps working. + """ + tree = xr.open_datatree(path, engine=ENGINE) + if int(tree.attrs.get("schema_version", 1)) < 2: + renamed = tree.map_over_datasets(_rename_legacy_dims) + renamed.set_close(tree.close) # closing the renamed tree releases the file + return renamed + return tree diff --git a/src/struphy/post_processing/tests/test_arrays.py b/src/struphy/post_processing/tests/test_arrays.py index 17d635aef..47713b503 100644 --- a/src/struphy/post_processing/tests/test_arrays.py +++ b/src/struphy/post_processing/tests/test_arrays.py @@ -20,15 +20,15 @@ def test_data_array_carries_names_coordinates_and_units(): data = data_array( np.ones((3, 4)), - ("t", "e1"), - {"t": [0, 1, 2], "e1": np.arange(4)}, + ("t", "eta1"), + {"t": [0, 1, 2], "eta1": np.arange(4)}, name="density", label="$n$", unit="m^-3", coord_units={"t": "s"}, ) assert isinstance(data, xr.DataArray) - assert data.sel(t=1).dims == ("e1",) + assert data.sel(t=1).dims == ("eta1",) assert axis_label(data, "t") == "$t$ [s]" assert value_label(data) == "$n$ [m^-3]" @@ -54,8 +54,8 @@ def test_field_wrapper_attaches_curvilinear_physical_coordinates(): physical = np.meshgrid(*logical, indexing="ij") raw = {0.0: [np.zeros((2, 3, 4))], 1.0: [np.ones((2, 3, 4))]} field = wrap_field_data(raw, logical, grids_phy=physical, name="phi", time_scale=2, time_unit="s") - assert field.dims == ("t", "e1", "e2", "e3") - assert field.X.dims == ("e1", "e2", "e3") + assert field.dims == ("t", "eta1", "eta2", "eta3") + assert field.X.dims == ("eta1", "eta2", "eta3") np.testing.assert_array_equal(field.t, [0, 2]) assert field.t.attrs["units"] == "s" @@ -63,14 +63,14 @@ def test_field_wrapper_attaches_curvilinear_physical_coordinates(): def test_vector_field_has_named_component_dimension(): component = np.zeros((2, 2, 2)) field = wrap_field_data({0.0: [component, component, component]}, name="E") - assert field.dims == ("t", "component", "e1", "e2", "e3") - assert field.isel(component=1).dims == ("t", "e1", "e2", "e3") + assert field.dims == ("t", "component", "eta1", "eta2", "eta3") + assert field.isel(component=1).dims == ("t", "eta1", "eta2", "eta3") def test_binned_wrapper_keeps_memory_mappable_values(): values = np.ones((2, 3, 4)) - data = wrap_binned_data(values, ("e1", "v1"), {"t": [0, 1], "e1": range(3), "v1": range(4)}, name="f") - assert data.dims == ("t", "e1", "v1") + data = wrap_binned_data(values, ("eta1", "v1"), {"t": [0, 1], "eta1": range(3), "v1": range(4)}, name="f") + assert data.dims == ("t", "eta1", "v1") assert data.attrs["label"] == "$f$" diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 1b9f83e6c..47997896e 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -29,7 +29,7 @@ def write_tree(root): t = np.linspace(0, 1, NT) np.save(os.path.join(pproc, "t_grid.npy"), t) - logical = {f"e{axis + 1}": np.linspace(0, 1, n) for axis, n in enumerate((N1, N2, N3))} + logical = {f"eta{axis + 1}": np.linspace(0, 1, n) for axis, n in enumerate((N1, N2, N3))} mapped = np.meshgrid(*logical.values(), indexing="ij") path = store.store_path(pproc) store.create(path) @@ -39,7 +39,7 @@ def write_tree(root): xr.Dataset( { "E": ( - ("t", "component", "e1", "e2", "e3"), + ("t", "component", "eta1", "eta2", "eta3"), np.stack([np.stack([np.full((N1, N2, N3), i + time) for i in range(3)]) for time in t]), ) }, @@ -47,7 +47,7 @@ def write_tree(root): "t": t, "component": [0, 1, 2], **logical, - **{name: (("e1", "e2", "e3"), grid) for name, grid in zip(("X", "Y", "Z"), mapped)}, + **{name: (("eta1", "eta2", "eta3"), grid) for name, grid in zip(("X", "Y", "Z"), mapped)}, }, ), ) @@ -55,16 +55,19 @@ def write_tree(root): path, "/kinetic_ions/e1_v1_density", xr.Dataset( - {"f": (("t", "e1", "v1"), np.ones((NT, N1, NV))), "delta_f": (("t", "e1", "v1"), np.zeros((NT, N1, NV)))}, - coords={"t": t, "e1": logical["e1"], "v1": np.linspace(-3, 3, NV)}, + { + "f": (("t", "eta1", "v1"), np.ones((NT, N1, NV))), + "delta_f": (("t", "eta1", "v1"), np.zeros((NT, N1, NV))), + }, + coords={"t": t, "eta1": logical["eta1"], "v1": np.linspace(-3, 3, NV)}, ), ) store.write_group( path, "/kinetic_ions/view_0", xr.Dataset( - {"n": (("t", "e1", "e2", "e3"), np.ones((NT, N1, N2, 1)))}, - coords={"t": t, "e1": logical["e1"], "e2": logical["e2"], "e3": np.zeros(1)}, + {"n": (("t", "eta1", "eta2", "eta3"), np.ones((NT, N1, N2, 1)))}, + coords={"t": t, "eta1": logical["eta1"], "eta2": logical["eta2"], "eta3": np.zeros(1)}, ), ) orbits = np.stack([np.full((N_MARKERS, 7), step) for step in range(NT)]) @@ -163,23 +166,23 @@ def test_catalog_is_structured_and_report_is_written(run, tmp_path): def test_field_has_named_and_curvilinear_coordinates(run): field = run.fields["em_fields/E"] - assert field.dims == ("t", "component", "e1", "e2", "e3") - assert field.X.dims == ("e1", "e2", "e3") + assert field.dims == ("t", "component", "eta1", "eta2", "eta3") + assert field.X.dims == ("eta1", "eta2", "eta3") np.testing.assert_allclose(field.isel(t=0, component=2), 2) assert run.field_catalog._cache["em_fields/E"] is field def test_binned_products_have_coordinates(run): data = run.distributions["kinetic_ions/e1_v1_density/f"] - assert data.dims == ("t", "e1", "v1") + assert data.dims == ("t", "eta1", "v1") np.testing.assert_allclose(data.v1, np.linspace(-3, 3, NV)) def test_sph_density_views_take_dimensions_from_their_grids(run): data = run.densities.kinetic_ions.view_0.n - assert data.dims == ("t", "e1", "e2", "e3") + assert data.dims == ("t", "eta1", "eta2", "eta3") assert data.shape == (NT, N1, N2, 1) - np.testing.assert_allclose(data.e2, np.linspace(0, 1, N2)) + np.testing.assert_allclose(data.eta2, np.linspace(0, 1, N2)) def test_orbit_product_is_a_dataset_of_named_quantities(run): @@ -281,7 +284,7 @@ def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): def test_evaluate_selects_positions_coordinates_and_slices(run): field = run.evaluate("em_fields/E", t=-1, component=2) - assert field.dims == ("t", "e1", "e2", "e3") + assert field.dims == ("t", "eta1", "eta2", "eta3") assert field.sizes["t"] == 1 np.testing.assert_allclose(field, 3.0) @@ -291,7 +294,7 @@ def test_evaluate_selects_positions_coordinates_and_slices(run): phase_space = run.evaluate( "kinetic_ions/f", dataset="e1_v1_density/f", - e1=0.49, + eta1=0.49, method="nearest", drop=True, ) @@ -315,14 +318,14 @@ def test_evaluate_scalars_and_particle_defaults(run): distribution = run.evaluate("kinetic_ions/f") assert distribution.name == "f" - assert distribution.dims == ("t", "e1", "v1") + assert distribution.dims == ("t", "eta1", "v1") fallback = run.evaluate("kinetic_ions/any_variable") assert fallback.name == "f" density = run.evaluate("kinetic_ions/n") assert density.name == "n" - assert density.dims == ("t", "e1", "e2", "e3") + assert density.dims == ("t", "eta1", "eta2", "eta3") selected = run.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") assert selected.name == "delta_f" @@ -368,17 +371,17 @@ class Field: space_id = "H1" def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): - e1, e2, e3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") - value = e1 + 10 * e2 + 100 * e3 + eta1, eta2, eta3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + value = eta1 + 10 * eta2 + 100 * eta3 return value.squeeze() if squeeze_out else value monkeypatch.setattr(run, "spline_fields", lambda *, t: {"em_fields": {"phi": Field()}}) values = run.evaluate("em_fields/phi", eta1=[0.25, 0.5], eta2=range(2), eta3=0.75, t=0) - assert values.dims == ("t", "e1", "e2") - np.testing.assert_allclose(values.e1, [0.25, 0.5]) - np.testing.assert_allclose(values.e2, [0.0, 1.0]) + assert values.dims == ("t", "eta1", "eta2") + np.testing.assert_allclose(values.eta1, [0.25, 0.5]) + np.testing.assert_allclose(values.eta2, [0.0, 1.0]) np.testing.assert_allclose(values[0], [[75.25, 85.25], [75.5, 85.5]]) @@ -394,8 +397,8 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): class Domain: def __call__(self, eta1, eta2, eta3): - e1, e2, e3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") - return e1 + 1.0, e2 + 2.0, e3 + 3.0 + eta1, eta2, eta3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + return eta1 + 1.0, eta2 + 2.0, eta3 + 3.0 run.grid = SimpleNamespace(num_elements=(2, 3, 4)) run.domain = Domain() @@ -403,12 +406,12 @@ def __call__(self, eta1, eta2, eta3): values = run.evaluate("em_fields/phi", t=0) - assert values.dims == ("t", "e1", "e2", "e3") + assert values.dims == ("t", "eta1", "eta2", "eta3") assert values.shape == (1, 2, 3, 4) - np.testing.assert_allclose(values.e1, [0.25, 0.75]) - np.testing.assert_allclose(values.e2, [1 / 6, 0.5, 5 / 6]) - np.testing.assert_allclose(values.e3, [0.125, 0.375, 0.625, 0.875]) - assert values.X.dims == values.Y.dims == values.Z.dims == ("e1", "e2", "e3") + np.testing.assert_allclose(values.eta1, [0.25, 0.75]) + np.testing.assert_allclose(values.eta2, [1 / 6, 0.5, 5 / 6]) + np.testing.assert_allclose(values.eta3, [0.125, 0.375, 0.625, 0.875]) + assert values.X.dims == values.Y.dims == values.Z.dims == ("eta1", "eta2", "eta3") np.testing.assert_allclose(values.X[:, 0, 0], [1.25, 1.75]) np.testing.assert_allclose(values.Y[0, :, 0], [2 + 1 / 6, 2.5, 2 + 5 / 6]) np.testing.assert_allclose(values.Z[0, 0, :], [3.125, 3.375, 3.625, 3.875]) @@ -425,7 +428,7 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): values = run.evaluate("em_fields/phi", eta1=[0.25, 0.75], t=0) - assert values.dims == ("t", "e1") + assert values.dims == ("t", "eta1") np.testing.assert_allclose(values[0], [1.25, 1.75]) @@ -476,9 +479,9 @@ def test_evaluate_transforms_hcurl_fields_on_mapped_domains(run, monkeypatch, do class Field: space_id = "Hcurl" - def __call__(self, e1, e2, e3, *, squeeze_out=False): - e1, e2, e3 = np.meshgrid(e1, e2, e3, indexing="ij") - return [1.0 + e1, 2.0 + e2, 3.0 + e3] + def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): + eta1, eta2, eta3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + return [1.0 + eta1, 2.0 + eta2, 3.0 + eta3] field = Field() monkeypatch.setattr(run, "domain", domain) @@ -508,7 +511,7 @@ def __call__(self, e1, e2, e3, *, squeeze_out=False): ) expected = np.squeeze(np.asarray(expected)) np.testing.assert_allclose(result.isel(t=0), expected) - assert result.dims == ("t", "component", "e1", "e2") + assert result.dims == ("t", "component", "eta1", "eta2") def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): @@ -679,3 +682,18 @@ def test_command_line_lists_keys_and_writes_a_report(tmp_path, monkeypatch, caps printed = capsys.readouterr().out.splitlines() assert printed[: len(Output(root).keys())] == list(Output(root).keys()) assert Path(printed[-1]).name == "report.html" and Path(printed[-1]).exists() + + +def test_stores_of_schema_version_1_are_read_with_eta_dimensions(tmp_path): + """Post-processing output written before the rename (e1, e2, e3) keeps working.""" + path = str(tmp_path / "output.nc") + xr.Dataset(attrs={"schema_version": 1}).to_netcdf(path, mode="w", engine=store.ENGINE) + legacy = xr.Dataset( + {"phi": (("t", "e1", "e2"), np.ones((2, 3, 4)))}, + coords={"t": [0.0, 1.0], "e1": np.linspace(0, 1, 3), "e2": np.linspace(0, 1, 4)}, + ) + store.write_group(path, "/em_fields", legacy) + tree = store.open_tree(path) + assert tree["em_fields"].ds.phi.dims == ("t", "eta1", "eta2") + tree.close() + assert store.SCHEMA_VERSION == 2 diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index 4f8159258..540f23e05 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -21,8 +21,8 @@ def test_pproc_mpi(show_plot=False): def do_plotting(run: Output, from_parallel=False): - e_field = run.fields.em_fields.e_field.isel(t=0, component=0, e2=0, e3=0) - phi = run.fields.em_fields.phi.isel(t=0, e2=0, e3=0) + e_field = run.fields.em_fields.e_field.isel(t=0, component=0, eta2=0, eta3=0) + phi = run.fields.em_fields.phi.isel(t=0, eta2=0, eta3=0) f = run.distributions.kinetic_ions.e1_v1_density f_binned = f.f.isel(t=0) df_binned = f.delta_f.isel(t=0) @@ -37,7 +37,7 @@ def do_plotting(run: Output, from_parallel=False): plt.legend() for index, (data, title) in enumerate(((f_binned, "full f"), (df_binned, "delta f")), 3): plt.subplot(2, 2, index) - data.plot(x="e1", y="v1") + data.plot(x="eta1", y="v1") plt.title(f"{title} at t={float(data.t)} on rank 0{extra}") return tuple(np.asarray(data) for data in (e_field, phi, f_binned, df_binned)) diff --git a/tutorials/tutorial_dam_break_sph.ipynb b/tutorials/tutorial_dam_break_sph.ipynb index ac6a9718c..cbb121bc9 100644 --- a/tutorials/tutorial_dam_break_sph.ipynb +++ b/tutorials/tutorial_dam_break_sph.ipynb @@ -282,7 +282,7 @@ "source": [ "# KDE density field: shape (Nt+1, pts_e1, pts_e2, 1)\n", "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", + "ee1, ee2, ee3 = np.meshgrid(density.eta1, density.eta2, density.eta3, indexing=\"ij\")\n", "n_sph = density\n", "\n", "# Marker orbits: xarray.Dataset with one (t, marker) variable per saved quantity\n", diff --git a/tutorials/tutorial_gas_expansion_sph.ipynb b/tutorials/tutorial_gas_expansion_sph.ipynb index d5462c3b9..2028e0257 100644 --- a/tutorials/tutorial_gas_expansion_sph.ipynb +++ b/tutorials/tutorial_gas_expansion_sph.ipynb @@ -397,12 +397,12 @@ "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", + "ee1, ee2, ee3 = np.meshgrid(density.eta1, density.eta2, density.eta3, indexing=\"ij\")\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", "f_e1e2 = out.evaluate(\"euler_fluid/f\", dataset=\"e1_e2_density/f\")\n", - "bc_x = np.asarray(f_e1e2[\"e1\"])\n", - "bc_y = np.asarray(f_e1e2[\"e2\"])\n", + "bc_x = np.asarray(f_e1e2[\"eta1\"])\n", + "bc_y = np.asarray(f_e1e2[\"eta2\"])\n", "\n", "# markers: one (t, marker) variable per saved quantity (x, y, z, v1, v2, v3, weight)\n", "orbits = out.evaluate(\"euler_fluid/orbits\")\n", diff --git a/tutorials/tutorial_hagen_poiseuille_sph.ipynb b/tutorials/tutorial_hagen_poiseuille_sph.ipynb index 46600be82..daee2d477 100644 --- a/tutorials/tutorial_hagen_poiseuille_sph.ipynb +++ b/tutorials/tutorial_hagen_poiseuille_sph.ipynb @@ -279,7 +279,7 @@ "outputs": [], "source": [ "j1_binned = out.evaluate(\"euler_fluid/f\", dataset=\"e2_current_1/f\")\n", - "e2_grid = j1_binned[\"e2\"] # logical y in [0, 1]\n", + "e2_grid = j1_binned[\"eta2\"] # logical y in [0, 1]\n", "\n", "Nt = int(Tend / dt)\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", diff --git a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb index 2a0e77720..24ea8d09d 100644 --- a/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb +++ b/tutorials/tutorial_linear_mhd_slab_waves_1d.ipynb @@ -264,7 +264,7 @@ " \"\"\"Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.\"\"\"\n", " if \"component\" in field.dims:\n", " field = field.isel(component=component)\n", - " data = field.isel(e1=0, e2=0).transpose(\"t\", \"e3\")\n", + " data = field.isel(eta1=0, eta2=0).transpose(\"t\", \"eta3\")\n", " time, z = data.t.values, data.Z.values\n", " nt, nz = data.shape\n", " power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2]\n", diff --git a/tutorials/tutorial_maxwell.ipynb b/tutorials/tutorial_maxwell.ipynb index a56821ef0..86abf2147 100644 --- a/tutorials/tutorial_maxwell.ipynb +++ b/tutorials/tutorial_maxwell.ipynb @@ -285,7 +285,7 @@ " \"\"\"Space-time power spectrum |F(omega, k)| of a field along z, at the first x and y grid point.\"\"\"\n", " if \"component\" in field.dims:\n", " field = field.isel(component=component)\n", - " data = field.isel(e1=0, e2=0).transpose(\"t\", \"e3\")\n", + " data = field.isel(eta1=0, eta2=0).transpose(\"t\", \"eta3\")\n", " time, z = data.t.values, data.Z.values\n", " nt, nz = data.shape\n", " power = (2.0 / nt) * (2.0 / nz) * np.abs(fft2(data.values))[: nt // 2, : nz // 2]\n", @@ -751,9 +751,9 @@ "t_end = t_grid[-1]\n", "\n", "# Numerical fields (Cartesian components)\n", - "Ex_num = e_field_phy.isel(t=-1, component=0, e3=0).values\n", - "Ey_num = e_field_phy.isel(t=-1, component=1, e3=0).values\n", - "Bz_num = b_field_phy.isel(t=-1, component=2, e3=0).values\n", + "Ex_num = e_field_phy.isel(t=-1, component=0, eta3=0).values\n", + "Ey_num = e_field_phy.isel(t=-1, component=1, eta3=0).values\n", + "Bz_num = b_field_phy.isel(t=-1, component=2, eta3=0).values\n", "\n", "# Analytical fields\n", "Er_analytic = E_r_analytic(X, Y, grids_phy[0], m, t_end)\n", diff --git a/tutorials/tutorial_poisson.ipynb b/tutorials/tutorial_poisson.ipynb index 0a40dcc1f..2ac861178 100644 --- a/tutorials/tutorial_poisson.ipynb +++ b/tutorials/tutorial_poisson.ipynb @@ -598,7 +598,7 @@ "\n", "# x4 comes from the field's own physical coordinate, matching phi4_log's grid exactly\n", "phi4_array = out4.evaluate(\"em_fields/phi\")\n", - "x4 = phi4_array.isel(t=0, e2=0, e3=0).X.values\n", + "x4 = phi4_array.isel(t=0, eta2=0, eta3=0).X.values\n", "phi4_log = _field_data(phi4_array)\n", "source4_log = _field_data(out4.evaluate(\"em_fields/source\"))\n", "t_times = sorted(phi4_log.keys())\n", diff --git a/tutorials/tutorial_post_processing.ipynb b/tutorials/tutorial_post_processing.ipynb index a857e47a0..eaa0a22e3 100644 --- a/tutorials/tutorial_post_processing.ipynb +++ b/tutorials/tutorial_post_processing.ipynb @@ -304,7 +304,7 @@ " eta2=np.linspace(0.0, 1.0, 48),\n", " t=-1,\n", ")\n", - "phi_plane.plot(x=\"e1\", y=\"e2\")\n", + "phi_plane.plot(x=\"eta1\", y=\"eta2\")\n", "\n", "# 3-D: vary all three coordinates; keep volume grids modest.\n", "phi_volume = out.evaluate(\n", @@ -317,7 +317,7 @@ "print(phi_volume.dims, phi_volume.shape)\n", "\n", "# xarray plots a 2-D slice of the volume; choose the mid-plane by coordinate index.\n", - "phi_volume.isel(e3=phi_volume.sizes[\"e3\"] // 2).plot(x=\"e1\", y=\"e2\")\n", + "phi_volume.isel(eta3=phi_volume.sizes[\"eta3\"] // 2).plot(x=\"eta1\", y=\"eta2\")\n", "\n", "# Without eta arguments the spline is evaluated at the cell centres of the simulation grid,\n", "# here 16 x 1 x 1 cells, so this run's grid gives a line along e1.\n", @@ -392,7 +392,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" + "phase_space.isel(t=-1).plot(x=\"eta1\", y=\"v1\")" ] }, { @@ -457,7 +457,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_space.isel(t=-1).plot(x=\"e1\", y=\"v1\")" + "phase_space.isel(t=-1).plot(x=\"eta1\", y=\"v1\")" ] }, { @@ -476,7 +476,7 @@ "outputs": [], "source": [ "phase_space.isel(t=np.linspace(0, phase_space.sizes[\"t\"] - 1, 5, dtype=int)).plot(\n", - " x=\"e1\", y=\"v1\", col=\"t\", col_wrap=5\n", + " x=\"eta1\", y=\"v1\", col=\"t\", col_wrap=5\n", ")" ] }, @@ -498,7 +498,7 @@ "outputs": [], "source": [ "final_phase_space = phase_space.isel(t=-1)\n", - "final_phase_space.plot(x=\"e1\", y=\"v1\")" + "final_phase_space.plot(x=\"eta1\", y=\"v1\")" ] }, { @@ -535,7 +535,7 @@ "metadata": {}, "outputs": [], "source": [ - "phase_space.isel(t=[0, -1]).plot(x=\"e1\", y=\"v1\", col=\"t\")" + "phase_space.isel(t=[0, -1]).plot(x=\"eta1\", y=\"v1\", col=\"t\")" ] }, { @@ -546,7 +546,7 @@ "outputs": [], "source": [ "fig, ax = plt.subplots()\n", - "phase_space.isel(t=-1).plot(ax=ax, x=\"e1\", y=\"v1\")\n", + "phase_space.isel(t=-1).plot(ax=ax, x=\"eta1\", y=\"v1\")\n", "fig.savefig(os.path.join(demo_root, \"phase_space_final.png\"), bbox_inches=\"tight\")" ] }, @@ -632,7 +632,7 @@ "outputs": [], "source": [ "# average over whichever logical space directions the binning kept\n", - "spatial = [dim for dim in (\"e1\", \"e2\", \"e3\") if dim in phase_space.dims]\n", + "spatial = [dim for dim in (\"eta1\", \"eta2\", \"eta3\") if dim in phase_space.dims]\n", "f_of_v = phase_space.mean(spatial)\n", "print(f_of_v.dims)\n", "f_of_v.plot(x=\"t\", y=\"v1\")" @@ -683,7 +683,7 @@ "\n", "phase_space_si = out.to_si(phase_space)\n", "print(phase_space_si.v1.attrs[\"units\"], phase_space_si.t.attrs[\"units\"])\n", - "phase_space_si.isel(t=-1).plot(x=\"e1\", y=\"v1\")" + "phase_space_si.isel(t=-1).plot(x=\"eta1\", y=\"v1\")" ] }, { @@ -859,8 +859,8 @@ "metadata": {}, "outputs": [], "source": [ - "density = out_sph.euler_fluid.view_0.n.isel(e2=0, e3=0)\n", - "density.plot(x=\"t\", y=\"e1\")" + "density = out_sph.euler_fluid.view_0.n.isel(eta2=0, eta3=0)\n", + "density.plot(x=\"t\", y=\"eta1\")" ] }, { @@ -878,7 +878,7 @@ "metadata": {}, "outputs": [], "source": [ - "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"e1\")" + "density.isel(t=[0, len(density.t) // 4, len(density.t) // 2]).plot.line(x=\"eta1\")" ] }, { @@ -929,7 +929,7 @@ "metadata": {}, "outputs": [], "source": [ - "out_coaxial.em_fields.b_field_xyz.isel(t=-1, component=2, e3=0).plot(x=\"e1\", y=\"e2\")" + "out_coaxial.em_fields.b_field_xyz.isel(t=-1, component=2, eta3=0).plot(x=\"eta1\", y=\"eta2\")" ] }, { @@ -939,7 +939,7 @@ "metadata": {}, "outputs": [], "source": [ - "out_coaxial.em_fields.b_field_xyz.isel(component=2, e3=0).plot(x=\"e1\", y=\"e2\", col=\"t\", col_wrap=4)" + "out_coaxial.em_fields.b_field_xyz.isel(component=2, eta3=0).plot(x=\"eta1\", y=\"eta2\", col=\"t\", col_wrap=4)" ] }, { diff --git a/tutorials/tutorial_velocity_diffusion_sph.ipynb b/tutorials/tutorial_velocity_diffusion_sph.ipynb index 6fa43d5a4..3e4ae3037 100644 --- a/tutorials/tutorial_velocity_diffusion_sph.ipynb +++ b/tutorials/tutorial_velocity_diffusion_sph.ipynb @@ -267,11 +267,11 @@ "outputs": [], "source": [ "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", - "ee1, ee2, ee3 = np.meshgrid(density.e1, density.e2, density.e3, indexing=\"ij\")\n", + "ee1, ee2, ee3 = np.meshgrid(density.eta1, density.eta2, density.eta3, indexing=\"ij\")\n", "n_sph = np.asarray(density) # shape (Nt+1, plot_pts, 1, 1)\n", "j1_current = out.evaluate(\"euler_fluid/f\", dataset=\"e1_current_1/f\")\n", "j1_binned = np.asarray(j1_current) # shape (Nt+1, n_bins)\n", - "e1_binned = np.asarray(j1_current[\"e1\"]) # logical x in [0, 1]\n", + "e1_binned = np.asarray(j1_current[\"eta1\"]) # logical x in [0, 1]\n", "n_binned = np.asarray(out.evaluate(\"euler_fluid/f\", dataset=\"e1_density/f\")) # shape (Nt+1, n_bins)\n", "\n", "Nt = int(Tend / dt)\n", diff --git a/tutorials/tutorial_viscous_euler_sph.ipynb b/tutorials/tutorial_viscous_euler_sph.ipynb index 80cb5b9bb..ac4c33f3a 100644 --- a/tutorials/tutorial_viscous_euler_sph.ipynb +++ b/tutorials/tutorial_viscous_euler_sph.ipynb @@ -281,7 +281,7 @@ "# Extract particle positions and density\n", "density = out.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", "ee1, ee2, ee3 = xp.meshgrid(\n", - " xp.asarray(density.e1), xp.asarray(density.e2), xp.asarray(density.e3), indexing=\"ij\"\n", + " xp.asarray(density.eta1), xp.asarray(density.eta2), xp.asarray(density.eta3), indexing=\"ij\"\n", ")\n", "n_sph = xp.asarray(density)\n", "\n", @@ -705,11 +705,11 @@ "import matplotlib.pyplot as plt\n", "\n", "density_damp = out_damp.evaluate(\"euler_fluid/n\", dataset=\"view_0/n\")\n", - "ee1, ee2, ee3 = np.meshgrid(density_damp.e1, density_damp.e2, density_damp.e3, indexing=\"ij\")\n", + "ee1, ee2, ee3 = np.meshgrid(density_damp.eta1, density_damp.eta2, density_damp.eta3, indexing=\"ij\")\n", "n_sph = np.asarray(density_damp) # shape (Nt+1, plot_pts, 1, 1)\n", "j1_current_damp = out_damp.evaluate(\"euler_fluid/f\", dataset=\"e1_current_1/f\")\n", "j1_binned = np.asarray(j1_current_damp) # shape (Nt+1, n_bins)\n", - "e1_binned = np.asarray(j1_current_damp[\"e1\"]) # logical x in [0,1]\n", + "e1_binned = np.asarray(j1_current_damp[\"eta1\"]) # logical x in [0,1]\n", "\n", "Nt = j1_binned.shape[0] - 1\n", "times = np.linspace(0.0, Tend, Nt + 1)\n", From cc1643f46f9945b54f0b7114e087d67ad837aadf Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 27 Sep 2026 09:52:11 +0200 Subject: [PATCH 185/193] Mention struphy-plots integration --- README.md | 2 + README.qmd | 2 + doc/markdown/output-api.md | 30 ++++++++++++- doc/sections/quickstart.rst | 5 ++- pyproject.toml | 3 ++ src/struphy/post_processing/output.py | 45 +++++++++++++++++++ .../post_processing/tests/test_output.py | 34 ++++++++++++++ 7 files changed, 118 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6b97b9bcd..b536c5bc7 100755 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ This will create `params_Maxwell.py` in your current working directory (cwd). Yo The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. +For plots and diagnostics of the output, install [struphy-plots](https://struphy-hub.github.io/struphy-plots) (`pip install struphy-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.struphy.plot` on every product. + Parallel simulations are run for example with pip install -U mpi4py diff --git a/README.qmd b/README.qmd index 2d86a1249..9634a2028 100644 --- a/README.qmd +++ b/README.qmd @@ -111,6 +111,8 @@ python params_Maxwell.py The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. +For plots and diagnostics of the output, install [struphy-plots](https://struphy-hub.github.io/struphy-plots) (`pip install struphy-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.struphy.plot` on every product. + Parallel simulations are run for example with ``` diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 484018b56..64c65aa2d 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -78,8 +78,8 @@ distribution = out.evaluate("kinetic_ions/f") delta_f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") ``` -To make a figure, select the dimensions to show and call xarray's native `.plot()` methods (see -[Plot data](#plot-data)). +To make a figure, select the dimensions to show and call xarray's native `.plot()` methods, or +use the plots and diagnostics of the struphy-plots package (see [Plot data](#plot-data)). ## Analyze and report data @@ -218,6 +218,32 @@ rho.isel(eta3=rho.sizes["eta3"] // 2).plot(x="eta1", y="eta2") xarray squeezes size-one dimensions before plotting, so an array that is 2-D on a grid with one cell in some direction plots as a line. Select until the array has the dimensions the plot needs. +### Plots and diagnostics with struphy-plots + +For plots and diagnostics made for Struphy output, install the separate package +[struphy-plots](https://struphy-hub.github.io/struphy-plots): + +```bash +pip install struphy-plots # or: pip install "struphy[plots]" +``` + +When it is installed, every `Output` loads it: no import is needed. It adds `out.plot` and +`out.analysis` for a whole run, and `.struphy.plot`, `.struphy.analysis` and `.struphy.data` on +every product: + +```python +out.plot.energies() # the energy budget of the run +phi = out.evaluate("em_fields/phi") +phi.struphy.plot.slice(coords="physical", plane="XY", t=-1, eta3=0) +phi.struphy.plot.animation(x="eta1", y="eta2", eta3=0) # over time +phi.struphy.analysis.mode_spectrum() # poloidal and toroidal mode numbers +out.kinetic_ions.orbits.struphy.plot.poloidal() # guiding-center orbits +``` + +`help(struphy_plots)` gives an overview of what the package does, and `help()` on any method, +e.g. `help(phi.struphy.plot.slice)`, its parameters. Its guides and full reference are at +. + ## MPI post-processing `Output` always uses `MPI.COMM_WORLD`; no communicator is passed to its constructor. diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 6b57432d0..32c3e83d4 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -76,7 +76,10 @@ For periodic boundary conditions we will stabilize via ``options``. sim = Simulation(model=model, domain=domain, grid=grid) 6. Run the simulation. ``sim.run()`` returns an :class:`~struphy.Output` object, - the entry point for all post-processing. + the entry point for all post-processing. For plots and diagnostics made for Struphy + output (``out.plot``, ``out.analysis`` and ``.struphy.plot`` on every product), install + the separate package `struphy-plots `_ with + ``pip install struphy-plots``; ``Output`` loads it automatically. .. code-block:: python diff --git a/pyproject.toml b/pyproject.toml index 955937c55..697b16c9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,9 @@ phys = [ "gvec>=1.1.0, <=1.5.0", "desc-opt<=0.17.1", ] +plots = [ + "struphy-plots", +] dev = [ "struphy[mpi]", "notebook", diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index a7ad76aca..d779d76df 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -58,6 +58,32 @@ def mpi_comm_world(): return MPI.COMM_WORLD +PLOTS_HINT = ( + "plots and diagnostics of Struphy output come from the struphy-plots package: " + "pip install struphy-plots (or struphy[plots]); see https://struphy-hub.github.io/struphy-plots" +) +_plots = {"loaded": False} + + +def load_struphy_plots() -> bool: + """Load struphy-plots if it is installed, and tell whether it is. + + Importing ``struphy_plots`` registers ``out.plot``, ``out.analysis`` and the ``.struphy`` + accessor on every product. :class:`Output` calls this when it is created, so none of that + needs an explicit ``import struphy_plots``. Struphy does not depend on the package. + """ + if not _plots["loaded"]: + try: + import struphy_plots # noqa: F401 (registers the accessors) + except ImportError: + return False + except Exception as error: # a broken install must not break reading output + logger.warning("struphy-plots is installed but could not be imported: %s", error) + return False + _plots["loaded"] = True + return True + + class ProductMapping(Mapping[str, xr.DataArray]): """A discoverable mapping whose products are loaded on first access.""" @@ -205,6 +231,20 @@ class Output: reconstructed lazily from saved metadata. No simulation object is created or retained. * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. + **Plots and diagnostics** of the output live in the separate package + `struphy-plots `_ (``pip install struphy-plots``, + or ``pip install "struphy[plots]"``). When it is installed, creating an ``Output`` loads it, + which adds: + + * ``out.plot`` and ``out.analysis``: whole-run plots and diagnostics, e.g. + ``out.plot.energies()``, ``out.analysis.time_fft("em_fields/phi")``; + * ``.struphy.plot``, ``.struphy.analysis`` and ``.struphy.data`` on every product, e.g. + ``out.evaluate("em_fields/phi").struphy.plot.slice(x="eta1", y="eta2", t=-1)``, or + ``orbits.struphy.plot.poloidal()`` for an orbits Dataset. + + ``help(struphy_plots)`` gives an overview of the package, and ``help()`` on any accessor + method (e.g. ``help(phi.struphy.plot.slice)``) its parameters. + Parameters ---------- path_out: @@ -212,6 +252,7 @@ class Output: """ def __init__(self, path_out): + load_struphy_plots() # out.plot, out.analysis and .struphy on every product, if installed self.path_out = Path(path_out).resolve() self._time_units = "normalized" self.comm = mpi_comm_world() @@ -2037,6 +2078,10 @@ def __getattr__(self, name: str) -> ProductNamespace: attribute.func(self) if isinstance(attribute, property): attribute.fget(self) # the property raised AttributeError itself; show its own error + if name in ("plot", "analysis"): + if load_struphy_plots() and name in type(self).__dict__: + return getattr(self, name) + raise AttributeError(f"Output has no {name!r} without struphy-plots; {PLOTS_HINT}") # the raw output names the species, so an unknown name never starts post-processing if name not in self._raw_species(): raise AttributeError(f"{name!r}; available species: {tuple(sorted(self._raw_species()))}") diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 47997896e..f9290d606 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -697,3 +697,37 @@ def test_stores_of_schema_version_1_are_read_with_eta_dimensions(tmp_path): assert tree["em_fields"].ds.phi.dims == ("t", "eta1", "eta2") tree.close() assert store.SCHEMA_VERSION == 2 + + +def test_output_loads_struphy_plots_when_it_is_installed(tmp_path): + """Creating an Output registers out.plot and the .struphy accessor, without an explicit import.""" + pytest.importorskip("struphy_plots") + import subprocess + import sys + + path = write_tree(str(tmp_path)) + script = ( + "import xarray as xr\n" + "from struphy.post_processing.output import Output\n" + "assert not hasattr(xr.DataArray, 'struphy'), 'struphy_plots was imported before Output()'\n" + f"out = Output({path!r})\n" + "assert hasattr(xr.DataArray, 'struphy') and hasattr(xr.Dataset, 'struphy')\n" + "assert type(out.plot).__name__ == 'OutputPlots' and type(out.analysis).__name__ == 'OutputAnalysis'\n" + ) + result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + +def test_plot_without_struphy_plots_says_how_to_get_it(run, monkeypatch): + import sys + + monkeypatch.setitem(sys.modules, "struphy_plots", None) # as if it were not installed + monkeypatch.setitem(output_module._plots, "loaded", False) + for name in ("plot", "analysis"): + if name in Output.__dict__: # registered by an earlier import in this session + monkeypatch.delattr(Output, name) + for name in ("plot", "analysis"): + with pytest.raises(AttributeError, match="pip install struphy-plots"): + getattr(run, name) + with pytest.raises(AttributeError, match="available species"): + run.not_a_species # other names keep their own error From eadc733d4db1c698d261f28c918d1f2006e999c2 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 27 Sep 2026 10:02:00 +0200 Subject: [PATCH 186/193] Updated docstrings --- doc/markdown/output-api.md | 2 +- src/struphy/post_processing/output.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 64c65aa2d..813e6c815 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -240,7 +240,7 @@ phi.struphy.analysis.mode_spectrum() # poloidal and t out.kinetic_ions.orbits.struphy.plot.poloidal() # guiding-center orbits ``` -`help(struphy_plots)` gives an overview of what the package does, and `help()` on any method, +`import struphy_plots; help(struphy_plots)` gives an overview of what the package does, and `help()` on any method, e.g. `help(phi.struphy.plot.slice)`, its parameters. Its guides and full reference are at . diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index d779d76df..dcde10543 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -242,8 +242,8 @@ class Output: ``out.evaluate("em_fields/phi").struphy.plot.slice(x="eta1", y="eta2", t=-1)``, or ``orbits.struphy.plot.poloidal()`` for an orbits Dataset. - ``help(struphy_plots)`` gives an overview of the package, and ``help()`` on any accessor - method (e.g. ``help(phi.struphy.plot.slice)``) its parameters. + ``import struphy_plots; help(struphy_plots)`` gives an overview of the package, and ``help()`` + on any accessor method (e.g. ``help(phi.struphy.plot.slice)``) its parameters. Parameters ---------- From f0860a391e04cf43bd8ed2ff233592a4352edafd Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 27 Sep 2026 15:03:15 +0200 Subject: [PATCH 187/193] Enable evaluate() without pproc() under mpirun --- doc/markdown/output-api.md | 23 +++-- doc/sections/userguide.rst | 10 +-- src/struphy/post_processing/manifest.py | 53 +++++++++++- src/struphy/post_processing/output.py | 60 ++++++++----- .../post_processing/tests/test_output.py | 84 ++++++++++++++++--- .../post_processing/tests/test_pproc.py | 25 ++++++ 6 files changed, 207 insertions(+), 48 deletions(-) diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 813e6c815..b6a7b5a46 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -248,8 +248,19 @@ e.g. `help(phi.struphy.plot.slice)`, its parameters. Its guides and full referen `Output` always uses `MPI.COMM_WORLD`; no communicator is passed to its constructor. -For serial post-processing under MPI, call `pproc()` on every rank. Rank 0 does the work and the -other ranks wait at the synchronization barrier. +Products are processed on first use under MPI too, so a script needs no `pproc()` call: + +```python +out = Output(path) +phi = out.evaluate("em_fields/phi") # works on any number of ranks +``` + +The first rank (or process) that needs products processes the run serially, holding a lock file +`.post_processing.lock` in the run folder; any other rank that needs them waits for the lock and +then loads them. Nothing is collective, so ranks that never ask for products are never waited for. + +To choose processing options, call `pproc()` on every rank. Rank 0 does the work and the other +ranks wait at the synchronization barrier. ```python out.pproc(physical=True) @@ -262,12 +273,8 @@ world communicator must have the same number of ranks as the run that wrote the out.pproc(parallel=True, physical=True) ``` -Automatic materialization through `evaluate()` is disabled by default when more than one MPI rank -is active, because `pproc()` is collective (rank 0 works while the rest wait at a barrier) and -`evaluate()` is not otherwise guaranteed to be called on every rank; auto-triggering it could hang -ranks that never reach the call instead of failing fast. Call `pproc()` explicitly first in that -case, or pass `parallel=True` to `evaluate()` when calling it collectively on every rank; this -triggers `pproc(parallel=True)` automatically on first use. +To process in parallel on first use, pass `parallel=True` to `evaluate()` and call it on every +rank; this triggers `pproc(parallel=True)`. ```python out.evaluate("em_fields/E", parallel=True) diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 585d47162..48a366725 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -510,9 +510,9 @@ moved to another location: This reads the ``run_metadata.json`` written by the simulation. Products are materialized with :meth:`~struphy.Output.pproc` on first access, using default -options; call ``pproc`` explicitly first to choose different options (see -below), and under MPI call it on every rank, since serial processing runs on -rank 0 while the other ranks wait. +options; call ``pproc`` explicitly first only to choose different options (see +below). This works under MPI as well: the first rank that needs products +processes the run, and the other ranks that need them wait for it. The output of a simulation is a :class:`~struphy.Output`. ``sim.run()`` returns it, and it stays available as ``sim.output``: @@ -573,8 +573,8 @@ choose the options, call ``pproc`` first: All arguments are optional and default to the values shown above. Products that were already made from the same raw output with the same options are reused, so a -plotting script can be re-run cheaply. Under MPI, call ``pproc`` on every rank: -serial processing runs on rank 0 while the other ranks wait, and +plotting script can be re-run cheaply. An explicit ``pproc`` under MPI is called +on every rank: serial processing runs on rank 0 while the other ranks wait, and ``parallel=True`` uses the allocated simulation on all ranks. diff --git a/src/struphy/post_processing/manifest.py b/src/struphy/post_processing/manifest.py index 12e1721d3..536e1be1e 100644 --- a/src/struphy/post_processing/manifest.py +++ b/src/struphy/post_processing/manifest.py @@ -1,10 +1,18 @@ -"""Manifest fingerprints and processing option comparison.""" +"""Manifest fingerprints, processing option comparison and the processing lock.""" import hashlib import json import os +import time +from contextlib import contextmanager + +try: + import fcntl +except ImportError: # Windows + fcntl = None MANIFEST_SCHEMA_VERSION = 1 +LOCK_NAME = ".post_processing.lock" def source_fingerprint(path_out: str) -> str: @@ -49,3 +57,46 @@ def is_processed(path_out: str, options: dict | None = None) -> bool: and manifest.get("source_fingerprint") == source_fingerprint(path_out) and (options is None or manifest.get("options") == normalize_options(**options)) ) + + +@contextmanager +def processing_lock(path_out: str, *, poll: float = 0.2): + """Hold the right to post-process ``path_out``, waiting while another process holds it. + + Ranks of one MPI job, or separate scripts, may start processing the same run at once; + they take turns here instead of meeting at a collective, so a rank that never asks for + products is never waited for. The lock is a POSIX record lock on a file next to the + products, which the operating system releases if its holder dies. File systems without + such locks (some Lustre or NFS mounts) fall back to creating the file exclusively; a + process killed while holding that leaves the file behind, and it must be removed by hand. + """ + path = os.path.join(path_out, LOCK_NAME) + with open(path, "a") as stream: + if _record_lock(stream): + try: + yield + finally: + fcntl.lockf(stream, fcntl.LOCK_UN) + return + held = path + ".held" + while True: + try: + os.close(os.open(held, os.O_CREAT | os.O_EXCL | os.O_WRONLY)) + break + except FileExistsError: + time.sleep(poll) + try: + yield + finally: + os.remove(held) + + +def _record_lock(stream) -> bool: + """Take an exclusive POSIX record lock on ``stream``, blocking; False where unsupported.""" + if fcntl is None: + return False + try: + fcntl.lockf(stream, fcntl.LOCK_EX) + except OSError: + return False + return True diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index dcde10543..2e70a75ad 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -39,6 +39,7 @@ MANIFEST_SCHEMA_VERSION, is_processed, normalize_options, + processing_lock, source_fingerprint, ) from struphy.post_processing.orbits import orbits_tools @@ -341,10 +342,10 @@ def evaluate( analysis. ``evaluate("scalars")`` returns an :class:`xarray.Dataset` containing all scalar histories; use ``variables=`` to select scalar names. - Under more than one MPI rank, automatic materialization is disabled unless ``parallel`` - is set: pass ``parallel=True`` only when calling ``evaluate()`` collectively on every - rank, which triggers :meth:`pproc` with ``parallel=True`` on first use. Otherwise call - :meth:`pproc` explicitly first. + This works on any number of MPI ranks, and need not be called on all of them: the first + rank that needs products processes the run serially, and any other rank that needs them + waits for it. Pass ``parallel=True`` to process with every rank instead; then call + ``evaluate()`` on every rank, with as many ranks as the saved run. Common selections can be passed directly: ``t`` selects saved snapshots by index (an integer, list of integers, or slice); omit it for every @@ -963,9 +964,10 @@ def pproc( ) -> "Output": """Materialize post-processed products; reuse matching existing products. - Call this on every MPI rank. Serial processing (the default) runs on rank 0 while - the other ranks wait. Parallel processing reconstructs the field decomposition - and requires the same number of ranks as the saved run. + Products are processed on first use with default options, so call this only to choose + other options. Under MPI, call it on every rank: serial processing (the default) runs on + rank 0 while the other ranks wait. Parallel processing reconstructs the field + decomposition and requires the same number of ranks as the saved run. Parameters ---------- @@ -1000,12 +1002,23 @@ def pproc( create_vtk=create_vtk, force=force, ) + if parallel: + self._process(parallel=True, **options) + return self try: - if parallel or self.comm.Get_rank() == 0: - self._setup_processing(parallel) - self._process_raw(**options) - if not parallel: - self.comm.Barrier() + if self.comm.Get_rank() == 0: + with processing_lock(str(self.path_out)): + self._process(parallel=False, **options) + self.comm.Barrier() + finally: + self._reset() # the other ranks load the new products + return self + + def _process(self, *, parallel: bool, **options): + """Run one processing pass on this rank (serial) or with every rank (parallel).""" + try: + self._setup_processing(parallel) + self._process_raw(**options) finally: self._reset() for name in ( @@ -1022,7 +1035,6 @@ def pproc( "_collect_recv_bufs", ): self.__dict__.pop(name, None) - return self def _setup_processing(self, parallel: bool): """Prepare the communicator and FEEC reconstruction for this processing run.""" @@ -2023,16 +2035,18 @@ def _post_process_n_sph( def _ensure_processed(self): if self.is_processed: return - if self.comm.Get_size() > 1: - # pproc() is collective (rank 0 works while the rest wait at a Barrier), but evaluate() - # is not guaranteed to be called on every rank; auto-triggering pproc() here could hang - # ranks that never reach this call instead of failing fast. - raise RuntimeError(f"{self.path_out} has no post-processed data; call out.pproc() on all ranks first") - logger.warning( - "\nNo post-processed data in %s, processing with default options (call out.pproc(...) to choose them)", - self.path_out, - ) - self.pproc() + # Not collective: products may be asked for on some ranks only, or in another order. The + # first process to get here processes the run serially; the others that need products + # wait for it at the lock, then find them complete. + with processing_lock(str(self.path_out)): + if self.is_processed: + self._reset() # another process has just written the products + return + logger.warning( + "\nNo post-processed data in %s, processing with default options (call out.pproc(...) to choose them)", + self.path_out, + ) + self._process(parallel=False, create_vtk=False) def _product_mappings(self) -> dict[str, ProductMapping]: if self._products is None: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index f9290d606..21e8a59c7 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -258,21 +258,19 @@ def forbidden(*args, **kwargs): def test_evaluate_triggers_default_processing_when_missing(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - # Automatic processing is serial only; the multi-rank refusal is tested separately. run = output_with_comm(monkeypatch, root, FakeComm()) calls = [] - def fake_pproc(self, **options): - calls.append(options) + def fake_process(self, *, parallel, **options): + calls.append(dict(parallel=parallel, **options)) write_manifest(root) self._reset() - return self - monkeypatch.setattr(Output, "pproc", fake_pproc) + monkeypatch.setattr(Output, "_process", fake_process) assert set(run.scalars.data_vars) == {"en_tot"} assert calls == [], "scalars come from the raw output" assert run.evaluate("em_fields/E").name == "E" - assert calls == [{}] + assert calls == [dict(parallel=False, create_vtk=False)] def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): @@ -514,12 +512,66 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): assert result.dims == ("t", "component", "eta1", "eta2") -def test_products_refuse_implicit_processing_on_many_ranks(tmp_path, monkeypatch): +@pytest.mark.parametrize("rank", [0, 1]) +def test_products_are_processed_on_first_use_on_any_rank(tmp_path, monkeypatch, rank): + root = write_tree(str(tmp_path)) + os.remove(os.path.join(root, "post_processing", "manifest.json")) + calls = [] + monkeypatch.setattr(Output, "_setup_processing", lambda self, parallel: calls.append(parallel)) + monkeypatch.setattr( + Output, "_process_raw", lambda self, **options: calls.append(options) or write_manifest(root, **options) + ) + comm = FakeComm(rank=rank, size=2) + assert tuple(output_with_comm(monkeypatch, root, comm).fields) == ("em_fields",) + assert calls == [False, dict(create_vtk=False)] + assert comm.barriers == 0 # never collective: other ranks need not ask for products + + +def test_products_written_while_waiting_for_the_lock_are_not_processed_again(tmp_path, monkeypatch): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - comm = FakeComm(size=2) - with pytest.raises(RuntimeError, match="on all ranks"): - output_with_comm(monkeypatch, root, comm).fields + run = output_with_comm(monkeypatch, root, FakeComm(rank=1, size=2)) + lock = output_module.processing_lock + + def lock_then_other_process_finishes(path_out): + write_manifest(root) + return lock(path_out) + + monkeypatch.setattr(output_module, "processing_lock", lock_then_other_process_finishes) + monkeypatch.setattr(Output, "_process_raw", lambda self, **options: pytest.fail("processed twice")) + assert tuple(run.fields) == ("em_fields",) + + +def test_processing_lock_is_exclusive_between_processes(tmp_path): + import multiprocessing + + from struphy.post_processing.manifest import processing_lock + + log = tmp_path / "log" + context = multiprocessing.get_context("spawn") + workers = [context.Process(target=_hold_lock, args=(str(tmp_path), str(log))) for _ in range(3)] + for worker in workers: + worker.start() + for worker in workers: + worker.join(60) + assert worker.exitcode == 0 + lines = log.read_text().split() + assert lines == ["enter", "exit"] * 3 # never two holders at once + with processing_lock(str(tmp_path)): + pass + + +def _hold_lock(path_out, log): + import time + + from struphy.post_processing.manifest import processing_lock + + with processing_lock(path_out): + with open(log, "a") as stream: + stream.write("enter\n") + time.sleep(0.3) + with open(log, "a") as stream: + stream.write("exit\n") def test_processing_options_are_part_of_the_manifest(tmp_path): @@ -665,7 +717,7 @@ def test_saved_rank_count_does_not_block_serial_implicit_processing(tmp_path, mo run.metadata["mpi_ranks"] = 8 (run.path_pproc / "manifest.json").unlink() calls = [] - monkeypatch.setattr(Output, "pproc", lambda self: calls.append(self.path_out)) + monkeypatch.setattr(Output, "_process", lambda self, **options: calls.append(self.path_out)) run._ensure_processed() assert calls == [run.path_out] @@ -731,3 +783,13 @@ def test_plot_without_struphy_plots_says_how_to_get_it(run, monkeypatch): getattr(run, name) with pytest.raises(AttributeError, match="available species"): run.not_a_species # other names keep their own error + + +def test_processing_lock_falls_back_to_an_exclusive_file(tmp_path, monkeypatch): + from struphy.post_processing import manifest + + monkeypatch.setattr(manifest, "fcntl", None) + held = tmp_path / (manifest.LOCK_NAME + ".held") + with manifest.processing_lock(str(tmp_path)): + assert held.exists() + assert not held.exists() diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index 540f23e05..cadc581dd 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -69,5 +69,30 @@ def do_plotting(run: Output, from_parallel=False): MPI.COMM_WORLD.Barrier() +@pytest.mark.mpi(min_size=2) +def test_products_are_processed_on_first_use_under_mpi(): + """Without pproc(), a rank that needs products processes them, and never waits for the others.""" + import shutil + + comm = MPI.COMM_WORLD + rank, last = comm.Get_rank(), comm.Get_size() - 1 + test_mod = import_parameters_py(str(PARAMS_PATH), name="weak_Landau_damping") + sim: Simulation = test_mod.test_weak_Landau(do_plot=False, exit_before_run=True) + sim.run(one_time_step=True) + if rank == 0: + shutil.rmtree(Path(sim.env.path_out) / "post_processing", ignore_errors=True) + comm.Barrier() + + # Only the last rank asks; rank 0 goes on without it and would hang at a collective. + run = Output(sim.env.path_out) + f = np.asarray(run.evaluate("kinetic_ions/f")) if rank == last else None + f = comm.bcast(f, root=last) + assert run.is_processed + + # Every rank now loads the same products. + assert np.array_equal(np.asarray(Output(sim.env.path_out).evaluate("kinetic_ions/f")), f) + comm.Barrier() + + if __name__ == "__main__": test_pproc_mpi(show_plot=True) From 5c2c536aad5dbada0578cfe102b7b33bb6399f45 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 27 Sep 2026 15:05:27 +0200 Subject: [PATCH 188/193] evaluate() now accepts species/dataset/variable names --- src/struphy/post_processing/output.py | 12 +++++++++++- src/struphy/post_processing/tests/test_output.py | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index dcde10543..73a70b5ac 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -354,6 +354,8 @@ def evaluate( products, a ``"species/variable"`` name selects the first matching binned product, then density/KDE product, then orbits. Pass ``dataset=`` to select a particular discovered product; use ``out.info("species/variable")`` to list them. + The full key of a particle product, ``"species/dataset/variable"`` (as :meth:`keys` + lists it, e.g. ``"kinetic_ions/e1_v1_density/f"``), selects that product directly. Supplying an ``eta`` evaluates a raw FEEC spline field directly on that logical grid. Each eta can be a scalar, a list, a one-dimensional array, or a ``range``; @@ -372,8 +374,16 @@ def evaluate( raise TypeError("physical is no longer supported; use eta1, eta2, eta3 and representation") if "as_numpy" in selectors: raise TypeError("evaluate() always returns xarray; call .to_numpy() on its result when needed") + if name.count("/") == 2: + # the full key of a particle product: "species/dataset/variable" + if dataset is not None: + raise ValueError("dataset= cannot be combined with a 'species/dataset/variable' name") + species, group, variable = name.split("/") + name, dataset = f"{species}/{variable}", f"{group}/{variable}" if name != "scalars" and name.count("/") != 1: - raise ValueError("evaluate() names must use the 'species/variable' form, or be 'scalars'") + raise ValueError( + "evaluate() names must use the 'species/variable' or 'species/dataset/variable' form, or be 'scalars'" + ) eta = (eta1, eta2, eta3) has_eta = any(value is not None for value in eta) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index f9290d606..85790b3bc 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -330,6 +330,18 @@ def test_evaluate_scalars_and_particle_defaults(run): selected = run.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") assert selected.name == "delta_f" + # the full key selects the same product, with selections as keywords + by_key = run.evaluate("kinetic_ions/e1_v1_density/delta_f") + xr.testing.assert_identical(by_key, selected) + first = float(by_key.eta1[0]) + xr.testing.assert_identical( + run.evaluate("kinetic_ions/e1_v1_density/delta_f", eta1=first), selected.sel(eta1=first) + ) + with pytest.raises(ValueError, match="dataset="): + run.evaluate("kinetic_ions/e1_v1_density/f", dataset="e1_v1_density/f") + with pytest.raises(KeyError): + run.evaluate("kinetic_ions/no_such_density/f") + orbits = run.evaluate("kinetic_ions/orbits") assert isinstance(orbits, xr.Dataset) and "weight" in orbits.data_vars From 8ba9fcb2d6d7944b445658bffdc42dccf54ac5ca Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 27 Sep 2026 16:10:13 +0200 Subject: [PATCH 189/193] Enable parallel processing in evaluate() --- .gitignore | 3 + doc/markdown/output-api.md | 33 ++++------- doc/sections/userguide.rst | 14 +++-- src/struphy/post_processing/output.py | 59 ++++++++++--------- .../post_processing/tests/test_output.py | 55 ++++++++++------- .../post_processing/tests/test_pproc.py | 32 +++++----- 6 files changed, 106 insertions(+), 90 deletions(-) diff --git a/.gitignore b/.gitignore index 136d46db9..323e1bcc4 100644 --- a/.gitignore +++ b/.gitignore @@ -73,12 +73,15 @@ __epyccel__/ *.h5py *.vts *acquisition.lock +.post_processing.lock* .testmondata* .output* # Default simulations and diagnostics my_diagnostics my_struphy_sims +struphy_verification_tests/ +struphy_examples/ /diagnostics.sh /run_STRUPHY*.sh logs diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index b6a7b5a46..bca4799ec 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -251,31 +251,22 @@ e.g. `help(phi.struphy.plot.slice)`, its parameters. Its guides and full referen Products are processed on first use under MPI too, so a script needs no `pproc()` call: ```python -out = Output(path) -phi = out.evaluate("em_fields/phi") # works on any number of ranks +out = sim.run() # or Output(path) +f = out.evaluate("kinetic_ions/f") # on every rank ``` -The first rank (or process) that needs products processes the run serially, holding a lock file -`.post_processing.lock` in the run folder; any other rank that needs them waits for the lock and -then loads them. Nothing is collective, so ranks that never ask for products are never waited for. +Processing is collective: while the run is not processed yet, ask for products on every rank. +When the job has as many ranks as the simulation, it runs in parallel: each rank reads its own raw +file and evaluates its part of the domain, and rank 0 gathers the products and writes them. +Otherwise rank 0 processes the whole run while the other ranks wait. Once processed, products are +read on any rank independently. -To choose processing options, call `pproc()` on every rank. Rank 0 does the work and the other -ranks wait at the synchronization barrier. +`pproc()` chooses processing options, and is called on every rank as well: ```python -out.pproc(physical=True) +out.pproc(physical=True) # parallel when the job is as large as the run +out.pproc(physical=True, parallel=False) # serial on rank 0 ``` -For parallel post-processing, also call it on every rank and pass `parallel=True`. The current -world communicator must have the same number of ranks as the run that wrote the raw output. - -```python -out.pproc(parallel=True, physical=True) -``` - -To process in parallel on first use, pass `parallel=True` to `evaluate()` and call it on every -rank; this triggers `pproc(parallel=True)`. - -```python -out.evaluate("em_fields/E", parallel=True) -``` +`parallel=True` forces parallel processing, which fails unless the job has as many ranks as the +run. `evaluate(..., parallel=...)` passes it on when it triggers processing. diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 48a366725..3c012b97f 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -511,8 +511,9 @@ moved to another location: This reads the ``run_metadata.json`` written by the simulation. Products are materialized with :meth:`~struphy.Output.pproc` on first access, using default options; call ``pproc`` explicitly first only to choose different options (see -below). This works under MPI as well: the first rank that needs products -processes the run, and the other ranks that need them wait for it. +below). Under MPI, processing is collective, so access products on every rank: +it runs in parallel when the job has as many ranks as the simulation, and +otherwise on rank 0 while the other ranks wait. The output of a simulation is a :class:`~struphy.Output`. ``sim.run()`` returns it, and it stays available as ``sim.output``: @@ -567,15 +568,16 @@ choose the options, call ``pproc`` first: guiding_center=False, # compute guiding-center coordinates for markers classify=False, # classify particles by trapping/passing etc. create_vtk=False, # write VTK files for 3D visualization - parallel=False, # evaluate fields on all MPI ranks + parallel=None, # all MPI ranks; by default when as many as the run's force=False, # reprocess even if matching products exist ) All arguments are optional and default to the values shown above. Products that were already made from the same raw output with the same options are reused, so a -plotting script can be re-run cheaply. An explicit ``pproc`` under MPI is called -on every rank: serial processing runs on rank 0 while the other ranks wait, and -``parallel=True`` uses the allocated simulation on all ranks. +plotting script can be re-run cheaply. Under MPI, call ``pproc`` on every rank: +parallel processing evaluates each rank's part of the simulation and gathers the +products on rank 0, which writes them; serial processing runs on rank 0 while the +other ranks wait. Standard plots and analysis diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 2e70a75ad..275b51409 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -331,7 +331,7 @@ def evaluate( representation: Representation | None = None, dataset: str | None = None, variables: str | Sequence[str] | None = None, - parallel: bool = False, + parallel: bool | None = None, **coordinates: Any, ) -> xr.DataArray | xr.Dataset: """Return a named simulation product as an xarray object. @@ -342,10 +342,10 @@ def evaluate( analysis. ``evaluate("scalars")`` returns an :class:`xarray.Dataset` containing all scalar histories; use ``variables=`` to select scalar names. - This works on any number of MPI ranks, and need not be called on all of them: the first - rank that needs products processes the run serially, and any other rank that needs them - waits for it. Pass ``parallel=True`` to process with every rank instead; then call - ``evaluate()`` on every rank, with as many ranks as the saved run. + Under MPI, call ``evaluate()`` on every rank as long as the run is not processed yet: + processing is collective. It runs in parallel when the job has as many ranks as the saved + run, and otherwise serially on rank 0 while the other ranks wait; ``parallel`` forces + either, see :meth:`pproc`. Common selections can be passed directly: ``t`` selects saved snapshots by index (an integer, list of integers, or slice); omit it for every @@ -418,8 +418,8 @@ def evaluate( if representation is not None and not is_raw_spline_field: raise ValueError("representation requires FEEC evaluation") if not is_raw_spline_field and name != "scalars": - if parallel and not self.is_processed: - self.pproc(parallel=True) + if parallel is not None and not self.is_processed: + self.pproc(parallel=parallel) array = self._product(name, dataset=dataset) if t is not None: if isinstance(t, (int, np.integer)): @@ -959,15 +959,15 @@ def pproc( guiding_center: bool = False, classify: bool = False, create_vtk: bool = False, - parallel: bool = False, + parallel: bool | None = None, force: bool = False, ) -> "Output": """Materialize post-processed products; reuse matching existing products. Products are processed on first use with default options, so call this only to choose - other options. Under MPI, call it on every rank: serial processing (the default) runs on - rank 0 while the other ranks wait. Parallel processing reconstructs the field - decomposition and requires the same number of ranks as the saved run. + other options. Under MPI, call it on every rank. Parallel processing reconstructs the + field decomposition of the saved run on as many ranks, and rank 0 gathers and writes the + products; serial processing runs on rank 0 while the other ranks wait. Parameters ---------- @@ -984,7 +984,8 @@ def pproc( create_vtk: Also write VTK files of the fields. parallel: - Evaluate fields on all ranks of this output's communicator. + Process on all ranks of this output's communicator, which must have as many ranks as + the saved run. By default parallel exactly when it has, and more than one. force: Reprocess even when matching products exist. @@ -1002,18 +1003,26 @@ def pproc( create_vtk=create_vtk, force=force, ) - if parallel: - self._process(parallel=True, **options) - return self + if parallel is None: + parallel = self._processes_in_parallel try: - if self.comm.Get_rank() == 0: + if parallel: + self._process(parallel=True, **options) + elif self.comm.Get_rank() == 0: with processing_lock(str(self.path_out)): self._process(parallel=False, **options) + # Rank 0 writes the manifest last; no rank may look for products before it has. self.comm.Barrier() finally: self._reset() # the other ranks load the new products return self + @property + def _processes_in_parallel(self) -> bool: + """Whether processing by default uses every rank: those of a job as large as the saved run.""" + size = self.comm.Get_size() + return size > 1 and size == self.mpi_ranks + def _process(self, *, parallel: bool, **options): """Run one processing pass on this rank (serial) or with every rank (parallel).""" try: @@ -2035,18 +2044,12 @@ def _post_process_n_sph( def _ensure_processed(self): if self.is_processed: return - # Not collective: products may be asked for on some ranks only, or in another order. The - # first process to get here processes the run serially; the others that need products - # wait for it at the lock, then find them complete. - with processing_lock(str(self.path_out)): - if self.is_processed: - self._reset() # another process has just written the products - return - logger.warning( - "\nNo post-processed data in %s, processing with default options (call out.pproc(...) to choose them)", - self.path_out, - ) - self._process(parallel=False, create_vtk=False) + logger.warning( + "\nNo post-processed data in %s, processing with default options (call out.pproc(...) to choose them)", + self.path_out, + ) + # Collective under MPI, in parallel when the job is as large as the saved run. + self.pproc() def _product_mappings(self) -> dict[str, ProductMapping]: if self._products is None: diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 21e8a59c7..d67c9872a 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -270,7 +270,18 @@ def fake_process(self, *, parallel, **options): assert set(run.scalars.data_vars) == {"en_tot"} assert calls == [], "scalars come from the raw output" assert run.evaluate("em_fields/E").name == "E" - assert calls == [dict(parallel=False, create_vtk=False)] + assert calls == [ + dict( + parallel=False, + step=1, + celldivide=1, + physical=False, + guiding_center=False, + classify=False, + create_vtk=False, + force=False, + ) + ] def test_evaluate_returns_xarray_and_xarray_exposes_the_product_tree(run): @@ -512,34 +523,36 @@ def __call__(self, eta1, eta2, eta3, *, squeeze_out=False): assert result.dims == ("t", "component", "eta1", "eta2") -@pytest.mark.parametrize("rank", [0, 1]) -def test_products_are_processed_on_first_use_on_any_rank(tmp_path, monkeypatch, rank): +@pytest.mark.parametrize( + "size, saved_ranks, parallel", [(1, 1, False), (2, 2, True), (4, 4, True), (2, 1, False), (2, 4, False)] +) +def test_first_use_processes_in_parallel_when_the_job_is_as_large_as_the_run( + tmp_path, monkeypatch, size, saved_ranks, parallel +): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) calls = [] monkeypatch.setattr(Output, "_setup_processing", lambda self, parallel: calls.append(parallel)) - monkeypatch.setattr( - Output, "_process_raw", lambda self, **options: calls.append(options) or write_manifest(root, **options) - ) - comm = FakeComm(rank=rank, size=2) - assert tuple(output_with_comm(monkeypatch, root, comm).fields) == ("em_fields",) - assert calls == [False, dict(create_vtk=False)] - assert comm.barriers == 0 # never collective: other ranks need not ask for products + monkeypatch.setattr(Output, "_process_raw", lambda self, **options: write_manifest(root)) + comm = FakeComm(size=size) + run = output_with_comm(monkeypatch, root, comm) + run.metadata["mpi_ranks"] = saved_ranks + assert tuple(run.fields) == ("em_fields",) + assert calls == [parallel] + assert comm.barriers == 1 # no rank looks for products before rank 0 has written them -def test_products_written_while_waiting_for_the_lock_are_not_processed_again(tmp_path, monkeypatch): +@pytest.mark.parametrize("parallel", [True, False]) +def test_evaluate_passes_parallel_on_to_processing(tmp_path, monkeypatch, parallel): root = write_tree(str(tmp_path)) os.remove(os.path.join(root, "post_processing", "manifest.json")) - run = output_with_comm(monkeypatch, root, FakeComm(rank=1, size=2)) - lock = output_module.processing_lock - - def lock_then_other_process_finishes(path_out): - write_manifest(root) - return lock(path_out) - - monkeypatch.setattr(output_module, "processing_lock", lock_then_other_process_finishes) - monkeypatch.setattr(Output, "_process_raw", lambda self, **options: pytest.fail("processed twice")) - assert tuple(run.fields) == ("em_fields",) + calls = [] + monkeypatch.setattr(Output, "_setup_processing", lambda self, parallel: calls.append(parallel)) + monkeypatch.setattr(Output, "_process_raw", lambda self, **options: write_manifest(root)) + run = output_with_comm(monkeypatch, root, FakeComm(size=2)) + run.metadata["mpi_ranks"] = 2 + assert run.evaluate("em_fields/E", parallel=parallel).name == "E" + assert calls == [parallel] def test_processing_lock_is_exclusive_between_processes(tmp_path): diff --git a/src/struphy/post_processing/tests/test_pproc.py b/src/struphy/post_processing/tests/test_pproc.py index cadc581dd..87ad790e8 100644 --- a/src/struphy/post_processing/tests/test_pproc.py +++ b/src/struphy/post_processing/tests/test_pproc.py @@ -50,7 +50,7 @@ def do_plotting(run: Output, from_parallel=False): run = Output(sim.env.path_out) # serial pproc - run.pproc(create_vtk=True) + run.pproc(create_vtk=True, parallel=False) if sim.rank == 0: serial = do_plotting(run) @@ -70,27 +70,31 @@ def do_plotting(run: Output, from_parallel=False): @pytest.mark.mpi(min_size=2) -def test_products_are_processed_on_first_use_under_mpi(): - """Without pproc(), a rank that needs products processes them, and never waits for the others.""" +def test_products_are_processed_in_parallel_on_first_use(): + """Without pproc(), evaluate() processes on every rank and gives the serial products.""" import shutil comm = MPI.COMM_WORLD - rank, last = comm.Get_rank(), comm.Get_size() - 1 test_mod = import_parameters_py(str(PARAMS_PATH), name="weak_Landau_damping") sim: Simulation = test_mod.test_weak_Landau(do_plot=False, exit_before_run=True) - sim.run(one_time_step=True) - if rank == 0: + out = sim.run(one_time_step=True) + if comm.Get_rank() == 0: shutil.rmtree(Path(sim.env.path_out) / "post_processing", ignore_errors=True) comm.Barrier() - # Only the last rank asks; rank 0 goes on without it and would hang at a collective. - run = Output(sim.env.path_out) - f = np.asarray(run.evaluate("kinetic_ions/f")) if rank == last else None - f = comm.bcast(f, root=last) - assert run.is_processed - - # Every rank now loads the same products. - assert np.array_equal(np.asarray(Output(sim.env.path_out).evaluate("kinetic_ions/f")), f) + modes = [] + setup = Output._setup_processing + Output._setup_processing = lambda self, parallel: modes.append(parallel) or setup(self, parallel) + try: + f = np.asarray(out.evaluate("kinetic_ions/f")) + e = np.asarray(out.fields.em_fields.e_field) + finally: + Output._setup_processing = setup + assert modes == [True] + + serial = Output(sim.env.path_out).pproc(parallel=False, force=True) + assert np.allclose(np.asarray(serial.evaluate("kinetic_ions/f")), f) + assert np.allclose(np.asarray(serial.fields.em_fields.e_field), e) comm.Barrier() From a0563b71e7c4acad68daebb9a829326eaaf77121 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 27 Sep 2026 22:12:06 +0200 Subject: [PATCH 190/193] Bugfix for output testing with mpi --- .../post_processing/tests/test_output.py | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index c17b982db..fd8b92230 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -133,6 +133,35 @@ def save_raw_field(run, *names): file["feec/em_fields"].create_dataset(name, data=np.empty(0)) +# Environment prefixes through which MPI launchers tell a process which rank of a job it is. +MPI_LAUNCHER_ENV_PREFIXES = ( + "OMPI_", + "OPAL_", + "PMIX_", + "PMI_", + "PRTE_", + "MV2_", + "HYDRA_", + "I_MPI_", + "MPI_LOCALRANKID", + "ALPS_", + "PALS_", +) + + +@pytest.fixture +def outside_mpi_job(monkeypatch): + """Let child processes start as independent programs, not as ranks of this test's MPI job. + + Importing struphy initializes MPI. A child that inherits the launcher variables of an + ``mpirun`` rank initializes as that same rank, which hangs or corrupts the parent job. + """ + for name in list(os.environ): + if name.startswith(MPI_LAUNCHER_ENV_PREFIXES): + monkeypatch.delenv(name) + monkeypatch.setenv("STRUPHY_MPI", "0") + + @pytest.fixture def run(tmp_path): return Output(write_tree(str(tmp_path))) @@ -573,38 +602,40 @@ def test_evaluate_passes_parallel_on_to_processing(tmp_path, monkeypatch, parall assert calls == [parallel] +# Holds the lock in a separate program that loads only the manifest module: importing +# struphy would initialize MPI in the child, which under mpirun makes it join the test's job. +HOLD_LOCK = """ +import importlib.util, sys, time + +spec = importlib.util.spec_from_file_location("manifest", sys.argv[1]) +manifest = importlib.util.module_from_spec(spec) +spec.loader.exec_module(manifest) +with manifest.processing_lock(sys.argv[2]): + with open(sys.argv[3], "a") as stream: + stream.write("enter\\n") + time.sleep(0.3) + with open(sys.argv[3], "a") as stream: + stream.write("exit\\n") +""" + + def test_processing_lock_is_exclusive_between_processes(tmp_path): - import multiprocessing + import subprocess + import sys - from struphy.post_processing.manifest import processing_lock + from struphy.post_processing import manifest log = tmp_path / "log" - context = multiprocessing.get_context("spawn") - workers = [context.Process(target=_hold_lock, args=(str(tmp_path), str(log))) for _ in range(3)] + command = [sys.executable, "-c", HOLD_LOCK, manifest.__file__, str(tmp_path), str(log)] + workers = [subprocess.Popen(command) for _ in range(3)] for worker in workers: - worker.start() - for worker in workers: - worker.join(60) - assert worker.exitcode == 0 + assert worker.wait(60) == 0 lines = log.read_text().split() assert lines == ["enter", "exit"] * 3 # never two holders at once - with processing_lock(str(tmp_path)): + with manifest.processing_lock(str(tmp_path)): pass -def _hold_lock(path_out, log): - import time - - from struphy.post_processing.manifest import processing_lock - - with processing_lock(path_out): - with open(log, "a") as stream: - stream.write("enter\n") - time.sleep(0.3) - with open(log, "a") as stream: - stream.write("exit\n") - - def test_processing_options_are_part_of_the_manifest(tmp_path): root = write_tree(str(tmp_path)) write_manifest(root, step=1, celldivide=2, physical=False) @@ -782,7 +813,7 @@ def test_stores_of_schema_version_1_are_read_with_eta_dimensions(tmp_path): assert store.SCHEMA_VERSION == 2 -def test_output_loads_struphy_plots_when_it_is_installed(tmp_path): +def test_output_loads_struphy_plots_when_it_is_installed(tmp_path, outside_mpi_job): """Creating an Output registers out.plot and the .struphy accessor, without an explicit import.""" pytest.importorskip("struphy_plots") import subprocess From cc919d095fa7a0db63917fcc31d216cbe2c1815f Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 28 Sep 2026 13:48:01 +0200 Subject: [PATCH 191/193] Rename struphy-plots to plasma-plots --- README.md | 2 +- README.qmd | 2 +- doc/markdown/output-api.md | 24 ++++++------- doc/sections/quickstart.rst | 6 ++-- pyproject.toml | 2 +- src/struphy/post_processing/output.py | 34 +++++++++---------- .../post_processing/tests/test_output.py | 16 ++++----- 7 files changed, 43 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index b536c5bc7..fc477ffbc 100755 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This will create `params_Maxwell.py` in your current working directory (cwd). Yo The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. -For plots and diagnostics of the output, install [struphy-plots](https://struphy-hub.github.io/struphy-plots) (`pip install struphy-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.struphy.plot` on every product. +For plots and diagnostics of the output, install [plasma-plots](https://struphy-hub.github.io/plasma-plots) (`pip install plasma-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.plasma.plot` on every product. Parallel simulations are run for example with diff --git a/README.qmd b/README.qmd index 9634a2028..cf8ceebd8 100644 --- a/README.qmd +++ b/README.qmd @@ -111,7 +111,7 @@ python params_Maxwell.py The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. -For plots and diagnostics of the output, install [struphy-plots](https://struphy-hub.github.io/struphy-plots) (`pip install struphy-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.struphy.plot` on every product. +For plots and diagnostics of the output, install [plasma-plots](https://struphy-hub.github.io/plasma-plots) (`pip install plasma-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.plasma.plot` on every product. Parallel simulations are run for example with diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index ea2b74c26..22f700f34 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -81,7 +81,7 @@ delta_f = out.evaluate("kinetic_ions/f", dataset="e1_v1_density/delta_f") ``` To make a figure, select the dimensions to show and call xarray's native `.plot()` methods, or -use the plots and diagnostics of the struphy-plots package (see [Plot data](#plot-data)). +use the plots and diagnostics of the plasma-plots package (see [Plot data](#plot-data)). ## Analyze and report data @@ -219,31 +219,31 @@ out.evaluate("diagnostics/rho_xyz", t=-1, eta3=0.5, method="nearest").plot(x="et xarray squeezes size-one dimensions before plotting, so an array that is 2-D on a grid with one cell in some direction plots as a line. Select until the array has the dimensions the plot needs. -### Plots and diagnostics with struphy-plots +### Plots and diagnostics with plasma-plots For plots and diagnostics made for Struphy output, install the separate package -[struphy-plots](https://struphy-hub.github.io/struphy-plots): +[plasma-plots](https://struphy-hub.github.io/plasma-plots): ```bash -pip install struphy-plots # or: pip install "struphy[plots]" +pip install plasma-plots # or: pip install "struphy[plots]" ``` When it is installed, every `Output` loads it: no import is needed. It adds `out.plot` and -`out.analysis` for a whole run, and `.struphy.plot`, `.struphy.analysis` and `.struphy.data` on +`out.analysis` for a whole run, and `.plasma.plot`, `.plasma.analysis` and `.plasma.data` on every product: ```python out.plot.energies() # the energy budget of the run phi = out.evaluate("em_fields/phi") -phi.struphy.plot.slice(coords="physical", plane="XY", t=-1, eta3=0) -phi.struphy.plot.animation(x="eta1", y="eta2", eta3=0) # over time -phi.struphy.analysis.mode_spectrum() # poloidal and toroidal mode numbers -out.kinetic_ions.orbits.struphy.plot.poloidal() # guiding-center orbits +phi.plasma.plot.slice(coords="physical", plane="XY", t=-1, eta3=0) +phi.plasma.plot.animation(x="eta1", y="eta2", eta3=0) # over time +phi.plasma.analysis.mode_spectrum() # poloidal and toroidal mode numbers +out.kinetic_ions.orbits.plasma.plot.poloidal() # guiding-center orbits ``` -`import struphy_plots; help(struphy_plots)` gives an overview of what the package does, and `help()` on any method, -e.g. `help(phi.struphy.plot.slice)`, its parameters. Its guides and full reference are at -. +`import plasma_plots; help(plasma_plots)` gives an overview of what the package does, and `help()` on any method, +e.g. `help(phi.plasma.plot.slice)`, its parameters. Its guides and full reference are at +. ## MPI post-processing diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index b753b69a3..1a355f0b9 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -77,9 +77,9 @@ For periodic boundary conditions we will stabilize via ``options``. 6. Run the simulation. ``sim.run()`` returns an :class:`~struphy.Output` object, the entry point for all post-processing. For plots and diagnostics made for Struphy - output (``out.plot``, ``out.analysis`` and ``.struphy.plot`` on every product), install - the separate package `struphy-plots `_ with - ``pip install struphy-plots``; ``Output`` loads it automatically. + output (``out.plot``, ``out.analysis`` and ``.plasma.plot`` on every product), install + the separate package `plasma-plots `_ with + ``pip install plasma-plots``; ``Output`` loads it automatically. .. code-block:: python diff --git a/pyproject.toml b/pyproject.toml index 697b16c9b..234d3608b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ phys = [ "desc-opt<=0.17.1", ] plots = [ - "struphy-plots", + "plasma-plots", ] dev = [ "struphy[mpi]", diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index aecf35d70..22947e6e7 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -60,26 +60,26 @@ def mpi_comm_world(): PLOTS_HINT = ( - "plots and diagnostics of Struphy output come from the struphy-plots package: " - "pip install struphy-plots (or struphy[plots]); see https://struphy-hub.github.io/struphy-plots" + "plots and diagnostics of Struphy output come from the plasma-plots package: " + "pip install plasma-plots (or struphy[plots]); see https://struphy-hub.github.io/plasma-plots" ) _plots = {"loaded": False} -def load_struphy_plots() -> bool: - """Load struphy-plots if it is installed, and tell whether it is. +def load_plasma_plots() -> bool: + """Load plasma-plots if it is installed, and tell whether it is. - Importing ``struphy_plots`` registers ``out.plot``, ``out.analysis`` and the ``.struphy`` + Importing ``plasma_plots`` registers ``out.plot``, ``out.analysis`` and the ``.plasma`` accessor on every product. :class:`Output` calls this when it is created, so none of that - needs an explicit ``import struphy_plots``. Struphy does not depend on the package. + needs an explicit ``import plasma_plots``. Struphy does not depend on the package. """ if not _plots["loaded"]: try: - import struphy_plots # noqa: F401 (registers the accessors) + import plasma_plots # noqa: F401 (registers the accessors) except ImportError: return False except Exception as error: # a broken install must not break reading output - logger.warning("struphy-plots is installed but could not be imported: %s", error) + logger.warning("plasma-plots is installed but could not be imported: %s", error) return False _plots["loaded"] = True return True @@ -233,18 +233,18 @@ class Output: * Every array carries the run in ``attrs["run"]`` (:attr:`label`) and ``attrs["run_name"]``. **Plots and diagnostics** of the output live in the separate package - `struphy-plots `_ (``pip install struphy-plots``, + `plasma-plots `_ (``pip install plasma-plots``, or ``pip install "struphy[plots]"``). When it is installed, creating an ``Output`` loads it, which adds: * ``out.plot`` and ``out.analysis``: whole-run plots and diagnostics, e.g. ``out.plot.energies()``, ``out.analysis.time_fft("em_fields/phi")``; - * ``.struphy.plot``, ``.struphy.analysis`` and ``.struphy.data`` on every product, e.g. - ``out.evaluate("em_fields/phi").struphy.plot.slice(x="eta1", y="eta2", t=-1)``, or - ``orbits.struphy.plot.poloidal()`` for an orbits Dataset. + * ``.plasma.plot``, ``.plasma.analysis`` and ``.plasma.data`` on every product, e.g. + ``out.evaluate("em_fields/phi").plasma.plot.slice(x="eta1", y="eta2", t=-1)``, or + ``orbits.plasma.plot.poloidal()`` for an orbits Dataset. - ``import struphy_plots; help(struphy_plots)`` gives an overview of the package, and ``help()`` - on any accessor method (e.g. ``help(phi.struphy.plot.slice)``) its parameters. + ``import plasma_plots; help(plasma_plots)`` gives an overview of the package, and ``help()`` + on any accessor method (e.g. ``help(phi.plasma.plot.slice)``) its parameters. Parameters ---------- @@ -253,7 +253,7 @@ class Output: """ def __init__(self, path_out): - load_struphy_plots() # out.plot, out.analysis and .struphy on every product, if installed + load_plasma_plots() # out.plot, out.analysis and .plasma on every product, if installed self.path_out = Path(path_out).resolve() self._time_units = "normalized" self.comm = mpi_comm_world() @@ -2106,9 +2106,9 @@ def __getattr__(self, name: str) -> ProductNamespace: if isinstance(attribute, property): attribute.fget(self) # the property raised AttributeError itself; show its own error if name in ("plot", "analysis"): - if load_struphy_plots() and name in type(self).__dict__: + if load_plasma_plots() and name in type(self).__dict__: return getattr(self, name) - raise AttributeError(f"Output has no {name!r} without struphy-plots; {PLOTS_HINT}") + raise AttributeError(f"Output has no {name!r} without plasma-plots; {PLOTS_HINT}") # the raw output names the species, so an unknown name never starts post-processing if name not in self._raw_species(): raise AttributeError(f"{name!r}; available species: {tuple(sorted(self._raw_species()))}") diff --git a/src/struphy/post_processing/tests/test_output.py b/src/struphy/post_processing/tests/test_output.py index 3afe68c41..1e0240c15 100644 --- a/src/struphy/post_processing/tests/test_output.py +++ b/src/struphy/post_processing/tests/test_output.py @@ -794,9 +794,9 @@ def test_stores_of_schema_version_1_are_read_with_eta_dimensions(tmp_path): assert store.SCHEMA_VERSION == 2 -def test_output_loads_struphy_plots_when_it_is_installed(tmp_path): - """Creating an Output registers out.plot and the .struphy accessor, without an explicit import.""" - pytest.importorskip("struphy_plots") +def test_output_loads_plasma_plots_when_it_is_installed(tmp_path): + """Creating an Output registers out.plot and the .plasma accessor, without an explicit import.""" + pytest.importorskip("plasma_plots") import subprocess import sys @@ -804,25 +804,25 @@ def test_output_loads_struphy_plots_when_it_is_installed(tmp_path): script = ( "import xarray as xr\n" "from struphy.post_processing.output import Output\n" - "assert not hasattr(xr.DataArray, 'struphy'), 'struphy_plots was imported before Output()'\n" + "assert not hasattr(xr.DataArray, 'plasma'), 'plasma_plots was imported before Output()'\n" f"out = Output({path!r})\n" - "assert hasattr(xr.DataArray, 'struphy') and hasattr(xr.Dataset, 'struphy')\n" + "assert hasattr(xr.DataArray, 'plasma') and hasattr(xr.Dataset, 'plasma')\n" "assert type(out.plot).__name__ == 'OutputPlots' and type(out.analysis).__name__ == 'OutputAnalysis'\n" ) result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr -def test_plot_without_struphy_plots_says_how_to_get_it(run, monkeypatch): +def test_plot_without_plasma_plots_says_how_to_get_it(run, monkeypatch): import sys - monkeypatch.setitem(sys.modules, "struphy_plots", None) # as if it were not installed + monkeypatch.setitem(sys.modules, "plasma_plots", None) # as if it were not installed monkeypatch.setitem(output_module._plots, "loaded", False) for name in ("plot", "analysis"): if name in Output.__dict__: # registered by an earlier import in this session monkeypatch.delattr(Output, name) for name in ("plot", "analysis"): - with pytest.raises(AttributeError, match="pip install struphy-plots"): + with pytest.raises(AttributeError, match="pip install plasma-plots"): getattr(run, name) with pytest.raises(AttributeError, match="available species"): run.not_a_species # other names keep their own error From 48e5151cd62983b9a1760a9a50f9af55af6adcf4 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Tue, 29 Sep 2026 14:21:28 +0200 Subject: [PATCH 192/193] Added plasma-plots to struphy-pproc --- README.md | 2 +- README.qmd | 2 +- doc/markdown/output-api.md | 2 +- doc/sections/quickstart.rst | 2 +- pyproject.toml | 5 ++++- src/struphy/post_processing/output.py | 4 ++-- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fc477ffbc..7b79a276c 100755 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This will create `params_Maxwell.py` in your current working directory (cwd). Yo The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. -For plots and diagnostics of the output, install [plasma-plots](https://struphy-hub.github.io/plasma-plots) (`pip install plasma-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.plasma.plot` on every product. +For plots and diagnostics of the output, install [plasma-plots](https://struphy-hub.github.io/plasma-plots) with `pip install "struphy[pproc]"`; `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.plasma.plot` on every product. Parallel simulations are run for example with diff --git a/README.qmd b/README.qmd index cf8ceebd8..d41f7a2c3 100644 --- a/README.qmd +++ b/README.qmd @@ -111,7 +111,7 @@ python params_Maxwell.py The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. -For plots and diagnostics of the output, install [plasma-plots](https://struphy-hub.github.io/plasma-plots) (`pip install plasma-plots`); `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.plasma.plot` on every product. +For plots and diagnostics of the output, install [plasma-plots](https://struphy-hub.github.io/plasma-plots) with `pip install "struphy[pproc]"`; `Output` loads it automatically, adding `out.plot`, `out.analysis` and `.plasma.plot` on every product. Parallel simulations are run for example with diff --git a/doc/markdown/output-api.md b/doc/markdown/output-api.md index 22f700f34..ef5924874 100644 --- a/doc/markdown/output-api.md +++ b/doc/markdown/output-api.md @@ -225,7 +225,7 @@ For plots and diagnostics made for Struphy output, install the separate package [plasma-plots](https://struphy-hub.github.io/plasma-plots): ```bash -pip install plasma-plots # or: pip install "struphy[plots]" +pip install plasma-plots # or: pip install "struphy[pproc]" ``` When it is installed, every `Output` loads it: no import is needed. It adds `out.plot` and diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 1a355f0b9..7cd9e248e 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -79,7 +79,7 @@ For periodic boundary conditions we will stabilize via ``options``. the entry point for all post-processing. For plots and diagnostics made for Struphy output (``out.plot``, ``out.analysis`` and ``.plasma.plot`` on every product), install the separate package `plasma-plots `_ with - ``pip install plasma-plots``; ``Output`` loads it automatically. + ``pip install "struphy[pproc]"``; ``Output`` loads it automatically. .. code-block:: python diff --git a/pyproject.toml b/pyproject.toml index 234d3608b..db5ab7448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,8 +63,11 @@ phys = [ "gvec>=1.1.0, <=1.5.0", "desc-opt<=0.17.1", ] +pproc = [ + "plasma-plots>=0.1.0", +] plots = [ - "plasma-plots", + "plasma-plots>=0.1.0", ] dev = [ "struphy[mpi]", diff --git a/src/struphy/post_processing/output.py b/src/struphy/post_processing/output.py index 22947e6e7..d764df223 100644 --- a/src/struphy/post_processing/output.py +++ b/src/struphy/post_processing/output.py @@ -61,7 +61,7 @@ def mpi_comm_world(): PLOTS_HINT = ( "plots and diagnostics of Struphy output come from the plasma-plots package: " - "pip install plasma-plots (or struphy[plots]); see https://struphy-hub.github.io/plasma-plots" + "pip install plasma-plots (or struphy[pproc]); see https://struphy-hub.github.io/plasma-plots" ) _plots = {"loaded": False} @@ -234,7 +234,7 @@ class Output: **Plots and diagnostics** of the output live in the separate package `plasma-plots `_ (``pip install plasma-plots``, - or ``pip install "struphy[plots]"``). When it is installed, creating an ``Output`` loads it, + or ``pip install "struphy[pproc]"``). When it is installed, creating an ``Output`` loads it, which adds: * ``out.plot`` and ``out.analysis``: whole-run plots and diagnostics, e.g. From dce862be431d6cd96fa69ce89ce0e6a67ce009d1 Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Tue, 29 Sep 2026 14:23:26 +0200 Subject: [PATCH 193/193] Removed plots optional dep --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index db5ab7448..2a1caa515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,11 +66,9 @@ phys = [ pproc = [ "plasma-plots>=0.1.0", ] -plots = [ - "plasma-plots>=0.1.0", -] dev = [ "struphy[mpi]", + "struphy[pproc]", "notebook", "autopep8", "isort", @@ -89,6 +87,7 @@ dev = [ ] doc = [ "struphy[phys]", + "struphy[pproc]", "jupyter", "nbconvert", "ipykernel", @@ -112,6 +111,7 @@ likwid = [ ] all = [ "struphy[phys]", + "struphy[pproc]", "struphy[dev]", "struphy[mpi]", "struphy[doc]",