From 5b1bb41d703b42f276b07969d637a69d5184b348 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 6 Feb 2026 16:06:46 +0100 Subject: [PATCH 01/80] Started to create new class Simulations. This class will replace main.run and main.pproc. Conceptually, a StruphyModel represents the physics model, including its variables and equation parameters. A simulation takes StruphyModel as input and additionally deals with geometry, initial conditions, allocation of memory, and the actual solution process. I started moving some methos/functions into the new Simulation class. --- src/struphy/io/setup.py | 60 --- src/struphy/main.py | 2 +- src/struphy/models/base.py | 389 +---------------- src/struphy/simulation/__init__.py | 0 src/struphy/simulation/sim.py | 666 +++++++++++++++++++++++++++++ 5 files changed, 668 insertions(+), 449 deletions(-) create mode 100644 src/struphy/simulation/__init__.py create mode 100644 src/struphy/simulation/sim.py diff --git a/src/struphy/io/setup.py b/src/struphy/io/setup.py index d8e2be106..af5e10f3d 100644 --- a/src/struphy/io/setup.py +++ b/src/struphy/io/setup.py @@ -23,66 +23,6 @@ def import_parameters_py(params_path: str) -> ModuleType: return params_in -def setup_folders( - path_out: str, - restart: bool, - verbose: bool = False, -): - """ - Setup output folders. - """ - if MPI.COMM_WORLD.Get_rank() == 0: - if verbose: - print("\nPREPARATION AND CLEAN-UP:") - - # create output folder if it does not exit - if not os.path.exists(path_out): - os.makedirs(path_out, exist_ok=True) - if verbose: - print("Created folder " + path_out) - - # create data folder in output folder if it does not exist - if not os.path.exists(os.path.join(path_out, "data/")): - os.mkdir(os.path.join(path_out, "data/")) - if verbose: - print("Created folder " + os.path.join(path_out, "data/")) - else: - # remove post_processing folder - folder = os.path.join(path_out, "post_processing") - if os.path.exists(folder): - shutil.rmtree(folder) - if verbose: - print("Removed existing folder " + folder) - - # remove meta file - file = os.path.join(path_out, "meta.txt") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove profiling file - file = os.path.join(path_out, "profile_tmp") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove .png files (if NOT a restart) - if not restart: - files = glob.glob(os.path.join(path_out, "*.png")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - def setup_derham( grid: TensorProductGrid, options: DerhamOptions, diff --git a/src/struphy/main.py b/src/struphy/main.py index 7f84c0c1e..95a4c62cd 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -222,7 +222,7 @@ def run( model.allocate_feec(grid, derham_opts) # equation paramters - model.setup_equation_params(units=model.units, verbose=verbose) + model.set_normalization_params(units=model.units, verbose=verbose) # allocate variables model.allocate_variables(verbose=verbose) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 8f413822a..fef5cf2f3 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -88,7 +88,7 @@ def update_scalar_quantities(self): ## setup methods - def setup_equation_params(self, units: Units, verbose=False): + def set_normalization_params(self, units: Units, verbose=False): """Set euqation parameters for each fluid and kinetic species.""" for _, species in self.fluid_species.items(): assert isinstance(species, FluidSpecies) @@ -98,44 +98,6 @@ def setup_equation_params(self, units: Units, verbose=False): assert isinstance(species, ParticleSpecies) species.setup_equation_params(units=units, verbose=verbose) - def setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium): - """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" - if equil is not None: - if isinstance(equil, NumericalMHDequilibrium): - self._domain = equil.domain - else: - self._domain = domain - equil.domain = domain - - if hasattr(equil, "units"): - assert isinstance(equil.units, Units) - equil.units.derive_units( - velocity_scale=self.velocity_scale, - A_bulk=self.bulk_species.mass_number, - Z_bulk=self.bulk_species.charge_number, - verbose=self.verbose, - ) - - else: - self._domain = domain - - self._equil = equil - - if MPI.COMM_WORLD.Get_rank() == 0 and self.verbose: - print("\nDOMAIN:") - print("type:".ljust(25), self.domain.__class__.__name__) - for key, val in self.domain.params.items(): - if key not in {"cx", "cy", "cz"}: - print((key + ":").ljust(25), val) - - print("\nFLUID BACKGROUND:") - if self.equil is not None: - print("type:".ljust(25), self.equil.__class__.__name__) - for key, val in self.equil.params.items(): - print((key + ":").ljust(25), val) - else: - print("None.") - ## species @property @@ -182,67 +144,6 @@ def species(self): ## allocate methods - def allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): - # create discrete derham sequence - if self.clone_config is None: - derham_comm = MPI.COMM_WORLD - else: - derham_comm = self.clone_config.sub_comm - - if grid is None or derham_opts is None: - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") - self._derham = None - else: - self._derham = setup_derham( - grid, - derham_opts, - comm=derham_comm, - domain=self.domain, - verbose=self.verbose, - ) - - # create weighted mass and basis operators - if self.derham is None: - self._mass_ops = None - self._basis_ops = None - else: - self._mass_ops = WeightedMassOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - self._basis_ops = BasisProjectionOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - # create projected equilibrium - if self.derham is None: - self._projected_equil = None - else: - if isinstance(self.equil, MHDequilibrium): - self._projected_equil = ProjectedMHDequilibrium( - self.equil, - self.derham, - ) - elif isinstance(self.equil, FluidEquilibriumWithB): - self._projected_equil = ProjectedFluidEquilibriumWithB( - self.equil, - self.derham, - ) - elif isinstance(self.equil, FluidEquilibrium): - self._projected_equil = ProjectedFluidEquilibrium( - self.equil, - self.derham, - ) - else: - self._projected_equil = None - def allocate_propagators(self): # set propagators base class attributes (then available to all propagators) Propagator.derham = self.derham @@ -1507,294 +1408,6 @@ def generate_default_parameter_file( return path - ################### - # Private methods : - ################### - - def compute_plasma_params(self, verbose=True): - """ - Compute and print volume averaged plasma parameters for each species of the model. - - Global parameters: - - plasma volume - - transit length - - magnetic field - - Species dependent parameters: - - mass - - charge - - density - - pressure - - thermal energy kBT - - Alfvén speed v_A - - thermal speed v_th - - thermal frequency Omega_th - - cyclotron frequency Omega_c - - plasma frequency Omega_p - - Alfvèn frequency Omega_A - - thermal Larmor radius rho_th - - MHD length scale v_a/Omega_c - - rho/L - - alpha = Omega_p/Omega_c - - epsilon = 1/(t*Omega_c) - """ - - # units affices for printing - units_affix = {} - units_affix["plasma volume"] = " m³" - units_affix["transit length"] = " m" - units_affix["magnetic field"] = " T" - units_affix["mass"] = " kg" - units_affix["charge"] = " C" - units_affix["density"] = " m⁻³" - units_affix["pressure"] = " bar" - units_affix["kBT"] = " keV" - units_affix["v_A"] = " m/s" - units_affix["v_th"] = " m/s" - units_affix["vth1"] = " m/s" - units_affix["vth2"] = " m/s" - units_affix["vth3"] = " m/s" - units_affix["Omega_th"] = " Mrad/s" - units_affix["Omega_c"] = " Mrad/s" - units_affix["Omega_p"] = " Mrad/s" - units_affix["Omega_A"] = " Mrad/s" - units_affix["rho_th"] = " m" - units_affix["v_A/Omega_c"] = " m" - units_affix["rho_th/L"] = "" - units_affix["alpha"] = "" - units_affix["epsilon"] = "" - - h = 1 / 20 - eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - - ## global parameters - - # plasma volume (hat x^3) - det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) - vol1 = xp.mean(xp.abs(det_tmp)) - # plasma volume (m⁻³) - plasma_volume = vol1 * self.units.x**3 - # transit length (m) - transit_length = plasma_volume ** (1 / 3) - # magnetic field (T) - if isinstance(self.equil, FluidEquilibriumWithB): - B_tmp = self.equil.absB0(eta1, eta2, eta3) - else: - B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) - magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B - B_max = xp.max(B_tmp) * self.units.B - B_min = xp.min(B_tmp) * self.units.B - - if magnetic_field < 1e-14: - magnetic_field = xp.nan - # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") - - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print("\nPLASMA PARAMETERS:") - print( - "Plasma volume:".ljust(25), - "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], - ) - print( - "Transit length:".ljust(25), - "{:4.3e}".format(transit_length) + units_affix["transit length"], - ) - print( - "Avg. magnetic field:".ljust(25), - "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], - ) - print( - "Max magnetic field:".ljust(25), - "{:4.3e}".format(B_max) + units_affix["magnetic field"], - ) - print( - "Min magnetic field:".ljust(25), - "{:4.3e}".format(B_min) + units_affix["magnetic field"], - ) - - # # species dependent parameters - # self._pparams = {} - - # if len(self.fluid_species) > 0: - # for species, val in self.fluid_species.items(): - # self._pparams[species] = {} - # # type - # self._pparams[species]["type"] = "fluid" - # # mass (kg) - # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p - # # charge (C) - # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e - # # density (m⁻³) - # self._pparams[species]["density"] = ( - # xp.mean( - # self.equil.n0( - # eta1, - # eta2, - # eta3, - # ) - # * xp.abs(det_tmp), - # ) - # * self.units.x ** 3 - # / plasma_volume - # * self.units.n - # ) - # # pressure (bar) - # self._pparams[species]["pressure"] = ( - # xp.mean( - # self.equil.p0( - # eta1, - # eta2, - # eta3, - # ) - # * xp.abs(det_tmp), - # ) - # * self.units.x ** 3 - # / plasma_volume - # * self.units.p - # * 1e-5 - # ) - # # thermal energy (keV) - # self._pparams[species]["kBT"] = self._pparams[species]["pressure"] * 1e5 / self._pparams[species]["density"] / e * 1e-3 - - # if len(self.kinetic) > 0: - # eta1mg, eta2mg, eta3mg = xp.meshgrid( - # eta1, - # eta2, - # eta3, - # indexing="ij", - # ) - - # for species, val in self.kinetic.items(): - # self._pparams[species] = {} - # # type - # self._pparams[species]["type"] = "kinetic" - # # mass (kg) - # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p - # # charge (C) - # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e - - # # create temp kinetic object for (default) parameter extraction - # tmp_bckgr = val["params"]["background"] - - # if val["space"] != "ParticlesSPH": - # tmp = None - # for fi, maxw_params in tmp_bckgr.items(): - # if fi[-2] == "_": - # fi_type = fi[:-2] - # else: - # fi_type = fi - - # if tmp is None: - # tmp = getattr(maxwellians, fi_type)( - # maxw_params=maxw_params, - # equil=self.equil, - # ) - # else: - # tmp = tmp + getattr(maxwellians, fi_type)( - # maxw_params=maxw_params, - # equil=self.equil, - # ) - - # if val["space"] != "ParticlesSPH" and tmp.coords == "constants_of_motion": - # # call parameters - # a1 = self.domain.params_map["a1"] - # r = eta1mg * (1 - a1) + a1 - # psi = self.equil.psi_r(r) - - # # density (m⁻³) - # self._pparams[species]["density"] = ( - # xp.mean(tmp.n(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.n - # ) - # # thermal speed (m/s) - # self._pparams[species]["v_th"] = ( - # xp.mean(tmp.vth(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.v - # ) - # # thermal energy (keV) - # self._pparams[species]["kBT"] = self._pparams[species]["mass"] * self._pparams[species]["v_th"] ** 2 / e * 1e-3 - # # pressure (bar) - # self._pparams[species]["pressure"] = ( - # self._pparams[species]["kBT"] * e * 1e3 * self._pparams[species]["density"] * 1e-5 - # ) - - # else: - # # density (m⁻³) - # # self._pparams[species]['density'] = xp.mean(tmp.n( - # # eta1mg, eta2mg, eta3mg) * xp.abs(det_tmp)) * units['x']**3 / plasma_volume * units['n'] - # self._pparams[species]["density"] = 99.0 - # # thermal speeds (m/s) - # vth = [] - # # vths = tmp.vth(eta1mg, eta2mg, eta3mg) - # vths = [99.0] - # for k in range(len(vths)): - # vth += [ - # vths[k] * xp.abs(det_tmp) * self.units.x ** 3 / plasma_volume * self.units.v, - # ] - # thermal_speed = 0.0 - # for dir in range(val["obj"].vdim): - # # self._pparams[species]['vth' + str(dir + 1)] = xp.mean(vth[dir]) - # self._pparams[species]["vth" + str(dir + 1)] = 99.0 - # thermal_speed += self._pparams[species]["vth" + str(dir + 1)] - # # TODO: here it is assumed that background density parameter is called "n", - # # and that background thermal speeds are called "vthn"; make this a convention? - # # self._pparams[species]['v_th'] = thermal_speed / \ - # # val['obj'].vdim - # self._pparams[species]["v_th"] = 99.0 - # # thermal energy (keV) - # # self._pparams[species]['kBT'] = self._pparams[species]['mass'] * \ - # # self._pparams[species]['v_th']**2 / e * 1e-3 - # self._pparams[species]["kBT"] = 99.0 - # # pressure (bar) - # # self._pparams[species]['pressure'] = self._pparams[species]['kBT'] * \ - # # e * 1e3 * self._pparams[species]['density'] * 1e-5 - # self._pparams[species]["pressure"] = 99.0 - - # for species in self._pparams: - # # alfvén speed (m/s) - # self._pparams[species]["v_A"] = magnetic_field / xp.sqrt( - # mu0 * self._pparams[species]["mass"] * self._pparams[species]["density"], - # ) - # # thermal speed (m/s) - # self._pparams[species]["v_th"] = xp.sqrt( - # self._pparams[species]["kBT"] * 1e3 * e / self._pparams[species]["mass"], - # ) - # # thermal frequency (Mrad/s) - # self._pparams[species]["Omega_th"] = self._pparams[species]["v_th"] / transit_length * 1e-6 - # # cyclotron frequency (Mrad/s) - # self._pparams[species]["Omega_c"] = self._pparams[species]["charge"] * magnetic_field / self._pparams[species]["mass"] * 1e-6 - # # plasma frequency (Mrad/s) - # self._pparams[species]["Omega_p"] = ( - # xp.sqrt( - # self._pparams[species]["density"] * (self._pparams[species]["charge"]) ** 2 / eps0 / self._pparams[species]["mass"], - # ) - # * 1e-6 - # ) - # # alfvén frequency (Mrad/s) - # self._pparams[species]["Omega_A"] = self._pparams[species]["v_A"] / transit_length * 1e-6 - # # Larmor radius (m) - # self._pparams[species]["rho_th"] = self._pparams[species]["v_th"] / (self._pparams[species]["Omega_c"] * 1e6) - # # MHD length scale (m) - # self._pparams[species]["v_A/Omega_c"] = self._pparams[species]["v_A"] / (xp.abs(self._pparams[species]["Omega_c"]) * 1e6) - # # dim-less ratios - # self._pparams[species]["rho_th/L"] = self._pparams[species]["rho_th"] / transit_length - - # if verbose and self.rank_world == 0: - # print("\nSPECIES PARAMETERS:") - # for species, ch in self._pparams.items(): - # print(f"\nname:".ljust(26), species) - # print(f"type:".ljust(25), ch["type"]) - # ch.pop("type") - # print(f"is bulk:".ljust(25), species == self.bulk_species()) - # for kinds, vals in ch.items(): - # print( - # kinds.ljust(25), - # "{:+4.3e}".format( - # vals, - # ), - # units_affix[kinds], - # ) - class MyDumper(yaml.SafeDumper): # HACK: insert blank lines between top-level objects diff --git a/src/struphy/simulation/__init__.py b/src/struphy/simulation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py new file mode 100644 index 000000000..fdc79474f --- /dev/null +++ b/src/struphy/simulation/sim.py @@ -0,0 +1,666 @@ +# api imports +from struphy import (EnvironmentOptions, + BaseUnits, + Time, + domains, + equils, + grids, + DerhamOptions, + ) + +# core imports +from struphy.models.base import StruphyModel +from struphy.geometry.base import Domain +from struphy.fields_background.base import FluidEquilibrium, NumericalMHDequilibrium +from struphy.io.setup import setup_folders +from struphy.io.options import Units +from struphy.utils.clone_config import CloneConfig + +# third party imports +from feectools.ddm.mpi import MockMPI +from feectools.ddm.mpi import mpi as MPI +from scope_profiler import ProfileManager +import os +import time +import pickle +import shutil +import sysconfig +import cunumpy as xp +import h5py +from line_profiler import profile +from pyevtk.hl import gridToVTK + + +class Simulation: + def __init__(self, + model: StruphyModel, + params_path: str = None, + env: EnvironmentOptions = EnvironmentOptions(), + base_units: BaseUnits = BaseUnits(), + time_opts: Time = Time(), + domain: Domain = domains.Cuboid(), + equil: FluidEquilibrium = equils.HomogenSlab(), + grid: grids.TensorProductGrid = None, + derham_opts: DerhamOptions = None, + verbose: bool = False, + ): + + self.model = model + self.params_path = params_path + self.env = env + self.base_units = base_units + self.time_opts = time_opts + self.domain = domain + self.equil = equil + self.grid = grid + self.derham_opts = derham_opts + self.verbose = verbose + + # setup profiling agent + ProfileManager.setup( + profiling_activated=env.profiling_activated, + time_trace=env.profiling_trace, + use_likwid=False, + file_path=os.path.join( + env.out_folders, + env.sim_folder, + "profiling_data.h5", + ), + ) + + # mpi info + if isinstance(MPI, MockMPI): + comm = None + rank = 0 + size = 1 + Barrier = lambda: None + else: + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + size = comm.Get_size() + Barrier = comm.Barrier + + if rank == 0: + print("") + + # synchronize MPI processes to set same start time of simulation for all processes + Barrier() + start_simulation = time.time() + + # check model + assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" + model.verbose = verbose + model_name = model.__class__.__name__ + + if rank == 0: + print(f"\n*** Starting run for model '{model_name}':") + + # meta-data + path_out = env.path_out + restart = env.restart + max_runtime = env.max_runtime + save_step = env.save_step + sort_step = env.sort_step + num_clones = env.num_clones + use_mpi = (comm is not None,) + + meta = {} + meta["platform"] = sysconfig.get_platform() + meta["python version"] = sysconfig.get_python_version() + meta["model name"] = model_name + meta["parameter file"] = params_path + meta["output folder"] = path_out + meta["MPI processes"] = size + meta["use MPI.COMM_WORLD"] = use_mpi + meta["number of domain clones"] = num_clones + meta["restart"] = restart + meta["max wall-clock [min]"] = max_runtime + meta["save interval [steps]"] = save_step + + if rank == 0: + print("\nMETADATA:") + for k, v in meta.items(): + print(f"{k}:".ljust(25), v) + + # creating output folders + self._setup_folders( + path_out=path_out, + restart=restart, + verbose=verbose, + ) + + # save parameter file + if rank == 0: + # save python param file + if params_path is not None: + assert params_path[-3:] == ".py" + shutil.copy2( + params_path, + os.path.join(path_out, "parameters.py"), + ) + # pickle struphy objects + else: + with open(os.path.join(path_out, "env.bin"), "wb") as f: + pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "base_units.bin"), "wb") as f: + pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: + pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "domain.bin"), "wb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} + pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "equil.bin"), "wb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + if equil is not None: + tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} + else: + tmp_dct = {} + pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "grid.bin"), "wb") as f: + pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: + pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "model_class.bin"), "wb") as f: + pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) + + # config clones + if comm is None: + clone_config = None + else: + if num_clones == 1: + clone_config = None + else: + # Setup domain cloning communicators + # MPI.COMM_WORLD : comm + # within a clone: : sub_comm + # between the clones : inter_comm + clone_config = CloneConfig(comm=comm, params=None, num_clones=num_clones) + clone_config.print_clone_config() + if model.particle_species: + clone_config.print_particle_config() + + self.clone_config = clone_config + Barrier() + + # units and normalization parameters + units = Units(base_units) + self.units = units + if model.bulk_species is None: + A_bulk = None + Z_bulk = None + else: + A_bulk = model.bulk_species.mass_number + Z_bulk = model.bulk_species.charge_number + self.units.derive_units( + velocity_scale=model.velocity_scale, + A_bulk=A_bulk, + Z_bulk=Z_bulk, + verbose=verbose, + ) + model.set_normalization_params(units=self.units, verbose=verbose) + + # domain and fluid background + self._setup_domain_and_equil(domain, equil, verbose=verbose) + + def allocate(self, verbose: bool = False): + # feec + self._allocate_feec(self.grid, self.derham_opts) + + # allocate model variables + self.model.allocate_variables(verbose=verbose) + self.model.allocate_helpers() + + # pass info to propagators + self.model.allocate_propagators() + + def store_geometry(self, verbose: bool = False): + # store geometry vtk + if rank == 0: + grids_log = [ + xp.linspace(1e-6, 1.0, 32), + xp.linspace(0.0, 1.0, 32), + xp.linspace(0.0, 1.0, 32), + ] + + tmp = model.domain(*grids_log) + grids_phy = [tmp[0], tmp[1], tmp[2]] + + pointData = {} + det_df = model.domain.jacobian_det(*grids_log) + pointData["det_df"] = det_df + + if model.equil is not None: + p0 = model.equil.p0(*grids_log) + pointData["p0"] = p0 + if isinstance(model.equil, FluidEquilibriumWithB): + absB0 = model.equil.absB0(*grids_log) + pointData["absB0"] = absB0 + + gridToVTK(os.path.join(path_out, "geometry"), *grids_phy, pointData=pointData) + + def compute_plasma_params(self, verbose=True): + """ + Compute and print volume averaged plasma parameters for each species of the model. + + Global parameters: + - plasma volume + - transit length + - magnetic field + + Species dependent parameters: + - mass + - charge + - density + - pressure + - thermal energy kBT + - Alfvén speed v_A + - thermal speed v_th + - thermal frequency Omega_th + - cyclotron frequency Omega_c + - plasma frequency Omega_p + - Alfvèn frequency Omega_A + - thermal Larmor radius rho_th + - MHD length scale v_a/Omega_c + - rho/L + - alpha = Omega_p/Omega_c + - epsilon = 1/(t*Omega_c) + """ + + # units affices for printing + units_affix = {} + units_affix["plasma volume"] = " m³" + units_affix["transit length"] = " m" + units_affix["magnetic field"] = " T" + units_affix["mass"] = " kg" + units_affix["charge"] = " C" + units_affix["density"] = " m⁻³" + units_affix["pressure"] = " bar" + units_affix["kBT"] = " keV" + units_affix["v_A"] = " m/s" + units_affix["v_th"] = " m/s" + units_affix["vth1"] = " m/s" + units_affix["vth2"] = " m/s" + units_affix["vth3"] = " m/s" + units_affix["Omega_th"] = " Mrad/s" + units_affix["Omega_c"] = " Mrad/s" + units_affix["Omega_p"] = " Mrad/s" + units_affix["Omega_A"] = " Mrad/s" + units_affix["rho_th"] = " m" + units_affix["v_A/Omega_c"] = " m" + units_affix["rho_th/L"] = "" + units_affix["alpha"] = "" + units_affix["epsilon"] = "" + + h = 1 / 20 + eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + + ## global parameters + + # plasma volume (hat x^3) + det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) + vol1 = xp.mean(xp.abs(det_tmp)) + # plasma volume (m⁻³) + plasma_volume = vol1 * self.units.x**3 + # transit length (m) + transit_length = plasma_volume ** (1 / 3) + # magnetic field (T) + if isinstance(self.equil, FluidEquilibriumWithB): + B_tmp = self.equil.absB0(eta1, eta2, eta3) + else: + B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) + magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B + B_max = xp.max(B_tmp) * self.units.B + B_min = xp.min(B_tmp) * self.units.B + + if magnetic_field < 1e-14: + magnetic_field = xp.nan + # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("\nPLASMA PARAMETERS:") + print( + "Plasma volume:".ljust(25), + "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], + ) + print( + "Transit length:".ljust(25), + "{:4.3e}".format(transit_length) + units_affix["transit length"], + ) + print( + "Avg. magnetic field:".ljust(25), + "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], + ) + print( + "Max magnetic field:".ljust(25), + "{:4.3e}".format(B_max) + units_affix["magnetic field"], + ) + print( + "Min magnetic field:".ljust(25), + "{:4.3e}".format(B_min) + units_affix["magnetic field"], + ) + + def run(self, verbose: bool = False): + if rank < 32: + if rank == 0: + print("") + print(f"Rank {rank}: executing main.run() for model {model_name} ...") + + if size > 32 and rank == 32: + print(f"Ranks > 31: executing main.run() for model {model_name} ...") + + # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) + # use MPI.COMM_WORLD as communicator when storing the outputs + data = DataContainer(path_out, comm=comm) + + # time quantities (current time value, value in seconds and index) + time_state = {} + time_state["value"] = xp.zeros(1, dtype=float) + time_state["value_sec"] = xp.zeros(1, dtype=float) + time_state["index"] = xp.zeros(1, dtype=int) + + # add time quantities to data object for saving + for key, val in time_state.items(): + key_time = "time/" + key + key_time_restart = "restart/time/" + key + data.add_data({key_time: val}) + data.add_data({key_time_restart: val}) + + # retrieve time parameters + dt = time_opts.dt + Tend = time_opts.Tend + split_algo = time_opts.split_algo + + # set initial conditions for all variables + if restart: + model.initialize_from_restart(data) + + with h5py.File(data.file_path, "a") as file: + time_state["value"][0] = file["restart/time/value"][-1] + time_state["value_sec"][0] = file["restart/time/value_sec"][-1] + time_state["index"][0] = file["restart/time/index"][-1] + + total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) + else: + total_steps = str(int(round(Tend / dt))) + + # compute initial scalars and kinetic data, pass time state to all propagators + model.update_scalar_quantities() + model.update_markers_to_be_saved() + model.update_distr_functions() + model.add_time_state(time_state["value"]) + + # add all variables to be saved to data object + save_keys_all, save_keys_end = model.initialize_data_output(data, size) + + # ======================== main time loop ====================== + model.update_scalar_quantities() + if rank == 0: + print("\nINITIAL SCALAR QUANTITIES:") + model.print_scalar_quantities() + + print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") + + # time loop + run_time_now = 0.0 + while True: + Barrier() + + # stop time loop? + break_cond_1 = time_state["value"][0] >= Tend + break_cond_2 = run_time_now > max_runtime + + if break_cond_1 or break_cond_2: + # save restart data (other data already saved below) + data.save_data(keys=save_keys_end) + end_simulation = time.time() + if rank == 0: + print(f"\nTime steps done: {time_state['index'][0]}") + print( + "wall-clock time of simulation [sec]: ", + end_simulation - start_simulation, + ) + print() + break + + if sort_step and time_state["index"][0] % sort_step == 0: + t0 = time.time() + for key, val in model.pointer.items(): + if isinstance(val, Particles): + val.do_sort() + t1 = time.time() + if rank == 0 and verbose: + message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( + run_time_now * 60, + t1 - t0, + ) + print(message, end="\n") + print() + + # update time and index (round time to 10 decimals for a clean time grid!) + time_state["value"][0] = round(time_state["value"][0] + dt, 10) + time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * model.units.t, 10) + time_state["index"][0] += 1 + + # perform one time step dt + t0 = time.time() + with ProfileManager.profile_region("model.integrate"): + model.integrate(dt, split_algo) + t1 = time.time() + + run_time_now = (time.time() - start_simulation) / 60 + + # update diagnostics data and save data + if time_state["index"][0] % save_step == 0: + # compute scalars and kinetic data + model.update_scalar_quantities() + model.update_markers_to_be_saved() + model.update_distr_functions() + + # extract FEEC coefficients + feec_species = model.field_species | model.fluid_species | model.diagnostic_species + for species, val in feec_species.items(): + assert isinstance(val, Species) + for variable, subval in val.variables.items(): + assert isinstance(subval, FEECVariable) + spline = subval.spline + # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! + spline.extract_coeffs(update_ghost_regions=False) + + # save data (everything but restart data) + data.save_data(keys=save_keys_all) + + # print current time and scalar quantities to screen + if rank == 0 and verbose: + step = str(time_state["index"][0]).zfill(len(total_steps)) + + message = "time step: " + step + "/" + str(total_steps) + message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) + message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( + time_state["value_sec"][0], + Tend * model.units.t, + ) + message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( + run_time_now * 60, + t1 - t0, + ) + + print(message, end="\n") + model.print_scalar_quantities() + print() + + # =================================================================== + + meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 + Barrier() + + if rank == 0: + # save meta-data + dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) + print("Struphy run finished.") + + if clone_config is not None: + clone_config.free() + + ProfileManager.finalize() + + def _setup_folders( + path_out: str, + restart: bool, + verbose: bool = False, + ): + """ + Setup output folders. + """ + if MPI.COMM_WORLD.Get_rank() == 0: + if verbose: + print("\nPREPARATION AND CLEAN-UP:") + + # create output folder if it does not exit + if not os.path.exists(path_out): + os.makedirs(path_out, exist_ok=True) + if verbose: + print("Created folder " + path_out) + + # create data folder in output folder if it does not exist + if not os.path.exists(os.path.join(path_out, "data/")): + os.mkdir(os.path.join(path_out, "data/")) + if verbose: + print("Created folder " + os.path.join(path_out, "data/")) + else: + # remove post_processing folder + folder = os.path.join(path_out, "post_processing") + if os.path.exists(folder): + shutil.rmtree(folder) + if verbose: + print("Removed existing folder " + folder) + + # remove meta file + file = os.path.join(path_out, "meta.txt") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove profiling file + file = os.path.join(path_out, "profile_tmp") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove .png files (if NOT a restart) + if not restart: + files = glob.glob(os.path.join(path_out, "*.png")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): + """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" + if equil is not None: + if isinstance(equil, NumericalMHDequilibrium): + self._domain = equil.domain + else: + self._domain = domain + equil.domain = domain + + if hasattr(equil, "units"): + assert isinstance(equil.units, Units) + equil.units.derive_units( + velocity_scale=self.velocity_scale, + A_bulk=self.bulk_species.mass_number, + Z_bulk=self.bulk_species.charge_number, + verbose=self.verbose, + ) + + else: + self._domain = domain + + self._equil = equil + + if MPI.COMM_WORLD.Get_rank() == 0 and verbose: + print("\nDOMAIN:") + print("type:".ljust(25), self.domain.__class__.__name__) + for key, val in self.domain.params.items(): + if key not in {"cx", "cy", "cz"}: + print((key + ":").ljust(25), val) + + print("\nFLUID BACKGROUND:") + if self.equil is not None: + print("type:".ljust(25), self.equil.__class__.__name__) + for key, val in self.equil.params.items(): + print((key + ":").ljust(25), val) + else: + print("None.") + + def _allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): + # create discrete derham sequence + if self.clone_config is None: + derham_comm = MPI.COMM_WORLD + else: + derham_comm = self.clone_config.sub_comm + + if grid is None or derham_opts is None: + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") + self._derham = None + else: + self._derham = setup_derham( + grid, + derham_opts, + comm=derham_comm, + domain=self.domain, + verbose=self.verbose, + ) + + # create weighted mass and basis operators + if self.derham is None: + self._mass_ops = None + self._basis_ops = None + else: + self._mass_ops = WeightedMassOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + self._basis_ops = BasisProjectionOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + # create projected equilibrium + if self.derham is None: + self._projected_equil = None + else: + if isinstance(self.equil, MHDequilibrium): + self._projected_equil = ProjectedMHDequilibrium( + self.equil, + self.derham, + ) + elif isinstance(self.equil, FluidEquilibriumWithB): + self._projected_equil = ProjectedFluidEquilibriumWithB( + self.equil, + self.derham, + ) + elif isinstance(self.equil, FluidEquilibrium): + self._projected_equil = ProjectedFluidEquilibrium( + self.equil, + self.derham, + ) + else: + self._projected_equil = None + \ No newline at end of file From 9892bd5add756785f864b36b74aba78b7a0d0d5d Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 9 Feb 2026 08:58:08 +0100 Subject: [PATCH 02/80] go back tobase.py from devel --- src/struphy/models/base.py | 389 +++++++++++++++++++++++++++++++++- src/struphy/simulation/sim.py | 54 ++--- 2 files changed, 415 insertions(+), 28 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index fef5cf2f3..8f413822a 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -88,7 +88,7 @@ def update_scalar_quantities(self): ## setup methods - def set_normalization_params(self, units: Units, verbose=False): + def setup_equation_params(self, units: Units, verbose=False): """Set euqation parameters for each fluid and kinetic species.""" for _, species in self.fluid_species.items(): assert isinstance(species, FluidSpecies) @@ -98,6 +98,44 @@ def set_normalization_params(self, units: Units, verbose=False): assert isinstance(species, ParticleSpecies) species.setup_equation_params(units=units, verbose=verbose) + def setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium): + """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" + if equil is not None: + if isinstance(equil, NumericalMHDequilibrium): + self._domain = equil.domain + else: + self._domain = domain + equil.domain = domain + + if hasattr(equil, "units"): + assert isinstance(equil.units, Units) + equil.units.derive_units( + velocity_scale=self.velocity_scale, + A_bulk=self.bulk_species.mass_number, + Z_bulk=self.bulk_species.charge_number, + verbose=self.verbose, + ) + + else: + self._domain = domain + + self._equil = equil + + if MPI.COMM_WORLD.Get_rank() == 0 and self.verbose: + print("\nDOMAIN:") + print("type:".ljust(25), self.domain.__class__.__name__) + for key, val in self.domain.params.items(): + if key not in {"cx", "cy", "cz"}: + print((key + ":").ljust(25), val) + + print("\nFLUID BACKGROUND:") + if self.equil is not None: + print("type:".ljust(25), self.equil.__class__.__name__) + for key, val in self.equil.params.items(): + print((key + ":").ljust(25), val) + else: + print("None.") + ## species @property @@ -144,6 +182,67 @@ def species(self): ## allocate methods + def allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): + # create discrete derham sequence + if self.clone_config is None: + derham_comm = MPI.COMM_WORLD + else: + derham_comm = self.clone_config.sub_comm + + if grid is None or derham_opts is None: + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") + self._derham = None + else: + self._derham = setup_derham( + grid, + derham_opts, + comm=derham_comm, + domain=self.domain, + verbose=self.verbose, + ) + + # create weighted mass and basis operators + if self.derham is None: + self._mass_ops = None + self._basis_ops = None + else: + self._mass_ops = WeightedMassOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + self._basis_ops = BasisProjectionOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + # create projected equilibrium + if self.derham is None: + self._projected_equil = None + else: + if isinstance(self.equil, MHDequilibrium): + self._projected_equil = ProjectedMHDequilibrium( + self.equil, + self.derham, + ) + elif isinstance(self.equil, FluidEquilibriumWithB): + self._projected_equil = ProjectedFluidEquilibriumWithB( + self.equil, + self.derham, + ) + elif isinstance(self.equil, FluidEquilibrium): + self._projected_equil = ProjectedFluidEquilibrium( + self.equil, + self.derham, + ) + else: + self._projected_equil = None + def allocate_propagators(self): # set propagators base class attributes (then available to all propagators) Propagator.derham = self.derham @@ -1408,6 +1507,294 @@ def generate_default_parameter_file( return path + ################### + # Private methods : + ################### + + def compute_plasma_params(self, verbose=True): + """ + Compute and print volume averaged plasma parameters for each species of the model. + + Global parameters: + - plasma volume + - transit length + - magnetic field + + Species dependent parameters: + - mass + - charge + - density + - pressure + - thermal energy kBT + - Alfvén speed v_A + - thermal speed v_th + - thermal frequency Omega_th + - cyclotron frequency Omega_c + - plasma frequency Omega_p + - Alfvèn frequency Omega_A + - thermal Larmor radius rho_th + - MHD length scale v_a/Omega_c + - rho/L + - alpha = Omega_p/Omega_c + - epsilon = 1/(t*Omega_c) + """ + + # units affices for printing + units_affix = {} + units_affix["plasma volume"] = " m³" + units_affix["transit length"] = " m" + units_affix["magnetic field"] = " T" + units_affix["mass"] = " kg" + units_affix["charge"] = " C" + units_affix["density"] = " m⁻³" + units_affix["pressure"] = " bar" + units_affix["kBT"] = " keV" + units_affix["v_A"] = " m/s" + units_affix["v_th"] = " m/s" + units_affix["vth1"] = " m/s" + units_affix["vth2"] = " m/s" + units_affix["vth3"] = " m/s" + units_affix["Omega_th"] = " Mrad/s" + units_affix["Omega_c"] = " Mrad/s" + units_affix["Omega_p"] = " Mrad/s" + units_affix["Omega_A"] = " Mrad/s" + units_affix["rho_th"] = " m" + units_affix["v_A/Omega_c"] = " m" + units_affix["rho_th/L"] = "" + units_affix["alpha"] = "" + units_affix["epsilon"] = "" + + h = 1 / 20 + eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + + ## global parameters + + # plasma volume (hat x^3) + det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) + vol1 = xp.mean(xp.abs(det_tmp)) + # plasma volume (m⁻³) + plasma_volume = vol1 * self.units.x**3 + # transit length (m) + transit_length = plasma_volume ** (1 / 3) + # magnetic field (T) + if isinstance(self.equil, FluidEquilibriumWithB): + B_tmp = self.equil.absB0(eta1, eta2, eta3) + else: + B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) + magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B + B_max = xp.max(B_tmp) * self.units.B + B_min = xp.min(B_tmp) * self.units.B + + if magnetic_field < 1e-14: + magnetic_field = xp.nan + # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("\nPLASMA PARAMETERS:") + print( + "Plasma volume:".ljust(25), + "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], + ) + print( + "Transit length:".ljust(25), + "{:4.3e}".format(transit_length) + units_affix["transit length"], + ) + print( + "Avg. magnetic field:".ljust(25), + "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], + ) + print( + "Max magnetic field:".ljust(25), + "{:4.3e}".format(B_max) + units_affix["magnetic field"], + ) + print( + "Min magnetic field:".ljust(25), + "{:4.3e}".format(B_min) + units_affix["magnetic field"], + ) + + # # species dependent parameters + # self._pparams = {} + + # if len(self.fluid_species) > 0: + # for species, val in self.fluid_species.items(): + # self._pparams[species] = {} + # # type + # self._pparams[species]["type"] = "fluid" + # # mass (kg) + # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p + # # charge (C) + # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e + # # density (m⁻³) + # self._pparams[species]["density"] = ( + # xp.mean( + # self.equil.n0( + # eta1, + # eta2, + # eta3, + # ) + # * xp.abs(det_tmp), + # ) + # * self.units.x ** 3 + # / plasma_volume + # * self.units.n + # ) + # # pressure (bar) + # self._pparams[species]["pressure"] = ( + # xp.mean( + # self.equil.p0( + # eta1, + # eta2, + # eta3, + # ) + # * xp.abs(det_tmp), + # ) + # * self.units.x ** 3 + # / plasma_volume + # * self.units.p + # * 1e-5 + # ) + # # thermal energy (keV) + # self._pparams[species]["kBT"] = self._pparams[species]["pressure"] * 1e5 / self._pparams[species]["density"] / e * 1e-3 + + # if len(self.kinetic) > 0: + # eta1mg, eta2mg, eta3mg = xp.meshgrid( + # eta1, + # eta2, + # eta3, + # indexing="ij", + # ) + + # for species, val in self.kinetic.items(): + # self._pparams[species] = {} + # # type + # self._pparams[species]["type"] = "kinetic" + # # mass (kg) + # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p + # # charge (C) + # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e + + # # create temp kinetic object for (default) parameter extraction + # tmp_bckgr = val["params"]["background"] + + # if val["space"] != "ParticlesSPH": + # tmp = None + # for fi, maxw_params in tmp_bckgr.items(): + # if fi[-2] == "_": + # fi_type = fi[:-2] + # else: + # fi_type = fi + + # if tmp is None: + # tmp = getattr(maxwellians, fi_type)( + # maxw_params=maxw_params, + # equil=self.equil, + # ) + # else: + # tmp = tmp + getattr(maxwellians, fi_type)( + # maxw_params=maxw_params, + # equil=self.equil, + # ) + + # if val["space"] != "ParticlesSPH" and tmp.coords == "constants_of_motion": + # # call parameters + # a1 = self.domain.params_map["a1"] + # r = eta1mg * (1 - a1) + a1 + # psi = self.equil.psi_r(r) + + # # density (m⁻³) + # self._pparams[species]["density"] = ( + # xp.mean(tmp.n(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.n + # ) + # # thermal speed (m/s) + # self._pparams[species]["v_th"] = ( + # xp.mean(tmp.vth(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.v + # ) + # # thermal energy (keV) + # self._pparams[species]["kBT"] = self._pparams[species]["mass"] * self._pparams[species]["v_th"] ** 2 / e * 1e-3 + # # pressure (bar) + # self._pparams[species]["pressure"] = ( + # self._pparams[species]["kBT"] * e * 1e3 * self._pparams[species]["density"] * 1e-5 + # ) + + # else: + # # density (m⁻³) + # # self._pparams[species]['density'] = xp.mean(tmp.n( + # # eta1mg, eta2mg, eta3mg) * xp.abs(det_tmp)) * units['x']**3 / plasma_volume * units['n'] + # self._pparams[species]["density"] = 99.0 + # # thermal speeds (m/s) + # vth = [] + # # vths = tmp.vth(eta1mg, eta2mg, eta3mg) + # vths = [99.0] + # for k in range(len(vths)): + # vth += [ + # vths[k] * xp.abs(det_tmp) * self.units.x ** 3 / plasma_volume * self.units.v, + # ] + # thermal_speed = 0.0 + # for dir in range(val["obj"].vdim): + # # self._pparams[species]['vth' + str(dir + 1)] = xp.mean(vth[dir]) + # self._pparams[species]["vth" + str(dir + 1)] = 99.0 + # thermal_speed += self._pparams[species]["vth" + str(dir + 1)] + # # TODO: here it is assumed that background density parameter is called "n", + # # and that background thermal speeds are called "vthn"; make this a convention? + # # self._pparams[species]['v_th'] = thermal_speed / \ + # # val['obj'].vdim + # self._pparams[species]["v_th"] = 99.0 + # # thermal energy (keV) + # # self._pparams[species]['kBT'] = self._pparams[species]['mass'] * \ + # # self._pparams[species]['v_th']**2 / e * 1e-3 + # self._pparams[species]["kBT"] = 99.0 + # # pressure (bar) + # # self._pparams[species]['pressure'] = self._pparams[species]['kBT'] * \ + # # e * 1e3 * self._pparams[species]['density'] * 1e-5 + # self._pparams[species]["pressure"] = 99.0 + + # for species in self._pparams: + # # alfvén speed (m/s) + # self._pparams[species]["v_A"] = magnetic_field / xp.sqrt( + # mu0 * self._pparams[species]["mass"] * self._pparams[species]["density"], + # ) + # # thermal speed (m/s) + # self._pparams[species]["v_th"] = xp.sqrt( + # self._pparams[species]["kBT"] * 1e3 * e / self._pparams[species]["mass"], + # ) + # # thermal frequency (Mrad/s) + # self._pparams[species]["Omega_th"] = self._pparams[species]["v_th"] / transit_length * 1e-6 + # # cyclotron frequency (Mrad/s) + # self._pparams[species]["Omega_c"] = self._pparams[species]["charge"] * magnetic_field / self._pparams[species]["mass"] * 1e-6 + # # plasma frequency (Mrad/s) + # self._pparams[species]["Omega_p"] = ( + # xp.sqrt( + # self._pparams[species]["density"] * (self._pparams[species]["charge"]) ** 2 / eps0 / self._pparams[species]["mass"], + # ) + # * 1e-6 + # ) + # # alfvén frequency (Mrad/s) + # self._pparams[species]["Omega_A"] = self._pparams[species]["v_A"] / transit_length * 1e-6 + # # Larmor radius (m) + # self._pparams[species]["rho_th"] = self._pparams[species]["v_th"] / (self._pparams[species]["Omega_c"] * 1e6) + # # MHD length scale (m) + # self._pparams[species]["v_A/Omega_c"] = self._pparams[species]["v_A"] / (xp.abs(self._pparams[species]["Omega_c"]) * 1e6) + # # dim-less ratios + # self._pparams[species]["rho_th/L"] = self._pparams[species]["rho_th"] / transit_length + + # if verbose and self.rank_world == 0: + # print("\nSPECIES PARAMETERS:") + # for species, ch in self._pparams.items(): + # print(f"\nname:".ljust(26), species) + # print(f"type:".ljust(25), ch["type"]) + # ch.pop("type") + # print(f"is bulk:".ljust(25), species == self.bulk_species()) + # for kinds, vals in ch.items(): + # print( + # kinds.ljust(25), + # "{:+4.3e}".format( + # vals, + # ), + # units_affix[kinds], + # ) + class MyDumper(yaml.SafeDumper): # HACK: insert blank lines between top-level objects diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index fdc79474f..56b6bebf1 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -11,7 +11,7 @@ # core imports from struphy.models.base import StruphyModel from struphy.geometry.base import Domain -from struphy.fields_background.base import FluidEquilibrium, NumericalMHDequilibrium +from struphy.fields_background.base import FluidEquilibrium, NumericalMHDequilibrium, FluidEquilibriumWithB from struphy.io.setup import setup_folders from struphy.io.options import Units from struphy.utils.clone_config import CloneConfig @@ -70,21 +70,21 @@ def __init__(self, # mpi info if isinstance(MPI, MockMPI): - comm = None - rank = 0 - size = 1 - Barrier = lambda: None + self.comm = None + self.rank = 0 + self.size = 1 + self.Barrier = lambda: None else: - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - Barrier = comm.Barrier + self.comm = MPI.COMM_WORLD + self.rank = self.comm.Get_rank() + self.size = self.comm.Get_size() + self.Barrier = self.comm.Barrier - if rank == 0: + if self.rank == 0: print("") # synchronize MPI processes to set same start time of simulation for all processes - Barrier() + self.Barrier() start_simulation = time.time() # check model @@ -92,7 +92,7 @@ def __init__(self, model.verbose = verbose model_name = model.__class__.__name__ - if rank == 0: + if self.rank == 0: print(f"\n*** Starting run for model '{model_name}':") # meta-data @@ -102,7 +102,7 @@ def __init__(self, save_step = env.save_step sort_step = env.sort_step num_clones = env.num_clones - use_mpi = (comm is not None,) + use_mpi = (self.comm is not None,) meta = {} meta["platform"] = sysconfig.get_platform() @@ -110,14 +110,14 @@ def __init__(self, meta["model name"] = model_name meta["parameter file"] = params_path meta["output folder"] = path_out - meta["MPI processes"] = size + meta["MPI processes"] = self.size meta["use MPI.COMM_WORLD"] = use_mpi meta["number of domain clones"] = num_clones meta["restart"] = restart meta["max wall-clock [min]"] = max_runtime meta["save interval [steps]"] = save_step - if rank == 0: + if self.rank == 0: print("\nMETADATA:") for k, v in meta.items(): print(f"{k}:".ljust(25), v) @@ -130,7 +130,7 @@ def __init__(self, ) # save parameter file - if rank == 0: + if self.rank == 0: # save python param file if params_path is not None: assert params_path[-3:] == ".py" @@ -165,7 +165,7 @@ def __init__(self, pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) # config clones - if comm is None: + if self.comm is None: clone_config = None else: if num_clones == 1: @@ -175,13 +175,13 @@ def __init__(self, # MPI.COMM_WORLD : comm # within a clone: : sub_comm # between the clones : inter_comm - clone_config = CloneConfig(comm=comm, params=None, num_clones=num_clones) + clone_config = CloneConfig(comm=self.comm, params=None, num_clones=num_clones) clone_config.print_clone_config() if model.particle_species: clone_config.print_particle_config() self.clone_config = clone_config - Barrier() + self.Barrier() # units and normalization parameters units = Units(base_units) @@ -216,28 +216,28 @@ def allocate(self, verbose: bool = False): def store_geometry(self, verbose: bool = False): # store geometry vtk - if rank == 0: + if self.rank == 0: grids_log = [ xp.linspace(1e-6, 1.0, 32), xp.linspace(0.0, 1.0, 32), xp.linspace(0.0, 1.0, 32), ] - tmp = model.domain(*grids_log) + tmp = self.domain(*grids_log) grids_phy = [tmp[0], tmp[1], tmp[2]] pointData = {} - det_df = model.domain.jacobian_det(*grids_log) + det_df = self.domain.jacobian_det(*grids_log) pointData["det_df"] = det_df - if model.equil is not None: - p0 = model.equil.p0(*grids_log) + if self.equil is not None: + p0 = self.equil.p0(*grids_log) pointData["p0"] = p0 - if isinstance(model.equil, FluidEquilibriumWithB): - absB0 = model.equil.absB0(*grids_log) + if isinstance(self.equil, FluidEquilibriumWithB): + absB0 = self.equil.absB0(*grids_log) pointData["absB0"] = absB0 - gridToVTK(os.path.join(path_out, "geometry"), *grids_phy, pointData=pointData) + gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) def compute_plasma_params(self, verbose=True): """ From 6f95936cc01360b546548b3e9322394242930773 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 9 Feb 2026 09:04:08 +0100 Subject: [PATCH 03/80] back to devel setup.py --- src/struphy/io/setup.py | 60 +++++++++++++++++++++++++++++++++++++++++ src/struphy/main.py | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/struphy/io/setup.py b/src/struphy/io/setup.py index af5e10f3d..d8e2be106 100644 --- a/src/struphy/io/setup.py +++ b/src/struphy/io/setup.py @@ -23,6 +23,66 @@ def import_parameters_py(params_path: str) -> ModuleType: return params_in +def setup_folders( + path_out: str, + restart: bool, + verbose: bool = False, +): + """ + Setup output folders. + """ + if MPI.COMM_WORLD.Get_rank() == 0: + if verbose: + print("\nPREPARATION AND CLEAN-UP:") + + # create output folder if it does not exit + if not os.path.exists(path_out): + os.makedirs(path_out, exist_ok=True) + if verbose: + print("Created folder " + path_out) + + # create data folder in output folder if it does not exist + if not os.path.exists(os.path.join(path_out, "data/")): + os.mkdir(os.path.join(path_out, "data/")) + if verbose: + print("Created folder " + os.path.join(path_out, "data/")) + else: + # remove post_processing folder + folder = os.path.join(path_out, "post_processing") + if os.path.exists(folder): + shutil.rmtree(folder) + if verbose: + print("Removed existing folder " + folder) + + # remove meta file + file = os.path.join(path_out, "meta.txt") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove profiling file + file = os.path.join(path_out, "profile_tmp") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove .png files (if NOT a restart) + if not restart: + files = glob.glob(os.path.join(path_out, "*.png")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + def setup_derham( grid: TensorProductGrid, options: DerhamOptions, diff --git a/src/struphy/main.py b/src/struphy/main.py index 95a4c62cd..7f84c0c1e 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -222,7 +222,7 @@ def run( model.allocate_feec(grid, derham_opts) # equation paramters - model.set_normalization_params(units=model.units, verbose=verbose) + model.setup_equation_params(units=model.units, verbose=verbose) # allocate variables model.allocate_variables(verbose=verbose) From be045e132733dda4fecf3635511ac10cf82af5b0 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 9 Feb 2026 10:56:56 +0100 Subject: [PATCH 04/80] use Simulations in main.run() --- src/struphy/main.py | 82 ++- src/struphy/simulation/sim.py | 1152 ++++++++++++++++----------------- 2 files changed, 612 insertions(+), 622 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index 7f84c0c1e..b3bc0afa3 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -43,6 +43,8 @@ from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml +from struphy.simulation.sim import Simulation + @profile def run( @@ -70,33 +72,21 @@ def run( Absolute path to .py parameter file. """ - ProfileManager.setup( - profiling_activated=env.profiling_activated, - time_trace=env.profiling_trace, - use_likwid=False, - file_path=os.path.join( - env.out_folders, - env.sim_folder, - "profiling_data.h5", - ), + sim = Simulation( + model=model, + params_path=params_path, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, ) - if isinstance(MPI, MockMPI): - comm = None - rank = 0 - size = 1 - Barrier = lambda: None - else: - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - Barrier = comm.Barrier - - if rank == 0: - print("") - # synchronize MPI processes to set same start time of simulation for all processes - Barrier() + sim.Barrier() start_simulation = time.time() # check model @@ -104,7 +94,7 @@ def run( model_name = model.__class__.__name__ model.verbose = verbose - if rank == 0: + if sim.rank == 0: print(f"\n*** Starting run for model '{model_name}':") # meta-data @@ -114,7 +104,7 @@ def run( save_step = env.save_step sort_step = env.sort_step num_clones = env.num_clones - use_mpi = (comm is not None,) + use_mpi = (sim.comm is not None,) meta = {} meta["platform"] = sysconfig.get_platform() @@ -122,14 +112,14 @@ def run( meta["model name"] = model_name meta["parameter file"] = params_path meta["output folder"] = path_out - meta["MPI processes"] = size + meta["MPI processes"] = sim.size meta["use MPI.COMM_WORLD"] = use_mpi meta["number of domain clones"] = num_clones meta["restart"] = restart meta["max wall-clock [min]"] = max_runtime meta["save interval [steps]"] = save_step - if rank == 0: + if sim.rank == 0: print("\nMETADATA:") for k, v in meta.items(): print(f"{k}:".ljust(25), v) @@ -145,7 +135,7 @@ def run( units = Units(base_units) # save parameter file - if rank == 0: + if sim.rank == 0: # save python param file if params_path is not None: assert params_path[-3:] == ".py" @@ -180,7 +170,7 @@ def run( pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) # config clones - if comm is None: + if sim.comm is None: clone_config = None else: if num_clones == 1: @@ -190,13 +180,13 @@ def run( # MPI.COMM_WORLD : comm # within a clone: : sub_comm # between the clones : inter_comm - clone_config = CloneConfig(comm=comm, params=None, num_clones=num_clones) + clone_config = CloneConfig(comm=sim.comm, params=None, num_clones=num_clones) clone_config.print_clone_config() if model.particle_species: clone_config.print_particle_config() model.clone_config = clone_config - Barrier() + sim.Barrier() ## configure model instance @@ -234,16 +224,16 @@ def run( # plasma parameters model.compute_plasma_params(verbose=verbose) - if rank < 32: - if rank == 0: + if sim.rank < 32: + if sim.rank == 0: print("") - print(f"Rank {rank}: executing main.run() for model {model_name} ...") + print(f"Rank {sim.rank}: executing main.run() for model {model_name} ...") - if size > 32 and rank == 32: + if sim.size > 32 and sim.rank == 32: print(f"Ranks > 31: executing main.run() for model {model_name} ...") # store geometry vtk - if rank == 0: + if sim.rank == 0: grids_log = [ xp.linspace(1e-6, 1.0, 32), xp.linspace(0.0, 1.0, 32), @@ -268,7 +258,7 @@ def run( # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs - data = DataContainer(path_out, comm=comm) + data = DataContainer(path_out, comm=sim.comm) # time quantities (current time value, value in seconds and index) time_state = {} @@ -308,11 +298,11 @@ def run( model.add_time_state(time_state["value"]) # add all variables to be saved to data object - save_keys_all, save_keys_end = model.initialize_data_output(data, size) + save_keys_all, save_keys_end = model.initialize_data_output(data, sim.size) # ======================== main time loop ====================== model.update_scalar_quantities() - if rank == 0: + if sim.rank == 0: print("\nINITIAL SCALAR QUANTITIES:") model.print_scalar_quantities() @@ -321,7 +311,7 @@ def run( # time loop run_time_now = 0.0 while True: - Barrier() + sim.Barrier() # stop time loop? break_cond_1 = time_state["value"][0] >= Tend @@ -331,7 +321,7 @@ def run( # save restart data (other data already saved below) data.save_data(keys=save_keys_end) end_simulation = time.time() - if rank == 0: + if sim.rank == 0: print(f"\nTime steps done: {time_state['index'][0]}") print( "wall-clock time of simulation [sec]: ", @@ -346,7 +336,7 @@ def run( if isinstance(val, Particles): val.do_sort() t1 = time.time() - if rank == 0 and verbose: + if sim.rank == 0 and verbose: message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( run_time_now * 60, t1 - t0, @@ -388,7 +378,7 @@ def run( data.save_data(keys=save_keys_all) # print current time and scalar quantities to screen - if rank == 0 and verbose: + if sim.rank == 0 and verbose: step = str(time_state["index"][0]).zfill(len(total_steps)) message = "time step: " + step + "/" + str(total_steps) @@ -409,9 +399,9 @@ def run( # =================================================================== meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 - Barrier() + sim.Barrier() - if rank == 0: + if sim.rank == 0: # save meta-data dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) print("Struphy run finished.") diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 56b6bebf1..e191b564c 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -13,7 +13,7 @@ from struphy.geometry.base import Domain from struphy.fields_background.base import FluidEquilibrium, NumericalMHDequilibrium, FluidEquilibriumWithB from struphy.io.setup import setup_folders -from struphy.io.options import Units +from struphy.physics.physics import Units from struphy.utils.clone_config import CloneConfig # third party imports @@ -83,584 +83,584 @@ def __init__(self, if self.rank == 0: print("") - # synchronize MPI processes to set same start time of simulation for all processes - self.Barrier() - start_simulation = time.time() - - # check model - assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" - model.verbose = verbose - model_name = model.__class__.__name__ - - if self.rank == 0: - print(f"\n*** Starting run for model '{model_name}':") - - # meta-data - path_out = env.path_out - restart = env.restart - max_runtime = env.max_runtime - save_step = env.save_step - sort_step = env.sort_step - num_clones = env.num_clones - use_mpi = (self.comm is not None,) - - meta = {} - meta["platform"] = sysconfig.get_platform() - meta["python version"] = sysconfig.get_python_version() - meta["model name"] = model_name - meta["parameter file"] = params_path - meta["output folder"] = path_out - meta["MPI processes"] = self.size - meta["use MPI.COMM_WORLD"] = use_mpi - meta["number of domain clones"] = num_clones - meta["restart"] = restart - meta["max wall-clock [min]"] = max_runtime - meta["save interval [steps]"] = save_step - - if self.rank == 0: - print("\nMETADATA:") - for k, v in meta.items(): - print(f"{k}:".ljust(25), v) - - # creating output folders - self._setup_folders( - path_out=path_out, - restart=restart, - verbose=verbose, - ) - - # save parameter file - if self.rank == 0: - # save python param file - if params_path is not None: - assert params_path[-3:] == ".py" - shutil.copy2( - params_path, - os.path.join(path_out, "parameters.py"), - ) - # pickle struphy objects - else: - with open(os.path.join(path_out, "env.bin"), "wb") as f: - pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "base_units.bin"), "wb") as f: - pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: - pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "domain.bin"), "wb") as f: - # WORKAROUND: cannot pickle pyccelized classes at the moment - tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} - pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "equil.bin"), "wb") as f: - # WORKAROUND: cannot pickle pyccelized classes at the moment - if equil is not None: - tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} - else: - tmp_dct = {} - pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "grid.bin"), "wb") as f: - pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: - pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "model_class.bin"), "wb") as f: - pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) - - # config clones - if self.comm is None: - clone_config = None - else: - if num_clones == 1: - clone_config = None - else: - # Setup domain cloning communicators - # MPI.COMM_WORLD : comm - # within a clone: : sub_comm - # between the clones : inter_comm - clone_config = CloneConfig(comm=self.comm, params=None, num_clones=num_clones) - clone_config.print_clone_config() - if model.particle_species: - clone_config.print_particle_config() - - self.clone_config = clone_config - self.Barrier() + # # synchronize MPI processes to set same start time of simulation for all processes + # self.Barrier() + # start_simulation = time.time() + + # # check model + # assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" + # model.verbose = verbose + # model_name = model.__class__.__name__ + + # if self.rank == 0: + # print(f"\n*** Starting run for model '{model_name}':") + + # # meta-data + # path_out = env.path_out + # restart = env.restart + # max_runtime = env.max_runtime + # save_step = env.save_step + # sort_step = env.sort_step + # num_clones = env.num_clones + # use_mpi = (self.comm is not None,) + + # meta = {} + # meta["platform"] = sysconfig.get_platform() + # meta["python version"] = sysconfig.get_python_version() + # meta["model name"] = model_name + # meta["parameter file"] = params_path + # meta["output folder"] = path_out + # meta["MPI processes"] = self.size + # meta["use MPI.COMM_WORLD"] = use_mpi + # meta["number of domain clones"] = num_clones + # meta["restart"] = restart + # meta["max wall-clock [min]"] = max_runtime + # meta["save interval [steps]"] = save_step + + # if self.rank == 0: + # print("\nMETADATA:") + # for k, v in meta.items(): + # print(f"{k}:".ljust(25), v) + + # # creating output folders + # self._setup_folders( + # path_out=path_out, + # restart=restart, + # verbose=verbose, + # ) + + # # save parameter file + # if self.rank == 0: + # # save python param file + # if params_path is not None: + # assert params_path[-3:] == ".py" + # shutil.copy2( + # params_path, + # os.path.join(path_out, "parameters.py"), + # ) + # # pickle struphy objects + # else: + # with open(os.path.join(path_out, "env.bin"), "wb") as f: + # pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "base_units.bin"), "wb") as f: + # pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: + # pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "domain.bin"), "wb") as f: + # # WORKAROUND: cannot pickle pyccelized classes at the moment + # tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} + # pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "equil.bin"), "wb") as f: + # # WORKAROUND: cannot pickle pyccelized classes at the moment + # if equil is not None: + # tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} + # else: + # tmp_dct = {} + # pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "grid.bin"), "wb") as f: + # pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: + # pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) + # with open(os.path.join(path_out, "model_class.bin"), "wb") as f: + # pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) + + # # config clones + # if self.comm is None: + # clone_config = None + # else: + # if num_clones == 1: + # clone_config = None + # else: + # # Setup domain cloning communicators + # # MPI.COMM_WORLD : comm + # # within a clone: : sub_comm + # # between the clones : inter_comm + # clone_config = CloneConfig(comm=self.comm, params=None, num_clones=num_clones) + # clone_config.print_clone_config() + # if model.particle_species: + # clone_config.print_particle_config() + + # self.clone_config = clone_config + # self.Barrier() - # units and normalization parameters - units = Units(base_units) - self.units = units - if model.bulk_species is None: - A_bulk = None - Z_bulk = None - else: - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species.charge_number - self.units.derive_units( - velocity_scale=model.velocity_scale, - A_bulk=A_bulk, - Z_bulk=Z_bulk, - verbose=verbose, - ) - model.set_normalization_params(units=self.units, verbose=verbose) - - # domain and fluid background - self._setup_domain_and_equil(domain, equil, verbose=verbose) - - def allocate(self, verbose: bool = False): - # feec - self._allocate_feec(self.grid, self.derham_opts) + # # units and normalization parameters + # units = Units(base_units) + # self.units = units + # if model.bulk_species is None: + # A_bulk = None + # Z_bulk = None + # else: + # A_bulk = model.bulk_species.mass_number + # Z_bulk = model.bulk_species.charge_number + # self.units.derive_units( + # velocity_scale=model.velocity_scale, + # A_bulk=A_bulk, + # Z_bulk=Z_bulk, + # verbose=verbose, + # ) + # model.set_normalization_params(units=self.units, verbose=verbose) + + # # domain and fluid background + # self._setup_domain_and_equil(domain, equil, verbose=verbose) + + # def allocate(self, verbose: bool = False): + # # feec + # self._allocate_feec(self.grid, self.derham_opts) - # allocate model variables - self.model.allocate_variables(verbose=verbose) - self.model.allocate_helpers() - - # pass info to propagators - self.model.allocate_propagators() - - def store_geometry(self, verbose: bool = False): - # store geometry vtk - if self.rank == 0: - grids_log = [ - xp.linspace(1e-6, 1.0, 32), - xp.linspace(0.0, 1.0, 32), - xp.linspace(0.0, 1.0, 32), - ] - - tmp = self.domain(*grids_log) - grids_phy = [tmp[0], tmp[1], tmp[2]] - - pointData = {} - det_df = self.domain.jacobian_det(*grids_log) - pointData["det_df"] = det_df - - if self.equil is not None: - p0 = self.equil.p0(*grids_log) - pointData["p0"] = p0 - if isinstance(self.equil, FluidEquilibriumWithB): - absB0 = self.equil.absB0(*grids_log) - pointData["absB0"] = absB0 - - gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) - - def compute_plasma_params(self, verbose=True): - """ - Compute and print volume averaged plasma parameters for each species of the model. - - Global parameters: - - plasma volume - - transit length - - magnetic field - - Species dependent parameters: - - mass - - charge - - density - - pressure - - thermal energy kBT - - Alfvén speed v_A - - thermal speed v_th - - thermal frequency Omega_th - - cyclotron frequency Omega_c - - plasma frequency Omega_p - - Alfvèn frequency Omega_A - - thermal Larmor radius rho_th - - MHD length scale v_a/Omega_c - - rho/L - - alpha = Omega_p/Omega_c - - epsilon = 1/(t*Omega_c) - """ - - # units affices for printing - units_affix = {} - units_affix["plasma volume"] = " m³" - units_affix["transit length"] = " m" - units_affix["magnetic field"] = " T" - units_affix["mass"] = " kg" - units_affix["charge"] = " C" - units_affix["density"] = " m⁻³" - units_affix["pressure"] = " bar" - units_affix["kBT"] = " keV" - units_affix["v_A"] = " m/s" - units_affix["v_th"] = " m/s" - units_affix["vth1"] = " m/s" - units_affix["vth2"] = " m/s" - units_affix["vth3"] = " m/s" - units_affix["Omega_th"] = " Mrad/s" - units_affix["Omega_c"] = " Mrad/s" - units_affix["Omega_p"] = " Mrad/s" - units_affix["Omega_A"] = " Mrad/s" - units_affix["rho_th"] = " m" - units_affix["v_A/Omega_c"] = " m" - units_affix["rho_th/L"] = "" - units_affix["alpha"] = "" - units_affix["epsilon"] = "" - - h = 1 / 20 - eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - - ## global parameters - - # plasma volume (hat x^3) - det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) - vol1 = xp.mean(xp.abs(det_tmp)) - # plasma volume (m⁻³) - plasma_volume = vol1 * self.units.x**3 - # transit length (m) - transit_length = plasma_volume ** (1 / 3) - # magnetic field (T) - if isinstance(self.equil, FluidEquilibriumWithB): - B_tmp = self.equil.absB0(eta1, eta2, eta3) - else: - B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) - magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B - B_max = xp.max(B_tmp) * self.units.B - B_min = xp.min(B_tmp) * self.units.B - - if magnetic_field < 1e-14: - magnetic_field = xp.nan - # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") - - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print("\nPLASMA PARAMETERS:") - print( - "Plasma volume:".ljust(25), - "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], - ) - print( - "Transit length:".ljust(25), - "{:4.3e}".format(transit_length) + units_affix["transit length"], - ) - print( - "Avg. magnetic field:".ljust(25), - "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], - ) - print( - "Max magnetic field:".ljust(25), - "{:4.3e}".format(B_max) + units_affix["magnetic field"], - ) - print( - "Min magnetic field:".ljust(25), - "{:4.3e}".format(B_min) + units_affix["magnetic field"], - ) - - def run(self, verbose: bool = False): - if rank < 32: - if rank == 0: - print("") - print(f"Rank {rank}: executing main.run() for model {model_name} ...") - - if size > 32 and rank == 32: - print(f"Ranks > 31: executing main.run() for model {model_name} ...") - - # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) - # use MPI.COMM_WORLD as communicator when storing the outputs - data = DataContainer(path_out, comm=comm) - - # time quantities (current time value, value in seconds and index) - time_state = {} - time_state["value"] = xp.zeros(1, dtype=float) - time_state["value_sec"] = xp.zeros(1, dtype=float) - time_state["index"] = xp.zeros(1, dtype=int) - - # add time quantities to data object for saving - for key, val in time_state.items(): - key_time = "time/" + key - key_time_restart = "restart/time/" + key - data.add_data({key_time: val}) - data.add_data({key_time_restart: val}) - - # retrieve time parameters - dt = time_opts.dt - Tend = time_opts.Tend - split_algo = time_opts.split_algo - - # set initial conditions for all variables - if restart: - model.initialize_from_restart(data) - - with h5py.File(data.file_path, "a") as file: - time_state["value"][0] = file["restart/time/value"][-1] - time_state["value_sec"][0] = file["restart/time/value_sec"][-1] - time_state["index"][0] = file["restart/time/index"][-1] - - total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) - else: - total_steps = str(int(round(Tend / dt))) - - # compute initial scalars and kinetic data, pass time state to all propagators - model.update_scalar_quantities() - model.update_markers_to_be_saved() - model.update_distr_functions() - model.add_time_state(time_state["value"]) - - # add all variables to be saved to data object - save_keys_all, save_keys_end = model.initialize_data_output(data, size) - - # ======================== main time loop ====================== - model.update_scalar_quantities() - if rank == 0: - print("\nINITIAL SCALAR QUANTITIES:") - model.print_scalar_quantities() - - print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") - - # time loop - run_time_now = 0.0 - while True: - Barrier() - - # stop time loop? - break_cond_1 = time_state["value"][0] >= Tend - break_cond_2 = run_time_now > max_runtime - - if break_cond_1 or break_cond_2: - # save restart data (other data already saved below) - data.save_data(keys=save_keys_end) - end_simulation = time.time() - if rank == 0: - print(f"\nTime steps done: {time_state['index'][0]}") - print( - "wall-clock time of simulation [sec]: ", - end_simulation - start_simulation, - ) - print() - break - - if sort_step and time_state["index"][0] % sort_step == 0: - t0 = time.time() - for key, val in model.pointer.items(): - if isinstance(val, Particles): - val.do_sort() - t1 = time.time() - if rank == 0 and verbose: - message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( - run_time_now * 60, - t1 - t0, - ) - print(message, end="\n") - print() - - # update time and index (round time to 10 decimals for a clean time grid!) - time_state["value"][0] = round(time_state["value"][0] + dt, 10) - time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * model.units.t, 10) - time_state["index"][0] += 1 - - # perform one time step dt - t0 = time.time() - with ProfileManager.profile_region("model.integrate"): - model.integrate(dt, split_algo) - t1 = time.time() - - run_time_now = (time.time() - start_simulation) / 60 - - # update diagnostics data and save data - if time_state["index"][0] % save_step == 0: - # compute scalars and kinetic data - model.update_scalar_quantities() - model.update_markers_to_be_saved() - model.update_distr_functions() - - # extract FEEC coefficients - feec_species = model.field_species | model.fluid_species | model.diagnostic_species - for species, val in feec_species.items(): - assert isinstance(val, Species) - for variable, subval in val.variables.items(): - assert isinstance(subval, FEECVariable) - spline = subval.spline - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - spline.extract_coeffs(update_ghost_regions=False) - - # save data (everything but restart data) - data.save_data(keys=save_keys_all) - - # print current time and scalar quantities to screen - if rank == 0 and verbose: - step = str(time_state["index"][0]).zfill(len(total_steps)) - - message = "time step: " + step + "/" + str(total_steps) - message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) - message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( - time_state["value_sec"][0], - Tend * model.units.t, - ) - message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( - run_time_now * 60, - t1 - t0, - ) - - print(message, end="\n") - model.print_scalar_quantities() - print() - - # =================================================================== - - meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 - Barrier() - - if rank == 0: - # save meta-data - dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) - print("Struphy run finished.") - - if clone_config is not None: - clone_config.free() - - ProfileManager.finalize() + # # allocate model variables + # self.model.allocate_variables(verbose=verbose) + # self.model.allocate_helpers() + + # # pass info to propagators + # self.model.allocate_propagators() + + # def store_geometry(self, verbose: bool = False): + # # store geometry vtk + # if self.rank == 0: + # grids_log = [ + # xp.linspace(1e-6, 1.0, 32), + # xp.linspace(0.0, 1.0, 32), + # xp.linspace(0.0, 1.0, 32), + # ] + + # tmp = self.domain(*grids_log) + # grids_phy = [tmp[0], tmp[1], tmp[2]] + + # pointData = {} + # det_df = self.domain.jacobian_det(*grids_log) + # pointData["det_df"] = det_df + + # if self.equil is not None: + # p0 = self.equil.p0(*grids_log) + # pointData["p0"] = p0 + # if isinstance(self.equil, FluidEquilibriumWithB): + # absB0 = self.equil.absB0(*grids_log) + # pointData["absB0"] = absB0 + + # gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) + + # def compute_plasma_params(self, verbose=True): + # """ + # Compute and print volume averaged plasma parameters for each species of the model. + + # Global parameters: + # - plasma volume + # - transit length + # - magnetic field + + # Species dependent parameters: + # - mass + # - charge + # - density + # - pressure + # - thermal energy kBT + # - Alfvén speed v_A + # - thermal speed v_th + # - thermal frequency Omega_th + # - cyclotron frequency Omega_c + # - plasma frequency Omega_p + # - Alfvèn frequency Omega_A + # - thermal Larmor radius rho_th + # - MHD length scale v_a/Omega_c + # - rho/L + # - alpha = Omega_p/Omega_c + # - epsilon = 1/(t*Omega_c) + # """ + + # # units affices for printing + # units_affix = {} + # units_affix["plasma volume"] = " m³" + # units_affix["transit length"] = " m" + # units_affix["magnetic field"] = " T" + # units_affix["mass"] = " kg" + # units_affix["charge"] = " C" + # units_affix["density"] = " m⁻³" + # units_affix["pressure"] = " bar" + # units_affix["kBT"] = " keV" + # units_affix["v_A"] = " m/s" + # units_affix["v_th"] = " m/s" + # units_affix["vth1"] = " m/s" + # units_affix["vth2"] = " m/s" + # units_affix["vth3"] = " m/s" + # units_affix["Omega_th"] = " Mrad/s" + # units_affix["Omega_c"] = " Mrad/s" + # units_affix["Omega_p"] = " Mrad/s" + # units_affix["Omega_A"] = " Mrad/s" + # units_affix["rho_th"] = " m" + # units_affix["v_A/Omega_c"] = " m" + # units_affix["rho_th/L"] = "" + # units_affix["alpha"] = "" + # units_affix["epsilon"] = "" + + # h = 1 / 20 + # eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + # eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + # eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + + # ## global parameters + + # # plasma volume (hat x^3) + # det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) + # vol1 = xp.mean(xp.abs(det_tmp)) + # # plasma volume (m⁻³) + # plasma_volume = vol1 * self.units.x**3 + # # transit length (m) + # transit_length = plasma_volume ** (1 / 3) + # # magnetic field (T) + # if isinstance(self.equil, FluidEquilibriumWithB): + # B_tmp = self.equil.absB0(eta1, eta2, eta3) + # else: + # B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) + # magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B + # B_max = xp.max(B_tmp) * self.units.B + # B_min = xp.min(B_tmp) * self.units.B + + # if magnetic_field < 1e-14: + # magnetic_field = xp.nan + # # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") + + # if verbose and MPI.COMM_WORLD.Get_rank() == 0: + # print("\nPLASMA PARAMETERS:") + # print( + # "Plasma volume:".ljust(25), + # "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], + # ) + # print( + # "Transit length:".ljust(25), + # "{:4.3e}".format(transit_length) + units_affix["transit length"], + # ) + # print( + # "Avg. magnetic field:".ljust(25), + # "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], + # ) + # print( + # "Max magnetic field:".ljust(25), + # "{:4.3e}".format(B_max) + units_affix["magnetic field"], + # ) + # print( + # "Min magnetic field:".ljust(25), + # "{:4.3e}".format(B_min) + units_affix["magnetic field"], + # ) + + # def run(self, verbose: bool = False): + # if rank < 32: + # if rank == 0: + # print("") + # print(f"Rank {rank}: executing main.run() for model {model_name} ...") + + # if size > 32 and rank == 32: + # print(f"Ranks > 31: executing main.run() for model {model_name} ...") + + # # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) + # # use MPI.COMM_WORLD as communicator when storing the outputs + # data = DataContainer(path_out, comm=comm) + + # # time quantities (current time value, value in seconds and index) + # time_state = {} + # time_state["value"] = xp.zeros(1, dtype=float) + # time_state["value_sec"] = xp.zeros(1, dtype=float) + # time_state["index"] = xp.zeros(1, dtype=int) + + # # add time quantities to data object for saving + # for key, val in time_state.items(): + # key_time = "time/" + key + # key_time_restart = "restart/time/" + key + # data.add_data({key_time: val}) + # data.add_data({key_time_restart: val}) + + # # retrieve time parameters + # dt = time_opts.dt + # Tend = time_opts.Tend + # split_algo = time_opts.split_algo + + # # set initial conditions for all variables + # if restart: + # model.initialize_from_restart(data) + + # with h5py.File(data.file_path, "a") as file: + # time_state["value"][0] = file["restart/time/value"][-1] + # time_state["value_sec"][0] = file["restart/time/value_sec"][-1] + # time_state["index"][0] = file["restart/time/index"][-1] + + # total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) + # else: + # total_steps = str(int(round(Tend / dt))) + + # # compute initial scalars and kinetic data, pass time state to all propagators + # model.update_scalar_quantities() + # model.update_markers_to_be_saved() + # model.update_distr_functions() + # model.add_time_state(time_state["value"]) + + # # add all variables to be saved to data object + # save_keys_all, save_keys_end = model.initialize_data_output(data, size) + + # # ======================== main time loop ====================== + # model.update_scalar_quantities() + # if rank == 0: + # print("\nINITIAL SCALAR QUANTITIES:") + # model.print_scalar_quantities() + + # print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") + + # # time loop + # run_time_now = 0.0 + # while True: + # Barrier() + + # # stop time loop? + # break_cond_1 = time_state["value"][0] >= Tend + # break_cond_2 = run_time_now > max_runtime + + # if break_cond_1 or break_cond_2: + # # save restart data (other data already saved below) + # data.save_data(keys=save_keys_end) + # end_simulation = time.time() + # if rank == 0: + # print(f"\nTime steps done: {time_state['index'][0]}") + # print( + # "wall-clock time of simulation [sec]: ", + # end_simulation - start_simulation, + # ) + # print() + # break + + # if sort_step and time_state["index"][0] % sort_step == 0: + # t0 = time.time() + # for key, val in model.pointer.items(): + # if isinstance(val, Particles): + # val.do_sort() + # t1 = time.time() + # if rank == 0 and verbose: + # message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( + # run_time_now * 60, + # t1 - t0, + # ) + # print(message, end="\n") + # print() + + # # update time and index (round time to 10 decimals for a clean time grid!) + # time_state["value"][0] = round(time_state["value"][0] + dt, 10) + # time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * model.units.t, 10) + # time_state["index"][0] += 1 + + # # perform one time step dt + # t0 = time.time() + # with ProfileManager.profile_region("model.integrate"): + # model.integrate(dt, split_algo) + # t1 = time.time() + + # run_time_now = (time.time() - start_simulation) / 60 + + # # update diagnostics data and save data + # if time_state["index"][0] % save_step == 0: + # # compute scalars and kinetic data + # model.update_scalar_quantities() + # model.update_markers_to_be_saved() + # model.update_distr_functions() + + # # extract FEEC coefficients + # feec_species = model.field_species | model.fluid_species | model.diagnostic_species + # for species, val in feec_species.items(): + # assert isinstance(val, Species) + # for variable, subval in val.variables.items(): + # assert isinstance(subval, FEECVariable) + # spline = subval.spline + # # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! + # spline.extract_coeffs(update_ghost_regions=False) + + # # save data (everything but restart data) + # data.save_data(keys=save_keys_all) + + # # print current time and scalar quantities to screen + # if rank == 0 and verbose: + # step = str(time_state["index"][0]).zfill(len(total_steps)) + + # message = "time step: " + step + "/" + str(total_steps) + # message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) + # message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( + # time_state["value_sec"][0], + # Tend * model.units.t, + # ) + # message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( + # run_time_now * 60, + # t1 - t0, + # ) + + # print(message, end="\n") + # model.print_scalar_quantities() + # print() + + # # =================================================================== + + # meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 + # Barrier() + + # if rank == 0: + # # save meta-data + # dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) + # print("Struphy run finished.") + + # if clone_config is not None: + # clone_config.free() + + # ProfileManager.finalize() - def _setup_folders( - path_out: str, - restart: bool, - verbose: bool = False, - ): - """ - Setup output folders. - """ - if MPI.COMM_WORLD.Get_rank() == 0: - if verbose: - print("\nPREPARATION AND CLEAN-UP:") - - # create output folder if it does not exit - if not os.path.exists(path_out): - os.makedirs(path_out, exist_ok=True) - if verbose: - print("Created folder " + path_out) - - # create data folder in output folder if it does not exist - if not os.path.exists(os.path.join(path_out, "data/")): - os.mkdir(os.path.join(path_out, "data/")) - if verbose: - print("Created folder " + os.path.join(path_out, "data/")) - else: - # remove post_processing folder - folder = os.path.join(path_out, "post_processing") - if os.path.exists(folder): - shutil.rmtree(folder) - if verbose: - print("Removed existing folder " + folder) - - # remove meta file - file = os.path.join(path_out, "meta.txt") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove profiling file - file = os.path.join(path_out, "profile_tmp") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove .png files (if NOT a restart) - if not restart: - files = glob.glob(os.path.join(path_out, "*.png")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) + # def _setup_folders( + # path_out: str, + # restart: bool, + # verbose: bool = False, + # ): + # """ + # Setup output folders. + # """ + # if MPI.COMM_WORLD.Get_rank() == 0: + # if verbose: + # print("\nPREPARATION AND CLEAN-UP:") + + # # create output folder if it does not exit + # if not os.path.exists(path_out): + # os.makedirs(path_out, exist_ok=True) + # if verbose: + # print("Created folder " + path_out) + + # # create data folder in output folder if it does not exist + # if not os.path.exists(os.path.join(path_out, "data/")): + # os.mkdir(os.path.join(path_out, "data/")) + # if verbose: + # print("Created folder " + os.path.join(path_out, "data/")) + # else: + # # remove post_processing folder + # folder = os.path.join(path_out, "post_processing") + # if os.path.exists(folder): + # shutil.rmtree(folder) + # if verbose: + # print("Removed existing folder " + folder) + + # # remove meta file + # file = os.path.join(path_out, "meta.txt") + # if os.path.exists(file): + # os.remove(file) + # if verbose: + # print("Removed existing file " + file) + + # # remove profiling file + # file = os.path.join(path_out, "profile_tmp") + # if os.path.exists(file): + # os.remove(file) + # if verbose: + # print("Removed existing file " + file) + + # # remove .png files (if NOT a restart) + # if not restart: + # files = glob.glob(os.path.join(path_out, "*.png")) + # for n, file in enumerate(files): + # os.remove(file) + # if verbose and n < 10: # print only ten statements in case of many processes + # print("Removed existing file " + file) + + # files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) + # for n, file in enumerate(files): + # os.remove(file) + # if verbose and n < 10: # print only ten statements in case of many processes + # print("Removed existing file " + file) - def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): - """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" - if equil is not None: - if isinstance(equil, NumericalMHDequilibrium): - self._domain = equil.domain - else: - self._domain = domain - equil.domain = domain - - if hasattr(equil, "units"): - assert isinstance(equil.units, Units) - equil.units.derive_units( - velocity_scale=self.velocity_scale, - A_bulk=self.bulk_species.mass_number, - Z_bulk=self.bulk_species.charge_number, - verbose=self.verbose, - ) - - else: - self._domain = domain - - self._equil = equil - - if MPI.COMM_WORLD.Get_rank() == 0 and verbose: - print("\nDOMAIN:") - print("type:".ljust(25), self.domain.__class__.__name__) - for key, val in self.domain.params.items(): - if key not in {"cx", "cy", "cz"}: - print((key + ":").ljust(25), val) - - print("\nFLUID BACKGROUND:") - if self.equil is not None: - print("type:".ljust(25), self.equil.__class__.__name__) - for key, val in self.equil.params.items(): - print((key + ":").ljust(25), val) - else: - print("None.") + # def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): + # """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" + # if equil is not None: + # if isinstance(equil, NumericalMHDequilibrium): + # self._domain = equil.domain + # else: + # self._domain = domain + # equil.domain = domain + + # if hasattr(equil, "units"): + # assert isinstance(equil.units, Units) + # equil.units.derive_units( + # velocity_scale=self.velocity_scale, + # A_bulk=self.bulk_species.mass_number, + # Z_bulk=self.bulk_species.charge_number, + # verbose=self.verbose, + # ) + + # else: + # self._domain = domain + + # self._equil = equil + + # if MPI.COMM_WORLD.Get_rank() == 0 and verbose: + # print("\nDOMAIN:") + # print("type:".ljust(25), self.domain.__class__.__name__) + # for key, val in self.domain.params.items(): + # if key not in {"cx", "cy", "cz"}: + # print((key + ":").ljust(25), val) + + # print("\nFLUID BACKGROUND:") + # if self.equil is not None: + # print("type:".ljust(25), self.equil.__class__.__name__) + # for key, val in self.equil.params.items(): + # print((key + ":").ljust(25), val) + # else: + # print("None.") - def _allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): - # create discrete derham sequence - if self.clone_config is None: - derham_comm = MPI.COMM_WORLD - else: - derham_comm = self.clone_config.sub_comm - - if grid is None or derham_opts is None: - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") - self._derham = None - else: - self._derham = setup_derham( - grid, - derham_opts, - comm=derham_comm, - domain=self.domain, - verbose=self.verbose, - ) - - # create weighted mass and basis operators - if self.derham is None: - self._mass_ops = None - self._basis_ops = None - else: - self._mass_ops = WeightedMassOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - self._basis_ops = BasisProjectionOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - # create projected equilibrium - if self.derham is None: - self._projected_equil = None - else: - if isinstance(self.equil, MHDequilibrium): - self._projected_equil = ProjectedMHDequilibrium( - self.equil, - self.derham, - ) - elif isinstance(self.equil, FluidEquilibriumWithB): - self._projected_equil = ProjectedFluidEquilibriumWithB( - self.equil, - self.derham, - ) - elif isinstance(self.equil, FluidEquilibrium): - self._projected_equil = ProjectedFluidEquilibrium( - self.equil, - self.derham, - ) - else: - self._projected_equil = None + # def _allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): + # # create discrete derham sequence + # if self.clone_config is None: + # derham_comm = MPI.COMM_WORLD + # else: + # derham_comm = self.clone_config.sub_comm + + # if grid is None or derham_opts is None: + # if MPI.COMM_WORLD.Get_rank() == 0: + # print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") + # self._derham = None + # else: + # self._derham = setup_derham( + # grid, + # derham_opts, + # comm=derham_comm, + # domain=self.domain, + # verbose=self.verbose, + # ) + + # # create weighted mass and basis operators + # if self.derham is None: + # self._mass_ops = None + # self._basis_ops = None + # else: + # self._mass_ops = WeightedMassOperators( + # self.derham, + # self.domain, + # verbose=self.verbose, + # eq_mhd=self.equil, + # ) + + # self._basis_ops = BasisProjectionOperators( + # self.derham, + # self.domain, + # verbose=self.verbose, + # eq_mhd=self.equil, + # ) + + # # create projected equilibrium + # if self.derham is None: + # self._projected_equil = None + # else: + # if isinstance(self.equil, MHDequilibrium): + # self._projected_equil = ProjectedMHDequilibrium( + # self.equil, + # self.derham, + # ) + # elif isinstance(self.equil, FluidEquilibriumWithB): + # self._projected_equil = ProjectedFluidEquilibriumWithB( + # self.equil, + # self.derham, + # ) + # elif isinstance(self.equil, FluidEquilibrium): + # self._projected_equil = ProjectedFluidEquilibrium( + # self.equil, + # self.derham, + # ) + # else: + # self._projected_equil = None \ No newline at end of file From 06c025b3d722ced473c88d444169b10afc8ac914 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 9 Feb 2026 11:27:27 +0100 Subject: [PATCH 05/80] everything until setup_equation_params has been moved to Simulation class --- src/struphy/main.py | 156 ++-------- src/struphy/simulation/sim.py | 560 +++++++++++++++++----------------- 2 files changed, 299 insertions(+), 417 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index b3bc0afa3..199532bc3 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -85,126 +85,6 @@ def run( verbose=verbose, ) - # synchronize MPI processes to set same start time of simulation for all processes - sim.Barrier() - start_simulation = time.time() - - # check model - assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" - model_name = model.__class__.__name__ - model.verbose = verbose - - if sim.rank == 0: - print(f"\n*** Starting run for model '{model_name}':") - - # meta-data - path_out = env.path_out - restart = env.restart - max_runtime = env.max_runtime - save_step = env.save_step - sort_step = env.sort_step - num_clones = env.num_clones - use_mpi = (sim.comm is not None,) - - meta = {} - meta["platform"] = sysconfig.get_platform() - meta["python version"] = sysconfig.get_python_version() - meta["model name"] = model_name - meta["parameter file"] = params_path - meta["output folder"] = path_out - meta["MPI processes"] = sim.size - meta["use MPI.COMM_WORLD"] = use_mpi - meta["number of domain clones"] = num_clones - meta["restart"] = restart - meta["max wall-clock [min]"] = max_runtime - meta["save interval [steps]"] = save_step - - if sim.rank == 0: - print("\nMETADATA:") - for k, v in meta.items(): - print(f"{k}:".ljust(25), v) - - # creating output folders - setup_folders( - path_out=path_out, - restart=restart, - verbose=verbose, - ) - - # add derived units - units = Units(base_units) - - # save parameter file - if sim.rank == 0: - # save python param file - if params_path is not None: - assert params_path[-3:] == ".py" - shutil.copy2( - params_path, - os.path.join(path_out, "parameters.py"), - ) - # pickle struphy objects - else: - with open(os.path.join(path_out, "env.bin"), "wb") as f: - pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "base_units.bin"), "wb") as f: - pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: - pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "domain.bin"), "wb") as f: - # WORKAROUND: cannot pickle pyccelized classes at the moment - tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} - pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "equil.bin"), "wb") as f: - # WORKAROUND: cannot pickle pyccelized classes at the moment - if equil is not None: - tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} - else: - tmp_dct = {} - pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "grid.bin"), "wb") as f: - pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: - pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) - with open(os.path.join(path_out, "model_class.bin"), "wb") as f: - pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) - - # config clones - if sim.comm is None: - clone_config = None - else: - if num_clones == 1: - clone_config = None - else: - # Setup domain cloning communicators - # MPI.COMM_WORLD : comm - # within a clone: : sub_comm - # between the clones : inter_comm - clone_config = CloneConfig(comm=sim.comm, params=None, num_clones=num_clones) - clone_config.print_clone_config() - if model.particle_species: - clone_config.print_particle_config() - - model.clone_config = clone_config - sim.Barrier() - - ## configure model instance - - # units - model.units = units - if model.bulk_species is None: - A_bulk = None - Z_bulk = None - else: - 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, - verbose=verbose, - ) - # domain and fluid background model.setup_domain_and_equil(domain, equil) @@ -212,7 +92,7 @@ def run( model.allocate_feec(grid, derham_opts) # equation paramters - model.setup_equation_params(units=model.units, verbose=verbose) + model.setup_equation_params(units=sim.units, verbose=verbose) # allocate variables model.allocate_variables(verbose=verbose) @@ -222,15 +102,15 @@ def run( model.allocate_propagators() # plasma parameters - model.compute_plasma_params(verbose=verbose) + sim.compute_plasma_params(verbose=verbose) if sim.rank < 32: if sim.rank == 0: print("") - print(f"Rank {sim.rank}: executing main.run() for model {model_name} ...") + print(f"Rank {sim.rank}: executing main.run() for model {model} ...") if sim.size > 32 and sim.rank == 32: - print(f"Ranks > 31: executing main.run() for model {model_name} ...") + print(f"Ranks > 31: executing main.run() for model {model} ...") # store geometry vtk if sim.rank == 0: @@ -254,11 +134,11 @@ def run( absB0 = model.equil.absB0(*grids_log) pointData["absB0"] = absB0 - gridToVTK(os.path.join(path_out, "geometry"), *grids_phy, pointData=pointData) + gridToVTK(os.path.join(sim.env.path_out, "geometry"), *grids_phy, pointData=pointData) # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs - data = DataContainer(path_out, comm=sim.comm) + data = DataContainer(sim.env.path_out, comm=sim.comm) # time quantities (current time value, value in seconds and index) time_state = {} @@ -279,7 +159,7 @@ def run( split_algo = time_opts.split_algo # set initial conditions for all variables - if restart: + if sim.env.restart: model.initialize_from_restart(data) with h5py.File(data.file_path, "a") as file: @@ -315,7 +195,7 @@ def run( # stop time loop? break_cond_1 = time_state["value"][0] >= Tend - break_cond_2 = run_time_now > max_runtime + break_cond_2 = run_time_now > sim.env.max_runtime if break_cond_1 or break_cond_2: # save restart data (other data already saved below) @@ -325,12 +205,12 @@ def run( print(f"\nTime steps done: {time_state['index'][0]}") print( "wall-clock time of simulation [sec]: ", - end_simulation - start_simulation, + end_simulation - sim.start_time, ) print() break - if sort_step and time_state["index"][0] % sort_step == 0: + if sim.env.sort_step and time_state["index"][0] % sim.env.sort_step == 0: t0 = time.time() for key, val in model.pointer.items(): if isinstance(val, Particles): @@ -346,7 +226,7 @@ def run( # update time and index (round time to 10 decimals for a clean time grid!) time_state["value"][0] = round(time_state["value"][0] + dt, 10) - time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * model.units.t, 10) + time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * sim.units.t, 10) time_state["index"][0] += 1 # perform one time step dt @@ -355,10 +235,10 @@ def run( model.integrate(dt, split_algo) t1 = time.time() - run_time_now = (time.time() - start_simulation) / 60 + run_time_now = (time.time() - sim.start_time) / 60 # update diagnostics data and save data - if time_state["index"][0] % save_step == 0: + if time_state["index"][0] % sim.env.save_step == 0: # compute scalars and kinetic data model.update_scalar_quantities() model.update_markers_to_be_saved() @@ -385,7 +265,7 @@ def run( message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( time_state["value_sec"][0], - Tend * model.units.t, + Tend * sim.units.t, ) message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( run_time_now * 60, @@ -398,16 +278,16 @@ def run( # =================================================================== - meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 + sim.meta["wall-clock time[min]"] = (end_simulation - sim.start_time) / 60 sim.Barrier() if sim.rank == 0: # save meta-data - dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) + dict_to_yaml(sim.meta, os.path.join(sim.env.path_out, "meta.yml")) print("Struphy run finished.") - if clone_config is not None: - clone_config.free() + if sim.clone_config is not None: + sim.clone_config.free() ProfileManager.finalize() diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index e191b564c..73a80300d 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -27,6 +27,7 @@ import sysconfig import cunumpy as xp import h5py +import glob from line_profiler import profile from pyevtk.hl import gridToVTK @@ -83,124 +84,124 @@ def __init__(self, if self.rank == 0: print("") - # # synchronize MPI processes to set same start time of simulation for all processes - # self.Barrier() - # start_simulation = time.time() - - # # check model - # assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" - # model.verbose = verbose - # model_name = model.__class__.__name__ - - # if self.rank == 0: - # print(f"\n*** Starting run for model '{model_name}':") - - # # meta-data - # path_out = env.path_out - # restart = env.restart - # max_runtime = env.max_runtime - # save_step = env.save_step - # sort_step = env.sort_step - # num_clones = env.num_clones - # use_mpi = (self.comm is not None,) - - # meta = {} - # meta["platform"] = sysconfig.get_platform() - # meta["python version"] = sysconfig.get_python_version() - # meta["model name"] = model_name - # meta["parameter file"] = params_path - # meta["output folder"] = path_out - # meta["MPI processes"] = self.size - # meta["use MPI.COMM_WORLD"] = use_mpi - # meta["number of domain clones"] = num_clones - # meta["restart"] = restart - # meta["max wall-clock [min]"] = max_runtime - # meta["save interval [steps]"] = save_step - - # if self.rank == 0: - # print("\nMETADATA:") - # for k, v in meta.items(): - # print(f"{k}:".ljust(25), v) - - # # creating output folders - # self._setup_folders( - # path_out=path_out, - # restart=restart, - # verbose=verbose, - # ) - - # # save parameter file - # if self.rank == 0: - # # save python param file - # if params_path is not None: - # assert params_path[-3:] == ".py" - # shutil.copy2( - # params_path, - # os.path.join(path_out, "parameters.py"), - # ) - # # pickle struphy objects - # else: - # with open(os.path.join(path_out, "env.bin"), "wb") as f: - # pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "base_units.bin"), "wb") as f: - # pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: - # pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "domain.bin"), "wb") as f: - # # WORKAROUND: cannot pickle pyccelized classes at the moment - # tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} - # pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "equil.bin"), "wb") as f: - # # WORKAROUND: cannot pickle pyccelized classes at the moment - # if equil is not None: - # tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} - # else: - # tmp_dct = {} - # pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "grid.bin"), "wb") as f: - # pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: - # pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) - # with open(os.path.join(path_out, "model_class.bin"), "wb") as f: - # pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) - - # # config clones - # if self.comm is None: - # clone_config = None - # else: - # if num_clones == 1: - # clone_config = None - # else: - # # Setup domain cloning communicators - # # MPI.COMM_WORLD : comm - # # within a clone: : sub_comm - # # between the clones : inter_comm - # clone_config = CloneConfig(comm=self.comm, params=None, num_clones=num_clones) - # clone_config.print_clone_config() - # if model.particle_species: - # clone_config.print_particle_config() - - # self.clone_config = clone_config - # self.Barrier() + # synchronize MPI processes to set same start time of simulation for all processes + self.Barrier() + self.start_time = time.time() + + # check model + assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" + model.verbose = verbose + model_name = model.__class__.__name__ + + if self.rank == 0: + print(f"\n*** Starting run for model '{model_name}':") + + # meta-data + path_out = env.path_out + restart = env.restart + max_runtime = env.max_runtime + save_step = env.save_step + sort_step = env.sort_step + num_clones = env.num_clones + use_mpi = (self.comm is not None,) + + self.meta = {} + self.meta["platform"] = sysconfig.get_platform() + self.meta["python version"] = sysconfig.get_python_version() + self.meta["model name"] = model_name + self.meta["parameter file"] = params_path + self.meta["output folder"] = path_out + self.meta["MPI processes"] = self.size + self.meta["use MPI.COMM_WORLD"] = use_mpi + self.meta["number of domain clones"] = num_clones + self.meta["restart"] = restart + self.meta["max wall-clock [min]"] = max_runtime + self.meta["save interval [steps]"] = save_step + + if self.rank == 0: + print("\nMETADATA:") + for k, v in self.meta.items(): + print(f"{k}:".ljust(25), v) + + # creating output folders + self._setup_folders( + path_out=path_out, + restart=restart, + verbose=verbose, + ) + + # save parameter file + if self.rank == 0: + # save python param file + if params_path is not None: + assert params_path[-3:] == ".py" + shutil.copy2( + params_path, + os.path.join(path_out, "parameters.py"), + ) + # pickle struphy objects + else: + with open(os.path.join(path_out, "env.bin"), "wb") as f: + pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "base_units.bin"), "wb") as f: + pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: + pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "domain.bin"), "wb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} + pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "equil.bin"), "wb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + if equil is not None: + tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} + else: + tmp_dct = {} + pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "grid.bin"), "wb") as f: + pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: + pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "model_class.bin"), "wb") as f: + pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) + + # config clones + if self.comm is None: + clone_config = None + else: + if num_clones == 1: + clone_config = None + else: + # Setup domain cloning communicators + # MPI.COMM_WORLD : comm + # within a clone: : sub_comm + # between the clones : inter_comm + clone_config = CloneConfig(comm=self.comm, params=None, num_clones=num_clones) + clone_config.print_clone_config() + if model.particle_species: + clone_config.print_particle_config() + + self.clone_config = model.clone_config = clone_config + self.Barrier() - # # units and normalization parameters - # units = Units(base_units) - # self.units = units - # if model.bulk_species is None: - # A_bulk = None - # Z_bulk = None - # else: - # A_bulk = model.bulk_species.mass_number - # Z_bulk = model.bulk_species.charge_number - # self.units.derive_units( - # velocity_scale=model.velocity_scale, - # A_bulk=A_bulk, - # Z_bulk=Z_bulk, - # verbose=verbose, - # ) - # model.set_normalization_params(units=self.units, verbose=verbose) - - # # domain and fluid background + # units and normalization parameters + units = Units(base_units) + self.units = units + if model.bulk_species is None: + A_bulk = None + Z_bulk = None + else: + A_bulk = model.bulk_species.mass_number + Z_bulk = model.bulk_species.charge_number + self.units.derive_units( + velocity_scale=model.velocity_scale, + A_bulk=A_bulk, + Z_bulk=Z_bulk, + verbose=verbose, + ) + model.setup_equation_params(units=self.units, verbose=verbose) + + # domain and fluid background # self._setup_domain_and_equil(domain, equil, verbose=verbose) # def allocate(self, verbose: bool = False): @@ -239,108 +240,108 @@ def __init__(self, # gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) - # def compute_plasma_params(self, verbose=True): - # """ - # Compute and print volume averaged plasma parameters for each species of the model. - - # Global parameters: - # - plasma volume - # - transit length - # - magnetic field - - # Species dependent parameters: - # - mass - # - charge - # - density - # - pressure - # - thermal energy kBT - # - Alfvén speed v_A - # - thermal speed v_th - # - thermal frequency Omega_th - # - cyclotron frequency Omega_c - # - plasma frequency Omega_p - # - Alfvèn frequency Omega_A - # - thermal Larmor radius rho_th - # - MHD length scale v_a/Omega_c - # - rho/L - # - alpha = Omega_p/Omega_c - # - epsilon = 1/(t*Omega_c) - # """ - - # # units affices for printing - # units_affix = {} - # units_affix["plasma volume"] = " m³" - # units_affix["transit length"] = " m" - # units_affix["magnetic field"] = " T" - # units_affix["mass"] = " kg" - # units_affix["charge"] = " C" - # units_affix["density"] = " m⁻³" - # units_affix["pressure"] = " bar" - # units_affix["kBT"] = " keV" - # units_affix["v_A"] = " m/s" - # units_affix["v_th"] = " m/s" - # units_affix["vth1"] = " m/s" - # units_affix["vth2"] = " m/s" - # units_affix["vth3"] = " m/s" - # units_affix["Omega_th"] = " Mrad/s" - # units_affix["Omega_c"] = " Mrad/s" - # units_affix["Omega_p"] = " Mrad/s" - # units_affix["Omega_A"] = " Mrad/s" - # units_affix["rho_th"] = " m" - # units_affix["v_A/Omega_c"] = " m" - # units_affix["rho_th/L"] = "" - # units_affix["alpha"] = "" - # units_affix["epsilon"] = "" - - # h = 1 / 20 - # eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - # eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - # eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - - # ## global parameters - - # # plasma volume (hat x^3) - # det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) - # vol1 = xp.mean(xp.abs(det_tmp)) - # # plasma volume (m⁻³) - # plasma_volume = vol1 * self.units.x**3 - # # transit length (m) - # transit_length = plasma_volume ** (1 / 3) - # # magnetic field (T) - # if isinstance(self.equil, FluidEquilibriumWithB): - # B_tmp = self.equil.absB0(eta1, eta2, eta3) - # else: - # B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) - # magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B - # B_max = xp.max(B_tmp) * self.units.B - # B_min = xp.min(B_tmp) * self.units.B - - # if magnetic_field < 1e-14: - # magnetic_field = xp.nan - # # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") - - # if verbose and MPI.COMM_WORLD.Get_rank() == 0: - # print("\nPLASMA PARAMETERS:") - # print( - # "Plasma volume:".ljust(25), - # "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], - # ) - # print( - # "Transit length:".ljust(25), - # "{:4.3e}".format(transit_length) + units_affix["transit length"], - # ) - # print( - # "Avg. magnetic field:".ljust(25), - # "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], - # ) - # print( - # "Max magnetic field:".ljust(25), - # "{:4.3e}".format(B_max) + units_affix["magnetic field"], - # ) - # print( - # "Min magnetic field:".ljust(25), - # "{:4.3e}".format(B_min) + units_affix["magnetic field"], - # ) + def compute_plasma_params(self, verbose=True): + """ + Compute and print volume averaged plasma parameters for each species of the model. + + Global parameters: + - plasma volume + - transit length + - magnetic field + + Species dependent parameters: + - mass + - charge + - density + - pressure + - thermal energy kBT + - Alfvén speed v_A + - thermal speed v_th + - thermal frequency Omega_th + - cyclotron frequency Omega_c + - plasma frequency Omega_p + - Alfvèn frequency Omega_A + - thermal Larmor radius rho_th + - MHD length scale v_a/Omega_c + - rho/L + - alpha = Omega_p/Omega_c + - epsilon = 1/(t*Omega_c) + """ + + # units affices for printing + units_affix = {} + units_affix["plasma volume"] = " m³" + units_affix["transit length"] = " m" + units_affix["magnetic field"] = " T" + units_affix["mass"] = " kg" + units_affix["charge"] = " C" + units_affix["density"] = " m⁻³" + units_affix["pressure"] = " bar" + units_affix["kBT"] = " keV" + units_affix["v_A"] = " m/s" + units_affix["v_th"] = " m/s" + units_affix["vth1"] = " m/s" + units_affix["vth2"] = " m/s" + units_affix["vth3"] = " m/s" + units_affix["Omega_th"] = " Mrad/s" + units_affix["Omega_c"] = " Mrad/s" + units_affix["Omega_p"] = " Mrad/s" + units_affix["Omega_A"] = " Mrad/s" + units_affix["rho_th"] = " m" + units_affix["v_A/Omega_c"] = " m" + units_affix["rho_th/L"] = "" + units_affix["alpha"] = "" + units_affix["epsilon"] = "" + + h = 1 / 20 + eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + + ## global parameters + + # plasma volume (hat x^3) + det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) + vol1 = xp.mean(xp.abs(det_tmp)) + # plasma volume (m⁻³) + plasma_volume = vol1 * self.units.x**3 + # transit length (m) + transit_length = plasma_volume ** (1 / 3) + # magnetic field (T) + if isinstance(self.equil, FluidEquilibriumWithB): + B_tmp = self.equil.absB0(eta1, eta2, eta3) + else: + B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) + magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B + B_max = xp.max(B_tmp) * self.units.B + B_min = xp.min(B_tmp) * self.units.B + + if magnetic_field < 1e-14: + magnetic_field = xp.nan + # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("\nPLASMA PARAMETERS:") + print( + "Plasma volume:".ljust(25), + "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], + ) + print( + "Transit length:".ljust(25), + "{:4.3e}".format(transit_length) + units_affix["transit length"], + ) + print( + "Avg. magnetic field:".ljust(25), + "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], + ) + print( + "Max magnetic field:".ljust(25), + "{:4.3e}".format(B_max) + units_affix["magnetic field"], + ) + print( + "Min magnetic field:".ljust(25), + "{:4.3e}".format(B_min) + units_affix["magnetic field"], + ) # def run(self, verbose: bool = False): # if rank < 32: @@ -493,12 +494,12 @@ def __init__(self, # # =================================================================== - # meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 + # self.meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 # Barrier() # if rank == 0: # # save meta-data - # dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) + # dict_to_yaml(self.meta, os.path.join(path_out, "meta.yml")) # print("Struphy run finished.") # if clone_config is not None: @@ -506,64 +507,65 @@ def __init__(self, # ProfileManager.finalize() - # def _setup_folders( - # path_out: str, - # restart: bool, - # verbose: bool = False, - # ): - # """ - # Setup output folders. - # """ - # if MPI.COMM_WORLD.Get_rank() == 0: - # if verbose: - # print("\nPREPARATION AND CLEAN-UP:") - - # # create output folder if it does not exit - # if not os.path.exists(path_out): - # os.makedirs(path_out, exist_ok=True) - # if verbose: - # print("Created folder " + path_out) - - # # create data folder in output folder if it does not exist - # if not os.path.exists(os.path.join(path_out, "data/")): - # os.mkdir(os.path.join(path_out, "data/")) - # if verbose: - # print("Created folder " + os.path.join(path_out, "data/")) - # else: - # # remove post_processing folder - # folder = os.path.join(path_out, "post_processing") - # if os.path.exists(folder): - # shutil.rmtree(folder) - # if verbose: - # print("Removed existing folder " + folder) - - # # remove meta file - # file = os.path.join(path_out, "meta.txt") - # if os.path.exists(file): - # os.remove(file) - # if verbose: - # print("Removed existing file " + file) - - # # remove profiling file - # file = os.path.join(path_out, "profile_tmp") - # if os.path.exists(file): - # os.remove(file) - # if verbose: - # print("Removed existing file " + file) - - # # remove .png files (if NOT a restart) - # if not restart: - # files = glob.glob(os.path.join(path_out, "*.png")) - # for n, file in enumerate(files): - # os.remove(file) - # if verbose and n < 10: # print only ten statements in case of many processes - # print("Removed existing file " + file) - - # files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) - # for n, file in enumerate(files): - # os.remove(file) - # if verbose and n < 10: # print only ten statements in case of many processes - # print("Removed existing file " + file) + def _setup_folders( + self, + path_out: str, + restart: bool, + verbose: bool = False, + ): + """ + Setup output folders. + """ + if MPI.COMM_WORLD.Get_rank() == 0: + if verbose: + print("\nPREPARATION AND CLEAN-UP:") + + # create output folder if it does not exit + if not os.path.exists(path_out): + os.makedirs(path_out, exist_ok=True) + if verbose: + print("Created folder " + path_out) + + # create data folder in output folder if it does not exist + if not os.path.exists(os.path.join(path_out, "data/")): + os.mkdir(os.path.join(path_out, "data/")) + if verbose: + print("Created folder " + os.path.join(path_out, "data/")) + else: + # remove post_processing folder + folder = os.path.join(path_out, "post_processing") + if os.path.exists(folder): + shutil.rmtree(folder) + if verbose: + print("Removed existing folder " + folder) + + # remove meta file + file = os.path.join(path_out, "meta.txt") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove profiling file + file = os.path.join(path_out, "profile_tmp") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove .png files (if NOT a restart) + if not restart: + files = glob.glob(os.path.join(path_out, "*.png")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) # def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): # """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" From 818dfcbf9961256dd5d05b8fc78cd1eaabe252ec Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 10 Feb 2026 15:21:15 +0100 Subject: [PATCH 06/80] sim.allocate and sim.compute_plasma_params work --- src/struphy/main.py | 17 +- src/struphy/models/base.py | 397 ------------------- src/struphy/simulation/sim.py | 695 +++++++++++++++++++++++++++++----- 3 files changed, 593 insertions(+), 516 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index 199532bc3..5b1390904 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -85,25 +85,14 @@ def run( verbose=verbose, ) - # domain and fluid background - model.setup_domain_and_equil(domain, equil) - - # feec - model.allocate_feec(grid, derham_opts) - # equation paramters - model.setup_equation_params(units=sim.units, verbose=verbose) - - # allocate variables - model.allocate_variables(verbose=verbose) - model.allocate_helpers() - - # pass info to propagators - model.allocate_propagators() + sim.allocate(verbose=verbose) # plasma parameters sim.compute_plasma_params(verbose=verbose) + exit() + if sim.rank < 32: if sim.rank == 0: print("") diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 8f413822a..f7052f776 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -180,85 +180,6 @@ def species(self): self._species = self.field_species | self.fluid_species | self.particle_species return self._species - ## allocate methods - - def allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): - # create discrete derham sequence - if self.clone_config is None: - derham_comm = MPI.COMM_WORLD - else: - derham_comm = self.clone_config.sub_comm - - if grid is None or derham_opts is None: - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") - self._derham = None - else: - self._derham = setup_derham( - grid, - derham_opts, - comm=derham_comm, - domain=self.domain, - verbose=self.verbose, - ) - - # create weighted mass and basis operators - if self.derham is None: - self._mass_ops = None - self._basis_ops = None - else: - self._mass_ops = WeightedMassOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - self._basis_ops = BasisProjectionOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - # create projected equilibrium - if self.derham is None: - self._projected_equil = None - else: - if isinstance(self.equil, MHDequilibrium): - self._projected_equil = ProjectedMHDequilibrium( - self.equil, - self.derham, - ) - elif isinstance(self.equil, FluidEquilibriumWithB): - self._projected_equil = ProjectedFluidEquilibriumWithB( - self.equil, - self.derham, - ) - elif isinstance(self.equil, FluidEquilibrium): - self._projected_equil = ProjectedFluidEquilibrium( - self.equil, - self.derham, - ) - else: - self._projected_equil = None - - def allocate_propagators(self): - # set propagators base class attributes (then available to all propagators) - Propagator.derham = self.derham - Propagator.domain = self.domain - if self.derham is not None: - Propagator.mass_ops = self.mass_ops - Propagator.basis_ops = self.basis_ops - Propagator.projected_equil = self.projected_equil - - assert len(self.prop_list) > 0, "No propagators in this model, check the model class." - for prop in self.prop_list: - assert isinstance(prop, Propagator) - prop.allocate() - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nAllocated propagator '{prop.__class__.__name__}'.") - @staticmethod def diagnostics_dct(): """Diagnostics dictionary. @@ -505,95 +426,6 @@ def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, comp "summands": summands, } - def update_scalar(self, name, value=None): - """Add a scalar that should be saved during the simulation. - - Parameters - ---------- - name : str - Dictionary key of the scalar. - - value : float, optional - Value to be saved. Required if there are no summands. - """ - - # Ensure the name is a string - assert isinstance(name, str) - - variable: PICVariable | SPHVariable = self._scalar_quantities[name]["variable"] - summands = self._scalar_quantities[name]["summands"] - compute = self._scalar_quantities[name]["compute"] - - if compute == "from_particles": - compute_operations = [ - "sum_within_clone", - "sum_between_clones", - "divide_n_mks", - ] - elif compute == "from_sph": - compute_operations = [ - "sum_world", - "divide_n_mks", - ] - elif compute == "from_field": - compute_operations = [] - else: - compute_operations = [] - - if summands is None: - # Ensure the value is a float if there are no summands - assert isinstance(value, float) - - # Create a numpy array to hold the scalar value - value_array = xp.array([value], dtype=xp.float64) - - # Perform MPI operations based on the compute flags - if "sum_world" in compute_operations and not isinstance(MPI, MockMPI): - MPI.COMM_WORLD.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - - if "sum_within_clone" in compute_operations and self.derham.comm is not None: - self.derham.comm.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - if self.clone_config is None: - num_clones = 1 - else: - num_clones = self.clone_config.num_clones - - if "sum_between_clones" in compute_operations and num_clones > 1: - self.clone_config.inter_comm.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - - if "average_between_clones" in compute_operations and num_clones > 1: - self.clone_config.inter_comm.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - value_array /= num_clones - - if "divide_n_mks" in compute_operations: - # Initialize the total number of markers - n_mks_tot = xp.array([variable.particles.Np]) - value_array /= n_mks_tot - - # Update the scalar value - self._scalar_quantities[name]["value"][0] = value_array[0] - - else: - # Sum the values of the summands - value = sum(self._scalar_quantities[summand]["value"][0] for summand in summands) - self._scalar_quantities[name]["value"][0] = value - def add_time_state(self, time_state): """Add a pointer to the time variable of the dynamics ('t') to the model and to all propagators of the model. @@ -609,85 +441,6 @@ def add_time_state(self, time_state): if isinstance(prop, Propagator): prop.add_time_state(time_state) - @profile - def allocate_variables(self, verbose: bool = False): - """ - Allocate memory for model variables and set initial conditions. - """ - # allocate memory for FE coeffs of electromagnetic fields/potentials - if self.field_species: - for species, spec in self.field_species.items(): - assert isinstance(spec, FieldSpecies) - for k, v in spec.variables.items(): - assert isinstance(v, FEECVariable) - v.allocate( - derham=self.derham, - domain=self.domain, - equil=self.equil, - ) - - # allocate memory for FE coeffs of fluid variables - if self.fluid_species: - for species, spec in self.fluid_species.items(): - assert isinstance(spec, FluidSpecies) - for k, v in spec.variables.items(): - assert isinstance(v, FEECVariable) - v.allocate( - derham=self.derham, - domain=self.domain, - equil=self.equil, - ) - - # allocate memory for marker arrays of kinetic variables - if self.particle_species: - for species, spec in self.particle_species.items(): - assert isinstance(spec, ParticleSpecies) - for k, v in spec.variables.items(): - if isinstance(v, PICVariable): - v.allocate( - clone_config=self.clone_config, - derham=self.derham, - domain=self.domain, - equil=self.equil, - projected_equil=self.projected_equil, - verbose=verbose, - ) - if isinstance(v, SPHVariable): - v.allocate( - derham=self.derham, - domain=self.domain, - equil=self.equil, - projected_equil=self.projected_equil, - verbose=verbose, - ) - - # allocate memory for FE coeffs of fluid variables - if self.diagnostic_species: - for species, spec in self.diagnostic_species.items(): - assert isinstance(spec, DiagnosticSpecies) - for k, v in spec.variables.items(): - assert isinstance(v, FEECVariable) - v.allocate( - derham=self.derham, - domain=self.domain, - equil=self.equil, - ) - - # TODO: allocate memory for FE coeffs of diagnostics - # if self.params.diagnostic_fields is not None: - # for key, val in self.diagnostics.items(): - # if "params" in key: - # continue - # else: - # val["obj"] = self.derham.create_spline_function( - # key, - # val["space"], - # bckgr_params=None, - # pert_params=None, - # ) - - # self._pointer[key] = val["obj"].vector - @profile def integrate(self, dt, split_algo="LieTrotter"): """ @@ -1024,156 +777,6 @@ def initialize_from_restart(self, data: DataContainer): if MPI.COMM_WORLD.Get_size() > 1: subval.particles.mpi_sort_markers(do_test=True) - def initialize_data_output(self, data: DataContainer, size): - """ - Create datasets in hdf5 files according to model unknowns and diagnostics data. - - Parameters - ---------- - data : struphy.io.output_handling.DataContainer - The data object that links to the hdf5 files. - - size : int - Number of MPI processes of the model run. - - Returns - ------- - save_keys_all : list - Keys of datasets which are saved during the simulation. - - save_keys_end : list - Keys of datasets which are saved at the end of a simulation to enable restarts. - """ - - # save scalar quantities in group 'scalar/' - for key, scalar in self.scalar_quantities.items(): - val = scalar["value"] - key_scalar = "scalar/" + key - data.add_data({key_scalar: val}) - - with h5py.File(data.file_path, "a") as file: - # store grid_info only for runs with 512 ranks or smaller - if self._scalar_quantities and self.derham is not None: - if size <= 512: - file["scalar"].attrs["grid_info"] = self.derham.domain_array - else: - file["scalar"].attrs["grid_info"] = self.derham.domain_array[0] - else: - pass - - # save feec data in group 'feec/' - feec_species = self.field_species | self.fluid_species | self.diagnostic_species - for species, val in feec_species.items(): - assert isinstance(val, Species) - - species_path = os.path.join("feec", species) - species_path_restart = os.path.join("restart", species) - - for variable, subval in val.variables.items(): - assert isinstance(subval, FEECVariable) - spline = subval.spline - - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - spline.extract_coeffs(update_ghost_regions=False) - - # save numpy array to be updated each time step. - if subval.save_data: - key_field = os.path.join(species_path, variable) - - if isinstance(spline.vector_stencil, StencilVector): - data.add_data( - {key_field: spline.vector_stencil._data}, - ) - - else: - for n in range(3): - key_component = os.path.join(key_field, str(n + 1)) - data.add_data( - {key_component: spline.vector_stencil[n]._data}, - ) - - # save field meta data - file[key_field].attrs["space_id"] = spline.space_id - file[key_field].attrs["starts"] = spline.starts - file[key_field].attrs["ends"] = spline.ends - file[key_field].attrs["pads"] = spline.pads - - # save numpy array to be updated only at the end of the simulation for restart. - key_field_restart = os.path.join(species_path_restart, variable) - - if isinstance(spline.vector_stencil, StencilVector): - data.add_data( - {key_field_restart: spline.vector_stencil._data}, - ) - else: - for n in range(3): - key_component_restart = os.path.join(key_field_restart, str(n + 1)) - data.add_data( - {key_component_restart: spline.vector_stencil[n]._data}, - ) - - # save kinetic data in group 'kinetic/' - for name, species in self.particle_species.items(): - assert isinstance(species, ParticleSpecies) - assert len(species.variables) == 1, "More than 1 variable per kinetic species is not allowed." - for varname, var in species.variables.items(): - assert isinstance(var, PICVariable | SPHVariable) - obj = var.particles - assert isinstance(obj, Particles) - - key_spec = os.path.join("kinetic", name) - key_spec_restart = os.path.join("restart", name) - - # restart data - data.add_data({key_spec_restart: obj.markers}) - - # marker data - key_mks = os.path.join(key_spec, "markers") - data.add_data({key_mks: var.saved_markers}) - - # binning plot data - for bin_plot in species.binning_plots: - # define slice name with binning quantity - slice, output_quantity = bin_plot.slice, bin_plot.output_quantity - slice = f"{slice}_{output_quantity}" - - key_f = os.path.join(key_spec, "f", slice) - key_df = os.path.join(key_spec, "df", slice) - - data.add_data({key_f: bin_plot.f}) - data.add_data({key_df: bin_plot.df}) - - for dim, be in enumerate(bin_plot.bin_edges): - file[key_f].attrs["bin_centers" + "_" + str(dim + 1)] = be[:-1] + (be[1] - be[0]) / 2 - - for i, kd_plot in enumerate(species.kernel_density_plots): - key_n = os.path.join(key_spec, "n_sph", f"view_{i}") - - data.add_data({key_n: kd_plot.n_sph}) - # save 1d point values, not meshgrids, because attrs size is limited - eta1 = kd_plot.plot_pts[0][:, 0, 0] - eta2 = kd_plot.plot_pts[1][0, :, 0] - eta3 = kd_plot.plot_pts[2][0, 0, :] - file[key_n].attrs["eta1"] = eta1 - file[key_n].attrs["eta2"] = eta2 - file[key_n].attrs["eta3"] = eta3 - - # TODO: maybe add other data - # else: - # data.add_data({key_dat: val1}) - - # keys to be saved at each time step and only at end (restart) - save_keys_all = [] - save_keys_end = [] - - for key in data.dset_dict: - if "restart" in key: - save_keys_end.append(key) - else: - save_keys_all.append(key) - - return save_keys_all, save_keys_end - ################### # Class methods : ################### diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 73a80300d..05ff6ecfc 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -15,10 +15,29 @@ from struphy.io.setup import setup_folders from struphy.physics.physics import Units from struphy.utils.clone_config import CloneConfig +from struphy.feec.basis_projection_ops import BasisProjectionOperators +from struphy.feec.mass import WeightedMassOperators +from struphy.fields_background.base import ( + FluidEquilibrium, + FluidEquilibriumWithB, + MHDequilibrium, + NumericalMHDequilibrium, +) +from struphy.fields_background.projected_equils import ( + ProjectedFluidEquilibrium, + ProjectedFluidEquilibriumWithB, + ProjectedMHDequilibrium, +) +from struphy.propagators.base import Propagator +from struphy.models.species import DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable +from struphy.io.output_handling import DataContainer +from struphy.pic.base import Particles # third party imports from feectools.ddm.mpi import MockMPI from feectools.ddm.mpi import mpi as MPI +from feectools.linalg.stencil import StencilVector from scope_profiler import ProfileManager import os import time @@ -51,8 +70,6 @@ def __init__(self, self.env = env self.base_units = base_units self.time_opts = time_opts - self.domain = domain - self.equil = equil self.grid = grid self.derham_opts = derham_opts self.verbose = verbose @@ -202,18 +219,18 @@ def __init__(self, model.setup_equation_params(units=self.units, verbose=verbose) # domain and fluid background - # self._setup_domain_and_equil(domain, equil, verbose=verbose) + self._setup_domain_and_equil(domain, equil, verbose=verbose) - # def allocate(self, verbose: bool = False): - # # feec - # self._allocate_feec(self.grid, self.derham_opts) + def allocate(self, verbose: bool = False): + # feec + self._allocate_feec(self.grid, self.derham_opts) - # # allocate model variables - # self.model.allocate_variables(verbose=verbose) - # self.model.allocate_helpers() + # allocate model variables + self._allocate_variables(verbose=verbose) + self.model.allocate_helpers() - # # pass info to propagators - # self.model.allocate_propagators() + # pass info to propagators + self._allocate_propagators() # def store_geometry(self, verbose: bool = False): # # store geometry vtk @@ -343,6 +360,111 @@ def compute_plasma_params(self, verbose=True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) + def update_scalar(self, name, value=None): + """Update a scalar during the simulation. + + Parameters + ---------- + name : str + Dictionary key of the scalar. + + value : float, optional + Value to be saved. Required if there are no summands. + """ + + # Ensure the name is a string + assert isinstance(name, str) + + scalars = self.model.scalar_quantities + + variable: PICVariable | SPHVariable = scalars[name]["variable"] + summands = scalars[name]["summands"] + compute = scalars[name]["compute"] + + if compute == "from_particles": + compute_operations = [ + "sum_within_clone", + "sum_between_clones", + "divide_n_mks", + ] + elif compute == "from_sph": + compute_operations = [ + "sum_world", + "divide_n_mks", + ] + elif compute == "from_field": + compute_operations = [] + else: + compute_operations = [] + + if summands is None: + # Ensure the value is a float if there are no summands + assert isinstance(value, float) + + # Create a numpy array to hold the scalar value + value_array = xp.array([value], dtype=xp.float64) + + # Perform MPI operations based on the compute flags + if "sum_world" in compute_operations and not isinstance(MPI, MockMPI): + MPI.COMM_WORLD.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + + if "sum_within_clone" in compute_operations and self.derham.comm is not None: + self.derham.comm.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + if self.clone_config is None: + num_clones = 1 + else: + num_clones = self.clone_config.num_clones + + if "sum_between_clones" in compute_operations and num_clones > 1: + self.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + + if "average_between_clones" in compute_operations and num_clones > 1: + self.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + value_array /= num_clones + + if "divide_n_mks" in compute_operations: + # Initialize the total number of markers + n_mks_tot = xp.array([variable.particles.Np]) + value_array /= n_mks_tot + + # Update the scalar value + scalars[name]["value"][0] = value_array[0] + + else: + # Sum the values of the summands + value = sum(scalars[summand]["value"][0] for summand in summands) + scalars[name]["value"][0] = value + + def add_time_state(self, time_state): + """Add a pointer to the time variable of the dynamics ('t') + to the model and to all propagators of the model. + + Parameters + ---------- + time_state : ndarray + Of size 1, holds the current physical time 't'. + """ + assert time_state.size == 1 + self._time_state = time_state + for _, prop in self.propagators.__dict__.items(): + if isinstance(prop, Propagator): + prop.add_time_state(time_state) # def run(self, verbose: bool = False): # if rank < 32: # if rank == 0: @@ -567,102 +689,465 @@ def _setup_folders( if verbose and n < 10: # print only ten statements in case of many processes print("Removed existing file " + file) - # def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): - # """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" - # if equil is not None: - # if isinstance(equil, NumericalMHDequilibrium): - # self._domain = equil.domain - # else: - # self._domain = domain - # equil.domain = domain - - # if hasattr(equil, "units"): - # assert isinstance(equil.units, Units) - # equil.units.derive_units( - # velocity_scale=self.velocity_scale, - # A_bulk=self.bulk_species.mass_number, - # Z_bulk=self.bulk_species.charge_number, - # verbose=self.verbose, - # ) + def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): + """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" + if equil is not None: + if isinstance(equil, NumericalMHDequilibrium): + self._domain = equil.domain + else: + self._domain = domain + equil.domain = domain + + if hasattr(equil, "units"): + assert isinstance(equil.units, Units) + equil.units.derive_units( + velocity_scale=self.velocity_scale, + A_bulk=self.bulk_species.mass_number, + Z_bulk=self.bulk_species.charge_number, + verbose=self.verbose, + ) - # else: - # self._domain = domain + else: + self._domain = domain + + self._equil = equil + + if MPI.COMM_WORLD.Get_rank() == 0 and verbose: + print("\nDOMAIN:") + print("type:".ljust(25), self.domain.__class__.__name__) + for key, val in self.domain.params.items(): + if key not in {"cx", "cy", "cz"}: + print((key + ":").ljust(25), val) + + print("\nFLUID BACKGROUND:") + if self.equil is not None: + print("type:".ljust(25), self.equil.__class__.__name__) + for key, val in self.equil.params.items(): + print((key + ":").ljust(25), val) + else: + print("None.") + + def _setup_derham( + self, + grid: grids.TensorProductGrid, + options: DerhamOptions, + comm: MPI.Intracomm = None, + domain: Domain = None, + verbose=False, +): + """ + Creates the 3d derham sequence for given grid parameters. - # self._equil = equil + Parameters + ---------- + grid : TensorProductGrid + The FEEC grid. - # if MPI.COMM_WORLD.Get_rank() == 0 and verbose: - # print("\nDOMAIN:") - # print("type:".ljust(25), self.domain.__class__.__name__) - # for key, val in self.domain.params.items(): - # if key not in {"cx", "cy", "cz"}: - # print((key + ":").ljust(25), val) + comm: Intracomm + MPI communicator (sub_comm if clones are used). - # print("\nFLUID BACKGROUND:") - # if self.equil is not None: - # print("type:".ljust(25), self.equil.__class__.__name__) - # for key, val in self.equil.params.items(): - # print((key + ":").ljust(25), val) - # else: - # print("None.") - - # def _allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): - # # create discrete derham sequence - # if self.clone_config is None: - # derham_comm = MPI.COMM_WORLD - # else: - # derham_comm = self.clone_config.sub_comm + domain : Domain, optional + The Struphy domain object for evaluating the mapping F : [0, 1]^3 --> R^3 and the corresponding metric coefficients. - # if grid is None or derham_opts is None: - # if MPI.COMM_WORLD.Get_rank() == 0: - # print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") - # self._derham = None - # else: - # self._derham = setup_derham( - # grid, - # derham_opts, - # comm=derham_comm, - # domain=self.domain, - # verbose=self.verbose, - # ) - - # # create weighted mass and basis operators - # if self.derham is None: - # self._mass_ops = None - # self._basis_ops = None - # else: - # self._mass_ops = WeightedMassOperators( - # self.derham, - # self.domain, - # verbose=self.verbose, - # eq_mhd=self.equil, - # ) - - # self._basis_ops = BasisProjectionOperators( - # self.derham, - # self.domain, - # verbose=self.verbose, - # eq_mhd=self.equil, - # ) - - # # create projected equilibrium - # if self.derham is None: - # self._projected_equil = None - # else: - # if isinstance(self.equil, MHDequilibrium): - # self._projected_equil = ProjectedMHDequilibrium( - # self.equil, - # self.derham, - # ) - # elif isinstance(self.equil, FluidEquilibriumWithB): - # self._projected_equil = ProjectedFluidEquilibriumWithB( - # self.equil, - # self.derham, - # ) - # elif isinstance(self.equil, FluidEquilibrium): - # self._projected_equil = ProjectedFluidEquilibrium( - # self.equil, - # self.derham, - # ) - # else: - # self._projected_equil = None - \ No newline at end of file + verbose : bool + Show info on screen. + + Returns + ------- + derham : struphy.feec.psydac_derham.Derham + Discrete de Rham sequence on the logical unit cube. + """ + + from struphy.feec.psydac_derham import Derham + + # number of grid cells + Nel = grid.Nel + # mpi + mpi_dims_mask = grid.mpi_dims_mask + + # spline degrees + p = options.p + # spline types (clamped vs. periodic) + spl_kind = options.spl_kind + # boundary conditions (Homogeneous Dirichlet or None) + dirichlet_bc = options.dirichlet_bc + # Number of quadrature points per histopolation cell + nq_pr = options.nq_pr + # Number of quadrature points per grid cell for L^2 + nquads = options.nquads + # C^k smoothness at eta_1=0 for polar domains + polar_ck = options.polar_ck + # local commuting projectors + local_projectors = options.local_projectors + + derham = Derham( + Nel, + p, + spl_kind, + dirichlet_bc=dirichlet_bc, + nquads=nquads, + nq_pr=nq_pr, + comm=comm, + mpi_dims_mask=mpi_dims_mask, + with_projectors=True, + polar_ck=polar_ck, + domain=domain, + local_projectors=local_projectors, + ) + + if MPI.COMM_WORLD.Get_rank() == 0 and verbose: + print("\nDERHAM:") + print("number of elements:".ljust(25), Nel) + print("spline degrees:".ljust(25), p) + print("periodic bcs:".ljust(25), spl_kind) + print("hom. Dirichlet bc:".ljust(25), dirichlet_bc) + print("GL quad pts (L2):".ljust(25), nquads) + print("GL quad pts (hist):".ljust(25), nq_pr) + print( + "MPI proc. per dir.:".ljust(25), + derham.domain_decomposition.nprocs, + ) + print("use polar splines:".ljust(25), derham.polar_ck == 1) + print("domain on process 0:".ljust(25), derham.domain_array[0]) + + return derham + + @profile + def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions): + # create discrete derham sequence + if self.clone_config is None: + derham_comm = MPI.COMM_WORLD + else: + derham_comm = self.clone_config.sub_comm + + if grid is None or derham_opts is None: + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") + self._derham = None + else: + self._derham = self._setup_derham( + grid, + derham_opts, + comm=derham_comm, + domain=self.domain, + verbose=self.verbose, + ) + + # create weighted mass and basis operators + if self.derham is None: + self._mass_ops = None + self._basis_ops = None + else: + self._mass_ops = WeightedMassOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + self._basis_ops = BasisProjectionOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + # create projected equilibrium + if self.derham is None: + self._projected_equil = None + else: + if isinstance(self.equil, MHDequilibrium): + self._projected_equil = ProjectedMHDequilibrium( + self.equil, + self.derham, + ) + elif isinstance(self.equil, FluidEquilibriumWithB): + self._projected_equil = ProjectedFluidEquilibriumWithB( + self.equil, + self.derham, + ) + elif isinstance(self.equil, FluidEquilibrium): + self._projected_equil = ProjectedFluidEquilibrium( + self.equil, + self.derham, + ) + else: + self._projected_equil = None + + @profile + def _allocate_variables(self, verbose: bool = False): + """ + Allocate memory for model variables and set initial conditions. + """ + # allocate memory for FE coeffs of electromagnetic fields/potentials + if self.model.field_species: + for species, spec in self.model.field_species.items(): + assert isinstance(spec, FieldSpecies) + for k, v in spec.variables.items(): + assert isinstance(v, FEECVariable) + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + ) + + # allocate memory for FE coeffs of fluid variables + if self.model.fluid_species: + for species, spec in self.model.fluid_species.items(): + assert isinstance(spec, FluidSpecies) + for k, v in spec.variables.items(): + assert isinstance(v, FEECVariable) + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + ) + + # allocate memory for marker arrays of kinetic variables + if self.model.particle_species: + for species, spec in self.model.particle_species.items(): + assert isinstance(spec, ParticleSpecies) + for k, v in spec.variables.items(): + if isinstance(v, PICVariable): + v.allocate( + clone_config=self.clone_config, + derham=self.derham, + domain=self.domain, + equil=self.equil, + projected_equil=self.projected_equil, + verbose=verbose, + ) + if isinstance(v, SPHVariable): + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + projected_equil=self.projected_equil, + verbose=verbose, + ) + + # allocate memory for FE coeffs of fluid variables + if self.model.diagnostic_species: + for species, spec in self.model.diagnostic_species.items(): + assert isinstance(spec, DiagnosticSpecies) + for k, v in spec.variables.items(): + assert isinstance(v, FEECVariable) + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + ) + + # TODO: allocate memory for FE coeffs of diagnostics + # if self.params.diagnostic_fields is not None: + # for key, val in self.diagnostics.items(): + # if "params" in key: + # continue + # else: + # val["obj"] = self.derham.create_spline_function( + # key, + # val["space"], + # bckgr_params=None, + # pert_params=None, + # ) + + # self._pointer[key] = val["obj"].vector + + @profile + def _allocate_propagators(self): + # set propagators base class attributes (then available to all propagators) + Propagator.derham = self.derham + Propagator.domain = self.domain + if self.derham is not None: + Propagator.mass_ops = self.mass_ops + Propagator.basis_ops = self.basis_ops + Propagator.projected_equil = self.projected_equil + + assert len(self.model.prop_list) > 0, "No propagators in this model, check the model class." + for prop in self.model.prop_list: + assert isinstance(prop, Propagator) + prop.allocate() + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nAllocated propagator '{prop.__class__.__name__}'.") + + @profile + def _initialize_data_output(self, data: DataContainer, size): + """ + Create datasets in hdf5 files according to model unknowns and diagnostics data. + + Parameters + ---------- + data : struphy.io.output_handling.DataContainer + The data object that links to the hdf5 files. + + size : int + Number of MPI processes of the model run. + + Returns + ------- + save_keys_all : list + Keys of datasets which are saved during the simulation. + + save_keys_end : list + Keys of datasets which are saved at the end of a simulation to enable restarts. + """ + + # save scalar quantities in group 'scalar/' + for key, scalar in self.model.scalar_quantities.items(): + val = scalar["value"] + key_scalar = "scalar/" + key + data.add_data({key_scalar: val}) + + with h5py.File(data.file_path, "a") as file: + # store grid_info only for runs with 512 ranks or smaller + if self.model.scalar_quantities and self.derham is not None: + if size <= 512: + file["scalar"].attrs["grid_info"] = self.derham.domain_array + else: + file["scalar"].attrs["grid_info"] = self.derham.domain_array[0] + else: + pass + + # save feec data in group 'feec/' + feec_species = self.model.field_species | self.model.fluid_species | self.model.diagnostic_species + for species, val in feec_species.items(): + assert isinstance(val, Species) + + species_path = os.path.join("feec", species) + species_path_restart = os.path.join("restart", species) + + for variable, subval in val.variables.items(): + assert isinstance(subval, FEECVariable) + spline = subval.spline + + # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! + spline.extract_coeffs(update_ghost_regions=False) + + # save numpy array to be updated each time step. + if subval.save_data: + key_field = os.path.join(species_path, variable) + + if isinstance(spline.vector_stencil, StencilVector): + data.add_data( + {key_field: spline.vector_stencil._data}, + ) + + else: + for n in range(3): + key_component = os.path.join(key_field, str(n + 1)) + data.add_data( + {key_component: spline.vector_stencil[n]._data}, + ) + + # save field meta data + file[key_field].attrs["space_id"] = spline.space_id + file[key_field].attrs["starts"] = spline.starts + file[key_field].attrs["ends"] = spline.ends + file[key_field].attrs["pads"] = spline.pads + + # save numpy array to be updated only at the end of the simulation for restart. + key_field_restart = os.path.join(species_path_restart, variable) + + if isinstance(spline.vector_stencil, StencilVector): + data.add_data( + {key_field_restart: spline.vector_stencil._data}, + ) + else: + for n in range(3): + key_component_restart = os.path.join(key_field_restart, str(n + 1)) + data.add_data( + {key_component_restart: spline.vector_stencil[n]._data}, + ) + + # save kinetic data in group 'kinetic/' + for name, species in self.model.particle_species.items(): + assert isinstance(species, ParticleSpecies) + assert len(species.variables) == 1, "More than 1 variable per kinetic species is not allowed." + for varname, var in species.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + obj = var.particles + assert isinstance(obj, Particles) + + key_spec = os.path.join("kinetic", name) + key_spec_restart = os.path.join("restart", name) + + # restart data + data.add_data({key_spec_restart: obj.markers}) + + # marker data + key_mks = os.path.join(key_spec, "markers") + data.add_data({key_mks: var.saved_markers}) + + # binning plot data + for bin_plot in species.binning_plots: + # define slice name with binning quantity + slice, output_quantity = bin_plot.slice, bin_plot.output_quantity + slice = f"{slice}_{output_quantity}" + + key_f = os.path.join(key_spec, "f", slice) + key_df = os.path.join(key_spec, "df", slice) + + data.add_data({key_f: bin_plot.f}) + data.add_data({key_df: bin_plot.df}) + + for dim, be in enumerate(bin_plot.bin_edges): + file[key_f].attrs["bin_centers" + "_" + str(dim + 1)] = be[:-1] + (be[1] - be[0]) / 2 + + for i, kd_plot in enumerate(species.kernel_density_plots): + key_n = os.path.join(key_spec, "n_sph", f"view_{i}") + + data.add_data({key_n: kd_plot.n_sph}) + # save 1d point values, not meshgrids, because attrs size is limited + eta1 = kd_plot.plot_pts[0][:, 0, 0] + eta2 = kd_plot.plot_pts[1][0, :, 0] + eta3 = kd_plot.plot_pts[2][0, 0, :] + file[key_n].attrs["eta1"] = eta1 + file[key_n].attrs["eta2"] = eta2 + file[key_n].attrs["eta3"] = eta3 + + # TODO: maybe add other data + # else: + # data.add_data({key_dat: val1}) + + # keys to be saved at each time step and only at end (restart) + save_keys_all = [] + save_keys_end = [] + + for key in data.dset_dict: + if "restart" in key: + save_keys_end.append(key) + else: + save_keys_all.append(key) + + return save_keys_all, save_keys_end + + @property + def domain(self): + """Domain object, see :ref:`avail_mappings`.""" + return self._domain + + @property + def equil(self): + """Fluid equilibrium object, see :ref:`fluid_equil`.""" + return self._equil + + @property + def derham(self): + """3d Derham sequence, see :ref:`derham`.""" + return self._derham + + @property + def mass_ops(self): + """WeighteMassOperators object, see :ref:`mass_ops`.""" + return self._mass_ops + + @property + def basis_ops(self): + """Basis projection operators.""" + return self._basis_ops + + @property + def projected_equil(self): + """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" + return self._projected_equil + \ No newline at end of file From d740d89873e7cddce5bcfdf44705d92bf2679e65 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 10 Feb 2026 16:19:31 +0100 Subject: [PATCH 07/80] uncomment sim.run --- src/struphy/main.py | 1 + src/struphy/models/base.py | 2 +- .../models/linear_mhd_driftkinetic_cc.py | 5 +- src/struphy/models/maxwell.py | 5 +- src/struphy/propagators/base.py | 4 +- src/struphy/simulation/sim.py | 378 +++++++++--------- 6 files changed, 202 insertions(+), 193 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index 5b1390904..e9dbedc64 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -91,6 +91,7 @@ def run( # plasma parameters sim.compute_plasma_params(verbose=verbose) + sim.run(verbose=verbose) exit() if sim.rank < 32: diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index f7052f776..c387f95e1 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -244,7 +244,7 @@ def units(self, new): self._units = new @property - def mass_ops(self): + def mass_ops(self) -> WeightedMassOperators: """WeighteMassOperators object, see :ref:`mass_ops`.""" return self._mass_ops diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index a1604cd66..03d8021b4 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -15,6 +15,7 @@ propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() @@ -188,7 +189,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - self._ones = self.projected_equil.p3.space.zeros() + self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: @@ -199,7 +200,7 @@ def allocate_helpers(self): self._en_tot = xp.empty(1, dtype=float) self._n_lost_particles = xp.empty(1, dtype=float) - self._PB = getattr(self.basis_ops, "PB") + self._PB = getattr(Propagator.basis_ops, "PB") self._PBb = self._PB.codomain.zeros() def update_scalar_quantities(self): diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index 905e89219..b1ca7d744 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -6,6 +6,7 @@ FieldSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -86,11 +87,11 @@ def allocate_helpers(self): pass def update_scalar_quantities(self): - en_E = 0.5 * self.mass_ops.M1.dot_inner( + en_E = 0.5 * Propagator.mass_ops.M1.dot_inner( self.em_fields.e_field.spline.vector, self.em_fields.e_field.spline.vector, ) - en_B = 0.5 * self.mass_ops.M2.dot_inner( + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner( self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector, ) diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index 159150bbb..ea836ad76 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -169,7 +169,7 @@ def domain(self, domain): self._domain = domain @property - def mass_ops(self): + def mass_ops(self) -> WeightedMassOperators: """Weighted mass operators.""" assert hasattr(self, "_mass_ops"), "Weighted mass operators not set. Please do obj.mass_ops = ..." assert isinstance(self._mass_ops, WeightedMassOperators) @@ -177,7 +177,7 @@ def mass_ops(self): @mass_ops.setter def mass_ops(self, mass_ops): - self._mass_ops = mass_ops + self._mass_ops: WeightedMassOperators = mass_ops @property def basis_ops(self): diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 05ff6ecfc..462ef28aa 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -33,6 +33,7 @@ from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.io.output_handling import DataContainer from struphy.pic.base import Particles +from struphy.utils.utils import dict_to_yaml # third party imports from feectools.ddm.mpi import MockMPI @@ -108,10 +109,10 @@ def __init__(self, # check model assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" model.verbose = verbose - model_name = model.__class__.__name__ + self.model_name = model.__class__.__name__ if self.rank == 0: - print(f"\n*** Starting run for model '{model_name}':") + print(f"\n*** Starting run for model '{self.model_name}':") # meta-data path_out = env.path_out @@ -125,7 +126,7 @@ def __init__(self, self.meta = {} self.meta["platform"] = sysconfig.get_platform() self.meta["python version"] = sysconfig.get_python_version() - self.meta["model name"] = model_name + self.meta["model name"] = self.model_name self.meta["parameter file"] = params_path self.meta["output folder"] = path_out self.meta["MPI processes"] = self.size @@ -232,30 +233,30 @@ def allocate(self, verbose: bool = False): # pass info to propagators self._allocate_propagators() - # def store_geometry(self, verbose: bool = False): - # # store geometry vtk - # if self.rank == 0: - # grids_log = [ - # xp.linspace(1e-6, 1.0, 32), - # xp.linspace(0.0, 1.0, 32), - # xp.linspace(0.0, 1.0, 32), - # ] + def store_geometry(self, verbose: bool = False): + # store geometry vtk + if self.rank == 0: + grids_log = [ + xp.linspace(1e-6, 1.0, 32), + xp.linspace(0.0, 1.0, 32), + xp.linspace(0.0, 1.0, 32), + ] - # tmp = self.domain(*grids_log) - # grids_phy = [tmp[0], tmp[1], tmp[2]] + tmp = self.domain(*grids_log) + grids_phy = [tmp[0], tmp[1], tmp[2]] - # pointData = {} - # det_df = self.domain.jacobian_det(*grids_log) - # pointData["det_df"] = det_df + pointData = {} + det_df = self.domain.jacobian_det(*grids_log) + pointData["det_df"] = det_df - # if self.equil is not None: - # p0 = self.equil.p0(*grids_log) - # pointData["p0"] = p0 - # if isinstance(self.equil, FluidEquilibriumWithB): - # absB0 = self.equil.absB0(*grids_log) - # pointData["absB0"] = absB0 + if self.equil is not None: + p0 = self.equil.p0(*grids_log) + pointData["p0"] = p0 + if isinstance(self.equil, FluidEquilibriumWithB): + absB0 = self.equil.absB0(*grids_log) + pointData["absB0"] = absB0 - # gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) + gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) def compute_plasma_params(self, verbose=True): """ @@ -465,169 +466,174 @@ def add_time_state(self, time_state): for _, prop in self.propagators.__dict__.items(): if isinstance(prop, Propagator): prop.add_time_state(time_state) - # def run(self, verbose: bool = False): - # if rank < 32: - # if rank == 0: - # print("") - # print(f"Rank {rank}: executing main.run() for model {model_name} ...") - - # if size > 32 and rank == 32: - # print(f"Ranks > 31: executing main.run() for model {model_name} ...") - - # # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) - # # use MPI.COMM_WORLD as communicator when storing the outputs - # data = DataContainer(path_out, comm=comm) - - # # time quantities (current time value, value in seconds and index) - # time_state = {} - # time_state["value"] = xp.zeros(1, dtype=float) - # time_state["value_sec"] = xp.zeros(1, dtype=float) - # time_state["index"] = xp.zeros(1, dtype=int) - - # # add time quantities to data object for saving - # for key, val in time_state.items(): - # key_time = "time/" + key - # key_time_restart = "restart/time/" + key - # data.add_data({key_time: val}) - # data.add_data({key_time_restart: val}) - - # # retrieve time parameters - # dt = time_opts.dt - # Tend = time_opts.Tend - # split_algo = time_opts.split_algo - - # # set initial conditions for all variables - # if restart: - # model.initialize_from_restart(data) - - # with h5py.File(data.file_path, "a") as file: - # time_state["value"][0] = file["restart/time/value"][-1] - # time_state["value_sec"][0] = file["restart/time/value_sec"][-1] - # time_state["index"][0] = file["restart/time/index"][-1] - - # total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) - # else: - # total_steps = str(int(round(Tend / dt))) - - # # compute initial scalars and kinetic data, pass time state to all propagators - # model.update_scalar_quantities() - # model.update_markers_to_be_saved() - # model.update_distr_functions() - # model.add_time_state(time_state["value"]) - - # # add all variables to be saved to data object - # save_keys_all, save_keys_end = model.initialize_data_output(data, size) - - # # ======================== main time loop ====================== - # model.update_scalar_quantities() - # if rank == 0: - # print("\nINITIAL SCALAR QUANTITIES:") - # model.print_scalar_quantities() - - # print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") - - # # time loop - # run_time_now = 0.0 - # while True: - # Barrier() - - # # stop time loop? - # break_cond_1 = time_state["value"][0] >= Tend - # break_cond_2 = run_time_now > max_runtime - - # if break_cond_1 or break_cond_2: - # # save restart data (other data already saved below) - # data.save_data(keys=save_keys_end) - # end_simulation = time.time() - # if rank == 0: - # print(f"\nTime steps done: {time_state['index'][0]}") - # print( - # "wall-clock time of simulation [sec]: ", - # end_simulation - start_simulation, - # ) - # print() - # break - - # if sort_step and time_state["index"][0] % sort_step == 0: - # t0 = time.time() - # for key, val in model.pointer.items(): - # if isinstance(val, Particles): - # val.do_sort() - # t1 = time.time() - # if rank == 0 and verbose: - # message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( - # run_time_now * 60, - # t1 - t0, - # ) - # print(message, end="\n") - # print() - - # # update time and index (round time to 10 decimals for a clean time grid!) - # time_state["value"][0] = round(time_state["value"][0] + dt, 10) - # time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * model.units.t, 10) - # time_state["index"][0] += 1 - - # # perform one time step dt - # t0 = time.time() - # with ProfileManager.profile_region("model.integrate"): - # model.integrate(dt, split_algo) - # t1 = time.time() - - # run_time_now = (time.time() - start_simulation) / 60 - - # # update diagnostics data and save data - # if time_state["index"][0] % save_step == 0: - # # compute scalars and kinetic data - # model.update_scalar_quantities() - # model.update_markers_to_be_saved() - # model.update_distr_functions() - - # # extract FEEC coefficients - # feec_species = model.field_species | model.fluid_species | model.diagnostic_species - # for species, val in feec_species.items(): - # assert isinstance(val, Species) - # for variable, subval in val.variables.items(): - # assert isinstance(subval, FEECVariable) - # spline = subval.spline - # # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - # spline.extract_coeffs(update_ghost_regions=False) - - # # save data (everything but restart data) - # data.save_data(keys=save_keys_all) - - # # print current time and scalar quantities to screen - # if rank == 0 and verbose: - # step = str(time_state["index"][0]).zfill(len(total_steps)) - - # message = "time step: " + step + "/" + str(total_steps) - # message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) - # message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( - # time_state["value_sec"][0], - # Tend * model.units.t, - # ) - # message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( - # run_time_now * 60, - # t1 - t0, - # ) - - # print(message, end="\n") - # model.print_scalar_quantities() - # print() - - # # =================================================================== - - # self.meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 - # Barrier() - - # if rank == 0: - # # save meta-data - # dict_to_yaml(self.meta, os.path.join(path_out, "meta.yml")) - # print("Struphy run finished.") - - # if clone_config is not None: - # clone_config.free() - - # ProfileManager.finalize() + + def run(self, verbose: bool = False): + if self.rank < 32: + if self.rank == 0: + print("") + print(f"Rank {self.rank}: executing main.run() for model {self.model_name} ...") + + if self.size > 32 and self.rank == 32: + print(f"Ranks > 31: executing main.run() for model {self.model_name} ...") + + # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) + # use MPI.COMM_WORLD as communicator when storing the outputs + data = DataContainer(self.env.path_out, comm=self.comm) + + # time quantities (current time value, value in seconds and index) + time_state = {} + time_state["value"] = xp.zeros(1, dtype=float) + time_state["value_sec"] = xp.zeros(1, dtype=float) + time_state["index"] = xp.zeros(1, dtype=int) + + # add time quantities to data object for saving + for key, val in time_state.items(): + key_time = "time/" + key + key_time_restart = "restart/time/" + key + data.add_data({key_time: val}) + data.add_data({key_time_restart: val}) + + # retrieve time parameters + dt = self.time_opts.dt + Tend = self.time_opts.Tend + split_algo = self.time_opts.split_algo + + # set initial conditions for all variables + if self.env.restart: + self.model.initialize_from_restart(data) + + with h5py.File(data.file_path, "a") as file: + time_state["value"][0] = file["restart/time/value"][-1] + time_state["value_sec"][0] = file["restart/time/value_sec"][-1] + time_state["index"][0] = file["restart/time/index"][-1] + + total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) + else: + total_steps = str(int(round(Tend / dt))) + + # compute initial scalars and kinetic data, pass time state to all propagators + self.model.update_scalar_quantities() + self.model.update_markers_to_be_saved() + self.model.update_distr_functions() + self.model.add_time_state(time_state["value"]) + + # add all variables to be saved to data object + save_keys_all, save_keys_end = self.model.initialize_data_output(data, self.size) + + # ======================== main time loop ====================== + self.model.update_scalar_quantities() + if self.rank == 0: + print("\nINITIAL SCALAR QUANTITIES:") + self.model.print_scalar_quantities() + + print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") + + # time loop + run_time_now = 0.0 + while True: + self.Barrier() + + # stop time loop? + break_cond_1 = time_state["value"][0] >= Tend + break_cond_2 = run_time_now > self.env.max_runtime + + if break_cond_1 or break_cond_2: + # save restart data (other data already saved below) + data.save_data(keys=save_keys_end) + end_time = time.time() + if self.rank == 0: + print(f"\nTime steps done: {time_state['index'][0]}") + print( + "wall-clock time of simulation [sec]: ", + end_time - self.start_time, + ) + print() + break + + if self.env.sort_step and time_state["index"][0] % self.env.sort_step == 0: + t0 = time.time() + for key, val in self.model.pointer.items(): + if isinstance(val, Particles): + val.do_sort() + t1 = time.time() + if self.rank == 0 and verbose: + message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( + run_time_now * 60, + t1 - t0, + ) + print(message, end="\n") + print() + + # update time and index (round time to 10 decimals for a clean time grid!) + time_state["value"][0] = round(time_state["value"][0] + dt, 10) + time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * self.model.units.t, 10) + time_state["index"][0] += 1 + + # perform one time step dt + t0 = time.time() + with ProfileManager.profile_region("model.integrate"): + self.model.integrate(dt, split_algo) + t1 = time.time() + + run_time_now = (time.time() - self.start_time) / 60 + + # update diagnostics data and save data + if time_state["index"][0] % self.env.save_step == 0: + # compute scalars and kinetic data + self.model.update_scalar_quantities() + self.model.update_markers_to_be_saved() + self.model.update_distr_functions() + + # extract FEEC coefficients + feec_species = self.model.field_species | self.model.fluid_species | self.model.diagnostic_species + for species, val in feec_species.items(): + assert isinstance(val, Species) + for variable, subval in val.variables.items(): + assert isinstance(subval, FEECVariable) + spline = subval.spline + # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! + spline.extract_coeffs(update_ghost_regions=False) + + # save data (everything but restart data) + data.save_data(keys=save_keys_all) + + # print current time and scalar quantities to screen + if self.rank == 0 and verbose: + step = str(time_state["index"][0]).zfill(len(total_steps)) + + message = "time step: " + step + "/" + str(total_steps) + message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) + message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( + time_state["value_sec"][0], + Tend * self.model.units.t, + ) + message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( + run_time_now * 60, + t1 - t0, + ) + + print(message, end="\n") + self.model.print_scalar_quantities() + print() + + # =================================================================== + + self.meta["wall-clock time[min]"] = (end_time - self.start_time) / 60 + self.Barrier() + + if self.rank == 0: + # save meta-data + dict_to_yaml(self.meta, os.path.join(self.env.path_out, "meta.yml")) + print("Struphy run finished.") + + if self.clone_config is not None: + self.clone_config.free() + + ProfileManager.finalize() + + # --------------- + # Private methods + # --------------- def _setup_folders( self, From d0a899f7f391587218c7695e13fce6fbf3e7cbeb Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 08:13:08 +0100 Subject: [PATCH 08/80] Simulation.run works for Maxwell --- src/struphy/models/base.py | 95 +++++++++++++++++++++++++++++++- src/struphy/propagators/base.py | 13 +++-- src/struphy/simulation/sim.py | 97 +-------------------------------- 3 files changed, 106 insertions(+), 99 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index c387f95e1..9e5ed7c15 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -86,7 +86,100 @@ def allocate_helpers(self): def update_scalar_quantities(self): """Specify an update rule for each item in ``scalar_quantities`` using :meth:`update_scalar`.""" - ## setup methods + # -------------- + # common methods + # -------------- + + def update_scalar(self, name, value=None): + """Update a scalar during the simulation. + + Parameters + ---------- + name : str + Dictionary key of the scalar. + + value : float, optional + Value to be saved. Required if there are no summands. + """ + + # Ensure the name is a string + assert isinstance(name, str) + + scalars = self.scalar_quantities + + variable: PICVariable | SPHVariable = scalars[name]["variable"] + summands = scalars[name]["summands"] + compute = scalars[name]["compute"] + + if compute == "from_particles": + compute_operations = [ + "sum_within_clone", + "sum_between_clones", + "divide_n_mks", + ] + elif compute == "from_sph": + compute_operations = [ + "sum_world", + "divide_n_mks", + ] + elif compute == "from_field": + compute_operations = [] + else: + compute_operations = [] + + if summands is None: + # Ensure the value is a float if there are no summands + assert isinstance(value, float) + + # Create a numpy array to hold the scalar value + value_array = xp.array([value], dtype=xp.float64) + + # Perform MPI operations based on the compute flags + if "sum_world" in compute_operations and not isinstance(MPI, MockMPI): + MPI.COMM_WORLD.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + + if "sum_within_clone" in compute_operations and Propagator.derham.comm is not None: + Propagator.derham.comm.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + if self.clone_config is None: + num_clones = 1 + else: + num_clones = self.clone_config.num_clones + + if "sum_between_clones" in compute_operations and num_clones > 1: + self.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + + if "average_between_clones" in compute_operations and num_clones > 1: + self.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + value_array, + op=MPI.SUM, + ) + value_array /= num_clones + + if "divide_n_mks" in compute_operations: + # Initialize the total number of markers + n_mks_tot = xp.array([variable.particles.Np]) + value_array /= n_mks_tot + + # Update the scalar value + scalars[name]["value"][0] = value_array[0] + + else: + # Sum the values of the summands + value = sum(scalars[summand]["value"][0] for summand in summands) + scalars[name]["value"][0] = value def setup_equation_params(self, units: Units, verbose=False): """Set euqation parameters for each fluid and kinetic species.""" diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index ea836ad76..e25a11a4a 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -144,7 +144,7 @@ def rank(self): return self._rank @property - def derham(self): + def derham(self) -> Derham: """Derham spaces and projectors.""" assert hasattr( self, @@ -155,10 +155,11 @@ def derham(self): @derham.setter def derham(self, derham): + assert isinstance(derham, Derham) self._derham = derham @property - def domain(self): + def domain(self) -> Domain: """Domain object that characterizes the mapping from the logical to the physical domain.""" assert hasattr(self, "_domain"), "Domain for analytical MHD equilibrium not set. Please do obj.domain = ..." assert isinstance(self._domain, Domain) @@ -166,6 +167,7 @@ def domain(self): @domain.setter def domain(self, domain): + assert isinstance(domain, Domain) self._domain = domain @property @@ -177,10 +179,11 @@ def mass_ops(self) -> WeightedMassOperators: @mass_ops.setter def mass_ops(self, mass_ops): - self._mass_ops: WeightedMassOperators = mass_ops + assert isinstance(mass_ops, WeightedMassOperators) + self._mass_ops = mass_ops @property - def basis_ops(self): + def basis_ops(self) -> BasisProjectionOperators: """Basis projection operators.""" assert hasattr(self, "_basis_ops"), "Basis projection operators not set. Please do obj.basis_ops = ..." assert isinstance(self._basis_ops, BasisProjectionOperators) @@ -188,6 +191,7 @@ def basis_ops(self): @basis_ops.setter def basis_ops(self, basis_ops): + assert isinstance(basis_ops, BasisProjectionOperators) self._basis_ops = basis_ops @property @@ -197,6 +201,7 @@ def projected_equil(self) -> ProjectedFluidEquilibriumWithB: self, "_projected_equil", ), "Projected MHD equilibrium not set." + assert isinstance(self._projected_equil, ProjectedFluidEquilibriumWithB) return self._projected_equil @projected_equil.setter diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 462ef28aa..bc49975aa 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -361,97 +361,6 @@ def compute_plasma_params(self, verbose=True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - def update_scalar(self, name, value=None): - """Update a scalar during the simulation. - - Parameters - ---------- - name : str - Dictionary key of the scalar. - - value : float, optional - Value to be saved. Required if there are no summands. - """ - - # Ensure the name is a string - assert isinstance(name, str) - - scalars = self.model.scalar_quantities - - variable: PICVariable | SPHVariable = scalars[name]["variable"] - summands = scalars[name]["summands"] - compute = scalars[name]["compute"] - - if compute == "from_particles": - compute_operations = [ - "sum_within_clone", - "sum_between_clones", - "divide_n_mks", - ] - elif compute == "from_sph": - compute_operations = [ - "sum_world", - "divide_n_mks", - ] - elif compute == "from_field": - compute_operations = [] - else: - compute_operations = [] - - if summands is None: - # Ensure the value is a float if there are no summands - assert isinstance(value, float) - - # Create a numpy array to hold the scalar value - value_array = xp.array([value], dtype=xp.float64) - - # Perform MPI operations based on the compute flags - if "sum_world" in compute_operations and not isinstance(MPI, MockMPI): - MPI.COMM_WORLD.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - - if "sum_within_clone" in compute_operations and self.derham.comm is not None: - self.derham.comm.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - if self.clone_config is None: - num_clones = 1 - else: - num_clones = self.clone_config.num_clones - - if "sum_between_clones" in compute_operations and num_clones > 1: - self.clone_config.inter_comm.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - - if "average_between_clones" in compute_operations and num_clones > 1: - self.clone_config.inter_comm.Allreduce( - MPI.IN_PLACE, - value_array, - op=MPI.SUM, - ) - value_array /= num_clones - - if "divide_n_mks" in compute_operations: - # Initialize the total number of markers - n_mks_tot = xp.array([variable.particles.Np]) - value_array /= n_mks_tot - - # Update the scalar value - scalars[name]["value"][0] = value_array[0] - - else: - # Sum the values of the summands - value = sum(scalars[summand]["value"][0] for summand in summands) - scalars[name]["value"][0] = value - def add_time_state(self, time_state): """Add a pointer to the time variable of the dynamics ('t') to the model and to all propagators of the model. @@ -518,7 +427,7 @@ def run(self, verbose: bool = False): self.model.add_time_state(time_state["value"]) # add all variables to be saved to data object - save_keys_all, save_keys_end = self.model.initialize_data_output(data, self.size) + save_keys_all, save_keys_end = self._initialize_data_output(data, self.size) # ======================== main time loop ====================== self.model.update_scalar_quantities() @@ -566,7 +475,7 @@ def run(self, verbose: bool = False): # update time and index (round time to 10 decimals for a clean time grid!) time_state["value"][0] = round(time_state["value"][0] + dt, 10) - time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * self.model.units.t, 10) + time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * self.units.t, 10) time_state["index"][0] += 1 # perform one time step dt @@ -605,7 +514,7 @@ def run(self, verbose: bool = False): message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( time_state["value_sec"][0], - Tend * self.model.units.t, + Tend * self.units.t, ) message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( run_time_now * 60, From ea3ec2674585139a8c15095e78eca15509b3bb0f Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 08:34:55 +0100 Subject: [PATCH 09/80] have methods called within sim.run() --- src/struphy/main.py | 7 +- src/struphy/models/base.py | 15 ---- src/struphy/simulation/sim.py | 126 +++++++++++++++++++--------------- 3 files changed, 71 insertions(+), 77 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index e9dbedc64..36681b80b 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -85,12 +85,7 @@ def run( verbose=verbose, ) - # equation paramters - sim.allocate(verbose=verbose) - - # plasma parameters - sim.compute_plasma_params(verbose=verbose) - + # run simulation sim.run(verbose=verbose) exit() diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 9e5ed7c15..160ac3582 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -519,21 +519,6 @@ def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, comp "summands": summands, } - def add_time_state(self, time_state): - """Add a pointer to the time variable of the dynamics ('t') - to the model and to all propagators of the model. - - Parameters - ---------- - time_state : ndarray - Of size 1, holds the current physical time 't'. - """ - assert time_state.size == 1 - self._time_state = time_state - for _, prop in self.propagators.__dict__.items(): - if isinstance(prop, Propagator): - prop.add_time_state(time_state) - @profile def integrate(self, dt, split_algo="LieTrotter"): """ diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index bc49975aa..e4b7605c0 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -11,8 +11,7 @@ # core imports from struphy.models.base import StruphyModel from struphy.geometry.base import Domain -from struphy.fields_background.base import FluidEquilibrium, NumericalMHDequilibrium, FluidEquilibriumWithB -from struphy.io.setup import setup_folders +from struphy.fields_background.base import (FluidEquilibrium, NumericalMHDequilibrium, FluidEquilibriumWithB,) from struphy.physics.physics import Units from struphy.utils.clone_config import CloneConfig from struphy.feec.basis_projection_ops import BasisProjectionOperators @@ -29,7 +28,7 @@ ProjectedMHDequilibrium, ) from struphy.propagators.base import Propagator -from struphy.models.species import DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species +from struphy.models.species import (DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species,) from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.io.output_handling import DataContainer from struphy.pic.base import Particles @@ -360,47 +359,47 @@ def compute_plasma_params(self, verbose=True): "Min magnetic field:".ljust(25), "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - - def add_time_state(self, time_state): - """Add a pointer to the time variable of the dynamics ('t') - to the model and to all propagators of the model. - - Parameters - ---------- - time_state : ndarray - Of size 1, holds the current physical time 't'. - """ - assert time_state.size == 1 - self._time_state = time_state - for _, prop in self.propagators.__dict__.items(): - if isinstance(prop, Propagator): - prop.add_time_state(time_state) - def run(self, verbose: bool = False): - if self.rank < 32: - if self.rank == 0: - print("") - print(f"Rank {self.rank}: executing main.run() for model {self.model_name} ...") - - if self.size > 32 and self.rank == 32: - print(f"Ranks > 31: executing main.run() for model {self.model_name} ...") - + def initialize_data(self, verbose: bool = False): # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs - data = DataContainer(self.env.path_out, comm=self.comm) + self.data = DataContainer(self.env.path_out, comm=self.comm) # time quantities (current time value, value in seconds and index) - time_state = {} - time_state["value"] = xp.zeros(1, dtype=float) - time_state["value_sec"] = xp.zeros(1, dtype=float) - time_state["index"] = xp.zeros(1, dtype=int) + self.time_state = {} + self.time_state["value"] = xp.zeros(1, dtype=float) + self.time_state["value_sec"] = xp.zeros(1, dtype=float) + self.time_state["index"] = xp.zeros(1, dtype=int) # add time quantities to data object for saving - for key, val in time_state.items(): + for key, val in self.time_state.items(): key_time = "time/" + key key_time_restart = "restart/time/" + key - data.add_data({key_time: val}) - data.add_data({key_time_restart: val}) + self.data.add_data({key_time: val}) + self.data.add_data({key_time_restart: val}) + + def run(self, verbose: bool = False): + + # equation paramters + self.allocate(verbose=self.verbose) + + # peek view into geometry + self.store_geometry(verbose=self.verbose) + + # plasma parameters + self.compute_plasma_params(verbose=self.verbose) + + # outout + self.initialize_data(verbose=self.verbose) + + # print info on mpi procs + if self.rank < 32: + if self.rank == 0: + print("") + print(f"Rank {self.rank}: executing run() for model {self.model_name} ...") + + if self.size > 32 and self.rank == 32: + print(f"Ranks > 31: executing run() for model {self.model_name} ...") # retrieve time parameters dt = self.time_opts.dt @@ -409,14 +408,14 @@ def run(self, verbose: bool = False): # set initial conditions for all variables if self.env.restart: - self.model.initialize_from_restart(data) + self.model.initialize_from_restart(self.data) - with h5py.File(data.file_path, "a") as file: - time_state["value"][0] = file["restart/time/value"][-1] - time_state["value_sec"][0] = file["restart/time/value_sec"][-1] - time_state["index"][0] = file["restart/time/index"][-1] + with h5py.File(self.data.file_path, "a") as file: + self.time_state["value"][0] = file["restart/time/value"][-1] + self.time_state["value_sec"][0] = file["restart/time/value_sec"][-1] + self.time_state["index"][0] = file["restart/time/index"][-1] - total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) + total_steps = str(int(round((Tend - self.time_state["value"][0]) / dt))) else: total_steps = str(int(round(Tend / dt))) @@ -424,10 +423,10 @@ def run(self, verbose: bool = False): self.model.update_scalar_quantities() self.model.update_markers_to_be_saved() self.model.update_distr_functions() - self.model.add_time_state(time_state["value"]) + self._add_time_state(self.time_state["value"]) # add all variables to be saved to data object - save_keys_all, save_keys_end = self._initialize_data_output(data, self.size) + save_keys_all, save_keys_end = self._initialize_hdf5_datasets(self.data, self.size) # ======================== main time loop ====================== self.model.update_scalar_quantities() @@ -443,15 +442,15 @@ def run(self, verbose: bool = False): self.Barrier() # stop time loop? - break_cond_1 = time_state["value"][0] >= Tend + break_cond_1 = self.time_state["value"][0] >= Tend break_cond_2 = run_time_now > self.env.max_runtime if break_cond_1 or break_cond_2: # save restart data (other data already saved below) - data.save_data(keys=save_keys_end) + self.data.save_data(keys=save_keys_end) end_time = time.time() if self.rank == 0: - print(f"\nTime steps done: {time_state['index'][0]}") + print(f"\nTime steps done: {self.time_state['index'][0]}") print( "wall-clock time of simulation [sec]: ", end_time - self.start_time, @@ -459,7 +458,7 @@ def run(self, verbose: bool = False): print() break - if self.env.sort_step and time_state["index"][0] % self.env.sort_step == 0: + if self.env.sort_step and self.time_state["index"][0] % self.env.sort_step == 0: t0 = time.time() for key, val in self.model.pointer.items(): if isinstance(val, Particles): @@ -474,9 +473,9 @@ def run(self, verbose: bool = False): print() # update time and index (round time to 10 decimals for a clean time grid!) - time_state["value"][0] = round(time_state["value"][0] + dt, 10) - time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * self.units.t, 10) - time_state["index"][0] += 1 + self.time_state["value"][0] = round(self.time_state["value"][0] + dt, 10) + self.time_state["value_sec"][0] = round(self.time_state["value_sec"][0] + dt * self.units.t, 10) + self.time_state["index"][0] += 1 # perform one time step dt t0 = time.time() @@ -487,7 +486,7 @@ def run(self, verbose: bool = False): run_time_now = (time.time() - self.start_time) / 60 # update diagnostics data and save data - if time_state["index"][0] % self.env.save_step == 0: + if self.time_state["index"][0] % self.env.save_step == 0: # compute scalars and kinetic data self.model.update_scalar_quantities() self.model.update_markers_to_be_saved() @@ -504,16 +503,16 @@ def run(self, verbose: bool = False): spline.extract_coeffs(update_ghost_regions=False) # save data (everything but restart data) - data.save_data(keys=save_keys_all) + self.data.save_data(keys=save_keys_all) # print current time and scalar quantities to screen if self.rank == 0 and verbose: - step = str(time_state["index"][0]).zfill(len(total_steps)) + step = str(self.time_state["index"][0]).zfill(len(total_steps)) message = "time step: " + step + "/" + str(total_steps) - message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) + message += " | " + "time: {0:10.5f}/{1:10.5f}".format(self.time_state["value"][0], Tend) message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( - time_state["value_sec"][0], + self.time_state["value_sec"][0], Tend * self.units.t, ) message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( @@ -886,7 +885,7 @@ def _allocate_propagators(self): print(f"\nAllocated propagator '{prop.__class__.__name__}'.") @profile - def _initialize_data_output(self, data: DataContainer, size): + def _initialize_hdf5_datasets(self, data: DataContainer, size): """ Create datasets in hdf5 files according to model unknowns and diagnostics data. @@ -1036,6 +1035,21 @@ def _initialize_data_output(self, data: DataContainer, size): return save_keys_all, save_keys_end + def _add_time_state(self, time_state): + """Add a pointer to the time variable of the dynamics ('t') + to the model and to all propagators of the model. + + Parameters + ---------- + time_state : ndarray + Of size 1, holds the current physical time 't'. + """ + assert time_state.size == 1 + self._time_state = time_state + for _, prop in self.model.propagators.__dict__.items(): + if isinstance(prop, Propagator): + prop.add_time_state(time_state) + @property def domain(self): """Domain object, see :ref:`avail_mappings`.""" From e58af43ec1c763c19acfcfd7270fb745162bd902 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 09:28:12 +0100 Subject: [PATCH 10/80] new base class Simulation; then inherit StruphySimulation(Simulation) --- src/struphy/main.py | 194 +--------------------------- src/struphy/simulation/base.py | 29 +++++ src/struphy/simulation/sim.py | 222 +++++++++++++++++---------------- 3 files changed, 147 insertions(+), 298 deletions(-) create mode 100644 src/struphy/simulation/base.py diff --git a/src/struphy/main.py b/src/struphy/main.py index 36681b80b..65fce7bd0 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -43,7 +43,7 @@ from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml -from struphy.simulation.sim import Simulation +from struphy.simulation.sim import StruphySimulation @profile @@ -72,7 +72,7 @@ def run( Absolute path to .py parameter file. """ - sim = Simulation( + sim = StruphySimulation( model=model, params_path=params_path, env=env, @@ -85,197 +85,7 @@ def run( verbose=verbose, ) - # run simulation sim.run(verbose=verbose) - exit() - - if sim.rank < 32: - if sim.rank == 0: - print("") - print(f"Rank {sim.rank}: executing main.run() for model {model} ...") - - if sim.size > 32 and sim.rank == 32: - print(f"Ranks > 31: executing main.run() for model {model} ...") - - # store geometry vtk - if sim.rank == 0: - grids_log = [ - xp.linspace(1e-6, 1.0, 32), - xp.linspace(0.0, 1.0, 32), - xp.linspace(0.0, 1.0, 32), - ] - - tmp = model.domain(*grids_log) - grids_phy = [tmp[0], tmp[1], tmp[2]] - - pointData = {} - det_df = model.domain.jacobian_det(*grids_log) - pointData["det_df"] = det_df - - if model.equil is not None: - p0 = model.equil.p0(*grids_log) - pointData["p0"] = p0 - if isinstance(model.equil, FluidEquilibriumWithB): - absB0 = model.equil.absB0(*grids_log) - pointData["absB0"] = absB0 - - gridToVTK(os.path.join(sim.env.path_out, "geometry"), *grids_phy, pointData=pointData) - - # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) - # use MPI.COMM_WORLD as communicator when storing the outputs - data = DataContainer(sim.env.path_out, comm=sim.comm) - - # time quantities (current time value, value in seconds and index) - time_state = {} - time_state["value"] = xp.zeros(1, dtype=float) - time_state["value_sec"] = xp.zeros(1, dtype=float) - time_state["index"] = xp.zeros(1, dtype=int) - - # add time quantities to data object for saving - for key, val in time_state.items(): - key_time = "time/" + key - key_time_restart = "restart/time/" + key - data.add_data({key_time: val}) - data.add_data({key_time_restart: val}) - - # retrieve time parameters - dt = time_opts.dt - Tend = time_opts.Tend - split_algo = time_opts.split_algo - - # set initial conditions for all variables - if sim.env.restart: - model.initialize_from_restart(data) - - with h5py.File(data.file_path, "a") as file: - time_state["value"][0] = file["restart/time/value"][-1] - time_state["value_sec"][0] = file["restart/time/value_sec"][-1] - time_state["index"][0] = file["restart/time/index"][-1] - - total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) - else: - total_steps = str(int(round(Tend / dt))) - - # compute initial scalars and kinetic data, pass time state to all propagators - model.update_scalar_quantities() - model.update_markers_to_be_saved() - model.update_distr_functions() - model.add_time_state(time_state["value"]) - - # add all variables to be saved to data object - save_keys_all, save_keys_end = model.initialize_data_output(data, sim.size) - - # ======================== main time loop ====================== - model.update_scalar_quantities() - if sim.rank == 0: - print("\nINITIAL SCALAR QUANTITIES:") - model.print_scalar_quantities() - - print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") - - # time loop - run_time_now = 0.0 - while True: - sim.Barrier() - - # stop time loop? - break_cond_1 = time_state["value"][0] >= Tend - break_cond_2 = run_time_now > sim.env.max_runtime - - if break_cond_1 or break_cond_2: - # save restart data (other data already saved below) - data.save_data(keys=save_keys_end) - end_simulation = time.time() - if sim.rank == 0: - print(f"\nTime steps done: {time_state['index'][0]}") - print( - "wall-clock time of simulation [sec]: ", - end_simulation - sim.start_time, - ) - print() - break - - if sim.env.sort_step and time_state["index"][0] % sim.env.sort_step == 0: - t0 = time.time() - for key, val in model.pointer.items(): - if isinstance(val, Particles): - val.do_sort() - t1 = time.time() - if sim.rank == 0 and verbose: - message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( - run_time_now * 60, - t1 - t0, - ) - print(message, end="\n") - print() - - # update time and index (round time to 10 decimals for a clean time grid!) - time_state["value"][0] = round(time_state["value"][0] + dt, 10) - time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * sim.units.t, 10) - time_state["index"][0] += 1 - - # perform one time step dt - t0 = time.time() - with ProfileManager.profile_region("model.integrate"): - model.integrate(dt, split_algo) - t1 = time.time() - - run_time_now = (time.time() - sim.start_time) / 60 - - # update diagnostics data and save data - if time_state["index"][0] % sim.env.save_step == 0: - # compute scalars and kinetic data - model.update_scalar_quantities() - model.update_markers_to_be_saved() - model.update_distr_functions() - - # extract FEEC coefficients - feec_species = model.field_species | model.fluid_species | model.diagnostic_species - for species, val in feec_species.items(): - assert isinstance(val, Species) - for variable, subval in val.variables.items(): - assert isinstance(subval, FEECVariable) - spline = subval.spline - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - spline.extract_coeffs(update_ghost_regions=False) - - # save data (everything but restart data) - data.save_data(keys=save_keys_all) - - # print current time and scalar quantities to screen - if sim.rank == 0 and verbose: - step = str(time_state["index"][0]).zfill(len(total_steps)) - - message = "time step: " + step + "/" + str(total_steps) - message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) - message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( - time_state["value_sec"][0], - Tend * sim.units.t, - ) - message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( - run_time_now * 60, - t1 - t0, - ) - - print(message, end="\n") - model.print_scalar_quantities() - print() - - # =================================================================== - - sim.meta["wall-clock time[min]"] = (end_simulation - sim.start_time) / 60 - sim.Barrier() - - if sim.rank == 0: - # save meta-data - dict_to_yaml(sim.meta, os.path.join(sim.env.path_out, "meta.yml")) - print("Struphy run finished.") - - if sim.clone_config is not None: - sim.clone_config.free() - - ProfileManager.finalize() - def pproc( path: str, diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py new file mode 100644 index 000000000..b5a91a6ac --- /dev/null +++ b/src/struphy/simulation/base.py @@ -0,0 +1,29 @@ +from abc import ABCMeta, abstractmethod + +class Simulation(metaclass=ABCMeta): + """Abstract base class for simulations.""" + + @abstractmethod + def __init__(self, **kwargs): + """Initialize the simulation.""" + pass + + @abstractmethod + def allocate(self, verbose: bool = False): + """Allocate the simulation variables in memory.""" + pass + + @abstractmethod + def save_geometry_and_equil_vtk(self, verbose: bool = False): + """Save geometry and equilibrium in VTK format.""" + pass + + @abstractmethod + def initialize_data(self, verbose: bool = False): + """Initialize the simulation data storage.""" + pass + + @abstractmethod + def run(self, verbose: bool = False): + """Run the simulation.""" + pass \ No newline at end of file diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index e4b7605c0..221ba600c 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -33,6 +33,7 @@ from struphy.io.output_handling import DataContainer from struphy.pic.base import Particles from struphy.utils.utils import dict_to_yaml +from struphy.simulation.base import Simulation # third party imports from feectools.ddm.mpi import MockMPI @@ -51,7 +52,12 @@ from pyevtk.hl import gridToVTK -class Simulation: +class StruphySimulation(Simulation): + + # ---------------- + # Abstract methods + # ---------------- + def __init__(self, model: StruphyModel, params_path: str = None, @@ -232,7 +238,7 @@ def allocate(self, verbose: bool = False): # pass info to propagators self._allocate_propagators() - def store_geometry(self, verbose: bool = False): + def save_geometry_and_equil_vtk(self, verbose: bool = False): # store geometry vtk if self.rank == 0: grids_log = [ @@ -256,109 +262,6 @@ def store_geometry(self, verbose: bool = False): pointData["absB0"] = absB0 gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) - - def compute_plasma_params(self, verbose=True): - """ - Compute and print volume averaged plasma parameters for each species of the model. - - Global parameters: - - plasma volume - - transit length - - magnetic field - - Species dependent parameters: - - mass - - charge - - density - - pressure - - thermal energy kBT - - Alfvén speed v_A - - thermal speed v_th - - thermal frequency Omega_th - - cyclotron frequency Omega_c - - plasma frequency Omega_p - - Alfvèn frequency Omega_A - - thermal Larmor radius rho_th - - MHD length scale v_a/Omega_c - - rho/L - - alpha = Omega_p/Omega_c - - epsilon = 1/(t*Omega_c) - """ - - # units affices for printing - units_affix = {} - units_affix["plasma volume"] = " m³" - units_affix["transit length"] = " m" - units_affix["magnetic field"] = " T" - units_affix["mass"] = " kg" - units_affix["charge"] = " C" - units_affix["density"] = " m⁻³" - units_affix["pressure"] = " bar" - units_affix["kBT"] = " keV" - units_affix["v_A"] = " m/s" - units_affix["v_th"] = " m/s" - units_affix["vth1"] = " m/s" - units_affix["vth2"] = " m/s" - units_affix["vth3"] = " m/s" - units_affix["Omega_th"] = " Mrad/s" - units_affix["Omega_c"] = " Mrad/s" - units_affix["Omega_p"] = " Mrad/s" - units_affix["Omega_A"] = " Mrad/s" - units_affix["rho_th"] = " m" - units_affix["v_A/Omega_c"] = " m" - units_affix["rho_th/L"] = "" - units_affix["alpha"] = "" - units_affix["epsilon"] = "" - - h = 1 / 20 - eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - - ## global parameters - - # plasma volume (hat x^3) - det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) - vol1 = xp.mean(xp.abs(det_tmp)) - # plasma volume (m⁻³) - plasma_volume = vol1 * self.units.x**3 - # transit length (m) - transit_length = plasma_volume ** (1 / 3) - # magnetic field (T) - if isinstance(self.equil, FluidEquilibriumWithB): - B_tmp = self.equil.absB0(eta1, eta2, eta3) - else: - B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) - magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B - B_max = xp.max(B_tmp) * self.units.B - B_min = xp.min(B_tmp) * self.units.B - - if magnetic_field < 1e-14: - magnetic_field = xp.nan - # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") - - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print("\nPLASMA PARAMETERS:") - print( - "Plasma volume:".ljust(25), - "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], - ) - print( - "Transit length:".ljust(25), - "{:4.3e}".format(transit_length) + units_affix["transit length"], - ) - print( - "Avg. magnetic field:".ljust(25), - "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], - ) - print( - "Max magnetic field:".ljust(25), - "{:4.3e}".format(B_max) + units_affix["magnetic field"], - ) - print( - "Min magnetic field:".ljust(25), - "{:4.3e}".format(B_min) + units_affix["magnetic field"], - ) def initialize_data(self, verbose: bool = False): # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) @@ -384,7 +287,7 @@ def run(self, verbose: bool = False): self.allocate(verbose=self.verbose) # peek view into geometry - self.store_geometry(verbose=self.verbose) + self.save_geometry_and_equil_vtk(verbose=self.verbose) # plasma parameters self.compute_plasma_params(verbose=self.verbose) @@ -539,6 +442,113 @@ def run(self, verbose: bool = False): ProfileManager.finalize() + # --------------------- + # Code specific methods + # --------------------- + + def compute_plasma_params(self, verbose=True): + """ + Compute and print volume averaged plasma parameters for each species of the model. + + Global parameters: + - plasma volume + - transit length + - magnetic field + + Species dependent parameters: + - mass + - charge + - density + - pressure + - thermal energy kBT + - Alfvén speed v_A + - thermal speed v_th + - thermal frequency Omega_th + - cyclotron frequency Omega_c + - plasma frequency Omega_p + - Alfvèn frequency Omega_A + - thermal Larmor radius rho_th + - MHD length scale v_a/Omega_c + - rho/L + - alpha = Omega_p/Omega_c + - epsilon = 1/(t*Omega_c) + """ + + # units affices for printing + units_affix = {} + units_affix["plasma volume"] = " m³" + units_affix["transit length"] = " m" + units_affix["magnetic field"] = " T" + units_affix["mass"] = " kg" + units_affix["charge"] = " C" + units_affix["density"] = " m⁻³" + units_affix["pressure"] = " bar" + units_affix["kBT"] = " keV" + units_affix["v_A"] = " m/s" + units_affix["v_th"] = " m/s" + units_affix["vth1"] = " m/s" + units_affix["vth2"] = " m/s" + units_affix["vth3"] = " m/s" + units_affix["Omega_th"] = " Mrad/s" + units_affix["Omega_c"] = " Mrad/s" + units_affix["Omega_p"] = " Mrad/s" + units_affix["Omega_A"] = " Mrad/s" + units_affix["rho_th"] = " m" + units_affix["v_A/Omega_c"] = " m" + units_affix["rho_th/L"] = "" + units_affix["alpha"] = "" + units_affix["epsilon"] = "" + + h = 1 / 20 + eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + + ## global parameters + + # plasma volume (hat x^3) + det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) + vol1 = xp.mean(xp.abs(det_tmp)) + # plasma volume (m⁻³) + plasma_volume = vol1 * self.units.x**3 + # transit length (m) + transit_length = plasma_volume ** (1 / 3) + # magnetic field (T) + if isinstance(self.equil, FluidEquilibriumWithB): + B_tmp = self.equil.absB0(eta1, eta2, eta3) + else: + B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) + magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B + B_max = xp.max(B_tmp) * self.units.B + B_min = xp.min(B_tmp) * self.units.B + + if magnetic_field < 1e-14: + magnetic_field = xp.nan + # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("\nPLASMA PARAMETERS:") + print( + "Plasma volume:".ljust(25), + "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], + ) + print( + "Transit length:".ljust(25), + "{:4.3e}".format(transit_length) + units_affix["transit length"], + ) + print( + "Avg. magnetic field:".ljust(25), + "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], + ) + print( + "Max magnetic field:".ljust(25), + "{:4.3e}".format(B_max) + units_affix["magnetic field"], + ) + print( + "Min magnetic field:".ljust(25), + "{:4.3e}".format(B_min) + units_affix["magnetic field"], + ) + # --------------- # Private methods # --------------- From 93f9d8464e02d7175cc6e14d7072d938473882d0 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 10:01:36 +0100 Subject: [PATCH 11/80] clean up StruphyModel base class --- src/struphy/main.py | 2 +- src/struphy/models/base.py | 1090 +++-------------- .../post_processing/post_processing_tools.py | 2 +- src/struphy/simulation/base.py | 2 +- src/struphy/simulation/{sim.py => codes.py} | 48 +- 5 files changed, 207 insertions(+), 937 deletions(-) rename src/struphy/simulation/{sim.py => codes.py} (95%) diff --git a/src/struphy/main.py b/src/struphy/main.py index 65fce7bd0..375239927 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -43,7 +43,7 @@ from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml -from struphy.simulation.sim import StruphySimulation +from struphy.simulation.codes import StruphySimulation @profile diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 160ac3582..1237da9f4 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -10,33 +10,24 @@ import yaml from feectools.ddm.mpi import MockMPI from feectools.ddm.mpi import mpi as MPI -from feectools.linalg.stencil import StencilVector from line_profiler import profile from scope_profiler import ProfileManager -from struphy.feec.basis_projection_ops import BasisProjectionOperators from struphy.feec.mass import WeightedMassOperators from struphy.fields_background.base import ( FluidEquilibrium, FluidEquilibriumWithB, - MHDequilibrium, NumericalMHDequilibrium, ) -from struphy.fields_background.projected_equils import ( - ProjectedFluidEquilibrium, - ProjectedFluidEquilibriumWithB, - ProjectedMHDequilibrium, -) + from struphy.geometry.base import Domain -from struphy.io.options import DerhamOptions, LiteralOptions +from struphy.io.options import LiteralOptions from struphy.io.output_handling import DataContainer -from struphy.io.setup import setup_derham from struphy.models.species import DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.physics.physics import Units from struphy.pic.base import Particles from struphy.propagators.base import Propagator -from struphy.topology.grids import TensorProductGrid from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml @@ -51,7 +42,9 @@ class StruphyModel(metaclass=ABCMeta): in one of the modules ``fluid.py``, ``kinetic.py``, ``hybrid.py`` or ``toy.py``. """ - ## abstract methods + # ---------------- + # Abstract methods + # ---------------- @classmethod @abstractmethod @@ -87,9 +80,45 @@ def update_scalar_quantities(self): """Specify an update rule for each item in ``scalar_quantities`` using :meth:`update_scalar`.""" # -------------- - # common methods + # Common methods # -------------- + @classmethod + def name(cls) -> str: + return cls.__name__ + + def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, compute=None, summands=None): + """ + Add a scalar to be saved during the simulation. + + Parameters + ---------- + name : str + Dictionary key for the scalar. + variable : PICVariable | SPHVariable, optional + The variable associated with the scalar. Required if compute is 'from_particles'. + compute : str, optional + Type of scalar, determines the compute operations. + Options: 'from_particles' or 'from_field'. Default is None. + summands : list, optional + List of other scalar names whose values should be summed + to compute the value of this scalar. Default is None. + """ + + assert isinstance(name, str), "name must be a string" + if compute == "from_particles": + assert isinstance(variable, (PICVariable, SPHVariable)), f"Variable is needed when {compute =}" + + if not hasattr(self, "_scalar_quantities"): + self._scalar_quantities = {} + + self._scalar_quantities[name] = { + "value": xp.empty(1, dtype=float), + "variable": variable, + "compute": compute, + "summands": summands, + } + def update_scalar(self, name, value=None): """Update a scalar during the simulation. @@ -181,6 +210,17 @@ def update_scalar(self, name, value=None): value = sum(scalars[summand]["value"][0] for summand in summands) scalars[name]["value"][0] = value + def print_scalar_quantities(self): + """ + Check if scalar_quantities are not "nan" and print to screen. + """ + sq_str = "" + for key, scalar_dict in self._scalar_quantities.items(): + val = scalar_dict["value"] + assert not xp.isnan(val[0]), f"Scalar {key} is {val[0]}." + sq_str += key + ": {:14.11f}".format(val[0]) + " " + print(sq_str) + def setup_equation_params(self, units: Units, verbose=False): """Set euqation parameters for each fluid and kinetic species.""" for _, species in self.fluid_species.items(): @@ -191,334 +231,6 @@ def setup_equation_params(self, units: Units, verbose=False): assert isinstance(species, ParticleSpecies) species.setup_equation_params(units=units, verbose=verbose) - def setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium): - """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" - if equil is not None: - if isinstance(equil, NumericalMHDequilibrium): - self._domain = equil.domain - else: - self._domain = domain - equil.domain = domain - - if hasattr(equil, "units"): - assert isinstance(equil.units, Units) - equil.units.derive_units( - velocity_scale=self.velocity_scale, - A_bulk=self.bulk_species.mass_number, - Z_bulk=self.bulk_species.charge_number, - verbose=self.verbose, - ) - - else: - self._domain = domain - - self._equil = equil - - if MPI.COMM_WORLD.Get_rank() == 0 and self.verbose: - print("\nDOMAIN:") - print("type:".ljust(25), self.domain.__class__.__name__) - for key, val in self.domain.params.items(): - if key not in {"cx", "cy", "cz"}: - print((key + ":").ljust(25), val) - - print("\nFLUID BACKGROUND:") - if self.equil is not None: - print("type:".ljust(25), self.equil.__class__.__name__) - for key, val in self.equil.params.items(): - print((key + ":").ljust(25), val) - else: - print("None.") - - ## species - - @property - def field_species(self) -> dict: - if not hasattr(self, "_field_species"): - self._field_species = {} - for k, v in self.__dict__.items(): - if isinstance(v, FieldSpecies): - self._field_species[k] = v - return self._field_species - - @property - def fluid_species(self) -> dict: - if not hasattr(self, "_fluid_species"): - self._fluid_species = {} - for k, v in self.__dict__.items(): - if isinstance(v, FluidSpecies): - self._fluid_species[k] = v - return self._fluid_species - - @property - def particle_species(self) -> dict: - if not hasattr(self, "_particle_species"): - self._particle_species = {} - for k, v in self.__dict__.items(): - if isinstance(v, ParticleSpecies): - self._particle_species[k] = v - return self._particle_species - - @property - def diagnostic_species(self) -> dict: - if not hasattr(self, "_diagnostic_species"): - self._diagnostic_species = {} - for k, v in self.__dict__.items(): - if isinstance(v, DiagnosticSpecies): - self._diagnostic_species[k] = v - return self._diagnostic_species - - @property - def species(self): - if not hasattr(self, "_species"): - self._species = self.field_species | self.fluid_species | self.particle_species - return self._species - - @staticmethod - def diagnostics_dct(): - """Diagnostics dictionary. - Model specific variables (FemField) which is going to be saved during the simulation. - """ - - ## basic properties - - @property - def params(self): - """Model parameters from :code:`parameters.yml`.""" - return self._params - - @property - def pparams(self): - """Plasma parameters for each species.""" - return self._pparams - - @property - def equation_params(self): - """Parameters appearing in model equation due to Struphy normalization.""" - return self._equation_params - - @property - def clone_config(self): - """Config in case domain clones are used.""" - return self._clone_config - - @clone_config.setter - def clone_config(self, new): - assert isinstance(new, CloneConfig) or new is None - self._clone_config = new - - @property - def domain(self): - """Domain object, see :ref:`avail_mappings`.""" - return self._domain - - @property - def equil(self): - """Fluid equilibrium object, see :ref:`fluid_equil`.""" - return self._equil - - @property - def derham(self): - """3d Derham sequence, see :ref:`derham`.""" - return self._derham - - @property - def projected_equil(self): - """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" - return self._projected_equil - - @property - def units(self) -> Units: - """All Struphy units.""" - return self._units - - @units.setter - def units(self, new): - assert isinstance(new, Units) - self._units = new - - @property - def mass_ops(self) -> WeightedMassOperators: - """WeighteMassOperators object, see :ref:`mass_ops`.""" - return self._mass_ops - - @property - def basis_ops(self): - """Basis projection operators.""" - return self._basis_ops - - @property - def prop_list(self): - """List of Propagator objects.""" - if not hasattr(self, "_prop_list"): - self._prop_list = list(self.propagators.__dict__.values()) - return self._prop_list - - @property - def prop_fields(self): - """Module :mod:`struphy.propagators.propagators_fields`.""" - return self._prop_fields - - @property - def prop_coupling(self): - """Module :mod:`struphy.propagators.propagators_coupling`.""" - return self._prop_coupling - - @property - def prop_markers(self): - """Module :mod:`struphy.propagators.propagators_markers`.""" - return self._prop_markers - - @property - def kwargs(self): - """Dictionary holding the keyword arguments for each propagator specified in :attr:`~propagators_cls`. - Keys must be the same as in :attr:`~propagators_cls`, values are dictionaries holding the keyword arguments.""" - return self._kwargs - - @property - def scalar_quantities(self): - """A dictionary of scalar quantities to be saved during the simulation.""" - if not hasattr(self, "_scalar_quantities"): - self._scalar_quantities = {} - return self._scalar_quantities - - @property - def time_state(self): - """A pointer to the time variable of the dynamics ('t').""" - return self._time_state - - @property - def verbose(self): - """Bool: show model info on screen.""" - try: - return self._verbose - except: - return False - - @verbose.setter - def verbose(self, new): - assert isinstance(new, bool) - self._verbose = new - - @classmethod - def name(cls) -> str: - return cls.__name__ - - @classmethod - def options(cls): - """Dictionary for available species options of the form {'em_fields': {}, 'fluid': {}, 'kinetic': {}}.""" - dct = {} - - for prop, vars in cls.propagators_dct().items(): - var = vars[0] - if var in cls.species()["em_fields"]: - species = "em_fields" - elif var in cls.species()["kinetic"]: - species = ["kinetic", var] - else: - spl = var.split("_") - var_stem = spl[0] - for el in spl[1:-1]: - var_stem += "_" + el - species = ["fluid", var_stem] - - cls.add_option( - species=species, - option=prop, - dct=dct, - ) - - return dct - - @classmethod - def add_option( - cls, - species: str | list, - option, - dct: dict, - *, - key=None, - ): - """Add an option to the dictionary of parameters under [species][options]. - - Test with "struphy params MODEL". - - Parameters - ---------- - species : str or list - path in the dict before the 'options' key - - option : any - value which should be added in the dict - - dct : dict - dictionary to which the value should be added at the corresponding position - - key : str or list - path in the dict after the 'options' key - """ - - def getFromDict(dataDict, mapList): - return reduce(operator.getitem, mapList, dataDict) - - def setInDict(dataDict, mapList, value): - # Loop over dicitionary and creaty empty dicts where the path does not exist - for k in range(len(mapList)): - if mapList[k] not in getFromDict(dataDict, mapList[:k]).keys(): - getFromDict(dataDict, mapList[:k])[mapList[k]] = {} - getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value - - # make sure that the base keys are top-level keys - for base_key in ["em_fields", "fluid", "kinetic"]: - if base_key not in dct.keys(): - dct[base_key] = {} - - if isinstance(species, str): - species = [species] - if isinstance(key, str): - key = [key] - - if inspect.isclass(option): - setInDict( - dct, - species + ["options"] + [option.__name__], - option.options(), - ) - else: - assert key is not None, "Must provide key if option is not a class." - setInDict(dct, species + ["options"] + key, option) - - def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, compute=None, summands=None): - """ - Add a scalar to be saved during the simulation. - - Parameters - ---------- - name : str - Dictionary key for the scalar. - variable : PICVariable | SPHVariable, optional - The variable associated with the scalar. Required if compute is 'from_particles'. - compute : str, optional - Type of scalar, determines the compute operations. - Options: 'from_particles' or 'from_field'. Default is None. - summands : list, optional - List of other scalar names whose values should be summed - to compute the value of this scalar. Default is None. - """ - - assert isinstance(name, str), "name must be a string" - if compute == "from_particles": - assert isinstance(variable, (PICVariable, SPHVariable)), f"Variable is needed when {compute =}" - - if not hasattr(self, "_scalar_quantities"): - self._scalar_quantities = {} - - self._scalar_quantities[name] = { - "value": xp.empty(1, dtype=float), - "variable": variable, - "compute": compute, - "summands": summands, - } - @profile def integrate(self, dt, split_algo="LieTrotter"): """ @@ -648,310 +360,6 @@ def update_distr_functions(self): ) kd_plot.n_sph[:] = n_sph - def print_scalar_quantities(self): - """ - Check if scalar_quantities are not "nan" and print to screen. - """ - sq_str = "" - for key, scalar_dict in self._scalar_quantities.items(): - val = scalar_dict["value"] - assert not xp.isnan(val[0]), f"Scalar {key} is {val[0]}." - sq_str += key + ": {:14.11f}".format(val[0]) + " " - print(sq_str) - - # def initialize_from_params(self): - # """ - # Set initial conditions for FE coefficients (electromagnetic and fluid) - # and markers according to parameter file. - # """ - - # # initialize em fields - # if self.field_species: - # with ProfileManager.profile_region("initialize_em_fields"): - # for key, val in self.em_fields.items(): - # if "params" in key: - # continue - # else: - # obj = val["obj"] - # assert isinstance(obj, SplineFunction) - - # obj.initialize_coeffs( - # domain=self.domain, - # bckgr_obj=self.equil, - # ) - - # if self.rank_world == 0 and self.verbose: - # print(f'\nEM field "{key}" was initialized with:') - - # _params = self.em_fields["params"] - - # if "background" in _params: - # if key in _params["background"]: - # bckgr_types = _params["background"][key] - # if bckgr_types is None: - # pass - # else: - # print("background:") - # for _type, _bp in bckgr_types.items(): - # print(" " * 4 + _type, ":") - # for _pname, _pval in _bp.items(): - # print((" " * 8 + _pname + ":").ljust(25), _pval) - # else: - # print("No background.") - # else: - # print("No background.") - - # if "perturbation" in _params: - # if key in _params["perturbation"]: - # pert_types = _params["perturbation"][key] - # if pert_types is None: - # pass - # else: - # print("perturbation:") - # for _type, _pp in pert_types.items(): - # print(" " * 4 + _type, ":") - # for _pname, _pval in _pp.items(): - # print((" " * 8 + _pname + ":").ljust(25), _pval) - # else: - # print("No perturbation.") - # else: - # print("No perturbation.") - - # if len(self.fluid) > 0: - # with ProfileManager.profile_region("initialize_fluids"): - # for species, val in self.fluid.items(): - # for variable, subval in val.items(): - # if "params" in variable: - # continue - # else: - # obj = subval["obj"] - # assert isinstance(obj, SplineFunction) - # obj.initialize_coeffs( - # domain=self.domain, - # bckgr_obj=self.equil, - # species=species, - # ) - - # if self.rank_world == 0 and self.verbose: - # print( - # f'\nFluid species "{species}" was initialized with:', - # ) - - # _params = val["params"] - - # if "background" in _params: - # for variable in val: - # if "params" in variable: - # continue - # if variable in _params["background"]: - # bckgr_types = _params["background"][variable] - # if bckgr_types is None: - # pass - # else: - # print(f"{variable} background:") - # for _type, _bp in bckgr_types.items(): - # print(" " * 4 + _type, ":") - # for _pname, _pval in _bp.items(): - # print((" " * 8 + _pname + ":").ljust(25), _pval) - # else: - # print(f"{variable}: no background.") - # else: - # print("No background.") - - # if "perturbation" in _params: - # for variable in val: - # if "params" in variable: - # continue - # if variable in _params["perturbation"]: - # pert_types = _params["perturbation"][variable] - # if pert_types is None: - # pass - # else: - # print(f"{variable} perturbation:") - # for _type, _pp in pert_types.items(): - # print(" " * 4 + _type, ":") - # for _pname, _pval in _pp.items(): - # print((" " * 8 + _pname + ":").ljust(25), _pval) - # else: - # print(f"{variable}: no perturbation.") - # else: - # print("No perturbation.") - - # # initialize particles - # if len(self.kinetic) > 0: - # with ProfileManager.profile_region("initialize_particles"): - # for species, val in self.kinetic.items(): - # obj = val["obj"] - # assert isinstance(obj, Particles) - - # if self.rank_world == 0 and self.verbose: - # _params = val["params"] - # assert "background" in _params, "Kinetic species must have background." - - # bckgr_types = _params["background"] - # print( - # f'\nKinetic species "{species}" was initialized with:', - # ) - # for _type, _bp in bckgr_types.items(): - # print(_type, ":") - # for _pname, _pval in _bp.items(): - # print((" " * 4 + _pname + ":").ljust(25), _pval) - - # if "perturbation" in _params: - # for variable, pert_types in _params["perturbation"].items(): - # if pert_types is None: - # pass - # else: - # print(f"{variable} perturbation:") - # for _type, _pp in pert_types.items(): - # print(" " * 4 + _type, ":") - # for _pname, _pval in _pp.items(): - # print((" " * 8 + _pname + ":").ljust(25), _pval) - # else: - # print("No perturbation.") - - # obj.draw_markers(sort=True, verbose=self.verbose) - # obj.mpi_sort_markers(do_test=True) - - # if not val["params"]["markers"]["loading"] == "restart": - # if obj.coords == "vpara_mu": - # obj.save_magnetic_moment() - - # obj.draw_markers(sort=True, verbose=self.verbose) - # if self.comm_world is not None: - # obj.mpi_sort_markers(do_test=True) - - # obj.initialize_weights( - # reject_weights=obj.weights_params["reject_weights"], - # threshold=obj.weights_params["threshold"], - # ) - - def initialize_from_restart(self, data: DataContainer): - """ - Set initial conditions for FE coefficients (electromagnetic and fluid) and markers from restart group in hdf5 files. - - Parameters - ---------- - data : struphy.io.output_handling.DataContainer - The data object that links to the hdf5 files. - """ - - with h5py.File(data.file_path, "a") as file: - for species, val in self.species.items(): - for variable, subval in val.variables.items(): - # initialize feec variables - if isinstance(subval, FEECVariable): - key_restart = os.path.join("restart", species, variable) - subval.spline.initialize_coeffs_from_restart_file( - file, - key=key_restart, - ) - - # initialize pic variables - elif isinstance(subval, PICVariable): - key_restart = os.path.join("restart", species) - subval.particles._markers[:, :] = file[key_restart][-1, :, :] - - if MPI.COMM_WORLD.Get_size() > 1: - subval.particles.mpi_sort_markers(do_test=True) - - ################### - # Class methods : - ################### - - @classmethod - def show_options(cls): - """Print available model options to screen.""" - - print( - 'Options are given under the keyword "options" for each species dict. \ -Available options stand in lists as dict values.\nThe first entry of a list denotes the default value.', - ) - - tab = " " - - print(f'\nAvailable options for model "{cls.__name__}":') - print("\nem_fields:") - if "options" in cls.options()["em_fields"]: - print(tab + "options:") - for opt_k, opt_v in cls.options()["em_fields"]["options"].items(): - if isinstance(opt_v, dict): - print((2 * tab + opt_k + " :").ljust(25)) - for key, val in opt_v.items(): - print((3 * tab + key + " :").ljust(25), val) - else: - print((2 * tab + opt_k + " :").ljust(25), opt_v) - else: - print("None.") - - print("\nfluid:") - if len(cls.species()["fluid"]) > 0: - for spec_name in cls.species()["fluid"]: - print(tab + spec_name + ":") - print(2 * tab + "options:") - if "options" in cls.options()["fluid"][spec_name]: - for opt_k, opt_v in cls.options()["fluid"][spec_name]["options"].items(): - if isinstance(opt_v, dict): - print((3 * tab + opt_k + " :").ljust(25)) - for key, val in opt_v.items(): - print((4 * tab + key + " :").ljust(25), val) - else: - print((3 * tab + opt_k + " :").ljust(25), opt_v) - else: - print("None.") - else: - print("None.") - - print("\nkinetic:") - if len(cls.species()["kinetic"]) > 0: - for spec_name in cls.species()["kinetic"]: - print(tab + spec_name + ":") - print(2 * tab + "options:") - if "options" in cls.options()["kinetic"][spec_name]: - for opt_k, opt_v in cls.options()["kinetic"][spec_name]["options"].items(): - if isinstance(opt_v, dict): - print((3 * tab + opt_k + " :").ljust(25)) - for key, val in opt_v.items(): - print((4 * tab + key + " :").ljust(25), val) - else: - print((3 * tab + opt_k + " :").ljust(25), opt_v) - else: - print("None.") - else: - print("None.") - - @classmethod - def write_parameters_to_file(cls, parameters=None, file=None, save=True, prompt=True): - import os - - import struphy.utils.utils as utils - - # Read struphy state file - state = utils.read_state() - - i_path = state["i_path"] - assert os.path.exists(i_path), f"The path '{i_path}' does not exist. Set path with `struphy --set-i PATH`" - - if file is None: - file = os.path.join(i_path, "params_" + cls.__name__ + ".yml") - else: - assert ".yml" in file or ".yaml" in file, "File must have a a .yml (.yaml) extension." - file = os.path.join(i_path, file) - - if save: - if not prompt: - yn = "Y" - else: - yn = input(f"Writing to {file}, are you sure (Y/n)? ") - - if yn in ("", "Y", "y", "yes", "Yes"): - dict_to_yaml(parameters, file) - print( - f'Default parameter file for {cls.__name__} has been created; you can now launch with "struphy run {cls.__name__}".', - ) - else: - pass - def generate_default_parameter_file( self, path: str = None, @@ -1188,294 +596,118 @@ def generate_default_parameter_file( return path - ################### - # Private methods : - ################### + # ------------- + # Model species + # ------------- - def compute_plasma_params(self, verbose=True): - """ - Compute and print volume averaged plasma parameters for each species of the model. - - Global parameters: - - plasma volume - - transit length - - magnetic field - - Species dependent parameters: - - mass - - charge - - density - - pressure - - thermal energy kBT - - Alfvén speed v_A - - thermal speed v_th - - thermal frequency Omega_th - - cyclotron frequency Omega_c - - plasma frequency Omega_p - - Alfvèn frequency Omega_A - - thermal Larmor radius rho_th - - MHD length scale v_a/Omega_c - - rho/L - - alpha = Omega_p/Omega_c - - epsilon = 1/(t*Omega_c) - """ + @property + def field_species(self) -> dict: + if not hasattr(self, "_field_species"): + self._field_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, FieldSpecies): + self._field_species[k] = v + return self._field_species - # units affices for printing - units_affix = {} - units_affix["plasma volume"] = " m³" - units_affix["transit length"] = " m" - units_affix["magnetic field"] = " T" - units_affix["mass"] = " kg" - units_affix["charge"] = " C" - units_affix["density"] = " m⁻³" - units_affix["pressure"] = " bar" - units_affix["kBT"] = " keV" - units_affix["v_A"] = " m/s" - units_affix["v_th"] = " m/s" - units_affix["vth1"] = " m/s" - units_affix["vth2"] = " m/s" - units_affix["vth3"] = " m/s" - units_affix["Omega_th"] = " Mrad/s" - units_affix["Omega_c"] = " Mrad/s" - units_affix["Omega_p"] = " Mrad/s" - units_affix["Omega_A"] = " Mrad/s" - units_affix["rho_th"] = " m" - units_affix["v_A/Omega_c"] = " m" - units_affix["rho_th/L"] = "" - units_affix["alpha"] = "" - units_affix["epsilon"] = "" - - h = 1 / 20 - eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - - ## global parameters - - # plasma volume (hat x^3) - det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) - vol1 = xp.mean(xp.abs(det_tmp)) - # plasma volume (m⁻³) - plasma_volume = vol1 * self.units.x**3 - # transit length (m) - transit_length = plasma_volume ** (1 / 3) - # magnetic field (T) - if isinstance(self.equil, FluidEquilibriumWithB): - B_tmp = self.equil.absB0(eta1, eta2, eta3) - else: - B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) - magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B - B_max = xp.max(B_tmp) * self.units.B - B_min = xp.min(B_tmp) * self.units.B - - if magnetic_field < 1e-14: - magnetic_field = xp.nan - # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") - - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print("\nPLASMA PARAMETERS:") - print( - "Plasma volume:".ljust(25), - "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], - ) - print( - "Transit length:".ljust(25), - "{:4.3e}".format(transit_length) + units_affix["transit length"], - ) - print( - "Avg. magnetic field:".ljust(25), - "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], - ) - print( - "Max magnetic field:".ljust(25), - "{:4.3e}".format(B_max) + units_affix["magnetic field"], - ) - print( - "Min magnetic field:".ljust(25), - "{:4.3e}".format(B_min) + units_affix["magnetic field"], - ) + @property + def fluid_species(self) -> dict: + if not hasattr(self, "_fluid_species"): + self._fluid_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, FluidSpecies): + self._fluid_species[k] = v + return self._fluid_species - # # species dependent parameters - # self._pparams = {} - - # if len(self.fluid_species) > 0: - # for species, val in self.fluid_species.items(): - # self._pparams[species] = {} - # # type - # self._pparams[species]["type"] = "fluid" - # # mass (kg) - # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p - # # charge (C) - # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e - # # density (m⁻³) - # self._pparams[species]["density"] = ( - # xp.mean( - # self.equil.n0( - # eta1, - # eta2, - # eta3, - # ) - # * xp.abs(det_tmp), - # ) - # * self.units.x ** 3 - # / plasma_volume - # * self.units.n - # ) - # # pressure (bar) - # self._pparams[species]["pressure"] = ( - # xp.mean( - # self.equil.p0( - # eta1, - # eta2, - # eta3, - # ) - # * xp.abs(det_tmp), - # ) - # * self.units.x ** 3 - # / plasma_volume - # * self.units.p - # * 1e-5 - # ) - # # thermal energy (keV) - # self._pparams[species]["kBT"] = self._pparams[species]["pressure"] * 1e5 / self._pparams[species]["density"] / e * 1e-3 - - # if len(self.kinetic) > 0: - # eta1mg, eta2mg, eta3mg = xp.meshgrid( - # eta1, - # eta2, - # eta3, - # indexing="ij", - # ) - - # for species, val in self.kinetic.items(): - # self._pparams[species] = {} - # # type - # self._pparams[species]["type"] = "kinetic" - # # mass (kg) - # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p - # # charge (C) - # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e - - # # create temp kinetic object for (default) parameter extraction - # tmp_bckgr = val["params"]["background"] - - # if val["space"] != "ParticlesSPH": - # tmp = None - # for fi, maxw_params in tmp_bckgr.items(): - # if fi[-2] == "_": - # fi_type = fi[:-2] - # else: - # fi_type = fi - - # if tmp is None: - # tmp = getattr(maxwellians, fi_type)( - # maxw_params=maxw_params, - # equil=self.equil, - # ) - # else: - # tmp = tmp + getattr(maxwellians, fi_type)( - # maxw_params=maxw_params, - # equil=self.equil, - # ) - - # if val["space"] != "ParticlesSPH" and tmp.coords == "constants_of_motion": - # # call parameters - # a1 = self.domain.params_map["a1"] - # r = eta1mg * (1 - a1) + a1 - # psi = self.equil.psi_r(r) - - # # density (m⁻³) - # self._pparams[species]["density"] = ( - # xp.mean(tmp.n(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.n - # ) - # # thermal speed (m/s) - # self._pparams[species]["v_th"] = ( - # xp.mean(tmp.vth(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.v - # ) - # # thermal energy (keV) - # self._pparams[species]["kBT"] = self._pparams[species]["mass"] * self._pparams[species]["v_th"] ** 2 / e * 1e-3 - # # pressure (bar) - # self._pparams[species]["pressure"] = ( - # self._pparams[species]["kBT"] * e * 1e3 * self._pparams[species]["density"] * 1e-5 - # ) - - # else: - # # density (m⁻³) - # # self._pparams[species]['density'] = xp.mean(tmp.n( - # # eta1mg, eta2mg, eta3mg) * xp.abs(det_tmp)) * units['x']**3 / plasma_volume * units['n'] - # self._pparams[species]["density"] = 99.0 - # # thermal speeds (m/s) - # vth = [] - # # vths = tmp.vth(eta1mg, eta2mg, eta3mg) - # vths = [99.0] - # for k in range(len(vths)): - # vth += [ - # vths[k] * xp.abs(det_tmp) * self.units.x ** 3 / plasma_volume * self.units.v, - # ] - # thermal_speed = 0.0 - # for dir in range(val["obj"].vdim): - # # self._pparams[species]['vth' + str(dir + 1)] = xp.mean(vth[dir]) - # self._pparams[species]["vth" + str(dir + 1)] = 99.0 - # thermal_speed += self._pparams[species]["vth" + str(dir + 1)] - # # TODO: here it is assumed that background density parameter is called "n", - # # and that background thermal speeds are called "vthn"; make this a convention? - # # self._pparams[species]['v_th'] = thermal_speed / \ - # # val['obj'].vdim - # self._pparams[species]["v_th"] = 99.0 - # # thermal energy (keV) - # # self._pparams[species]['kBT'] = self._pparams[species]['mass'] * \ - # # self._pparams[species]['v_th']**2 / e * 1e-3 - # self._pparams[species]["kBT"] = 99.0 - # # pressure (bar) - # # self._pparams[species]['pressure'] = self._pparams[species]['kBT'] * \ - # # e * 1e3 * self._pparams[species]['density'] * 1e-5 - # self._pparams[species]["pressure"] = 99.0 - - # for species in self._pparams: - # # alfvén speed (m/s) - # self._pparams[species]["v_A"] = magnetic_field / xp.sqrt( - # mu0 * self._pparams[species]["mass"] * self._pparams[species]["density"], - # ) - # # thermal speed (m/s) - # self._pparams[species]["v_th"] = xp.sqrt( - # self._pparams[species]["kBT"] * 1e3 * e / self._pparams[species]["mass"], - # ) - # # thermal frequency (Mrad/s) - # self._pparams[species]["Omega_th"] = self._pparams[species]["v_th"] / transit_length * 1e-6 - # # cyclotron frequency (Mrad/s) - # self._pparams[species]["Omega_c"] = self._pparams[species]["charge"] * magnetic_field / self._pparams[species]["mass"] * 1e-6 - # # plasma frequency (Mrad/s) - # self._pparams[species]["Omega_p"] = ( - # xp.sqrt( - # self._pparams[species]["density"] * (self._pparams[species]["charge"]) ** 2 / eps0 / self._pparams[species]["mass"], - # ) - # * 1e-6 - # ) - # # alfvén frequency (Mrad/s) - # self._pparams[species]["Omega_A"] = self._pparams[species]["v_A"] / transit_length * 1e-6 - # # Larmor radius (m) - # self._pparams[species]["rho_th"] = self._pparams[species]["v_th"] / (self._pparams[species]["Omega_c"] * 1e6) - # # MHD length scale (m) - # self._pparams[species]["v_A/Omega_c"] = self._pparams[species]["v_A"] / (xp.abs(self._pparams[species]["Omega_c"]) * 1e6) - # # dim-less ratios - # self._pparams[species]["rho_th/L"] = self._pparams[species]["rho_th"] / transit_length - - # if verbose and self.rank_world == 0: - # print("\nSPECIES PARAMETERS:") - # for species, ch in self._pparams.items(): - # print(f"\nname:".ljust(26), species) - # print(f"type:".ljust(25), ch["type"]) - # ch.pop("type") - # print(f"is bulk:".ljust(25), species == self.bulk_species()) - # for kinds, vals in ch.items(): - # print( - # kinds.ljust(25), - # "{:+4.3e}".format( - # vals, - # ), - # units_affix[kinds], - # ) + @property + def particle_species(self) -> dict: + if not hasattr(self, "_particle_species"): + self._particle_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, ParticleSpecies): + self._particle_species[k] = v + return self._particle_species + + @property + def diagnostic_species(self) -> dict: + if not hasattr(self, "_diagnostic_species"): + self._diagnostic_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, DiagnosticSpecies): + self._diagnostic_species[k] = v + return self._diagnostic_species + @property + def species(self): + if not hasattr(self, "_species"): + self._species = self.field_species | self.fluid_species | self.particle_species + return self._species + + # ----------------- + # Common properties + # ----------------- + + @property + def clone_config(self): + """Config in case domain clones are used.""" + return self._clone_config + + @clone_config.setter + def clone_config(self, new): + assert isinstance(new, CloneConfig) or new is None + self._clone_config = new + + @property + def prop_list(self): + """List of Propagator objects.""" + if not hasattr(self, "_prop_list"): + self._prop_list = list(self.propagators.__dict__.values()) + return self._prop_list + + # @property + # def prop_fields(self): + # """Module :mod:`struphy.propagators.propagators_fields`.""" + # return self._prop_fields + + # @property + # def prop_coupling(self): + # """Module :mod:`struphy.propagators.propagators_coupling`.""" + # return self._prop_coupling + + # @property + # def prop_markers(self): + # """Module :mod:`struphy.propagators.propagators_markers`.""" + # return self._prop_markers + + # @property + # def kwargs(self): + # """Dictionary holding the keyword arguments for each propagator specified in :attr:`~propagators_cls`. + # Keys must be the same as in :attr:`~propagators_cls`, values are dictionaries holding the keyword arguments.""" + # return self._kwargs + + @property + def scalar_quantities(self): + """A dictionary of scalar quantities to be saved during the simulation.""" + if not hasattr(self, "_scalar_quantities"): + self._scalar_quantities = {} + return self._scalar_quantities + + # @property + # def time_state(self): + # """A pointer to the time variable of the dynamics ('t').""" + # return self._time_state + + @property + def verbose(self): + """Bool: show model info on screen.""" + try: + return self._verbose + except: + return False + + @verbose.setter + def verbose(self, new): + assert isinstance(new, bool) + self._verbose = new class MyDumper(yaml.SafeDumper): # HACK: insert blank lines between top-level objects diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index dfb418e8c..2d8d1e85d 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -16,7 +16,7 @@ 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, setup_derham +from struphy.models.base import StruphyModel from struphy.models.species import ParticleSpecies from struphy.models.variables import PICVariable from struphy.topology.grids import TensorProductGrid diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index b5a91a6ac..06e0685f4 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -19,7 +19,7 @@ def save_geometry_and_equil_vtk(self, verbose: bool = False): pass @abstractmethod - def initialize_data(self, verbose: bool = False): + def initialize_data_storage(self, verbose: bool = False): """Initialize the simulation data storage.""" pass diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/codes.py similarity index 95% rename from src/struphy/simulation/sim.py rename to src/struphy/simulation/codes.py index 221ba600c..833064c40 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/codes.py @@ -263,7 +263,7 @@ def save_geometry_and_equil_vtk(self, verbose: bool = False): gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) - def initialize_data(self, verbose: bool = False): + def initialize_data_storage(self, verbose: bool = False): # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs self.data = DataContainer(self.env.path_out, comm=self.comm) @@ -286,15 +286,15 @@ def run(self, verbose: bool = False): # equation paramters self.allocate(verbose=self.verbose) + # output + self.initialize_data_storage(verbose=self.verbose) + # peek view into geometry self.save_geometry_and_equil_vtk(verbose=self.verbose) # plasma parameters self.compute_plasma_params(verbose=self.verbose) - # outout - self.initialize_data(verbose=self.verbose) - # print info on mpi procs if self.rank < 32: if self.rank == 0: @@ -311,7 +311,7 @@ def run(self, verbose: bool = False): # set initial conditions for all variables if self.env.restart: - self.model.initialize_from_restart(self.data) + self._initialize_from_restart(self.data) with h5py.File(self.data.file_path, "a") as file: self.time_state["value"][0] = file["restart/time/value"][-1] @@ -1060,6 +1060,44 @@ def _add_time_state(self, time_state): if isinstance(prop, Propagator): prop.add_time_state(time_state) + def _initialize_from_restart(self, data: DataContainer): + """ + Set initial conditions for FE coefficients (electromagnetic and fluid) and markers from restart group in hdf5 files. + + Parameters + ---------- + data : struphy.io.output_handling.DataContainer + The data object that links to the hdf5 files. + """ + with h5py.File(data.file_path, "a") as file: + for species, val in self.model.species.items(): + for variable, subval in val.variables.items(): + # initialize feec variables + if isinstance(subval, FEECVariable): + key_restart = os.path.join("restart", species, variable) + subval.spline.initialize_coeffs_from_restart_file( + file, + key=key_restart, + ) + + # initialize pic variables + elif isinstance(subval, PICVariable): + key_restart = os.path.join("restart", species) + subval.particles._markers[:, :] = file[key_restart][-1, :, :] + + if MPI.COMM_WORLD.Get_size() > 1: + subval.particles.mpi_sort_markers(do_test=True) + + @property + def clone_config(self): + """Config in case domain clones are used.""" + return self._clone_config + + @clone_config.setter + def clone_config(self, new): + assert isinstance(new, CloneConfig) or new is None + self._clone_config = new + @property def domain(self): """Domain object, see :ref:`avail_mappings`.""" From 408fd448c0e19271aea2aaddb2b8384419d45ba7 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 10:07:51 +0100 Subject: [PATCH 12/80] remove unused dependencies in StruphyModel --- src/struphy/models/base.py | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 1237da9f4..dac28e006 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -1,35 +1,20 @@ -import inspect -import operator import os from abc import ABCMeta, abstractmethod -from functools import reduce from textwrap import indent import cunumpy as xp -import h5py -import yaml from feectools.ddm.mpi import MockMPI from feectools.ddm.mpi import mpi as MPI from line_profiler import profile from scope_profiler import ProfileManager -from struphy.feec.mass import WeightedMassOperators -from struphy.fields_background.base import ( - FluidEquilibrium, - FluidEquilibriumWithB, - NumericalMHDequilibrium, -) - -from struphy.geometry.base import Domain from struphy.io.options import LiteralOptions -from struphy.io.output_handling import DataContainer from struphy.models.species import DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.physics.physics import Units from struphy.pic.base import Particles from struphy.propagators.base import Propagator from struphy.utils.clone_config import CloneConfig -from struphy.utils.utils import dict_to_yaml class StruphyModel(metaclass=ABCMeta): @@ -707,16 +692,4 @@ def verbose(self): @verbose.setter def verbose(self, new): assert isinstance(new, bool) - self._verbose = new - -class MyDumper(yaml.SafeDumper): - # HACK: insert blank lines between top-level objects - # inspired by https://stackoverflow.com/a/44284819/3786245 - def write_line_break(self, data=None): - super().write_line_break(data) - - if len(self.indents) == 1: - super().write_line_break() - - def ignore_aliases(self, data): - return True + self._verbose = new \ No newline at end of file From a6fec849854bf97e6305a4f5d0e30c21c1cdb2bc Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 10:52:32 +0100 Subject: [PATCH 13/80] move initial Poisson solve into allocate_helpers; adapt some models --- src/struphy/models/base.py | 2 +- src/struphy/models/cold_plasma.py | 7 ++- src/struphy/models/cold_plasma_vlasov.py | 63 +++++++++---------- .../drift_kinetic_electrostatic_adiabatic.py | 24 +++---- src/struphy/models/guiding_center.py | 3 +- src/struphy/models/hasegawa_wakatani.py | 17 +++-- .../models/linear_extended_mh_duniform.py | 21 ++++--- src/struphy/simulation/codes.py | 4 +- 8 files changed, 68 insertions(+), 73 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index dac28e006..2969ef62b 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -58,7 +58,7 @@ def velocity_scale() -> str: @abstractmethod def allocate_helpers(self): - """Allocate helper arrays that are needed during simulation.""" + """Allocate helper arrays and perform initial solves if needed.""" @abstractmethod def update_scalar_quantities(self): diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index 836699bb2..508051c85 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -7,6 +7,7 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -118,9 +119,9 @@ def update_scalar_quantities(self): b = self.em_fields.b_field.spline.vector j = self.electrons.current.spline.vector - en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) - en_B = 0.5 * self.mass_ops.M2.dot_inner(b, b) - en_J = 0.5 * self._alpha**2 * self.mass_ops.M1ninv.dot_inner(j, j) + en_E = 0.5 * Propagator.mass_ops.M1.dot_inner(e, e) + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) + en_J = 0.5 * self._alpha**2 * Propagator.mass_ops.M1ninv.dot_inner(j, j) self.update_scalar("electric energy", en_E) self.update_scalar("magnetic energy", en_B) diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 1503f15dd..8472d49c4 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -11,6 +11,7 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -151,40 +152,12 @@ def velocity_scale(self): return "light" def allocate_helpers(self): - self._tmp = xp.empty(1, dtype=float) - - def update_scalar_quantities(self): - # e*M1*e/2 - e = self.em_fields.e_field.spline.vector - en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) - self.update_scalar("en_E", en_E) - - # alpha^2 / 2 / N * sum_p w_p v_p^2 - particles = self.hot_elec.var.particles - alpha = self.hot_elec.equation_params.alpha - self._tmp[0] = ( - alpha**2 - / (2 * particles.Np) - * xp.dot( - particles.markers_wo_holes[:, 3] ** 2 - + particles.markers_wo_holes[:, 4] ** 2 - + particles.markers_wo_holes[:, 5] ** 2, - particles.markers_wo_holes[:, 6], - ) - ) - self.update_scalar("en_f", self._tmp[0]) - - # en_tot = en_w + en_e - self.update_scalar("en_tot", en_E + self._tmp[0]) - - def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - - # initialize fields and particles - super().allocate_propagators() + # helper fields + self._tmp = xp.empty(1, dtype=float) if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -202,8 +175,8 @@ def allocate_propagators(self): particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), - self.mass_ops, - self.domain.args_domain, + Propagator.mass_ops, + Propagator.domain.args_domain, ) # another sanity check: compute FE coeffs of density @@ -222,10 +195,34 @@ def allocate_propagators(self): self.initial_poisson(1.0) phi = self.initial_poisson.variables.phi.spline.vector - self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + en_E = 0.5 * Propagator.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) + + # alpha^2 / 2 / N * sum_p w_p v_p^2 + particles = self.hot_elec.var.particles + alpha = self.hot_elec.equation_params.alpha + self._tmp[0] = ( + alpha**2 + / (2 * particles.Np) + * xp.dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + particles.markers_wo_holes[:, 6], + ) + ) + self.update_scalar("en_f", self._tmp[0]) + + # en_tot = en_w + en_e + self.update_scalar("en_tot", en_E + self._tmp[0]) + ## default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index bb406852c..a0e2f32a4 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -12,6 +12,7 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels_gc from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, propagators_markers, @@ -126,19 +127,14 @@ def velocity_scale(self): return "thermal" def allocate_helpers(self): - self._tmp3 = xp.empty(1, dtype=float) - self._e_field = self.derham.Vh["1"].zeros() - - assert self.kinetic_ions.charge_number > 0, "Model written only for positive ions." - - def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ + self._tmp3 = xp.empty(1, dtype=float) + self._e_field = Propagator.derham.Vh["1"].zeros() - # initialize fields and particles - super().allocate_propagators() + assert self.kinetic_ions.charge_number > 0, "Model written only for positive ions." # Poisson right-hand side particles = self.kinetic_ions.var.particles @@ -149,8 +145,8 @@ def allocate_propagators(self): particles, "H1", Pyccelkernel(accum_kernels_gc.gc_density_0form), - self.mass_ops, - self.domain.args_domain, + Propagator.mass_ops, + Propagator.domain.args_domain, ) rho = charge_accum @@ -161,7 +157,7 @@ def allocate_propagators(self): f0e = Z * particles.f0 assert isinstance(f0e, KineticBackground) rho_eh = FEECVariable(space="H1") - rho_eh.allocate(derham=self.derham, domain=self.domain) + rho_eh.allocate(derham=Propagator.derham, domain=Propagator.domain) rho_eh.spline.vector = l2_proj.get_dofs(f0e.n) rho = [rho] rho += [rho_eh] @@ -180,11 +176,11 @@ def update_scalar_quantities(self): epsilon = self.kinetic_ions.equation_params.epsilon # energy from polarization - e1 = self.derham.grad.dot(-phi, out=self._e_field) - en_phi1 = 0.5 * self.mass_ops.M1gyro.dot_inner(e1, e1) + e1 = Propagator.derham.grad.dot(-phi, out=self._e_field) + en_phi1 = 0.5 * Propagator.mass_ops.M1gyro.dot_inner(e1, e1) # energy from adiabatic electrons - en_phi = 0.5 / epsilon**2 * self.mass_ops.M0ad.dot_inner(phi, phi) + en_phi = 0.5 / epsilon**2 * Propagator.mass_ops.M0ad.dot_inner(phi, phi) # for Landau damping test # en_phi = 0. diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index 88ec38839..0ce35cb1a 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -7,6 +7,7 @@ ParticleSpecies, ) from struphy.models.variables import PICVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_markers, ) @@ -123,7 +124,7 @@ def update_scalar_quantities(self): self.update_scalar("en_tot", self._en_tot[0]) self._n_lost_particles[0] = particles.n_lost_markers - self.derham.comm.Allreduce( + Propagator.derham.comm.Allreduce( MPI.IN_PLACE, self._n_lost_particles, op=MPI.SUM, diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index 9eaae2278..7220dd8be 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -8,6 +8,7 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -97,24 +98,17 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): - self._rho: StencilVector = self.derham.Vh["0"].zeros() - self.update_rho() - def update_rho(self): omega = self.plasma.vorticity.spline.vector - self._rho = self.mass_ops.M0.dot(omega, out=self._rho) + self._rho = Propagator.mass_ops.M0.dot(omega, out=self._rho) self._rho.update_ghost_regions() return self._rho - - def allocate_propagators(self): + + def allocate_helpers(self): """Solve initial Poisson equation. :meta private: """ - # initialize fields and particles - super().allocate_propagators() - if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -123,6 +117,9 @@ def allocate_propagators(self): if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") + + self._rho: StencilVector = Propagator.derham.Vh["0"].zeros() + self.update_rho() def update_scalar_quantities(self): pass diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index 4836b20f3..8dcfaf58c 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -9,6 +9,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -123,18 +124,18 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - self._b_eq = self.projected_equil.b1 - self._a_eq = self.projected_equil.a1 - self._p_eq = self.projected_equil.p3 + self._b_eq = Propagator.projected_equil.b1 + self._a_eq = Propagator.projected_equil.a1 + self._p_eq = Propagator.projected_equil.p3 - self._ones = self.projected_equil.p3.space.zeros() + self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_b1: BlockVector = self.derham.Vh["1"].zeros() # TODO: replace derham.Vh dict by class - self._tmp_b2: BlockVector = self.derham.Vh["1"].zeros() + self._tmp_b1: BlockVector = Propagator.derham.Vh["1"].zeros() # TODO: replace derham.Vh dict by class + self._tmp_b2: BlockVector = Propagator.derham.Vh["1"].zeros() # adjust coupling parameters epsilon = self.mhd.equation_params.epsilon @@ -148,8 +149,8 @@ def update_scalar_quantities(self): p = self.mhd.pressure.spline.vector b = self.em_fields.b_field.spline.vector - en_U = 0.5 * self.mass_ops.M2n.dot_inner(u, u) - b1 = self.mass_ops.M1.dot(b, out=self._tmp_b1) + en_U = 0.5 * Propagator.mass_ops.M2n.dot_inner(u, u) + b1 = Propagator.mass_ops.M1.dot(b, out=self._tmp_b1) en_B = 0.5 * b.inner(b1) helicity = 2.0 * self._a_eq.inner(b1) en_p_i = p.inner(self._ones) / (5.0 / 3.0 - 1.0) @@ -161,7 +162,7 @@ def update_scalar_quantities(self): self.update_scalar("en_tot", en_U + en_B + en_p_i) # background fields - b1 = self.mass_ops.M1.dot(self._b_eq, apply_bc=False, out=self._tmp_b1) + b1 = Propagator.mass_ops.M1.dot(self._b_eq, apply_bc=False, out=self._tmp_b1) en_B0 = self._b_eq.inner(b1) / 2.0 en_p0 = self._p_eq.inner(self._ones) / (5.0 / 3.0 - 1.0) @@ -172,7 +173,7 @@ def update_scalar_quantities(self): b1 = self._b_eq.copy(out=self._tmp_b1) self._tmp_b1 += b - b2 = self.mass_ops.M1.dot(b1, apply_bc=False, out=self._tmp_b2) + b2 = Propagator.mass_ops.M1.dot(b1, apply_bc=False, out=self._tmp_b2) en_Btot = b1.inner(b2) / 2.0 self.update_scalar("en_B_tot", en_Btot) diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 833064c40..0cee2702d 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -233,10 +233,12 @@ def allocate(self, verbose: bool = False): # allocate model variables self._allocate_variables(verbose=verbose) - self.model.allocate_helpers() # pass info to propagators self._allocate_propagators() + + # allocate helper fields and perform initial solves if needed + self.model.allocate_helpers() def save_geometry_and_equil_vtk(self, verbose: bool = False): # store geometry vtk From 1b80e8aa89a3fcf7a35ae415f450d469caf114b2 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 11:10:20 +0100 Subject: [PATCH 14/80] use Propagator in models to retrieve derham, mass_ops, etc. --- src/struphy/models/cold_plasma_vlasov.py | 2 +- .../drift_kinetic_electrostatic_adiabatic.py | 2 +- src/struphy/models/linear_mhd.py | 21 ++-- .../models/linear_mhd_driftkinetic_cc.py | 9 +- src/struphy/models/linear_mhd_vlasov_cc.py | 9 +- src/struphy/models/linear_mhd_vlasov_pc.py | 11 ++- .../linear_vlasov_ampere_one_species.py | 96 +++++++++---------- .../linear_vlasov_maxwell_one_species.py | 3 +- src/struphy/models/poisson.py | 16 +--- src/struphy/models/shear_alfven.py | 15 +-- .../models/variational_barotropic_fluid.py | 5 +- .../models/variational_compressible_fluid.py | 5 +- .../models/variational_pressureless_fluid.py | 3 +- .../models/visco_resistive_deltaf_mhd.py | 19 ++-- .../visco_resistive_deltaf_mhd_with_q.py | 15 +-- .../models/visco_resistive_linear_mhd.py | 19 ++-- .../visco_resistive_linear_mhd_with_q.py | 15 +-- src/struphy/models/visco_resistive_mhd.py | 13 +-- .../models/visco_resistive_mhd_with_p.py | 13 +-- .../models/visco_resistive_mhd_with_q.py | 11 ++- src/struphy/models/viscous_euler_sph.py | 6 -- src/struphy/models/viscous_fluid.py | 7 +- .../models/vlasov_ampere_one_species.py | 60 ++++++------ .../models/vlasov_maxwell_one_species.py | 74 +++++++------- 24 files changed, 221 insertions(+), 228 deletions(-) diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 8472d49c4..c1287d0cd 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -180,7 +180,7 @@ def allocate_helpers(self): ) # another sanity check: compute FE coeffs of density - # charge_accum.show_accumulated_spline_field(self.mass_ops) + # charge_accum.show_accumulated_spline_field(Propagator.mass_ops) alpha = self.hot_elec.equation_params.alpha epsilon = self.hot_elec.equation_params.epsilon diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index a0e2f32a4..6223c5691 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -153,7 +153,7 @@ def allocate_helpers(self): # get neutralizing background density if not particles.control_variate: - l2_proj = L2Projector("H1", self.mass_ops) + l2_proj = L2Projector("H1", Propagator.mass_ops) f0e = Z * particles.f0 assert isinstance(f0e, KineticBackground) rho_eh = FEECVariable(space="H1") diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index df2b93479..35037c485 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -9,6 +9,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -110,22 +111,22 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - self._ones = self.projected_equil.p3.space.zeros() + self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_b1: BlockVector = self.derham.Vh["2"].zeros() # TODO: replace derham.Vh dict by class - self._tmp_b2: BlockVector = self.derham.Vh["2"].zeros() + self._tmp_b1: BlockVector = Propagator.derham.Vh["2"].zeros() # TODO: replace derham.Vh dict by class + self._tmp_b2: BlockVector = Propagator.derham.Vh["2"].zeros() def update_scalar_quantities(self): # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner( + en_U = 0.5 * Propagator.mass_ops.M2n.dot_inner( self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector, ) - en_B = 0.5 * self.mass_ops.M2.dot_inner( + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner( self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector, ) @@ -137,19 +138,19 @@ def update_scalar_quantities(self): self.update_scalar("en_tot", en_U + en_B + en_p) # background fields - self.mass_ops.M2.dot(self.projected_equil.b2, apply_bc=False, out=self._tmp_b1) + Propagator.mass_ops.M2.dot(Propagator.projected_equil.b2, apply_bc=False, out=self._tmp_b1) - en_B0 = self.projected_equil.b2.inner(self._tmp_b1) / 2 - en_p0 = self.projected_equil.p3.inner(self._ones) / (5 / 3 - 1) + en_B0 = Propagator.projected_equil.b2.inner(self._tmp_b1) / 2 + en_p0 = Propagator.projected_equil.p3.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_B_eq", en_B0) self.update_scalar("en_p_eq", en_p0) # total magnetic field - self.projected_equil.b2.copy(out=self._tmp_b1) + Propagator.projected_equil.b2.copy(out=self._tmp_b1) self._tmp_b1 += self.em_fields.b_field.spline.vector - self.mass_ops.M2.dot(self._tmp_b1, apply_bc=False, out=self._tmp_b2) + Propagator.mass_ops.M2.dot(self._tmp_b1, apply_bc=False, out=self._tmp_b2) en_Btot = self._tmp_b1.inner(self._tmp_b2) / 2 diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index 03d8021b4..e32ed6cef 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -10,6 +10,7 @@ ) from struphy.models.variables import FEECVariable, PICVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -209,11 +210,11 @@ def update_scalar_quantities(self): Ah = self.energetic_ions.var.species.mass_number # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner( + en_U = 0.5 * Propagator.mass_ops.M2n.dot_inner( self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector, ) - en_B = 0.5 * self.mass_ops.M2.dot_inner( + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner( self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector, ) @@ -253,8 +254,8 @@ def update_scalar_quantities(self): # print number of lost particles n_lost_markers = xp.array(particles.n_lost_markers) - if self.derham.comm is not None: - self.derham.comm.Allreduce( + if Propagator.derham.comm is not None: + Propagator.derham.comm.Allreduce( MPI.IN_PLACE, n_lost_markers, op=MPI.SUM, diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index bcd7f1fa9..1c40894fd 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -10,6 +10,7 @@ ) from struphy.models.variables import FEECVariable, PICVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -156,7 +157,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - self._ones = self.projected_equil.p3.space.zeros() + self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: @@ -167,7 +168,7 @@ def allocate_helpers(self): # add control variate to mass_ops object if self.energetic_ions.var.particles.control_variate: - self.mass_ops.weights["f0"] = self.energetic_ions.var.particles.f0 + Propagator.mass_ops.weights["f0"] = self.energetic_ions.var.particles.f0 self._Ah = self.energetic_ions.mass_number self._Ab = self.mhd.mass_number @@ -179,8 +180,8 @@ def update_scalar_quantities(self): b = self.em_fields.b_field.spline.vector particles = self.energetic_ions.var.particles - en_U = 0.5 * self.mass_ops.M2n.dot_inner(u, u) - en_B = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_U = 0.5 * Propagator.mass_ops.M2n.dot_inner(u, u) + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) en_p = p.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_U", en_U) diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index fc889f65d..ab7f4ac55 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -10,6 +10,7 @@ ) from struphy.models.variables import FEECVariable, PICVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -170,7 +171,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - self._ones = self.projected_equil.p3.space.zeros() + self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: @@ -185,11 +186,11 @@ def update_scalar_quantities(self): Ah = self.energetic_ions.var.species.mass_number # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner( + en_U = 0.5 * Propagator.mass_ops.M2n.dot_inner( self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector, ) - en_B = 0.5 * self.mass_ops.M2.dot_inner( + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner( self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector, ) @@ -219,8 +220,8 @@ def update_scalar_quantities(self): # print number of lost particles n_lost_markers = xp.array(particles.n_lost_markers) - if self.derham.comm is not None: - self.derham.comm.Allreduce( + if Propagator.derham.comm is not None: + Propagator.derham.comm.Allreduce( MPI.IN_PLACE, n_lost_markers, op=MPI.SUM, diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index 1ec9db876..22d0d26ed 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -11,6 +11,7 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -164,56 +165,11 @@ def velocity_scale(self): return "light" def allocate_helpers(self): - self._tmp = xp.empty(1, dtype=float) - - def update_scalar_quantities(self): - # e*M1*e/2 - e = self.em_fields.e_field.spline.vector - particles = self.kinetic_ions.var.particles - - en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) - self.update_scalar("en_E", en_E) - - # evaluate f0 - if not hasattr(self, "_f0"): - backgrounds = self.kinetic_ions.var.backgrounds - if isinstance(backgrounds, list): - self._f0 = backgrounds[0] - else: - self._f0 = backgrounds - self._f0_values = xp.zeros( - self.kinetic_ions.var.particles.markers.shape[0], - dtype=float, - ) - assert isinstance(self._f0, Maxwellian3D) - - self._f0_values[particles.valid_mks] = self._f0(*particles.phasespace_coords.T) - - # alpha^2 * v_th^2 / (2*N) * sum_p s_0 * w_p^2 / f_{0,p} - alpha = self.kinetic_ions.equation_params.alpha - vth = self._f0.maxw_params["vth1"][0] - - self._tmp[0] = ( - alpha**2 - * vth**2 - / (2 * particles.Np) - * xp.dot( - particles.weights**2, # w_p^2 - particles.sampling_density / self._f0_values[particles.valid_mks], # s_{0,p} / f_{0,p} - ) - ) - - self.update_scalar("en_w", self._tmp[0]) - self.update_scalar("en_tot", self._tmp[0] + en_E) - - def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - - # initialize fields and particles - super().allocate_propagators() + self._tmp = xp.empty(1, dtype=float) if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -231,12 +187,12 @@ def allocate_propagators(self): particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), - self.mass_ops, - self.domain.args_domain, + Propagator.mass_ops, + Propagator.domain.args_domain, ) # another sanity check: compute FE coeffs of density - # charge_accum.show_accumulated_spline_field(self.mass_ops) + # charge_accum.show_accumulated_spline_field(Propagator.mass_ops) alpha = self.kinetic_ions.equation_params.alpha epsilon = self.kinetic_ions.equation_params.epsilon @@ -251,10 +207,50 @@ def allocate_propagators(self): self.initial_poisson(1.0) phi = self.initial_poisson.variables.phi.spline.vector - self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + particles = self.kinetic_ions.var.particles + + en_E = 0.5 * Propagator.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) + + # evaluate f0 + if not hasattr(self, "_f0"): + backgrounds = self.kinetic_ions.var.backgrounds + if isinstance(backgrounds, list): + self._f0 = backgrounds[0] + else: + self._f0 = backgrounds + self._f0_values = xp.zeros( + self.kinetic_ions.var.particles.markers.shape[0], + dtype=float, + ) + assert isinstance(self._f0, Maxwellian3D) + + self._f0_values[particles.valid_mks] = self._f0(*particles.phasespace_coords.T) + + # alpha^2 * v_th^2 / (2*N) * sum_p s_0 * w_p^2 / f_{0,p} + alpha = self.kinetic_ions.equation_params.alpha + vth = self._f0.maxw_params["vth1"][0] + + self._tmp[0] = ( + alpha**2 + * vth**2 + / (2 * particles.Np) + * xp.dot( + particles.weights**2, # w_p^2 + particles.sampling_density / self._f0_values[particles.valid_mks], # s_{0,p} / f_{0,p} + ) + ) + + self.update_scalar("en_w", self._tmp[0]) + self.update_scalar("en_tot", self._tmp[0] + en_E) + ## default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index 530c16385..9cb4898ff 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -7,6 +7,7 @@ ParticleSpecies, ) from struphy.models.variables import FEECVariable, PICVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -164,5 +165,5 @@ def update_scalar_quantities(self): # 0.5 * b^T * M_2 * b b = self.em_fields.b_field.spline.vector - en_B = 0.5 * self._mass_ops.M2.dot_inner(b, b) + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_tot", self.scalar_quantities["en_tot"]["value"][0] + en_B) diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index d50e91440..109f79235 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -6,6 +6,7 @@ FieldSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -85,22 +86,12 @@ def velocity_scale(self): return None def allocate_helpers(self): - pass - - def update_scalar_quantities(self): - pass - - def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - - # initialize fields and particles - super().allocate_propagators() - # # use setter to assign source - # self.propagators.poisson.rho = self.mass_ops.M0.dot(self.em_fields.source.spline.vector) + # self.propagators.poisson.rho = Propagator.mass_ops.M0.dot(self.em_fields.source.spline.vector) # Solve with dt=1. and compute electric field if MPI.COMM_WORLD.Get_rank() == 0: @@ -111,6 +102,9 @@ def allocate_propagators(self): if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") + def update_scalar_quantities(self): + pass + # default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index ee27383eb..e24fb1916 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -7,6 +7,7 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -69,7 +70,7 @@ def velocity_scale(self): def allocate_helpers(self): # project background magnetic field (2-form) and pressure (3-form) - self._b_eq = self.derham.P["2"]( + self._b_eq = Propagator.derham.P["2"]( [ self.equil.b2_1, self.equil.b2_2, @@ -78,8 +79,8 @@ def allocate_helpers(self): ) # temporary vectors for scalar quantities - self._tmp_b1 = self.derham.Vh["2"].zeros() - self._tmp_b2 = self.derham.Vh["2"].zeros() + self._tmp_b1 = Propagator.derham.Vh["2"].zeros() + self._tmp_b2 = Propagator.derham.Vh["2"].zeros() def __init__(self): if rank == 0: @@ -107,8 +108,8 @@ def __init__(self): def update_scalar_quantities(self): # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector) - en_B = 0.5 * self.mass_ops.M2.dot_inner( + en_U = 0.5 * Propagator.mass_ops.M2n.dot_inner(self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector) + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner( self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector, ) @@ -118,7 +119,7 @@ def update_scalar_quantities(self): self.update_scalar("en_tot", en_U + en_B) # background fields - self.mass_ops.M2.dot(self._b_eq, apply_bc=False, out=self._tmp_b1) + Propagator.mass_ops.M2.dot(self._b_eq, apply_bc=False, out=self._tmp_b1) en_B0 = self._b_eq.inner(self._tmp_b1) / 2 self.update_scalar("en_B_eq", en_B0) @@ -126,7 +127,7 @@ def update_scalar_quantities(self): self._b_eq.copy(out=self._tmp_b1) self._tmp_b1 += self.em_fields.b_field.spline.vector - self.mass_ops.M2.dot(self._tmp_b1, apply_bc=False, out=self._tmp_b2) + Propagator.mass_ops.M2.dot(self._tmp_b1, apply_bc=False, out=self._tmp_b2) en_Btot = self._tmp_b1.inner(self._tmp_b2) / 2 self.update_scalar("en_B_tot", en_Btot) diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index 8105caa0c..b9dfd5ab9 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -6,6 +6,7 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -96,10 +97,10 @@ def update_scalar_quantities(self): rho = self.fluid.density.spline.vector u = self.fluid.velocity.spline.vector - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_thermo = 0.5 * self.mass_ops.M3.dot_inner(rho, rho) + en_thermo = 0.5 * Propagator.mass_ops.M3.dot_inner(rho, rho) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 5d45e0b81..4e6fccb10 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -11,6 +11,7 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -110,13 +111,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._energy_evaluator = InternalEnergyEvaluator(self.derham, self.propagators.variat_ent.options.gamma) + self._energy_evaluator = InternalEnergyEvaluator(Propagator.derham, self.propagators.variat_ent.options.gamma) def update_scalar_quantities(self): rho = self.fluid.density.spline.vector u = self.fluid.velocity.spline.vector - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) en_thermo = self.update_thermo_energy() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index 74b46b5b4..92318fa51 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -6,6 +6,7 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -90,7 +91,7 @@ def allocate_helpers(self): def update_scalar_quantities(self): u = self.fluid.velocity.spline.vector - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) # default parameters diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index ff90840ce..0dae56539 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -11,6 +11,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -157,13 +158,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -175,16 +176,16 @@ def update_scalar_quantities(self): gamma = self.propagators.variat_pb.options.gamma - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag1 = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) + en_mag2 = Propagator.mass_ops.M2.dot_inner(bt2, Propagator.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_thermo = self.mass_ops.M3.dot_inner(pt3, self._integrator) / (gamma - 1.0) + en_thermo = Propagator.mass_ops.M3.dot_inner(pt3, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag1 + en_mag2 @@ -193,14 +194,14 @@ def update_scalar_quantities(self): # dens_tot = self._ones.inner(rho) # self.update_scalar("dens_tot", dens_tot) - # div_B = self.derham.div.dot(b, out=self._tmp_div_B) + # div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) # self.update_scalar("tot_div_B", L2_div_B) - en_thermo_l1 = self.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) + en_thermo_l1 = Propagator.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo_l1", en_thermo_l1) - en_mag_l1 = self.mass_ops.M2.dot_inner(b, self.projected_equil.b2) + en_mag_l1 = Propagator.mass_ops.M2.dot_inner(b, Propagator.projected_equil.b2) self.update_scalar("en_mag_l1", en_mag_l1) en_tot_l1 = en_thermo_l1 + en_mag_l1 diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index 6fe0d3334..1e80ec363 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -11,6 +11,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -154,13 +155,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -172,19 +173,19 @@ def update_scalar_quantities(self): gamma = self.propagators.variat_qb.options.gamma - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag1 = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) + en_mag2 = Propagator.mass_ops.M2.dot_inner(bt2, Propagator.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_th_1 = 1.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(q, q) + en_th_1 = 1.0 / (gamma - 1.0) * Propagator.mass_ops.M3.dot_inner(q, q) self.update_scalar("en_thermo_1", en_th_1) - en_th_2 = 2.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(qt3, self.projected_equil.q3) + en_th_2 = 2.0 / (gamma - 1.0) * Propagator.mass_ops.M3.dot_inner(qt3, Propagator.projected_equil.q3) self.update_scalar("en_thermo_2", en_th_2) en_tot = en_U + en_th_1 + en_th_2 + en_mag1 + en_mag2 diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index 8047dc8ae..890c6f28f 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -11,6 +11,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -154,13 +155,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -172,16 +173,16 @@ def update_scalar_quantities(self): gamma = self.propagators.variat_pb.options.gamma - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag1 = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) + en_mag2 = Propagator.mass_ops.M2.dot_inner(bt2, Propagator.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_thermo = self.mass_ops.M3.dot_inner(pt3, self._integrator) / (gamma - 1.0) + en_thermo = Propagator.mass_ops.M3.dot_inner(pt3, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag1 + en_mag2 @@ -190,14 +191,14 @@ def update_scalar_quantities(self): # dens_tot = self._ones.inner(rho) # self.update_scalar("dens_tot", dens_tot) - # div_B = self.derham.div.dot(b, out=self._tmp_div_B) + # div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) # self.update_scalar("tot_div_B", L2_div_B) - en_thermo_l1 = self.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) + en_thermo_l1 = Propagator.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo_l1", en_thermo_l1) - en_mag_l1 = self.mass_ops.M2.dot_inner(b, self.projected_equil.b2) + en_mag_l1 = Propagator.mass_ops.M2.dot_inner(b, Propagator.projected_equil.b2) self.update_scalar("en_mag_l1", en_mag_l1) en_tot_l1 = en_thermo_l1 + en_mag_l1 diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index e4f4df364..ef07f4bd4 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -11,6 +11,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -151,13 +152,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -169,19 +170,19 @@ def update_scalar_quantities(self): gamma = self.propagators.variat_qb.options.gamma - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag1 = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) + en_mag2 = Propagator.mass_ops.M2.dot_inner(bt2, Propagator.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_th_1 = 1.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(q, q) + en_th_1 = 1.0 / (gamma - 1.0) * Propagator.mass_ops.M3.dot_inner(q, q) self.update_scalar("en_thermo_1", en_th_1) - en_th_2 = 2.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(qt3, self.projected_equil.q3) + en_th_2 = 2.0 / (gamma - 1.0) * Propagator.mass_ops.M3.dot_inner(qt3, Propagator.projected_equil.q3) self.update_scalar("en_thermo_2", en_th_2) en_tot = en_U + en_th_1 + en_th_2 + en_mag1 + en_mag2 diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index d4839a191..7126b0ad7 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -13,6 +13,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -152,15 +153,15 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._energy_evaluator = InternalEnergyEvaluator(self.derham, self.propagators.variat_ent.options.gamma) + self._energy_evaluator = InternalEnergyEvaluator(Propagator.derham, self.propagators.variat_ent.options.gamma) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -168,10 +169,10 @@ def update_scalar_quantities(self): s = self.mhd.entropy.spline.vector b = self.em_fields.b_field.spline.vector - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) en_thermo = self.update_thermo_energy() @@ -184,7 +185,7 @@ def update_scalar_quantities(self): entr_tot = self._ones.inner(s) self.update_scalar("entr_tot", entr_tot) - div_B = self.derham.div.dot(b, out=self._tmp_div_B) + div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index ea5b3cbc5..5380d0acc 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -11,6 +11,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -152,13 +153,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -168,13 +169,13 @@ def update_scalar_quantities(self): gamma = self.propagators.variat_pb.options.gamma - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) - en_thermo = self.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) + en_thermo = Propagator.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag @@ -183,7 +184,7 @@ def update_scalar_quantities(self): dens_tot = self._ones.inner(rho) self.update_scalar("dens_tot", dens_tot) - div_B = self.derham.div.dot(b, out=self._tmp_div_B) + div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 30fe30edf..4f98f2ac8 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -11,6 +11,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -154,13 +155,13 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + self._tmp_div_B = Propagator.derham.Vh_pol["3"].zeros() def update_scalar_quantities(self): rho = self.mhd.density.spline.vector @@ -170,10 +171,10 @@ def update_scalar_quantities(self): gamma = self.propagators.variat_qb.options.gamma - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_mag = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) en_thermo = 1.0 / (gamma - 1.0) * self._mass_ops.M3.dot_inner(q, q) @@ -185,7 +186,7 @@ def update_scalar_quantities(self): dens_tot = self._ones.inner(rho) self.update_scalar("dens_tot", dens_tot) - div_B = self.derham.div.dot(b, out=self._tmp_div_B) + div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index e99280c9d..1dc3a8e89 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -106,12 +106,6 @@ def velocity_scale(self): def allocate_helpers(self): pass - # @staticmethod - # def diagnostics_dct(): - # dct = {} - # dct["projected_density"] = "L2" - # return dct - def update_scalar_quantities(self): particles = self.euler_fluid.var.particles valid_markers = particles.markers_wo_holes_and_ghost diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index 0310b9fad..d2d38e208 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -12,6 +12,7 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) @@ -120,9 +121,9 @@ def f(e1, e2, e3): f = xp.vectorize(f) self._integrator = projV3(f) - self._energy_evaluator = InternalEnergyEvaluator(self.derham, self.propagators.variat_ent.options.gamma) + self._energy_evaluator = InternalEnergyEvaluator(Propagator.derham, self.propagators.variat_ent.options.gamma) - self._ones = self.derham.Vh_pol["3"].zeros() + self._ones = Propagator.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: @@ -133,7 +134,7 @@ def update_scalar_quantities(self): u = self.fluid.velocity.spline.vector s = self.fluid.entropy.spline.vector - en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) + en_U = 0.5 * Propagator.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) en_thermo = self.update_thermo_energy() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index ffb785c8f..3d0a1b6ec 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -10,6 +10,7 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -155,40 +156,11 @@ def velocity_scale(self): return "light" def allocate_helpers(self): - self._tmp = xp.empty(1, dtype=float) - - def update_scalar_quantities(self): - # e*M1*e/2 - e = self.em_fields.e_field.spline.vector - en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) - self.update_scalar("en_E", en_E) - - # alpha^2 / 2 / N * sum_p w_p v_p^2 - particles = self.kinetic_ions.var.particles - alpha = self.kinetic_ions.equation_params.alpha - self._tmp[0] = ( - alpha**2 - / (2 * particles.Np) - * xp.dot( - particles.markers_wo_holes[:, 3] ** 2 - + particles.markers_wo_holes[:, 4] ** 2 - + particles.markers_wo_holes[:, 5] ** 2, - particles.markers_wo_holes[:, 6], - ) - ) - self.update_scalar("en_f", self._tmp[0]) - - # en_tot = en_w + en_e - self.update_scalar("en_tot", en_E + self._tmp[0]) - - def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - - # initialize fields and particles - super().allocate_propagators() + self._tmp = xp.empty(1, dtype=float) if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -207,7 +179,7 @@ def allocate_propagators(self): "H1", Pyccelkernel(accum_kernels.charge_density_0form), self.mass_ops, - self.domain.args_domain, + Propagator.domain.args_domain, ) # another sanity check: compute FE coeffs of density @@ -226,10 +198,34 @@ def allocate_propagators(self): self.initial_poisson(1.0) phi = self.initial_poisson.variables.phi.spline.vector - self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) + + # alpha^2 / 2 / N * sum_p w_p v_p^2 + particles = self.kinetic_ions.var.particles + alpha = self.kinetic_ions.equation_params.alpha + self._tmp[0] = ( + alpha**2 + / (2 * particles.Np) + * xp.dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + particles.markers_wo_holes[:, 6], + ) + ) + self.update_scalar("en_f", self._tmp[0]) + + # en_tot = en_w + en_e + self.update_scalar("en_tot", en_E + self._tmp[0]) + ## default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 7b9b2d7f2..8b1802291 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -10,6 +10,7 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, @@ -166,45 +167,11 @@ def velocity_scale(self): return "light" def allocate_helpers(self): - self._tmp = xp.empty(1, dtype=float) - - def update_scalar_quantities(self): - # e*M1*e/2 - e = self.em_fields.e_field.spline.vector - b = self.em_fields.b_field.spline.vector - - en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) - self.update_scalar("en_E", en_E) - - en_B = 0.5 * self.mass_ops.M2.dot_inner(b, b) - self.update_scalar("en_B", en_B) - - # alpha^2 / 2 / N * sum_p w_p v_p^2 - particles = self.kinetic_ions.var.particles - alpha = self.kinetic_ions.equation_params.alpha - self._tmp[0] = ( - alpha**2 - / (2 * particles.Np) - * xp.dot( - particles.markers_wo_holes[:, 3] ** 2 - + particles.markers_wo_holes[:, 4] ** 2 - + particles.markers_wo_holes[:, 5] ** 2, - particles.markers_wo_holes[:, 6], - ) - ) - self.update_scalar("en_f", self._tmp[0]) - - # en_tot = en_w + en_e - self.update_scalar("en_tot", en_E + self._tmp[0]) - - def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - - # initialize fields and particles - super().allocate_propagators() + self._tmp = xp.empty(1, dtype=float) if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -222,12 +189,12 @@ def allocate_propagators(self): particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), - self.mass_ops, - self.domain.args_domain, + Propagator.mass_ops, + Propagator.domain.args_domain, ) # another sanity check: compute FE coeffs of density - # charge_accum.show_accumulated_spline_field(self.mass_ops) + # charge_accum.show_accumulated_spline_field(Propagator.mass_ops) alpha = self.kinetic_ions.equation_params.alpha epsilon = self.kinetic_ions.equation_params.epsilon @@ -242,10 +209,39 @@ def allocate_propagators(self): self.initial_poisson(1.0) phi = self.initial_poisson.variables.phi.spline.vector - self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + b = self.em_fields.b_field.spline.vector + + en_E = 0.5 * Propagator.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) + + en_B = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) + self.update_scalar("en_B", en_B) + + # alpha^2 / 2 / N * sum_p w_p v_p^2 + particles = self.kinetic_ions.var.particles + alpha = self.kinetic_ions.equation_params.alpha + self._tmp[0] = ( + alpha**2 + / (2 * particles.Np) + * xp.dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + particles.markers_wo_holes[:, 6], + ) + ) + self.update_scalar("en_f", self._tmp[0]) + + # en_tot = en_w + en_e + self.update_scalar("en_tot", en_E + self._tmp[0]) + ## default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) From fcbb2c32d695b1c08cb15af8e2fafdc7e33ea09b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 11 Feb 2026 13:11:22 +0100 Subject: [PATCH 15/80] new method StruphyImulation.pproc(); move the pproc logic to the sim class; it now orks for fields; restart logic becomes simpler too. --- src/struphy/main.py | 213 ----- src/struphy/models/tests/utils_testing.py | 37 +- src/struphy/simulation/base.py | 5 + src/struphy/simulation/codes.py | 1048 +++++++++++++++++++-- 4 files changed, 990 insertions(+), 313 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index 375239927..203eb5d05 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -87,219 +87,6 @@ def run( sim.run(verbose=verbose) -def pproc( - path: str, - *, - step: int = 1, - celldivide: int = 1, - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - no_vtk: bool = False, - time_trace: bool = False, -): - """Post-processing finished Struphy runs. - - Parameters - ---------- - path : str - Absolute path of simulation output folder to post-process. - - step : int - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - celldivide : int - Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. - - physical : bool - Wether to do post-processing into push-forwarded physical (xyz) components of fields. - - guiding_center : bool - Compute guiding-center coordinates (only from Particles6D). - - classify : bool - Classify guiding-center trajectories (passing, trapped or lost). - - no_vtk : bool - whether vtk files creation should be skipped - - time_trace : bool - whether to plot the time trace of each measured region - """ - - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n*** Start post-processing of {path}:") - - # import parameters - params_in = get_params_of_run(path) - model = params_in.model - domain = params_in.domain - - # create post-processing folder - path_pproc = os.path.join(path, "post_processing") - - try: - os.mkdir(path_pproc) - except: - shutil.rmtree(path_pproc) - os.mkdir(path_pproc) - - if time_trace: - from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart, plot_time_vs_duration - - path_time_trace = os.path.join(path, "profiling_time_trace.pkl") - plot_time_vs_duration(path_time_trace, output_path=path_pproc) - plot_gantt_chart(path_time_trace, output_path=path_pproc) - return - - # check for fields and kinetic data in hdf5 file that need post processing - with h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") as file: - # save time grid at which post-processing data is created - xp.save(os.path.join(path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) - - if "feec" in file.keys(): - exist_fields = True - else: - exist_fields = False - - if "kinetic" in file.keys(): - exist_kinetic = {"markers": False, "f": False, "n_sph": False} - kinetic_species = [] - kinetic_kinds = [] - for name in file["kinetic"].keys(): - kinetic_species += [name] - kinetic_kinds += [next(iter(model.species[name].variables.values())).space] - - # check for saved markers - if "markers" in file["kinetic"][name]: - exist_kinetic["markers"] = True - # check for saved distribution function - if "f" in file["kinetic"][name]: - exist_kinetic["f"] = True - # check for saved sph density - if "n_sph" in file["kinetic"][name]: - exist_kinetic["n_sph"] = True - else: - exist_kinetic = None - - # field post-processing - if exist_fields: - fields, t_grid = create_femfields(path, params_in=params_in, step=step) - - point_data, grids_log, grids_phy = eval_femfields(params_in, fields, celldivide=[celldivide] * 3) - - if physical: - point_data_phy, grids_log, grids_phy = eval_femfields( - params_in, - fields, - celldivide=[celldivide] * 3, - physical=True, - ) - - # directory for field data - path_fields = os.path.join(path_pproc, "fields_data") - - try: - os.mkdir(path_fields) - except: - shutil.rmtree(path_fields) - os.mkdir(path_fields) - - # save data dicts for each field - for species, vars in point_data.items(): - 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) - - 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) - - # create vtk files - if not no_vtk: - create_vtk(path_fields, t_grid, grids_phy, point_data) - if physical: - create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) - - # kinetic post-processing - if exist_kinetic is not None: - # directory for kinetic data - path_kinetics = os.path.join(path_pproc, "kinetic_data") - - try: - os.mkdir(path_kinetics) - except: - shutil.rmtree(path_kinetics) - os.mkdir(path_kinetics) - - # kinetic post-processing for each species - for n, species in enumerate(kinetic_species): - # directory for each species - path_kinetics_species = os.path.join(path_kinetics, species) - - try: - os.mkdir(path_kinetics_species) - except: - shutil.rmtree(path_kinetics_species) - os.mkdir(path_kinetics_species) - - # markers - if exist_kinetic["markers"]: - post_process_markers( - path, - path_kinetics_species, - species, - domain, - kinetic_kinds[n], - step, - ) - - if guiding_center: - assert kinetic_kinds[n] == "Particles6D" - orbits_tools.post_process_orbit_guiding_center(path, path_kinetics_species, species) - - if classify: - orbits_tools.post_process_orbit_classification(path_kinetics_species, species) - - # distribution function - if exist_kinetic["f"]: - if kinetic_kinds[n] == "DeltaFParticles6D": - compute_bckgr = True - else: - compute_bckgr = False - - post_process_f( - path, - params_in, - path_kinetics_species, - species, - step, - compute_bckgr=compute_bckgr, - ) - - # sph density - if exist_kinetic["n_sph"]: - post_process_n_sph( - path, - params_in, - path_kinetics_species, - species, - step, - ) - - class SimData: """Holds post-processed Struphy data as attributes. diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index c4444c58b..9279294c9 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -10,6 +10,7 @@ from struphy import EnvironmentOptions, main from struphy.io.setup import import_parameters_py from struphy.models.base import StruphyModel +from struphy.simulation.codes import StruphySimulation rank = MPI.COMM_WORLD.Get_rank() @@ -55,8 +56,8 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = model = params_in.model # test - main.run( - model, + sim = StruphySimulation( + model=model, params_path=path, env=env, base_units=base_units, @@ -68,36 +69,18 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = verbose=verbose, ) - # Restart and run one more timestep - params_in = import_parameters_py(path) - base_units = params_in.base_units - 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 - env.restart = True - time_opts.Tend += time_opts.dt + sim.run(verbose=verbose) # test restart - main.run( - model, - params_path=path, - env=env, - base_units=base_units, - time_opts=time_opts, - domain=domain, - equil=equil, - grid=grid, - derham_opts=derham_opts, - verbose=verbose, - ) + env.restart = True + time_opts.Tend += time_opts.dt + + sim.run(verbose=verbose) MPI.COMM_WORLD.Barrier() if rank == 0: path_out = os.path.join(test_folder, model_name) - main.pproc(path=path_out) - main.load_data(path=path_out) + sim.pproc(verbose=verbose) + # main.load_data(path=path_out) shutil.rmtree(test_folder) MPI.COMM_WORLD.Barrier() diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index 06e0685f4..4c85d61dd 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -26,4 +26,9 @@ def initialize_data_storage(self, verbose: bool = False): @abstractmethod def run(self, verbose: bool = False): """Run the simulation.""" + pass + + @abstractmethod + def pproc(self, verbose: bool = False): + """Post-process the simulation results.""" pass \ No newline at end of file diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 0cee2702d..0f391791f 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -1,9 +1,9 @@ # api imports -from struphy import (EnvironmentOptions, - BaseUnits, - Time, - domains, - equils, +from struphy import (EnvironmentOptions, + BaseUnits, + Time, + domains, + equils, grids, DerhamOptions, ) @@ -34,6 +34,7 @@ from struphy.pic.base import Particles from struphy.utils.utils import dict_to_yaml from struphy.simulation.base import Simulation +from struphy.feec.psydac_derham import SplineFunction # third party imports from feectools.ddm.mpi import MockMPI @@ -48,38 +49,40 @@ import cunumpy as xp import h5py import glob +import yaml +from tqdm import tqdm from line_profiler import profile from pyevtk.hl import gridToVTK class StruphySimulation(Simulation): - + # ---------------- # Abstract methods # ---------------- - - def __init__(self, - model: StruphyModel, - params_path: str = None, - env: EnvironmentOptions = EnvironmentOptions(), - base_units: BaseUnits = BaseUnits(), - time_opts: Time = Time(), - domain: Domain = domains.Cuboid(), - equil: FluidEquilibrium = equils.HomogenSlab(), - grid: grids.TensorProductGrid = None, - derham_opts: DerhamOptions = None, - verbose: bool = False, - ): - + + def __init__(self, + model: StruphyModel, + params_path: str = None, + env: EnvironmentOptions = EnvironmentOptions(), + base_units: BaseUnits = BaseUnits(), + time_opts: Time = Time(), + domain: Domain = domains.Cuboid(), + equil: FluidEquilibrium = equils.HomogenSlab(), + grid: grids.TensorProductGrid = None, + derham_opts: DerhamOptions = None, + verbose: bool = False, + ): + self.model = model self.params_path = params_path self.env = env self.base_units = base_units self.time_opts = time_opts self.grid = grid - self.derham_opts = derham_opts + self.derham_opts = derham_opts self.verbose = verbose - + # setup profiling agent ProfileManager.setup( profiling_activated=env.profiling_activated, @@ -96,12 +99,12 @@ def __init__(self, if isinstance(MPI, MockMPI): self.comm = None self.rank = 0 - self.size = 1 + self.comm_size = 1 self.Barrier = lambda: None else: self.comm = MPI.COMM_WORLD self.rank = self.comm.Get_rank() - self.size = self.comm.Get_size() + self.comm_size = self.comm.Get_size() self.Barrier = self.comm.Barrier if self.rank == 0: @@ -134,7 +137,7 @@ def __init__(self, self.meta["model name"] = self.model_name self.meta["parameter file"] = params_path self.meta["output folder"] = path_out - self.meta["MPI processes"] = self.size + self.meta["MPI processes"] = self.comm_size self.meta["use MPI.COMM_WORLD"] = use_mpi self.meta["number of domain clones"] = num_clones self.meta["restart"] = restart @@ -206,7 +209,7 @@ def __init__(self, self.clone_config = model.clone_config = clone_config self.Barrier() - + # units and normalization parameters units = Units(base_units) self.units = units @@ -230,13 +233,13 @@ def __init__(self, def allocate(self, verbose: bool = False): # feec self._allocate_feec(self.grid, self.derham_opts) - + # allocate model variables self._allocate_variables(verbose=verbose) # pass info to propagators self._allocate_propagators() - + # allocate helper fields and perform initial solves if needed self.model.allocate_helpers() @@ -264,7 +267,7 @@ def save_geometry_and_equil_vtk(self, verbose: bool = False): pointData["absB0"] = absB0 gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) - + def initialize_data_storage(self, verbose: bool = False): # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs @@ -282,20 +285,21 @@ def initialize_data_storage(self, verbose: bool = False): key_time_restart = "restart/time/" + key self.data.add_data({key_time: val}) self.data.add_data({key_time_restart: val}) - + def run(self, verbose: bool = False): - - # equation paramters - self.allocate(verbose=self.verbose) - # output - self.initialize_data_storage(verbose=self.verbose) - - # peek view into geometry - self.save_geometry_and_equil_vtk(verbose=self.verbose) + if not self.env.restart: + # equation paramters + self.allocate(verbose=self.verbose) + + # output + self.initialize_data_storage(verbose=self.verbose) - # plasma parameters - self.compute_plasma_params(verbose=self.verbose) + # peek view into geometry + self.save_geometry_and_equil_vtk(verbose=self.verbose) + + # plasma parameters + self.compute_plasma_params(verbose=self.verbose) # print info on mpi procs if self.rank < 32: @@ -303,7 +307,7 @@ def run(self, verbose: bool = False): print("") print(f"Rank {self.rank}: executing run() for model {self.model_name} ...") - if self.size > 32 and self.rank == 32: + if self.comm_size > 32 and self.rank == 32: print(f"Ranks > 31: executing run() for model {self.model_name} ...") # retrieve time parameters @@ -321,6 +325,14 @@ def run(self, verbose: bool = False): self.time_state["index"][0] = file["restart/time/index"][-1] total_steps = str(int(round((Tend - self.time_state["value"][0]) / dt))) + print(f"""\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +RESTARTing from: +{self.time_state["value"][0]=} +{self.time_state["value_sec"][0]=} +{self.time_state["index"][0]=} +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +""" + ) else: total_steps = str(int(round(Tend / dt))) @@ -331,7 +343,7 @@ def run(self, verbose: bool = False): self._add_time_state(self.time_state["value"]) # add all variables to be saved to data object - save_keys_all, save_keys_end = self._initialize_hdf5_datasets(self.data, self.size) + save_keys_all, save_keys_end = self._initialize_hdf5_datasets(self.data, self.comm_size) # ======================== main time loop ====================== self.model.update_scalar_quantities() @@ -443,11 +455,104 @@ def run(self, verbose: bool = False): self.clone_config.free() ProfileManager.finalize() - + + def pproc( + self, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + no_vtk: bool = False, + time_trace: bool = False, + verbose: bool = False, + ): + """Post-processing finished Struphy runs. + + Parameters + ---------- + step : int + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + + celldivide : int + Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. + + physical : bool + Wether to do post-processing into push-forwarded physical (xyz) components of fields. + + guiding_center : bool + Compute guiding-center coordinates (only from Particles6D). + + classify : bool + Classify guiding-center trajectories (passing, trapped or lost). + + no_vtk : bool + whether vtk files creation should be skipped + + time_trace : bool + whether to plot the time trace of each measured region + """ + + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n*** Start post-processing of {self.env.path_out}:") + + # create post-processing folder + self.path_pproc = os.path.join(self.env.path_out, "post_processing") + + try: + os.mkdir(self.path_pproc) + except: + shutil.rmtree(self.path_pproc) + os.mkdir(self.path_pproc) + + if time_trace: + from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration + + path_time_trace = os.path.join(self.env.path_out, "profiling_time_trace.pkl") + plot_time_vs_duration(path_time_trace, output_path=self.path_pproc) + plot_gantt_chart_plotly(path_time_trace, output_path=self.path_pproc) + return + + # check for fields and kinetic data in hdf5 file that need post processing + with h5py.File(os.path.join(self.env.path_out, "data/", "data_proc0.hdf5"), "r") as file: + # 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()) + + if "feec" in file.keys(): + exist_fields = True + else: + exist_fields = False + + if "kinetic" in file.keys(): + exist_particles = {"markers": False, "f": False, "n_sph": False} + kinetic_species = [] + kinetic_kinds = [] + for name in file["kinetic"].keys(): + kinetic_species += [name] + kinetic_kinds += [next(iter(self.model.species[name].variables.values())).space] + + # check for saved markers + if "markers" in file["kinetic"][name]: + exist_particles["markers"] = True + # check for saved distribution function + if "f" in file["kinetic"][name]: + exist_particles["f"] = True + # check for saved sph density + if "n_sph" in file["kinetic"][name]: + exist_particles["n_sph"] = True + else: + exist_particles = None + + # post-processing + if exist_fields: + self.pproc_fields(step=step, celldivide=celldivide, physical=physical) + if exist_particles is not None: + self.pproc_particles() + # --------------------- # Code specific methods # --------------------- - + def compute_plasma_params(self, verbose=True): """ Compute and print volume averaged plasma parameters for each species of the model. @@ -506,7 +611,7 @@ def compute_plasma_params(self, verbose=True): eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) - ## global parameters + # global parameters # plasma volume (hat x^3) det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) @@ -550,11 +655,123 @@ def compute_plasma_params(self, verbose=True): "Min magnetic field:".ljust(25), "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - - # --------------- + + def pproc_fields(self, step: int = 1, celldivide: int = 1, physical: bool = False, no_vtk: bool = False, verbose: bool = False,): + fields, t_grid = self._create_femfields(step=step) + point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) + if physical: + point_data_phy, _, _ = self._eval_femfields( + fields, + celldivide=[celldivide] * 3, + physical=True, + ) + + # directory for field data + path_fields = os.path.join(self.path_pproc, "fields_data") + + try: + os.mkdir(path_fields) + except: + shutil.rmtree(path_fields) + os.mkdir(path_fields) + + # save data dicts for each field + for species, vars in point_data.items(): + 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) + + 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) + + # create vtk files + if not no_vtk: + 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) + + def pproc_particles(self): + # directory for kinetic data + path_kinetics = os.path.join(path_pproc, "kinetic_data") + + try: + os.mkdir(path_kinetics) + except: + shutil.rmtree(path_kinetics) + os.mkdir(path_kinetics) + + # kinetic post-processing for each species + for n, species in enumerate(kinetic_species): + # directory for each species + path_kinetics_species = os.path.join(path_kinetics, species) + + try: + os.mkdir(path_kinetics_species) + except: + shutil.rmtree(path_kinetics_species) + os.mkdir(path_kinetics_species) + + # markers + if exist_particles["markers"]: + post_process_markers( + self.env.path_out, + path_kinetics_species, + species, + domain, + kinetic_kinds[n], + step, + ) + + if guiding_center: + assert kinetic_kinds[n] == "Particles6D" + orbits_tools.post_process_orbit_guiding_center(self.env.path_out, path_kinetics_species, species) + + if classify: + orbits_tools.post_process_orbit_classification(path_kinetics_species, species) + + # distribution function + if exist_particles["f"]: + if kinetic_kinds[n] == "DeltaFParticles6D": + compute_bckgr = True + else: + compute_bckgr = False + + post_process_f( + self.env.path_out, + params_in, + path_kinetics_species, + species, + step, + compute_bckgr=compute_bckgr, + ) + + # sph density + if exist_particles["n_sph"]: + post_process_n_sph( + self.env.path_out, + params_in, + path_kinetics_species, + species, + step, + ) + + # --------------- # Private methods # --------------- - + def _setup_folders( self, path_out: str, @@ -614,8 +831,8 @@ def _setup_folders( os.remove(file) if verbose and n < 10: # print only ten statements in case of many processes print("Removed existing file " + file) - - def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool=False): + + def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool = False): """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" if equil is not None: if isinstance(equil, NumericalMHDequilibrium): @@ -652,15 +869,15 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo print((key + ":").ljust(25), val) else: print("None.") - + def _setup_derham( self, - grid: grids.TensorProductGrid, - options: DerhamOptions, - comm: MPI.Intracomm = None, - domain: Domain = None, - verbose=False, -): + grid: grids.TensorProductGrid, + options: DerhamOptions, + comm: MPI.Intracomm = None, + domain: Domain = None, + verbose=False, + ): """ Creates the 3d derham sequence for given grid parameters. @@ -737,8 +954,8 @@ def _setup_derham( print("domain on process 0:".ljust(25), derham.domain_array[0]) return derham - - @profile + + @profile def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions): # create discrete derham sequence if self.clone_config is None: @@ -748,7 +965,7 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio if grid is None or derham_opts is None: if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") + print(f"\n{grid=}, {derham_opts=}: no Derham object set up.") self._derham = None else: self._derham = self._setup_derham( @@ -799,7 +1016,7 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio ) else: self._projected_equil = None - + @profile def _allocate_variables(self, verbose: bool = False): """ @@ -878,7 +1095,7 @@ def _allocate_variables(self, verbose: bool = False): # ) # self._pointer[key] = val["obj"].vector - + @profile def _allocate_propagators(self): # set propagators base class attributes (then available to all propagators) @@ -895,7 +1112,7 @@ def _allocate_propagators(self): prop.allocate() if MPI.COMM_WORLD.Get_rank() == 0: print(f"\nAllocated propagator '{prop.__class__.__name__}'.") - + @profile def _initialize_hdf5_datasets(self, data: DataContainer, size): """ @@ -1046,7 +1263,7 @@ def _initialize_hdf5_datasets(self, data: DataContainer, size): save_keys_all.append(key) return save_keys_all, save_keys_end - + def _add_time_state(self, time_state): """Add a pointer to the time variable of the dynamics ('t') to the model and to all propagators of the model. @@ -1061,7 +1278,7 @@ def _add_time_state(self, time_state): for _, prop in self.model.propagators.__dict__.items(): if isinstance(prop, Propagator): prop.add_time_state(time_state) - + def _initialize_from_restart(self, data: DataContainer): """ Set initial conditions for FE coefficients (electromagnetic and fluid) and markers from restart group in hdf5 files. @@ -1089,7 +1306,693 @@ def _initialize_from_restart(self, data: DataContainer): if MPI.COMM_WORLD.Get_size() > 1: subval.particles.mpi_sort_markers(do_test=True) - + + def _create_femfields(self, step: int = 1): + """Creates instances of :class:`~struphy.feec.psydac_derham.SplineFunction` from distributed Struphy data. + + Parameters + ---------- + step : int + Whether to create FEM fields at every time step (step=1, default), every second time step (step=2), etc. + + Returns + ------- + fields : dict + Nested dictionary holding :class:`~struphy.feec.psydac_derham.SplineFunction`: fields[t][name] contains the Field with the name "name" in the hdf5 file at time t. + + t_grid : xp.ndarray + Time grid. + """ + # get fields names, space IDs and time grid from 0-th rank hdf5 file + with h5py.File(os.path.join(self.env.path_out, "data/", "data_proc0.hdf5"), "r") as file: + space_ids = {} + print("\nReading hdf5 data of following species:") + for species, dset in file["feec"].items(): + space_ids[species] = {} + print(f"{species}:") + for var, ddset in dset.items(): + space_ids[species][var] = ddset.attrs["space_id"] + print(f" {var}:", ddset) + + t_grid = file["time/value"][::step].copy() + + # create one FemField for each snapshot + fields = {} + for t in t_grid: + fields[t] = {} + for species, vars in space_ids.items(): + fields[t][species] = {} + for var, id in vars.items(): + fields[t][species][var] = self.derham.create_spline_function( + var, + id, + verbose=False, + ) + + # get hdf5 data + print("") + for rank in range(int(self.comm_size)): + # open hdf5 file + with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + for species, dset in file["feec"].items(): + for var, ddset in tqdm(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) + + # loop over time + for n, t in enumerate(t_grid): + # scalar field + if gl_s.shape == (3,): + s1, s2, s3 = gl_s + e1, e2, e3 = gl_e + p1, p2, p3 = pads + + data = ddset[n * step, p1:-p1, p2:-p2, p3:-p3].copy() + + fields[t][species][var].vector[ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = data + # update after each data addition, can be made more efficient + fields[t][species][var].vector.update_ghost_regions() + + # 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] + + data = ddset[str(comp + 1)][ + n * step, + p1:-p1, + p2:-p2, + p3:-p3, + ].copy() + + fields[t][species][var].vector[comp][ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = data + # update after each data addition, can be made more efficient + fields[t][species][var].vector.update_ghost_regions() + + print("Creation of Struphy Fields done.") + + return fields, t_grid + + def _eval_femfields( + self, + fields: dict, + *, + celldivide: list = [1, 1, 1], + physical: bool = False, + ): + """Evaluate FEM fields obtained from :meth:`struphy.post_processing.post_processing_tools.create_femfields`. + + Parameters + ---------- + params_in : ParamsIn + Simulation parameters. + + fields : dict + Obtained from struphy.diagnostics.post_processing.create_femfields. + + celldivide : list of ints + Grid refinement in each eta direction. + + physical : bool + Wether to do post-processing into push-forwarded physical (xyz) components of fields. + + Returns + ------- + point_data : dict + Nested dictionary holding values of FemFields on the grid as list of 3d xp.arrays: + point_data[name][t] contains the values of the field with name "name" in fields[t].keys() at time t. + + If physical is True, physical components of fields are saved. + Otherwise, logical components (differential n-forms) are saved. + + grids_log : 3-list + 1d logical grids in each eta-direction with Nel[i]*cell_divide[i] + 1 entries in each direction. + + grids_phy : 3-list + Mapped (physical) grids obtained by domain(*grids_log). + """ + + # create logical and physical grids + assert isinstance(fields, dict) + assert isinstance(celldivide, list) + assert len(celldivide) == 3 + + Nel = self.grid.Nel + + grids_log = [xp.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] + grids_phy = [ + self.domain(*grids_log)[0], + self.domain(*grids_log)[1], + self.domain(*grids_log)[2], + ] + + # evaluate fields at evaluation grid and push-forward + point_data = {} + for species, vars in fields[list(fields.keys())[0]].items(): + point_data[species] = {} + for name, field in vars.items(): + point_data[species][name] = {} + + print("\nEvaluating fields ...") + for t in tqdm(fields): + for species, vars in fields[t].items(): + for name, field in vars.items(): + assert isinstance(field, SplineFunction) + space_id = field.space_id + + # field evaluation + temp_val = field(*grids_log) + + point_data[species][name][t] = [] + + # scalar spaces + if isinstance(temp_val, xp.ndarray): + if physical: + # push-forward + if space_id == "H1": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="0", + ), + ) + elif space_id == "L2": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="3", + ), + ) + + else: + point_data[species][name][t].append(temp_val) + + # vector-valued spaces + else: + for j in range(3): + if physical: + # push-forward + if space_id == "Hcurl": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="1", + )[j], + ) + elif space_id == "Hdiv": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="2", + )[j], + ) + elif space_id == "H1vec": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="v", + )[j], + ) + + else: + point_data[species][name][t].append(temp_val[j]) + + return point_data, grids_log, grids_phy + + def _create_vtk( + self, + path: str, + t_grid: xp.ndarray, + grids_phy: list, + point_data: dict, + *, + physical: bool = False, + ): + """Creates structured virtual toolkit files (.vts) for Paraview from evaluated field data. + + Parameters + ---------- + path : str + Absolute path of where to store the .vts files. Will then be in path/vtk/step_.vts. + + t_grid : xp.ndarray + Time grid. + + grids_phy : 3-list + Mapped (physical) grids obtained from struphy.diagnostics.post_processing.eval_femfields. + + point_data : dict + Field data obtained from struphy.diagnostics.post_processing.eval_femfields. + + physical : bool + Wether to create vtk for push-forwarded physical (xyz) components of fields. + """ + for species, vars in point_data.items(): + species_path = os.path.join(path, species, "vtk" + physical * "_phy") + try: + os.mkdir(species_path) + except: + shutil.rmtree(species_path) + os.mkdir(species_path) + + # time loop + nt = len(t_grid) - 1 + log_nt = int(xp.log10(nt)) + 1 + + print(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( + path_out: str, + species: str, + domain: Domain, + kind: str = "Particles6D", + step: int = 1, + ): + """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation + and writes them to a .npy files and to .txt files. + Also saves the weights. + + * ``.npy`` files: + + * Particles6D: + + ===== ===== ============== ============= ====== + index | 0 | | 1 | 2 | 3 | | 4 | 5 | 6 | | 7 | + ===== ===== ============== ============= ====== + value ID position (xyz) velocities weight + ===== ===== ============== ============= ====== + + * Particles5D: + + ===== ===== ================ ========== ====== ====== ============ + index | 0 | | 1 | 2 | | 3 | 4 5 | 6 | 7 + ===== ===== ================ ========== ====== ====== ============ + value ID guiding_center v_parallel v_perp weight magn. moment + ===== ===== ================ ========== ====== ====== ============ + + * Particles3D: + + ===== ===== ============== ====== + index | 0 | | 1 | 2 | 3 | | 4 | + ===== ===== ============== ====== + value ID position (xyz) weight + ===== ===== ============== ====== + + * ``.txt`` files : + + ===== ===== ============== ====== + index | 0 | | 1 | 2 | 3 | | 4 | + ===== ===== ============== ====== + value ID position (xyz) weight + ===== ===== ============== ====== + + ``.txt`` files can be imported to e.g. Paraview, see `08 - Kinetic data `_ for details. + + Parameters + ---------- + path_in : str + Absolute path of simulation output folder. + + path_out : str + Absolute path of where to store the .txt files. Will be in path_out/orbits. + + species : str + Name of the species for which the post processing should be performed. + + domain : Domain + Domain object. + + kind : str + Name of the kinetic kind (Particles6D, Particles5D or Particles3D). + + step : int, optional + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + """ + # get # of MPI processes from meta.txt file + with open(os.path.join(path_in, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] + + # open hdf5 files and get names and number of saved markers of kinetic species + with h5py.File(os.path.join(path_in, "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 + + 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_out, "orbits") + + if "5D" in kind: + save_index = list(range(0, 6)) + [10] + [-1] + elif "6D" in kind or "SPH" in kind: + save_index = list(range(0, 7)) + [-1] + else: + save_index = list(range(0, 4)) + [-1] + + try: + os.mkdir(path_orbits) + except: + shutil.rmtree(path_orbits) + os.mkdir(path_orbits) + + # temporary array + temp = xp.empty((n_markers, len(save_index)), order="C") + lost_particles_mask = xp.empty(n_markers, dtype=bool) + + print(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 i in range(int(nproc)): + with h5py.File(os.path.join(path_in, "data/", f"data_proc{i}.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] + + # 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 = domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) + temp[~lost_particles_mask, :3] = pos_phys + + # 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=", ") + + def _post_process_f( + path_out, + species, + step=1, + compute_bckgr=False, + ): + """Computes and saves distribution functions of saved binning data during a simulation. + + Parameters + ---------- + path_in : str + Absolute path of simulation output folder. + + params_in : ParamsIn + Simulation parameters. + + path_out : str + Absolute path of where to store the .txt files. Will be in path_out/orbits. + + species : str + Name of the species for which the post processing should be performed. + + step : int, optional + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + + compute_bckgr : bool + Whether to compute the kinetic background values and add them to the binning data. + This is used if non-standard weights are binned. + """ + # get # of MPI processes from meta file + with open(os.path.join(path_in, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] + + # directory for .npy files + path_distr = os.path.join(path_out, "distribution_function") + + try: + os.mkdir(path_distr) + except: + shutil.rmtree(path_distr) + os.mkdir(path_distr) + + print("Evaluation of distribution functions for " + str(species)) + + # Create grids + with h5py.File(os.path.join(path_in, "data/data_proc0.hdf5"), "r") as file_0: + for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): + # create a new folder for each slice + path_slice = os.path.join(path_distr, slice_name) + os.mkdir(path_slice) + + # Find out all names of slices + slice_names = 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_names[n_gr] + ".npy", + ) + xp.save(grid_path, grid[:]) + + # compute distribution function + for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): + # path to folder of slice + path_slice = os.path.join(path_distr, slice_name) + + # Find out all names of slices + slice_names = slice_name.split("_") + + # load full-f data + data = file_0["kinetic/" + species + "/f/" + slice_name][::step].copy() + data_df = file_0["kinetic/" + species + "/df/" + slice_name][::step].copy() + for rank in range(1, int(nproc)): + with h5py.File(os.path.join(path_in, "data/", f"data_proc{rank}.hdf5"), "r") as file: + data += file["kinetic/" + species + "/f/" + slice_name][::step] + data_df += file["kinetic/" + species + "/df/" + slice_name][::step] + + # 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) + + 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, + # ) + + spec: ParticleSpecies = getattr(params_in.model, species) + var: PICVariable = spec.var + f_bckgr: KineticBackground = var.backgrounds + + # 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_names: + 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_names: + 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])], + ) + + def _post_process_n_sph( + path_out, + species, + step=1, + ): + """Computes and saves the density n of saved sph data during a simulation. + + Parameters + ---------- + path_in : str + Absolute path of simulation output folder. + + params_in : ParamsIn + Simulation parameters. + + path_out : str + Absolute path of where to store the .txt files. Will be in path_out/orbits. + + species : str + Name of the species for which the post processing should be performed. + + step : int, optional + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + """ + # get model name and # of MPI processes from meta file + with open(os.path.join(path_in, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] + + # directory for .npy files + path_n_sph = os.path.join(path_out, "n_sph") + + try: + os.mkdir(path_n_sph) + except: + shutil.rmtree(path_n_sph) + os.mkdir(path_n_sph) + + print("Evaluation of sph density for " + str(species)) + + with h5py.File(os.path.join(path_in, "data/data_proc0.hdf5"), "r") as file_0: + # Create grids + for i, view in enumerate(file_0["kinetic/" + species + "/n_sph"]): + # create a new folder for each view + path_view = os.path.join(path_n_sph, view) + os.mkdir(path_view) + + # 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", + ) + + grid_path = os.path.join( + path_view, + "grid_n_sph.npy", + ) + xp.save(grid_path, (ee1, ee2, ee3)) + + # load n_sph data + data = file_0["kinetic/" + species + "/n_sph/" + view][::step].copy() + for rank in range(1, int(nproc)): + with h5py.File(os.path.join(path_in, "data/", f"data_proc{rank}.hdf5"), "r") as file: + data += file["kinetic/" + species + "/n_sph/" + view][::step] + + # save distribution functions + xp.save(os.path.join(path_view, "n_sph.npy"), data) + + # ----------------- + # Common properties + # ----------------- + @property def clone_config(self): """Config in case domain clones are used.""" @@ -1099,7 +2002,7 @@ def clone_config(self): def clone_config(self, new): assert isinstance(new, CloneConfig) or new is None self._clone_config = new - + @property def domain(self): """Domain object, see :ref:`avail_mappings`.""" @@ -1108,13 +2011,13 @@ def domain(self): @property def equil(self): """Fluid equilibrium object, see :ref:`fluid_equil`.""" - return self._equil - + return self._equil + @property def derham(self): """3d Derham sequence, see :ref:`derham`.""" - return self._derham - + return self._derham + @property def mass_ops(self): """WeighteMassOperators object, see :ref:`mass_ops`.""" @@ -1124,9 +2027,8 @@ def mass_ops(self): def basis_ops(self): """Basis projection operators.""" return self._basis_ops - + @property def projected_equil(self): """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" return self._projected_equil - \ No newline at end of file From 7e11b9d79e7789817a79f0f600ce2e6f0fa6d41b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 08:50:28 +0100 Subject: [PATCH 16/80] model tests pass with pproc --- src/struphy/models/hasegawa_wakatani.py | 5 +- src/struphy/models/shear_alfven.py | 8 +- .../models/variational_compressible_fluid.py | 2 +- .../models/visco_resistive_deltaf_mhd.py | 4 +- .../visco_resistive_deltaf_mhd_with_q.py | 2 +- .../models/visco_resistive_linear_mhd.py | 4 +- .../visco_resistive_linear_mhd_with_q.py | 2 +- src/struphy/models/visco_resistive_mhd.py | 4 +- .../models/visco_resistive_mhd_with_p.py | 4 +- .../models/visco_resistive_mhd_with_q.py | 6 +- src/struphy/models/viscous_fluid.py | 2 +- .../models/vlasov_ampere_one_species.py | 6 +- src/struphy/simulation/codes.py | 386 +++++++++--------- 13 files changed, 204 insertions(+), 231 deletions(-) diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index 7220dd8be..ece72f4d6 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -109,6 +109,9 @@ def allocate_helpers(self): :meta private: """ + self._rho: StencilVector = Propagator.derham.Vh["0"].zeros() + self.update_rho() + if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -118,8 +121,6 @@ def allocate_helpers(self): if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") - self._rho: StencilVector = Propagator.derham.Vh["0"].zeros() - self.update_rho() def update_scalar_quantities(self): pass diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index e24fb1916..15a1839d8 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -70,13 +70,7 @@ def velocity_scale(self): def allocate_helpers(self): # project background magnetic field (2-form) and pressure (3-form) - self._b_eq = Propagator.derham.P["2"]( - [ - self.equil.b2_1, - self.equil.b2_2, - self.equil.b2_3, - ], - ) + self._b_eq = Propagator.projected_equil.b2 # temporary vectors for scalar quantities self._tmp_b1 = Propagator.derham.Vh["2"].zeros() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 4e6fccb10..dc0784726 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -103,7 +103,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index 0dae56539..32a475dbb 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -150,7 +150,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 @@ -195,7 +195,7 @@ def update_scalar_quantities(self): # self.update_scalar("dens_tot", dens_tot) # div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) - # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) + # L2_div_B = Propagator.mass_ops.M3.dot_inner(div_B, div_B) # self.update_scalar("tot_div_B", L2_div_B) en_thermo_l1 = Propagator.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index 1e80ec363..3c1ef1e71 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -147,7 +147,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index 890c6f28f..dabda13c1 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -147,7 +147,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 @@ -192,7 +192,7 @@ def update_scalar_quantities(self): # self.update_scalar("dens_tot", dens_tot) # div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) - # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) + # L2_div_B = Propagator.mass_ops.M3.dot_inner(div_B, div_B) # self.update_scalar("tot_div_B", L2_div_B) en_thermo_l1 = Propagator.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index ef07f4bd4..c1097c314 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -144,7 +144,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index 7126b0ad7..996909d47 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -145,7 +145,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 @@ -186,7 +186,7 @@ def update_scalar_quantities(self): self.update_scalar("entr_tot", entr_tot) div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) - L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) + L2_div_B = Propagator.mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) def update_thermo_energy(self): diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index 5380d0acc..9956f3085 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -145,7 +145,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 @@ -185,7 +185,7 @@ def update_scalar_quantities(self): self.update_scalar("dens_tot", dens_tot) div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) - L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) + L2_div_B = Propagator.mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) # default parameters diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 4f98f2ac8..35b40e606 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -147,7 +147,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 @@ -177,7 +177,7 @@ def update_scalar_quantities(self): en_mag = 0.5 * Propagator.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) - en_thermo = 1.0 / (gamma - 1.0) * self._mass_ops.M3.dot_inner(q, q) + en_thermo = 1.0 / (gamma - 1.0) * Propagator.mass_ops.M3.dot_inner(q, q) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag @@ -187,7 +187,7 @@ def update_scalar_quantities(self): self.update_scalar("dens_tot", dens_tot) div_B = Propagator.derham.div.dot(b, out=self._tmp_div_B) - L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) + L2_div_B = Propagator.mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) # default parameters diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index d2d38e208..2144c8bc2 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -113,7 +113,7 @@ def velocity_scale(self): return "alfvén" def allocate_helpers(self): - projV3 = L2Projector("L2", self._mass_ops) + projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): return 1 diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 3d0a1b6ec..01b244fa5 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -178,12 +178,12 @@ def allocate_helpers(self): particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), - self.mass_ops, + Propagator.mass_ops, Propagator.domain.args_domain, ) # another sanity check: compute FE coeffs of density - # charge_accum.show_accumulated_spline_field(self.mass_ops) + # charge_accum.show_accumulated_spline_field(Propagator.mass_ops) alpha = self.kinetic_ions.equation_params.alpha epsilon = self.kinetic_ions.equation_params.epsilon @@ -205,7 +205,7 @@ def allocate_helpers(self): def update_scalar_quantities(self): # e*M1*e/2 e = self.em_fields.e_field.spline.vector - en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + en_E = 0.5 * Propagator.mass_ops.M1.dot_inner(e, e) self.update_scalar("en_E", en_E) # alpha^2 / 2 / N * sum_p w_p v_p^2 diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 0f391791f..10a1e9d12 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -35,6 +35,8 @@ from struphy.utils.utils import dict_to_yaml from struphy.simulation.base import Simulation from struphy.feec.psydac_derham import SplineFunction +from struphy.post_processing.orbits import orbits_tools +from struphy.kinetic_background.base import KineticBackground # third party imports from feectools.ddm.mpi import MockMPI @@ -463,7 +465,7 @@ def pproc( physical: bool = False, guiding_center: bool = False, classify: bool = False, - no_vtk: bool = False, + create_vtk: bool = True, time_trace: bool = False, verbose: bool = False, ): @@ -486,8 +488,8 @@ def pproc( classify : bool Classify guiding-center trajectories (passing, trapped or lost). - no_vtk : bool - whether vtk files creation should be skipped + create_vtk : bool + Whether vtk files should be created. time_trace : bool whether to plot the time trace of each measured region @@ -497,7 +499,7 @@ def pproc( print(f"\n*** Start post-processing of {self.env.path_out}:") # create post-processing folder - self.path_pproc = os.path.join(self.env.path_out, "post_processing") + self._path_pproc = os.path.join(self.env.path_out, "post_processing") try: os.mkdir(self.path_pproc) @@ -524,35 +526,148 @@ def pproc( exist_fields = False if "kinetic" in file.keys(): - exist_particles = {"markers": False, "f": False, "n_sph": False} - kinetic_species = [] - kinetic_kinds = [] + self.exist_particles = {"markers": False, "f": False, "n_sph": False} + self.kinetic_species = [] + self.kinetic_kinds = [] for name in file["kinetic"].keys(): - kinetic_species += [name] - kinetic_kinds += [next(iter(self.model.species[name].variables.values())).space] + 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]: - exist_particles["markers"] = True + self.exist_particles["markers"] = True # check for saved distribution function if "f" in file["kinetic"][name]: - exist_particles["f"] = True + self.exist_particles["f"] = True # check for saved sph density if "n_sph" in file["kinetic"][name]: - exist_particles["n_sph"] = True + self.exist_particles["n_sph"] = True else: - exist_particles = None + self.exist_particles = None # post-processing if exist_fields: - self.pproc_fields(step=step, celldivide=celldivide, physical=physical) - if exist_particles is not None: - self.pproc_particles() + self.pproc_fields(step=step, celldivide=celldivide, physical=physical, + create_vtk=create_vtk, verbose=verbose,) + if self.exist_particles is not None: + self.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) # --------------------- # Code specific methods # --------------------- + def pproc_fields(self, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + create_vtk: bool = True, + verbose: bool = False, + ): + fields, t_grid = self._create_femfields(step=step) + point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) + if physical: + point_data_phy, _, _ = self._eval_femfields( + fields, + celldivide=[celldivide] * 3, + physical=True, + ) + + # directory for field data + path_fields = os.path.join(self.path_pproc, "fields_data") + + try: + os.mkdir(path_fields) + except: + shutil.rmtree(path_fields) + os.mkdir(path_fields) + + # save data dicts for each field + for species, vars in point_data.items(): + 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) + + 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) + + # create vtk files + if create_vtk: + 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) + + def pproc_particles(self, + step: int = 1, + guiding_center: bool = False, + classify: bool = False, + verbose: bool = False,): + # directory for kinetic data + path_kinetics = os.path.join(self.path_pproc, "kinetic_data") + + try: + os.mkdir(path_kinetics) + except: + shutil.rmtree(path_kinetics) + os.mkdir(path_kinetics) + + # 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) + + try: + os.mkdir(path_kinetics_species) + except: + shutil.rmtree(path_kinetics_species) + os.mkdir(path_kinetics_species) + + # 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.env.path_out, 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 compute_plasma_params(self, verbose=True): """ Compute and print volume averaged plasma parameters for each species of the model. @@ -656,118 +771,6 @@ def compute_plasma_params(self, verbose=True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - def pproc_fields(self, step: int = 1, celldivide: int = 1, physical: bool = False, no_vtk: bool = False, verbose: bool = False,): - fields, t_grid = self._create_femfields(step=step) - point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) - if physical: - point_data_phy, _, _ = self._eval_femfields( - fields, - celldivide=[celldivide] * 3, - physical=True, - ) - - # directory for field data - path_fields = os.path.join(self.path_pproc, "fields_data") - - try: - os.mkdir(path_fields) - except: - shutil.rmtree(path_fields) - os.mkdir(path_fields) - - # save data dicts for each field - for species, vars in point_data.items(): - 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) - - 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) - - # create vtk files - if not no_vtk: - 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) - - def pproc_particles(self): - # directory for kinetic data - path_kinetics = os.path.join(path_pproc, "kinetic_data") - - try: - os.mkdir(path_kinetics) - except: - shutil.rmtree(path_kinetics) - os.mkdir(path_kinetics) - - # kinetic post-processing for each species - for n, species in enumerate(kinetic_species): - # directory for each species - path_kinetics_species = os.path.join(path_kinetics, species) - - try: - os.mkdir(path_kinetics_species) - except: - shutil.rmtree(path_kinetics_species) - os.mkdir(path_kinetics_species) - - # markers - if exist_particles["markers"]: - post_process_markers( - self.env.path_out, - path_kinetics_species, - species, - domain, - kinetic_kinds[n], - step, - ) - - if guiding_center: - assert kinetic_kinds[n] == "Particles6D" - orbits_tools.post_process_orbit_guiding_center(self.env.path_out, path_kinetics_species, species) - - if classify: - orbits_tools.post_process_orbit_classification(path_kinetics_species, species) - - # distribution function - if exist_particles["f"]: - if kinetic_kinds[n] == "DeltaFParticles6D": - compute_bckgr = True - else: - compute_bckgr = False - - post_process_f( - self.env.path_out, - params_in, - path_kinetics_species, - species, - step, - compute_bckgr=compute_bckgr, - ) - - # sph density - if exist_particles["n_sph"]: - post_process_n_sph( - self.env.path_out, - params_in, - path_kinetics_species, - species, - step, - ) - # --------------- # Private methods # --------------- @@ -1606,10 +1609,8 @@ def _create_vtk( ) def _post_process_markers( - path_out: str, - species: str, - domain: Domain, - kind: str = "Particles6D", + self, + path_kinetic_species: str, step: int = 1, ): """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation @@ -1654,42 +1655,36 @@ def _post_process_markers( Parameters ---------- - path_in : str - Absolute path of simulation output folder. - - path_out : str - Absolute path of where to store the .txt files. Will be in path_out/orbits. - - species : str - Name of the species for which the post processing should be performed. - - domain : Domain - Domain object. - - kind : str - Name of the kinetic kind (Particles6D, Particles5D or Particles3D). + path_kinetic_species : str + Path to kinetic data of considered species. step : int, optional Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. """ - # get # of MPI processes from meta.txt file - with open(os.path.join(path_in, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] - + + 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(path_in, "data/data_proc0.hdf5"), "r") as file_0: + with h5py.File(os.path.join(self.env.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 varname, var in species_obj.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + obj: Particles = var.particles + vdim = obj.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_out, "orbits") + path_orbits = os.path.join(path_kinetic_species, "orbits") - if "5D" in kind: + if vdim == 2: save_index = list(range(0, 6)) + [10] + [-1] - elif "6D" in kind or "SPH" in kind: + elif vdim == 3: save_index = list(range(0, 7)) + [-1] else: save_index = list(range(0, 4)) + [-1] @@ -1721,8 +1716,8 @@ def _post_process_markers( species + "_{0:0{1}d}.txt".format(n, log_nt), ) - for i in range(int(nproc)): - with h5py.File(os.path.join(path_in, "data/", f"data_proc{i}.hdf5"), "r") as file: + for i in range(int(self.comm_size)): + with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{i}.hdf5"), "r") as file: markers = file["kinetic/" + species + "/markers"] ids = markers[n * step, :, -1].astype("int") ids = ids[ids != -1] # exclude holes @@ -1744,7 +1739,7 @@ def _post_process_markers( assert xp.all(sorted(ids) == xp.arange(n_markers)) # compute physical positions (x, y, z) - pos_phys = domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) + pos_phys = self.domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) temp[~lost_particles_mask, :3] = pos_phys # save numpy @@ -1754,8 +1749,8 @@ def _post_process_markers( xp.savetxt(file_txt, temp[:, (0, 1, 2, 3, -1)], fmt="%12.6f", delimiter=", ") def _post_process_f( - path_out, - species, + self, + path_kinetic_species, step=1, compute_bckgr=False, ): @@ -1763,17 +1758,8 @@ def _post_process_f( Parameters ---------- - path_in : str - Absolute path of simulation output folder. - - params_in : ParamsIn - Simulation parameters. - - path_out : str - Absolute path of where to store the .txt files. Will be in path_out/orbits. - - species : str - Name of the species for which the post processing should be performed. + path_kinetic_species : str + Path to kinetic data of considered species. step : int, optional Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. @@ -1782,13 +1768,11 @@ def _post_process_f( Whether to compute the kinetic background values and add them to the binning data. This is used if non-standard weights are binned. """ - # get # of MPI processes from meta file - with open(os.path.join(path_in, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] + species = path_kinetic_species.split("/")[-1] + species_obj: ParticleSpecies = self.model.particle_species[species] # directory for .npy files - path_distr = os.path.join(path_out, "distribution_function") + path_distr = os.path.join(path_kinetic_species, "distribution_function") try: os.mkdir(path_distr) @@ -1799,7 +1783,7 @@ def _post_process_f( print("Evaluation of distribution functions for " + str(species)) # Create grids - with h5py.File(os.path.join(path_in, "data/data_proc0.hdf5"), "r") as file_0: + with h5py.File(os.path.join(self.env.path_out, "data/data_proc0.hdf5"), "r") as file_0: for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): # create a new folder for each slice path_slice = os.path.join(path_distr, slice_name) @@ -1827,8 +1811,8 @@ def _post_process_f( # load full-f data data = file_0["kinetic/" + species + "/f/" + slice_name][::step].copy() data_df = file_0["kinetic/" + species + "/df/" + slice_name][::step].copy() - for rank in range(1, int(nproc)): - with h5py.File(os.path.join(path_in, "data/", f"data_proc{rank}.hdf5"), "r") as file: + for rank in range(1, int(self.comm_size)): + with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: data += file["kinetic/" + species + "/f/" + slice_name][::step] data_df += file["kinetic/" + species + "/df/" + slice_name][::step] @@ -1854,10 +1838,11 @@ def _post_process_f( # f_bckgr = f_bckgr + getattr(maxwellians, fi_type)( # maxw_params=maxw_params, # ) - - spec: ParticleSpecies = getattr(params_in.model, species) - var: PICVariable = spec.var - f_bckgr: KineticBackground = var.backgrounds + + for _, var in species_obj.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + f_bckgr: KineticBackground = var.backgrounds + break # load all grids of the variables of f grid_tot = [] @@ -1916,36 +1901,24 @@ def _post_process_f( ) def _post_process_n_sph( - path_out, - species, + self, + path_kinetic_species, step=1, ): """Computes and saves the density n of saved sph data during a simulation. Parameters ---------- - path_in : str - Absolute path of simulation output folder. - - params_in : ParamsIn - Simulation parameters. - - path_out : str - Absolute path of where to store the .txt files. Will be in path_out/orbits. - - species : str - Name of the species for which the post processing should be performed. + path_kinetic_species : str + Path to kinetic data of considered species. step : int, optional Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. """ - # get model name and # of MPI processes from meta file - with open(os.path.join(path_in, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] + species = path_kinetic_species.split("/")[-1] # directory for .npy files - path_n_sph = os.path.join(path_out, "n_sph") + path_n_sph = os.path.join(path_kinetic_species, "n_sph") try: os.mkdir(path_n_sph) @@ -1955,7 +1928,7 @@ def _post_process_n_sph( print("Evaluation of sph density for " + str(species)) - with h5py.File(os.path.join(path_in, "data/data_proc0.hdf5"), "r") as file_0: + with h5py.File(os.path.join(self.env.path_out, "data/data_proc0.hdf5"), "r") as file_0: # Create grids for i, view in enumerate(file_0["kinetic/" + species + "/n_sph"]): # create a new folder for each view @@ -1982,8 +1955,8 @@ def _post_process_n_sph( # load n_sph data data = file_0["kinetic/" + species + "/n_sph/" + view][::step].copy() - for rank in range(1, int(nproc)): - with h5py.File(os.path.join(path_in, "data/", f"data_proc{rank}.hdf5"), "r") as file: + for rank in range(1, int(self.comm_size)): + with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: data += file["kinetic/" + species + "/n_sph/" + view][::step] # save distribution functions @@ -2032,3 +2005,8 @@ def basis_ops(self): def projected_equil(self): """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" return self._projected_equil + + @property + def path_pproc(self): + """Path to post-processing folder.""" + return self._path_pproc \ No newline at end of file From 59f77ef74bb87a96a2d6f6fee62597806c6f67e7 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 12:00:18 +0100 Subject: [PATCH 17/80] move pproc and load_plotting_data into module file and allow for passing a StruphySimulation OR a path --- src/struphy/main.py | 10 +- src/struphy/models/tests/utils_testing.py | 7 +- .../post_processing/post_processing_tools.py | 994 ++++++------------ src/struphy/simulation/base.py | 5 + src/struphy/simulation/codes.py | 92 +- 5 files changed, 309 insertions(+), 799 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index 203eb5d05..9a67868f0 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -7,6 +7,7 @@ import sysconfig import time from typing import Optional, TypedDict +import pickle import cunumpy as xp import h5py @@ -29,15 +30,6 @@ from struphy.physics.physics import Units from struphy.pic.base import Particles from struphy.post_processing.orbits import orbits_tools -from struphy.post_processing.post_processing_tools import ( - create_femfields, - create_vtk, - eval_femfields, - get_params_of_run, - post_process_f, - post_process_markers, - post_process_n_sph, -) from struphy.topology import grids from struphy.topology.grids import TensorProductGrid from struphy.utils.clone_config import CloneConfig diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 9279294c9..765abced0 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -5,9 +5,7 @@ from feectools.ddm.mpi import mpi as MPI -import struphy.models as models -import struphy.models.utils as models_utils -from struphy import EnvironmentOptions, main +from struphy import EnvironmentOptions from struphy.io.setup import import_parameters_py from struphy.models.base import StruphyModel from struphy.simulation.codes import StruphySimulation @@ -79,8 +77,7 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = MPI.COMM_WORLD.Barrier() if rank == 0: - path_out = os.path.join(test_folder, model_name) sim.pproc(verbose=verbose) - # main.load_data(path=path_out) + sim.load_plotting_data(verbose=verbose) shutil.rmtree(test_folder) MPI.COMM_WORLD.Barrier() diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 2d8d1e85d..1a80cbdec 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -6,6 +6,8 @@ import h5py import yaml from tqdm import tqdm +from feectools.ddm.mpi import mpi as MPI +from typing import TYPE_CHECKING from struphy.feec.psydac_derham import SplineFunction from struphy.fields_background import equils @@ -21,6 +23,9 @@ from struphy.models.variables import PICVariable from struphy.topology.grids import TensorProductGrid +if TYPE_CHECKING: + from struphy.simulation.codes import StruphySimulation + class ParamsIn: """Holds the input parameters of a Struphy simulation as attributes.""" @@ -46,6 +51,73 @@ def __init__( self.model = model +class SimData: + """Holds post-processed Struphy data as attributes. + + Parameters + ---------- + path : str + Absolute path of simulation output folder to post-process. + """ + + def __init__(self, path: str): + self.path = path + self._orbits = {} + self._f = {} + self._spline_values = {} + self._n_sph = {} + self.grids_log: list[xp.ndarray] = None + self.grids_phy: list[xp.ndarray] = None + self.t_grid: xp.ndarray = None + + @property + def orbits(self) -> dict[str, xp.ndarray]: + """Keys: species name. Values: 3d arrays indexed by (n, p, a), where 'n' is the time index, 'p' the particle index and 'a' the attribute index.""" + return self._orbits + + @property + def f(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: + """Keys: species name. Values: dicts of slice names ('e1_v1' etc.) holding dicts of corresponding xp.arrays for plotting.""" + return self._f + + @property + def spline_values(self) -> dict[str, dict[str, xp.ndarray]]: + """Keys: species name. Values: dicts of variable names with values being 3d arrays on the grid.""" + return self._spline_values + + @property + def n_sph(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: + """Keys: species name. Values: dicts of view names ('view_0' etc.) holding dicts of corresponding xp.arrays for plotting.""" + return self._n_sph + + @property + def Nt(self) -> dict[str, int]: + """Number of available time points (snap shots) for each species.""" + if not hasattr(self, "_Nt"): + self._Nt = {} + for spec, orbs in self.orbits.items(): + self._Nt[spec] = orbs.shape[0] + return self._Nt + + @property + def Np(self) -> dict[str, int]: + """Number of particle orbits for each species.""" + if not hasattr(self, "_Np"): + self._Np = {} + for spec, orbs in self.orbits.items(): + self._Np[spec] = orbs.shape[1] + return self._Np + + @property + def Nattr(self) -> dict[str, int]: + """Number of particle attributes for each species.""" + if not hasattr(self, "_Nattr"): + self._Nattr = {} + for spec, orbs in self.orbits.items(): + self._Nattr[spec] = orbs.shape[2] + return self._Nattr + + def get_params_of_run(path: str) -> ParamsIn: """Retrieve parameters of finished Struphy run. @@ -114,722 +186,246 @@ def get_params_of_run(path: str) -> ParamsIn: ) -def create_femfields( - path: str, - params_in: ParamsIn, - *, - step: int = 1, -): - """Creates instances of :class:`~struphy.feec.psydac_derham.SplineFunction` from distributed Struphy data. +def pproc(sim: "StruphySimulation" = None, + path_out: str = None, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = True, + time_trace: bool = False, + verbose: bool = False, + ): + """Post-processing finished Struphy runs. Parameters ---------- - path : str - Absolute path of simulation output folder. - - params_in : ParamsIn - Simulation parameters. - + sim : StruphySimulation + StruphySimulation object of finished run. + step : int - Whether to create FEM fields at every time step (step=1, default), every second time step (step=2), etc. - - Returns - ------- - fields : dict - Nested dictionary holding :class:`~struphy.feec.psydac_derham.SplineFunction`: fields[t][name] contains the Field with the name "name" in the hdf5 file at time t. - - t_grid : xp.ndarray - Time grid. - """ - - with open(os.path.join(path, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] - - derham = setup_derham( - params_in.grid, - params_in.derham_opts, - comm=None, - domain=params_in.domain, - ) - - # get fields names, space IDs and time grid from 0-th rank hdf5 file - with h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") as file: - space_ids = {} - print("\nReading hdf5 data of following species:") - for species, dset in file["feec"].items(): - space_ids[species] = {} - print(f"{species}:") - for var, ddset in dset.items(): - space_ids[species][var] = ddset.attrs["space_id"] - print(f" {var}:", ddset) - - t_grid = file["time/value"][::step].copy() - - # create one FemField for each snapshot - fields = {} - for t in t_grid: - fields[t] = {} - for species, vars in space_ids.items(): - fields[t][species] = {} - for var, id in vars.items(): - fields[t][species][var] = derham.create_spline_function( - var, - id, - verbose=False, - ) - - # get hdf5 data - print("") - for rank in range(int(nproc)): - # open hdf5 file - with h5py.File(os.path.join(path, "data/", f"data_proc{rank}.hdf5"), "r") as file: - for species, dset in file["feec"].items(): - for var, ddset in tqdm(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) - - # loop over time - for n, t in enumerate(t_grid): - # scalar field - if gl_s.shape == (3,): - s1, s2, s3 = gl_s - e1, e2, e3 = gl_e - p1, p2, p3 = pads - - data = ddset[n * step, p1:-p1, p2:-p2, p3:-p3].copy() - - fields[t][species][var].vector[ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = data - # update after each data addition, can be made more efficient - fields[t][species][var].vector.update_ghost_regions() - - # 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] - - data = ddset[str(comp + 1)][ - n * step, - p1:-p1, - p2:-p2, - p3:-p3, - ].copy() - - fields[t][species][var].vector[comp][ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = data - # update after each data addition, can be made more efficient - fields[t][species][var].vector.update_ghost_regions() - - print("Creation of Struphy Fields done.") - - return fields, t_grid - - -def eval_femfields( - params_in: ParamsIn, - fields: dict, - *, - celldivide: list = [1, 1, 1], - physical: bool = False, -): - """Evaluate FEM fields obtained from :meth:`struphy.post_processing.post_processing_tools.create_femfields`. - - Parameters - ---------- - params_in : ParamsIn - Simulation parameters. - - fields : dict - Obtained from struphy.diagnostics.post_processing.create_femfields. + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - celldivide : list of ints - Grid refinement in each eta direction. + celldivide : int + Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. physical : bool Wether to do post-processing into push-forwarded physical (xyz) components of fields. - Returns - ------- - point_data : dict - Nested dictionary holding values of FemFields on the grid as list of 3d xp.arrays: - point_data[name][t] contains the values of the field with name "name" in fields[t].keys() at time t. - - If physical is True, physical components of fields are saved. - Otherwise, logical components (differential n-forms) are saved. - - grids_log : 3-list - 1d logical grids in each eta-direction with Nel[i]*cell_divide[i] + 1 entries in each direction. - - grids_phy : 3-list - Mapped (physical) grids obtained by domain(*grids_log). - """ - - # get domain - domain = params_in.domain - - # create logical and physical grids - assert isinstance(fields, dict) - assert isinstance(celldivide, list) - assert len(celldivide) == 3 - - Nel = params_in.grid.Nel - - grids_log = [xp.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] - grids_phy = [ - domain(*grids_log)[0], - domain(*grids_log)[1], - domain(*grids_log)[2], - ] - - # evaluate fields at evaluation grid and push-forward - point_data = {} - for species, vars in fields[list(fields.keys())[0]].items(): - point_data[species] = {} - for name, field in vars.items(): - point_data[species][name] = {} - - print("\nEvaluating fields ...") - for t in tqdm(fields): - for species, vars in fields[t].items(): - for name, field in vars.items(): - assert isinstance(field, SplineFunction) - space_id = field.space_id - - # field evaluation - temp_val = field(*grids_log) - - point_data[species][name][t] = [] - - # scalar spaces - if isinstance(temp_val, xp.ndarray): - if physical: - # push-forward - if space_id == "H1": - point_data[species][name][t].append( - domain.push( - temp_val, - *grids_log, - kind="0", - ), - ) - elif space_id == "L2": - point_data[species][name][t].append( - domain.push( - temp_val, - *grids_log, - kind="3", - ), - ) - - else: - point_data[species][name][t].append(temp_val) - - # vector-valued spaces - else: - for j in range(3): - if physical: - # push-forward - if space_id == "Hcurl": - point_data[species][name][t].append( - domain.push( - temp_val, - *grids_log, - kind="1", - )[j], - ) - elif space_id == "Hdiv": - point_data[species][name][t].append( - domain.push( - temp_val, - *grids_log, - kind="2", - )[j], - ) - elif space_id == "H1vec": - point_data[species][name][t].append( - domain.push( - temp_val, - *grids_log, - kind="v", - )[j], - ) - - else: - point_data[species][name][t].append(temp_val[j]) - - return point_data, grids_log, grids_phy - - -def create_vtk( - path: str, - t_grid: xp.ndarray, - grids_phy: list, - point_data: dict, - *, - physical: bool = False, -): - """Creates structured virtual toolkit files (.vts) for Paraview from evaluated field data. - - Parameters - ---------- - path : str - Absolute path of where to store the .vts files. Will then be in path/vtk/step_.vts. - - t_grid : xp.ndarray - Time grid. - - grids_phy : 3-list - Mapped (physical) grids obtained from struphy.diagnostics.post_processing.eval_femfields. - - point_data : dict - Field data obtained from struphy.diagnostics.post_processing.eval_femfields. - - physical : bool - Wether to create vtk for push-forwarded physical (xyz) components of fields. - """ - - from pyevtk.hl import gridToVTK - - for species, vars in point_data.items(): - species_path = os.path.join(path, species, "vtk" + physical * "_phy") - try: - os.mkdir(species_path) - except: - shutil.rmtree(species_path) - os.mkdir(species_path) - - # time loop - nt = len(t_grid) - 1 - log_nt = int(xp.log10(nt)) + 1 - - print(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], - ) + guiding_center : bool + Compute guiding-center coordinates (only from Particles6D). + classify : bool + Classify guiding-center trajectories (passing, trapped or lost). -def post_process_markers( - path_in: str, - path_out: str, - species: str, - domain: Domain, - kind: str = "Particles6D", - step: int = 1, -): - """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation - and writes them to a .npy files and to .txt files. - Also saves the weights. + create_vtk : bool + Whether vtk files should be created. - * ``.npy`` files: - - * Particles6D: - - ===== ===== ============== ============= ====== - index | 0 | | 1 | 2 | 3 | | 4 | 5 | 6 | | 7 | - ===== ===== ============== ============= ====== - value ID position (xyz) velocities weight - ===== ===== ============== ============= ====== - - * Particles5D: - - ===== ===== ================ ========== ====== ====== ============ - index | 0 | | 1 | 2 | | 3 | 4 5 | 6 | 7 - ===== ===== ================ ========== ====== ====== ============ - value ID guiding_center v_parallel v_perp weight magn. moment - ===== ===== ================ ========== ====== ====== ============ - - * Particles3D: - - ===== ===== ============== ====== - index | 0 | | 1 | 2 | 3 | | 4 | - ===== ===== ============== ====== - value ID position (xyz) weight - ===== ===== ============== ====== - - * ``.txt`` files : - - ===== ===== ============== ====== - index | 0 | | 1 | 2 | 3 | | 4 | - ===== ===== ============== ====== - value ID position (xyz) weight - ===== ===== ============== ====== - - ``.txt`` files can be imported to e.g. Paraview, see `08 - Kinetic data `_ for details. - - Parameters - ---------- - path_in : str - Absolute path of simulation output folder. - - path_out : str - Absolute path of where to store the .txt files. Will be in path_out/orbits. - - species : str - Name of the species for which the post processing should be performed. - - domain : Domain - Domain object. - - kind : str - Name of the kinetic kind (Particles6D, Particles5D or Particles3D). - - step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + time_trace : bool + whether to plot the time trace of each measured region """ - # get # of MPI processes from meta.txt file - with open(os.path.join(path_in, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] - - # open hdf5 files and get names and number of saved markers of kinetic species - with h5py.File(os.path.join(path_in, "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 - - 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_out, "orbits") - - if "5D" in kind: - save_index = list(range(0, 6)) + [10] + [-1] - elif "6D" in kind or "SPH" in kind: - save_index = list(range(0, 7)) + [-1] + 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: - save_index = list(range(0, 4)) + [-1] + path_out = sim.env.path_out - try: - os.mkdir(path_orbits) - except: - shutil.rmtree(path_orbits) - os.mkdir(path_orbits) - - # temporary array - temp = xp.empty((n_markers, len(save_index)), order="C") - lost_particles_mask = xp.empty(n_markers, dtype=bool) - - print(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 i in range(int(nproc)): - with h5py.File(os.path.join(path_in, "data/", f"data_proc{i}.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] - - # 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 = domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) - temp[~lost_particles_mask, :3] = pos_phys - - # 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=", ") - - -def post_process_f( - path_in, - params_in: ParamsIn, - path_out, - species, - step=1, - compute_bckgr=False, -): - """Computes and saves distribution functions of saved binning data during a simulation. + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n*** Start post-processing of {path_out}:") - Parameters - ---------- - path_in : str - Absolute path of simulation output folder. - - params_in : ParamsIn - Simulation parameters. - - path_out : str - Absolute path of where to store the .txt files. Will be in path_out/orbits. - - species : str - Name of the species for which the post processing should be performed. - - step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - compute_bckgr : bool - Whether to compute the kinetic background values and add them to the binning data. - This is used if non-standard weights are binned. - """ - # get # of MPI processes from meta file - with open(os.path.join(path_in, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] - - # directory for .npy files - path_distr = os.path.join(path_out, "distribution_function") + # create post-processing folder + sim._path_pproc = os.path.join(path_out, "post_processing") try: - os.mkdir(path_distr) + os.mkdir(sim.path_pproc) except: - shutil.rmtree(path_distr) - os.mkdir(path_distr) - - print("Evaluation of distribution functions for " + str(species)) - - # Create grids - with h5py.File(os.path.join(path_in, "data/data_proc0.hdf5"), "r") as file_0: - for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - # create a new folder for each slice - path_slice = os.path.join(path_distr, slice_name) - os.mkdir(path_slice) - - # Find out all names of slices - slice_names = 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_names[n_gr] + ".npy", - ) - xp.save(grid_path, grid[:]) - - # compute distribution function - for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - # path to folder of slice - path_slice = os.path.join(path_distr, slice_name) - - # Find out all names of slices - slice_names = slice_name.split("_") - - # load full-f data - data = file_0["kinetic/" + species + "/f/" + slice_name][::step].copy() - data_df = file_0["kinetic/" + species + "/df/" + slice_name][::step].copy() - for rank in range(1, int(nproc)): - with h5py.File(os.path.join(path_in, "data/", f"data_proc{rank}.hdf5"), "r") as file: - data += file["kinetic/" + species + "/f/" + slice_name][::step] - data_df += file["kinetic/" + species + "/df/" + slice_name][::step] - - # 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) - - 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, - # ) - - spec: ParticleSpecies = getattr(params_in.model, species) - var: PICVariable = spec.var - f_bckgr: KineticBackground = var.backgrounds - - # 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_names: - 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_names: - 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])], - ) - - -def post_process_n_sph( - path_in, - params_in: ParamsIn, - path_out, - species, - step=1, -): - """Computes and saves the density n of saved sph data during a simulation. - - Parameters - ---------- - path_in : str - Absolute path of simulation output folder. - - params_in : ParamsIn - Simulation parameters. - - path_out : str - Absolute path of where to store the .txt files. Will be in path_out/orbits. - - species : str - Name of the species for which the post processing should be performed. - - step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - """ - # get model name and # of MPI processes from meta file - with open(os.path.join(path_in, "meta.yml"), "r") as f: - meta = yaml.load(f, Loader=yaml.FullLoader) - nproc = meta["MPI processes"] - - # directory for .npy files - path_n_sph = os.path.join(path_out, "n_sph") + shutil.rmtree(sim.path_pproc) + os.mkdir(sim.path_pproc) + + if time_trace: + from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration + + path_time_trace = os.path.join(path_out, "profiling_time_trace.pkl") + plot_time_vs_duration(path_time_trace, output_path=sim.path_pproc) + plot_gantt_chart_plotly(path_time_trace, output_path=sim.path_pproc) + return + + # check for fields and kinetic data in hdf5 file that need post processing + with h5py.File(os.path.join(path_out, "data/", "data_proc0.hdf5"), "r") as file: + # save time grid at which post-processing data is created + xp.save(os.path.join(sim.path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) + + if "feec" in file.keys(): + exist_fields = True + else: + exist_fields = False + + if "kinetic" in file.keys(): + sim.exist_particles = {"markers": False, "f": False, "n_sph": False} + sim.kinetic_species = [] + sim.kinetic_kinds = [] + for name in file["kinetic"].keys(): + sim.kinetic_species += [name] + sim.kinetic_kinds += [next(iter(sim.model.species[name].variables.values())).space] + + # check for saved markers + if "markers" in file["kinetic"][name]: + sim.exist_particles["markers"] = True + # check for saved distribution function + if "f" in file["kinetic"][name]: + sim.exist_particles["f"] = True + # check for saved sph density + if "n_sph" in file["kinetic"][name]: + sim.exist_particles["n_sph"] = True + else: + sim.exist_particles = None + + # post-processing + if exist_fields: + sim.pproc_fields(step=step, celldivide=celldivide, physical=physical, + create_vtk=create_vtk, verbose=verbose,) + if sim.exist_particles is not None: + sim.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) + + +def load_plotting_data(sim: "StruphySimulation" = None, path_out: str = None,) -> SimData: + """Load data generated during post-processing.""" + 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 + + path_pproc = os.path.join(path_out, "post_processing") + assert os.path.exists(path_pproc), f"Path {path_pproc} does not exist, run 'pproc' first?" + print("\n*** Loading post-processed simulation data:") + print(f"{path_out =}") + + simdata = SimData(path_out) + + # load time grid + simdata.t_grid = xp.load(os.path.join(path_pproc, "t_grid.npy")) + + # data paths + path_fields = os.path.join(path_pproc, "fields_data") + path_kinetic = os.path.join(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: + simdata.grids_log = pickle.load(f) + with open(os.path.join(path_fields, "grids_phy.bin"), "rb") as f: + simdata.grids_phy = pickle.load(f) + + # species folders + species = next(os.walk(path_fields))[1] + for spec in species: + simdata._spline_values[spec] = {} + # simdata.arrays[spec] = {} + path_spec = os.path.join(path_fields, spec) + wlk = os.walk(path_spec) + files = next(wlk)[2] + print(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: + simdata._spline_values[spec][var] = pickle.load(f) + # simdata.arrays[spec][var] = pickle.load(f) + + if os.path.exists(path_kinetic): + # species folders + species = next(os.walk(path_kinetic))[1] + print(f"{species =}") + 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 + for file in files: + # print(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: + simdata._orbits[spec] = xp.zeros((Nt, *tmp.shape), dtype=float) + simdata._orbits[spec][step] = tmp + n += 1 + + elif "distribution_function" in folder: + simdata._f[spec] = {} + slices = next(sub_wlk)[1] + # print(f"{slices = }") + for sli in slices: + simdata._f[spec][sli] = {} + # print(f"{sli = }") + files = next(sub_wlk)[2] + # print(f"{files = }") + for file in files: + name = file.split(".")[0] + tmp = xp.load(os.path.join(path_dat, sli, file)) + # print(f"{name = }") + simdata._f[spec][sli][name] = tmp + + elif "n_sph" in folder: + simdata._n_sph[spec] = {} + slices = next(sub_wlk)[1] + # print(f"{slices = }") + for sli in slices: + simdata._n_sph[spec][sli] = {} + # print(f"{sli = }") + files = next(sub_wlk)[2] + # print(f"{files = }") + for file in files: + name = file.split(".")[0] + tmp = xp.load(os.path.join(path_dat, sli, file)) + # print(f"{name = }") + simdata._n_sph[spec][sli][name] = tmp - try: - os.mkdir(path_n_sph) - except: - shutil.rmtree(path_n_sph) - os.mkdir(path_n_sph) - - print("Evaluation of sph density for " + str(species)) - - with h5py.File(os.path.join(path_in, "data/data_proc0.hdf5"), "r") as file_0: - # Create grids - for i, view in enumerate(file_0["kinetic/" + species + "/n_sph"]): - # create a new folder for each view - path_view = os.path.join(path_n_sph, view) - os.mkdir(path_view) - - # 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", - ) - - grid_path = os.path.join( - path_view, - "grid_n_sph.npy", - ) - xp.save(grid_path, (ee1, ee2, ee3)) - - # load n_sph data - data = file_0["kinetic/" + species + "/n_sph/" + view][::step].copy() - for rank in range(1, int(nproc)): - with h5py.File(os.path.join(path_in, "data/", f"data_proc{rank}.hdf5"), "r") as file: - data += file["kinetic/" + species + "/n_sph/" + view][::step] - - # save distribution functions - xp.save(os.path.join(path_view, "n_sph.npy"), data) + else: + print(f"{folder =}") + raise NotImplementedError + + print("\nThe following data has been loaded:") + print("\ngrids:") + print(f"{simdata.t_grid.shape =}") + if simdata.grids_log is not None: + print(f"{simdata.grids_log[0].shape =}") + print(f"{simdata.grids_log[1].shape =}") + print(f"{simdata.grids_log[2].shape =}") + if simdata.grids_phy is not None: + print(f"{simdata.grids_phy[0].shape =}") + print(f"{simdata.grids_phy[1].shape =}") + print(f"{simdata.grids_phy[2].shape =}") + print("\nsimdata.spline_values:") + for k, v in simdata.spline_values.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + print("\nsimdata.orbits:") + for k, v in simdata.orbits.items(): + print(f" {k}") + print("\nsimdata.f:") + for k, v in simdata.f.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + for kkk, vvv in vv.items(): + print(f" {kkk}") + print("\nsimdata.n_sph:") + for k, v in simdata.n_sph.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + for kkk, vvv in vv.items(): + print(f" {kkk}") + + return simdata \ No newline at end of file diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index 4c85d61dd..40b12a2a3 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -31,4 +31,9 @@ def run(self, verbose: bool = False): @abstractmethod def pproc(self, verbose: bool = False): """Post-process the simulation results.""" + pass + + @abstractmethod + def load_plotting_data(self, verbose: bool = False): + """Load post-processed data for visualization.""" pass \ No newline at end of file diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 10a1e9d12..3f849a826 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -37,6 +37,7 @@ from struphy.feec.psydac_derham import SplineFunction from struphy.post_processing.orbits import orbits_tools from struphy.kinetic_background.base import KineticBackground +from struphy.post_processing.post_processing_tools import pproc, load_plotting_data # third party imports from feectools.ddm.mpi import MockMPI @@ -458,99 +459,18 @@ def run(self, verbose: bool = False): ProfileManager.finalize() - def pproc( - self, - step: int = 1, + def pproc(self, step: int = 1, celldivide: int = 1, physical: bool = False, guiding_center: bool = False, classify: bool = False, create_vtk: bool = True, time_trace: bool = False, - verbose: bool = False, - ): - """Post-processing finished Struphy runs. - - Parameters - ---------- - step : int - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - celldivide : int - Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. - - physical : bool - Wether to do post-processing into push-forwarded physical (xyz) components of fields. - - guiding_center : bool - Compute guiding-center coordinates (only from Particles6D). - - classify : bool - Classify guiding-center trajectories (passing, trapped or lost). + verbose: bool = False,): + pproc(sim=self, step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, time_trace=time_trace, verbose=verbose,) - create_vtk : bool - Whether vtk files should be created. - - time_trace : bool - whether to plot the time trace of each measured region - """ - - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n*** Start post-processing of {self.env.path_out}:") - - # create post-processing folder - self._path_pproc = os.path.join(self.env.path_out, "post_processing") - - try: - os.mkdir(self.path_pproc) - except: - shutil.rmtree(self.path_pproc) - os.mkdir(self.path_pproc) - - if time_trace: - from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration - - path_time_trace = os.path.join(self.env.path_out, "profiling_time_trace.pkl") - plot_time_vs_duration(path_time_trace, output_path=self.path_pproc) - plot_gantt_chart_plotly(path_time_trace, output_path=self.path_pproc) - return - - # check for fields and kinetic data in hdf5 file that need post processing - with h5py.File(os.path.join(self.env.path_out, "data/", "data_proc0.hdf5"), "r") as file: - # 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()) - - if "feec" in file.keys(): - exist_fields = True - else: - 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 - - # post-processing - if exist_fields: - self.pproc_fields(step=step, celldivide=celldivide, physical=physical, - create_vtk=create_vtk, verbose=verbose,) - if self.exist_particles is not None: - self.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) + def load_plotting_data(self, verbose: bool = False): + load_plotting_data(sim=self) # --------------------- # Code specific methods From ad28994e0e8c800716a644000909ad612bbae1b3 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 12:35:15 +0100 Subject: [PATCH 18/80] add pproc and load_plotting_data to api --- src/struphy/__init__.py | 3 +++ src/struphy/api/post_processing/__init__.py | 3 +++ src/struphy/simulation/codes.py | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/struphy/api/post_processing/__init__.py diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index bdb9dea76..6ce07680b 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -18,6 +18,7 @@ WeightsParameters, ) from struphy.api.perturbations import perturbations +from struphy.api.post_processing import pproc, load_plotting_data __all__ = [ "domains", @@ -36,4 +37,6 @@ "DerhamOptions", "FieldsBackground", "ButcherTableau", + "pproc", + "load_plotting_data", ] diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py new file mode 100644 index 000000000..e8293c56e --- /dev/null +++ b/src/struphy/api/post_processing/__init__.py @@ -0,0 +1,3 @@ +from struphy.post_processing.post_processing_tools import pproc, load_plotting_data + +__all__ = ["pproc", "load_plotting_data",] \ No newline at end of file diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 3f849a826..0ec6c5136 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -6,6 +6,8 @@ equils, grids, DerhamOptions, + pproc, + load_plotting_data, ) # core imports @@ -37,7 +39,6 @@ from struphy.feec.psydac_derham import SplineFunction from struphy.post_processing.orbits import orbits_tools from struphy.kinetic_background.base import KineticBackground -from struphy.post_processing.post_processing_tools import pproc, load_plotting_data # third party imports from feectools.ddm.mpi import MockMPI From ae7c9294cd988d2aac68e6b11998aae6d04457e9 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 13:17:29 +0100 Subject: [PATCH 19/80] add verbose= to all methods in StruphySimulation --- src/struphy/__init__.py | 2 + src/struphy/models/base.py | 17 +- src/struphy/models/cold_plasma.py | 2 +- src/struphy/models/cold_plasma_vlasov.py | 2 +- .../deterministic_particle_diffusion.py | 2 +- .../drift_kinetic_electrostatic_adiabatic.py | 2 +- src/struphy/models/euler_sph.py | 2 +- src/struphy/models/guiding_center.py | 2 +- src/struphy/models/hasegawa_wakatani.py | 2 +- .../models/linear_extended_mh_duniform.py | 2 +- src/struphy/models/linear_mhd.py | 2 +- .../models/linear_mhd_driftkinetic_cc.py | 2 +- src/struphy/models/linear_mhd_vlasov_cc.py | 2 +- src/struphy/models/linear_mhd_vlasov_pc.py | 2 +- .../linear_vlasov_ampere_one_species.py | 2 +- src/struphy/models/maxwell.py | 2 +- src/struphy/models/poisson.py | 2 +- src/struphy/models/pressure_less_sph.py | 2 +- .../models/random_particle_diffusion.py | 2 +- src/struphy/models/shear_alfven.py | 2 +- .../models/two_fluid_quasi_neutral_toy.py | 2 +- .../models/variational_barotropic_fluid.py | 2 +- .../models/variational_compressible_fluid.py | 2 +- .../models/variational_pressureless_fluid.py | 2 +- .../models/visco_resistive_deltaf_mhd.py | 2 +- .../visco_resistive_deltaf_mhd_with_q.py | 2 +- .../models/visco_resistive_linear_mhd.py | 2 +- .../visco_resistive_linear_mhd_with_q.py | 2 +- src/struphy/models/visco_resistive_mhd.py | 2 +- .../models/visco_resistive_mhd_with_p.py | 2 +- .../models/visco_resistive_mhd_with_q.py | 2 +- src/struphy/models/viscous_euler_sph.py | 2 +- src/struphy/models/viscous_fluid.py | 2 +- src/struphy/models/vlasov.py | 2 +- .../models/vlasov_ampere_one_species.py | 2 +- .../models/vlasov_maxwell_one_species.py | 2 +- .../post_processing/post_processing_tools.py | 2 +- src/struphy/propagators/base.py | 2 +- .../propagators/propagators_coupling.py | 12 +- src/struphy/propagators/propagators_fields.py | 46 +++--- .../propagators/propagators_markers.py | 20 +-- src/struphy/simulation/codes.py | 146 +++++++++--------- 42 files changed, 151 insertions(+), 164 deletions(-) diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index 6ce07680b..8622766f8 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -19,6 +19,7 @@ ) from struphy.api.perturbations import perturbations from struphy.api.post_processing import pproc, load_plotting_data +from struphy.api.simulation import StruphySimulation __all__ = [ "domains", @@ -39,4 +40,5 @@ "ButcherTableau", "pproc", "load_plotting_data", + "StruphySimulation", ] diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 2969ef62b..d3ae7325b 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -57,7 +57,7 @@ def velocity_scale() -> str: Must be one of "alfvén", "cyclotron", "light" or "thermal".""" @abstractmethod - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Allocate helper arrays and perform initial solves if needed.""" @abstractmethod @@ -679,17 +679,4 @@ def scalar_quantities(self): # @property # def time_state(self): # """A pointer to the time variable of the dynamics ('t').""" - # return self._time_state - - @property - def verbose(self): - """Bool: show model info on screen.""" - try: - return self._verbose - except: - return False - - @verbose.setter - def verbose(self, new): - assert isinstance(new, bool) - self._verbose = new \ No newline at end of file + # return self._time_state \ No newline at end of file diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index 508051c85..b2890c542 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -111,7 +111,7 @@ def bulk_species(self): def velocity_scale(self): return "light" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._alpha = self.electrons.equation_params.alpha def update_scalar_quantities(self): diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index c1287d0cd..3dd7117d0 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -151,7 +151,7 @@ def bulk_species(self): def velocity_scale(self): return "light" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/models/deterministic_particle_diffusion.py b/src/struphy/models/deterministic_particle_diffusion.py index a32e62e70..da3bed1a5 100644 --- a/src/struphy/models/deterministic_particle_diffusion.py +++ b/src/struphy/models/deterministic_particle_diffusion.py @@ -84,7 +84,7 @@ def bulk_species(self): def velocity_scale(self): return None - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 6223c5691..4773948db 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -126,7 +126,7 @@ def bulk_species(self): def velocity_scale(self): return "thermal" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/models/euler_sph.py b/src/struphy/models/euler_sph.py index 3bc6ae037..e015ce83b 100644 --- a/src/struphy/models/euler_sph.py +++ b/src/struphy/models/euler_sph.py @@ -101,7 +101,7 @@ def bulk_species(self): def velocity_scale(self): return "thermal" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass # @staticmethod diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index 0ce35cb1a..e1a78f906 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -95,7 +95,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._en_fv = xp.empty(1, dtype=float) self._en_fB = xp.empty(1, dtype=float) self._en_tot = xp.empty(1, dtype=float) diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index ece72f4d6..9d4e54b0f 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -104,7 +104,7 @@ def update_rho(self): self._rho.update_ghost_regions() return self._rho - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index 8dcfaf58c..8af83a8a7 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -123,7 +123,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._b_eq = Propagator.projected_equil.b1 self._a_eq = Propagator.projected_equil.a1 self._p_eq = Propagator.projected_equil.p3 diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index 35037c485..02a9774c7 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -110,7 +110,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index e32ed6cef..3d0af26cb 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -189,7 +189,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index 1c40894fd..b163c71a1 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -156,7 +156,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index ab7f4ac55..8d1f00c98 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -170,7 +170,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index 22d0d26ed..57b5a3a69 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -164,7 +164,7 @@ def bulk_species(self): def velocity_scale(self): return "light" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index b1ca7d744..a266eb2e1 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -83,7 +83,7 @@ def bulk_species(self): def velocity_scale(self): return "light" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index 109f79235..388281145 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -85,7 +85,7 @@ def bulk_species(self): def velocity_scale(self): return None - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/models/pressure_less_sph.py b/src/struphy/models/pressure_less_sph.py index 6068fed6a..df2c8c7f9 100644 --- a/src/struphy/models/pressure_less_sph.py +++ b/src/struphy/models/pressure_less_sph.py @@ -84,7 +84,7 @@ def velocity_scale(self): # dct["projected_density"] = "L2" # return dct - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/random_particle_diffusion.py b/src/struphy/models/random_particle_diffusion.py index 78bcb4a3f..e0d21e998 100644 --- a/src/struphy/models/random_particle_diffusion.py +++ b/src/struphy/models/random_particle_diffusion.py @@ -83,7 +83,7 @@ def bulk_species(self): def velocity_scale(self): return None - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index 15a1839d8..63ce116bb 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -68,7 +68,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): # project background magnetic field (2-form) and pressure (3-form) self._b_eq = Propagator.projected_equil.b2 diff --git a/src/struphy/models/two_fluid_quasi_neutral_toy.py b/src/struphy/models/two_fluid_quasi_neutral_toy.py index 081d7d255..9c58f8f1f 100644 --- a/src/struphy/models/two_fluid_quasi_neutral_toy.py +++ b/src/struphy/models/two_fluid_quasi_neutral_toy.py @@ -108,7 +108,7 @@ def bulk_species(self): def velocity_scale(self): return "thermal" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index b9dfd5ab9..fdb6f6bb8 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -90,7 +90,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index dc0784726..ee777964a 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -102,7 +102,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index 92318fa51..cf7c21f7d 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -86,7 +86,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index 32a475dbb..3fd5a012a 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -149,7 +149,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index 3c1ef1e71..d47c4836b 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -146,7 +146,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index dabda13c1..d5f44e17e 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -146,7 +146,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index c1097c314..247bcaad0 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -143,7 +143,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index 996909d47..d1f001145 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -144,7 +144,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index 9956f3085..06e607792 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -144,7 +144,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 35b40e606..e406cd45c 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -146,7 +146,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index 1dc3a8e89..be418eeed 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -103,7 +103,7 @@ def bulk_species(self): def velocity_scale(self): return "thermal" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): pass def update_scalar_quantities(self): diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index 2144c8bc2..76c2de3ba 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -112,7 +112,7 @@ def bulk_species(self): def velocity_scale(self): return "alfvén" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): projV3 = L2Projector("L2", Propagator.mass_ops) def f(e1, e2, e3): diff --git a/src/struphy/models/vlasov.py b/src/struphy/models/vlasov.py index a24b2bd86..dde6a8f47 100644 --- a/src/struphy/models/vlasov.py +++ b/src/struphy/models/vlasov.py @@ -80,7 +80,7 @@ def bulk_species(self): def velocity_scale(self): return "cyclotron" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): self._tmp = xp.empty(1, dtype=float) def update_scalar_quantities(self): diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 01b244fa5..b3d2434d9 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -155,7 +155,7 @@ def bulk_species(self): def velocity_scale(self): return "light" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 8b1802291..214c1d3fc 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -166,7 +166,7 @@ def bulk_species(self): def velocity_scale(self): return "light" - def allocate_helpers(self): + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. :meta private: diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 1a80cbdec..c815009f9 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -288,7 +288,7 @@ def pproc(sim: "StruphySimulation" = None, sim.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) -def load_plotting_data(sim: "StruphySimulation" = None, path_out: str = None,) -> SimData: +def load_plotting_data(sim: "StruphySimulation" = None, path_out: str = None, verbose: bool = False,) -> SimData: """Load data generated during post-processing.""" 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." diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index e25a11a4a..d6d30d748 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -78,7 +78,7 @@ def options(self, new): self._options = new @abstractmethod - def allocate(self): + def allocate(self, verbose: bool = False): """Allocate all data/objects of the instance.""" @abstractmethod diff --git a/src/struphy/propagators/propagators_coupling.py b/src/struphy/propagators/propagators_coupling.py index 15d8fa692..19e190604 100644 --- a/src/struphy/propagators/propagators_coupling.py +++ b/src/struphy/propagators/propagators_coupling.py @@ -136,7 +136,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # scaling factors alpha = self.variables.ions.species.equation_params.alpha epsilon = self.variables.ions.species.equation_params.epsilon @@ -387,7 +387,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._alpha = self.options.alpha self._kappa = self.options.kappa @@ -622,7 +622,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.u_space == "H1vec": self._u_form_int = 0 else: @@ -943,7 +943,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._space_key_int = int(self.derham.space_to_form[self.options.u_space]) particles = self.variables.ions.particles @@ -1273,7 +1273,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.u_space == "H1vec": self._u_form_int = 0 else: @@ -1546,7 +1546,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.u_space == "H1vec": self._u_form_int = 0 else: diff --git a/src/struphy/propagators/propagators_fields.py b/src/struphy/propagators/propagators_fields.py index 028c3f5c8..23b186d33 100644 --- a/src/struphy/propagators/propagators_fields.py +++ b/src/struphy/propagators/propagators_fields.py @@ -144,7 +144,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # obtain needed matrices M1 = self.mass_ops.M1 M2 = self.mass_ops.M2 @@ -336,7 +336,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._info = self.options.solver_params.info self._alpha = self.variables.j.species.equation_params.alpha @@ -465,7 +465,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._info = self.options.solver_params.info epsilon = self.variables.j.species.equation_params.epsilon @@ -617,7 +617,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): u_space = self.options.u_space # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) @@ -818,7 +818,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._info = self.options.solver_params.info # define inverse of M1 @@ -970,7 +970,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.epsilon_from is None: epsilon = 1.0 else: @@ -1144,7 +1144,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): u_space = self.options.u_space self._info = self.options.solver_params.info @@ -1367,7 +1367,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._info = self.options.solver_params.info self._bc = self.derham.dirichlet_bc @@ -1737,7 +1737,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._space_key_int = int(self.derham.space_to_form[self.options.u_space]) particles = self.options.energetic_ions.particles @@ -2021,7 +2021,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._u_form = self.derham.space_to_form[self.options.u_space] # call operatros @@ -2388,7 +2388,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.u_space == "H1vec": self._u_form_int = 0 else: @@ -2584,7 +2584,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # always stabilize if xp.abs(self.options.sigma_1) < 1e-14: self.options.sigma_1 = 1e-14 @@ -2933,7 +2933,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._lin_solver = self.options.solver_params self._nonlin_solver = self.options.nonlin_solver @@ -3226,7 +3226,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.model == "full": assert self.options.s is not None @@ -3759,7 +3759,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.model == "full": assert self.options.rho is not None @@ -4163,7 +4163,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._model = self.options.model self._lin_solver = self.options.solver_params self._nonlin_solver = self.options.nonlin_solver @@ -4564,7 +4564,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._model = self.options.model self._lin_solver = self.options.solver_params self._nonlin_solver = self.options.nonlin_solver @@ -5162,7 +5162,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._model = self.options.model self._lin_solver = self.options.solver_params self._nonlin_solver = self.options.nonlin_solver @@ -5758,7 +5758,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._model = self.options.model self._gamma = self.options.gamma self._lin_solver = self.options.solver_params @@ -6516,7 +6516,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._model = self.options.model self._gamma = self.options.gamma self._eta = self.options.eta @@ -7215,7 +7215,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): if self.options.hfun == "cos": def hfun(t): @@ -7546,7 +7546,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # default phi if self.options.phi is None: self.options.phi = FEECVariable(space="H1") @@ -7828,7 +7828,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._info = self.options.solver_params.info if self.derham.comm is not None: self._rank = self.derham.comm.Get_rank() diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index 1e752e924..250a9a6ca 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -90,7 +90,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # get kernel kernel = Pyccelkernel(pusher_kernels.push_eta_stage) @@ -189,7 +189,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # scaling factor self._epsilon = self.variables.ions.species.equation_params.epsilon assert self.derham is not None, f"{self.__class__.__name__} needs a Derham object." @@ -322,7 +322,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # scaling factor self._epsilon = self.variables.var.species.equation_params.epsilon @@ -442,7 +442,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._u_tilde = self.options.u_tilde.spline.vector # get kernell: @@ -591,7 +591,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # scaling factor self._epsilon = self.variables.ions.species.equation_params.epsilon @@ -1032,7 +1032,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # scaling factor self._epsilon = self.variables.ions.species.equation_params.epsilon @@ -1441,7 +1441,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._bc_type = self.options.bc_type self._diffusion = self.options.diff_coeff @@ -1574,7 +1574,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): self._bc_type = self.options.bc_type self._diffusion = self.options.diff_coeff @@ -1703,7 +1703,7 @@ def options(self, new): self._options = new @profile - def allocate(self): + def allocate(self, verbose: bool = False): # init kernel for evaluating density etc. before each time step. init_kernel = eval_kernels_gc.sph_pressure_coeffs @@ -1840,7 +1840,7 @@ def options(self, new): self._options = new @profile - def allocate(self): # ersetzt init + def allocate(self, verbose: bool = False): # ersetzt init particles = self.variables.fluid.particles # init kernel for evaluating density etc. before each time step. diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 0ec6c5136..3820177dd 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -61,10 +61,6 @@ class StruphySimulation(Simulation): - # ---------------- - # Abstract methods - # ---------------- - def __init__(self, model: StruphyModel, params_path: str = None, @@ -85,7 +81,6 @@ def __init__(self, self.time_opts = time_opts self.grid = grid self.derham_opts = derham_opts - self.verbose = verbose # setup profiling agent ProfileManager.setup( @@ -120,7 +115,6 @@ def __init__(self, # check model assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" - model.verbose = verbose self.model_name = model.__class__.__name__ if self.rank == 0: @@ -234,18 +228,66 @@ def __init__(self, # domain and fluid background self._setup_domain_and_equil(domain, equil, verbose=verbose) + # ----------------- + # Common properties + # ----------------- + + @property + def domain(self): + """Domain object, see :ref:`avail_mappings`.""" + return self._domain + + @property + def equil(self): + """Fluid equilibrium object, see :ref:`fluid_equil`.""" + return self._equil + + @property + def derham(self): + """3d Derham sequence, see :ref:`derham`.""" + return self._derham + + @property + def mass_ops(self): + """WeighteMassOperators object, see :ref:`mass_ops`.""" + return self._mass_ops + + @property + def basis_ops(self): + """Basis projection operators.""" + return self._basis_ops + + @property + def projected_equil(self): + """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" + return self._projected_equil + + @property + def clone_config(self): + """Config in case domain clones are used.""" + return self._clone_config + + @clone_config.setter + def clone_config(self, new): + assert isinstance(new, CloneConfig) or new is None + self._clone_config = new + + # ---------------- + # Abstract methods + # ---------------- + def allocate(self, verbose: bool = False): # feec - self._allocate_feec(self.grid, self.derham_opts) + self._allocate_feec(self.grid, self.derham_opts, verbose=verbose) # allocate model variables self._allocate_variables(verbose=verbose) # pass info to propagators - self._allocate_propagators() + self._allocate_propagators(verbose=verbose) # allocate helper fields and perform initial solves if needed - self.model.allocate_helpers() + self.model.allocate_helpers(verbose=verbose) def save_geometry_and_equil_vtk(self, verbose: bool = False): # store geometry vtk @@ -294,16 +336,16 @@ def run(self, verbose: bool = False): if not self.env.restart: # equation paramters - self.allocate(verbose=self.verbose) + self.allocate(verbose=verbose) # output - self.initialize_data_storage(verbose=self.verbose) + self.initialize_data_storage(verbose=verbose) # peek view into geometry - self.save_geometry_and_equil_vtk(verbose=self.verbose) + self.save_geometry_and_equil_vtk(verbose=verbose) # plasma parameters - self.compute_plasma_params(verbose=self.verbose) + self.compute_plasma_params(verbose=verbose) # print info on mpi procs if self.rank < 32: @@ -471,7 +513,7 @@ def pproc(self, step: int = 1, pproc(sim=self, step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, time_trace=time_trace, verbose=verbose,) def load_plotting_data(self, verbose: bool = False): - load_plotting_data(sim=self) + load_plotting_data(sim=self, verbose=verbose) # --------------------- # Code specific methods @@ -589,7 +631,7 @@ def pproc_particles(self, step, ) - def compute_plasma_params(self, verbose=True): + def compute_plasma_params(self, verbose: bool=True): """ Compute and print volume averaged plasma parameters for each species of the model. @@ -771,7 +813,7 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo velocity_scale=self.velocity_scale, A_bulk=self.bulk_species.mass_number, Z_bulk=self.bulk_species.charge_number, - verbose=self.verbose, + verbose=verbose, ) else: @@ -880,7 +922,7 @@ def _setup_derham( return derham @profile - def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions): + def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions, verbose: bool = False): # create discrete derham sequence if self.clone_config is None: derham_comm = MPI.COMM_WORLD @@ -897,7 +939,7 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio derham_opts, comm=derham_comm, domain=self.domain, - verbose=self.verbose, + verbose=verbose, ) # create weighted mass and basis operators @@ -908,14 +950,14 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio self._mass_ops = WeightedMassOperators( self.derham, self.domain, - verbose=self.verbose, + verbose=verbose, eq_mhd=self.equil, ) self._basis_ops = BasisProjectionOperators( self.derham, self.domain, - verbose=self.verbose, + verbose=verbose, eq_mhd=self.equil, ) @@ -1021,7 +1063,7 @@ def _allocate_variables(self, verbose: bool = False): # self._pointer[key] = val["obj"].vector @profile - def _allocate_propagators(self): + def _allocate_propagators(self, verbose: bool = False): # set propagators base class attributes (then available to all propagators) Propagator.derham = self.derham Propagator.domain = self.domain @@ -1033,12 +1075,12 @@ def _allocate_propagators(self): assert len(self.model.prop_list) > 0, "No propagators in this model, check the model class." for prop in self.model.prop_list: assert isinstance(prop, Propagator) - prop.allocate() + prop.allocate(verbose=verbose) if MPI.COMM_WORLD.Get_rank() == 0: print(f"\nAllocated propagator '{prop.__class__.__name__}'.") @profile - def _initialize_hdf5_datasets(self, data: DataContainer, size): + def _initialize_hdf5_datasets(self, data: DataContainer, size, verbose: bool = False): """ Create datasets in hdf5 files according to model unknowns and diagnostics data. @@ -1203,7 +1245,7 @@ def _add_time_state(self, time_state): if isinstance(prop, Propagator): prop.add_time_state(time_state) - def _initialize_from_restart(self, data: DataContainer): + def _initialize_from_restart(self, data: DataContainer, verbose: bool = False): """ Set initial conditions for FE coefficients (electromagnetic and fluid) and markers from restart group in hdf5 files. @@ -1231,7 +1273,7 @@ def _initialize_from_restart(self, data: DataContainer): if MPI.COMM_WORLD.Get_size() > 1: subval.particles.mpi_sort_markers(do_test=True) - def _create_femfields(self, step: int = 1): + def _create_femfields(self, step: int = 1, verbose: bool = False): """Creates instances of :class:`~struphy.feec.psydac_derham.SplineFunction` from distributed Struphy data. Parameters @@ -1339,6 +1381,7 @@ def _eval_femfields( *, celldivide: list = [1, 1, 1], physical: bool = False, + verbose: bool = False, ): """Evaluate FEM fields obtained from :meth:`struphy.post_processing.post_processing_tools.create_femfields`. @@ -1472,6 +1515,7 @@ def _create_vtk( point_data: dict, *, physical: bool = False, + verbose: bool = False, ): """Creates structured virtual toolkit files (.vts) for Paraview from evaluated field data. @@ -1533,6 +1577,7 @@ def _post_process_markers( self, path_kinetic_species: str, step: int = 1, + verbose: bool = False, ): """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation and writes them to a .npy files and to .txt files. @@ -1674,6 +1719,7 @@ def _post_process_f( path_kinetic_species, step=1, compute_bckgr=False, + verbose: bool=False, ): """Computes and saves distribution functions of saved binning data during a simulation. @@ -1825,6 +1871,7 @@ def _post_process_n_sph( self, path_kinetic_species, step=1, + verbose: bool=False, ): """Computes and saves the density n of saved sph data during a simulation. @@ -1882,52 +1929,3 @@ def _post_process_n_sph( # save distribution functions xp.save(os.path.join(path_view, "n_sph.npy"), data) - - # ----------------- - # Common properties - # ----------------- - - @property - def clone_config(self): - """Config in case domain clones are used.""" - return self._clone_config - - @clone_config.setter - def clone_config(self, new): - assert isinstance(new, CloneConfig) or new is None - self._clone_config = new - - @property - def domain(self): - """Domain object, see :ref:`avail_mappings`.""" - return self._domain - - @property - def equil(self): - """Fluid equilibrium object, see :ref:`fluid_equil`.""" - return self._equil - - @property - def derham(self): - """3d Derham sequence, see :ref:`derham`.""" - return self._derham - - @property - def mass_ops(self): - """WeighteMassOperators object, see :ref:`mass_ops`.""" - return self._mass_ops - - @property - def basis_ops(self): - """Basis projection operators.""" - return self._basis_ops - - @property - def projected_equil(self): - """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" - return self._projected_equil - - @property - def path_pproc(self): - """Path to post-processing folder.""" - return self._path_pproc \ No newline at end of file From b0645f384733e8251e92878e02a9a1fc8d921017 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 13:35:00 +0100 Subject: [PATCH 20/80] remove path_pproc attibute from StruphySimulation --- .../tests/verification/test_verif_Maxwell.py | 25 +++++++++---------- .../post_processing/post_processing_tools.py | 18 ++++++------- src/struphy/simulation/codes.py | 10 ++++++-- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index adbb0c6cd..8c87e1fa8 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -7,9 +7,10 @@ from matplotlib import pyplot as plt from scipy.special import jv, yn -from struphy import BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations +from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations, StruphySimulation,) from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import Maxwell +from struphy.post_processing.post_processing_tools import SimData @pytest.mark.parametrize("algo", ["implicit", "explicit"]) @@ -47,12 +48,9 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=0, seed=123)) model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=1, seed=123)) - # start run - verbose = True - - main.run( - model, - params_path=None, + # instantiate Simulation and run + sim = StruphySimulation( + model=model, env=env, base_units=base_units, time_opts=time_opts, @@ -60,16 +58,17 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): equil=equil, grid=grid, derham_opts=derham_opts, - verbose=verbose, - ) + verbose=True,) + + sim.run(verbose=True) # post processing if MPI.COMM_WORLD.Get_rank() == 0: - main.pproc(env.path_out) + sim.pproc(verbose=True) # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - simdata = main.load_data(env.path_out) + simdata = sim.load_plotting_data(verbose=True) # fft E_of_t = simdata.spline_values["em_fields"]["e_field_log"] @@ -268,5 +267,5 @@ def to_E_theta(X, Y, E_x, E_y): if __name__ == "__main__": - # test_light_wave_1d(algo="explicit", do_plot=True) - test_coaxial(do_plot=True) + test_light_wave_1d(algo="explicit", do_plot=True) + # test_coaxial(do_plot=True) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index c815009f9..9de142033 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -186,7 +186,7 @@ def get_params_of_run(path: str) -> ParamsIn: ) -def pproc(sim: "StruphySimulation" = None, +def pproc(sim: StruphySimulation = None, path_out: str = None, step: int = 1, celldivide: int = 1, @@ -234,26 +234,26 @@ def pproc(sim: "StruphySimulation" = None, print(f"\n*** Start post-processing of {path_out}:") # create post-processing folder - sim._path_pproc = os.path.join(path_out, "post_processing") + path_pproc = os.path.join(path_out, "post_processing") try: - os.mkdir(sim.path_pproc) + os.mkdir(path_pproc) except: - shutil.rmtree(sim.path_pproc) - os.mkdir(sim.path_pproc) + shutil.rmtree(path_pproc) + os.mkdir(path_pproc) if time_trace: from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration path_time_trace = os.path.join(path_out, "profiling_time_trace.pkl") - plot_time_vs_duration(path_time_trace, output_path=sim.path_pproc) - plot_gantt_chart_plotly(path_time_trace, output_path=sim.path_pproc) + plot_time_vs_duration(path_time_trace, output_path=path_pproc) + plot_gantt_chart_plotly(path_time_trace, output_path=path_pproc) return # check for fields and kinetic data in hdf5 file that need post processing with h5py.File(os.path.join(path_out, "data/", "data_proc0.hdf5"), "r") as file: # save time grid at which post-processing data is created - xp.save(os.path.join(sim.path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) + xp.save(os.path.join(path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) if "feec" in file.keys(): exist_fields = True @@ -288,7 +288,7 @@ def pproc(sim: "StruphySimulation" = None, sim.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) -def load_plotting_data(sim: "StruphySimulation" = None, path_out: str = None, verbose: bool = False,) -> SimData: +def load_plotting_data(sim: StruphySimulation = None, path_out: str = None, verbose: bool = False,) -> SimData: """Load data generated during post-processing.""" 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." diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/codes.py index 3820177dd..b8cf1615c 100644 --- a/src/struphy/simulation/codes.py +++ b/src/struphy/simulation/codes.py @@ -39,6 +39,7 @@ from struphy.feec.psydac_derham import SplineFunction from struphy.post_processing.orbits import orbits_tools from struphy.kinetic_background.base import KineticBackground +from struphy.post_processing.post_processing_tools import SimData # third party imports from feectools.ddm.mpi import MockMPI @@ -272,6 +273,11 @@ def clone_config(self, new): assert isinstance(new, CloneConfig) or new is None self._clone_config = new + @property + def path_pproc(self): + """Path to post-processing folder.""" + return os.path.join(self.env.path_out, "post_processing") + # ---------------- # Abstract methods # ---------------- @@ -512,8 +518,8 @@ def pproc(self, step: int = 1, verbose: bool = False,): pproc(sim=self, step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, time_trace=time_trace, verbose=verbose,) - def load_plotting_data(self, verbose: bool = False): - load_plotting_data(sim=self, verbose=verbose) + def load_plotting_data(self, verbose: bool = False) -> SimData: + return load_plotting_data(sim=self, verbose=verbose) # --------------------- # Code specific methods From 4298cb66358e931efc812292d59a3d13725cdc77 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 15:12:22 +0100 Subject: [PATCH 21/80] rename codes.yp -> sim.py --- src/struphy/api/simulation/__init__.py | 3 +++ src/struphy/main.py | 2 +- src/struphy/models/tests/utils_testing.py | 2 +- src/struphy/post_processing/post_processing_tools.py | 2 +- src/struphy/simulation/{codes.py => sim.py} | 0 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 src/struphy/api/simulation/__init__.py rename src/struphy/simulation/{codes.py => sim.py} (100%) diff --git a/src/struphy/api/simulation/__init__.py b/src/struphy/api/simulation/__init__.py new file mode 100644 index 000000000..47e4bd190 --- /dev/null +++ b/src/struphy/api/simulation/__init__.py @@ -0,0 +1,3 @@ +from struphy.simulation.sim import StruphySimulation + +all = ["StruphySimulation",] \ No newline at end of file diff --git a/src/struphy/main.py b/src/struphy/main.py index 9a67868f0..c3b8def54 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -35,7 +35,7 @@ from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml -from struphy.simulation.codes import StruphySimulation +from struphy.simulation.sim import StruphySimulation @profile diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 765abced0..03fac909d 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -8,7 +8,7 @@ from struphy import EnvironmentOptions from struphy.io.setup import import_parameters_py from struphy.models.base import StruphyModel -from struphy.simulation.codes import StruphySimulation +from struphy.simulation.sim import StruphySimulation rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 9de142033..75e30ab76 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -24,7 +24,7 @@ from struphy.topology.grids import TensorProductGrid if TYPE_CHECKING: - from struphy.simulation.codes import StruphySimulation + from struphy.simulation.sim import StruphySimulation class ParamsIn: diff --git a/src/struphy/simulation/codes.py b/src/struphy/simulation/sim.py similarity index 100% rename from src/struphy/simulation/codes.py rename to src/struphy/simulation/sim.py From 957c6c4c3149e88f1dd158c6fe37ce51be6fc08a Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 12 Feb 2026 17:30:10 +0100 Subject: [PATCH 22/80] in the middle of reworking pproc --- src/struphy/__init__.py | 6 +- src/struphy/api/post_processing/__init__.py | 4 +- src/struphy/io/setup.py | 60 - src/struphy/main.py | 2 +- .../tests/verification/test_verif_Maxwell.py | 1 - .../post_processing/post_processing_tools.py | 1419 +++++++++++++---- src/struphy/simulation/sim.py | 900 +---------- 7 files changed, 1136 insertions(+), 1256 deletions(-) diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index 8622766f8..31317f159 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -18,7 +18,7 @@ WeightsParameters, ) from struphy.api.perturbations import perturbations -from struphy.api.post_processing import pproc, load_plotting_data +from struphy.api.post_processing import PostProcessor, PlottingData from struphy.api.simulation import StruphySimulation __all__ = [ @@ -38,7 +38,7 @@ "DerhamOptions", "FieldsBackground", "ButcherTableau", - "pproc", - "load_plotting_data", + "PostProcessor", + "PlottingData", "StruphySimulation", ] diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index e8293c56e..139295cf8 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.post_processing_tools import pproc, load_plotting_data +from struphy.post_processing.post_processing_tools import PostProcessor, PlottingData -__all__ = ["pproc", "load_plotting_data",] \ No newline at end of file +__all__ = ["PostProcessor", "PlottingData"] \ No newline at end of file diff --git a/src/struphy/io/setup.py b/src/struphy/io/setup.py index d8e2be106..af5e10f3d 100644 --- a/src/struphy/io/setup.py +++ b/src/struphy/io/setup.py @@ -23,66 +23,6 @@ def import_parameters_py(params_path: str) -> ModuleType: return params_in -def setup_folders( - path_out: str, - restart: bool, - verbose: bool = False, -): - """ - Setup output folders. - """ - if MPI.COMM_WORLD.Get_rank() == 0: - if verbose: - print("\nPREPARATION AND CLEAN-UP:") - - # create output folder if it does not exit - if not os.path.exists(path_out): - os.makedirs(path_out, exist_ok=True) - if verbose: - print("Created folder " + path_out) - - # create data folder in output folder if it does not exist - if not os.path.exists(os.path.join(path_out, "data/")): - os.mkdir(os.path.join(path_out, "data/")) - if verbose: - print("Created folder " + os.path.join(path_out, "data/")) - else: - # remove post_processing folder - folder = os.path.join(path_out, "post_processing") - if os.path.exists(folder): - shutil.rmtree(folder) - if verbose: - print("Removed existing folder " + folder) - - # remove meta file - file = os.path.join(path_out, "meta.txt") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove profiling file - file = os.path.join(path_out, "profile_tmp") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove .png files (if NOT a restart) - if not restart: - files = glob.glob(os.path.join(path_out, "*.png")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - def setup_derham( grid: TensorProductGrid, options: DerhamOptions, diff --git a/src/struphy/main.py b/src/struphy/main.py index c3b8def54..718a15cbb 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -23,7 +23,7 @@ from struphy.geometry.base import Domain from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, Time from struphy.io.output_handling import DataContainer -from struphy.io.setup import import_parameters_py, setup_folders +from struphy.io.setup import import_parameters_py from struphy.models.base import StruphyModel from struphy.models.species import Species from struphy.models.variables import FEECVariable diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 8c87e1fa8..4d5b20219 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -10,7 +10,6 @@ from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations, StruphySimulation,) from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import Maxwell -from struphy.post_processing.post_processing_tools import SimData @pytest.mark.parametrize("algo", ["implicit", "explicit"]) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 75e30ab76..250f2b512 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -8,6 +8,7 @@ from tqdm import tqdm from feectools.ddm.mpi import mpi as MPI from typing import TYPE_CHECKING +from pyevtk.hl import gridToVTK from struphy.feec.psydac_derham import SplineFunction from struphy.fields_background import equils @@ -20,27 +21,79 @@ 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 +from struphy.models.variables import PICVariable, SPHVariable from struphy.topology.grids import TensorProductGrid +from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration +from struphy.feec.psydac_derham import SplineFunction +from struphy.post_processing.orbits import orbits_tools +from struphy.kinetic_background.base import KineticBackground +from struphy.pic.base import Particles +from struphy.io.setup import setup_derham if TYPE_CHECKING: from struphy.simulation.sim import StruphySimulation class ParamsIn: - """Holds the input parameters of a Struphy simulation as attributes.""" + """Holds the input parameters of a Struphy simulation as attributes. + + Parameters + ---------- + path : str + Absolute path of simulation output folder. + """ def __init__( - self, - env: EnvironmentOptions = None, - base_units: BaseUnits = None, - time_opts: Time = None, - domain=None, - equil=None, - grid: TensorProductGrid = None, - derham_opts=None, - model: StruphyModel = None, - ): + self, + path: str, + ): + print(f"\nReading in paramters from {path} ... ") + + params_path = os.path.join(path, "parameters.py") + bin_path = os.path.join(path, "env.bin") + + if os.path.exists(params_path): + params_in = import_parameters_py(params_path) + env = params_in.env + base_units = params_in.base_units + 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 + + elif os.path.exists(bin_path): + with open(os.path.join(path, "env.bin"), "rb") as f: + env = pickle.load(f) + with open(os.path.join(path, "base_units.bin"), "rb") as f: + base_units = pickle.load(f) + with open(os.path.join(path, "time_opts.bin"), "rb") as f: + time_opts = pickle.load(f) + with open(os.path.join(path, "domain.bin"), "rb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + domain_dct = pickle.load(f) + domain: Domain = getattr(domains, domain_dct["name"])(**domain_dct["params"]) + with open(os.path.join(path, "equil.bin"), "rb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + equil_dct = pickle.load(f) + if equil_dct: + equil: FluidEquilibrium = getattr(equils, equil_dct["name"])(**equil_dct["params"]) + else: + equil = None + with open(os.path.join(path, "grid.bin"), "rb") as f: + grid = pickle.load(f) + with open(os.path.join(path, "derham_opts.bin"), "rb") as f: + derham_opts = pickle.load(f) + with open(os.path.join(path, "model_class.bin"), "rb") as f: + model_class: StruphyModel = pickle.load(f) + model = model_class() + + else: + raise FileNotFoundError(f"Neither of the paths {params_path} or {bin_path} exists.") + + print("done.") + self.env = env self.units = base_units self.time_opts = time_opts @@ -51,8 +104,912 @@ def __init__( self.model = model -class SimData: - """Holds post-processed Struphy data as attributes. +class PostProcessor: + """Post-processing finished Struphy runs, eithr from Simulation object or from output path. + + Parameters + ---------- + sim : StruphySimulation + StruphySimulation object of finished run. + + path_out: str + Path to Struphy output folder (in case no sim is given). + """ + + def __init__(self, + sim: "StruphySimulation" = None, + path_out: str = None, + ): + + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n*** Start post-processing of {path_out}:") + + # create post-processing folder + 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 + # with + # comm_size = + else: + path_out = sim.env.path_out + grid = sim.grid + derham_opts = sim.derham_opts + domain = sim.domain + comm_size = sim.comm_size + + self.path_out = path_out + self.path_pproc = os.path.join(path_out, "post_processing") + self.derham = setup_derham( + grid, + derham_opts, + comm=None, + domain=domain, + ) + self.comm_size = comm_size + + try: + os.mkdir(self.path_pproc) + except: + shutil.rmtree(self.path_pproc) + os.mkdir(self.path_pproc) + + def plot_time_traces(self): + path_time_trace = os.path.join(self.path_out, "profiling_time_trace.pkl") + plot_time_vs_duration(path_time_trace, output_path=self.path_pproc) + plot_gantt_chart_plotly(path_time_trace, output_path=self.path_pproc) + return + + def pproc(self, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = True, + verbose: bool = False, + ): + """Do post processing for folder path_out. + + Parameters + ---------- + step : int + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + + celldivide : int + Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. + + physical : bool + Wether to do post-processing into push-forwarded physical (xyz) components of fields. + + guiding_center : bool + Compute guiding-center coordinates (only from Particles6D). + + classify : bool + Classify guiding-center trajectories (passing, trapped or lost). + + create_vtk : bool + Whether vtk files should be created. + """ + + # 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: + # 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()) + + 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 = [] + for name in file["kinetic"].keys(): + self.kinetic_species += [name] + + # 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 + self.pproc_fields(step=step, celldivide=celldivide, physical=physical, + create_vtk=create_vtk, verbose=verbose,) + + # particle variables + self.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) + + def pproc_fields(self, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + create_vtk: bool = True, + verbose: bool = False, + ): + fields, t_grid = self._create_femfields(step=step) + point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) + if physical: + point_data_phy, _, _ = self._eval_femfields( + fields, + celldivide=[celldivide] * 3, + physical=True, + ) + + if not self.exist_fields: + print("No feec fields found in hdf5 file, skipping post-processing of fields.") + return + + # directory for field data + path_fields = os.path.join(self.path_pproc, "fields_data") + + try: + os.mkdir(path_fields) + except: + shutil.rmtree(path_fields) + os.mkdir(path_fields) + + # save data dicts for each field + for species, vars in point_data.items(): + 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) + + 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) + + # create vtk files + if create_vtk: + 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) + + def pproc_particles(self, + step: int = 1, + guiding_center: bool = False, + classify: bool = False, + verbose: bool = False,): + + if self.exist_particles is None: + print("No 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") + + try: + os.mkdir(path_kinetics) + except: + shutil.rmtree(path_kinetics) + os.mkdir(path_kinetics) + + # 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) + + try: + os.mkdir(path_kinetics_species) + except: + shutil.rmtree(path_kinetics_species) + os.mkdir(path_kinetics_species) + + # 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.path_out, 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, verbose: bool = False): + """Creates instances of :class:`~struphy.feec.psydac_derham.SplineFunction` from distributed Struphy data. + + Parameters + ---------- + step : int + Whether to create FEM fields at every time step (step=1, default), every second time step (step=2), etc. + + Returns + ------- + fields : dict + Nested dictionary holding :class:`~struphy.feec.psydac_derham.SplineFunction`: fields[t][name] contains the Field with the name "name" in the hdf5 file at time t. + + t_grid : xp.ndarray + Time grid. + """ + # 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 = {} + print("\nReading hdf5 data of following species:") + for species, dset in file["feec"].items(): + space_ids[species] = {} + print(f"{species}:") + for var, ddset in dset.items(): + space_ids[species][var] = ddset.attrs["space_id"] + print(f" {var}:", ddset) + + t_grid = file["time/value"][::step].copy() + + # create one FemField for each snapshot + fields = {} + for t in t_grid: + fields[t] = {} + for species, vars in space_ids.items(): + fields[t][species] = {} + for var, id in vars.items(): + fields[t][species][var] = self.derham.create_spline_function( + var, + id, + verbose=False, + ) + + # get hdf5 data + print("") + for rank in range(int(self.comm_size)): + # open hdf5 file + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + for species, dset in file["feec"].items(): + for var, ddset in tqdm(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) + + # loop over time + for n, t in enumerate(t_grid): + # scalar field + if gl_s.shape == (3,): + s1, s2, s3 = gl_s + e1, e2, e3 = gl_e + p1, p2, p3 = pads + + data = ddset[n * step, p1:-p1, p2:-p2, p3:-p3].copy() + + fields[t][species][var].vector[ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = data + # update after each data addition, can be made more efficient + fields[t][species][var].vector.update_ghost_regions() + + # 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] + + data = ddset[str(comp + 1)][ + n * step, + p1:-p1, + p2:-p2, + p3:-p3, + ].copy() + + fields[t][species][var].vector[comp][ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = data + # update after each data addition, can be made more efficient + fields[t][species][var].vector.update_ghost_regions() + + print("Creation of Struphy Fields done.") + + return fields, t_grid + + def _eval_femfields( + self, + fields: dict, + *, + celldivide: list = [1, 1, 1], + physical: bool = False, + verbose: bool = False, + ): + """Evaluate FEM fields obtained from :meth:`struphy.post_processing.post_processing_tools.create_femfields`. + + Parameters + ---------- + params_in : ParamsIn + Simulation parameters. + + fields : dict + Obtained from struphy.diagnostics.post_processing.create_femfields. + + celldivide : list of ints + Grid refinement in each eta direction. + + physical : bool + Wether to do post-processing into push-forwarded physical (xyz) components of fields. + + Returns + ------- + point_data : dict + Nested dictionary holding values of FemFields on the grid as list of 3d xp.arrays: + point_data[name][t] contains the values of the field with name "name" in fields[t].keys() at time t. + + If physical is True, physical components of fields are saved. + Otherwise, logical components (differential n-forms) are saved. + + grids_log : 3-list + 1d logical grids in each eta-direction with Nel[i]*cell_divide[i] + 1 entries in each direction. + + grids_phy : 3-list + Mapped (physical) grids obtained by domain(*grids_log). + """ + + # create logical and physical grids + assert isinstance(fields, dict) + assert isinstance(celldivide, list) + assert len(celldivide) == 3 + + Nel = self.grid.Nel + + grids_log = [xp.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] + grids_phy = [ + self.domain(*grids_log)[0], + self.domain(*grids_log)[1], + self.domain(*grids_log)[2], + ] + + # evaluate fields at evaluation grid and push-forward + point_data = {} + for species, vars in fields[list(fields.keys())[0]].items(): + point_data[species] = {} + for name, field in vars.items(): + point_data[species][name] = {} + + print("\nEvaluating fields ...") + for t in tqdm(fields): + for species, vars in fields[t].items(): + for name, field in vars.items(): + assert isinstance(field, SplineFunction) + space_id = field.space_id + + # field evaluation + temp_val = field(*grids_log) + + point_data[species][name][t] = [] + + # scalar spaces + if isinstance(temp_val, xp.ndarray): + if physical: + # push-forward + if space_id == "H1": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="0", + ), + ) + elif space_id == "L2": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="3", + ), + ) + + else: + point_data[species][name][t].append(temp_val) + + # vector-valued spaces + else: + for j in range(3): + if physical: + # push-forward + if space_id == "Hcurl": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="1", + )[j], + ) + elif space_id == "Hdiv": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="2", + )[j], + ) + elif space_id == "H1vec": + point_data[species][name][t].append( + self.domain.push( + temp_val, + *grids_log, + kind="v", + )[j], + ) + + else: + point_data[species][name][t].append(temp_val[j]) + + return point_data, grids_log, grids_phy + + def _create_vtk( + self, + path: str, + t_grid: xp.ndarray, + grids_phy: list, + point_data: dict, + *, + physical: bool = False, + verbose: bool = False, + ): + """Creates structured virtual toolkit files (.vts) for Paraview from evaluated field data. + + Parameters + ---------- + path : str + Absolute path of where to store the .vts files. Will then be in path/vtk/step_.vts. + + t_grid : xp.ndarray + Time grid. + + grids_phy : 3-list + Mapped (physical) grids obtained from struphy.diagnostics.post_processing.eval_femfields. + + point_data : dict + Field data obtained from struphy.diagnostics.post_processing.eval_femfields. + + physical : bool + Wether to create vtk for push-forwarded physical (xyz) components of fields. + """ + for species, vars in point_data.items(): + species_path = os.path.join(path, species, "vtk" + physical * "_phy") + try: + os.mkdir(species_path) + except: + shutil.rmtree(species_path) + os.mkdir(species_path) + + # time loop + nt = len(t_grid) - 1 + log_nt = int(xp.log10(nt)) + 1 + + print(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, + verbose: bool = False, + ): + """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation + and writes them to a .npy files and to .txt files. + Also saves the weights. + + * ``.npy`` files: + + * Particles6D: + + ===== ===== ============== ============= ====== + index | 0 | | 1 | 2 | 3 | | 4 | 5 | 6 | | 7 | + ===== ===== ============== ============= ====== + value ID position (xyz) velocities weight + ===== ===== ============== ============= ====== + + * Particles5D: + + ===== ===== ================ ========== ====== ====== ============ + index | 0 | | 1 | 2 | | 3 | 4 5 | 6 | 7 + ===== ===== ================ ========== ====== ====== ============ + value ID guiding_center v_parallel v_perp weight magn. moment + ===== ===== ================ ========== ====== ====== ============ + + * Particles3D: + + ===== ===== ============== ====== + index | 0 | | 1 | 2 | 3 | | 4 | + ===== ===== ============== ====== + value ID position (xyz) weight + ===== ===== ============== ====== + + * ``.txt`` files : + + ===== ===== ============== ====== + index | 0 | | 1 | 2 | 3 | | 4 | + ===== ===== ============== ====== + value ID position (xyz) weight + ===== ===== ============== ====== + + ``.txt`` files can be imported to e.g. Paraview, see `08 - Kinetic data `_ for details. + + Parameters + ---------- + path_kinetic_species : str + Path to kinetic data of considered species. + + step : int, optional + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + """ + + 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 varname, var in species_obj.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + obj: Particles = var.particles + vdim = obj.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] + + try: + os.mkdir(path_orbits) + except: + shutil.rmtree(path_orbits) + os.mkdir(path_orbits) + + # temporary array + temp = xp.empty((n_markers, len(save_index)), order="C") + lost_particles_mask = xp.empty(n_markers, dtype=bool) + + print(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 i in range(int(self.comm_size)): + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{i}.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] + + # 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 + + # 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=", ") + + def _post_process_f( + self, + path_kinetic_species, + step=1, + compute_bckgr=False, + verbose: bool=False, + ): + """Computes and saves distribution functions of saved binning data during a simulation. + + Parameters + ---------- + path_kinetic_species : str + Path to kinetic data of considered species. + + step : int, optional + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + + compute_bckgr : bool + Whether to compute the kinetic background values and add them to the binning data. + This is used if non-standard weights are binned. + """ + 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") + + try: + os.mkdir(path_distr) + except: + shutil.rmtree(path_distr) + os.mkdir(path_distr) + + print("Evaluation of distribution functions for " + str(species)) + + # Create 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"]): + # create a new folder for each slice + path_slice = os.path.join(path_distr, slice_name) + os.mkdir(path_slice) + + # Find out all names of slices + slice_names = 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_names[n_gr] + ".npy", + ) + xp.save(grid_path, grid[:]) + + # compute distribution function + for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): + # path to folder of slice + path_slice = os.path.join(path_distr, slice_name) + + # Find out all names of slices + slice_names = slice_name.split("_") + + # load full-f data + data = file_0["kinetic/" + species + "/f/" + slice_name][::step].copy() + data_df = file_0["kinetic/" + species + "/df/" + slice_name][::step].copy() + for rank in range(1, int(self.comm_size)): + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + data += file["kinetic/" + species + "/f/" + slice_name][::step] + data_df += file["kinetic/" + species + "/df/" + slice_name][::step] + + # 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) + + 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 + + # 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_names: + 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_names: + 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])], + ) + + def _post_process_n_sph( + self, + path_kinetic_species, + step=1, + verbose: bool=False, + ): + """Computes and saves the density n of saved sph data during a simulation. + + Parameters + ---------- + path_kinetic_species : str + Path to kinetic data of considered species. + + step : int, optional + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + """ + species = path_kinetic_species.split("/")[-1] + + # directory for .npy files + path_n_sph = os.path.join(path_kinetic_species, "n_sph") + + try: + os.mkdir(path_n_sph) + except: + shutil.rmtree(path_n_sph) + os.mkdir(path_n_sph) + + print("Evaluation of sph density for " + str(species)) + + with h5py.File(os.path.join(self.path_out, "data/data_proc0.hdf5"), "r") as file_0: + # Create grids + for i, view in enumerate(file_0["kinetic/" + species + "/n_sph"]): + # create a new folder for each view + path_view = os.path.join(path_n_sph, view) + os.mkdir(path_view) + + # 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", + ) + + grid_path = os.path.join( + path_view, + "grid_n_sph.npy", + ) + xp.save(grid_path, (ee1, ee2, ee3)) + + # load n_sph data + data = file_0["kinetic/" + species + "/n_sph/" + view][::step].copy() + for rank in range(1, int(self.comm_size)): + with h5py.File(os.path.join(self.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: + data += file["kinetic/" + species + "/n_sph/" + view][::step] + + # save distribution functions + xp.save(os.path.join(path_view, "n_sph.npy"), data) + + +class PlottingData: + """Holds post-processed plotting data as attributes. Parameters ---------- @@ -60,8 +1017,19 @@ class SimData: Absolute path of simulation output folder to post-process. """ - def __init__(self, path: str): - self.path = path + def __init__(self, sim: "StruphySimulation" = 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_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?" + print("\n*** Loading post-processed plotting data:") + print(f"{path_out =}") + + # dictionaries to hold data self._orbits = {} self._f = {} self._spline_values = {} @@ -116,316 +1084,133 @@ def Nattr(self) -> dict[str, int]: for spec, orbs in self.orbits.items(): self._Nattr[spec] = orbs.shape[2] return self._Nattr - - -def get_params_of_run(path: str) -> ParamsIn: - """Retrieve parameters of finished Struphy run. - - Parameters - ---------- - path : str - Absolute path of simulation output folder. - """ - - print(f"\nReading in paramters from {path} ... ") - - params_path = os.path.join(path, "parameters.py") - bin_path = os.path.join(path, "env.bin") - - if os.path.exists(params_path): - params_in = import_parameters_py(params_path) - env = params_in.env - base_units = params_in.base_units - 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 - - elif os.path.exists(bin_path): - with open(os.path.join(path, "env.bin"), "rb") as f: - env = pickle.load(f) - with open(os.path.join(path, "base_units.bin"), "rb") as f: - base_units = pickle.load(f) - with open(os.path.join(path, "time_opts.bin"), "rb") as f: - time_opts = pickle.load(f) - with open(os.path.join(path, "domain.bin"), "rb") as f: - # WORKAROUND: cannot pickle pyccelized classes at the moment - domain_dct = pickle.load(f) - domain: Domain = getattr(domains, domain_dct["name"])(**domain_dct["params"]) - with open(os.path.join(path, "equil.bin"), "rb") as f: - # WORKAROUND: cannot pickle pyccelized classes at the moment - equil_dct = pickle.load(f) - if equil_dct: - equil: FluidEquilibrium = getattr(equils, equil_dct["name"])(**equil_dct["params"]) - else: - equil = None - with open(os.path.join(path, "grid.bin"), "rb") as f: - grid = pickle.load(f) - with open(os.path.join(path, "derham_opts.bin"), "rb") as f: - derham_opts = pickle.load(f) - with open(os.path.join(path, "model_class.bin"), "rb") as f: - model_class: StruphyModel = pickle.load(f) - model = model_class() - - else: - raise FileNotFoundError(f"Neither of the paths {params_path} or {bin_path} exists.") - - print("done.") - - return ParamsIn( - env=env, - base_units=base_units, - time_opts=time_opts, - domain=domain, - equil=equil, - grid=grid, - derham_opts=derham_opts, - model=model, - ) - - -def pproc(sim: StruphySimulation = None, - path_out: str = None, - step: int = 1, - celldivide: int = 1, - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - create_vtk: bool = True, - time_trace: bool = False, - verbose: bool = False, - ): - """Post-processing finished Struphy runs. - - Parameters - ---------- - sim : StruphySimulation - StruphySimulation object of finished run. - step : int - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - celldivide : int - Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. + def load(self, verbose: bool = False): + """Load data generated during post-processing.""" - physical : bool - Wether to do post-processing into push-forwarded physical (xyz) components of fields. + # load time grid + self.t_grid = xp.load(os.path.join(self.path_pproc, "t_grid.npy")) - guiding_center : bool - Compute guiding-center coordinates (only from Particles6D). + # data paths + path_fields = os.path.join(self.path_pproc, "fields_data") + path_kinetic = os.path.join(self.path_pproc, "kinetic_data") - classify : bool - Classify guiding-center trajectories (passing, trapped or lost). + # 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) - create_vtk : bool - Whether vtk files should be created. + # species folders + species = next(os.walk(path_fields))[1] + for spec in species: + self._spline_values[spec] = {} + # self.arrays[spec] = {} + path_spec = os.path.join(path_fields, spec) + wlk = os.walk(path_spec) + files = next(wlk)[2] + print(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: + self._spline_values[spec][var] = pickle.load(f) + # self.arrays[spec][var] = pickle.load(f) - time_trace : bool - whether to plot the time trace of each measured region - """ - 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 + if os.path.exists(path_kinetic): + # species folders + species = next(os.walk(path_kinetic))[1] + print(f"{species =}") + 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 MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n*** Start post-processing of {path_out}:") - - # create post-processing folder - path_pproc = os.path.join(path_out, "post_processing") - - try: - os.mkdir(path_pproc) - except: - shutil.rmtree(path_pproc) - os.mkdir(path_pproc) - - if time_trace: - from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration + if "orbits" in folder: + files = next(sub_wlk)[2] + Nt = len(files) // 2 + n = 0 + for file in files: + # print(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: + self._orbits[spec] = xp.zeros((Nt, *tmp.shape), dtype=float) + self._orbits[spec][step] = tmp + n += 1 - path_time_trace = os.path.join(path_out, "profiling_time_trace.pkl") - plot_time_vs_duration(path_time_trace, output_path=path_pproc) - plot_gantt_chart_plotly(path_time_trace, output_path=path_pproc) - return + elif "distribution_function" in folder: + self._f[spec] = {} + slices = next(sub_wlk)[1] + # print(f"{slices = }") + for sli in slices: + self._f[spec][sli] = {} + # print(f"{sli = }") + files = next(sub_wlk)[2] + # print(f"{files = }") + for file in files: + name = file.split(".")[0] + tmp = xp.load(os.path.join(path_dat, sli, file)) + # print(f"{name = }") + self._f[spec][sli][name] = tmp - # check for fields and kinetic data in hdf5 file that need post processing - with h5py.File(os.path.join(path_out, "data/", "data_proc0.hdf5"), "r") as file: - # save time grid at which post-processing data is created - xp.save(os.path.join(path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) + elif "n_sph" in folder: + self._n_sph[spec] = {} + slices = next(sub_wlk)[1] + # print(f"{slices = }") + for sli in slices: + self._n_sph[spec][sli] = {} + # print(f"{sli = }") + files = next(sub_wlk)[2] + # print(f"{files = }") + for file in files: + name = file.split(".")[0] + tmp = xp.load(os.path.join(path_dat, sli, file)) + # print(f"{name = }") + self._n_sph[spec][sli][name] = tmp - if "feec" in file.keys(): - exist_fields = True - else: - exist_fields = False - - if "kinetic" in file.keys(): - sim.exist_particles = {"markers": False, "f": False, "n_sph": False} - sim.kinetic_species = [] - sim.kinetic_kinds = [] - for name in file["kinetic"].keys(): - sim.kinetic_species += [name] - sim.kinetic_kinds += [next(iter(sim.model.species[name].variables.values())).space] - - # check for saved markers - if "markers" in file["kinetic"][name]: - sim.exist_particles["markers"] = True - # check for saved distribution function - if "f" in file["kinetic"][name]: - sim.exist_particles["f"] = True - # check for saved sph density - if "n_sph" in file["kinetic"][name]: - sim.exist_particles["n_sph"] = True - else: - sim.exist_particles = None - - # post-processing - if exist_fields: - sim.pproc_fields(step=step, celldivide=celldivide, physical=physical, - create_vtk=create_vtk, verbose=verbose,) - if sim.exist_particles is not None: - sim.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) - + else: + print(f"{folder =}") + raise NotImplementedError -def load_plotting_data(sim: StruphySimulation = None, path_out: str = None, verbose: bool = False,) -> SimData: - """Load data generated during post-processing.""" - 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 - - path_pproc = os.path.join(path_out, "post_processing") - assert os.path.exists(path_pproc), f"Path {path_pproc} does not exist, run 'pproc' first?" - print("\n*** Loading post-processed simulation data:") - print(f"{path_out =}") - - simdata = SimData(path_out) - - # load time grid - simdata.t_grid = xp.load(os.path.join(path_pproc, "t_grid.npy")) - - # data paths - path_fields = os.path.join(path_pproc, "fields_data") - path_kinetic = os.path.join(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: - simdata.grids_log = pickle.load(f) - with open(os.path.join(path_fields, "grids_phy.bin"), "rb") as f: - simdata.grids_phy = pickle.load(f) - - # species folders - species = next(os.walk(path_fields))[1] - for spec in species: - simdata._spline_values[spec] = {} - # simdata.arrays[spec] = {} - path_spec = os.path.join(path_fields, spec) - wlk = os.walk(path_spec) - files = next(wlk)[2] - print(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: - simdata._spline_values[spec][var] = pickle.load(f) - # simdata.arrays[spec][var] = pickle.load(f) - - if os.path.exists(path_kinetic): - # species folders - species = next(os.walk(path_kinetic))[1] - print(f"{species =}") - 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 - for file in files: - # print(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: - simdata._orbits[spec] = xp.zeros((Nt, *tmp.shape), dtype=float) - simdata._orbits[spec][step] = tmp - n += 1 - - elif "distribution_function" in folder: - simdata._f[spec] = {} - slices = next(sub_wlk)[1] - # print(f"{slices = }") - for sli in slices: - simdata._f[spec][sli] = {} - # print(f"{sli = }") - files = next(sub_wlk)[2] - # print(f"{files = }") - for file in files: - name = file.split(".")[0] - tmp = xp.load(os.path.join(path_dat, sli, file)) - # print(f"{name = }") - simdata._f[spec][sli][name] = tmp - - elif "n_sph" in folder: - simdata._n_sph[spec] = {} - slices = next(sub_wlk)[1] - # print(f"{slices = }") - for sli in slices: - simdata._n_sph[spec][sli] = {} - # print(f"{sli = }") - files = next(sub_wlk)[2] - # print(f"{files = }") - for file in files: - name = file.split(".")[0] - tmp = xp.load(os.path.join(path_dat, sli, file)) - # print(f"{name = }") - simdata._n_sph[spec][sli][name] = tmp + print("\nThe following data has been loaded:") + print("\ngrids:") + print(f"{self.t_grid.shape =}") + if self.grids_log is not None: + print(f"{self.grids_log[0].shape =}") + print(f"{self.grids_log[1].shape =}") + print(f"{self.grids_log[2].shape =}") + if self.grids_phy is not None: + print(f"{self.grids_phy[0].shape =}") + print(f"{self.grids_phy[1].shape =}") + print(f"{self.grids_phy[2].shape =}") + print("\nself.spline_values:") + for k, v in self.spline_values.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + print("\nself.orbits:") + for k, v in self.orbits.items(): + print(f" {k}") + print("\nself.f:") + for k, v in self.f.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + for kkk, vvv in vv.items(): + print(f" {kkk}") + print("\nself.n_sph:") + for k, v in self.n_sph.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + for kkk, vvv in vv.items(): + print(f" {kkk}") - else: - print(f"{folder =}") - raise NotImplementedError - - print("\nThe following data has been loaded:") - print("\ngrids:") - print(f"{simdata.t_grid.shape =}") - if simdata.grids_log is not None: - print(f"{simdata.grids_log[0].shape =}") - print(f"{simdata.grids_log[1].shape =}") - print(f"{simdata.grids_log[2].shape =}") - if simdata.grids_phy is not None: - print(f"{simdata.grids_phy[0].shape =}") - print(f"{simdata.grids_phy[1].shape =}") - print(f"{simdata.grids_phy[2].shape =}") - print("\nsimdata.spline_values:") - for k, v in simdata.spline_values.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - print("\nsimdata.orbits:") - for k, v in simdata.orbits.items(): - print(f" {k}") - print("\nsimdata.f:") - for k, v in simdata.f.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - for kkk, vvv in vv.items(): - print(f" {kkk}") - print("\nsimdata.n_sph:") - for k, v in simdata.n_sph.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - for kkk, vvv in vv.items(): - print(f" {kkk}") - - return simdata \ No newline at end of file diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index b8cf1615c..bf98e68bd 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -6,8 +6,8 @@ equils, grids, DerhamOptions, - pproc, - load_plotting_data, + PostProcessor, + PlottingData, ) # core imports @@ -36,10 +36,7 @@ from struphy.pic.base import Particles from struphy.utils.utils import dict_to_yaml from struphy.simulation.base import Simulation -from struphy.feec.psydac_derham import SplineFunction -from struphy.post_processing.orbits import orbits_tools -from struphy.kinetic_background.base import KineticBackground -from struphy.post_processing.post_processing_tools import SimData +from struphy.io.setup import setup_derham # third party imports from feectools.ddm.mpi import MockMPI @@ -54,8 +51,6 @@ import cunumpy as xp import h5py import glob -import yaml -from tqdm import tqdm from line_profiler import profile from pyevtk.hl import gridToVTK @@ -228,6 +223,10 @@ def __init__(self, # domain and fluid background self._setup_domain_and_equil(domain, equil, verbose=verbose) + + # setup post processor and plotting + self._post_processor = PostProcessor(sim=self) + self._plotting_data = PlottingData(sim=self) # ----------------- # Common properties @@ -263,6 +262,16 @@ 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 + @property def clone_config(self): """Config in case domain clones are used.""" @@ -272,12 +281,7 @@ def clone_config(self): def clone_config(self, new): assert isinstance(new, CloneConfig) or new is None self._clone_config = new - - @property - def path_pproc(self): - """Path to post-processing folder.""" - return os.path.join(self.env.path_out, "post_processing") - + # ---------------- # Abstract methods # ---------------- @@ -516,127 +520,21 @@ def pproc(self, step: int = 1, create_vtk: bool = True, time_trace: bool = False, verbose: bool = False,): - pproc(sim=self, step=step, celldivide=celldivide, physical=physical, guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, time_trace=time_trace, verbose=verbose,) + if time_trace: + self.post_processor.plot_time_traces(verbose=verbose) + + self.post_processor.pproc(step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, verbose=verbose,) - def load_plotting_data(self, verbose: bool = False) -> SimData: - return load_plotting_data(sim=self, verbose=verbose) + def load_plotting_data(self, verbose: bool = False): + self.plotting_data.load(verbose=verbose) # --------------------- # Code specific methods # --------------------- - def pproc_fields(self, - step: int = 1, - celldivide: int = 1, - physical: bool = False, - create_vtk: bool = True, - verbose: bool = False, - ): - fields, t_grid = self._create_femfields(step=step) - point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) - if physical: - point_data_phy, _, _ = self._eval_femfields( - fields, - celldivide=[celldivide] * 3, - physical=True, - ) - - # directory for field data - path_fields = os.path.join(self.path_pproc, "fields_data") - - try: - os.mkdir(path_fields) - except: - shutil.rmtree(path_fields) - os.mkdir(path_fields) - - # save data dicts for each field - for species, vars in point_data.items(): - 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) - - 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) - - # create vtk files - if create_vtk: - 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) - - def pproc_particles(self, - step: int = 1, - guiding_center: bool = False, - classify: bool = False, - verbose: bool = False,): - # directory for kinetic data - path_kinetics = os.path.join(self.path_pproc, "kinetic_data") - - try: - os.mkdir(path_kinetics) - except: - shutil.rmtree(path_kinetics) - os.mkdir(path_kinetics) - - # 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) - - try: - os.mkdir(path_kinetics_species) - except: - shutil.rmtree(path_kinetics_species) - os.mkdir(path_kinetics_species) - - # 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.env.path_out, 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 compute_plasma_params(self, verbose: bool=True): """ Compute and print volume averaged plasma parameters for each species of the model. @@ -842,91 +740,6 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo else: print("None.") - def _setup_derham( - self, - grid: grids.TensorProductGrid, - options: DerhamOptions, - comm: MPI.Intracomm = None, - domain: Domain = None, - verbose=False, - ): - """ - Creates the 3d derham sequence for given grid parameters. - - Parameters - ---------- - grid : TensorProductGrid - The FEEC grid. - - comm: Intracomm - MPI communicator (sub_comm if clones are used). - - domain : Domain, optional - The Struphy domain object for evaluating the mapping F : [0, 1]^3 --> R^3 and the corresponding metric coefficients. - - verbose : bool - Show info on screen. - - Returns - ------- - derham : struphy.feec.psydac_derham.Derham - Discrete de Rham sequence on the logical unit cube. - """ - - from struphy.feec.psydac_derham import Derham - - # number of grid cells - Nel = grid.Nel - # mpi - mpi_dims_mask = grid.mpi_dims_mask - - # spline degrees - p = options.p - # spline types (clamped vs. periodic) - spl_kind = options.spl_kind - # boundary conditions (Homogeneous Dirichlet or None) - dirichlet_bc = options.dirichlet_bc - # Number of quadrature points per histopolation cell - nq_pr = options.nq_pr - # Number of quadrature points per grid cell for L^2 - nquads = options.nquads - # C^k smoothness at eta_1=0 for polar domains - polar_ck = options.polar_ck - # local commuting projectors - local_projectors = options.local_projectors - - derham = Derham( - Nel, - p, - spl_kind, - dirichlet_bc=dirichlet_bc, - nquads=nquads, - nq_pr=nq_pr, - comm=comm, - mpi_dims_mask=mpi_dims_mask, - with_projectors=True, - polar_ck=polar_ck, - domain=domain, - local_projectors=local_projectors, - ) - - if MPI.COMM_WORLD.Get_rank() == 0 and verbose: - print("\nDERHAM:") - print("number of elements:".ljust(25), Nel) - print("spline degrees:".ljust(25), p) - print("periodic bcs:".ljust(25), spl_kind) - print("hom. Dirichlet bc:".ljust(25), dirichlet_bc) - print("GL quad pts (L2):".ljust(25), nquads) - print("GL quad pts (hist):".ljust(25), nq_pr) - print( - "MPI proc. per dir.:".ljust(25), - derham.domain_decomposition.nprocs, - ) - print("use polar splines:".ljust(25), derham.polar_ck == 1) - print("domain on process 0:".ljust(25), derham.domain_array[0]) - - return derham - @profile def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions, verbose: bool = False): # create discrete derham sequence @@ -940,7 +753,7 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio print(f"\n{grid=}, {derham_opts=}: no Derham object set up.") self._derham = None else: - self._derham = self._setup_derham( + self._derham = setup_derham( grid, derham_opts, comm=derham_comm, @@ -1278,660 +1091,3 @@ def _initialize_from_restart(self, data: DataContainer, verbose: bool = False): if MPI.COMM_WORLD.Get_size() > 1: subval.particles.mpi_sort_markers(do_test=True) - - def _create_femfields(self, step: int = 1, verbose: bool = False): - """Creates instances of :class:`~struphy.feec.psydac_derham.SplineFunction` from distributed Struphy data. - - Parameters - ---------- - step : int - Whether to create FEM fields at every time step (step=1, default), every second time step (step=2), etc. - - Returns - ------- - fields : dict - Nested dictionary holding :class:`~struphy.feec.psydac_derham.SplineFunction`: fields[t][name] contains the Field with the name "name" in the hdf5 file at time t. - - t_grid : xp.ndarray - Time grid. - """ - # get fields names, space IDs and time grid from 0-th rank hdf5 file - with h5py.File(os.path.join(self.env.path_out, "data/", "data_proc0.hdf5"), "r") as file: - space_ids = {} - print("\nReading hdf5 data of following species:") - for species, dset in file["feec"].items(): - space_ids[species] = {} - print(f"{species}:") - for var, ddset in dset.items(): - space_ids[species][var] = ddset.attrs["space_id"] - print(f" {var}:", ddset) - - t_grid = file["time/value"][::step].copy() - - # create one FemField for each snapshot - fields = {} - for t in t_grid: - fields[t] = {} - for species, vars in space_ids.items(): - fields[t][species] = {} - for var, id in vars.items(): - fields[t][species][var] = self.derham.create_spline_function( - var, - id, - verbose=False, - ) - - # get hdf5 data - print("") - for rank in range(int(self.comm_size)): - # open hdf5 file - with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: - for species, dset in file["feec"].items(): - for var, ddset in tqdm(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) - - # loop over time - for n, t in enumerate(t_grid): - # scalar field - if gl_s.shape == (3,): - s1, s2, s3 = gl_s - e1, e2, e3 = gl_e - p1, p2, p3 = pads - - data = ddset[n * step, p1:-p1, p2:-p2, p3:-p3].copy() - - fields[t][species][var].vector[ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = data - # update after each data addition, can be made more efficient - fields[t][species][var].vector.update_ghost_regions() - - # 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] - - data = ddset[str(comp + 1)][ - n * step, - p1:-p1, - p2:-p2, - p3:-p3, - ].copy() - - fields[t][species][var].vector[comp][ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = data - # update after each data addition, can be made more efficient - fields[t][species][var].vector.update_ghost_regions() - - print("Creation of Struphy Fields done.") - - return fields, t_grid - - def _eval_femfields( - self, - fields: dict, - *, - celldivide: list = [1, 1, 1], - physical: bool = False, - verbose: bool = False, - ): - """Evaluate FEM fields obtained from :meth:`struphy.post_processing.post_processing_tools.create_femfields`. - - Parameters - ---------- - params_in : ParamsIn - Simulation parameters. - - fields : dict - Obtained from struphy.diagnostics.post_processing.create_femfields. - - celldivide : list of ints - Grid refinement in each eta direction. - - physical : bool - Wether to do post-processing into push-forwarded physical (xyz) components of fields. - - Returns - ------- - point_data : dict - Nested dictionary holding values of FemFields on the grid as list of 3d xp.arrays: - point_data[name][t] contains the values of the field with name "name" in fields[t].keys() at time t. - - If physical is True, physical components of fields are saved. - Otherwise, logical components (differential n-forms) are saved. - - grids_log : 3-list - 1d logical grids in each eta-direction with Nel[i]*cell_divide[i] + 1 entries in each direction. - - grids_phy : 3-list - Mapped (physical) grids obtained by domain(*grids_log). - """ - - # create logical and physical grids - assert isinstance(fields, dict) - assert isinstance(celldivide, list) - assert len(celldivide) == 3 - - Nel = self.grid.Nel - - grids_log = [xp.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] - grids_phy = [ - self.domain(*grids_log)[0], - self.domain(*grids_log)[1], - self.domain(*grids_log)[2], - ] - - # evaluate fields at evaluation grid and push-forward - point_data = {} - for species, vars in fields[list(fields.keys())[0]].items(): - point_data[species] = {} - for name, field in vars.items(): - point_data[species][name] = {} - - print("\nEvaluating fields ...") - for t in tqdm(fields): - for species, vars in fields[t].items(): - for name, field in vars.items(): - assert isinstance(field, SplineFunction) - space_id = field.space_id - - # field evaluation - temp_val = field(*grids_log) - - point_data[species][name][t] = [] - - # scalar spaces - if isinstance(temp_val, xp.ndarray): - if physical: - # push-forward - if space_id == "H1": - point_data[species][name][t].append( - self.domain.push( - temp_val, - *grids_log, - kind="0", - ), - ) - elif space_id == "L2": - point_data[species][name][t].append( - self.domain.push( - temp_val, - *grids_log, - kind="3", - ), - ) - - else: - point_data[species][name][t].append(temp_val) - - # vector-valued spaces - else: - for j in range(3): - if physical: - # push-forward - if space_id == "Hcurl": - point_data[species][name][t].append( - self.domain.push( - temp_val, - *grids_log, - kind="1", - )[j], - ) - elif space_id == "Hdiv": - point_data[species][name][t].append( - self.domain.push( - temp_val, - *grids_log, - kind="2", - )[j], - ) - elif space_id == "H1vec": - point_data[species][name][t].append( - self.domain.push( - temp_val, - *grids_log, - kind="v", - )[j], - ) - - else: - point_data[species][name][t].append(temp_val[j]) - - return point_data, grids_log, grids_phy - - def _create_vtk( - self, - path: str, - t_grid: xp.ndarray, - grids_phy: list, - point_data: dict, - *, - physical: bool = False, - verbose: bool = False, - ): - """Creates structured virtual toolkit files (.vts) for Paraview from evaluated field data. - - Parameters - ---------- - path : str - Absolute path of where to store the .vts files. Will then be in path/vtk/step_.vts. - - t_grid : xp.ndarray - Time grid. - - grids_phy : 3-list - Mapped (physical) grids obtained from struphy.diagnostics.post_processing.eval_femfields. - - point_data : dict - Field data obtained from struphy.diagnostics.post_processing.eval_femfields. - - physical : bool - Wether to create vtk for push-forwarded physical (xyz) components of fields. - """ - for species, vars in point_data.items(): - species_path = os.path.join(path, species, "vtk" + physical * "_phy") - try: - os.mkdir(species_path) - except: - shutil.rmtree(species_path) - os.mkdir(species_path) - - # time loop - nt = len(t_grid) - 1 - log_nt = int(xp.log10(nt)) + 1 - - print(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, - verbose: bool = False, - ): - """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation - and writes them to a .npy files and to .txt files. - Also saves the weights. - - * ``.npy`` files: - - * Particles6D: - - ===== ===== ============== ============= ====== - index | 0 | | 1 | 2 | 3 | | 4 | 5 | 6 | | 7 | - ===== ===== ============== ============= ====== - value ID position (xyz) velocities weight - ===== ===== ============== ============= ====== - - * Particles5D: - - ===== ===== ================ ========== ====== ====== ============ - index | 0 | | 1 | 2 | | 3 | 4 5 | 6 | 7 - ===== ===== ================ ========== ====== ====== ============ - value ID guiding_center v_parallel v_perp weight magn. moment - ===== ===== ================ ========== ====== ====== ============ - - * Particles3D: - - ===== ===== ============== ====== - index | 0 | | 1 | 2 | 3 | | 4 | - ===== ===== ============== ====== - value ID position (xyz) weight - ===== ===== ============== ====== - - * ``.txt`` files : - - ===== ===== ============== ====== - index | 0 | | 1 | 2 | 3 | | 4 | - ===== ===== ============== ====== - value ID position (xyz) weight - ===== ===== ============== ====== - - ``.txt`` files can be imported to e.g. Paraview, see `08 - Kinetic data `_ for details. - - Parameters - ---------- - path_kinetic_species : str - Path to kinetic data of considered species. - - step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - """ - - 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.env.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 varname, var in species_obj.variables.items(): - assert isinstance(var, PICVariable | SPHVariable) - obj: Particles = var.particles - vdim = obj.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] - - try: - os.mkdir(path_orbits) - except: - shutil.rmtree(path_orbits) - os.mkdir(path_orbits) - - # temporary array - temp = xp.empty((n_markers, len(save_index)), order="C") - lost_particles_mask = xp.empty(n_markers, dtype=bool) - - print(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 i in range(int(self.comm_size)): - with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{i}.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] - - # 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 - - # 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=", ") - - def _post_process_f( - self, - path_kinetic_species, - step=1, - compute_bckgr=False, - verbose: bool=False, - ): - """Computes and saves distribution functions of saved binning data during a simulation. - - Parameters - ---------- - path_kinetic_species : str - Path to kinetic data of considered species. - - step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - compute_bckgr : bool - Whether to compute the kinetic background values and add them to the binning data. - This is used if non-standard weights are binned. - """ - 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") - - try: - os.mkdir(path_distr) - except: - shutil.rmtree(path_distr) - os.mkdir(path_distr) - - print("Evaluation of distribution functions for " + str(species)) - - # Create grids - with h5py.File(os.path.join(self.env.path_out, "data/data_proc0.hdf5"), "r") as file_0: - for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - # create a new folder for each slice - path_slice = os.path.join(path_distr, slice_name) - os.mkdir(path_slice) - - # Find out all names of slices - slice_names = 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_names[n_gr] + ".npy", - ) - xp.save(grid_path, grid[:]) - - # compute distribution function - for slice_name in tqdm(file_0["kinetic/" + species + "/f"]): - # path to folder of slice - path_slice = os.path.join(path_distr, slice_name) - - # Find out all names of slices - slice_names = slice_name.split("_") - - # load full-f data - data = file_0["kinetic/" + species + "/f/" + slice_name][::step].copy() - data_df = file_0["kinetic/" + species + "/df/" + slice_name][::step].copy() - for rank in range(1, int(self.comm_size)): - with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: - data += file["kinetic/" + species + "/f/" + slice_name][::step] - data_df += file["kinetic/" + species + "/df/" + slice_name][::step] - - # 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) - - 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 - - # 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_names: - 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_names: - 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])], - ) - - def _post_process_n_sph( - self, - path_kinetic_species, - step=1, - verbose: bool=False, - ): - """Computes and saves the density n of saved sph data during a simulation. - - Parameters - ---------- - path_kinetic_species : str - Path to kinetic data of considered species. - - step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - """ - species = path_kinetic_species.split("/")[-1] - - # directory for .npy files - path_n_sph = os.path.join(path_kinetic_species, "n_sph") - - try: - os.mkdir(path_n_sph) - except: - shutil.rmtree(path_n_sph) - os.mkdir(path_n_sph) - - print("Evaluation of sph density for " + str(species)) - - with h5py.File(os.path.join(self.env.path_out, "data/data_proc0.hdf5"), "r") as file_0: - # Create grids - for i, view in enumerate(file_0["kinetic/" + species + "/n_sph"]): - # create a new folder for each view - path_view = os.path.join(path_n_sph, view) - os.mkdir(path_view) - - # 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", - ) - - grid_path = os.path.join( - path_view, - "grid_n_sph.npy", - ) - xp.save(grid_path, (ee1, ee2, ee3)) - - # load n_sph data - data = file_0["kinetic/" + species + "/n_sph/" + view][::step].copy() - for rank in range(1, int(self.comm_size)): - with h5py.File(os.path.join(self.env.path_out, "data/", f"data_proc{rank}.hdf5"), "r") as file: - data += file["kinetic/" + species + "/n_sph/" + view][::step] - - # save distribution functions - xp.save(os.path.join(path_view, "n_sph.npy"), data) From 5935970b670844f0ddff353eeec16fb69e826dd7 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 08:07:58 +0100 Subject: [PATCH 23/80] debug post_processing_tools --- .../post_processing/post_processing_tools.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 250f2b512..d0b652584 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -131,13 +131,16 @@ def __init__(self, grid = params_in.grid derham_opts = params_in.derham_opts domain = params_in.domain - # with - # comm_size = + model = params_in.model + with open(os.path.join(path_out, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + comm_size = meta["MPI processes"] else: path_out = sim.env.path_out grid = sim.grid derham_opts = sim.derham_opts domain = sim.domain + model = sim.model comm_size = sim.comm_size self.path_out = path_out @@ -148,6 +151,8 @@ def __init__(self, comm=None, domain=domain, ) + self.domain = domain + self.model = model self.comm_size = comm_size try: @@ -207,8 +212,10 @@ def pproc(self, 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]: @@ -236,6 +243,10 @@ def pproc_fields(self, create_vtk: bool = True, verbose: bool = False, ): + if not self.exist_fields: + print("No feec fields found in hdf5 file, skipping post-processing of fields.") + return + fields, t_grid = self._create_femfields(step=step) point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) if physical: @@ -245,10 +256,6 @@ def pproc_fields(self, physical=True, ) - if not self.exist_fields: - print("No feec fields found in hdf5 file, skipping post-processing of fields.") - return - # directory for field data path_fields = os.path.join(self.path_pproc, "fields_data") @@ -497,7 +504,7 @@ def _eval_femfields( assert isinstance(celldivide, list) assert len(celldivide) == 3 - Nel = self.grid.Nel + Nel = self.derham.Nel grids_log = [xp.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] grids_phy = [ From 94fd8ba5075ef64d009f948ad2bac33cb72a301d Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 08:08:35 +0100 Subject: [PATCH 24/80] formatting --- src/struphy/main.py | 5 +- src/struphy/models/base.py | 12 +- src/struphy/models/cold_plasma.py | 2 +- src/struphy/models/cold_plasma_vlasov.py | 4 +- .../drift_kinetic_electrostatic_adiabatic.py | 2 +- src/struphy/models/guiding_center.py | 2 +- src/struphy/models/hasegawa_wakatani.py | 7 +- .../models/linear_extended_mh_duniform.py | 2 +- src/struphy/models/linear_mhd.py | 2 +- .../models/linear_mhd_driftkinetic_cc.py | 3 +- src/struphy/models/linear_mhd_vlasov_cc.py | 2 +- src/struphy/models/linear_mhd_vlasov_pc.py | 2 +- .../linear_vlasov_ampere_one_species.py | 2 +- .../linear_vlasov_maxwell_one_species.py | 2 +- src/struphy/models/maxwell.py | 2 +- src/struphy/models/poisson.py | 2 +- src/struphy/models/shear_alfven.py | 2 +- src/struphy/models/tests/utils_testing.py | 2 +- .../tests/verification/test_verif_Maxwell.py | 16 +- .../models/variational_barotropic_fluid.py | 2 +- .../models/variational_compressible_fluid.py | 2 +- .../models/variational_pressureless_fluid.py | 2 +- .../models/visco_resistive_deltaf_mhd.py | 2 +- .../visco_resistive_deltaf_mhd_with_q.py | 2 +- .../models/visco_resistive_linear_mhd.py | 2 +- .../visco_resistive_linear_mhd_with_q.py | 2 +- src/struphy/models/visco_resistive_mhd.py | 2 +- .../models/visco_resistive_mhd_with_p.py | 2 +- .../models/visco_resistive_mhd_with_q.py | 2 +- src/struphy/models/viscous_fluid.py | 2 +- .../models/vlasov_ampere_one_species.py | 2 +- .../models/vlasov_maxwell_one_species.py | 2 +- .../post_processing/post_processing_tools.py | 177 ++++++++++-------- src/struphy/simulation/base.py | 11 +- src/struphy/simulation/sim.py | 145 +++++++------- 35 files changed, 236 insertions(+), 196 deletions(-) diff --git a/src/struphy/main.py b/src/struphy/main.py index 718a15cbb..966af9441 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -7,7 +7,6 @@ import sysconfig import time from typing import Optional, TypedDict -import pickle import cunumpy as xp import h5py @@ -30,13 +29,12 @@ from struphy.physics.physics import Units from struphy.pic.base import Particles from struphy.post_processing.orbits import orbits_tools +from struphy.simulation.sim import StruphySimulation from struphy.topology import grids from struphy.topology.grids import TensorProductGrid from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml -from struphy.simulation.sim import StruphySimulation - @profile def run( @@ -79,6 +77,7 @@ def run( sim.run(verbose=verbose) + class SimData: """Holds post-processed Struphy data as attributes. diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index d3ae7325b..f3370d5b6 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -67,11 +67,11 @@ def update_scalar_quantities(self): # -------------- # Common methods # -------------- - + @classmethod def name(cls) -> str: return cls.__name__ - + def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, compute=None, summands=None): """ Add a scalar to be saved during the simulation. @@ -103,7 +103,7 @@ def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, comp "compute": compute, "summands": summands, } - + def update_scalar(self, name, value=None): """Update a scalar during the simulation. @@ -118,7 +118,7 @@ def update_scalar(self, name, value=None): # Ensure the name is a string assert isinstance(name, str) - + scalars = self.scalar_quantities variable: PICVariable | SPHVariable = scalars[name]["variable"] @@ -581,7 +581,7 @@ def generate_default_parameter_file( return path - # ------------- + # ------------- # Model species # ------------- @@ -679,4 +679,4 @@ def scalar_quantities(self): # @property # def time_state(self): # """A pointer to the time variable of the dynamics ('t').""" - # return self._time_state \ No newline at end of file + # return self._time_state diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index b2890c542..a90ad1284 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -7,10 +7,10 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 3dd7117d0..7649c83a7 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -11,12 +11,12 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator from struphy.utils.pyccel import Pyccelkernel rank = MPI.COMM_WORLD.Get_rank() @@ -222,7 +222,7 @@ def update_scalar_quantities(self): # en_tot = en_w + en_e self.update_scalar("en_tot", en_E + self._tmp[0]) - + ## default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 4773948db..449aa8d9a 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -12,11 +12,11 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels_gc from struphy.pic.accumulation.particles_to_grid import AccumulatorVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator from struphy.utils.pyccel import Pyccelkernel rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index e1a78f906..be20f8129 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -7,10 +7,10 @@ ParticleSpecies, ) from struphy.models.variables import PICVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_markers, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index 9d4e54b0f..5dce8cb95 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -8,10 +8,10 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() @@ -103,7 +103,7 @@ def update_rho(self): self._rho = Propagator.mass_ops.M0.dot(omega, out=self._rho) self._rho.update_ghost_regions() return self._rho - + def allocate_helpers(self, verbose: bool = False): """Solve initial Poisson equation. @@ -111,7 +111,7 @@ def allocate_helpers(self, verbose: bool = False): """ self._rho: StencilVector = Propagator.derham.Vh["0"].zeros() self.update_rho() - + if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") @@ -120,7 +120,6 @@ def allocate_helpers(self, verbose: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") - def update_scalar_quantities(self): pass diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index 8af83a8a7..cfa7b310e 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -9,10 +9,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index 02a9774c7..8f1dc54f6 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -9,10 +9,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index 3d0af26cb..3c5264e2c 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -10,13 +10,12 @@ ) from struphy.models.variables import FEECVariable, PICVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) -from struphy.propagators.base import Propagator +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index b163c71a1..f37ab335b 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -10,12 +10,12 @@ ) from struphy.models.variables import FEECVariable, PICVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index 8d1f00c98..abee852de 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -10,12 +10,12 @@ ) from struphy.models.variables import FEECVariable, PICVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index 57b5a3a69..b82294852 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -11,12 +11,12 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator from struphy.utils.pyccel import Pyccelkernel rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index 9cb4898ff..70ac125dc 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -7,12 +7,12 @@ ParticleSpecies, ) from struphy.models.variables import FEECVariable, PICVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index a266eb2e1..182a29a84 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -6,10 +6,10 @@ FieldSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index 388281145..f42c6e4ac 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -6,10 +6,10 @@ FieldSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index 63ce116bb..c86f9ae7e 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -7,10 +7,10 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 03fac909d..841655bda 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -72,7 +72,7 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = # test restart env.restart = True time_opts.Tend += time_opts.dt - + sim.run(verbose=verbose) MPI.COMM_WORLD.Barrier() diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 4d5b20219..3c0106d44 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -7,7 +7,18 @@ from matplotlib import pyplot as plt from scipy.special import jv, yn -from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations, StruphySimulation,) +from struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + StruphySimulation, + Time, + domains, + equils, + grids, + main, + perturbations, +) from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import Maxwell @@ -57,7 +68,8 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): equil=equil, grid=grid, derham_opts=derham_opts, - verbose=True,) + verbose=True, + ) sim.run(verbose=True) diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index fdb6f6bb8..33c0d09e9 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -6,10 +6,10 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index ee777964a..1b8839f6a 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -11,10 +11,10 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index cf7c21f7d..a593e96d6 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -6,10 +6,10 @@ FluidSpecies, ) from struphy.models.variables import FEECVariable -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index 3fd5a012a..732de4115 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -11,10 +11,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index d47c4836b..74fc7299c 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -11,10 +11,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index d5f44e17e..a00e98d1e 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -11,10 +11,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index 247bcaad0..a23594b45 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -11,10 +11,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index d1f001145..b970de095 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -13,10 +13,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index 06e607792..b980b3fd5 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -11,10 +11,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index e406cd45c..6f9c6af24 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -11,10 +11,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index 76c2de3ba..d323731f7 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -12,10 +12,10 @@ ) from struphy.models.variables import FEECVariable from struphy.polar.basic import PolarVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_fields, ) +from struphy.propagators.base import Propagator rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index b3d2434d9..287c4bb3f 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -10,12 +10,12 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator from struphy.utils.pyccel import Pyccelkernel rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 214c1d3fc..369f73a33 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -10,12 +10,12 @@ from struphy.models.variables import FEECVariable, PICVariable from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector -from struphy.propagators.base import Propagator from struphy.propagators import ( propagators_coupling, propagators_fields, propagators_markers, ) +from struphy.propagators.base import Propagator from struphy.utils.pyccel import Pyccelkernel rank = MPI.COMM_WORLD.Get_rank() diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index d0b652584..93d53e172 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1,14 +1,14 @@ import os import pickle import shutil +from typing import TYPE_CHECKING import cunumpy as xp import h5py import yaml -from tqdm import tqdm -from feectools.ddm.mpi import mpi as MPI -from typing import TYPE_CHECKING +from feectools.ddm.mpi import mpi as MPI from pyevtk.hl import gridToVTK +from tqdm import tqdm from struphy.feec.psydac_derham import SplineFunction from struphy.fields_background import equils @@ -16,19 +16,16 @@ from struphy.geometry import domains from struphy.geometry.base import Domain from struphy.io.options import BaseUnits, EnvironmentOptions, Time -from struphy.io.setup import import_parameters_py +from struphy.io.setup import import_parameters_py, setup_derham 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.topology.grids import TensorProductGrid +from struphy.pic.base import Particles from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart_plotly, plot_time_vs_duration -from struphy.feec.psydac_derham import SplineFunction from struphy.post_processing.orbits import orbits_tools -from struphy.kinetic_background.base import KineticBackground -from struphy.pic.base import Particles -from struphy.io.setup import setup_derham +from struphy.topology.grids import TensorProductGrid if TYPE_CHECKING: from struphy.simulation.sim import StruphySimulation @@ -36,7 +33,7 @@ class ParamsIn: """Holds the input parameters of a Struphy simulation as attributes. - + Parameters ---------- path : str @@ -44,9 +41,9 @@ class ParamsIn: """ def __init__( - self, - path: str, - ): + self, + path: str, + ): print(f"\nReading in paramters from {path} ... ") params_path = os.path.join(path, "parameters.py") @@ -62,7 +59,7 @@ def __init__( grid = params_in.grid derham_opts = params_in.derham_opts model = params_in.model - + elif os.path.exists(bin_path): with open(os.path.join(path, "env.bin"), "rb") as f: env = pickle.load(f) @@ -93,7 +90,7 @@ def __init__( raise FileNotFoundError(f"Neither of the paths {params_path} or {bin_path} exists.") print("done.") - + self.env = env self.units = base_units self.time_opts = time_opts @@ -107,26 +104,29 @@ def __init__( class PostProcessor: """Post-processing finished Struphy runs, eithr from Simulation object or from output path. - Parameters - ---------- - sim : StruphySimulation - StruphySimulation object of finished run. - - path_out: str - Path to Struphy output folder (in case no sim is given). - """ + Parameters + ---------- + sim : StruphySimulation + StruphySimulation object of finished run. + + path_out: str + Path to Struphy output folder (in case no sim is given). + """ + + def __init__( + self, + sim: "StruphySimulation" = None, + path_out: str = None, + ): - def __init__(self, - sim: "StruphySimulation" = None, - path_out: str = None, - ): - if MPI.COMM_WORLD.Get_rank() == 0: print(f"\n*** Start post-processing of {path_out}:") - + # create post-processing folder 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." + 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 @@ -134,7 +134,7 @@ def __init__(self, model = params_in.model with open(os.path.join(path_out, "meta.yml"), "r") as f: meta = yaml.load(f, Loader=yaml.FullLoader) - comm_size = meta["MPI processes"] + comm_size = meta["MPI processes"] else: path_out = sim.env.path_out grid = sim.grid @@ -142,15 +142,15 @@ def __init__(self, domain = sim.domain model = sim.model comm_size = sim.comm_size - - self.path_out = path_out + + self.path_out = path_out self.path_pproc = os.path.join(path_out, "post_processing") self.derham = setup_derham( - grid, - derham_opts, - comm=None, - domain=domain, - ) + grid, + derham_opts, + comm=None, + domain=domain, + ) self.domain = domain self.model = model self.comm_size = comm_size @@ -167,15 +167,16 @@ def plot_time_traces(self): plot_gantt_chart_plotly(path_time_trace, output_path=self.path_pproc) return - def pproc(self, - step: int = 1, - celldivide: int = 1, - physical: bool = False, - guiding_center: bool = False, - classify: bool = False, - create_vtk: bool = True, - verbose: bool = False, - ): + def pproc( + self, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + create_vtk: bool = True, + verbose: bool = False, + ): """Do post processing for folder path_out. Parameters @@ -198,7 +199,7 @@ def pproc(self, create_vtk : bool Whether vtk files should be created. """ - + # 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: # save time grid at which post-processing data is created @@ -228,25 +229,36 @@ def pproc(self, self.exist_particles["n_sph"] = True else: self.exist_particles = None - + # feec variables - self.pproc_fields(step=step, celldivide=celldivide, physical=physical, - create_vtk=create_vtk, verbose=verbose,) - + self.pproc_fields( + step=step, + celldivide=celldivide, + physical=physical, + create_vtk=create_vtk, + verbose=verbose, + ) + # particle variables - self.pproc_particles(step=step, guiding_center=guiding_center, classify=classify, verbose=verbose,) - - def pproc_fields(self, - step: int = 1, - celldivide: int = 1, - physical: bool = False, - create_vtk: bool = True, - verbose: bool = False, - ): + self.pproc_particles( + step=step, + guiding_center=guiding_center, + classify=classify, + verbose=verbose, + ) + + def pproc_fields( + self, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + create_vtk: bool = True, + verbose: bool = False, + ): if not self.exist_fields: print("No feec fields found in hdf5 file, skipping post-processing of fields.") return - + fields, t_grid = self._create_femfields(step=step) point_data, grids_log, grids_phy = self._eval_femfields(fields, celldivide=[celldivide] * 3) if physical: @@ -255,7 +267,7 @@ def pproc_fields(self, celldivide=[celldivide] * 3, physical=True, ) - + # directory for field data path_fields = os.path.join(self.path_pproc, "fields_data") @@ -293,16 +305,18 @@ def pproc_fields(self, if physical: self._create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) - def pproc_particles(self, - step: int = 1, - guiding_center: bool = False, - classify: bool = False, - verbose: bool = False,): - + def pproc_particles( + self, + step: int = 1, + guiding_center: bool = False, + classify: bool = False, + verbose: bool = False, + ): + if self.exist_particles is None: print("No 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") @@ -711,15 +725,15 @@ def _post_process_markers( step : int, optional Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. """ - + 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 varname, var in species_obj.variables.items(): assert isinstance(var, PICVariable | SPHVariable) @@ -803,7 +817,7 @@ def _post_process_f( path_kinetic_species, step=1, compute_bckgr=False, - verbose: bool=False, + verbose: bool = False, ): """Computes and saves distribution functions of saved binning data during a simulation. @@ -889,7 +903,7 @@ def _post_process_f( # 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 @@ -955,7 +969,7 @@ def _post_process_n_sph( self, path_kinetic_species, step=1, - verbose: bool=False, + verbose: bool = False, ): """Computes and saves the density n of saved sph data during a simulation. @@ -1025,9 +1039,11 @@ class PlottingData: """ def __init__(self, sim: "StruphySimulation" = 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." + 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 @@ -1035,7 +1051,7 @@ def __init__(self, sim: "StruphySimulation" = None, path_out: str = None): assert os.path.exists(self.path_pproc), f"Path {self.path_pproc} does not exist, run 'pproc' first?" print("\n*** Loading post-processed plotting data:") print(f"{path_out =}") - + # dictionaries to hold data self._orbits = {} self._f = {} @@ -1091,7 +1107,7 @@ def Nattr(self) -> dict[str, int]: for spec, orbs in self.orbits.items(): self._Nattr[spec] = orbs.shape[2] return self._Nattr - + def load(self, verbose: bool = False): """Load data generated during post-processing.""" @@ -1220,4 +1236,3 @@ def load(self, verbose: bool = False): print(f" {kk}") for kkk, vvv in vv.items(): print(f" {kkk}") - diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index 40b12a2a3..adf5f6ed9 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -1,5 +1,6 @@ from abc import ABCMeta, abstractmethod + class Simulation(metaclass=ABCMeta): """Abstract base class for simulations.""" @@ -7,7 +8,7 @@ class Simulation(metaclass=ABCMeta): def __init__(self, **kwargs): """Initialize the simulation.""" pass - + @abstractmethod def allocate(self, verbose: bool = False): """Allocate the simulation variables in memory.""" @@ -17,7 +18,7 @@ def allocate(self, verbose: bool = False): def save_geometry_and_equil_vtk(self, verbose: bool = False): """Save geometry and equilibrium in VTK format.""" pass - + @abstractmethod def initialize_data_storage(self, verbose: bool = False): """Initialize the simulation data storage.""" @@ -27,13 +28,13 @@ def initialize_data_storage(self, verbose: bool = False): def run(self, verbose: bool = False): """Run the simulation.""" pass - + @abstractmethod def pproc(self, verbose: bool = False): """Post-process the simulation results.""" pass - + @abstractmethod def load_plotting_data(self, verbose: bool = False): """Load post-processed data for visualization.""" - pass \ No newline at end of file + pass diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index bf98e68bd..b6a01e8ce 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,21 +1,33 @@ # api imports -from struphy import (EnvironmentOptions, - BaseUnits, - Time, - domains, - equils, - grids, - DerhamOptions, - PostProcessor, - PlottingData, - ) +import glob +import os +import pickle +import shutil +import sysconfig +import time -# core imports -from struphy.models.base import StruphyModel -from struphy.geometry.base import Domain -from struphy.fields_background.base import (FluidEquilibrium, NumericalMHDequilibrium, FluidEquilibriumWithB,) -from struphy.physics.physics import Units -from struphy.utils.clone_config import CloneConfig +import cunumpy as xp +import h5py + +# third party imports +from feectools.ddm.mpi import MockMPI +from feectools.ddm.mpi import mpi as MPI +from feectools.linalg.stencil import StencilVector +from line_profiler import profile +from pyevtk.hl import gridToVTK +from scope_profiler import ProfileManager + +from struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + PlottingData, + PostProcessor, + Time, + domains, + equils, + grids, +) from struphy.feec.basis_projection_ops import BasisProjectionOperators from struphy.feec.mass import WeightedMassOperators from struphy.fields_background.base import ( @@ -29,46 +41,42 @@ ProjectedFluidEquilibriumWithB, ProjectedMHDequilibrium, ) -from struphy.propagators.base import Propagator -from struphy.models.species import (DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species,) -from struphy.models.variables import FEECVariable, PICVariable, SPHVariable +from struphy.geometry.base import Domain from struphy.io.output_handling import DataContainer -from struphy.pic.base import Particles -from struphy.utils.utils import dict_to_yaml -from struphy.simulation.base import Simulation from struphy.io.setup import setup_derham -# third party imports -from feectools.ddm.mpi import MockMPI -from feectools.ddm.mpi import mpi as MPI -from feectools.linalg.stencil import StencilVector -from scope_profiler import ProfileManager -import os -import time -import pickle -import shutil -import sysconfig -import cunumpy as xp -import h5py -import glob -from line_profiler import profile -from pyevtk.hl import gridToVTK +# core imports +from struphy.models.base import StruphyModel +from struphy.models.species import ( + DiagnosticSpecies, + FieldSpecies, + FluidSpecies, + ParticleSpecies, + Species, +) +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable +from struphy.physics.physics import Units +from struphy.pic.base import Particles +from struphy.propagators.base import Propagator +from struphy.simulation.base import Simulation +from struphy.utils.clone_config import CloneConfig +from struphy.utils.utils import dict_to_yaml class StruphySimulation(Simulation): - - def __init__(self, - model: StruphyModel, - params_path: str = None, - env: EnvironmentOptions = EnvironmentOptions(), - base_units: BaseUnits = BaseUnits(), - time_opts: Time = Time(), - domain: Domain = domains.Cuboid(), - equil: FluidEquilibrium = equils.HomogenSlab(), - grid: grids.TensorProductGrid = None, - derham_opts: DerhamOptions = None, - verbose: bool = False, - ): + def __init__( + self, + model: StruphyModel, + params_path: str = None, + env: EnvironmentOptions = EnvironmentOptions(), + base_units: BaseUnits = BaseUnits(), + time_opts: Time = Time(), + domain: Domain = domains.Cuboid(), + equil: FluidEquilibrium = equils.HomogenSlab(), + grid: grids.TensorProductGrid = None, + derham_opts: DerhamOptions = None, + verbose: bool = False, + ): self.model = model self.params_path = params_path @@ -223,7 +231,7 @@ def __init__(self, # domain and fluid background self._setup_domain_and_equil(domain, equil, verbose=verbose) - + # setup post processor and plotting self._post_processor = PostProcessor(sim=self) self._plotting_data = PlottingData(sim=self) @@ -261,17 +269,17 @@ def basis_ops(self): 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 - + @property def clone_config(self): """Config in case domain clones are used.""" @@ -281,7 +289,7 @@ def clone_config(self): def clone_config(self, new): assert isinstance(new, CloneConfig) or new is None self._clone_config = new - + # ---------------- # Abstract methods # ---------------- @@ -387,8 +395,7 @@ def run(self, verbose: bool = False): {self.time_state["value_sec"][0]=} {self.time_state["index"][0]=} !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -""" - ) +""") else: total_steps = str(int(round(Tend / dt))) @@ -512,21 +519,29 @@ def run(self, verbose: bool = False): ProfileManager.finalize() - def pproc(self, step: int = 1, + def pproc( + self, + step: int = 1, celldivide: int = 1, physical: bool = False, guiding_center: bool = False, classify: bool = False, create_vtk: bool = True, time_trace: bool = False, - verbose: bool = False,): + verbose: bool = False, + ): if time_trace: self.post_processor.plot_time_traces(verbose=verbose) - - self.post_processor.pproc(step=step, - celldivide=celldivide, - physical=physical, - guiding_center=guiding_center, classify=classify, create_vtk=create_vtk, verbose=verbose,) + + self.post_processor.pproc( + step=step, + celldivide=celldivide, + physical=physical, + guiding_center=guiding_center, + classify=classify, + create_vtk=create_vtk, + verbose=verbose, + ) def load_plotting_data(self, verbose: bool = False): self.plotting_data.load(verbose=verbose) @@ -535,7 +550,7 @@ def load_plotting_data(self, verbose: bool = False): # Code specific methods # --------------------- - def compute_plasma_params(self, verbose: bool=True): + def compute_plasma_params(self, verbose: bool = True): """ Compute and print volume averaged plasma parameters for each species of the model. From 75e42a268271c64265ff7937e4920e4a34a85bcb Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 08:50:12 +0100 Subject: [PATCH 25/80] return post_processor and plotting data objects in Simulation --- .../models/tests/verification/test_verif_Maxwell.py | 8 ++++---- src/struphy/simulation/sim.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 3c0106d44..a2908c41a 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -79,15 +79,15 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - simdata = sim.load_plotting_data(verbose=True) + sim.load_plotting_data(verbose=True) # fft - E_of_t = simdata.spline_values["em_fields"]["e_field_log"] + E_of_t = sim.plotting_data.spline_values["em_fields"]["e_field_log"] _1, _2, _3, coeffs = power_spectrum_2d( E_of_t, "e_field_log", - grids=simdata.grids_log, - grids_mapped=simdata.grids_phy, + grids=sim.plotting_data.grids_log, + grids_mapped=sim.plotting_data.grids_phy, component=0, slice_at=[0, 0, None], do_plot=do_plot, diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index b6a01e8ce..6426eed36 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -542,9 +542,11 @@ def pproc( create_vtk=create_vtk, verbose=verbose, ) + return self.post_processor def load_plotting_data(self, verbose: bool = False): self.plotting_data.load(verbose=verbose) + return self.plotting_data # --------------------- # Code specific methods From b023cdc941cc0023ddd8ffb103b24614447eba8f Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 14:51:06 +0100 Subject: [PATCH 26/80] add setters to Simulation parameters attributes; use __repr__ to display parameters --- src/struphy/fields_background/base.py | 6 + src/struphy/geometry/base.py | 6 + src/struphy/io/options.py | 20 +++ src/struphy/models/tests/utils_testing.py | 7 +- .../tests/verification/test_verif_Maxwell.py | 13 +- .../post_processing/post_processing_tools.py | 10 +- src/struphy/simulation/sim.py | 132 ++++++++++++++++-- src/struphy/topology/grids.py | 5 + 8 files changed, 171 insertions(+), 28 deletions(-) diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index 7ad6e3887..91a08de64 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -49,6 +49,12 @@ def domain(self): def domain(self, new_domain): assert isinstance(new_domain, Domain) or new_domain is None self._domain = new_domain + + def __repr__(self): + print(f"{self.__class__.__name__}") + for k, v in self.params.items(): + print(f"{k}:".ljust(20), v) + return "" ########################### # Vector-valued callables # diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index d2b21688e..91d0a172d 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -132,6 +132,12 @@ def __init__( self.cz.copy(), # make sure we don't have stride = 0 ) + def __repr__(self): + print(f"{self.__class__.__name__}") + for k, v in self.params.items(): + print(f"{k}:".ljust(20), v) + return "" + @property def kind_map(self) -> int: """Integer defining the mapping: diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index d6994a43d..17585173e 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -121,6 +121,11 @@ class Time: def __post_init__(self): check_option(self.split_algo, LiteralOptions.SplitAlgos) + + def __repr__(self): + for k, v in self.__dict__.items(): + print(f"{k}:".ljust(20), v) + return "" @dataclass @@ -148,6 +153,10 @@ class BaseUnits: n: float = 1.0 kBT: float = None + def __repr__(self): + for k, v in self.__dict__.items(): + print(f"{k}:".ljust(20), v) + return "" @dataclass class DerhamOptions: @@ -187,6 +196,11 @@ class DerhamOptions: def __post_init__(self): check_option(self.polar_ck, LiteralOptions.PolarRegularity) + + def __repr__(self): + for k, v in self.__dict__.items(): + print(f"{k}:".ljust(20), v) + return "" @dataclass @@ -212,6 +226,11 @@ class FieldsBackground: def __post_init__(self): check_option(self.type, LiteralOptions.BackgroundTypes) + + def __repr__(self): + for k, v in self.__dict__.items(): + print(f"{k}:".ljust(20), v) + return "" @dataclass @@ -266,3 +285,4 @@ def __post_init__(self): def __repr__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) + return "" diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index 841655bda..f642e7ea4 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -16,8 +16,6 @@ # generic function for calling model tests def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = True): model_name = model.name() - if rank == 0: - print(f"\n*** Testing '{model_name}':") # exceptions if model_name == "TwoFluidQuasiNeutralToy" and MPI.COMM_WORLD.Get_size() > 1: @@ -66,12 +64,15 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = derham_opts=derham_opts, verbose=verbose, ) + + sim.show_parameters() sim.run(verbose=verbose) - + # test restart env.restart = True time_opts.Tend += time_opts.dt + sim.show_parameters() sim.run(verbose=verbose) diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index a2908c41a..d32e08cd0 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -25,11 +25,15 @@ @pytest.mark.parametrize("algo", ["implicit", "explicit"]) def test_light_wave_1d(algo: str, do_plot: bool = False): - # environment options + # setup model + model = Maxwell() + sim = StruphySimulation(model=model) + + # set environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "Maxwell") - env = EnvironmentOptions(out_folders=out_folders, sim_folder="light_wave_1d") - + sim.env = EnvironmentOptions(out_folders=out_folders, sim_folder="light_wave_1d") + # units base_units = BaseUnits() @@ -48,9 +52,6 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # derham options derham_opts = DerhamOptions(p=(1, 1, 3)) - # light-weight model instance - model = Maxwell() - # propagator options model.propagators.maxwell.options = model.propagators.maxwell.Options(algo=algo) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 93d53e172..7304869be 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -119,9 +119,6 @@ def __init__( path_out: str = None, ): - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n*** Start post-processing of {path_out}:") - # create post-processing folder if sim is None: assert path_out is not None, ( @@ -199,6 +196,9 @@ def pproc( create_vtk : bool Whether vtk files should be created. """ + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n*** Start post-processing::") + print(f"Post-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: @@ -1049,8 +1049,6 @@ def __init__(self, sim: "StruphySimulation" = None, path_out: str = None): 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?" - print("\n*** Loading post-processed plotting data:") - print(f"{path_out =}") # dictionaries to hold data self._orbits = {} @@ -1110,6 +1108,8 @@ def Nattr(self) -> dict[str, int]: def load(self, verbose: bool = False): """Load data generated during post-processing.""" + print("\n*** Loading post-processed plotting data:") + print(f"Data path: {self.path_pproc}") # load time grid self.t_grid = xp.load(os.path.join(self.path_pproc, "t_grid.npy")) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 6426eed36..e8c9037bf 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -83,6 +83,7 @@ def __init__( self.env = env self.base_units = base_units self.time_opts = time_opts + self._setup_domain_and_equil(domain, equil, verbose=verbose) self.grid = grid self.derham_opts = derham_opts @@ -122,7 +123,7 @@ def __init__( self.model_name = model.__class__.__name__ if self.rank == 0: - print(f"\n*** Starting run for model '{self.model_name}':") + print(f"\n*** Instantiating simulation for model '{self.model_name}':") # meta-data path_out = env.path_out @@ -229,26 +230,107 @@ def __init__( ) model.setup_equation_params(units=self.units, verbose=verbose) - # domain and fluid background - self._setup_domain_and_equil(domain, equil, verbose=verbose) - # setup post processor and plotting self._post_processor = PostProcessor(sim=self) self._plotting_data = PlottingData(sim=self) - # ----------------- - # Common properties - # ----------------- + # ------------------------------------------------------ + # Common properties with setters (from input parameters) + # ------------------------------------------------------ + + @property + def model(self): + """StruphyModel object containing the PDE of the model.""" + return self._model + + @model.setter + def model(self, new): + assert isinstance(new, StruphyModel) + self._model = new + + @property + def params_path(self): + """Path to parameter file used for the run.""" + return self._params_path + + @params_path.setter + def params_path(self, new): + assert isinstance(new, str) or new is None + self._params_path = new + + @property + def env(self): + """EnvironmentOptions object containing options related to the environment of the run.""" + return self._env + + @env.setter + def env(self, new): + assert isinstance(new, EnvironmentOptions) + self._env = new + + @property + def base_units(self): + """BaseUnits object containing the four base units for the run.""" + return self._base_units + + @base_units.setter + def base_units(self, new): + assert isinstance(new, BaseUnits) + self._base_units = new + + @property + def time_opts(self): + """Time object containing time stepping parameters.""" + return self._time_opts + + @time_opts.setter + def time_opts(self, new): + assert isinstance(new, Time) + self._time_opts = new @property def domain(self): """Domain object, see :ref:`avail_mappings`.""" return self._domain + + @domain.setter + def domain(self, new): + assert isinstance(new, Domain) + self._domain = new @property def equil(self): """Fluid equilibrium object, see :ref:`fluid_equil`.""" return self._equil + + @equil.setter + def equil(self, new): + assert isinstance(new, FluidEquilibrium) or new is None + self._equil = new + + @property + def grid(self): + """Grid object, see :ref:`grids`.""" + return self._grid + + @grid.setter + def grid(self, new): + assert isinstance(new, grids.TensorProductGrid) or new is None + self._grid = new + + @property + def derham_opts(self): + """DerhamOptions object containing options for the setup of the 3d Derham sequence.""" + return self._derham_opts + + @derham_opts.setter + def derham_opts(self, new): + assert isinstance(new, DerhamOptions) or new is None + self._derham_opts = new + + # ----------------------------------------------------------------- + # Common properties (derived from the above properties, no setters) + # ----------------------------------------------------------------- @property def derham(self): @@ -293,6 +375,28 @@ def clone_config(self, new): # ---------------- # Abstract methods # ---------------- + def show_parameters(self): + if self.rank == 0: + print("\nSIMULATION PARAMETERS:") + print("Model:") + print(self.model) + print("\nParmameter file path:") + print(self.params_path) + print("\nEnvironment options:") + print(self.env) + print("Base units:") + print(self.base_units) + print("Time stepping options:") + print(self.time_opts) + print("Domain:") + print(self.domain) + print("Fluid equilibrium:") + print(self.equil) + print("Grid:") + print(self.grid) + print("Derham options:") + print(self.derham_opts) + print("") def allocate(self, verbose: bool = False): # feec @@ -723,24 +827,24 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" if equil is not None: if isinstance(equil, NumericalMHDequilibrium): - self._domain = equil.domain + self.domain = equil.domain else: - self._domain = domain + self.domain = domain equil.domain = domain if hasattr(equil, "units"): assert isinstance(equil.units, Units) equil.units.derive_units( - velocity_scale=self.velocity_scale, - A_bulk=self.bulk_species.mass_number, - Z_bulk=self.bulk_species.charge_number, + velocity_scale=self.model.velocity_scale, + A_bulk=self.model.bulk_species.mass_number, + Z_bulk=self.model.bulk_species.charge_number, verbose=verbose, ) else: - self._domain = domain + self.domain = domain - self._equil = equil + self.equil = equil if MPI.COMM_WORLD.Get_rank() == 0 and verbose: print("\nDOMAIN:") diff --git a/src/struphy/topology/grids.py b/src/struphy/topology/grids.py index b26887326..82e313a3a 100644 --- a/src/struphy/topology/grids.py +++ b/src/struphy/topology/grids.py @@ -19,3 +19,8 @@ class TensorProductGrid: Nel: tuple = (24, 10, 1) mpi_dims_mask: tuple = (True, True, True) + + def __repr__(self): + for k, v in self.__dict__.items(): + print(f"{k}:".ljust(20), v) + return "" From 37d1c56069585d88f609f81146768876584803fc Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 15:58:53 +0100 Subject: [PATCH 27/80] remove setters in SImulation --- src/struphy/models/base.py | 6 + src/struphy/models/species.py | 5 + .../tests/verification/test_verif_Maxwell.py | 14 +- src/struphy/models/variables.py | 3 + src/struphy/simulation/sim.py | 295 ++++++++---------- 5 files changed, 146 insertions(+), 177 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index f3370d5b6..dd4484dd4 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -67,6 +67,12 @@ def update_scalar_quantities(self): # -------------- # Common methods # -------------- + def __repr__(self): + print(self.__class__.__name__) + for k, v in self.species.items(): + print(f" {k}:") + print(v) + return "" @classmethod def name(cls) -> str: diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index cb1424db0..a2cec5080 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -22,6 +22,11 @@ class Species(metaclass=ABCMeta): def __init__(self): self.init_variables() + def __repr__(self): + for k, v in self.variables.items(): + print(f" {k}:".ljust(20), v) + return "" + # set species attribute for each variable def init_variables(self): self._variables = {} diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index d32e08cd0..74610bdc0 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -25,17 +25,13 @@ @pytest.mark.parametrize("algo", ["implicit", "explicit"]) def test_light_wave_1d(algo: str, do_plot: bool = False): - # setup model + # choose model model = Maxwell() - sim = StruphySimulation(model=model) # set environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "Maxwell") - sim.env = EnvironmentOptions(out_folders=out_folders, sim_folder="light_wave_1d") - - # units - base_units = BaseUnits() + env = EnvironmentOptions(out_folders=out_folders, sim_folder="light_wave_1d") # time stepping time_opts = Time(dt=0.05, Tend=50.0) @@ -59,19 +55,19 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=0, seed=123)) model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=1, seed=123)) - # instantiate Simulation and run + # instance of simulation sim = StruphySimulation( model=model, env=env, - base_units=base_units, time_opts=time_opts, domain=domain, equil=equil, grid=grid, derham_opts=derham_opts, - verbose=True, ) + # run + sim.show_parameters() sim.run(verbose=True) # post processing diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 02fb9971f..8474272d6 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -36,6 +36,9 @@ class Variable(metaclass=ABCMeta): @abstractmethod def allocate(self): """Alocate object and memory for variable.""" + + def __repr__(self): + return self.__class__.__name__ @property def backgrounds(self): diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index e8c9037bf..c9477da67 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -73,20 +73,20 @@ def __init__( time_opts: Time = Time(), domain: Domain = domains.Cuboid(), equil: FluidEquilibrium = equils.HomogenSlab(), - grid: grids.TensorProductGrid = None, - derham_opts: DerhamOptions = None, + grid: grids.TensorProductGrid = grids.TensorProductGrid(), + derham_opts: DerhamOptions = DerhamOptions(), verbose: bool = False, ): - self.model = model - self.params_path = params_path - self.env = env - self.base_units = base_units - self.time_opts = time_opts + self._model = model + self._params_path = params_path + self._env = env + self._base_units = base_units + self._time_opts = time_opts self._setup_domain_and_equil(domain, equil, verbose=verbose) - self.grid = grid - self.derham_opts = derham_opts - + self._grid = grid + self._derham_opts = derham_opts + # setup profiling agent ProfileManager.setup( profiling_activated=env.profiling_activated, @@ -113,6 +113,8 @@ def __init__( if self.rank == 0: print("") + if verbose: + self.show_parameters() # synchronize MPI processes to set same start time of simulation for all processes self.Barrier() @@ -123,7 +125,7 @@ def __init__( self.model_name = model.__class__.__name__ if self.rank == 0: - print(f"\n*** Instantiating simulation for model '{self.model_name}':") + print(f"*** Instantiating simulation for model '{self.model_name}':") # meta-data path_out = env.path_out @@ -234,153 +236,16 @@ def __init__( self._post_processor = PostProcessor(sim=self) self._plotting_data = PlottingData(sim=self) - # ------------------------------------------------------ - # Common properties with setters (from input parameters) - # ------------------------------------------------------ - - @property - def model(self): - """StruphyModel object containing the PDE of the model.""" - return self._model - - @model.setter - def model(self, new): - assert isinstance(new, StruphyModel) - self._model = new - - @property - def params_path(self): - """Path to parameter file used for the run.""" - return self._params_path - - @params_path.setter - def params_path(self, new): - assert isinstance(new, str) or new is None - self._params_path = new - - @property - def env(self): - """EnvironmentOptions object containing options related to the environment of the run.""" - return self._env - - @env.setter - def env(self, new): - assert isinstance(new, EnvironmentOptions) - self._env = new - - @property - def base_units(self): - """BaseUnits object containing the four base units for the run.""" - return self._base_units - - @base_units.setter - def base_units(self, new): - assert isinstance(new, BaseUnits) - self._base_units = new - - @property - def time_opts(self): - """Time object containing time stepping parameters.""" - return self._time_opts - - @time_opts.setter - def time_opts(self, new): - assert isinstance(new, Time) - self._time_opts = new - - @property - def domain(self): - """Domain object, see :ref:`avail_mappings`.""" - return self._domain - - @domain.setter - def domain(self, new): - assert isinstance(new, Domain) - self._domain = new - - @property - def equil(self): - """Fluid equilibrium object, see :ref:`fluid_equil`.""" - return self._equil - - @equil.setter - def equil(self, new): - assert isinstance(new, FluidEquilibrium) or new is None - self._equil = new - - @property - def grid(self): - """Grid object, see :ref:`grids`.""" - return self._grid - - @grid.setter - def grid(self, new): - assert isinstance(new, grids.TensorProductGrid) or new is None - self._grid = new - - @property - def derham_opts(self): - """DerhamOptions object containing options for the setup of the 3d Derham sequence.""" - return self._derham_opts - - @derham_opts.setter - def derham_opts(self, new): - assert isinstance(new, DerhamOptions) or new is None - self._derham_opts = new - - # ----------------------------------------------------------------- - # Common properties (derived from the above properties, no setters) - # ----------------------------------------------------------------- - - @property - def derham(self): - """3d Derham sequence, see :ref:`derham`.""" - return self._derham - - @property - def mass_ops(self): - """WeighteMassOperators object, see :ref:`mass_ops`.""" - return self._mass_ops - - @property - def basis_ops(self): - """Basis projection operators.""" - return self._basis_ops - - @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 - - @property - def clone_config(self): - """Config in case domain clones are used.""" - return self._clone_config - - @clone_config.setter - def clone_config(self, new): - assert isinstance(new, CloneConfig) or new is None - self._clone_config = new - # ---------------- # Abstract methods # ---------------- + def show_parameters(self): if self.rank == 0: print("\nSIMULATION PARAMETERS:") - print("Model:") + print("\nModel:") print(self.model) - print("\nParmameter file path:") + print("Parameter file path:") print(self.params_path) print("\nEnvironment options:") print(self.env) @@ -439,6 +304,7 @@ def save_geometry_and_equil_vtk(self, verbose: bool = False): def initialize_data_storage(self, verbose: bool = False): # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs + self.data = DataContainer(self.env.path_out, comm=self.comm) # time quantities (current time value, value in seconds and index) @@ -827,9 +693,9 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" if equil is not None: if isinstance(equil, NumericalMHDequilibrium): - self.domain = equil.domain + self._domain = equil.domain else: - self.domain = domain + self._domain = domain equil.domain = domain if hasattr(equil, "units"): @@ -842,24 +708,24 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo ) else: - self.domain = domain + self._domain = domain - self.equil = equil + self._equil = equil - if MPI.COMM_WORLD.Get_rank() == 0 and verbose: - print("\nDOMAIN:") - print("type:".ljust(25), self.domain.__class__.__name__) - for key, val in self.domain.params.items(): - if key not in {"cx", "cy", "cz"}: - print((key + ":").ljust(25), val) + # if MPI.COMM_WORLD.Get_rank() == 0 and verbose: + # print("\nDOMAIN:") + # print("type:".ljust(25), self.domain.__class__.__name__) + # for key, val in self.domain.params.items(): + # if key not in {"cx", "cy", "cz"}: + # print((key + ":").ljust(25), val) - print("\nFLUID BACKGROUND:") - if self.equil is not None: - print("type:".ljust(25), self.equil.__class__.__name__) - for key, val in self.equil.params.items(): - print((key + ":").ljust(25), val) - else: - print("None.") + # print("\nFLUID BACKGROUND:") + # if self.equil is not None: + # print("type:".ljust(25), self.equil.__class__.__name__) + # for key, val in self.equil.params.items(): + # print((key + ":").ljust(25), val) + # else: + # print("None.") @profile def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions, verbose: bool = False): @@ -1212,3 +1078,96 @@ def _initialize_from_restart(self, data: DataContainer, verbose: bool = False): if MPI.COMM_WORLD.Get_size() > 1: subval.particles.mpi_sort_markers(do_test=True) + + # ------------------------------------------------------ + # Common properties with setters (from input parameters) + # ------------------------------------------------------ + + @property + def model(self): + """StruphyModel object containing the PDE of the model.""" + return self._model + + @property + def params_path(self): + """Path to parameter file used for the run.""" + return self._params_path + + @property + def env(self): + """EnvironmentOptions object containing options related to the environment of the run.""" + return self._env + + @property + def base_units(self): + """BaseUnits object containing the four base units for the run.""" + return self._base_units + + @property + def time_opts(self): + """Time object containing time stepping parameters.""" + return self._time_opts + + @property + def domain(self): + """Domain object, see :ref:`avail_mappings`.""" + return self._domain + + @property + def equil(self): + """Fluid equilibrium object, see :ref:`fluid_equil`.""" + return self._equil + + @property + def grid(self): + """Grid object, see :ref:`grids`.""" + return self._grid + + @property + def derham_opts(self): + """DerhamOptions object containing options for the setup of the 3d Derham sequence.""" + return self._derham_opts + + # ----------------------------------------------------------------- + # Common properties (derived from the above properties, no setters) + # ----------------------------------------------------------------- + + @property + def derham(self): + """3d Derham sequence, see :ref:`derham`.""" + return self._derham + + @property + def mass_ops(self): + """WeighteMassOperators object, see :ref:`mass_ops`.""" + return self._mass_ops + + @property + def basis_ops(self): + """Basis projection operators.""" + return self._basis_ops + + @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 + + @property + def clone_config(self): + """Config in case domain clones are used.""" + return self._clone_config + + @clone_config.setter + def clone_config(self, new): + assert isinstance(new, CloneConfig) or new is None + self._clone_config = new \ No newline at end of file From 41f8c897a09b22ac62e81ed8fec51b35092462e4 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 16:38:57 +0100 Subject: [PATCH 28/80] make verification tests run --- src/struphy/io/options.py | 5 +- .../tests/verification/test_verif_EulerSPH.py | 33 ++++++------ .../verification/test_verif_LinearMHD.py | 46 +++++++--------- .../tests/verification/test_verif_Maxwell.py | 47 +++++++--------- .../tests/verification/test_verif_Poisson.py | 53 +++++++------------ .../test_verif_VlasovAmpereOneSpecies.py | 31 +++++------ src/struphy/models/variables.py | 8 ++- .../post_processing/post_processing_tools.py | 15 +++--- src/struphy/simulation/sim.py | 11 ++-- 9 files changed, 109 insertions(+), 140 deletions(-) diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index 17585173e..a78eb4759 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -154,8 +154,9 @@ class BaseUnits: kBT: float = None def __repr__(self): - for k, v in self.__dict__.items(): - print(f"{k}:".ljust(20), v) + units = ["m", "T", "1e20/m^3", "keV"] + for (k, v), unit in zip(self.__dict__.items(), units): + print(f"{k}:".ljust(20), v, unit) return "" @dataclass diff --git a/src/struphy/models/tests/verification/test_verif_EulerSPH.py b/src/struphy/models/tests/verification/test_verif_EulerSPH.py index 96b4d0f76..95b1bb9c9 100644 --- a/src/struphy/models/tests/verification/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_EulerSPH.py @@ -20,7 +20,9 @@ equils, main, perturbations, + StruphySimulation, ) +from struphy.models import EulerSPH @pytest.mark.parametrize("nx", [12, 24]) @@ -29,9 +31,9 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): """Verification test for SPH discretization of isthermal Euler equations. A standing sound wave with c_s=1 traveserses the domain once. """ - # import model - from struphy.models import EulerSPH - + # light-weight model instance + model = EulerSPH(with_B0=False) + # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "EulerSPH") @@ -47,18 +49,12 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): r1 = 2.5 domain = domains.Cuboid(r1=r1) - # fluid equilibrium (can be used as part of initial conditions) - equil = None - # grid grid = None # derham options derham_opts = None - # light-weight model instance - model = EulerSPH(with_B0=False) - # species parameters model.euler_fluid.set_phys_params() @@ -97,29 +93,30 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): perturbation = perturbations.ModesSin(ls=(1,), amps=(1.0e-2,)) model.euler_fluid.var.add_perturbation(del_n=perturbation) - # start run - main.run( - model, - params_path=None, + # instance of simulation + sim = StruphySimulation( + model=model, env=env, base_units=base_units, time_opts=time_opts, domain=domain, - equil=equil, grid=grid, derham_opts=derham_opts, verbose=True, ) + + # run + sim.run(verbose=True) # post processing if MPI.COMM_WORLD.Get_rank() == 0: - main.pproc(env.path_out) + sim.pproc(verbose=True) # diagnostics - simdata = main.load_data(env.path_out) + sim.load_plotting_data(env.path_out) - ee1, ee2, ee3 = simdata.n_sph["euler_fluid"]["view_0"]["grid_n_sph"] - n_sph = simdata.n_sph["euler_fluid"]["view_0"]["n_sph"] + ee1, ee2, ee3 = sim.plotting_data.n_sph["euler_fluid"]["view_0"]["grid_n_sph"] + n_sph = sim.plotting_data.n_sph["euler_fluid"]["view_0"]["n_sph"] if do_plot: ppb = 8 diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index ce797ac99..65f55dc83 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -5,25 +5,21 @@ import pytest from feectools.ddm.mpi import mpi as MPI -from struphy import BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations +from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations, StruphySimulation,) from struphy.diagnostics.diagn_tools import power_spectrum_2d +from struphy.models import LinearMHD @pytest.mark.parametrize("algo", ["implicit", "explicit"]) def test_slab_waves_1d(algo: str, do_plot: bool = False): - # import model, set verbosity - from struphy.models import LinearMHD - - verbose = True + # light-weight model instance + model = LinearMHD() # environment options test_folder = os.path.join(os.getcwd(), "verification_tests") out_folders = os.path.join(test_folder, "LinearMHD") env = EnvironmentOptions(out_folders=out_folders, sim_folder="slab_waves_1d") - # units - base_units = BaseUnits() - # time stepping time_opts = Time(dt=0.15, Tend=180.0) @@ -44,9 +40,6 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): # derham options derham_opts = DerhamOptions(p=(1, 1, 3)) - # light-weight model instance - model = LinearMHD() - # species parameters model.mhd.set_phys_params() @@ -59,30 +52,31 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): model.mhd.velocity.add_perturbation(perturbations.Noise(amp=0.1, comp=1, seed=123)) model.mhd.velocity.add_perturbation(perturbations.Noise(amp=0.1, comp=2, seed=123)) - # start run - main.run( - model, - params_path=None, + # instance of simulation + sim = StruphySimulation( + model=model, env=env, - base_units=base_units, time_opts=time_opts, domain=domain, - equil=equil, grid=grid, derham_opts=derham_opts, - verbose=verbose, + equil=equil, + verbose=True, ) + + # run + sim.run(verbose=True) # post processing if MPI.COMM_WORLD.Get_rank() == 0: - main.pproc(env.path_out) + sim.pproc(verbose=True) # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - simdata = main.load_data(env.path_out) + sim.load_plotting_data(verbose=True) # first fft - u_of_t = simdata.spline_values["mhd"]["velocity_log"] + u_of_t = sim.plotting_data.spline_values["mhd"]["velocity_log"] Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 @@ -92,8 +86,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): _1, _2, _3, coeffs = power_spectrum_2d( u_of_t, "velocity_log", - grids=simdata.grids_log, - grids_mapped=simdata.grids_phy, + grids=sim.plotting_data.grids_log, + grids_mapped=sim.plotting_data.grids_phy, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -112,13 +106,13 @@ 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 = simdata.spline_values["mhd"]["pressure_log"] + p_of_t = sim.plotting_data.spline_values["mhd"]["pressure_log"] _1, _2, _3, coeffs = power_spectrum_2d( p_of_t, "pressure_log", - grids=simdata.grids_log, - grids_mapped=simdata.grids_phy, + grids=sim.plotting_data.grids_log, + grids_mapped=sim.plotting_data.grids_phy, 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 74610bdc0..ffa9ad11a 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -25,7 +25,7 @@ @pytest.mark.parametrize("algo", ["implicit", "explicit"]) def test_light_wave_1d(algo: str, do_plot: bool = False): - # choose model + # light-weight model instance model = Maxwell() # set environment options @@ -39,9 +39,6 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # geometry domain = domains.Cuboid(r3=20.0) - # fluid equilibrium (can be used as part of initial conditions) - equil = None - # grid grid = grids.TensorProductGrid(Nel=(1, 1, 128)) @@ -61,13 +58,12 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): env=env, time_opts=time_opts, domain=domain, - equil=equil, grid=grid, derham_opts=derham_opts, + verbose=True, ) # run - sim.show_parameters() sim.run(verbose=True) # post processing @@ -103,19 +99,14 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): def test_coaxial(do_plot: bool = False): - # import model, set verbosity - from struphy.models import Maxwell - - verbose = True + # light-weight model instance + model = Maxwell() # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "Maxwell") env = EnvironmentOptions(out_folders=out_folders, sim_folder="coaxial") - # units - base_units = BaseUnits() - # time time_opts = Time(dt=0.05, Tend=10.0) @@ -138,9 +129,6 @@ def test_coaxial(do_plot: bool = False): dirichlet_bc=((True, True), (False, False), (False, False)), ) - # light-weight model instance - model = Maxwell() - # propagator options model.propagators.maxwell.options = model.propagators.maxwell.Options(algo="implicit") @@ -150,23 +138,24 @@ def test_coaxial(do_plot: bool = False): model.em_fields.e_field.add_perturbation(perturbations.CoaxialWaveguideElectric_theta(m=m, a1=a1, a2=a2)) model.em_fields.b_field.add_perturbation(perturbations.CoaxialWaveguideMagnetic(m=m, a1=a1, a2=a2)) - # start run - main.run( - model, - params_path=None, + # instance of simulation + sim = StruphySimulation( + model=model, env=env, - base_units=base_units, time_opts=time_opts, domain=domain, equil=equil, grid=grid, derham_opts=derham_opts, - verbose=verbose, + verbose=True, ) + + # run + sim.run(verbose=True) # post processing if MPI.COMM_WORLD.Get_rank() == 0: - main.pproc(env.path_out, physical=True) + sim.pproc(physical=True, verbose=True) # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: @@ -177,12 +166,12 @@ def test_coaxial(do_plot: bool = False): modes = m # load data - simdata = main.load_data(env.path_out) + sim.load_plotting_data(verbose=True) - t_grid = simdata.t_grid - grids_phy = simdata.grids_phy - e_field_phy = simdata.spline_values["em_fields"]["e_field_phy"] - b_field_phy = simdata.spline_values["em_fields"]["b_field_phy"] + t_grid = sim.plotting_data.t_grid + grids_phy = sim.plotting_data.grids_phy + e_field_phy = sim.plotting_data.spline_values["em_fields"]["e_field_phy"] + b_field_phy = sim.plotting_data.spline_values["em_fields"]["b_field_phy"] X = grids_phy[0][:, :, 0] Y = grids_phy[1][:, :, 0] @@ -276,4 +265,4 @@ def to_E_theta(X, Y, E_x, E_y): if __name__ == "__main__": test_light_wave_1d(algo="explicit", do_plot=True) - # test_coaxial(do_plot=True) + test_coaxial(do_plot=True) diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 5547183ed..a06e94bf4 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -5,33 +5,29 @@ from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt -from struphy import BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, grids, main, perturbations +from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, grids, main, perturbations, StruphySimulation,) from struphy.models import Poisson def test_poisson_1d(do_plot=False): + # light-weight model instance + model = Poisson() + # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "Poisson") env = EnvironmentOptions(out_folders=out_folders, sim_folder="time_source_1d") - # units - base_units = BaseUnits() - # time stepping time_opts = Time(dt=0.1, Tend=2.0) # geometry l1 = -5.0 r1 = 5.0 - l2 = -5.0 - r2 = 5.0 - l3 = -6.0 - r3 = 6.0 domain = domains.Cuboid( l1=l1, r1=r1, - ) # l2=l2, r2=r2, l3=l3, r3=r3) + ) # fluid equilibrium (can be used as part of initial conditions) equil = None @@ -39,12 +35,6 @@ def test_poisson_1d(do_plot=False): # grid grid = grids.TensorProductGrid(Nel=(48, 1, 1)) - # derham options - derham_opts = DerhamOptions() - - # light-weight model instance - model = Poisson() - # propagator options omega = 2 * xp.pi model.propagators.source.options = model.propagators.source.Options(omega=omega) @@ -63,36 +53,34 @@ def test_poisson_1d(do_plot=False): amp / (l * 2 * xp.pi / Lx) ** 2 * xp.cos(l * 2 * xp.pi / Lx * e1) * xp.cos(omega * t) ) - # start run - verbose = True - - main.run( - model, - params_path=None, + # instance of simulation + sim = StruphySimulation( + model=model, env=env, - base_units=base_units, time_opts=time_opts, domain=domain, equil=equil, grid=grid, - derham_opts=derham_opts, - verbose=verbose, + verbose=True, ) + # run + sim.run(verbose=True) + # post processing if MPI.COMM_WORLD.Get_rank() == 0: - main.pproc(env.path_out) + sim.pproc(verbose=True) # diagnostics if MPI.COMM_WORLD.Get_rank() == 0: - simdata = main.load_data(env.path_out) + sim.load_plotting_data(verbose=True) - phi = simdata.spline_values["em_fields"]["phi_log"] - source = simdata.spline_values["em_fields"]["source_log"] - x = simdata.grids_phy[0][:, 0, 0] - y = simdata.grids_phy[1][0, :, 0] - z = simdata.grids_phy[2][0, 0, :] - time = simdata.t_grid + phi = sim.plotting_data.spline_values["em_fields"]["phi_log"] + source = sim.plotting_data.spline_values["em_fields"]["source_log"] + x = sim.plotting_data.grids_phy[0][:, 0, 0] + y = sim.plotting_data.grids_phy[1][0, :, 0] + z = sim.plotting_data.grids_phy[2][0, 0, :] + time = sim.plotting_data.t_grid interval = 2 c = 0 @@ -134,5 +122,4 @@ def test_poisson_1d(do_plot=False): if __name__ == "__main__": - # test_light_wave_1d(algo="explicit", do_plot=True) test_poisson_1d(do_plot=False) diff --git a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py index 443a7ef3a..e0ccc3e51 100644 --- a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py @@ -20,24 +20,23 @@ main, maxwellians, perturbations, + StruphySimulation, ) +from struphy.models import VlasovAmpereOneSpecies def test_weak_Landau(do_plot: bool = False): """Verification test for weak Landau damping. The computed damping rate is compared to the analytical rate. """ - # import model - from struphy.models import VlasovAmpereOneSpecies - + # light-weight model instance + model = VlasovAmpereOneSpecies(with_B0=False) + # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "VlasovAmpereOneSpecies") env = EnvironmentOptions(out_folders=out_folders, sim_folder="weak_Landau") - # units - base_units = BaseUnits() - # time stepping time_opts = Time(dt=0.05, Tend=15) @@ -45,18 +44,12 @@ def test_weak_Landau(do_plot: bool = False): r1 = 12.56 domain = domains.Cuboid(r1=r1) - # fluid equilibrium (can be used as part of initial conditions) - equil = None - # grid grid = grids.TensorProductGrid(Nel=(32, 1, 1)) # derham options derham_opts = DerhamOptions(p=(3, 1, 1)) - # light-weight model instance - model = VlasovAmpereOneSpecies(with_B0=False) - # species parameters model.kinetic_ions.set_phys_params(alpha=1.0, epsilon=-1.0) @@ -91,19 +84,19 @@ def test_weak_Landau(do_plot: bool = False): init = maxwellians.Maxwellian3D(n=(1.0, perturbation)) model.kinetic_ions.var.add_initial_condition(init) - # start run - main.run( - model, - params_path=None, + # instance of simulation + sim = StruphySimulation( + model=model, env=env, - base_units=base_units, time_opts=time_opts, domain=domain, - equil=equil, grid=grid, derham_opts=derham_opts, - verbose=False, + verbose=True, ) + + # run + sim.run(verbose=True) # post processing not needed for scalar data diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 8474272d6..ec6f0717d 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -33,12 +33,18 @@ class Variable(metaclass=ABCMeta): which satisfy a PDE within a model. """ + @property + @abstractmethod + def space(self): + """The function space of the variable, e.g. 'H1' for finite element variables or 'Particles6D' for PIC variables.""" + pass + @abstractmethod def allocate(self): """Alocate object and memory for variable.""" def __repr__(self): - return self.__class__.__name__ + return f"{self.__class__.__name__} ({self.space})" @property def backgrounds(self): diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 7304869be..160834797 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -142,12 +142,15 @@ def __init__( self.path_out = path_out self.path_pproc = os.path.join(path_out, "post_processing") - self.derham = setup_derham( - grid, - derham_opts, - comm=None, - domain=domain, - ) + if grid is None or derham_opts is None: + self.derham = None + else: + self.derham = setup_derham( + grid, + derham_opts, + comm=None, + domain=domain, + ) self.domain = domain self.model = model self.comm_size = comm_size diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index c9477da67..8bde186e0 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,15 +1,12 @@ -# api imports +# third party imports import glob import os import pickle import shutil import sysconfig import time - import cunumpy as xp import h5py - -# third party imports from feectools.ddm.mpi import MockMPI from feectools.ddm.mpi import mpi as MPI from feectools.linalg.stencil import StencilVector @@ -17,6 +14,7 @@ from pyevtk.hl import gridToVTK from scope_profiler import ProfileManager +# api imports from struphy import ( BaseUnits, DerhamOptions, @@ -28,6 +26,8 @@ equils, grids, ) + +# core imports from struphy.feec.basis_projection_ops import BasisProjectionOperators from struphy.feec.mass import WeightedMassOperators from struphy.fields_background.base import ( @@ -45,7 +45,6 @@ from struphy.io.output_handling import DataContainer from struphy.io.setup import setup_derham -# core imports from struphy.models.base import StruphyModel from struphy.models.species import ( DiagnosticSpecies, @@ -72,7 +71,7 @@ def __init__( base_units: BaseUnits = BaseUnits(), time_opts: Time = Time(), domain: Domain = domains.Cuboid(), - equil: FluidEquilibrium = equils.HomogenSlab(), + equil: FluidEquilibrium = None, grid: grids.TensorProductGrid = grids.TensorProductGrid(), derham_opts: DerhamOptions = DerhamOptions(), verbose: bool = False, From 7caaceaffc1d8c8958ff966ecdedebee73192a12 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 13 Feb 2026 16:39:27 +0100 Subject: [PATCH 29/80] formatting --- src/struphy/fields_background/base.py | 2 +- src/struphy/io/options.py | 7 +++-- src/struphy/models/tests/utils_testing.py | 4 +-- .../tests/verification/test_verif_EulerSPH.py | 6 ++-- .../verification/test_verif_LinearMHD.py | 15 ++++++++-- .../tests/verification/test_verif_Maxwell.py | 4 +-- .../tests/verification/test_verif_Poisson.py | 16 ++++++++-- .../test_verif_VlasovAmpereOneSpecies.py | 6 ++-- src/struphy/models/variables.py | 2 +- src/struphy/simulation/sim.py | 30 +++++++++---------- src/struphy/topology/grids.py | 2 +- 11 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index 91a08de64..23143f2bd 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -49,7 +49,7 @@ def domain(self): def domain(self, new_domain): assert isinstance(new_domain, Domain) or new_domain is None self._domain = new_domain - + def __repr__(self): print(f"{self.__class__.__name__}") for k, v in self.params.items(): diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index a78eb4759..8cacabb72 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -121,7 +121,7 @@ class Time: def __post_init__(self): check_option(self.split_algo, LiteralOptions.SplitAlgos) - + def __repr__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) @@ -159,6 +159,7 @@ def __repr__(self): print(f"{k}:".ljust(20), v, unit) return "" + @dataclass class DerhamOptions: """Set options for the Derham spaces in parameter/launch files. See :ref:`geomFE`. @@ -197,7 +198,7 @@ class DerhamOptions: def __post_init__(self): check_option(self.polar_ck, LiteralOptions.PolarRegularity) - + def __repr__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) @@ -227,7 +228,7 @@ class FieldsBackground: def __post_init__(self): check_option(self.type, LiteralOptions.BackgroundTypes) - + def __repr__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index f642e7ea4..bfc9f744b 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -64,11 +64,11 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = derham_opts=derham_opts, verbose=verbose, ) - + sim.show_parameters() sim.run(verbose=verbose) - + # test restart env.restart = True time_opts.Tend += time_opts.dt diff --git a/src/struphy/models/tests/verification/test_verif_EulerSPH.py b/src/struphy/models/tests/verification/test_verif_EulerSPH.py index 95b1bb9c9..448a20fce 100644 --- a/src/struphy/models/tests/verification/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_EulerSPH.py @@ -14,13 +14,13 @@ EnvironmentOptions, KernelDensityPlot, LoadingParameters, + StruphySimulation, Time, WeightsParameters, domains, equils, main, perturbations, - StruphySimulation, ) from struphy.models import EulerSPH @@ -33,7 +33,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): """ # light-weight model instance model = EulerSPH(with_B0=False) - + # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "EulerSPH") @@ -104,7 +104,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): derham_opts=derham_opts, verbose=True, ) - + # run sim.run(verbose=True) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 65f55dc83..6de84fb37 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -5,7 +5,18 @@ import pytest from feectools.ddm.mpi import mpi as MPI -from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, equils, grids, main, perturbations, StruphySimulation,) +from struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + StruphySimulation, + Time, + domains, + equils, + grids, + main, + perturbations, +) from struphy.diagnostics.diagn_tools import power_spectrum_2d from struphy.models import LinearMHD @@ -63,7 +74,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): equil=equil, verbose=True, ) - + # run sim.run(verbose=True) diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index ffa9ad11a..021f25606 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -27,7 +27,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): # light-weight model instance model = Maxwell() - + # set environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "Maxwell") @@ -149,7 +149,7 @@ def test_coaxial(do_plot: bool = False): derham_opts=derham_opts, verbose=True, ) - + # run sim.run(verbose=True) diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index a06e94bf4..8673deea5 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -5,14 +5,24 @@ from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt -from struphy import (BaseUnits, DerhamOptions, EnvironmentOptions, Time, domains, grids, main, perturbations, StruphySimulation,) +from struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + StruphySimulation, + Time, + domains, + grids, + main, + perturbations, +) from struphy.models import Poisson def test_poisson_1d(do_plot=False): # light-weight model instance model = Poisson() - + # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "Poisson") @@ -27,7 +37,7 @@ def test_poisson_1d(do_plot=False): domain = domains.Cuboid( l1=l1, r1=r1, - ) + ) # fluid equilibrium (can be used as part of initial conditions) equil = None diff --git a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py index e0ccc3e51..8ea698bb5 100644 --- a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py @@ -13,6 +13,7 @@ DerhamOptions, EnvironmentOptions, LoadingParameters, + StruphySimulation, Time, WeightsParameters, domains, @@ -20,7 +21,6 @@ main, maxwellians, perturbations, - StruphySimulation, ) from struphy.models import VlasovAmpereOneSpecies @@ -31,7 +31,7 @@ def test_weak_Landau(do_plot: bool = False): """ # light-weight model instance model = VlasovAmpereOneSpecies(with_B0=False) - + # environment options test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") out_folders = os.path.join(test_folder, "VlasovAmpereOneSpecies") @@ -94,7 +94,7 @@ def test_weak_Landau(do_plot: bool = False): derham_opts=derham_opts, verbose=True, ) - + # run sim.run(verbose=True) diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index ec6f0717d..60ec0046e 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -42,7 +42,7 @@ def space(self): @abstractmethod def allocate(self): """Alocate object and memory for variable.""" - + def __repr__(self): return f"{self.__class__.__name__} ({self.space})" diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 8bde186e0..b795ec30c 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -5,6 +5,7 @@ import shutil import sysconfig import time + import cunumpy as xp import h5py from feectools.ddm.mpi import MockMPI @@ -44,7 +45,6 @@ from struphy.geometry.base import Domain from struphy.io.output_handling import DataContainer from struphy.io.setup import setup_derham - from struphy.models.base import StruphyModel from struphy.models.species import ( DiagnosticSpecies, @@ -85,7 +85,7 @@ def __init__( self._setup_domain_and_equil(domain, equil, verbose=verbose) self._grid = grid self._derham_opts = derham_opts - + # setup profiling agent ProfileManager.setup( profiling_activated=env.profiling_activated, @@ -238,7 +238,7 @@ def __init__( # ---------------- # Abstract methods # ---------------- - + def show_parameters(self): if self.rank == 0: print("\nSIMULATION PARAMETERS:") @@ -303,7 +303,7 @@ def save_geometry_and_equil_vtk(self, verbose: bool = False): def initialize_data_storage(self, verbose: bool = False): # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs - + self.data = DataContainer(self.env.path_out, comm=self.comm) # time quantities (current time value, value in seconds and index) @@ -1081,27 +1081,27 @@ def _initialize_from_restart(self, data: DataContainer, verbose: bool = False): # ------------------------------------------------------ # Common properties with setters (from input parameters) # ------------------------------------------------------ - + @property def model(self): """StruphyModel object containing the PDE of the model.""" return self._model - + @property - def params_path(self): + def params_path(self): """Path to parameter file used for the run.""" return self._params_path @property - def env(self): + def env(self): """EnvironmentOptions object containing options related to the environment of the run.""" return self._env - + @property - def base_units(self): + def base_units(self): """BaseUnits object containing the four base units for the run.""" return self._base_units - + @property def time_opts(self): """Time object containing time stepping parameters.""" @@ -1116,14 +1116,14 @@ def domain(self): def equil(self): """Fluid equilibrium object, see :ref:`fluid_equil`.""" return self._equil - + @property def grid(self): """Grid object, see :ref:`grids`.""" return self._grid - + @property - def derham_opts(self): + def derham_opts(self): """DerhamOptions object containing options for the setup of the 3d Derham sequence.""" return self._derham_opts @@ -1169,4 +1169,4 @@ def clone_config(self): @clone_config.setter def clone_config(self, new): assert isinstance(new, CloneConfig) or new is None - self._clone_config = new \ No newline at end of file + self._clone_config = new diff --git a/src/struphy/topology/grids.py b/src/struphy/topology/grids.py index 82e313a3a..3f304c3c7 100644 --- a/src/struphy/topology/grids.py +++ b/src/struphy/topology/grids.py @@ -19,7 +19,7 @@ class TensorProductGrid: Nel: tuple = (24, 10, 1) mpi_dims_mask: tuple = (True, True, True) - + def __repr__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) From acc0f9045c3e561c1b1bef3bc197c391867ee9ee Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 10:25:08 +0100 Subject: [PATCH 30/80] improved the look of the generated default parameter fiel: added a description, more comments and headings, use sim.run() --- src/struphy/models/base.py | 224 ++++++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 92 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index dd4484dd4..2e516026d 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -403,46 +403,48 @@ def generate_default_parameter_file( else: print("exiting ...") exit() - - file.write("from struphy import EnvironmentOptions, BaseUnits, Time\n") - file.write("from struphy import domains\n") - file.write("from struphy import equils\n") - - species_params = "\n# species parameters\n" - particle_params = "" - has_plasma = False + + # loop over species to create parameter snippets + species_params = "" + variables_params = "" + particle_params = """\n# ------------------- +# Particle parameters +# -------------------\n""" has_feec = False has_pic = False has_sph = False for sn, species in self.species.items(): assert isinstance(species, Species) - - if isinstance(species, (FluidSpecies, ParticleSpecies)): - has_plasma = True - species_params += f"model.{sn}.set_phys_params()\n" - if isinstance(species, ParticleSpecies): - particle_params += "\nloading_params = LoadingParameters()\n" - particle_params += "weights_params = WeightsParameters()\n" - particle_params += "boundary_params = BoundaryParameters()\n" - particle_params += f"model.{sn}.set_markers(loading_params=loading_params,\n" - txt = "weights_params=weights_params,\n" - particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) - txt = "boundary_params=boundary_params,\n" - particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) - txt = ")\n" - particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) - particle_params += f"model.{sn}.set_sorting_boxes()\n" - particle_params += f"model.{sn}.set_save_data()\n" + species_params += f"model.{sn}.set_phys_params()\n" + + if isinstance(species, ParticleSpecies): + particle_params += "\nloading_params = LoadingParameters()\n" + particle_params += "weights_params = WeightsParameters()\n" + particle_params += "boundary_params = BoundaryParameters()\n" + particle_params += f"model.{sn}.set_markers(loading_params=loading_params,\n" + txt = "weights_params=weights_params,\n" + particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) + txt = "boundary_params=boundary_params,\n" + particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) + txt = ")\n" + particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) + particle_params += f"model.{sn}.set_sorting_boxes()\n" + particle_params += f"model.{sn}.set_save_data()\n" for vn, var in species.variables.items(): + + variables_params += f"model.{sn}.{vn}.save_data = True\n" + if isinstance(var, FEECVariable): has_feec = True + init_bckgr_feec = "\n# Background for (some) FEEC variables\n" + init_pert_feec = "\n# Perturbations for (some) FEEC variables\n" if var.space in ("H1", "L2"): - init_bckgr_feec = f"model.{sn}.{vn}.add_background(FieldsBackground())\n" - init_pert_feec = f"model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos())\n" + init_bckgr_feec += f"model.{sn}.{vn}.add_background(FieldsBackground())\n" + init_pert_feec += f"model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos())\n" else: - init_bckgr_feec = f"model.{sn}.{vn}.add_background(FieldsBackground())\n" - init_pert_feec = ( + init_bckgr_feec += f"model.{sn}.{vn}.add_background(FieldsBackground())\n" + init_pert_feec += ( f"model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=0))\n\ model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=1))\n\ model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=2))\n" @@ -450,18 +452,17 @@ def generate_default_parameter_file( elif isinstance(var, PICVariable): has_pic = True - init_pert_pic = ( - "\n# if .add_initial_condition is not called, the background is the kinetic initial condition\n" - ) + init_bckgr_pic = "\n# Background for (some) kinetic species\n" + init_pert_pic = "\n# Perturbations for (some) kinetic species\n" init_pert_pic += "perturbation = perturbations.TorusModesCos()\n" if "6D" in var.space: - init_bckgr_pic = "maxwellian_1 = maxwellians.Maxwellian3D(n=(1.0, None))\n" + init_bckgr_pic += "maxwellian_1 = maxwellians.Maxwellian3D(n=(1.0, None))\n" init_bckgr_pic += "maxwellian_2 = maxwellians.Maxwellian3D(n=(0.1, None))\n" 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 "5D" in var.space: - init_bckgr_pic = "maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None), equil=equil)\n" + init_bckgr_pic += "maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None), equil=equil)\n" init_bckgr_pic += "maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None), equil=equil)\n" init_pert_pic += ( "maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil)\n" @@ -469,7 +470,7 @@ def generate_default_parameter_file( 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: - init_bckgr_pic = "maxwellian_1 = maxwellians.ColdPlasma(n=(1.0, None))\n" + init_bckgr_pic += "maxwellian_1 = maxwellians.ColdPlasma(n=(1.0, None))\n" init_bckgr_pic += "maxwellian_2 = maxwellians.ColdPlasma(n=(0.1, None))\n" init_pert_pic += "maxwellian_1pt = maxwellians.ColdPlasma(n=(1.0, perturbation))\n" init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" @@ -477,48 +478,89 @@ def generate_default_parameter_file( init_bckgr_pic += "background = maxwellian_1 + maxwellian_2\n" init_bckgr_pic += f"model.{sn}.{vn}.add_background(background)\n" - exclude = "# model.....save_data = False\n" - elif isinstance(var, SPHVariable): has_sph = True - init_bckgr_sph = "background = equils.ConstantVelocity()\n" + init_bckgr_sph = "\n# Background for (some) sph variables\n" + init_pert_sph = "\n# Perturbations for (some) sph variables\n" + init_bckgr_sph += "background = equils.ConstantVelocity()\n" init_bckgr_sph += f"model.{sn}.{vn}.add_background(background)\n" - init_pert_sph = "perturbation = perturbations.TorusModesCos()\n" + init_pert_sph += "perturbation = perturbations.TorusModesCos()\n" init_pert_sph += f"model.{sn}.{vn}.add_perturbation(del_n=perturbation)\n" - exclude = f"# model.{sn}.{vn}.save_data = False\n" - - file.write("from struphy import grids\n") - file.write("from struphy import DerhamOptions\n") - file.write("from struphy import FieldsBackground\n") - file.write("from struphy import perturbations\n") - - file.write("from struphy import maxwellians\n") - file.write( - "from struphy import (LoadingParameters,\n\ - WeightsParameters,\n\ - BoundaryParameters,\n\ - BinningPlot,\n\ - KernelDensityPlot,\n\ - )\n", - ) - file.write("from struphy import main\n") - - file.write("\n# import model\n") - file.write(f"from struphy.models import {self.__class__.__name__}\n") + + file.write(f"""# ----------------------------- +# 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 = f\"\"\"\nThis is the default simulation for the model {self.__class__.__name__}. +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.\n\"\"\" +\nprint(f"\\nRunning {{__file__}}.") +print(description)\n""") + + file.write("""\n# ------------------ +# Import Struphy API +# ------------------\n""") + + file.write("""\nfrom struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + FieldsBackground, + StruphySimulation, + Time, + domains, + equils, + grids, + perturbations, +)\n""") - file.write("\n# environment options\n") + if has_pic or has_sph: + file.write("""\n# For particles:\nfrom struphy import ( + BinningPlot, + BoundaryParameters, + KernelDensityPlot, + LoadingParameters, + WeightsParameters, + maxwellians, +)\n""") + + file.write("""\n# --------------------- +# Instance of the model +# ---------------------\n""") + + file.write(f"\nfrom struphy.models import {self.__class__.__name__}\n") + file.write(f"model = {self.__class__.__name__}()\n") + + file.write("\n# List all species and set their physical properties (charge and mass number, etc.)\n") + file.write(species_params) + + file.write("\n# List all variables and decide whether to save their data\n") + file.write(variables_params) + + file.write("""\n# -------------------------- +# Instance of the simulation +# --------------------------\n""") + + # file.write("\nfrom struphy import StruphySimulation\n") + + file.write("\n# Environment options\n") file.write("env = EnvironmentOptions()\n") - file.write("\n# units\n") + file.write("\n# Units\n") file.write("base_units = BaseUnits()\n") - file.write("\n# time stepping\n") + file.write("\n# Time stepping\n") file.write("time_opts = Time()\n") - file.write("\n# geometry\n") + file.write("\n# Geometry\n") file.write("domain = domains.Cuboid()\n") - file.write("\n# fluid equilibrium (can be used as part of initial conditions)\n") + file.write("\n# Fluid equilibrium (can be used as part of initial conditions)\n") file.write("equil = equils.HomogenSlab()\n") # if has_feec: @@ -528,26 +570,41 @@ def generate_default_parameter_file( # grid = "grid = None\n" # derham = "derham_opts = None\n" - file.write("\n# grid\n") + file.write("\n# Grid\n") file.write(grid) - file.write("\n# derham options\n") + file.write("\n# Derham options\n") file.write(derham) - - file.write("\n# light-weight model instance\n") - file.write(f"model = {self.__class__.__name__}()\n") - - if has_plasma: - file.write(species_params) + + file.write("\n# Simulation object\n") + file.write("""sim = StruphySimulation( + model=model, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, +)\n""") if has_pic or has_sph: file.write(particle_params) - file.write("\n# propagator options\n") + file.write("""\n# ------------------ +# Propagator options +# ------------------\n\n""") for prop in self.propagators.__dict__: file.write(f"model.propagators.{prop}.options = model.propagators.{prop}.Options()\n") - file.write("\n# background, perturbations and initial conditions\n") + file.write("""\n# ------------------ +# Initial conditions +# ------------------ +# Initial conditions are the sum of the background(s) and the perturbation(s). +# 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). +# If perturbations are not specified, they are assumed to be zero. \n""") if has_feec: file.write(init_bckgr_feec) file.write(init_pert_feec) @@ -558,25 +615,8 @@ def generate_default_parameter_file( file.write(init_bckgr_sph) file.write(init_pert_sph) - file.write("\n# optional: exclude variables from saving\n") - file.write(exclude) - file.write('\nif __name__ == "__main__":\n') - file.write(" # start run\n") - file.write(" verbose = True\n\n") - file.write( - " main.run(model,\n\ - params_path=__file__,\n\ - env=env,\n\ - base_units=base_units,\n\ - time_opts=time_opts,\n\ - domain=domain,\n\ - equil=equil,\n\ - grid=grid,\n\ - derham_opts=derham_opts,\n\ - verbose=verbose,\n\ - )", - ) + file.write(" sim.run(verbose=False)") file.close() From 98b6b9a899208ca9a267103102a1e3aa66073619 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 10:29:31 +0100 Subject: [PATCH 31/80] formatting --- src/struphy/__init__.py | 2 +- src/struphy/api/post_processing/__init__.py | 4 ++-- src/struphy/models/base.py | 21 ++++++++++----------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index 31317f159..e49cbfc17 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -18,7 +18,7 @@ WeightsParameters, ) from struphy.api.perturbations import perturbations -from struphy.api.post_processing import PostProcessor, PlottingData +from struphy.api.post_processing import PlottingData, PostProcessor from struphy.api.simulation import StruphySimulation __all__ = [ diff --git a/src/struphy/api/post_processing/__init__.py b/src/struphy/api/post_processing/__init__.py index 139295cf8..2b392318e 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.post_processing_tools import PostProcessor, PlottingData +from struphy.post_processing.post_processing_tools import PlottingData, PostProcessor -__all__ = ["PostProcessor", "PlottingData"] \ No newline at end of file +__all__ = ["PostProcessor", "PlottingData"] diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 2e516026d..78084fb12 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -403,7 +403,7 @@ def generate_default_parameter_file( else: print("exiting ...") exit() - + # loop over species to create parameter snippets species_params = "" variables_params = "" @@ -432,9 +432,8 @@ def generate_default_parameter_file( particle_params += f"model.{sn}.set_save_data()\n" for vn, var in species.variables.items(): - variables_params += f"model.{sn}.{vn}.save_data = True\n" - + if isinstance(var, FEECVariable): has_feec = True init_bckgr_feec = "\n# Background for (some) FEEC variables\n" @@ -486,7 +485,7 @@ def generate_default_parameter_file( init_bckgr_sph += f"model.{sn}.{vn}.add_background(background)\n" init_pert_sph += "perturbation = perturbations.TorusModesCos()\n" init_pert_sph += f"model.{sn}.{vn}.add_perturbation(del_n=perturbation)\n" - + file.write(f"""# ----------------------------- # Description of the simulation # ----------------------------- @@ -500,7 +499,7 @@ def generate_default_parameter_file( 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.\n\"\"\" \nprint(f"\\nRunning {{__file__}}.") -print(description)\n""") +print(description)\n""") file.write("""\n# ------------------ # Import Struphy API @@ -532,22 +531,22 @@ def generate_default_parameter_file( file.write("""\n# --------------------- # Instance of the model # ---------------------\n""") - + file.write(f"\nfrom struphy.models import {self.__class__.__name__}\n") file.write(f"model = {self.__class__.__name__}()\n") - + file.write("\n# List all species and set their physical properties (charge and mass number, etc.)\n") file.write(species_params) - + file.write("\n# List all variables and decide whether to save their data\n") file.write(variables_params) file.write("""\n# -------------------------- # Instance of the simulation # --------------------------\n""") - + # file.write("\nfrom struphy import StruphySimulation\n") - + file.write("\n# Environment options\n") file.write("env = EnvironmentOptions()\n") @@ -575,7 +574,7 @@ def generate_default_parameter_file( file.write("\n# Derham options\n") file.write(derham) - + file.write("\n# Simulation object\n") file.write("""sim = StruphySimulation( model=model, From e26aed44db502dcb52cf6eae8c3da9890b5d29f4 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 11:32:07 +0100 Subject: [PATCH 32/80] rename set_phys_params() to set_species_properties() --- .../subsections/models-normalization.rst | 2 +- doc/sections/userguide.rst | 2 +- src/struphy/models/base.py | 2 +- src/struphy/models/species.py | 17 ++++++++++++++++- .../tests/verification/test_verif_EulerSPH.py | 2 +- .../tests/verification/test_verif_LinearMHD.py | 2 +- .../test_verif_VlasovAmpereOneSpecies.py | 2 +- struphy-tutorials | 2 +- 8 files changed, 23 insertions(+), 8 deletions(-) diff --git a/doc/sections/subsections/models-normalization.rst b/doc/sections/subsections/models-normalization.rst index cd25f8e3a..d1d5840a5 100644 --- a/doc/sections/subsections/models-normalization.rst +++ b/doc/sections/subsections/models-normalization.rst @@ -116,7 +116,7 @@ featuring the plasma- and cyclotron frequency of species :math:`\textrm{s}`, res where :math:`Z_\textrm{s}` and :math:`A_\textrm{s}` stand for the species' charge and mass number, respectively. These equation parameters are defined in :class:`~struphy.models.species.Species.EquationParameters` and can be overridden -in the launch file via :func:`~struphy.models.species.Species.set_phys_params`. +in the launch file via :func:`~struphy.models.species.Species.set_species_properties`. .. autoclass:: struphy.models.species.Species.EquationParameters :members: diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index c0a817d46..81805ec79 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -68,7 +68,7 @@ Each Struphy model is a collection of species of one of the following types: .. autoclass:: struphy.models.species.ParticleSpecies -.. automethod:: struphy.models.species.Species.set_phys_params +.. automethod:: struphy.models.species.Species.set_species_properties Variable types diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 78084fb12..037e36ffe 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -415,7 +415,7 @@ def generate_default_parameter_file( has_sph = False for sn, species in self.species.items(): assert isinstance(species, Species) - species_params += f"model.{sn}.set_phys_params()\n" + species_params += f"model.{sn}.set_species_properties()\n" if isinstance(species, ParticleSpecies): particle_params += "\nloading_params = LoadingParameters()\n" diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index a2cec5080..2a8c4604e 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -50,7 +50,7 @@ def mass_number(self) -> int: """Mass number in units of proton mass.""" return self._mass_number - def set_phys_params( + def set_species_properties( self, charge_number: int = 1, mass_number: int = 1, @@ -60,6 +60,7 @@ def set_phys_params( ): """Set charge- and mass number of species in parameter/launch files. Optional: Set equation parameters (alpha, epsilon, kappa) to override units.""" + self._charge_number = charge_number self._mass_number = mass_number self.alpha = alpha @@ -140,6 +141,20 @@ def setup_equation_params(self, units: Units, verbose=False): class FieldSpecies(Species): """Species without mass and charge (so-called 'fields').""" + + def set_species_properties( + self, + alpha: float = None, + epsilon: float = None, + kappa: float = None, + ): + """Set equation parameters (alpha, epsilon, kappa) to override units.""" + + self._charge_number = 0 + self._mass_number = 0 + self.alpha = alpha + self.epsilon = epsilon + self.kappa = kappa class FluidSpecies(Species): diff --git a/src/struphy/models/tests/verification/test_verif_EulerSPH.py b/src/struphy/models/tests/verification/test_verif_EulerSPH.py index 448a20fce..86347dc7d 100644 --- a/src/struphy/models/tests/verification/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_EulerSPH.py @@ -56,7 +56,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): derham_opts = None # species parameters - model.euler_fluid.set_phys_params() + model.euler_fluid.set_species_properties() loading_params = LoadingParameters(ppb=8, loading="tesselation") weights_params = WeightsParameters() diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 6de84fb37..88061e8d4 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -52,7 +52,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): derham_opts = DerhamOptions(p=(1, 1, 3)) # species parameters - model.mhd.set_phys_params() + model.mhd.set_species_properties() # propagator options model.propagators.shear_alf.options = model.propagators.shear_alf.Options(algo=algo) diff --git a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py index 8ea698bb5..477eba1fc 100644 --- a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py @@ -51,7 +51,7 @@ def test_weak_Landau(do_plot: bool = False): derham_opts = DerhamOptions(p=(3, 1, 1)) # species parameters - model.kinetic_ions.set_phys_params(alpha=1.0, epsilon=-1.0) + model.kinetic_ions.set_species_properties(alpha=1.0, epsilon=-1.0) ppc = 1000 loading_params = LoadingParameters(ppc=ppc, seed=1234) diff --git a/struphy-tutorials b/struphy-tutorials index f4ad223e9..4fff8be0e 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit f4ad223e9aabf57457baa89a3bf9765964954716 +Subproject commit 4fff8be0e0e675098f205ed80e0cede75c93cc7b From 90fe4e0660adfb4bfe638a221a2132833bc4f083 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 11:50:43 +0100 Subject: [PATCH 33/80] set background for all kinetic species --- src/struphy/models/base.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 037e36ffe..30287dfb3 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -410,6 +410,7 @@ def generate_default_parameter_file( particle_params = """\n# ------------------- # Particle parameters # -------------------\n""" + init_bckgr_pic = "\n# Background for kinetic species\n" has_feec = False has_pic = False has_sph = False @@ -451,7 +452,6 @@ def generate_default_parameter_file( elif isinstance(var, PICVariable): has_pic = True - init_bckgr_pic = "\n# Background for (some) kinetic species\n" init_pert_pic = "\n# Perturbations for (some) kinetic species\n" init_pert_pic += "perturbation = perturbations.TorusModesCos()\n" if "6D" in var.space: @@ -600,14 +600,15 @@ def generate_default_parameter_file( # Initial conditions # ------------------ # Initial conditions are the sum of the background(s) and the perturbation(s). -# 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). -# If perturbations are not specified, they are assumed to be zero. \n""") +# If backgrounds or perturbations are not specified, they are assumed to be zero.\n""") if has_feec: file.write(init_bckgr_feec) file.write(init_pert_feec) if has_pic: + file.write(""" +# 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).\n""") file.write(init_bckgr_pic) file.write(init_pert_pic) if has_sph: From ce67cc1e70d662ca9c95358dd792c95018153398 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 12:56:20 +0100 Subject: [PATCH 34/80] add -x flag to pytest commands --- .github/actions/environment/unit-mpi/action.yml | 4 ++-- .github/actions/environment/unit-serial/action.yml | 2 +- .github/workflows/reusable-scheduled.yml | 4 ++-- .github/workflows/test-PR-models-clones.yml | 4 ++-- .github/workflows/test-PR-models.yml | 10 +++++----- .github/workflows/test-PR-pure-python.yml | 10 +++++----- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/actions/environment/unit-mpi/action.yml b/.github/actions/environment/unit-mpi/action.yml index 2bd80b437..c44c62110 100644 --- a/.github/actions/environment/unit-mpi/action.yml +++ b/.github/actions/environment/unit-mpi/action.yml @@ -13,8 +13,8 @@ runs: shell: bash run: | TESTMON_KEY="testmon-unit-mpi" - RUN_MAXWELL="mpirun -n 1 pytest -m single --testmon-forceselect -s --with-mpi --model-name Maxwell" - PYTEST_CMD="mpirun -n ${{ inputs.n-procs }} pytest --testmon --with-mpi" + RUN_MAXWELL="mpirun -n 1 pytest -m single --testmon-forceselect -x --with-mpi --model-name Maxwell" + PYTEST_CMD="mpirun -n ${{ inputs.n-procs }} pytest -x --testmon --with-mpi" echo "TESTMON_KEY=${TESTMON_KEY}" >> $GITHUB_ENV echo "RUN_MAXWELL=${RUN_MAXWELL}" >> $GITHUB_ENV echo "PYTEST_CMD=${PYTEST_CMD}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/actions/environment/unit-serial/action.yml b/.github/actions/environment/unit-serial/action.yml index d4a9a1a53..03cb5daf9 100644 --- a/.github/actions/environment/unit-serial/action.yml +++ b/.github/actions/environment/unit-serial/action.yml @@ -10,7 +10,7 @@ runs: run: | TESTMON_KEY="testmon-unit" UNINSTALL_MPI="pip uninstall -y mpi4py" - PYTEST_CMD="pytest --testmon" + PYTEST_CMD="pytest -x --testmon" echo "TESTMON_KEY=${TESTMON_KEY}" >> $GITHUB_ENV echo "UNINSTALL_MPI=${UNINSTALL_MPI}" >> $GITHUB_ENV echo "PYTEST_CMD=${PYTEST_CMD}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/reusable-scheduled.yml b/.github/workflows/reusable-scheduled.yml index 08ddd7fe7..8baef906c 100644 --- a/.github/workflows/reusable-scheduled.yml +++ b/.github/workflows/reusable-scheduled.yml @@ -86,10 +86,10 @@ jobs: if: matrix.test-type == 'model' shell: bash run: | - mpirun -oversubscribe -n 2 pytest -m models --with-mpi $STRUPHY_PATH/models/tests/default_params/ + mpirun -oversubscribe -n 2 pytest -x -m models --with-mpi $STRUPHY_PATH/models/tests/default_params/ - name: Run verification tests with MPI if: matrix.test-type == 'verification' shell: bash run: | - mpirun --oversubscribe -n 2 pytest --with-mpi $STRUPHY_PATH/models/tests/verification/ + mpirun --oversubscribe -n 2 pytest -x --with-mpi $STRUPHY_PATH/models/tests/verification/ diff --git a/.github/workflows/test-PR-models-clones.yml b/.github/workflows/test-PR-models-clones.yml index c89f74a6f..1c545e0e3 100644 --- a/.github/workflows/test-PR-models-clones.yml +++ b/.github/workflows/test-PR-models-clones.yml @@ -84,5 +84,5 @@ jobs: run: | source /struphy_fortran_/env_fortran_/bin/activate cd /struphy_fortran_/src/struphy - mpirun -n 1 pytest -m single --testmon-forceselect -s --with-mpi --model-name Maxwell - mpirun --oversubscribe -n 4 pytest --testmon --with-mpi --nclones 2 models/tests/verification/ \ No newline at end of file + 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 diff --git a/.github/workflows/test-PR-models.yml b/.github/workflows/test-PR-models.yml index ed6fba509..3bb7cba43 100644 --- a/.github/workflows/test-PR-models.yml +++ b/.github/workflows/test-PR-models.yml @@ -91,7 +91,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-model run: | source /struphy_fortran_/env_fortran_/bin/activate - pytest -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 @@ -100,7 +100,7 @@ jobs: run: | source /struphy_fortran_/env_fortran_/bin/activate cd /struphy_fortran_/src/struphy - pytest --testmon models/tests/verification/ + pytest -x --testmon models/tests/verification/ - name: Model tests with MPI shell: bash @@ -109,8 +109,8 @@ jobs: # init .testmondata with non-MPI call run: | source /struphy_fortran_/env_fortran_/bin/activate - mpirun -n 1 pytest -m single --testmon-forceselect -s --with-mpi --model-name Maxwell /struphy_fortran_/src/struphy - mpirun -oversubscribe -n 2 pytest -m models --testmon-forceselect --with-mpi /struphy_fortran_/src/struphy/models/tests/default_params/ + mpirun -n 1 pytest -m single --testmon-forceselect -xs --with-mpi --model-name Maxwell /struphy_fortran_/src/struphy + mpirun -oversubscribe -n 2 pytest -x -m models --testmon-forceselect --with-mpi /struphy_fortran_/src/struphy/models/tests/default_params/ - name: Verification tests with 2 MPI shell: bash @@ -118,4 +118,4 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-model-mpi run: | source /struphy_fortran_/env_fortran_/bin/activate - mpirun --oversubscribe -n 2 pytest --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/ \ 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 1fd4b4711..0069b7271 100644 --- a/.github/workflows/test-PR-pure-python.yml +++ b/.github/workflows/test-PR-pure-python.yml @@ -158,8 +158,8 @@ jobs: # init .testmondata with non-MPI call run: | source env/bin/activate - mpirun -n 1 pytest -m single --testmon-forceselect -s --with-mpi --model-name Maxwell $STRUPHY_PATH - mpirun -n 2 pytest -m single --testmon-forceselect -s --with-mpi --model-name Vlasov $STRUPHY_PATH + mpirun -n 1 pytest -m single --testmon-forceselect -xs --with-mpi --model-name Maxwell $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name Vlasov $STRUPHY_PATH - name: GuidingCenter test MPI shell: bash @@ -167,7 +167,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -s --with-mpi --model-name GuidingCenter $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name GuidingCenter $STRUPHY_PATH - name: VlasovAmpere test MPI shell: bash @@ -175,7 +175,7 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -s --with-mpi --model-name VlasovAmpereOneSpecies $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name VlasovAmpereOneSpecies $STRUPHY_PATH - name: EulerSPH test MPI shell: bash @@ -183,4 +183,4 @@ jobs: TESTMON_DATAFILE: ${{ github.workspace }}/.testmondata-pure-python-mpi run: | source env/bin/activate - mpirun -n 2 pytest -m single --testmon-forceselect -s --with-mpi --model-name EulerSPH $STRUPHY_PATH + mpirun -n 2 pytest -m single --testmon-forceselect -xs --with-mpi --model-name EulerSPH $STRUPHY_PATH From 9c2cd4d156d30618883392345c3a4ddfd01f60d2 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 13:08:55 +0100 Subject: [PATCH 35/80] initialize PostProcessor and PlottingData only on rank 0 --- src/struphy/simulation/sim.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index b795ec30c..5c586ac81 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -232,8 +232,9 @@ def __init__( model.setup_equation_params(units=self.units, verbose=verbose) # setup post processor and plotting - self._post_processor = PostProcessor(sim=self) - self._plotting_data = PlottingData(sim=self) + if MPI.COMM_WORLD.Get_rank() == 0: + self._post_processor = PostProcessor(sim=self) + self._plotting_data = PlottingData(sim=self) # ---------------- # Abstract methods From 2bc2bf0731ecc6c4643e1cb7b5fdd5ea0c6ed155 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 13:13:09 +0100 Subject: [PATCH 36/80] run ruff check --- 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 160834797..ad44576d3 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -200,7 +200,7 @@ def pproc( Whether vtk files should be created. """ if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n*** Start post-processing::") + print("\n*** Start post-processing::") print(f"Post-processing path: {self.path_out}") # check for fields and kinetic data in hdf5 file that need post processing From ba86e7e8c22b842eeabbf4fb48492303583b5884 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 13:13:52 +0100 Subject: [PATCH 37/80] formatting --- src/struphy/models/species.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 2a8c4604e..b2a329583 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -60,7 +60,7 @@ def set_species_properties( ): """Set charge- and mass number of species in parameter/launch files. Optional: Set equation parameters (alpha, epsilon, kappa) to override units.""" - + self._charge_number = charge_number self._mass_number = mass_number self.alpha = alpha @@ -141,7 +141,7 @@ def setup_equation_params(self, units: Units, verbose=False): class FieldSpecies(Species): """Species without mass and charge (so-called 'fields').""" - + def set_species_properties( self, alpha: float = None, @@ -149,7 +149,7 @@ def set_species_properties( kappa: float = None, ): """Set equation parameters (alpha, epsilon, kappa) to override units.""" - + self._charge_number = 0 self._mass_number = 0 self.alpha = alpha From 5ac6a39c08aa0d2e7ae538b511bf0c8ea30f5fa3 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 13:17:55 +0100 Subject: [PATCH 38/80] new commit in struphy-tutorials --- struphy-tutorials | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/struphy-tutorials b/struphy-tutorials index 4fff8be0e..6e111785e 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 4fff8be0e0e675098f205ed80e0cede75c93cc7b +Subproject commit 6e111785e8fcbe28f8fd127a867740ed4726ab39 From e0544ebcd158263bc1ef29069102db6de94fbc2c Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 13:21:12 +0100 Subject: [PATCH 39/80] disable the ruff format check --- .github/workflows/static_analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 6a356005a..edbba2c49 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -112,9 +112,9 @@ jobs: pip install ruff ruff check - - name: ruff format --check - run: | - ruff format --check + # - name: ruff format --check + # run: | + # ruff format --check # pylint: # runs-on: ubuntu-latest From 408ace494554bec223f466bc91d6cede660fb837 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Feb 2026 14:42:59 +0100 Subject: [PATCH 40/80] improve screen output of simulation --- doc/sections/quickstart.rst | 6 +- src/struphy/feec/psydac_derham.py | 13 +-- .../fields_background/projected_equils.py | 18 +++- src/struphy/models/cold_plasma.py | 2 +- src/struphy/models/cold_plasma_vlasov.py | 7 +- .../deterministic_particle_diffusion.py | 2 +- .../drift_kinetic_electrostatic_adiabatic.py | 2 +- src/struphy/models/euler_sph.py | 2 +- src/struphy/models/guiding_center.py | 5 +- src/struphy/models/hasegawa_wakatani.py | 2 +- .../models/linear_extended_mh_duniform.py | 2 +- src/struphy/models/linear_mhd.py | 2 +- .../models/linear_mhd_driftkinetic_cc.py | 2 +- src/struphy/models/linear_mhd_vlasov_cc.py | 2 +- src/struphy/models/linear_mhd_vlasov_pc.py | 2 +- .../linear_vlasov_ampere_one_species.py | 4 +- .../linear_vlasov_maxwell_one_species.py | 2 +- src/struphy/models/maxwell.py | 2 +- src/struphy/models/poisson.py | 4 +- src/struphy/models/pressure_less_sph.py | 2 +- .../models/random_particle_diffusion.py | 2 +- src/struphy/models/shear_alfven.py | 2 +- .../models/two_fluid_quasi_neutral_toy.py | 2 +- src/struphy/models/variables.py | 74 +++++++++----- .../models/variational_barotropic_fluid.py | 2 +- .../models/variational_compressible_fluid.py | 2 +- .../models/variational_pressureless_fluid.py | 2 +- .../models/visco_resistive_deltaf_mhd.py | 2 +- .../visco_resistive_deltaf_mhd_with_q.py | 2 +- .../models/visco_resistive_linear_mhd.py | 2 +- .../visco_resistive_linear_mhd_with_q.py | 2 +- src/struphy/models/visco_resistive_mhd.py | 2 +- .../models/visco_resistive_mhd_with_p.py | 2 +- .../models/visco_resistive_mhd_with_q.py | 2 +- src/struphy/models/viscous_euler_sph.py | 2 +- src/struphy/models/viscous_fluid.py | 2 +- .../models/vlasov_ampere_one_species.py | 7 +- .../models/vlasov_maxwell_one_species.py | 4 +- src/struphy/propagators/base.py | 10 +- .../propagators/propagators_coupling.py | 24 ----- src/struphy/propagators/propagators_fields.py | 97 ------------------- .../propagators/propagators_markers.py | 40 -------- src/struphy/simulation/sim.py | 42 +++++++- 43 files changed, 165 insertions(+), 246 deletions(-) diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 4f764c8f2..47b602f37 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -45,10 +45,10 @@ The data can be accessed through the Struphy API. If ``ipython`` is installed, t and then:: - from struphy.main import pproc, load_data + from struphy import PostProcessor, PlottingData import os - path = os.path.join(os.getcwd(), "sim_1") - pproc(path) + pproc = PostProcessor(path_out=os.path.join(os.getcwd(), "sim_1")) + simdata = load_data(path) The variable ``simdata`` is of type :class:`~struphy.main.SimData` and holds grid and orbit information. diff --git a/src/struphy/feec/psydac_derham.py b/src/struphy/feec/psydac_derham.py index c0b98da30..5ea6f6df0 100644 --- a/src/struphy/feec/psydac_derham.py +++ b/src/struphy/feec/psydac_derham.py @@ -890,7 +890,7 @@ def create_spline_function( perturbations: Perturbation | list = None, domain: Domain = None, equil: FluidEquilibrium = None, - verbose: bool = True, + verbose: bool = False, ): """Creat a callable spline function. @@ -1442,7 +1442,7 @@ def __init__( perturbations: Perturbation | list = None, domain: Domain = None, equil: FluidEquilibrium = None, - verbose: bool = True, + verbose: bool = False, ): self._name = name self._space_id = space_id @@ -1494,7 +1494,7 @@ def __init__( print(f"\nAllocated SplineFuntion '{self.name}' in space '{self.space_id}'.") if self.backgrounds is not None or self.perturbations is not None: - self.initialize_coeffs(domain=self.domain, equil=self.equil) + self.initialize_coeffs(domain=self.domain, equil=self.equil, verbose=verbose) @property def name(self): @@ -1675,6 +1675,7 @@ def initialize_coeffs( perturbations: Perturbation | list = None, domain: Domain = None, equil: FluidEquilibrium = None, + verbose: bool = False, ): """ Set the initial conditions for self.vector. @@ -1707,14 +1708,14 @@ def initialize_coeffs( # start from zero coeffs self._vector *= 0.0 - if MPI.COMM_WORLD.Get_rank() == 0: + if verbose and MPI.COMM_WORLD.Get_rank() == 0: print(f"Initializing {self.name} ...") # add backgrounds to initial vector if self.backgrounds is not None: for fb in self.backgrounds: assert isinstance(fb, FieldsBackground) - if MPI.COMM_WORLD.Get_rank() == 0: + if verbose and MPI.COMM_WORLD.Get_rank() == 0: print(f"Adding background {fb} ...") # special case of const @@ -1769,7 +1770,7 @@ def f_tmp(e1, e2, e3): # add perturbations to coefficient vector if self.perturbations is not None: for ptb in self.perturbations: - if MPI.COMM_WORLD.Get_rank() == 0: + if verbose and MPI.COMM_WORLD.Get_rank() == 0: print(f"Adding perturbation {ptb} ...") # special case of white noise in logical space for different components diff --git a/src/struphy/fields_background/projected_equils.py b/src/struphy/fields_background/projected_equils.py index b429e995a..2055119aa 100644 --- a/src/struphy/fields_background/projected_equils.py +++ b/src/struphy/fields_background/projected_equils.py @@ -14,10 +14,15 @@ class ProjectedFluidEquilibrium: :class:`~struphy.fields_background.base.FluidEquilibrium` into Derham spaces. Return coefficients.""" - def __init__(self, equil: FluidEquilibrium, derham: Derham): + def __init__(self, equil: FluidEquilibrium, derham: Derham, verbose: bool = False): + self._equil = equil self._derham = derham + if verbose and derham.comm.Get_rank() == 0: + print(f"Projecting equilibrium '{equil.__class__.__name__}' into Derham spaces ...") + print(f"{self.derham = }") + # commuting projectors self._P0 = derham.P["0"] self._P1 = derham.P["1"] @@ -31,6 +36,9 @@ def __init__(self, equil: FluidEquilibrium, derham: Derham): self._E2T = derham.extraction_ops["2"].transpose() self._E3T = derham.extraction_ops["3"].transpose() self._EvT = derham.extraction_ops["v"].transpose() + + if verbose and derham.comm.Get_rank() == 0: + print("... Done.") @property def equil(self): @@ -187,8 +195,8 @@ class ProjectedFluidEquilibriumWithB(ProjectedFluidEquilibrium): :class:`~struphy.fields_background.base.FluidEquilibriumWithB` into Derham spaces. Return coefficients.""" - def __init__(self, equil: FluidEquilibriumWithB, derham: Derham): - super().__init__(equil, derham) + def __init__(self, equil: FluidEquilibriumWithB, derham: Derham, verbose: bool = False): + super().__init__(equil, derham, verbose=verbose) # ---------# # 0-forms # @@ -323,8 +331,8 @@ class ProjectedMHDequilibrium(ProjectedFluidEquilibriumWithB): :class:`~struphy.fields_background.base.MHDequilibrium` into Derham spaces. Return coefficients.""" - def __init__(self, equil: MHDequilibrium, derham: Derham): - super().__init__(equil, derham) + def __init__(self, equil: MHDequilibrium, derham: Derham, verbose: bool = False): + super().__init__(equil, derham, verbose=verbose) # ---------# # 0-forms # diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index a90ad1284..0f825a72f 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -79,7 +79,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 7649c83a7..51ac546c2 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -107,7 +107,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model '{self.__class__.__name__}' ...") # 1. instantiate all species self.em_fields = self.EMFields() @@ -142,6 +142,9 @@ def __init__(self): # initial Poisson (not a propagator used in time stepping) self.initial_poisson = propagators_fields.Poisson() self.initial_poisson.variables.phi = self.em_fields.phi + + if rank == 0: + print("... Done.") @property def bulk_species(self): @@ -197,7 +200,7 @@ def allocate_helpers(self, verbose: bool = False): phi = self.initial_poisson.variables.phi.spline.vector Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: - print("Done.") + print("... Done.") def update_scalar_quantities(self): # e*M1*e/2 diff --git a/src/struphy/models/deterministic_particle_diffusion.py b/src/struphy/models/deterministic_particle_diffusion.py index da3bed1a5..addada42d 100644 --- a/src/struphy/models/deterministic_particle_diffusion.py +++ b/src/struphy/models/deterministic_particle_diffusion.py @@ -60,7 +60,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 449aa8d9a..3a47c5557 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -99,7 +99,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/euler_sph.py b/src/struphy/models/euler_sph.py index e015ce83b..dea14f1f1 100644 --- a/src/struphy/models/euler_sph.py +++ b/src/struphy/models/euler_sph.py @@ -74,7 +74,7 @@ def __init__(self, with_B0: bool = True): def __init__(self, with_B0: bool = True): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") self.with_B0 = with_B0 diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index be20f8129..c7efdcc98 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -70,7 +70,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}' ***") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.kinetic_ions = self.KineticIons() @@ -86,6 +86,9 @@ def __init__(self): self.add_scalar("en_fv", compute="from_particles", variable=self.kinetic_ions.var) self.add_scalar("en_fB", compute="from_particles", variable=self.kinetic_ions.var) self.add_scalar("en_tot", compute="from_particles", variable=self.kinetic_ions.var) + + if rank == 0: + print("Done.") @property def bulk_species(self): diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index 5dce8cb95..b0c231e65 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -74,7 +74,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index cfa7b310e..749ec7957 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -86,7 +86,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index 8f1dc54f6..d91fc5ba0 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -76,7 +76,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index 3c5264e2c..d9758b508 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -141,7 +141,7 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): def __init__(self, turn_off: tuple[str, ...] = (None,)): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index f37ab335b..1a129a9da 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -114,7 +114,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index abee852de..fa0762d2b 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -121,7 +121,7 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): def __init__(self, turn_off: tuple[str, ...] = (None,)): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index b82294852..eca0c3c17 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -129,7 +129,7 @@ def __init__( with_E0: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() @@ -209,7 +209,7 @@ def allocate_helpers(self, verbose: bool = False): phi = self.initial_poisson.variables.phi.spline.vector Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: - print("Done.") + print("... Done.") def update_scalar_quantities(self): # e*M1*e/2 diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index 70ac125dc..1cc5978a6 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -129,7 +129,7 @@ def __init__( with_E0: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index 182a29a84..a64e81d70 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -58,7 +58,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index f42c6e4ac..e2fa74de3 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -65,7 +65,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() @@ -100,7 +100,7 @@ def allocate_helpers(self, verbose: bool = False): self.propagators.poisson(1.0) if MPI.COMM_WORLD.Get_rank() == 0: - print("Done.") + print("... Done.") def update_scalar_quantities(self): pass diff --git a/src/struphy/models/pressure_less_sph.py b/src/struphy/models/pressure_less_sph.py index df2c8c7f9..10ef6d200 100644 --- a/src/struphy/models/pressure_less_sph.py +++ b/src/struphy/models/pressure_less_sph.py @@ -55,7 +55,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.cold_fluid = self.ColdFluid() diff --git a/src/struphy/models/random_particle_diffusion.py b/src/struphy/models/random_particle_diffusion.py index e0d21e998..ebeeb65fa 100644 --- a/src/struphy/models/random_particle_diffusion.py +++ b/src/struphy/models/random_particle_diffusion.py @@ -59,7 +59,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index c86f9ae7e..78fcf8527 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -78,7 +78,7 @@ def allocate_helpers(self, verbose: bool = False): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/two_fluid_quasi_neutral_toy.py b/src/struphy/models/two_fluid_quasi_neutral_toy.py index 9c58f8f1f..811377c3a 100644 --- a/src/struphy/models/two_fluid_quasi_neutral_toy.py +++ b/src/struphy/models/two_fluid_quasi_neutral_toy.py @@ -83,7 +83,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMfields() diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 60ec0046e..ac9ff3401 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -95,13 +95,36 @@ def add_background(self, background, verbose=True): if not isinstance(self.backgrounds, list): self._backgrounds = [self.backgrounds] self._backgrounds += [background] - - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print( - f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added background '{background.__class__.__name__}' with:", - ) - for k, v in background.__dict__.items(): - print(f" {k}: {v}") + + def show_backgrounds(self): + if self.backgrounds is not None: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - backgrounds:") + if isinstance(self.backgrounds, list): + for background in self.backgrounds: + print(f" {background.__class__.__name__}:") + for k, v in background.__dict__.items(): + print(f" {k}: {v}") + else: + print(f" {self.backgrounds.__class__.__name__}:") + for k, v in self.backgrounds.__dict__.items(): + print(f" {k}: {v}") + else: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no background.") + + def show_perturbations(self): + if self.perturbations is not None: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - perturbations:") + if isinstance(self.perturbations, list): + for perturbation in self.perturbations: + print(f" {perturbation.__class__.__name__}:") + for k, v in perturbation.__dict__.items(): + print(f" {k}: {v}") + else: + print(f" {self.perturbations.__class__.__name__}:") + for k, v in self.perturbations.__dict__.items(): + print(f" {k}: {v}") + else: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no perturbation.") class FEECVariable(Variable): @@ -142,18 +165,12 @@ def add_perturbation(self, perturbation: Perturbation, verbose=True): self._perturbations = [self.perturbations] self._perturbations += [perturbation] - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print( - f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added perturbation '{perturbation.__class__.__name__}' with:", - ) - for k, v in perturbation.__dict__.items(): - print(f" {k}: {v}") - def allocate( self, derham: Derham, domain: Domain = None, equil: FluidEquilibrium = None, + verbose: bool = False, ): self._spline = derham.create_spline_function( name=self.__name__, @@ -162,6 +179,7 @@ def allocate( perturbations=self.perturbations, domain=domain, equil=equil, + verbose=verbose, ) @@ -206,12 +224,15 @@ def add_background(self, background: KineticBackground, n_as_volume_form: bool = def add_initial_condition(self, init: KineticBackground, verbose=True): """The initial condition must be consistent with the background.""" self._initial_condition = init - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print( - f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added initial condition '{init.__class__.__name__}' with:", - ) - for k, v in init.__dict__.items(): - print(f" {k}: {v}") + + def show_initial_condition(self): + if self.initial_condition is not None: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - initial condition:") + print(f" {self.initial_condition.__class__.__name__}:") + for k, v in self.initial_condition.__dict__.items(): + print(f" {k}: {v}") + else: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no initial condition.") @property def initial_condition(self) -> KineticBackground: @@ -360,10 +381,15 @@ def add_perturbation( self._perturbations["u2"] = del_u2 self._perturbations["u3"] = del_u3 - if verbose and MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added perturbation:") - for k, v in self._perturbations.items(): - print(f" {k}: {v}") + def show_perturbations(self): + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - perturbations:") + for key, perturbation in self.perturbations.items(): + if perturbation is not None: + print(f" {key}: {perturbation.__class__.__name__}") + for k, v in perturbation.__dict__.items(): + print(f" {k}: {v}") + else: + print(f" {key}: None") @property def perturbations(self) -> dict[str, Perturbation]: diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index 33c0d09e9..6757ee186 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -64,7 +64,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 1b8839f6a..1f52f39a5 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -74,7 +74,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index a593e96d6..61090628c 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -62,7 +62,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index 732de4115..a1256fa46 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -103,7 +103,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index 74fc7299c..dc1b9ae79 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -103,7 +103,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index a00e98d1e..2f48a0ead 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -101,7 +101,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index a23594b45..722f9bfdf 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -101,7 +101,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index b970de095..87cd6286d 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -100,7 +100,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index b980b3fd5..31b486f17 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -101,7 +101,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 6f9c6af24..9dde000fe 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -103,7 +103,7 @@ def __init__( with_resistivity: bool = True, ): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index be418eeed..1226a61c7 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -75,7 +75,7 @@ def __init__(self, with_B0: bool = True): def __init__(self, with_B0: bool = True): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") self.with_B0 = with_B0 diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index d323731f7..fc7ea010c 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -79,7 +79,7 @@ def __init__(self, with_viscosity: bool = True): def __init__(self, with_viscosity: bool = True): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 287c4bb3f..788354d22 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -120,7 +120,7 @@ def __init__(self, with_B0: bool = True): def __init__(self, with_B0: bool = True): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") self.with_B0 = with_B0 @@ -146,6 +146,9 @@ def __init__(self, with_B0: bool = True): # initial Poisson (not a propagator used in time stepping) self.initial_poisson = propagators_fields.Poisson() self.initial_poisson.variables.phi = self.em_fields.phi + + if rank == 0: + print("... Done.") @property def bulk_species(self): @@ -200,7 +203,7 @@ def allocate_helpers(self, verbose: bool = False): phi = self.initial_poisson.variables.phi.spline.vector Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: - print("Done.") + print("... Done.") def update_scalar_quantities(self): # e*M1*e/2 diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 369f73a33..74607bccb 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -131,7 +131,7 @@ def __init__(self): def __init__(self): if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() @@ -211,7 +211,7 @@ def allocate_helpers(self, verbose: bool = False): phi = self.initial_poisson.variables.phi.spline.vector Propagator.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) if MPI.COMM_WORLD.Get_rank() == 0: - print("Done.") + print("... Done.") def update_scalar_quantities(self): # e*M1*e/2 diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index d6d30d748..e7736fab8 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -71,10 +71,6 @@ def options(self) -> Options: @abstractmethod def options(self, new): assert isinstance(new, self.Options) - if True: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @abstractmethod @@ -92,6 +88,12 @@ def __call__(self, dt: float): Time step size. """ + def show_options(self): + """Print the options of the propagator.""" + print(f"\nOptions for propagator '{self.__class__.__name__}':") + for k, v in self.options.__dict__.items(): + print(f" {k}:".ljust(20), v) + def update_feec_variables(self, **new_coeffs): r"""Return max_diff = max(abs(new - old)) for each new_coeffs, update feec coefficients and update ghost regions. diff --git a/src/struphy/propagators/propagators_coupling.py b/src/struphy/propagators/propagators_coupling.py index 19e190604..9db561f9d 100644 --- a/src/struphy/propagators/propagators_coupling.py +++ b/src/struphy/propagators/propagators_coupling.py @@ -129,10 +129,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -380,10 +376,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -615,10 +607,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -936,10 +924,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1266,10 +1250,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1539,10 +1519,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile diff --git a/src/struphy/propagators/propagators_fields.py b/src/struphy/propagators/propagators_fields.py index 23b186d33..d6dbb056f 100644 --- a/src/struphy/propagators/propagators_fields.py +++ b/src/struphy/propagators/propagators_fields.py @@ -137,10 +137,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -329,10 +325,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -458,10 +450,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -610,10 +598,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -811,10 +795,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -963,10 +943,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1137,10 +1113,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1360,10 +1332,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1730,10 +1698,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -2014,10 +1978,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -2381,10 +2341,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -2577,10 +2533,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -2842,11 +2794,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - if "sigma" not in k and k not in ("divide_by_dt", "diffusion_mat"): - print(f" {k}: {v}") self._options = new @@ -2926,10 +2873,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -3219,10 +3162,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -3752,10 +3691,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -4156,10 +4091,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -4557,10 +4488,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -5155,10 +5082,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -5751,10 +5674,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -6509,10 +6428,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -7208,10 +7123,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -7539,10 +7450,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -7821,10 +7728,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index 250a9a6ca..98e52b6b7 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -83,10 +83,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -182,10 +178,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -315,10 +307,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -435,10 +423,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -584,10 +568,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1025,10 +1005,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1434,10 +1410,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1567,10 +1539,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1696,10 +1664,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile @@ -1833,10 +1797,6 @@ def options(self) -> Options: @options.setter def options(self, new): assert isinstance(new, self.Options) - if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\nNew options for propagator '{self.__class__.__name__}':") - for k, v in new.__dict__.items(): - print(f" {k}: {v}") self._options = new @profile diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 5c586ac81..0dc5e5ce9 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -124,7 +124,7 @@ def __init__( self.model_name = model.__class__.__name__ if self.rank == 0: - print(f"*** Instantiating simulation for model '{self.model_name}':") + print(f"Instance of simulation for model {self.model_name} ...") # meta-data path_out = env.path_out @@ -235,6 +235,7 @@ def __init__( if MPI.COMM_WORLD.Get_rank() == 0: self._post_processor = PostProcessor(sim=self) self._plotting_data = PlottingData(sim=self) + print("\n... Done.") # ---------------- # Abstract methods @@ -264,6 +265,10 @@ def show_parameters(self): print("") def allocate(self, verbose: bool = False): + + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nAllocating simulation data ...") + # feec self._allocate_feec(self.grid, self.derham_opts, verbose=verbose) @@ -275,6 +280,9 @@ def allocate(self, verbose: bool = False): # allocate helper fields and perform initial solves if needed self.model.allocate_helpers(verbose=verbose) + + if MPI.COMM_WORLD.Get_rank() == 0: + print("... Done.") def save_geometry_and_equil_vtk(self, verbose: bool = False): # store geometry vtk @@ -321,6 +329,26 @@ def initialize_data_storage(self, verbose: bool = False): self.data.add_data({key_time_restart: val}) def run(self, verbose: bool = False): + print(f"\nStarting simulation run for model {self.model_name} ...") + + # Display propagator options and intial conditions: + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nPROPAGATOR OPTIONS:") + for prop in self.model.prop_list: + assert isinstance(prop, Propagator) + prop.show_options() + + print("\nINITIAL CONDITIONS:") + for species in self.model.species.values(): + assert isinstance(species, Species) + for variable in species.variables.values(): + if isinstance(variable, FEECVariable): + variable.show_backgrounds() + variable.show_perturbations() + elif isinstance(variable, PICVariable) or isinstance(variable, SPHVariable): + variable.show_backgrounds() + variable.show_perturbations() + variable.show_initial_condition() if not self.env.restart: # equation paramters @@ -756,15 +784,15 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio self._mass_ops = WeightedMassOperators( self.derham, self.domain, - verbose=verbose, eq_mhd=self.equil, + verbose=verbose ) self._basis_ops = BasisProjectionOperators( self.derham, self.domain, - verbose=verbose, eq_mhd=self.equil, + verbose=verbose, ) # create projected equilibrium @@ -775,16 +803,19 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio self._projected_equil = ProjectedMHDequilibrium( self.equil, self.derham, + verbose=verbose, ) elif isinstance(self.equil, FluidEquilibriumWithB): self._projected_equil = ProjectedFluidEquilibriumWithB( self.equil, self.derham, + verbose=verbose, ) elif isinstance(self.equil, FluidEquilibrium): self._projected_equil = ProjectedFluidEquilibrium( self.equil, self.derham, + verbose=verbose, ) else: self._projected_equil = None @@ -804,6 +835,7 @@ def _allocate_variables(self, verbose: bool = False): derham=self.derham, domain=self.domain, equil=self.equil, + verbose=verbose, ) # allocate memory for FE coeffs of fluid variables @@ -816,6 +848,7 @@ def _allocate_variables(self, verbose: bool = False): derham=self.derham, domain=self.domain, equil=self.equil, + verbose=verbose, ) # allocate memory for marker arrays of kinetic variables @@ -851,6 +884,7 @@ def _allocate_variables(self, verbose: bool = False): derham=self.derham, domain=self.domain, equil=self.equil, + verbose=verbose, ) # TODO: allocate memory for FE coeffs of diagnostics @@ -882,7 +916,7 @@ def _allocate_propagators(self, verbose: bool = False): for prop in self.model.prop_list: assert isinstance(prop, Propagator) prop.allocate(verbose=verbose) - if MPI.COMM_WORLD.Get_rank() == 0: + if verbose and MPI.COMM_WORLD.Get_rank() == 0: print(f"\nAllocated propagator '{prop.__class__.__name__}'.") @profile From 729613fc01e2aa744789e2b2ec9652a555c283ee Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 07:50:06 +0100 Subject: [PATCH 41/80] bug fix and formatting --- .../fields_background/projected_equils.py | 4 ++-- src/struphy/models/cold_plasma_vlasov.py | 2 +- src/struphy/models/guiding_center.py | 2 +- src/struphy/models/variables.py | 8 +++++--- .../models/vlasov_ampere_one_species.py | 2 +- src/struphy/simulation/sim.py | 19 +++++++------------ 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/struphy/fields_background/projected_equils.py b/src/struphy/fields_background/projected_equils.py index 2055119aa..a74cfca9f 100644 --- a/src/struphy/fields_background/projected_equils.py +++ b/src/struphy/fields_background/projected_equils.py @@ -15,7 +15,7 @@ class ProjectedFluidEquilibrium: Return coefficients.""" def __init__(self, equil: FluidEquilibrium, derham: Derham, verbose: bool = False): - + self._equil = equil self._derham = derham @@ -36,7 +36,7 @@ def __init__(self, equil: FluidEquilibrium, derham: Derham, verbose: bool = Fals self._E2T = derham.extraction_ops["2"].transpose() self._E3T = derham.extraction_ops["3"].transpose() self._EvT = derham.extraction_ops["v"].transpose() - + if verbose and derham.comm.Get_rank() == 0: print("... Done.") diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 51ac546c2..cd65dd6dd 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -142,7 +142,7 @@ def __init__(self): # initial Poisson (not a propagator used in time stepping) self.initial_poisson = propagators_fields.Poisson() self.initial_poisson.variables.phi = self.em_fields.phi - + if rank == 0: print("... Done.") diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index c7efdcc98..1adff8123 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -86,7 +86,7 @@ def __init__(self): self.add_scalar("en_fv", compute="from_particles", variable=self.kinetic_ions.var) self.add_scalar("en_fB", compute="from_particles", variable=self.kinetic_ions.var) self.add_scalar("en_tot", compute="from_particles", variable=self.kinetic_ions.var) - + if rank == 0: print("Done.") diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index ac9ff3401..6709b8561 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -95,7 +95,7 @@ def add_background(self, background, verbose=True): if not isinstance(self.backgrounds, list): self._backgrounds = [self.backgrounds] self._backgrounds += [background] - + def show_backgrounds(self): if self.backgrounds is not None: print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - backgrounds:") @@ -110,7 +110,7 @@ def show_backgrounds(self): print(f" {k}: {v}") else: print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no background.") - + def show_perturbations(self): if self.perturbations is not None: print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - perturbations:") @@ -232,7 +232,9 @@ def show_initial_condition(self): for k, v in self.initial_condition.__dict__.items(): print(f" {k}: {v}") else: - print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no initial condition.") + print( + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no initial condition." + ) @property def initial_condition(self) -> KineticBackground: diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 788354d22..9d2d6bd47 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -146,7 +146,7 @@ def __init__(self, with_B0: bool = True): # initial Poisson (not a propagator used in time stepping) self.initial_poisson = propagators_fields.Poisson() self.initial_poisson.variables.phi = self.em_fields.phi - + if rank == 0: print("... Done.") diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 0dc5e5ce9..22318fbfa 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -265,10 +265,10 @@ def show_parameters(self): print("") def allocate(self, verbose: bool = False): - + if MPI.COMM_WORLD.Get_rank() == 0: print("\nAllocating simulation data ...") - + # feec self._allocate_feec(self.grid, self.derham_opts, verbose=verbose) @@ -280,7 +280,7 @@ def allocate(self, verbose: bool = False): # allocate helper fields and perform initial solves if needed self.model.allocate_helpers(verbose=verbose) - + if MPI.COMM_WORLD.Get_rank() == 0: print("... Done.") @@ -337,15 +337,15 @@ def run(self, verbose: bool = False): for prop in self.model.prop_list: assert isinstance(prop, Propagator) prop.show_options() - + print("\nINITIAL CONDITIONS:") for species in self.model.species.values(): assert isinstance(species, Species) for variable in species.variables.values(): - if isinstance(variable, FEECVariable): + if isinstance(variable, FEECVariable) or isinstance(variable, SPHVariable): variable.show_backgrounds() variable.show_perturbations() - elif isinstance(variable, PICVariable) or isinstance(variable, SPHVariable): + elif isinstance(variable, PICVariable): variable.show_backgrounds() variable.show_perturbations() variable.show_initial_condition() @@ -781,12 +781,7 @@ def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptio self._mass_ops = None self._basis_ops = None else: - self._mass_ops = WeightedMassOperators( - self.derham, - self.domain, - eq_mhd=self.equil, - verbose=verbose - ) + self._mass_ops = WeightedMassOperators(self.derham, self.domain, eq_mhd=self.equil, verbose=verbose) self._basis_ops = BasisProjectionOperators( self.derham, From 30f054839dd5b65ecdee6a63b62849bbf21fc602 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 09:26:31 +0100 Subject: [PATCH 42/80] pass params_path=__file__ to StruphySimulation --- src/struphy/models/base.py | 1 + .../post_processing/post_processing_tools.py | 2 +- src/struphy/simulation/sim.py | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 30287dfb3..a1c3791b5 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -578,6 +578,7 @@ def generate_default_parameter_file( file.write("\n# Simulation object\n") file.write("""sim = StruphySimulation( model=model, + params_path=__file__, env=env, base_units=base_units, time_opts=time_opts, diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index ad44576d3..bc95d122f 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -89,7 +89,7 @@ def __init__( else: raise FileNotFoundError(f"Neither of the paths {params_path} or {bin_path} exists.") - print("done.") + print("\n... Done.") self.env = env self.units = base_units diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 22318fbfa..faf924191 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -139,7 +139,7 @@ def __init__( self.meta["platform"] = sysconfig.get_platform() self.meta["python version"] = sysconfig.get_python_version() self.meta["model name"] = self.model_name - self.meta["parameter file"] = params_path + self.meta["parameter file"] = self.params_path self.meta["output folder"] = path_out self.meta["MPI processes"] = self.comm_size self.meta["use MPI.COMM_WORLD"] = use_mpi @@ -163,12 +163,15 @@ def __init__( # save parameter file if self.rank == 0: # save python param file - if params_path is not None: - assert params_path[-3:] == ".py" - shutil.copy2( - params_path, - os.path.join(path_out, "parameters.py"), - ) + if self.params_path is not None: + assert self.params_path[-3:] == ".py" + try: + shutil.copy2( + self.params_path, + os.path.join(path_out, "parameters.py"), + ) + except shutil.SameFileError: + pass # pickle struphy objects else: with open(os.path.join(path_out, "env.bin"), "wb") as f: @@ -1119,7 +1122,7 @@ def model(self): @property def params_path(self): - """Path to parameter file used for the run.""" + """Path to parameter file used for the run. Can be None if Simulation is instantiated in a notebook environment (no parameter file in this case).""" return self._params_path @property From 1449b5e352bb6fd427db2e95092534a71c6ddee3 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 11:07:11 +0100 Subject: [PATCH 43/80] introduce class properties in Particles classes; move deletion of data out of _setup_folders into separate method; instantiate PostPorcessor and PlottingData when methods are called in the sim class --- src/struphy/models/variables.py | 14 +++ src/struphy/pic/particles.py | 94 +++++---------- .../post_processing/post_processing_tools.py | 18 +-- src/struphy/simulation/sim.py | 112 +++++++++--------- 4 files changed, 107 insertions(+), 131 deletions(-) diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 6709b8561..78d38bb7c 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -3,6 +3,7 @@ from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING +import inspect import cunumpy as xp from feectools.ddm.mpi import mpi as MPI @@ -144,6 +145,8 @@ def space(self): @property def spline(self) -> SplineFunction: + if not hasattr(self, "_spline"): + raise ValueError("Warning: spline not allocated yet. Call allocate() first.") return self._spline @property @@ -195,13 +198,22 @@ class PICVariable(Variable): def __init__(self, space: LiteralOptions.OptsPICSpace = "Particles6D"): check_option(space, LiteralOptions.OptsPICSpace) self._space = space + for name, cls in inspect.getmembers(particles): + if inspect.isclass(cls) and cls.__module__ == particles.__name__ and name == space: + self._particles_class = cls @property def space(self): return self._space + + @property + def particles_class(self) -> Particles: + return self._particles_class @property def particles(self) -> Particles: + if not hasattr(self, "_particles"): + raise ValueError("Warning: particles not allocated yet. Call allocate() first.") return self._particles @property @@ -348,6 +360,8 @@ def space(self): @property def particles(self) -> ParticlesSPH: + if not hasattr(self, "_particles"): + raise ValueError("Warning: particles not allocated yet. Call allocate() first.") return self._particles @property diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 15d46af37..84502827a 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -26,6 +26,12 @@ class Particles6D(Particles): value position (eta) velocities weight s0 w0 buffer ===== ============== ======================= ======= ====== ====== ========== """ + + # Class properties + vdim = 3 + type = "full_f" + default_background = maxwellians.Maxwellian3D() + default_n_cols = {"diagnostics": 0, "aux": 5} def __post_init__(self): if isinstance(self.background, maxwellians.CanonicalMaxwellian): @@ -37,22 +43,6 @@ def __post_init__(self): self._derham = self.projected_equil.derham self._epsilon = self.equation_params["epsilon"] - @property - def type(self): - return "full_f" - - @property - def vdim(self): - return 3 - - @property - def default_background(self): - return maxwellians.Maxwellian3D() - - @property - def default_n_cols(self): - return {"diagnostics": 0, "aux": 5} - def svol(self, eta1, eta2, eta3, *v): """Sampling density function as volume form. @@ -221,6 +211,9 @@ class DeltaFParticles6D(Particles6D): A class for kinetic species in full 6D phase space that solve for delta_f = f - f0. """ + # Class properties + type = "delta_f" + def __post_init__(self): self.weights_params.control_variate = False @@ -272,6 +265,12 @@ class Particles5D(Particles): Parameters for markers, see :class:`~struphy.pic.base.Particles`. """ + # Class properties + vdim = 2 + type = "full_f" + default_background = maxwellians.GyroMaxwellian2D() + default_n_cols = {"diagnostics": 3, "aux": 12} + def __post_init__(self): assert self.projected_equil is not None, "Particles5D needs a projected MHD equilibrium." @@ -287,23 +286,6 @@ def __post_init__(self): self._tmp0 = self.derham.Vh["0"].zeros() self._tmp2 = self.derham.Vh["2"].zeros() - @property - def type(self): - return "full_f" - - @property - def vdim(self): - """Dimension of the velocity space.""" - return 2 - - @property - def default_background(self): - return maxwellians.GyroMaxwellian2D() - - @property - def default_n_cols(self): - return {"diagnostics": 3, "aux": 12} - @property def magn_bckgr(self): """Fluid equilibrium with B.""" @@ -573,26 +555,15 @@ class Particles3D(Particles): Parameters for markers, see :class:`~struphy.pic.base.Particles`. """ + # Class properties + vdim = 0 + type = "full_f" + default_background = maxwellians.ColdPlasma() + default_n_cols = {"diagnostics": 0, "aux": 5} + def __post_init__(self): pass - @property - def type(self): - return "full_f" - - @property - def vdim(self): - """Dimension of the velocity space.""" - return 0 - - @property - def default_background(self): - return maxwellians.ColdPlasma() - - @property - def default_n_cols(self): - return {"diagnostics": 0, "aux": 5} - def svol(self, eta1, eta2, eta3): """Sampling density function as volume form. @@ -677,27 +648,16 @@ class ParticlesSPH(Particles): Parameters for markers, see :class:`~struphy.pic.base.Particles`. """ + # Class properties + vdim = 3 + type = "sph" + default_background = equils.ConstantVelocity() + default_n_cols = {"diagnostics": 0, "aux": 24} + def __post_init__(self): assert self.clone_config is None, "SPH can only be launched with --nclones 1" self.background.domain = self.domain - @property - def type(self): - return "sph" - - @property - def vdim(self): - """Dimension of the velocity space.""" - return 3 - - @property - def default_background(self): - return equils.ConstantVelocity() - - @property - def default_n_cols(self): - return {"diagnostics": 0, "aux": 24} - def svol(self, eta1, eta2, eta3, *v): """Sampling density function as volume form. diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index bc95d122f..6ca86e5ea 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -167,7 +167,7 @@ def plot_time_traces(self): plot_gantt_chart_plotly(path_time_trace, output_path=self.path_pproc) return - def pproc( + def process( self, step: int = 1, celldivide: int = 1, @@ -177,7 +177,7 @@ def pproc( create_vtk: bool = True, verbose: bool = False, ): - """Do post processing for folder path_out. + """Do post processing of data in self.path_out. Parameters ---------- @@ -234,7 +234,7 @@ def pproc( self.exist_particles = None # feec variables - self.pproc_fields( + self.process_fields( step=step, celldivide=celldivide, physical=physical, @@ -243,14 +243,14 @@ def pproc( ) # particle variables - self.pproc_particles( + self.process_particles( step=step, guiding_center=guiding_center, classify=classify, verbose=verbose, ) - def pproc_fields( + def process_fields( self, step: int = 1, celldivide: int = 1, @@ -308,7 +308,7 @@ def pproc_fields( if physical: self._create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) - def pproc_particles( + def process_particles( self, step: int = 1, guiding_center: bool = False, @@ -738,10 +738,10 @@ def _post_process_markers( nt, n_markers, n_cols = file_0["kinetic/" + species + "/markers"].shape # get velocity dimension from one of the variables of the species - for varname, var in species_obj.variables.items(): + for _, var in species_obj.variables.items(): assert isinstance(var, PICVariable | SPHVariable) - obj: Particles = var.particles - vdim = obj.vdim + cls: Particles = var.particles_class + vdim = cls.vdim break log_nt = int(xp.log10(int(((nt - 1) / step)))) + 1 diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index faf924191..4f09894e8 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -155,8 +155,6 @@ def __init__( # creating output folders self._setup_folders( - path_out=path_out, - restart=restart, verbose=verbose, ) @@ -233,11 +231,8 @@ def __init__( verbose=verbose, ) model.setup_equation_params(units=self.units, verbose=verbose) - - # setup post processor and plotting - if MPI.COMM_WORLD.Get_rank() == 0: - self._post_processor = PostProcessor(sim=self) - self._plotting_data = PlottingData(sim=self) + + if self.rank == 0: print("\n... Done.") # ---------------- @@ -333,6 +328,8 @@ def initialize_data_storage(self, verbose: bool = False): def run(self, verbose: bool = False): print(f"\nStarting simulation run for model {self.model_name} ...") + + self._remove_existing_output_files(verbose=verbose) # Display propagator options and intial conditions: if MPI.COMM_WORLD.Get_rank() == 0: @@ -531,10 +528,14 @@ def pproc( time_trace: bool = False, verbose: bool = False, ): + # setup post processor and plotting + if not hasattr(self, "_post_processor") and self.rank == 0: + self._post_processor = PostProcessor(sim=self) + if time_trace: self.post_processor.plot_time_traces(verbose=verbose) - self.post_processor.pproc( + self.post_processor.process( step=step, celldivide=celldivide, physical=physical, @@ -546,6 +547,8 @@ def pproc( return self.post_processor def load_plotting_data(self, verbose: bool = False): + if not hasattr(self, "_plotting_data") and self.rank == 0: + self._plotting_data = PlottingData(sim=self) self.plotting_data.load(verbose=verbose) return self.plotting_data @@ -660,12 +663,7 @@ def compute_plasma_params(self, verbose: bool = True): # Private methods # --------------- - def _setup_folders( - self, - path_out: str, - restart: bool, - verbose: bool = False, - ): + def _setup_folders(self, verbose: bool = False): """ Setup output folders. """ @@ -674,51 +672,55 @@ def _setup_folders( print("\nPREPARATION AND CLEAN-UP:") # create output folder if it does not exit - if not os.path.exists(path_out): - os.makedirs(path_out, exist_ok=True) + if not os.path.exists(self.env.path_out): + os.makedirs(self.env.path_out, exist_ok=True) if verbose: - print("Created folder " + path_out) + print("Created folder " + self.env.path_out) # create data folder in output folder if it does not exist - if not os.path.exists(os.path.join(path_out, "data/")): - os.mkdir(os.path.join(path_out, "data/")) + if not os.path.exists(os.path.join(self.env.path_out, "data/")): + os.mkdir(os.path.join(self.env.path_out, "data/")) if verbose: - print("Created folder " + os.path.join(path_out, "data/")) - else: - # remove post_processing folder - folder = os.path.join(path_out, "post_processing") - if os.path.exists(folder): - shutil.rmtree(folder) - if verbose: - print("Removed existing folder " + folder) - - # remove meta file - file = os.path.join(path_out, "meta.txt") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove profiling file - file = os.path.join(path_out, "profile_tmp") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove .png files (if NOT a restart) - if not restart: - files = glob.glob(os.path.join(path_out, "*.png")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) + print("Created folder " + os.path.join(self.env.path_out, "data/")) + + def _remove_existing_output_files(self, verbose: bool = False): + """Removes post_processing/, meta.txt and profile_tmp. + If not restart, also removes existing hdf5 and png files in output folder.""" + # remove post_processing folder + folder = os.path.join(self.env.path_out, "post_processing") + if os.path.exists(folder): + shutil.rmtree(folder) + if verbose: + print("Removed existing folder " + folder) + + # remove meta file + file = os.path.join(self.env.path_out, "meta.txt") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove profiling file + file = os.path.join(self.env.path_out, "profile_tmp") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) + + # remove hdf5 and png files (if NOT a restart) + if not self.env.restart: + files = glob.glob(os.path.join(self.env.path_out, "data", "*.hdf5")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + files = glob.glob(os.path.join(self.env.path_out, "*.png")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool = False): """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" From 3d0ded5aae6bebc975a58eb0246110e8dfccd843 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 11:14:16 +0100 Subject: [PATCH 44/80] fix quickstart --- doc/sections/quickstart.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 47b602f37..d0e5a068e 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -47,24 +47,29 @@ and then:: from struphy import PostProcessor, PlottingData import os - pproc = PostProcessor(path_out=os.path.join(os.getcwd(), "sim_1")) - - simdata = load_data(path) -The variable ``simdata`` is of type :class:`~struphy.main.SimData` and holds grid and orbit information. + path_out = os.path.join(os.getcwd(), "sim_1") + + pp = PostProcessor(path_out=path_out) + pp.process(verbose=True) + + pdata = PlottingData(path_out=path_out) + pdata.load(verbose=True) + +The object ``pdata`` holds grid and orbit information. You can deduce the kind of info held from the screen output. For instance, you have access several ``grids`` as well as to, for instance:: - print(simdata.spline_values["em_fields"]["e_field_log"].keys()) - print(simdata.orbits["kinetic_ions"].shape) - print(simdata.f["kinetic_ions"]["e1_density"].keys()) + print(pdata.spline_values["em_fields"]["e_field_log"].keys()) + print(pdata.orbits["kinetic_ions"].shape) + print(pdata.f["kinetic_ions"]["e1_density"].keys()) -Under ``simdata.spline_values`` you find dictionaries holding splines values at the pre-defined ``simdata.grids_log`` +Under ``pdata.spline_values`` you find dictionaries holding splines values at the pre-defined ``pdata.grids_log`` (or the physical grid); the keys are the time points of evaluation. -Under ``simdata.orbits`` you find numpy arrays holding orbit data, indexed by ``[time, particle, attribute]``. +Under ``pdata.orbits`` you find numpy arrays holding orbit data, indexed by ``[time, particle, attribute]``. -Under ``simdata.f`` you find binning data, in this case a 1d binning plot in the first logical coordinate :math:`\eta_1`-direction +Under ``pdata.f`` you find binning data, in this case a 1d binning plot in the first logical coordinate :math:`\eta_1`-direction (see :ref:`binning` for details). Parallel simulations can invoked from the same launch file for instance by:: From d9802b9300b491c00a051c8a13196650162b671f Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 11:32:58 +0100 Subject: [PATCH 45/80] new method spawn_sister() in StruphySimulation --- src/struphy/simulation/sim.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 4f09894e8..cecffcd21 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -659,6 +659,37 @@ def compute_plasma_params(self, verbose: bool = True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) + def spawn_sister(self, params_path: str = None, + env: EnvironmentOptions = None, + time_opts: Time = None, + grid: grids.TensorProductGrid = None, + derham_opts: DerhamOptions = None, + ): + """Spawn a sister simulation with the same model, base_units, domain and equilibrium + but different parameters and options otherwise. + This can be used to run multiple simulations with the same model + but different parameters in parallel on the same cluster.""" + if env is None: + env = self.env + if time_opts is None: + time_opts = self.time_opts + if grid is None: + grid = self.grid + if derham_opts is None: + derham_opts = self.derham_opts + + sister = StruphySimulation(model=self.model, + params_path=params_path, + env=env, + base_units=self.base_units, + time_opts=time_opts, + domain=self.domain, + equil=self.equil, + grid=grid, + derham_opts=derham_opts, + verbose=False) + return sister + # --------------- # Private methods # --------------- @@ -721,7 +752,6 @@ def _remove_existing_output_files(self, verbose: bool = False): if verbose and n < 10: # print only ten statements in case of many processes print("Removed existing file " + file) - def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool = False): """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" if equil is not None: From e6f7dfe795e93b543c509730b51a7ab34051c9f2 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 15:27:35 +0100 Subject: [PATCH 46/80] make Maxwell the default model in a simulation --- src/struphy/models/maxwell.py | 2 -- src/struphy/simulation/sim.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index a64e81d70..aeaa429d4 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -57,8 +57,6 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index cecffcd21..5c20a33c3 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -27,6 +27,7 @@ equils, grids, ) +from struphy.models import Maxwell # core imports from struphy.feec.basis_projection_ops import BasisProjectionOperators @@ -65,7 +66,7 @@ class StruphySimulation(Simulation): def __init__( self, - model: StruphyModel, + model: StruphyModel = Maxwell(), params_path: str = None, env: EnvironmentOptions = EnvironmentOptions(), base_units: BaseUnits = BaseUnits(), @@ -668,7 +669,7 @@ def spawn_sister(self, params_path: str = None, """Spawn a sister simulation with the same model, base_units, domain and equilibrium but different parameters and options otherwise. This can be used to run multiple simulations with the same model - but different parameters in parallel on the same cluster.""" + but different discretization parameters or MPI configs.""" if env is None: env = self.env if time_opts is None: From 94a3b6bd4e9d4984f99212793fa9df717f820ddd Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Feb 2026 15:29:19 +0100 Subject: [PATCH 47/80] remove print statement in model __init__ --- src/struphy/models/cold_plasma.py | 3 +-- src/struphy/models/deterministic_particle_diffusion.py | 3 +-- src/struphy/models/drift_kinetic_electrostatic_adiabatic.py | 3 +-- src/struphy/models/euler_sph.py | 3 +-- src/struphy/models/guiding_center.py | 3 +-- src/struphy/models/hasegawa_wakatani.py | 3 +-- src/struphy/models/linear_extended_mh_duniform.py | 3 +-- src/struphy/models/linear_mhd.py | 3 +-- src/struphy/models/linear_mhd_driftkinetic_cc.py | 3 +-- src/struphy/models/linear_mhd_vlasov_cc.py | 3 +-- src/struphy/models/linear_mhd_vlasov_pc.py | 3 +-- src/struphy/models/linear_vlasov_ampere_one_species.py | 3 +-- src/struphy/models/linear_vlasov_maxwell_one_species.py | 3 +-- src/struphy/models/poisson.py | 3 +-- src/struphy/models/pressure_less_sph.py | 3 +-- src/struphy/models/random_particle_diffusion.py | 3 +-- src/struphy/models/shear_alfven.py | 3 +-- src/struphy/models/two_fluid_quasi_neutral_toy.py | 3 +-- src/struphy/models/variational_barotropic_fluid.py | 3 +-- src/struphy/models/variational_compressible_fluid.py | 3 +-- src/struphy/models/variational_pressureless_fluid.py | 3 +-- src/struphy/models/visco_resistive_deltaf_mhd.py | 3 +-- src/struphy/models/visco_resistive_deltaf_mhd_with_q.py | 3 +-- src/struphy/models/visco_resistive_linear_mhd.py | 3 +-- src/struphy/models/visco_resistive_linear_mhd_with_q.py | 3 +-- src/struphy/models/visco_resistive_mhd.py | 3 +-- src/struphy/models/visco_resistive_mhd_with_p.py | 3 +-- src/struphy/models/visco_resistive_mhd_with_q.py | 3 +-- src/struphy/models/viscous_euler_sph.py | 3 +-- src/struphy/models/viscous_fluid.py | 3 +-- src/struphy/models/vlasov_ampere_one_species.py | 3 +-- src/struphy/models/vlasov_maxwell_one_species.py | 3 +-- 32 files changed, 32 insertions(+), 64 deletions(-) diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index 0f825a72f..a7e46c207 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -78,8 +78,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/deterministic_particle_diffusion.py b/src/struphy/models/deterministic_particle_diffusion.py index addada42d..0073543a7 100644 --- a/src/struphy/models/deterministic_particle_diffusion.py +++ b/src/struphy/models/deterministic_particle_diffusion.py @@ -59,8 +59,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 3a47c5557..35f61d762 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -98,8 +98,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/euler_sph.py b/src/struphy/models/euler_sph.py index dea14f1f1..ddae4a1de 100644 --- a/src/struphy/models/euler_sph.py +++ b/src/struphy/models/euler_sph.py @@ -73,8 +73,7 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + self.with_B0 = with_B0 diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index 1adff8123..0de4224cf 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -69,8 +69,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index b0c231e65..a2ffe2019 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -73,8 +73,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index 749ec7957..8a3dedfa3 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -85,8 +85,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index d91fc5ba0..afbb21b9f 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -75,8 +75,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index d9758b508..bb23342ce 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -140,8 +140,7 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index 1a129a9da..e1a9dcc0e 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -113,8 +113,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index fa0762d2b..b7283742a 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -120,8 +120,7 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index eca0c3c17..6d23620d5 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -128,8 +128,7 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index 1cc5978a6..e20d5d31b 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -128,8 +128,7 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index e2fa74de3..c86aae566 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -64,8 +64,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/pressure_less_sph.py b/src/struphy/models/pressure_less_sph.py index 10ef6d200..882111899 100644 --- a/src/struphy/models/pressure_less_sph.py +++ b/src/struphy/models/pressure_less_sph.py @@ -54,8 +54,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.cold_fluid = self.ColdFluid() diff --git a/src/struphy/models/random_particle_diffusion.py b/src/struphy/models/random_particle_diffusion.py index ebeeb65fa..e31676ef2 100644 --- a/src/struphy/models/random_particle_diffusion.py +++ b/src/struphy/models/random_particle_diffusion.py @@ -58,8 +58,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index 78fcf8527..0a60bb1de 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -77,8 +77,7 @@ def allocate_helpers(self, verbose: bool = False): self._tmp_b2 = Propagator.derham.Vh["2"].zeros() def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/two_fluid_quasi_neutral_toy.py b/src/struphy/models/two_fluid_quasi_neutral_toy.py index 811377c3a..b2ae05fa6 100644 --- a/src/struphy/models/two_fluid_quasi_neutral_toy.py +++ b/src/struphy/models/two_fluid_quasi_neutral_toy.py @@ -82,8 +82,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMfields() diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index 6757ee186..40a8ac5ef 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -63,8 +63,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 1f52f39a5..84248d7f1 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -73,8 +73,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index 61090628c..ffebd91fe 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -61,8 +61,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index a1256fa46..c016af76c 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -102,8 +102,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index dc1b9ae79..44e3253f5 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -102,8 +102,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index 2f48a0ead..d52ea637c 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -100,8 +100,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index 722f9bfdf..143c9dd65 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -100,8 +100,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index 87cd6286d..c6af16586 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -99,8 +99,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index 31b486f17..c8189d681 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -100,8 +100,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 9dde000fe..8250544df 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -102,8 +102,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index 1226a61c7..afe20e2d4 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -74,8 +74,7 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + self.with_B0 = with_B0 diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index fc7ea010c..ccfb7670f 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -78,8 +78,7 @@ def __init__(self, with_viscosity: bool = True): ## abstract methods def __init__(self, with_viscosity: bool = True): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 9d2d6bd47..715d29183 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -119,8 +119,7 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + self.with_B0 = with_B0 diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 74607bccb..0a959d5f1 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -130,8 +130,7 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model {self.__class__.__name__} ...") + # 1. instantiate all species self.em_fields = self.EMFields() From fc51176808134cf8995887b3b5e5bfe9e6a18a39 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 18 Feb 2026 17:16:58 +0100 Subject: [PATCH 48/80] fixed tutorial 02; nices screen output; new classes for plotting data --- src/struphy/initial/base.py | 6 + src/struphy/initial/perturbations.py | 49 +++--- src/struphy/io/options.py | 2 +- src/struphy/kinetic_background/base.py | 7 + src/struphy/models/base.py | 10 +- src/struphy/models/cold_plasma_vlasov.py | 2 - src/struphy/models/species.py | 6 +- src/struphy/models/variables.py | 20 +-- src/struphy/models/vlasov.py | 2 - .../post_processing/post_processing_tools.py | 150 ++++++++++++------ src/struphy/simulation/sim.py | 34 ++-- struphy-tutorials | 2 +- 12 files changed, 176 insertions(+), 114 deletions(-) diff --git a/src/struphy/initial/base.py b/src/struphy/initial/base.py index 61a8cceb3..acee33124 100644 --- a/src/struphy/initial/base.py +++ b/src/struphy/initial/base.py @@ -15,6 +15,12 @@ def __call__(self, eta1, eta2, eta3, flat_eval=False): def prepare_eval_pts(self): # TODO: we could prepare the arguments via a method in this base class (flat_eval, sparse meshgrid, etc.). pass + + def __repr__(self): + print(f" {self.__class__.__name__}:") + for k, v in self.__dict__.items(): + print(f" {k}: {v}") + return "" @property def given_in_basis(self) -> str: diff --git a/src/struphy/initial/perturbations.py b/src/struphy/initial/perturbations.py index aa5a4bc8b..7694bd044 100644 --- a/src/struphy/initial/perturbations.py +++ b/src/struphy/initial/perturbations.py @@ -163,25 +163,26 @@ def __init__( else: assert len(pfuns_params) == n_modes - self._pfuns = [] + self.pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": - self._pfuns += [lambda eta3: 1.0] + self.pfuns += [lambda eta3: 1.0] elif pfun == "localize": - self._pfuns += [ + self.pfuns += [ lambda eta3: xp.tanh((eta3 - 0.5) / params) / xp.cosh((eta3 - 0.5) / params), ] else: raise ValueError(f"Profile function {pfun} is not defined..") - self._ls = ls - self._ms = ms - self._ns = ns - self._amps = amps - self._Lx = Lx - self._Ly = Ly - self._Lz = Lz - self._theta = theta + self.ls = tuple(ls) + self.ms = tuple(ms) + self.ns = tuple(ns) + self.amps = tuple(amps) + self.Lx = Lx + self.Ly = Ly + self.Lz = Lz + self.theta = tuple(theta) + self.pfuns = tuple(self.pfuns) # use the setters self.given_in_basis = given_in_basis @@ -190,14 +191,14 @@ def __init__( def __call__(self, x, y, z): val = 0.0 - for amp, l, m, n, t, pfun in zip(self._amps, self._ls, self._ms, self._ns, self._theta, self._pfuns): + for amp, l, m, n, t, pfun in zip(self.amps, self.ls, self.ms, self.ns, self.theta, self.pfuns): val += ( amp * pfun(z) * xp.sin( - l * 2.0 * xp.pi / self._Lx * x - + m * 2.0 * xp.pi / self._Ly * y - + n * 2.0 * xp.pi / self._Lz * z + l * 2.0 * xp.pi / self.Lx * x + + m * 2.0 * xp.pi / self.Ly * y + + n * 2.0 * xp.pi / self.Lz * z + t, ) ) @@ -281,13 +282,13 @@ def __init__( else: assert len(amps) == n_modes - self._ls = ls - self._ms = ms - self._ns = ns - self._amps = amps - self._Lx = Lx - self._Ly = Ly - self._Lz = Lz + self.ls = tuple(ls) + self.ms = tuple(ms) + self.ns = tuple(ns) + self.amps = tuple(amps) + self.Lx = Lx + self.Ly = Ly + self.Lz = Lz # use the setters self.given_in_basis = given_in_basis @@ -296,9 +297,9 @@ def __init__( def __call__(self, x, y, z): val = 0.0 - for amp, l, m, n in zip(self._amps, self._ls, self._ms, self._ns): + for amp, l, m, n in zip(self.amps, self.ls, self.ms, self.ns): val += amp * xp.cos( - l * 2.0 * xp.pi / self._Lx * x + m * 2.0 * xp.pi / self._Ly * y + n * 2.0 * xp.pi / self._Lz * z, + l * 2.0 * xp.pi / self.Lx * x + m * 2.0 * xp.pi / self.Ly * y + n * 2.0 * xp.pi / self.Lz * z, ) # print( "Cos max value", val.max()) return val diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index 8cacabb72..bbc0a328f 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -188,7 +188,7 @@ class DerhamOptions: Whether to build the local commuting projectors based on quasi-inter-/histopolation. """ - p: tuple = (1, 1, 1) + p: tuple = (3, 2, 1) spl_kind: tuple = (True, True, True) dirichlet_bc: tuple = ((False, False), (False, False), (False, False)) nquads: tuple = None diff --git a/src/struphy/kinetic_background/base.py b/src/struphy/kinetic_background/base.py index 765ee1508..a223ab7d6 100644 --- a/src/struphy/kinetic_background/base.py +++ b/src/struphy/kinetic_background/base.py @@ -368,6 +368,13 @@ def check_maxw_params(self): assert isinstance(v[0], (float, int, Callable)) assert isinstance(v[1], Perturbation) or v[1] is None + def __repr__(self): + out = f" {self.__class__.__name__}:" + out += "\n maxw_params: (background, perturbation)" + for k, v in self.maxw_params.items(): + out += f"\n {k}: {v}" + return out + @classmethod def gaussian(self, v, u=0.0, vth=1.0, polar=False, volume_form=False): """1-dim. normal distribution, to which array-valued mean- and thermal velocities can be passed. diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index a1c3791b5..2bcbce5d6 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -68,11 +68,11 @@ def update_scalar_quantities(self): # Common methods # -------------- def __repr__(self): - print(self.__class__.__name__) + out = f"{self.__class__.__name__}\n" for k, v in self.species.items(): - print(f" {k}:") - print(v) - return "" + out += f" {k}:\n" + out += f"{v}" + return out @classmethod def name(cls) -> str: @@ -209,7 +209,7 @@ def print_scalar_quantities(self): for key, scalar_dict in self._scalar_quantities.items(): val = scalar_dict["value"] assert not xp.isnan(val[0]), f"Scalar {key} is {val[0]}." - sq_str += key + ": {:14.11f}".format(val[0]) + " " + sq_str += f"{key}:".ljust(25) + "{:3.1e}\n".format(val[0]).rjust(26) print(sq_str) def setup_equation_params(self, units: Units, verbose=False): diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index cd65dd6dd..3120c0753 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -106,8 +106,6 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"Creating light-weight instance of model '{self.__class__.__name__}' ...") # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index b2a329583..8b933f1d2 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -23,9 +23,11 @@ def __init__(self): self.init_variables() def __repr__(self): + out = "" for k, v in self.variables.items(): - print(f" {k}:".ljust(20), v) - return "" + out += f" {k}:".ljust(20) + out += f"{v}\n" + return out # set species attribute for each variable def init_variables(self): diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 78d38bb7c..c8e8b51b1 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -102,13 +102,9 @@ def show_backgrounds(self): print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - backgrounds:") if isinstance(self.backgrounds, list): for background in self.backgrounds: - print(f" {background.__class__.__name__}:") - for k, v in background.__dict__.items(): - print(f" {k}: {v}") + print(background) else: - print(f" {self.backgrounds.__class__.__name__}:") - for k, v in self.backgrounds.__dict__.items(): - print(f" {k}: {v}") + print(self.backgrounds) else: print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no background.") @@ -117,13 +113,9 @@ def show_perturbations(self): print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - perturbations:") if isinstance(self.perturbations, list): for perturbation in self.perturbations: - print(f" {perturbation.__class__.__name__}:") - for k, v in perturbation.__dict__.items(): - print(f" {k}: {v}") + print(perturbation) else: - print(f" {self.perturbations.__class__.__name__}:") - for k, v in self.perturbations.__dict__.items(): - print(f" {k}: {v}") + print(self.perturbations) else: print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no perturbation.") @@ -240,9 +232,7 @@ def add_initial_condition(self, init: KineticBackground, verbose=True): def show_initial_condition(self): if self.initial_condition is not None: print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - initial condition:") - print(f" {self.initial_condition.__class__.__name__}:") - for k, v in self.initial_condition.__dict__.items(): - print(f" {k}: {v}") + print(self.initial_condition) else: print( f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no initial condition." diff --git a/src/struphy/models/vlasov.py b/src/struphy/models/vlasov.py index dde6a8f47..b8a5fc342 100644 --- a/src/struphy/models/vlasov.py +++ b/src/struphy/models/vlasov.py @@ -56,8 +56,6 @@ def __init__(self): ## abstract methods def __init__(self): - if rank == 0: - print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}' ***") # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 6ca86e5ea..e517e6368 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -2,6 +2,7 @@ import pickle import shutil from typing import TYPE_CHECKING +import inspect import cunumpy as xp import h5py @@ -31,6 +32,70 @@ from struphy.simulation.sim import StruphySimulation +class SplineValues: + def __repr__(self): + out = "" + for name, species in inspect.getmembers(self): + if isinstance(species, SpecHolder): + out += f" {name}\n" + out += f"{species}\n" + return out + +class Orbits: + def __repr__(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: + def __repr__(self): + out = "" + for name, species in inspect.getmembers(self): + if isinstance(species, SpecHolder): + out += f" {name}\n" + out += f"{species}\n" + return out + +class DensitySPH: + def __repr__(self): + out = "" + for name, species in inspect.getmembers(self): + if isinstance(species, SpecHolder): + out += f" {name}\n" + out += f"{species}\n" + return out + +class SpecHolder: + def __repr__(self): + out = "" + for name, val in self.__dict__.items(): + out += f" {name}\n" + return out + +class Slice: + pass + +class DataDict: + def __init__(self, data: dict): + self.data = data + + def __repr__(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 + class ParamsIn: """Holds the input parameters of a Struphy simulation as attributes. @@ -100,7 +165,6 @@ def __init__( self.derham_opts = derham_opts self.model = model - class PostProcessor: """Post-processing finished Struphy runs, eithr from Simulation object or from output path. @@ -200,8 +264,7 @@ def process( Whether vtk files should be created. """ if MPI.COMM_WORLD.Get_rank() == 0: - print("\n*** Start post-processing::") - print(f"Post-processing path: {self.path_out}") + print(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: @@ -259,7 +322,7 @@ def process_fields( verbose: bool = False, ): if not self.exist_fields: - print("No feec fields found in hdf5 file, skipping post-processing of fields.") + print("\nNo feec fields found in hdf5 file, skipping post-processing of fields.") return fields, t_grid = self._create_femfields(step=step) @@ -317,7 +380,7 @@ def process_particles( ): if self.exist_particles is None: - print("No kinetic data found in hdf5 file, skipping post-processing of kinetic data.") + print("\nNo kinetic data found in hdf5 file, skipping post-processing of kinetic data.") return # directory for kinetic data @@ -1031,7 +1094,6 @@ def _post_process_n_sph( # save distribution functions xp.save(os.path.join(path_view, "n_sph.npy"), data) - class PlottingData: """Holds post-processed plotting data as attributes. @@ -1054,31 +1116,31 @@ def __init__(self, sim: "StruphySimulation" = None, path_out: str = None): assert os.path.exists(self.path_pproc), f"Path {self.path_pproc} does not exist, run 'pproc' first?" # dictionaries to hold data - self._orbits = {} - self._f = {} - self._spline_values = {} - self._n_sph = {} + self._orbits = Orbits() + self._f = DistributionFunction() + self._spline_values = SplineValues() + self._n_sph = DensitySPH() self.grids_log: list[xp.ndarray] = None self.grids_phy: list[xp.ndarray] = None self.t_grid: xp.ndarray = None @property - def orbits(self) -> dict[str, xp.ndarray]: + def orbits(self) -> Orbits: """Keys: species name. Values: 3d arrays indexed by (n, p, a), where 'n' is the time index, 'p' the particle index and 'a' the attribute index.""" return self._orbits @property - def f(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: + def f(self) -> DistributionFunction: """Keys: species name. Values: dicts of slice names ('e1_v1' etc.) holding dicts of corresponding xp.arrays for plotting.""" return self._f @property - def spline_values(self) -> dict[str, dict[str, xp.ndarray]]: + def spline_values(self) -> SplineValues: """Keys: species name. Values: dicts of variable names with values being 3d arrays on the grid.""" return self._spline_values @property - def n_sph(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: + def n_sph(self) -> DensitySPH: """Keys: species name. Values: dicts of view names ('view_0' etc.) holding dicts of corresponding xp.arrays for plotting.""" return self._n_sph @@ -1111,7 +1173,7 @@ def Nattr(self) -> dict[str, int]: def load(self, verbose: bool = False): """Load data generated during post-processing.""" - print("\n*** Loading post-processed plotting data:") + print("\nLoading post-processed plotting data:") print(f"Data path: {self.path_pproc}") # load time grid @@ -1132,7 +1194,8 @@ def load(self, verbose: bool = False): # species folders species = next(os.walk(path_fields))[1] for spec in species: - self._spline_values[spec] = {} + 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) @@ -1143,13 +1206,13 @@ def load(self, verbose: bool = False): var = file.split(".")[0] with open(os.path.join(path_spec, file), "rb") as f: # try: - self._spline_values[spec][var] = pickle.load(f) + data_dict = DataDict(pickle.load(f)) + 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] - print(f"{species =}") for spec in species: path_spec = os.path.join(path_kinetic, spec) wlk = os.walk(path_spec) @@ -1168,16 +1231,19 @@ def load(self, verbose: bool = False): step = int(file.split(".")[0].split("_")[-1]) tmp = xp.load(os.path.join(path_dat, file)) if n == 0: - self._orbits[spec] = xp.zeros((Nt, *tmp.shape), dtype=float) - self._orbits[spec][step] = tmp + arr = xp.zeros((Nt, *tmp.shape), dtype=float) + setattr(self.orbits, spec, arr) + arr[step] = tmp n += 1 elif "distribution_function" in folder: - self._f[spec] = {} + spec_holder = SpecHolder() + setattr(self.f, spec, spec_holder) slices = next(sub_wlk)[1] # print(f"{slices = }") for sli in slices: - self._f[spec][sli] = {} + s = Slice() + setattr(spec_holder, sli, s) # print(f"{sli = }") files = next(sub_wlk)[2] # print(f"{files = }") @@ -1185,14 +1251,16 @@ def load(self, verbose: bool = False): name = file.split(".")[0] tmp = xp.load(os.path.join(path_dat, sli, file)) # print(f"{name = }") - self._f[spec][sli][name] = tmp + setattr(s, name, tmp) elif "n_sph" in folder: - self._n_sph[spec] = {} + spec_holder = SpecHolder() + setattr(self.n_sph, spec, spec_holder) slices = next(sub_wlk)[1] # print(f"{slices = }") for sli in slices: - self._n_sph[spec][sli] = {} + s = Slice() + setattr(spec_holder, sli, s) # print(f"{sli = }") files = next(sub_wlk)[2] # print(f"{files = }") @@ -1200,7 +1268,7 @@ def load(self, verbose: bool = False): name = file.split(".")[0] tmp = xp.load(os.path.join(path_dat, sli, file)) # print(f"{name = }") - self._n_sph[spec][sli][name] = tmp + setattr(s, name, tmp) else: print(f"{folder =}") @@ -1218,24 +1286,12 @@ def load(self, verbose: bool = False): print(f"{self.grids_phy[1].shape =}") print(f"{self.grids_phy[2].shape =}") print("\nself.spline_values:") - for k, v in self.spline_values.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - print("\nself.orbits:") - for k, v in self.orbits.items(): - print(f" {k}") - print("\nself.f:") - for k, v in self.f.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - for kkk, vvv in vv.items(): - print(f" {kkk}") - print("\nself.n_sph:") - for k, v in self.n_sph.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - for kkk, vvv in vv.items(): - print(f" {kkk}") + print(self.spline_values) + print("self.orbits:") + print(self.orbits) + print("self.f:") + print(self.f) + print("self.n_sph:") + print(self.n_sph) + + diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 5c20a33c3..cafe77e06 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -66,7 +66,7 @@ class StruphySimulation(Simulation): def __init__( self, - model: StruphyModel = Maxwell(), + model: StruphyModel, params_path: str = None, env: EnvironmentOptions = EnvironmentOptions(), base_units: BaseUnits = BaseUnits(), @@ -452,8 +452,8 @@ def run(self, verbose: bool = False): print() # update time and index (round time to 10 decimals for a clean time grid!) - self.time_state["value"][0] = round(self.time_state["value"][0] + dt, 10) - self.time_state["value_sec"][0] = round(self.time_state["value_sec"][0] + dt * self.units.t, 10) + self.time_state["value"][0] = round(self.time_state["value"][0] + dt, 14) + self.time_state["value_sec"][0] = round(self.time_state["value_sec"][0] + dt * self.units.t, 14) self.time_state["index"][0] += 1 # perform one time step dt @@ -489,19 +489,16 @@ def run(self, verbose: bool = False): step = str(self.time_state["index"][0]).zfill(len(total_steps)) message = "time step: " + step + "/" + str(total_steps) - message += " | " + "time: {0:10.5f}/{1:10.5f}".format(self.time_state["value"][0], Tend) - message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( + message += "\n" + "normalized time:".ljust(25) + "{0:3.1e} / {1:3.1e}".format(self.time_state["value"][0], Tend).rjust(25) + message += "\n" + "physical time [s]:".ljust(25) + "{0:3.1e} / {1:3.1e}".format( self.time_state["value_sec"][0], Tend * self.units.t, - ) - message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( - run_time_now * 60, - t1 - t0, - ) + ).rjust(25) + message += "\n" + "wall clock time [s]:".ljust(25) + "{0:8.4f}".format(run_time_now * 60).rjust(25) + message += "\n" + "last step duration [s]:".ljust(25) + "{0:8.4f}".format(t1 - t0).rjust(25) - print(message, end="\n") + print(message) self.model.print_scalar_quantities() - print() # =================================================================== @@ -545,13 +542,20 @@ def pproc( create_vtk=create_vtk, verbose=verbose, ) - return self.post_processor def load_plotting_data(self, verbose: bool = False): if not hasattr(self, "_plotting_data") and self.rank == 0: self._plotting_data = PlottingData(sim=self) self.plotting_data.load(verbose=verbose) - return self.plotting_data + + # 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.grids_log = self.plotting_data.grids_log + self.grids_phy = self.plotting_data.grids_phy + self.t_grid = self.plotting_data.t_grid # --------------------- # Code specific methods @@ -1149,7 +1153,7 @@ def _initialize_from_restart(self, data: DataContainer, verbose: bool = False): # ------------------------------------------------------ @property - def model(self): + def model(self) -> StruphyModel: """StruphyModel object containing the PDE of the model.""" return self._model diff --git a/struphy-tutorials b/struphy-tutorials index 6e111785e..64ad515b0 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 6e111785e8fcbe28f8fd127a867740ed4726ab39 +Subproject commit 64ad515b0bcc67ea2896338394ed956a88d93479 From d555866b3cc89e8c2401f058a0426e9c4bad1e3b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 14:50:08 +0100 Subject: [PATCH 49/80] set defaults for species properties; new method Simulation.normalize_model() should be called when species properties are changed. --- src/struphy/fields_background/base.py | 7 +- src/struphy/io/options.py | 2 +- src/struphy/models/species.py | 44 +++++++++-- src/struphy/models/variables.py | 23 ++++-- .../post_processing/post_processing_tools.py | 33 +------- .../propagators/propagators_markers.py | 10 ++- src/struphy/simulation/sim.py | 76 ++++++++++++------- struphy-tutorials | 2 +- 8 files changed, 113 insertions(+), 84 deletions(-) diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index 23143f2bd..bd3498ef6 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -51,10 +51,11 @@ def domain(self, new_domain): self._domain = new_domain def __repr__(self): - print(f"{self.__class__.__name__}") + out = f"{self.__class__.__name__}" for k, v in self.params.items(): - print(f"{k}:".ljust(20), v) - return "" + out += f"\n {k}:".ljust(20) + out += f"{v}" + return out ########################### # Vector-valued callables # diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index bbc0a328f..8cacabb72 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -188,7 +188,7 @@ class DerhamOptions: Whether to build the local commuting projectors based on quasi-inter-/histopolation. """ - p: tuple = (3, 2, 1) + p: tuple = (1, 1, 1) spl_kind: tuple = (True, True, True) dirichlet_bc: tuple = ((False, False), (False, False), (False, False)) nquads: tuple = None diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 8b933f1d2..0a3fa4641 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -44,13 +44,38 @@ def variables(self) -> dict: @property def charge_number(self) -> int: - """Charge number in units of elementary charge.""" + """Charge number in units of elementary charge (default = 1).""" + if not hasattr(self, "_charge_number"): + self._charge_number = 1 return self._charge_number @property def mass_number(self) -> int: - """Mass number in units of proton mass.""" + """Mass number in units of proton mass (default = 1).""" + if not hasattr(self, "_mass_number"): + self._mass_number = 1 return self._mass_number + + @property + def alpha(self) -> float: + """The ratio of plasma frequency to cyclotron frequency, Omega_p / Omega_c (default = None).""" + if not hasattr(self, "_alpha"): + self._alpha = None + return self._alpha + + @property + def epsilon(self) -> float: + """The normalized cyclotron period, 1/(Omega_c * time_unit), default = None.""" + if not hasattr(self, "_epsilon"): + self._epsilon = None + return self._epsilon + + @property + def kappa(self) -> float: + """The normalized plasma frequency, Omega_p * time_unit (default = None).""" + if not hasattr(self, "_kappa"): + self._kappa = None + return self._kappa def set_species_properties( self, @@ -65,9 +90,12 @@ def set_species_properties( self._charge_number = charge_number self._mass_number = mass_number - self.alpha = alpha - self.epsilon = epsilon - self.kappa = kappa + self._alpha = alpha + self._epsilon = epsilon + self._kappa = kappa + + if MPI.COMM_WORLD.Get_rank() == 0: + warnings.warn("\nSpecies.set_species_properties() should be run before instantiating a simulation.\nRun Simulation.normalize_model() for existing simulation objects.") class EquationParameters: """Normalization parameters of one species, appearing in scaled equations.""" @@ -154,9 +182,9 @@ def set_species_properties( self._charge_number = 0 self._mass_number = 0 - self.alpha = alpha - self.epsilon = epsilon - self.kappa = kappa + self._alpha = alpha + self._epsilon = epsilon + self._kappa = kappa class FluidSpecies(Species): diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index c8e8b51b1..894ba58b7 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -347,6 +347,10 @@ def __init__(self): @property def space(self): return self._space + + @property + def particles_class(self) -> Particles: + return ParticlesSPH @property def particles(self) -> ParticlesSPH: @@ -388,14 +392,17 @@ def add_perturbation( self._perturbations["u3"] = del_u3 def show_perturbations(self): - print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - perturbations:") - for key, perturbation in self.perturbations.items(): - if perturbation is not None: - print(f" {key}: {perturbation.__class__.__name__}") - for k, v in perturbation.__dict__.items(): - print(f" {k}: {v}") - else: - print(f" {key}: None") + if self.perturbations is not None: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - perturbations:") + for key, perturbation in self.perturbations.items(): + if perturbation is not None: + print(f" {key}: {perturbation.__class__.__name__}") + for k, v in perturbation.__dict__.items(): + print(f" {k}: {v}") + else: + print(f" {key}: None") + else: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - no perturbation.") @property def perturbations(self) -> dict[str, Perturbation]: diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index e517e6368..dfa2b5a27 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -38,7 +38,7 @@ def __repr__(self): for name, species in inspect.getmembers(self): if isinstance(species, SpecHolder): out += f" {name}\n" - out += f"{species}\n" + out += f"{species}" return out class Orbits: @@ -58,7 +58,7 @@ def __repr__(self): for name, species in inspect.getmembers(self): if isinstance(species, SpecHolder): out += f" {name}\n" - out += f"{species}\n" + out += f"{species}" return out class DensitySPH: @@ -67,7 +67,7 @@ def __repr__(self): for name, species in inspect.getmembers(self): if isinstance(species, SpecHolder): out += f" {name}\n" - out += f"{species}\n" + out += f"{species}" return out class SpecHolder: @@ -1144,33 +1144,6 @@ def n_sph(self) -> DensitySPH: """Keys: species name. Values: dicts of view names ('view_0' etc.) holding dicts of corresponding xp.arrays for plotting.""" return self._n_sph - @property - def Nt(self) -> dict[str, int]: - """Number of available time points (snap shots) for each species.""" - if not hasattr(self, "_Nt"): - self._Nt = {} - for spec, orbs in self.orbits.items(): - self._Nt[spec] = orbs.shape[0] - return self._Nt - - @property - def Np(self) -> dict[str, int]: - """Number of particle orbits for each species.""" - if not hasattr(self, "_Np"): - self._Np = {} - for spec, orbs in self.orbits.items(): - self._Np[spec] = orbs.shape[1] - return self._Np - - @property - def Nattr(self) -> dict[str, int]: - """Number of particle attributes for each species.""" - if not hasattr(self, "_Nattr"): - self._Nattr = {} - for spec, orbs in self.orbits.items(): - self._Nattr[spec] = orbs.shape[2] - return self._Nattr - def load(self, verbose: bool = False): """Load data generated during post-processing.""" print("\nLoading post-processed plotting data:") diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index 98e52b6b7..565b2b6a3 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -93,10 +93,12 @@ def allocate(self, verbose: bool = False): # define algorithm butcher = self.options.butcher # temp fix due to refactoring of ButcherTableau: - import cunumpy as xp - - butcher._a = xp.diag(butcher.a, k=-1) - butcher._a = xp.array(list(butcher.a) + [0.0]) + try: + import cunumpy as xp + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher.a) + [0.0]) + except ValueError: + pass args_kernel = ( butcher.a, diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index cafe77e06..27e970f00 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -217,21 +217,8 @@ def __init__( self.Barrier() # units and normalization parameters - units = Units(base_units) - self.units = units - if model.bulk_species is None: - A_bulk = None - Z_bulk = None - else: - A_bulk = model.bulk_species.mass_number - Z_bulk = model.bulk_species.charge_number - self.units.derive_units( - velocity_scale=model.velocity_scale, - A_bulk=A_bulk, - Z_bulk=Z_bulk, - verbose=verbose, - ) - model.setup_equation_params(units=self.units, verbose=verbose) + self.units = Units(base_units) + self.normalize_model() if self.rank == 0: print("\n... Done.") @@ -560,6 +547,23 @@ def load_plotting_data(self, verbose: bool = False): # --------------------- # Code specific methods # --------------------- + def normalize_model(self, verbose: bool = False): + """Compute derived units and normalization coefficients of equations. + Must be re-run when species properties have been changed. + """ + if self.model.bulk_species is None: + A_bulk = None + Z_bulk = None + else: + A_bulk = self.model.bulk_species.mass_number + Z_bulk = self.model.bulk_species.charge_number + self.units.derive_units( + velocity_scale=self.model.velocity_scale, + A_bulk=A_bulk, + Z_bulk=Z_bulk, + verbose=verbose, + ) + self.model.setup_equation_params(units=self.units, verbose=verbose) def compute_plasma_params(self, verbose: bool = True): """ @@ -664,35 +668,49 @@ def compute_plasma_params(self, verbose: bool = True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - def spawn_sister(self, params_path: str = None, - env: EnvironmentOptions = None, - time_opts: Time = None, - grid: grids.TensorProductGrid = None, - derham_opts: DerhamOptions = None, + def spawn_sister(self, + model: StruphyModel = None, + params_path: str = None, + env: EnvironmentOptions = None, + base_units: BaseUnits = None, + time_opts: Time = None, + domain: Domain = None, + equil: FluidEquilibrium = None, + grid: grids.TensorProductGrid = None, + derham_opts: DerhamOptions = None, + verbose: bool = False, ): - """Spawn a sister simulation with the same model, base_units, domain and equilibrium - but different parameters and options otherwise. - This can be used to run multiple simulations with the same model - but different discretization parameters or MPI configs.""" + """Spawn a sister simulation with parameters that default to the current instance. + This can be used to quickly generate multiple similar simulations.""" + if model is None: + model = self.model + if params_path is None: + params_path = self.params_path if env is None: env = self.env + if base_units is None: + base_units = self.base_units if time_opts is None: time_opts = self.time_opts + if domain is None: + domain = self.domain + if equil is None: + equil = self.equil if grid is None: grid = self.grid if derham_opts is None: derham_opts = self.derham_opts - sister = StruphySimulation(model=self.model, + sister = StruphySimulation(model=model, params_path=params_path, env=env, - base_units=self.base_units, + base_units=base_units, time_opts=time_opts, - domain=self.domain, - equil=self.equil, + domain=domain, + equil=equil, grid=grid, derham_opts=derham_opts, - verbose=False) + verbose=verbose) return sister # --------------- diff --git a/struphy-tutorials b/struphy-tutorials index 64ad515b0..20160bb15 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 64ad515b0bcc67ea2896338394ed956a88d93479 +Subproject commit 20160bb1514ae452f814457e8cb62efd3fd6e953 From 936fb4f3a5721bf9b02b9bb470d1cbbf3924a357 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 15:24:31 +0100 Subject: [PATCH 50/80] fix test_init_perturbations.py --- .../initial/tests/test_init_perturbations.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/struphy/initial/tests/test_init_perturbations.py b/src/struphy/initial/tests/test_init_perturbations.py index 86893fd42..f4f6d9535 100644 --- a/src/struphy/initial/tests/test_init_perturbations.py +++ b/src/struphy/initial/tests/test_init_perturbations.py @@ -111,13 +111,13 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False continue if "Modes" in key and fun_form == "physical": - perturbation._Lx = Lx - perturbation._Ly = Ly - perturbation._Lz = Lz + perturbation.Lx = Lx + perturbation.Ly = Ly + perturbation.Lz = Lz else: - perturbation._Lx = 1.0 - perturbation._Ly = 1.0 - perturbation._Lz = 1.0 + perturbation.Lx = 1.0 + perturbation.Ly = 1.0 + perturbation.Lz = 1.0 # use the setter perturbation.given_in_basis = fun_form @@ -207,13 +207,13 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False continue if "Modes" in key and fun_form == "physical": - perturbation._Lx = Lx - perturbation._Ly = Ly - perturbation._Lz = Lz + perturbation.Lx = Lx + perturbation.Ly = Ly + perturbation.Lz = Lz else: - perturbation._Lx = 1.0 - perturbation._Ly = 1.0 - perturbation._Lz = 1.0 + perturbation.Lx = 1.0 + perturbation.Ly = 1.0 + perturbation.Lz = 1.0 perturbation_0 = perturbation perturbation_1 = deepcopy(perturbation) perturbation_2 = deepcopy(perturbation) From 3e6d417910c7de1a344afaba319fcfd460ea7cef Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 15:30:25 +0100 Subject: [PATCH 51/80] fix verification tests --- .../tests/verification/test_verif_EulerSPH.py | 4 ++-- .../tests/verification/test_verif_LinearMHD.py | 12 ++++++------ .../tests/verification/test_verif_Maxwell.py | 14 +++++++------- .../tests/verification/test_verif_Poisson.py | 12 ++++++------ 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_EulerSPH.py b/src/struphy/models/tests/verification/test_verif_EulerSPH.py index 86347dc7d..f87f8915b 100644 --- a/src/struphy/models/tests/verification/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_EulerSPH.py @@ -115,8 +115,8 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): # diagnostics sim.load_plotting_data(env.path_out) - ee1, ee2, ee3 = sim.plotting_data.n_sph["euler_fluid"]["view_0"]["grid_n_sph"] - n_sph = sim.plotting_data.n_sph["euler_fluid"]["view_0"]["n_sph"] + ee1, ee2, ee3 = sim.n_sph.euler_fluid.view_0.grid_n_sph + n_sph = sim.n_sph.euler_fluid.view_0.n_sph if do_plot: ppb = 8 diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 88061e8d4..787817cc8 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): sim.load_plotting_data(verbose=True) # first fft - u_of_t = sim.plotting_data.spline_values["mhd"]["velocity_log"] + u_of_t = sim.spline_values.mhd.velocity_log Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 @@ -97,8 +97,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): _1, _2, _3, coeffs = power_spectrum_2d( u_of_t, "velocity_log", - grids=sim.plotting_data.grids_log, - grids_mapped=sim.plotting_data.grids_phy, + grids=sim.grids_log, + grids_mapped=sim.grids_phy, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -117,13 +117,13 @@ 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.plotting_data.spline_values["mhd"]["pressure_log"] + p_of_t = sim.spline_values.mhd.pressure_log _1, _2, _3, coeffs = power_spectrum_2d( p_of_t, "pressure_log", - grids=sim.plotting_data.grids_log, - grids_mapped=sim.plotting_data.grids_phy, + grids=sim.grids_log, + grids_mapped=sim.grids_phy, 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 021f25606..6d5b29f85 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -75,12 +75,12 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): sim.load_plotting_data(verbose=True) # fft - E_of_t = sim.plotting_data.spline_values["em_fields"]["e_field_log"] + E_of_t = sim.spline_values.em_fields.e_field_log _1, _2, _3, coeffs = power_spectrum_2d( E_of_t, "e_field_log", - grids=sim.plotting_data.grids_log, - grids_mapped=sim.plotting_data.grids_phy, + grids=sim.grids_log, + grids_mapped=sim.grids_phy, component=0, slice_at=[0, 0, None], do_plot=do_plot, @@ -168,10 +168,10 @@ def test_coaxial(do_plot: bool = False): # load data sim.load_plotting_data(verbose=True) - t_grid = sim.plotting_data.t_grid - grids_phy = sim.plotting_data.grids_phy - e_field_phy = sim.plotting_data.spline_values["em_fields"]["e_field_phy"] - b_field_phy = sim.plotting_data.spline_values["em_fields"]["b_field_phy"] + t_grid = sim.t_grid + grids_phy = sim.grids_phy + e_field_phy = sim.spline_values.em_fields.e_field_phy + b_field_phy = sim.spline_values.em_fields.b_field_phy X = grids_phy[0][:, :, 0] Y = grids_phy[1][:, :, 0] diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 8673deea5..54678ce5e 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -85,12 +85,12 @@ def test_poisson_1d(do_plot=False): if MPI.COMM_WORLD.Get_rank() == 0: sim.load_plotting_data(verbose=True) - phi = sim.plotting_data.spline_values["em_fields"]["phi_log"] - source = sim.plotting_data.spline_values["em_fields"]["source_log"] - x = sim.plotting_data.grids_phy[0][:, 0, 0] - y = sim.plotting_data.grids_phy[1][0, :, 0] - z = sim.plotting_data.grids_phy[2][0, 0, :] - time = sim.plotting_data.t_grid + phi = sim.spline_values.em_fields.phi_log + source = sim.spline_values.em_fields.source_log + 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 interval = 2 c = 0 From 09c21ca1c02079ff58569f6d7300592c20287387 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 15:32:12 +0100 Subject: [PATCH 52/80] formatting --- src/struphy/initial/base.py | 2 +- src/struphy/initial/perturbations.py | 5 +- src/struphy/models/cold_plasma.py | 1 - .../deterministic_particle_diffusion.py | 1 - .../drift_kinetic_electrostatic_adiabatic.py | 1 - src/struphy/models/euler_sph.py | 1 - src/struphy/models/guiding_center.py | 1 - src/struphy/models/hasegawa_wakatani.py | 1 - .../models/linear_extended_mh_duniform.py | 1 - src/struphy/models/linear_mhd.py | 1 - .../models/linear_mhd_driftkinetic_cc.py | 1 - src/struphy/models/linear_mhd_vlasov_cc.py | 1 - src/struphy/models/linear_mhd_vlasov_pc.py | 1 - .../linear_vlasov_ampere_one_species.py | 1 - .../linear_vlasov_maxwell_one_species.py | 1 - src/struphy/models/poisson.py | 1 - src/struphy/models/pressure_less_sph.py | 1 - .../models/random_particle_diffusion.py | 1 - src/struphy/models/shear_alfven.py | 1 - src/struphy/models/species.py | 12 +-- .../models/two_fluid_quasi_neutral_toy.py | 1 - src/struphy/models/variables.py | 6 +- .../models/variational_barotropic_fluid.py | 1 - .../models/variational_compressible_fluid.py | 1 - .../models/variational_pressureless_fluid.py | 1 - .../models/visco_resistive_deltaf_mhd.py | 1 - .../visco_resistive_deltaf_mhd_with_q.py | 1 - .../models/visco_resistive_linear_mhd.py | 1 - .../visco_resistive_linear_mhd_with_q.py | 1 - src/struphy/models/visco_resistive_mhd.py | 1 - .../models/visco_resistive_mhd_with_p.py | 1 - .../models/visco_resistive_mhd_with_q.py | 1 - src/struphy/models/viscous_euler_sph.py | 1 - src/struphy/models/viscous_fluid.py | 1 - .../models/vlasov_ampere_one_species.py | 1 - .../models/vlasov_maxwell_one_species.py | 1 - src/struphy/pic/particles.py | 2 +- .../post_processing/post_processing_tools.py | 21 +++-- .../propagators/propagators_markers.py | 1 + src/struphy/simulation/sim.py | 87 +++++++++++-------- 40 files changed, 77 insertions(+), 91 deletions(-) diff --git a/src/struphy/initial/base.py b/src/struphy/initial/base.py index acee33124..ecaf7c133 100644 --- a/src/struphy/initial/base.py +++ b/src/struphy/initial/base.py @@ -15,7 +15,7 @@ def __call__(self, eta1, eta2, eta3, flat_eval=False): def prepare_eval_pts(self): # TODO: we could prepare the arguments via a method in this base class (flat_eval, sparse meshgrid, etc.). pass - + def __repr__(self): print(f" {self.__class__.__name__}:") for k, v in self.__dict__.items(): diff --git a/src/struphy/initial/perturbations.py b/src/struphy/initial/perturbations.py index 7694bd044..4746b8bc3 100644 --- a/src/struphy/initial/perturbations.py +++ b/src/struphy/initial/perturbations.py @@ -196,10 +196,7 @@ def __call__(self, x, y, z): amp * pfun(z) * xp.sin( - l * 2.0 * xp.pi / self.Lx * x - + m * 2.0 * xp.pi / self.Ly * y - + n * 2.0 * xp.pi / self.Lz * z - + t, + l * 2.0 * xp.pi / self.Lx * x + m * 2.0 * xp.pi / self.Ly * y + n * 2.0 * xp.pi / self.Lz * z + t, ) ) diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index a7e46c207..90ec54697 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -78,7 +78,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/deterministic_particle_diffusion.py b/src/struphy/models/deterministic_particle_diffusion.py index 0073543a7..23ec5d670 100644 --- a/src/struphy/models/deterministic_particle_diffusion.py +++ b/src/struphy/models/deterministic_particle_diffusion.py @@ -59,7 +59,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 35f61d762..176ab80ed 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -98,7 +98,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/euler_sph.py b/src/struphy/models/euler_sph.py index ddae4a1de..5c9ddbdb2 100644 --- a/src/struphy/models/euler_sph.py +++ b/src/struphy/models/euler_sph.py @@ -73,7 +73,6 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - self.with_B0 = with_B0 diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index 0de4224cf..0fa22077f 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -69,7 +69,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index a2ffe2019..7817f8c3f 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -73,7 +73,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index 8a3dedfa3..d0a23b8b1 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -85,7 +85,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index afbb21b9f..626a93225 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -75,7 +75,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index bb23342ce..00224a7f6 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -140,7 +140,6 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index e1a9dcc0e..1a0ede783 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -113,7 +113,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index b7283742a..bef73483c 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -120,7 +120,6 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index 6d23620d5..dd10e36e2 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -128,7 +128,6 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index e20d5d31b..eea2447e3 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -128,7 +128,6 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index c86aae566..89b8b685b 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -64,7 +64,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/pressure_less_sph.py b/src/struphy/models/pressure_less_sph.py index 882111899..3888d81f5 100644 --- a/src/struphy/models/pressure_less_sph.py +++ b/src/struphy/models/pressure_less_sph.py @@ -54,7 +54,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.cold_fluid = self.ColdFluid() diff --git a/src/struphy/models/random_particle_diffusion.py b/src/struphy/models/random_particle_diffusion.py index e31676ef2..c647e91da 100644 --- a/src/struphy/models/random_particle_diffusion.py +++ b/src/struphy/models/random_particle_diffusion.py @@ -58,7 +58,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index 0a60bb1de..cb390c9ba 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -77,7 +77,6 @@ def allocate_helpers(self, verbose: bool = False): self._tmp_b2 = Propagator.derham.Vh["2"].zeros() def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 0a3fa4641..167ecd129 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -55,21 +55,21 @@ def mass_number(self) -> int: if not hasattr(self, "_mass_number"): self._mass_number = 1 return self._mass_number - + @property def alpha(self) -> float: """The ratio of plasma frequency to cyclotron frequency, Omega_p / Omega_c (default = None).""" if not hasattr(self, "_alpha"): self._alpha = None return self._alpha - + @property def epsilon(self) -> float: """The normalized cyclotron period, 1/(Omega_c * time_unit), default = None.""" if not hasattr(self, "_epsilon"): self._epsilon = None return self._epsilon - + @property def kappa(self) -> float: """The normalized plasma frequency, Omega_p * time_unit (default = None).""" @@ -93,9 +93,11 @@ def set_species_properties( self._alpha = alpha self._epsilon = epsilon self._kappa = kappa - + if MPI.COMM_WORLD.Get_rank() == 0: - warnings.warn("\nSpecies.set_species_properties() should be run before instantiating a simulation.\nRun Simulation.normalize_model() for existing simulation objects.") + warnings.warn( + "\nSpecies.set_species_properties() should be run before instantiating a simulation.\nRun Simulation.normalize_model() for existing simulation objects." + ) class EquationParameters: """Normalization parameters of one species, appearing in scaled equations.""" diff --git a/src/struphy/models/two_fluid_quasi_neutral_toy.py b/src/struphy/models/two_fluid_quasi_neutral_toy.py index b2ae05fa6..40076f2c3 100644 --- a/src/struphy/models/two_fluid_quasi_neutral_toy.py +++ b/src/struphy/models/two_fluid_quasi_neutral_toy.py @@ -82,7 +82,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMfields() diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 894ba58b7..9a171a589 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -1,9 +1,9 @@ # for type checking (cyclic imports) from __future__ import annotations +import inspect from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING -import inspect import cunumpy as xp from feectools.ddm.mpi import mpi as MPI @@ -197,7 +197,7 @@ def __init__(self, space: LiteralOptions.OptsPICSpace = "Particles6D"): @property def space(self): return self._space - + @property def particles_class(self) -> Particles: return self._particles_class @@ -347,7 +347,7 @@ def __init__(self): @property def space(self): return self._space - + @property def particles_class(self) -> Particles: return ParticlesSPH diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index 40a8ac5ef..9980f109b 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -63,7 +63,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 84248d7f1..4b36a85e1 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -73,7 +73,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index ffebd91fe..0c474ff8d 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -61,7 +61,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index c016af76c..6c0d8b305 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -102,7 +102,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index 44e3253f5..ee9aaecb8 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -102,7 +102,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index d52ea637c..c2e259e72 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -100,7 +100,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index 143c9dd65..ad8203aa6 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -100,7 +100,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index c6af16586..e1bfe98d9 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -99,7 +99,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index c8189d681..9acffe1f1 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -100,7 +100,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 8250544df..6cc05b976 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -102,7 +102,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index afe20e2d4..255891f49 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -74,7 +74,6 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - self.with_B0 = with_B0 diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index ccfb7670f..8ae7e72b1 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -78,7 +78,6 @@ def __init__(self, with_viscosity: bool = True): ## abstract methods def __init__(self, with_viscosity: bool = True): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 715d29183..ec55bad24 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -119,7 +119,6 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - self.with_B0 = with_B0 diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 0a959d5f1..ef43a456e 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -130,7 +130,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 84502827a..2146e0ec4 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -26,7 +26,7 @@ class Particles6D(Particles): value position (eta) velocities weight s0 w0 buffer ===== ============== ======================= ======= ====== ====== ========== """ - + # Class properties vdim = 3 type = "full_f" diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index dfa2b5a27..17e06dab4 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1,8 +1,8 @@ +import inspect import os import pickle import shutil from typing import TYPE_CHECKING -import inspect import cunumpy as xp import h5py @@ -41,6 +41,7 @@ def __repr__(self): out += f"{species}" return out + class Orbits: def __repr__(self): out = "" @@ -52,6 +53,7 @@ def __repr__(self): out += f" Number of attributes: {shp[2]}\n" return out + class DistributionFunction: def __repr__(self): out = "" @@ -61,6 +63,7 @@ def __repr__(self): out += f"{species}" return out + class DensitySPH: def __repr__(self): out = "" @@ -70,20 +73,23 @@ def __repr__(self): out += f"{species}" return out + class SpecHolder: def __repr__(self): out = "" for name, val in self.__dict__.items(): out += f" {name}\n" return out - + + class Slice: pass - + + class DataDict: def __init__(self, data: dict): self.data = data - + def __repr__(self): out = f"{type(self.data) = }\n" out += f"{len(self.data) = }\n" @@ -96,6 +102,7 @@ def __repr__(self): out += f"shape = {shp}\n" return out + class ParamsIn: """Holds the input parameters of a Struphy simulation as attributes. @@ -165,6 +172,7 @@ def __init__( self.derham_opts = derham_opts self.model = model + class PostProcessor: """Post-processing finished Struphy runs, eithr from Simulation object or from output path. @@ -1094,6 +1102,7 @@ def _post_process_n_sph( # save distribution functions xp.save(os.path.join(path_view, "n_sph.npy"), data) + class PlottingData: """Holds post-processed plotting data as attributes. @@ -1205,7 +1214,7 @@ def load(self, verbose: bool = False): 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) + setattr(self.orbits, spec, arr) arr[step] = tmp n += 1 @@ -1266,5 +1275,3 @@ def load(self, verbose: bool = False): print(self.f) print("self.n_sph:") print(self.n_sph) - - diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index 565b2b6a3..874ce8844 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -95,6 +95,7 @@ def allocate(self, verbose: bool = False): # temp fix due to refactoring of ButcherTableau: try: import cunumpy as xp + butcher._a = xp.diag(butcher.a, k=-1) butcher._a = xp.array(list(butcher.a) + [0.0]) except ValueError: diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 27e970f00..c8ea9e93a 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -27,7 +27,6 @@ equils, grids, ) -from struphy.models import Maxwell # core imports from struphy.feec.basis_projection_ops import BasisProjectionOperators @@ -46,6 +45,7 @@ from struphy.geometry.base import Domain from struphy.io.output_handling import DataContainer from struphy.io.setup import setup_derham +from struphy.models import Maxwell from struphy.models.base import StruphyModel from struphy.models.species import ( DiagnosticSpecies, @@ -219,7 +219,7 @@ def __init__( # units and normalization parameters self.units = Units(base_units) self.normalize_model() - + if self.rank == 0: print("\n... Done.") @@ -316,7 +316,7 @@ def initialize_data_storage(self, verbose: bool = False): def run(self, verbose: bool = False): print(f"\nStarting simulation run for model {self.model_name} ...") - + self._remove_existing_output_files(verbose=verbose) # Display propagator options and intial conditions: @@ -476,11 +476,19 @@ def run(self, verbose: bool = False): step = str(self.time_state["index"][0]).zfill(len(total_steps)) message = "time step: " + step + "/" + str(total_steps) - message += "\n" + "normalized time:".ljust(25) + "{0:3.1e} / {1:3.1e}".format(self.time_state["value"][0], Tend).rjust(25) - message += "\n" + "physical time [s]:".ljust(25) + "{0:3.1e} / {1:3.1e}".format( - self.time_state["value_sec"][0], - Tend * self.units.t, - ).rjust(25) + message += ( + "\n" + + "normalized time:".ljust(25) + + "{0:3.1e} / {1:3.1e}".format(self.time_state["value"][0], Tend).rjust(25) + ) + message += ( + "\n" + + "physical time [s]:".ljust(25) + + "{0:3.1e} / {1:3.1e}".format( + self.time_state["value_sec"][0], + Tend * self.units.t, + ).rjust(25) + ) message += "\n" + "wall clock time [s]:".ljust(25) + "{0:8.4f}".format(run_time_now * 60).rjust(25) message += "\n" + "last step duration [s]:".ljust(25) + "{0:8.4f}".format(t1 - t0).rjust(25) @@ -516,7 +524,7 @@ def pproc( # setup post processor and plotting if not hasattr(self, "_post_processor") and self.rank == 0: self._post_processor = PostProcessor(sim=self) - + if time_trace: self.post_processor.plot_time_traces(verbose=verbose) @@ -534,7 +542,7 @@ def load_plotting_data(self, verbose: bool = False): if not hasattr(self, "_plotting_data") and self.rank == 0: self._plotting_data = PlottingData(sim=self) self.plotting_data.load(verbose=verbose) - + # expose attributes self.orbits = self.plotting_data.orbits self.f = self.plotting_data.f @@ -549,7 +557,7 @@ def load_plotting_data(self, verbose: bool = False): # --------------------- def normalize_model(self, verbose: bool = False): """Compute derived units and normalization coefficients of equations. - Must be re-run when species properties have been changed. + Must be re-run when species properties have been changed. """ if self.model.bulk_species is None: A_bulk = None @@ -668,19 +676,20 @@ def compute_plasma_params(self, verbose: bool = True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - def spawn_sister(self, - model: StruphyModel = None, - params_path: str = None, - env: EnvironmentOptions = None, - base_units: BaseUnits = None, - time_opts: Time = None, - domain: Domain = None, - equil: FluidEquilibrium = None, - grid: grids.TensorProductGrid = None, - derham_opts: DerhamOptions = None, - verbose: bool = False, - ): - """Spawn a sister simulation with parameters that default to the current instance. + def spawn_sister( + self, + model: StruphyModel = None, + params_path: str = None, + env: EnvironmentOptions = None, + base_units: BaseUnits = None, + time_opts: Time = None, + domain: Domain = None, + equil: FluidEquilibrium = None, + grid: grids.TensorProductGrid = None, + derham_opts: DerhamOptions = None, + verbose: bool = False, + ): + """Spawn a sister simulation with parameters that default to the current instance. This can be used to quickly generate multiple similar simulations.""" if model is None: model = self.model @@ -700,17 +709,19 @@ def spawn_sister(self, grid = self.grid if derham_opts is None: derham_opts = self.derham_opts - - sister = StruphySimulation(model=model, - params_path=params_path, - env=env, - base_units=base_units, - time_opts=time_opts, - domain=domain, - equil=equil, - grid=grid, - derham_opts=derham_opts, - verbose=verbose) + + sister = StruphySimulation( + model=model, + params_path=params_path, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) return sister # --------------- @@ -736,9 +747,9 @@ def _setup_folders(self, verbose: bool = False): os.mkdir(os.path.join(self.env.path_out, "data/")) if verbose: print("Created folder " + os.path.join(self.env.path_out, "data/")) - + def _remove_existing_output_files(self, verbose: bool = False): - """Removes post_processing/, meta.txt and profile_tmp. + """Removes post_processing/, meta.txt and profile_tmp. If not restart, also removes existing hdf5 and png files in output folder.""" # remove post_processing folder folder = os.path.join(self.env.path_out, "post_processing") @@ -768,7 +779,7 @@ def _remove_existing_output_files(self, verbose: bool = False): os.remove(file) if verbose and n < 10: # print only ten statements in case of many processes print("Removed existing file " + file) - + files = glob.glob(os.path.join(self.env.path_out, "*.png")) for n, file in enumerate(files): os.remove(file) From 0fe18e0b57b16bcc162bb88962ff9f964f991622 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 15:42:46 +0100 Subject: [PATCH 53/80] rename StruphySimulation -> Simulation --- src/struphy/__init__.py | 4 ++-- src/struphy/api/simulation/__init__.py | 4 ++-- src/struphy/main.py | 4 ++-- src/struphy/models/base.py | 6 ++---- src/struphy/models/tests/utils_testing.py | 4 ++-- .../models/tests/verification/test_verif_EulerSPH.py | 4 ++-- .../models/tests/verification/test_verif_LinearMHD.py | 4 ++-- .../models/tests/verification/test_verif_Maxwell.py | 6 +++--- .../models/tests/verification/test_verif_Poisson.py | 4 ++-- .../verification/test_verif_VlasovAmpereOneSpecies.py | 4 ++-- src/struphy/pic/particles.py | 4 ---- src/struphy/post_processing/post_processing_tools.py | 10 +++++----- src/struphy/simulation/base.py | 2 +- src/struphy/simulation/sim.py | 6 +++--- struphy-tutorials | 2 +- 15 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/struphy/__init__.py b/src/struphy/__init__.py index e49cbfc17..c2e0a7b83 100644 --- a/src/struphy/__init__.py +++ b/src/struphy/__init__.py @@ -19,7 +19,7 @@ ) from struphy.api.perturbations import perturbations from struphy.api.post_processing import PlottingData, PostProcessor -from struphy.api.simulation import StruphySimulation +from struphy.api.simulation import Simulation __all__ = [ "domains", @@ -40,5 +40,5 @@ "ButcherTableau", "PostProcessor", "PlottingData", - "StruphySimulation", + "Simulation", ] diff --git a/src/struphy/api/simulation/__init__.py b/src/struphy/api/simulation/__init__.py index 47e4bd190..8d5a9c7c2 100644 --- a/src/struphy/api/simulation/__init__.py +++ b/src/struphy/api/simulation/__init__.py @@ -1,3 +1,3 @@ -from struphy.simulation.sim import StruphySimulation +from struphy.simulation.sim import Simulation -all = ["StruphySimulation",] \ No newline at end of file +all = ["Simulation",] \ No newline at end of file diff --git a/src/struphy/main.py b/src/struphy/main.py index 966af9441..a7a0d78bf 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -29,7 +29,7 @@ from struphy.physics.physics import Units from struphy.pic.base import Particles from struphy.post_processing.orbits import orbits_tools -from struphy.simulation.sim import StruphySimulation +from struphy.simulation.sim import Simulation from struphy.topology import grids from struphy.topology.grids import TensorProductGrid from struphy.utils.clone_config import CloneConfig @@ -62,7 +62,7 @@ def run( Absolute path to .py parameter file. """ - sim = StruphySimulation( + sim = Simulation( model=model, params_path=params_path, env=env, diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 2bcbce5d6..fa6644d0e 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -510,7 +510,7 @@ def generate_default_parameter_file( DerhamOptions, EnvironmentOptions, FieldsBackground, - StruphySimulation, + Simulation, Time, domains, equils, @@ -545,8 +545,6 @@ def generate_default_parameter_file( # Instance of the simulation # --------------------------\n""") - # file.write("\nfrom struphy import StruphySimulation\n") - file.write("\n# Environment options\n") file.write("env = EnvironmentOptions()\n") @@ -576,7 +574,7 @@ def generate_default_parameter_file( file.write(derham) file.write("\n# Simulation object\n") - file.write("""sim = StruphySimulation( + file.write("""sim = Simulation( model=model, params_path=__file__, env=env, diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index bfc9f744b..f09f2903e 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -8,7 +8,7 @@ from struphy import EnvironmentOptions from struphy.io.setup import import_parameters_py from struphy.models.base import StruphyModel -from struphy.simulation.sim import StruphySimulation +from struphy.simulation.sim import Simulation rank = MPI.COMM_WORLD.Get_rank() @@ -52,7 +52,7 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = model = params_in.model # test - sim = StruphySimulation( + sim = Simulation( model=model, params_path=path, env=env, diff --git a/src/struphy/models/tests/verification/test_verif_EulerSPH.py b/src/struphy/models/tests/verification/test_verif_EulerSPH.py index f87f8915b..988128cca 100644 --- a/src/struphy/models/tests/verification/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_EulerSPH.py @@ -14,7 +14,7 @@ EnvironmentOptions, KernelDensityPlot, LoadingParameters, - StruphySimulation, + Simulation, Time, WeightsParameters, domains, @@ -94,7 +94,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): model.euler_fluid.var.add_perturbation(del_n=perturbation) # instance of simulation - sim = StruphySimulation( + sim = Simulation( model=model, env=env, base_units=base_units, diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 787817cc8..5c71ec709 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -9,7 +9,7 @@ BaseUnits, DerhamOptions, EnvironmentOptions, - StruphySimulation, + Simulation, Time, domains, equils, @@ -64,7 +64,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): model.mhd.velocity.add_perturbation(perturbations.Noise(amp=0.1, comp=2, seed=123)) # instance of simulation - sim = StruphySimulation( + sim = Simulation( model=model, env=env, time_opts=time_opts, diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 6d5b29f85..4143c9d91 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -11,7 +11,7 @@ BaseUnits, DerhamOptions, EnvironmentOptions, - StruphySimulation, + Simulation, Time, domains, equils, @@ -53,7 +53,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=1, seed=123)) # instance of simulation - sim = StruphySimulation( + sim = Simulation( model=model, env=env, time_opts=time_opts, @@ -139,7 +139,7 @@ def test_coaxial(do_plot: bool = False): model.em_fields.b_field.add_perturbation(perturbations.CoaxialWaveguideMagnetic(m=m, a1=a1, a2=a2)) # instance of simulation - sim = StruphySimulation( + sim = Simulation( model=model, env=env, time_opts=time_opts, diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 54678ce5e..4630bd173 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -9,7 +9,7 @@ BaseUnits, DerhamOptions, EnvironmentOptions, - StruphySimulation, + Simulation, Time, domains, grids, @@ -64,7 +64,7 @@ def test_poisson_1d(do_plot=False): ) # instance of simulation - sim = StruphySimulation( + sim = Simulation( model=model, env=env, time_opts=time_opts, diff --git a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py index 477eba1fc..00583e13c 100644 --- a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py @@ -13,7 +13,7 @@ DerhamOptions, EnvironmentOptions, LoadingParameters, - StruphySimulation, + Simulation, Time, WeightsParameters, domains, @@ -85,7 +85,7 @@ def test_weak_Landau(do_plot: bool = False): model.kinetic_ions.var.add_initial_condition(init) # instance of simulation - sim = StruphySimulation( + sim = Simulation( model=model, env=env, time_opts=time_opts, diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 2146e0ec4..dc670ad55 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -217,10 +217,6 @@ class DeltaFParticles6D(Particles6D): def __post_init__(self): self.weights_params.control_variate = False - @property - def type(self): - return "delta_f" - def _set_initial_condition(self): self.set_n_to_zero(self.initial_condition) super()._set_initial_condition() diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 17e06dab4..112c64872 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -29,7 +29,7 @@ from struphy.topology.grids import TensorProductGrid if TYPE_CHECKING: - from struphy.simulation.sim import StruphySimulation + from struphy.simulation.sim import Simulation class SplineValues: @@ -178,8 +178,8 @@ class PostProcessor: Parameters ---------- - sim : StruphySimulation - StruphySimulation object of finished run. + sim : Simulation + Simulation object of finished run. path_out: str Path to Struphy output folder (in case no sim is given). @@ -187,7 +187,7 @@ class PostProcessor: def __init__( self, - sim: "StruphySimulation" = None, + sim: "Simulation" = None, path_out: str = None, ): @@ -1112,7 +1112,7 @@ class PlottingData: Absolute path of simulation output folder to post-process. """ - def __init__(self, sim: "StruphySimulation" = None, path_out: str = None): + def __init__(self, sim: "Simulation" = None, path_out: str = None): if sim is None: assert path_out is not None, ( diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index adf5f6ed9..e6ea1802c 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod -class Simulation(metaclass=ABCMeta): +class SimulationBase(metaclass=ABCMeta): """Abstract base class for simulations.""" @abstractmethod diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index c8ea9e93a..1d3db9d74 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -58,12 +58,12 @@ from struphy.physics.physics import Units from struphy.pic.base import Particles from struphy.propagators.base import Propagator -from struphy.simulation.base import Simulation +from struphy.simulation.base import SimulationBase from struphy.utils.clone_config import CloneConfig from struphy.utils.utils import dict_to_yaml -class StruphySimulation(Simulation): +class Simulation(SimulationBase): def __init__( self, model: StruphyModel, @@ -710,7 +710,7 @@ def spawn_sister( if derham_opts is None: derham_opts = self.derham_opts - sister = StruphySimulation( + sister = Simulation( model=model, params_path=params_path, env=env, diff --git a/struphy-tutorials b/struphy-tutorials index 20160bb15..25e5099a7 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 20160bb1514ae452f814457e8cb62efd3fd6e953 +Subproject commit 25e5099a7445e2b9166b36482a742e685748ef83 From a08545a652080cc016ce46da7e1ab625872a78f8 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 15:50:20 +0100 Subject: [PATCH 54/80] add .data when laoding spline_values --- .../models/tests/verification/test_verif_LinearMHD.py | 4 ++-- src/struphy/models/tests/verification/test_verif_Maxwell.py | 6 +++--- src/struphy/models/tests/verification/test_verif_Poisson.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 5c71ec709..333c9720a 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): sim.load_plotting_data(verbose=True) # first fft - u_of_t = sim.spline_values.mhd.velocity_log + u_of_t = sim.spline_values.mhd.velocity_log.data Bsquare = B0x**2 + B0y**2 + B0z**2 p0 = beta * Bsquare / 2 @@ -117,7 +117,7 @@ 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 + p_of_t = sim.spline_values.mhd.pressure_log.data _1, _2, _3, coeffs = power_spectrum_2d( p_of_t, diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index 4143c9d91..adcd3209d 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -75,7 +75,7 @@ def test_light_wave_1d(algo: str, do_plot: bool = False): sim.load_plotting_data(verbose=True) # fft - E_of_t = sim.spline_values.em_fields.e_field_log + 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", @@ -170,8 +170,8 @@ def test_coaxial(do_plot: bool = False): t_grid = sim.t_grid grids_phy = sim.grids_phy - e_field_phy = sim.spline_values.em_fields.e_field_phy - b_field_phy = sim.spline_values.em_fields.b_field_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 X = grids_phy[0][:, :, 0] Y = grids_phy[1][:, :, 0] diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 4630bd173..2ccf88a22 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -85,8 +85,8 @@ def test_poisson_1d(do_plot=False): if MPI.COMM_WORLD.Get_rank() == 0: sim.load_plotting_data(verbose=True) - phi = sim.spline_values.em_fields.phi_log - source = sim.spline_values.em_fields.source_log + 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, :] From 7e02b7f2aac8bbe9bafaabb24e076947cbd5eed9 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 17:35:36 +0100 Subject: [PATCH 55/80] actually remove main.py --- src/struphy/main.py | 290 -------------------------------------------- 1 file changed, 290 deletions(-) delete mode 100644 src/struphy/main.py diff --git a/src/struphy/main.py b/src/struphy/main.py deleted file mode 100644 index a7a0d78bf..000000000 --- a/src/struphy/main.py +++ /dev/null @@ -1,290 +0,0 @@ -import copy -import datetime -import glob -import os -import pickle -import shutil -import sysconfig -import time -from typing import Optional, TypedDict - -import cunumpy as xp -import h5py -from feectools.ddm.mpi import MockMPI -from feectools.ddm.mpi import mpi as MPI -from line_profiler import profile -from pyevtk.hl import gridToVTK -from scope_profiler import ProfileManager - -from struphy.fields_background.base import FluidEquilibrium, FluidEquilibriumWithB -from struphy.fields_background.equils import HomogenSlab -from struphy.geometry import domains -from struphy.geometry.base import Domain -from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, Time -from struphy.io.output_handling import DataContainer -from struphy.io.setup import import_parameters_py -from struphy.models.base import StruphyModel -from struphy.models.species import Species -from struphy.models.variables import FEECVariable -from struphy.physics.physics import Units -from struphy.pic.base import Particles -from struphy.post_processing.orbits import orbits_tools -from struphy.simulation.sim import Simulation -from struphy.topology import grids -from struphy.topology.grids import TensorProductGrid -from struphy.utils.clone_config import CloneConfig -from struphy.utils.utils import dict_to_yaml - - -@profile -def run( - model: StruphyModel, - *, - params_path: str = None, - env: EnvironmentOptions = EnvironmentOptions(), - base_units: BaseUnits = BaseUnits(), - time_opts: Time = Time(), - domain: Domain = domains.Cuboid(), - equil: FluidEquilibrium = HomogenSlab(), - grid: TensorProductGrid = None, - derham_opts: DerhamOptions = None, - verbose: bool = False, -): - """ - Run a Struphy model. - - Parameters - ---------- - model : StruphyModel - The model to run. Check https://struphy-hub.github.io/struphy/sections/models.html for available models. - - params_path : str - Absolute path to .py parameter file. - """ - - sim = Simulation( - model=model, - params_path=params_path, - env=env, - base_units=base_units, - time_opts=time_opts, - domain=domain, - equil=equil, - grid=grid, - derham_opts=derham_opts, - verbose=verbose, - ) - - sim.run(verbose=verbose) - - -class SimData: - """Holds post-processed Struphy data as attributes. - - Parameters - ---------- - path : str - Absolute path of simulation output folder to post-process. - """ - - def __init__(self, path: str): - self.path = path - self._orbits = {} - self._f = {} - self._spline_values = {} - self._n_sph = {} - self.grids_log: list[xp.ndarray] = None - self.grids_phy: list[xp.ndarray] = None - self.t_grid: xp.ndarray = None - - @property - def orbits(self) -> dict[str, xp.ndarray]: - """Keys: species name. Values: 3d arrays indexed by (n, p, a), where 'n' is the time index, 'p' the particle index and 'a' the attribute index.""" - return self._orbits - - @property - def f(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: - """Keys: species name. Values: dicts of slice names ('e1_v1' etc.) holding dicts of corresponding xp.arrays for plotting.""" - return self._f - - @property - def spline_values(self) -> dict[str, dict[str, xp.ndarray]]: - """Keys: species name. Values: dicts of variable names with values being 3d arrays on the grid.""" - return self._spline_values - - @property - def n_sph(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: - """Keys: species name. Values: dicts of view names ('view_0' etc.) holding dicts of corresponding xp.arrays for plotting.""" - return self._n_sph - - @property - def Nt(self) -> dict[str, int]: - """Number of available time points (snap shots) for each species.""" - if not hasattr(self, "_Nt"): - self._Nt = {} - for spec, orbs in self.orbits.items(): - self._Nt[spec] = orbs.shape[0] - return self._Nt - - @property - def Np(self) -> dict[str, int]: - """Number of particle orbits for each species.""" - if not hasattr(self, "_Np"): - self._Np = {} - for spec, orbs in self.orbits.items(): - self._Np[spec] = orbs.shape[1] - return self._Np - - @property - def Nattr(self) -> dict[str, int]: - """Number of particle attributes for each species.""" - if not hasattr(self, "_Nattr"): - self._Nattr = {} - for spec, orbs in self.orbits.items(): - self._Nattr[spec] = orbs.shape[2] - return self._Nattr - - -def load_data(path: str) -> SimData: - """Load data generated during post-processing. - - Parameters - ---------- - path : str - Absolute path of simulation output folder to post-process. - """ - - path_pproc = os.path.join(path, "post_processing") - assert os.path.exists(path_pproc), f"Path {path_pproc} does not exist, run 'pproc' first?" - print("\n*** Loading post-processed simulation data:") - print(f"{path =}") - - simdata = SimData(path) - - # load time grid - simdata.t_grid = xp.load(os.path.join(path_pproc, "t_grid.npy")) - - # data paths - path_fields = os.path.join(path_pproc, "fields_data") - path_kinetic = os.path.join(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: - simdata.grids_log = pickle.load(f) - with open(os.path.join(path_fields, "grids_phy.bin"), "rb") as f: - simdata.grids_phy = pickle.load(f) - - # species folders - species = next(os.walk(path_fields))[1] - for spec in species: - simdata._spline_values[spec] = {} - # simdata.arrays[spec] = {} - path_spec = os.path.join(path_fields, spec) - wlk = os.walk(path_spec) - files = next(wlk)[2] - print(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: - simdata._spline_values[spec][var] = pickle.load(f) - # simdata.arrays[spec][var] = pickle.load(f) - - if os.path.exists(path_kinetic): - # species folders - species = next(os.walk(path_kinetic))[1] - print(f"{species =}") - 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 - for file in files: - # print(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: - simdata._orbits[spec] = xp.zeros((Nt, *tmp.shape), dtype=float) - simdata._orbits[spec][step] = tmp - n += 1 - - elif "distribution_function" in folder: - simdata._f[spec] = {} - slices = next(sub_wlk)[1] - # print(f"{slices = }") - for sli in slices: - simdata._f[spec][sli] = {} - # print(f"{sli = }") - files = next(sub_wlk)[2] - # print(f"{files = }") - for file in files: - name = file.split(".")[0] - tmp = xp.load(os.path.join(path_dat, sli, file)) - # print(f"{name = }") - simdata._f[spec][sli][name] = tmp - - elif "n_sph" in folder: - simdata._n_sph[spec] = {} - slices = next(sub_wlk)[1] - # print(f"{slices = }") - for sli in slices: - simdata._n_sph[spec][sli] = {} - # print(f"{sli = }") - files = next(sub_wlk)[2] - # print(f"{files = }") - for file in files: - name = file.split(".")[0] - tmp = xp.load(os.path.join(path_dat, sli, file)) - # print(f"{name = }") - simdata._n_sph[spec][sli][name] = tmp - - else: - print(f"{folder =}") - raise NotImplementedError - - print("\nThe following data has been loaded:") - print("\ngrids:") - print(f"{simdata.t_grid.shape =}") - if simdata.grids_log is not None: - print(f"{simdata.grids_log[0].shape =}") - print(f"{simdata.grids_log[1].shape =}") - print(f"{simdata.grids_log[2].shape =}") - if simdata.grids_phy is not None: - print(f"{simdata.grids_phy[0].shape =}") - print(f"{simdata.grids_phy[1].shape =}") - print(f"{simdata.grids_phy[2].shape =}") - print("\nsimdata.spline_values:") - for k, v in simdata.spline_values.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - print("\nsimdata.orbits:") - for k, v in simdata.orbits.items(): - print(f" {k}") - print("\nsimdata.f:") - for k, v in simdata.f.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - for kkk, vvv in vv.items(): - print(f" {kkk}") - print("\nsimdata.n_sph:") - for k, v in simdata.n_sph.items(): - print(f" {k}") - for kk, vv in v.items(): - print(f" {kk}") - for kkk, vvv in vv.items(): - print(f" {kkk}") - - return simdata From 8502a39d9b60b0687bc051de7f3fa355e4bbf2a2 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Thu, 19 Feb 2026 18:03:40 +0100 Subject: [PATCH 56/80] add docstrings to Simulation class --- src/struphy/simulation/sim.py | 106 ++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 1d3db9d74..ac2e463ac 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -64,6 +64,48 @@ class Simulation(SimulationBase): + """Top-level class to configure and run a Struphy simulation. + + The `Simulation` class wraps model setup, MPI configuration, output + management, normalization (units), FEEC allocation and time stepping. + It initializes the model's variables and propagators, prepares runtime + metadata and output folders, and provides the main `run()` entry point + to execute the simulation. + + Parameters + ---------- + model : StruphyModel + Physics model that provides species, propagators and variables. + params_path : str, optional + Path to a Python parameter file to save alongside outputs. + env : EnvironmentOptions + Runtime and output environment options. + base_units : BaseUnits + Units used for normalization. + time_opts : Time + Time-stepping options (dt, Tend, split algorithm, ...). + domain : Domain + Computational domain description. + equil : FluidEquilibrium, optional + Initial fluid equilibrium (may be None). + grid : TensorProductGrid + Spatial grid used for FEEC variables. + derham_opts : DerhamOptions + Options for discrete differential operators. + verbose : bool, optional + If True, print additional setup information. + + Attributes + ---------- + meta : dict + Metadata about the run (platform, python version, model name, etc.). + units : Units + Unit/normalization helper created from `base_units`. + data : DataContainer + Output container used to store simulation data. + start_time : float + Wall-clock time when the simulation object was created. + """ def __init__( self, model: StruphyModel, @@ -228,6 +270,10 @@ def __init__( # ---------------- def show_parameters(self): + """Print the current simulation configuration to stdout. + + Only the MPI rank 0 prints to avoid clutter from multiple processes. + """ if self.rank == 0: print("\nSIMULATION PARAMETERS:") print("\nModel:") @@ -251,6 +297,12 @@ def show_parameters(self): print("") def allocate(self, verbose: bool = False): + """Allocate FEEC structures, model variables and propagators. + + This prepares FEEC operators, allocates variable storage for all + species (fields, fluids, particles) and passes allocation info to + propagators. Prints progress on MPI rank 0. + """ if MPI.COMM_WORLD.Get_rank() == 0: print("\nAllocating simulation data ...") @@ -271,6 +323,11 @@ def allocate(self, verbose: bool = False): print("... Done.") def save_geometry_and_equil_vtk(self, verbose: bool = False): + """Write a VTK file with geometry and (projected) equilibrium fields. + + Only executed on MPI rank 0. Outputs basic diagnostic fields such as + jacobian determinant, pressure and |B| when available. + """ # store geometry vtk if self.rank == 0: grids_log = [ @@ -296,6 +353,13 @@ def save_geometry_and_equil_vtk(self, verbose: bool = False): gridToVTK(os.path.join(self.env.path_out, "geometry"), *grids_phy, pointData=pointData) def initialize_data_storage(self, verbose: bool = False): + """Create the `DataContainer` and register time datasets. + + Initializes `time_state` arrays (normalized and physical time and + index) and registers them with the output `DataContainer` so they + are saved during the run (and on restart). + """ + # data object for saving (will either create new hdf5 files if restart==False or open existing files if restart==True) # use MPI.COMM_WORLD as communicator when storing the outputs @@ -315,6 +379,19 @@ def initialize_data_storage(self, verbose: bool = False): self.data.add_data({key_time_restart: val}) def run(self, verbose: bool = False): + """Main entry point to execute the simulation time loop. + + Responsibilities include allocation (when not restarting), + initialization of output storage, handling restarts, running the + main time-stepping loop, saving data at intervals, and finalizing + profiling and metadata. Prints progress on MPI rank 0. + + Parameters + ---------- + verbose : bool + If True, print additional runtime information. + """ + print(f"\nStarting simulation run for model {self.model_name} ...") self._remove_existing_output_files(verbose=verbose) @@ -521,6 +598,12 @@ def pproc( time_trace: bool = False, verbose: bool = False, ): + """Run post-processing on saved simulation data. + + Uses `PostProcessor` to generate plots, process guiding-center or + physical field views, and optionally produce VTK outputs. + """ + # setup post processor and plotting if not hasattr(self, "_post_processor") and self.rank == 0: self._post_processor = PostProcessor(sim=self) @@ -539,6 +622,13 @@ def pproc( ) def load_plotting_data(self, verbose: bool = False): + """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. + """ + if not hasattr(self, "_plotting_data") and self.rank == 0: self._plotting_data = PlottingData(sim=self) self.plotting_data.load(verbose=verbose) @@ -826,6 +916,14 @@ def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbo @profile def _allocate_feec(self, grid: grids.TensorProductGrid, derham_opts: DerhamOptions, verbose: bool = False): + """Create the discrete Derham sequence, mass/basis operators and projected equilibrium. + + This sets up the 3D Derham object (unless grid or derham_opts are + None), creates weighted mass and basis projection operators, and + constructs a projected equilibrium appropriate for the chosen + equilibrium type. + """ + # create discrete derham sequence if self.clone_config is None: derham_comm = MPI.COMM_WORLD @@ -968,6 +1066,14 @@ def _allocate_variables(self, verbose: bool = False): @profile def _allocate_propagators(self, verbose: bool = False): + """Allocate propagators and bind shared FEEC/domain operators. + + Assigns `derham`, `domain`, `mass_ops`, `basis_ops` and + `projected_equil` on the `Propagator` base class so individual + propagator instances can access shared resources, then calls each + propagator's `allocate` method. + """ + # set propagators base class attributes (then available to all propagators) Propagator.derham = self.derham Propagator.domain = self.domain From de65955d0a35585588b8668f071416a7e6cfa29a Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 09:59:50 +0100 Subject: [PATCH 57/80] remove import main --- src/struphy/models/tests/verification/test_verif_EulerSPH.py | 1 - src/struphy/models/tests/verification/test_verif_LinearMHD.py | 1 - src/struphy/models/tests/verification/test_verif_Maxwell.py | 1 - src/struphy/models/tests/verification/test_verif_Poisson.py | 1 - .../tests/verification/test_verif_VlasovAmpereOneSpecies.py | 1 - src/struphy/simulation/sim.py | 2 +- struphy-tutorials | 2 +- 7 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_EulerSPH.py b/src/struphy/models/tests/verification/test_verif_EulerSPH.py index 988128cca..3e88a3911 100644 --- a/src/struphy/models/tests/verification/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_EulerSPH.py @@ -19,7 +19,6 @@ WeightsParameters, domains, equils, - main, perturbations, ) from struphy.models import EulerSPH diff --git a/src/struphy/models/tests/verification/test_verif_LinearMHD.py b/src/struphy/models/tests/verification/test_verif_LinearMHD.py index 333c9720a..2cccda5a1 100644 --- a/src/struphy/models/tests/verification/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/verification/test_verif_LinearMHD.py @@ -14,7 +14,6 @@ domains, equils, grids, - main, perturbations, ) from struphy.diagnostics.diagn_tools import power_spectrum_2d diff --git a/src/struphy/models/tests/verification/test_verif_Maxwell.py b/src/struphy/models/tests/verification/test_verif_Maxwell.py index adcd3209d..f105c2764 100644 --- a/src/struphy/models/tests/verification/test_verif_Maxwell.py +++ b/src/struphy/models/tests/verification/test_verif_Maxwell.py @@ -16,7 +16,6 @@ domains, equils, grids, - main, perturbations, ) from struphy.diagnostics.diagn_tools import power_spectrum_2d diff --git a/src/struphy/models/tests/verification/test_verif_Poisson.py b/src/struphy/models/tests/verification/test_verif_Poisson.py index 2ccf88a22..8ddca0877 100644 --- a/src/struphy/models/tests/verification/test_verif_Poisson.py +++ b/src/struphy/models/tests/verification/test_verif_Poisson.py @@ -13,7 +13,6 @@ Time, domains, grids, - main, perturbations, ) from struphy.models import Poisson diff --git a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py index 00583e13c..7f0bf1466 100644 --- a/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/verification/test_verif_VlasovAmpereOneSpecies.py @@ -18,7 +18,6 @@ WeightsParameters, domains, grids, - main, maxwellians, perturbations, ) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index ac2e463ac..4bc4b4435 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -67,7 +67,7 @@ class Simulation(SimulationBase): """Top-level class to configure and run a Struphy simulation. The `Simulation` class wraps model setup, MPI configuration, output - management, normalization (units), FEEC allocation and time stepping. + management, normalization (units), memory allocation and time stepping. It initializes the model's variables and propagators, prepares runtime metadata and output folders, and provides the main `run()` entry point to execute the simulation. diff --git a/struphy-tutorials b/struphy-tutorials index 25e5099a7..bc9160387 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 25e5099a7445e2b9166b36482a742e685748ef83 +Subproject commit bc9160387bf5c61367eba729362bd00690235ebe From 72d3ad89c6a9aec2e9c18f90be5708cdcfde3a0b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 10:19:02 +0100 Subject: [PATCH 58/80] added some docstrings to StruphyModel, Species and Variable --- src/struphy/models/base.py | 91 ++++++++++++- src/struphy/models/species.py | 113 +++++++++++++++- src/struphy/models/variables.py | 220 +++++++++++++++++++++++++++++--- 3 files changed, 392 insertions(+), 32 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index fa6644d0e..4f32e0175 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -19,12 +19,91 @@ class StruphyModel(metaclass=ABCMeta): """ - Base class for all Struphy models. - - Note - ---- - All Struphy models are subclasses of ``StruphyModel`` and should be added to ``struphy/models/`` - in one of the modules ``fluid.py``, ``kinetic.py``, ``hybrid.py`` or ``toy.py``. + Abstract base class for all Struphy models. + + This class defines the interface for plasma simulation models in Struphy. Concrete implementations + must specify the model type (Fluid, Kinetic, Hybrid, or Toy), define propagators for time integration, + and configure species (field, fluid, particle, and diagnostic). The class provides core functionality + for managing species, scalar quantities, time integration, and particle diagnostics. + + Attributes + ---------- + species : dict + Dictionary of all species (field, fluid, and particle) in the model. + field_species : dict + Dictionary of field species in the model. + fluid_species : dict + Dictionary of fluid species in the model. + particle_species : dict + Dictionary of particle species in the model. + diagnostic_species : dict + Dictionary of diagnostic species in the model. + scalar_quantities : dict + Dictionary of scalar quantities to be tracked and saved during simulation. + prop_list : list + List of propagator objects controlling time integration. + clone_config : CloneConfig or None + Configuration for domain clones if used in parallelization. + + Abstract Methods (must be implemented by subclasses) + ---------------------------------------------------- + model_type : classmethod + Must return one of "Fluid", "Kinetic", "Hybrid", or "Toy". + bulk_species : property + Must specify the dominant plasma species. + velocity_scale : property + Must return velocity scale: "alfvén", "cyclotron", "light", or "thermal". + allocate_helpers : method + Must allocate helper arrays and perform initial solves. + update_scalar_quantities : method + Must define update rules for each scalar quantity. + Propagators : class + Must define the propagators used for time integration. + __init__ : method + Must perform a light-weight initialization of the model. + + Notes + ----- + All Struphy models must be subclasses of ``StruphyModel`` and should be added to ``struphy/models/`` + in one of the modules: ``fluid.py``, ``kinetic.py``, ``hybrid.py``, or ``toy.py``. + + Time integration is performed by calling the ``integrate()`` method with a time step and + splitting algorithm (Lie-Trotter or Strang). The model manages the execution of all propagators + in sequence to advance the simulation state. + + Species management is handled automatically through property caching. Species attributes are + discovered at runtime and categorized by type. + + Examples + -------- + Subclasses should implement: + + .. code-block:: python + + class MyFluidModel(StruphyModel): + @classmethod + def model_type(cls): + return "Fluid" + + @property + def bulk_species(self): + return self.electrons + + @property + def velocity_scale(self): + return "thermal" + + def allocate_helpers(self, verbose=False): + # Initialize helper arrays + pass + + def update_scalar_quantities(self): + # Update tracked scalars + pass + + class Propagators: + # Define propagators + pass """ # ---------------- diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 167ecd129..514e500f4 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -16,7 +16,49 @@ class Species(metaclass=ABCMeta): - """Single species of a StruphyModel.""" + """ + Abstract base class representing a single plasma species in a StruphyModel. + + This class manages variables, charge/mass properties, and equation parameters for a plasma species. + Concrete implementations include FieldSpecies, FluidSpecies, ParticleSpecies, and DiagnosticSpecies. + + Attributes + ---------- + variables : dict + Dictionary of Variable objects associated with this species, keyed by variable name. + Variables are automatically discovered during initialization. + charge_number : int + Charge number in units of elementary charge (default = 1). + For field species, this is set to 0. + mass_number : int + Mass number in units of proton mass (default = 1). + For field species, this is set to 0. + alpha : float, optional + Dimensionless parameter: plasma frequency / cyclotron frequency. + If None, computed from units and charge/mass numbers. + epsilon : float, optional + Normalized cyclotron period: 1 / (cyclotron frequency × time unit). + If None, computed from units and charge/mass numbers. + kappa : float, optional + Normalized plasma frequency: plasma frequency × time unit. + If None, computed from units and charge/mass numbers. + equation_params : EquationParameters + Object containing normalization parameters (alpha, epsilon, kappa) for scaled equations. + + Methods + ------- + init_variables() + Discover and cache Variable objects from instance attributes. + set_species_properties(charge_number, mass_number, alpha, epsilon, kappa) + Set physical and equation parameters in parameter files. + setup_equation_params(units, verbose) + Compute equation normalization parameters from physical units. + + Notes + ----- + All Species subclasses must implement __init__() and call init_variables() to properly + set up the variables dictionary. + """ @abstractmethod def __init__(self): @@ -172,7 +214,18 @@ def setup_equation_params(self, units: Units, verbose=False): class FieldSpecies(Species): - """Species without mass and charge (so-called 'fields').""" + """ + Represents a field species with zero mass and charge. + + Field species are used to represent electromagnetic or other non-particle fields in a plasma + model. They have no direct physical mass or charge properties (charge_number = 0, mass_number = 0), + but may have associated equation parameters for scaled formulations. + + Examples + -------- + >>> E_field = FieldSpecies() + >>> E_field.set_species_properties(alpha=0.5, epsilon=0.1, kappa=0.2) + """ def set_species_properties( self, @@ -190,11 +243,45 @@ def set_species_properties( class FluidSpecies(Species): - """Single fluid species in 3d configuration space.""" + """ + Represents a single fluid species evolving in 3D configuration space. + + FluidSpecies describes macroscopic plasma dynamics using fluid or moment-based equations + (e.g., Euler equations, MHD equations). Each fluid species has a specific charge and mass number + and evolves according to fluid propagators. + + Examples + -------- + >>> ions = FluidSpecies() + >>> ions.set_species_properties(charge_number=1, mass_number=1836) # Protons + """ class ParticleSpecies(Species): - """Single kinetic species in 3d + vdim phase space.""" + """ + Represents a single kinetic species in 3D configuration space plus velocity space. + + ParticleSpecies describes plasma dynamics using kinetic theory via particles or markers + in 3D + vdim (where vdim is 2 or 3) phase space. This class manages particle initialization, + sorting, and diagnostic output. + + Methods + ------- + set_markers(loading_params, weights_params, boundary_params, bufsize) + Configure marker initialization and weight parameters. + set_sorting_boxes(do_sort, sorting_frequency, boxes_per_dim, box_bufsize, dims_mask) + Configure spatial sorting for memory efficiency and kernel evaluation. + set_save_data(n_markers, binning_plots, kernel_density_plots) + Configure diagnostic output for particles and distribution functions. + + Examples + -------- + >>> electrons = ParticleSpecies() + >>> electrons.set_species_properties(charge_number=-1, mass_number=1/1836) + >>> load_params = LoadingParameters(Np=100000) + >>> electrons.set_markers(loading_params=load_params) + >>> electrons.set_sorting_boxes(do_sort=True, sorting_frequency=10) + """ def set_markers( self, @@ -299,4 +386,20 @@ def set_save_data( class DiagnosticSpecies(Species): - """Diagnostic species (fields) without mass and charge.""" + """ + Represents a diagnostic species for output and analysis of non-physical quantities. + + DiagnosticSpecies are used to track derived quantities and diagnostics that are not part + of the primary simulation fields. They have zero mass and charge and are typically computed + from other species data during simulation postprocessing or on-the-fly diagnostics. + + Notes + ----- + Diagnostic species do not directly participate in the dynamics equations but are updated + based on values from other species (field, fluid, or particle species). + + Examples + -------- + >>> vorticity = DiagnosticSpecies() # For tracking curl of velocity field + >>> energy = DiagnosticSpecies() # For tracking kinetic/thermal energy density + """ diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index 9a171a589..17ab29c95 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -26,12 +26,47 @@ class Variable(metaclass=ABCMeta): - """Single variable of a Species object. - - The solution of a model is a collection of Variables. - Multiple Variables can be combined within a Species. - For example, a Species 'Hydrogen' could be composed of the Variables density, velocity and temperature, - which satisfy a PDE within a model. + """ + Abstract base class for a single variable of a Species object. + + Variables represent the fundamental degrees of freedom in a plasma simulation. The complete solution + of a StruphyModel is a collection of Variables organized within Species. Multiple Variables can be + combined within a Species to represent different physical quantities (e.g., density, velocity, temperature). + + Attributes + ---------- + space : str + The function space or discretization type of the variable (e.g., 'H1' for finite elements, + 'Particles6D' for PIC, 'ParticlesSPH' for SPH). Must be implemented by subclasses. + backgrounds : FluidEquilibrium | KineticBackground | None + Static background(s) representing equilibrium or reference state. Multiple backgrounds can be added + and are combined by addition. Defaults to None. + perturbations : Perturbation | list[Perturbation] | None + Initial perturbations added to backgrounds to define initial conditions. + Multiple perturbations can be added and are combined by addition. Defaults to None. + save_data : bool + Flag indicating whether this variable's data should be saved during simulation (default=True). + species : Species + Reference to the parent Species object containing this variable. + + Methods + ------- + allocate() + Allocate memory and initialize data structures. Must be implemented by subclasses. + add_background(background) + Add a static background condition to the variable. + show_backgrounds() + Display information about defined backgrounds. + show_perturbations() + Display information about defined perturbations. + + Notes + ----- + The initial condition for a variable is constructed as: background(s) + perturbation(s). + If neither is specified, the variable initializes to zero. + + All concrete implementations (FEECVariable, PICVariable, SPHVariable) must define the `space` property + and implement the `allocate()` method. """ @property @@ -121,10 +156,48 @@ def show_perturbations(self): class FEECVariable(Variable): - """Basic finite element variable for grid-based methods. - - Initial conditions for a FEECVariable consist of a background plus a perturbation, which are added up. - If neither a background nor a perturbation is present, the Variable is initialized as zero. + """ + Grid-based finite element variable using FEEC (Finite Element Exterior Calculus) discretization. + + FEECVariable represents field quantities discretized on a computational grid using finite element + methods. It supports arbitrary FEEC spaces (H1, Hdiv, Hcurl, L2) and is used for electromagnetic + fields, fluid moments, or other spatially distributed quantities. + + Attributes + ---------- + space : str + The FEEC function space. Options: 'H1', 'HDiv', 'HCurl', 'L2'. + Determines the continuity and smoothness properties of the discretization. + spline : SplineFunction + The underlying spline-based representation of the field on the computational mesh. + Only available after calling `allocate()`. + species : FieldSpecies | FluidSpecies + Parent species containing this variable. + backgrounds : FieldsBackground | None + Equilibrium fields added to initial conditions. + perturbations : Perturbation | list[Perturbation] | None + Initial perturbations added to backgrounds. + + Methods + ------- + add_background(background) + Add an equilibrium field background. + add_perturbation(perturbation) + Add initial perturbations to the field. + allocate(derham, domain, equil, verbose) + Allocate spline function and initialize on the mesh. + + Notes + ----- + Initial conditions are constructed by combining backgrounds and perturbations: + initial_field = background(s) + perturbation(s) + + If neither background nor perturbation is specified, the variable initializes to zero. + + Examples + -------- + >>> E_field = FEECVariable(space='Hdiv') # For electric field + >>> E_field.add_background(FieldsBackground(type='uniform', magnitude=1.0)) """ def __init__(self, space: LiteralOptions.OptsFEECSpace = "H1"): @@ -179,13 +252,65 @@ def allocate( class PICVariable(Variable): - """Basic particle variable in PIC methods. - - A background is mandatory and can be used for noise reduction for instance. - The initial condition is a kinetic background with optional perturbations added to it. - If no inital condition is specified, the background is taken as inital condition. - If both a background and an initial condition are specified, they should be consistent - (i.e. the initial condition should be the background with perturbations on top).""" + """ + Kinetic variable using Particle-In-Cell (PIC) discretization in phase space. + + PICVariable represents kinetic distribution functions discretized via marker/particle methods + in 3D configuration space plus velocity space (3D+2D or 3D+3D). Supports various phase-space + decompositions for efficient kinetic simulations. + + Attributes + ---------- + space : str + Phase space decomposition. Options include, among others: + - 'Particles6D': Full 3D config + 3D velocity space + - 'Particles5D': 3D config + 2D gyro-velocity space (for strong magnetic fields) + particles : Particles + The marker/particle object managing kinetic data. Only available after `allocate()`. + particles_class : type + Reference to the Particles class corresponding to the space. + species : ParticleSpecies + Parent kinetic species containing this variable. + backgrounds : KineticBackground + Mandatory equilibrium distribution function (e.g., Maxwellian). + initial_condition : KineticBackground + The initial kinetic distribution. If not explicitly set, uses the background. + perturbations : Perturbation | list[Perturbation] | None + Perturbations to moments of the distribution function. + n_as_volume_form : bool + Whether number density is represented as a differential form (volume-weighted) or scalar. + n_to_save : int + Number of markers for which trajectories are saved. + saved_markers : ndarray + Array storing saved marker data. + + Methods + ------- + add_background(background, n_as_volume_form) + Set mandatory equilibrium distribution (e.g., Maxwellian). + add_initial_condition(init) + Set initial kinetic distribution (must be consistent with background). + show_initial_condition() + Display current initial condition information. + allocate(clone_config, derham, domain, equil, projected_equil, verbose) + Initialize particles and allocate marker arrays. + + Notes + ----- + The background is mandatory and often used for noise reduction. The initial condition should be + consistent with the background to avoid numerical artifacts. + + If no initial condition is specified, the background is used as the initial condition. + If both are specified, they should be the same base distribution with perturbations on top. + + Examples + -------- + >>> electrons = PICVariable(space='Particles6D') + >>> maxwellian = Maxwellian3D(n=(1.0, None), vth2=(0.1, None)) + >>> electrons.add_background(maxwellian) + >>> pert = TorusModesCos(amps=(0.1,)) + >>> electrons.add_perturbation(pert) + """ def __init__(self, space: LiteralOptions.OptsPICSpace = "Particles6D"): check_option(space, LiteralOptions.OptsPICSpace) @@ -333,10 +458,63 @@ def saved_markers(self) -> xp.ndarray: class SPHVariable(Variable): - """Basic variable for SPH methods. - - Initial conditions for a SPHVariable consist of a background plus a perturbation for density and velocity, which are added up. - If neither a background nor a perturbation is present, the Variable is initialized as zero. + """ + Fluid variable using Smoothed Particle Hydrodynamics (SPH) discretization. + + SPHVariable represents macroscopic fluid quantities (density, velocity, temperature) using + SPH methods, where fields are reconstructed from marker positions and properties. This is a + particle-based Lagrangian approach to fluid dynamics. + + Attributes + ---------- + space : str + Always 'ParticlesSPH' for SPH method. + particles : ParticlesSPH + The SPH marker/particle object. Only available after `allocate()`. + particles_class : type + Reference to the ParticlesSPH class. + particle_data : dict + Dictionary storing particle-associated data fields. + species : ParticleSpecies + Parent SPH species containing this variable. + backgrounds : FluidEquilibrium + Background fluid state (density, velocity, pressure profiles). + perturbations : dict[str, Perturbation] + Perturbations to density ('n') and velocity components ('u1', 'u2', 'u3'). + Each component can have independent perturbations or None. + n_as_volume_form : bool + Always True for SPH; number density represented as volume-weighted quantity. + n_to_save : int + Number of SPH particles for which data is saved. + saved_markers : ndarray + Array storing saved particle data. + + Methods + ------- + add_background(background) + Set the background fluid equilibrium state. + add_perturbation(del_n, del_u1, del_u2, del_u3) + Add perturbations to density and/or velocity components. + show_perturbations() + Display detailed information about density and velocity perturbations. + allocate(derham, domain, equil, projected_equil, verbose) + Initialize SPH particles and allocate marker arrays. + + Notes + ----- + Initial conditions combine background and perturbations: + - density: background + del_n + - velocity: background + (del_u1, del_u2, del_u3) + + If neither background nor perturbations are specified, fields initialize to zero. + + Examples + -------- + >>> fluid = SPHVariable() + >>> equil = HomogenSlab() + >>> fluid.add_background(equil) + >>> pert = TorusModesCos(amps=(0.1,)) + >>> fluid.add_perturbation(del_n=pert, del_u1=pert) """ def __init__(self): From 1cdd3c0dab02cedc253a17dfe854f534bab8961e Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 10:19:53 +0100 Subject: [PATCH 59/80] formatting --- src/struphy/simulation/sim.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 4bc4b4435..8b6745892 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -106,6 +106,7 @@ class Simulation(SimulationBase): start_time : float Wall-clock time when the simulation object was created. """ + def __init__( self, model: StruphyModel, From e482fdb50a122369fc6ba3402a6f94a3af61e3d5 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 13:00:00 +0100 Subject: [PATCH 60/80] add docstrings to classes that are exposed in the API --- src/struphy/fields_background/base.py | 135 ++++++-- src/struphy/geometry/base.py | 84 ++++- src/struphy/initial/base.py | 53 ++- src/struphy/particles/parameters.py | 166 ++++++---- .../post_processing/post_processing_tools.py | 302 +++++++++++------- 5 files changed, 524 insertions(+), 216 deletions(-) diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index bd3498ef6..2272f3df3 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -11,14 +11,60 @@ class FluidEquilibrium(metaclass=ABCMeta): """ - Base class for callable fluid equilibria consisting of at least - u (velocity), p (pressure) and n (density). - - Any child class must provide the following callables: - - * either ``u_xyz`` or override ``uv`` - * either ``p_xyz`` or override ``p0`` - * either ``n_xyz`` or override ``n0`` + Abstract base class for callable fluid equilibria on arbitrary domains. + + This class provides a unified interface for representing fluid equilibrium states, + including velocity, pressure, and number density fields. It supports coordinate + transformations between logical (reference) and physical domains through the Domain + object, enabling computations on mapped domains. + + Attributes + ---------- + params : dict + Dictionary of parameters passed to the class initialization. Automatically + strips 'self' and '__class__' entries. + domain : Domain + Domain object that characterizes the mapping from the logical cube [0, 1]^3 + to the physical domain. Enables coordinate transformations and differential + form conversions (0-forms, 1-forms, 2-forms, 3-forms). + + Implementation Requirements + --------------------------- + Child classes must provide at least one method from each of these pairs: + + * Velocity: either ``u_xyz`` (Cartesian) or override ``uv`` (logical coordinates) + * Pressure: either ``p_xyz`` (Cartesian) or override ``p0`` (0-form on logical domain) + * Number Density: either ``n_xyz`` (Cartesian) or override ``n0`` (0-form on logical domain) + + Derived Quantities + ------------------ + The class automatically provides derived fields computed from the basic fields: + + * Temperature: ``t0``, ``t3`` (from p/n) + * Thermal velocity: ``vth0``, ``vth3`` (from temperature) + * Entropy density: ``s0_monoatomic``, ``s3_monoatomic``, ``s0_diatomic``, ``s3_diatomic`` + + Differential Forms + ------------------- + Vector fields (velocity) are available as: + + * ``uv``: contravariant components on logical domain + * ``u1``: 1-form components + * ``u2``: 2-form components + * ``u_cart``: Cartesian components with physical coordinates + + Scalar fields are available as 0-forms (point values) and 3-forms (densities): + + * ``p0``, ``p3``: pressure + * ``n0``, ``n3``: number density + * ``t0``, ``t3``: temperature + * ``q0``, ``q3``: square root of pressure + + Notes + ----- + The class uses abstract methods to enforce implementation in child classes. + Subclasses should override coordinate-appropriate base methods (CartesianFluidEquilibrium + or LogicalFluidEquilibrium) to simplify implementation. """ @property @@ -263,7 +309,11 @@ def u_cart_3(self, *etas, squeeze_out=False): class CartesianFluidEquilibrium(FluidEquilibrium): r""" - The callables ``u_xyz``, ``p_xyz`` and ``n_xyz`` must be provided in Cartesian coordinates. + Specialization for equilibria defined in Cartesian coordinates. + + Child classes must implement the abstract methods ``u_xyz``, ``p_xyz``, and ``n_xyz``, + which return velocity, pressure, and number density in Cartesian physical space (x, y, z). + The base class automatically handles coordinate transformations and differential form conversions. """ @abstractmethod @@ -289,7 +339,12 @@ def domain(self, new_domain): class LogicalFluidEquilibrium(FluidEquilibrium): r""" - The callables ``uv``, ``p0`` and ``n0`` must be provided on the logical cube [0, 1]^3. + Specialization for equilibria defined on the logical cube [0, 1]^3. + + Child classes must implement the abstract methods ``uv``, ``p0``, and ``n0``, + which return contravariant velocity, 0-form pressure, and 0-form number density + on the logical reference domain. Useful for direct implementation when physical + coordinates are obtained via coordinate transformation through the domain mapping. """ @abstractmethod @@ -316,8 +371,11 @@ def domain(self, new_domain): class NumericalFluidEquilibrium(LogicalFluidEquilibrium): r""" - Must provide a (numerical) mapping from the logical cube [0, 1]^3 to the physical domain. - Overrides base class domain. + Specialization for equilibria with numerically computed domain mappings. + + Child classes must provide a ``numerical_domain`` property that returns a Domain object + representing the mapping from the logical cube [0, 1]^3 to the physical domain. + This class overrides the domain property to use the numerically computed mapping. """ @property @@ -334,12 +392,11 @@ def domain(self): class FluidEquilibriumWithB(FluidEquilibrium): """ - :ref:`FluidEquilibrium` with B (magnetic field) in addition. - - Any child class must provide the following callables: + Extension of FluidEquilibrium with magnetic field and its gradient. - * either ``b_xyz`` or override ``bv`` - * either ``gradB_xyz`` or override ``gradB1`` + Child classes must implement either Cartesian (``b_xyz``, ``gradB_xyz``) or + logical (``bv``, ``gradB1``) methods for magnetic field and its gradient. + Provides methods for 1-form and 2-form transformations of the magnetic field. """ @FluidEquilibrium.domain.setter @@ -631,7 +688,10 @@ def av_3(self, *etas, squeeze_out=False): class CartesianFluidEquilibriumWithB(CartesianFluidEquilibrium): r""" - The callables ``b_xyz`` and ``gradB_xyz`` must be provided in Cartesian coordinates. + Specialization for fluid equilibria with magnetic field in Cartesian coordinates. + + Child classes must implement the abstract methods ``b_xyz`` and ``gradB_xyz``, + which return magnetic field and its gradient strength in Cartesian physical space. """ @abstractmethod @@ -652,7 +712,10 @@ def domain(self, new_domain): class LogicalFluidEquilibriumWithB(LogicalFluidEquilibrium): r""" - The callable ``bv`` must be provided on the logical cube [0, 1]^3. + Specialization for fluid equilibria with magnetic field on the logical cube [0, 1]^3. + + Child classes must implement the abstract methods ``bv`` (contravariant magnetic field) + and ``gradB1`` (1-form gradient of magnetic field strength) on the logical domain. """ @abstractmethod @@ -676,8 +739,10 @@ def domain(self, new_domain): class NumericalFluidEquilibriumWithB(LogicalFluidEquilibriumWithB): r""" - Must provide a (numerical) mapping from the logical cube [0, 1]^3 to the physical domain. - Overrides base class domain. + Specialization for fluid equilibria with magnetic field and numerically computed domain mappings. + + Child classes must provide a ``numerical_domain`` property that returns a Domain object. + This class overrides the domain property to use the numerically computed mapping. """ @property @@ -694,12 +759,12 @@ def domain(self): class MHDequilibrium(FluidEquilibriumWithB): """ - :ref:`FluidEquilibriumWithB` with j (current density) in addition. - The mean velocity is returned as j/n (overriding the base class). - - Any child class must provide the following callables: + Extension of FluidEquilibriumWithB with current density field. - * either ``j_xyz`` or override ``jv`` + Child classes must implement either Cartesian (``j_xyz``) or logical (``jv``) methods + for current density. The velocity field is derived from current density as j/n, + overriding the base FluidEquilibrium. Provides methods for 1-form and 2-form + transformations of the current density. """ @FluidEquilibriumWithB.domain.setter @@ -1228,8 +1293,10 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): class CartesianMHDequilibrium(MHDequilibrium): r""" - The callables ``b_xyz``, ``j_xyz``, ``p_xyz``, ``n_xyz`` and ``gradB_xyz`` - must be provided in Cartesian coordinates. + Specialization for MHD equilibria in Cartesian coordinates. + + Child classes must implement the abstract methods ``b_xyz``, ``j_xyz``, ``p_xyz``, + ``n_xyz``, and ``gradB_xyz`` in Cartesian physical space. """ @abstractmethod @@ -1402,8 +1469,10 @@ def domain(self, new_domain): class LogicalMHDequilibrium(MHDequilibrium): r""" - The callables ``bv``, ``jv``, ``p0``, ``n0`` and ``gradB1`` - must be provided on the logical cube [0, 1]^3. + Specialization for MHD equilibria on the logical cube [0, 1]^3. + + Child classes must implement the abstract methods ``bv``, ``jv``, ``p0``, ``n0``, + and ``gradB1`` on the logical reference domain. """ @abstractmethod @@ -1446,8 +1515,10 @@ def domain(self, new_domain): class NumericalMHDequilibrium(LogicalMHDequilibrium): r""" - Must provide a (numerical) mapping from the logical cube [0, 1]^3 to the physical domain. - Overrides base class domain. + Specialization for MHD equilibria with numerically computed domain mappings. + + Child classes must provide a ``numerical_domain`` property that returns a Domain object. + This class overrides the domain property to use the numerically computed mapping. """ @property diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index 91d0a172d..30bf2c5ff 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -15,8 +15,15 @@ class Domain(metaclass=ABCMeta): - r"""Base class for mapped domains (single patch). + r""" + Abstract base class for parametric domains in plasma simulations (single patch). + The Domain class represents a computational domain through a parametric mapping from a logical unit cube + to a physical region. This supports both analytical mappings (cylindrical, toroidal, Shafranov) and + spline-based isogeometric analysis (IGA) mappings. + + Mathematical Background + ----------------------- The (physical) domain :math:`\Omega \subset \mathbb R^3` is an open subset of :math:`\mathbb R^3`, defined by a diffeomorphism @@ -26,15 +33,84 @@ class Domain(metaclass=ABCMeta): mapping points :math:`\boldsymbol{\eta} \in (0, 1)^3 = \hat\Omega` of the (logical) unit cube to physical points :math:`\mathbf x \in \Omega`. - The corresponding Jacobain matrix :math:`DF:\hat\Omega \to \mathbb R^{3\times 3}`, - its volume element :math:`\sqrt g: \hat\Omega \to \mathbb R` - and the metric tensor :math:`G:\hat\Omega \to \mathbb R^{3\times 3}` are defined by + + The corresponding Jacobian matrix :math:`DF:\hat\Omega \to \mathbb R^{3\times 3}`, + volume element :math:`\sqrt g: \hat\Omega \to \mathbb R`, and metric tensor + :math:`G:\hat\Omega \to \mathbb R^{3\times 3}` are defined by .. math:: DF_{i,j} = \frac{\partial F_i}{\partial \eta_j}\,,\qquad \sqrt g = |\textnormal{det}(DF)|\,,\qquad G = DF^\top DF\,. Only right-handed mappings (:math:`\textnormal{det}(DF) > 0`) are admitted. + + Attributes + ---------- + kind_map : int + Mapping type identifier: + - 0-9: Spline (IGA) mappings + - 10-19: Analytical mappings with cubic domain boundary + - 20-29: Cylinder and torus analytical mappings + - 30-39: Shafranov mappings (tokamak equilibrium) + params : dict + Mapping parameters as a dictionary for reference. + params_numpy : ndarray + Mapping parameters as a 1D numpy array for efficient computation. + pole : bool + Whether the mapping has one polar singularity point. + periodic_eta3 : bool + Whether the domain is periodic in the :math:`\eta_3` direction. + cx, cy, cz : ndarray + Control points for spline mapping components :math:`F_x`, :math:`F_y`, :math:`F_z` (3D arrays). + Nel : tuple[int] + Number of elements in each logical direction (for spline mappings). + p : tuple[int] + B-spline degree in each direction (for spline mappings). + spl_kind : tuple[bool] + Spline type in each direction: True for periodic, False for clamped (for spline mappings). + NbaseN : list[int] + Number of basis functions in each direction. + T : list[ndarray] + Knot vectors for each direction. + indN : list[ndarray] + Global indices of non-vanishing splines per element. + + Methods + ------- + __call__(*etas, change_out_order, squeeze_out, remove_outside, identity_map) + Evaluate the physical coordinates from logical coordinates using mapping F. + jacobian(*etas, transposed, change_out_order, squeeze_out, remove_outside) + Evaluate the Jacobian matrix DF at logical coordinates. + jacobian_det(*etas, squeeze_out, remove_outside) + Evaluate the Jacobian determinant (volume element) at logical coordinates. + jacobian_inv(*etas, transposed, change_out_order, squeeze_out, remove_outside) + Evaluate the inverse Jacobian matrix at logical coordinates. + metric_tensor(*etas, change_out_order, squeeze_out, remove_outside) + Evaluate the metric tensor G = DF^T * DF. + pull_back_0form(field, *etas, remove_outside) + Pull back scalar fields (0-forms) from physical to logical space. + push_forward_1form(field, *etas, remove_outside) + Push forward vector fields (1-forms) from logical to physical space. + transform(input_field, trans_type, *etas, remove_outside) + General transformation between different field representations. + + Notes + ----- + This is an abstract base class. Concrete implementations should be created in the + `struphy.geometry.domains` module and specify the mapping via the `kind_map` property + and mapping parameters. + + The logical coordinates (eta1, eta2, eta3) must lie in (0, 1)^3. Points outside this + range are typically flagged with value -1 in outputs, or optionally removed. + + Examples + -------- + Concrete domain implementations can be created and used as follows: + + >>> domain = Cuboid() # Simple cubic domain + >>> x = domain(0.5, 0.5, 0.5) # Evaluate mapping at logical coordinates + >>> J = domain.jacobian(0.5, 0.5, 0.5) # Evaluate Jacobian matrix + >>> detJ = domain.jacobian_det(0.5, 0.5, 0.5) # Volume element """ def __init__( diff --git a/src/struphy/initial/base.py b/src/struphy/initial/base.py index ecaf7c133..a6c7447e5 100644 --- a/src/struphy/initial/base.py +++ b/src/struphy/initial/base.py @@ -6,10 +6,61 @@ class Perturbation(metaclass=ABCMeta): - """Base class for perturbations that can be chosen as initial conditions.""" + """Abstract base class for perturbation functions used as initial conditions in simulations. + + This class provides the interface and common functionality for defining perturbation fields + in logical (eta) or physical coordinate spaces. Subclasses must implement the ``__call__`` + method to define the perturbation as a callable function of spatial coordinates. + + The class supports flexible representation bases (p-forms, vector fields, physical coordinates) + and allows specification of which component is perturbed for vector-valued quantities. + + Attributes + ---------- + given_in_basis : str + Specifies the basis representation of the perturbation. Options: + + - '0', '1', '2', '3' : Differential form basis (0-form=scalar, 1-form, etc.) + - 'v' : Vector field basis + - 'physical' : Physical (mapped) domain coordinates + - 'physical_at_eta' : Physical components evaluated in logical (eta) domain, u(F(eta)) + - 'norm' : Normalized co-variant basis (delta_i / |delta_i|) + + comp : int + Component index for vector-valued perturbations (0-2 for vector components, + 0 for scalar-valued functions). Default is 0. + + Examples + -------- + Subclasses should override ``__call__`` to implement specific perturbation fields: + + >>> class CustomPerturbation(Perturbation): + ... def __init__(self): + ... self.given_in_basis = 'physical' + ... def __call__(self, eta1, eta2, eta3, flat_eval=False): + ... return eta1 * eta2 # Example perturbation field + """ @abstractmethod def __call__(self, eta1, eta2, eta3, flat_eval=False): + """Evaluate the perturbation field at given coordinates. + + Parameters + ---------- + eta1, eta2, eta3 : ndarray or float + Coordinate values in the eta (logical) space, or physical space depending on + the perturbation's basis representation. + + flat_eval : bool, default=False + If True, treat inputs as flattened arrays and return flattened output. + If False, preserve the array shapes for meshgrid-like evaluation. + + Returns + ------- + ndarray or float + Perturbation field values at the given coordinates, with shape matching + the input coordinates (or flattened if flat_eval=True). + """ pass def prepare_eval_pts(self): diff --git a/src/struphy/particles/parameters.py b/src/struphy/particles/parameters.py index a8b37f87c..2f90599d6 100644 --- a/src/struphy/particles/parameters.py +++ b/src/struphy/particles/parameters.py @@ -5,49 +5,59 @@ class LoadingParameters: - """Options for particle loading in parameter/launch files. + """Configuration for particle (marker) loading strategies and data sources. + + This class encapsulates all parameters needed to initialize particles in simulations, + including population size, spatial and velocity distributions, loading algorithms, and + restart/external data sources. Supports multiple loading strategies: Monte-Carlo based + distributions with customizable moments, regular grid tesselation, specific manually-defined + markers, and loading from restart or external HDF5 files. Parameters ---------- - Np : int - Total number of particles to load. + Np : int, optional + Total number of particles to load into the simulation. - ppc : int - Particles to load per cell if a grid is defined. Cells are defined from ``domain_array``. + ppc : int, optional + Particles per cell to load if a grid is defined. Cell divisions follow ``domain_array``. - ppb : int - Particles to load per sorting box. Sorting boxes are defined from ``boxes_per_dim``. + ppb : int, default=10 + Particles per sorting box. Sorting boxes are defined by ``boxes_per_dim``. - loading : LiteralOptions.OptsLoading - How to load markers: multiple options for Monte-Carlo, or "tesselation" for positioning them on a regular grid. + loading : LiteralOptions.OptsLoading, default="pseudo_random" + Loading algorithm strategy. Options include various Monte-Carlo methods or + 'tesselation' for regular grid positioning. - seed : int - Seed for random generator. If None, no seed is taken. + seed : int, optional + Seed for the random number generator for reproducible results. + If None, no seed is applied. - moments : tuple - Mean velocities and temperatures for the Gaussian sampling distribution. - If None, these are auto-calculated form the given background. + moments : tuple, optional + Mean velocities and temperatures defining the Gaussian velocity distribution. + If None, automatically computed from the background distribution. - spatial : LiteralOptions.OptsSpatialLoading - Draw uniformly in eta, or draw uniformly on the "disc" image of (eta1, eta2). + spatial : LiteralOptions.OptsSpatialLoading, default="uniform" + Spatial sampling method: 'uniform' samples uniformly in (eta1, eta2) coordinates, + while 'disc' samples uniformly on the disc image of the coordinate space. - specific_markers : tuple[tuple] - Each entry is a tuple of phase space coordinates (floats) of a specific marker to be initialized. + specific_markers : tuple[tuple], optional + Manually-defined markers as tuples of phase space coordinates (floats). + Each tuple represents a single particle's initial state. - n_quad : int - Number of quadrature points for tesselation. + n_quad : int, default=1 + Number of quadrature points used for tesselation-based particle loading. - dir_external : str - Load markers from external .hdf5 file (absolute path). + dir_external : str, optional + Absolute path to HDF5 file from which to load external marker data. - dir_particles_abs : str - Load markers from restart .hdf5 file (absolute path). + dir_particles_abs : str, optional + Absolute path to HDF5 file from which to load restart marker data. - dir_particles : str - Load markers from restart .hdf5 file (relative path to output folder). + dir_particles : str, optional + Relative path (relative to output folder) to HDF5 restart file for loading markers. - restart_key : str - Key in .hdf5 file's restart/ folder where marker array is stored. + restart_key : str, optional + HDF5 dataset key within the 'restart/' folder containing marker array data. """ def __init__( @@ -82,18 +92,23 @@ def __init__( class WeightsParameters: - """Options for particle weights in parameter/launch files. + """Configuration for particle weight handling and variance reduction. + + Manages particle weighting strategies used in Monte-Carlo type simulations, + including control variate techniques for variance reduction and weight thresholding + to eliminate negligibly-weighted particles. Parameters ---------- - control_variate : bool - Whether to use a control variate for noise reduction. + control_variate : bool, default=False + Whether to apply control variate variance reduction technique to particle weights. - reject_weights : bool - Whether to reject weights below threshold. + reject_weights : bool, default=False + Whether to filter out particles with weights below the specified threshold. - threshold : float - Threshold for rejecting weights. + threshold : float, default=0.0 + Minimum weight threshold. Particles with weights below this value are rejected + when ``reject_weights`` is True. """ def __init__( @@ -108,19 +123,25 @@ def __init__( class BoundaryParameters: - """Options for particle boundary conditions and SPH-reconstruction boundary conditions in parameter/launch files. + """Configuration for boundary conditions applied to particles and kernel reconstructions. + + Defines how particles behave at domain boundaries (particle boundary conditions) and + how smoothed particle hydrodynamics (SPH) kernel reconstructions are handled at domain + edges. Supports independent boundary conditions per spatial dimension. Parameters ---------- - bc : tuple[LiteralOptions.OptsMarkerBC] - Boundary conditions for particle movement. - Either 'remove', 'reflect', 'periodic' or 'refill' in each direction. + bc : tuple[LiteralOptions.OptsMarkerBC], default=("periodic", "periodic", "periodic") + Particle boundary conditions for each spatial direction (3D). Options per direction: + 'remove' (delete particles), 'reflect' (specular reflection), 'periodic' (wrap around), + or 'refill' (reload particles). - bc_refill : list - Either 'inner' or 'outer'. + bc_refill : list, optional + Refill strategy when 'refill' boundary condition is active. Either 'inner' or 'outer'. - bc_sph : tuple[LiteralOptions.OptsRecontructBC] - Boundary conditions for sph kernel reconstruction. + bc_sph : tuple[LiteralOptions.OptsRecontructBC], default=("periodic", "periodic", "periodic") + Boundary conditions for SPH kernel reconstruction in each spatial direction. + Typically matches or differs from ``bc`` depending on reconstruction needs. """ def __init__( @@ -135,25 +156,35 @@ def __init__( class BinningPlot: - """Options for particle binning (plots) in parameter/launch files. + """Configuration for particle phase-space binning and histogram generation. + + Produces binned distributions from particle data across specified phase-space coordinates. + Supports arbitrary combinations of spatial (eta) and velocity (v) coordinates with flexible + binning resolution and coordinate ranges. Automatically computes bin edges and allocates + storage for full and delta-f distributions. Parameters ---------- - slice : str - Coordinate-slice in phase space to bin. A combination of "e1", "e2", "e3", "v1", etc., separated by an underscore "_". - For example, "e1" showas a 1D binning plot over eta1, whereas "e1_v1" shows a 2D binning plot over eta1 and v1. - - n_bins : int | tuple[int] - Number of bins for each coordinate. - - ranges : tuple[int] | tuple[tuple[int]] = (0.0, 1.0) - Binning range (as an interval in R) for each coordinate. - - divide_by_jac : bool - Whether to divide by the Jacobian determinant (volume-to-0-form). - - output_quantity : BinningOutput - String literal used to determine weights in binning and the type of output + slice : str, default="e1" + Phase-space coordinates to bin, specified as underscore-separated coordinate names + (e.g., 'e1', 'e1_e2', 'e1_v1'). Valid coordinates: 'e1', 'e2', 'e3' (spatial), + 'v1', 'v2', 'v3' (velocity). Example: 'e1' produces 1D binning over eta1; + 'e1_v1' produces 2D binning over eta1 and v1. + + n_bins : int | tuple[int], default=128 + Number of bins per coordinate. If int, applies to all coordinates; if tuple, + specifies bins for each coordinate separately. + + ranges : tuple[float] | tuple[tuple[float]], default=(0.0, 1.0) + Binning ranges as intervals [min, max] for each coordinate. If a single tuple, + applies to all coordinates; if nested tuples, specifies range for each coordinate. + + divide_by_jac : bool, default=True + Whether to normalize distributions by the Jacobian determinant (volume-to-0-form + conversion). Set False to use unnormalized bin counts. + + output_quantity : LiteralOptions.BinningQuantity, default="density" + Quantity to compute in binning: determines weighting scheme and output format. """ def __init__( @@ -207,12 +238,23 @@ def df(self) -> xp.ndarray: class KernelDensityPlot: - """Options for SPH density plots in parameter/launch files. + """Configuration for smoothed particle hydrodynamics (SPH) density reconstructions. + + Evaluates particle density fields at structured grid points using SPH kernel + interpolation. Supports 1D, 2D, and 3D evaluations with independent resolution + control per dimension. Parameters ---------- - pts_e1, pts_e2, pts_e3 : int - Number of evaluation points in each direction. + pts_e1 : int, default=16 + Number of evaluation grid points in the first spatial direction (eta1). + + pts_e2 : int, default=16 + Number of evaluation grid points in the second spatial direction (eta2). + + pts_e3 : int, default=1 + Number of evaluation grid points in the third spatial direction (eta3). + Set to 1 for 2D density plots. """ def __init__( diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 112c64872..b9fde59e0 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -174,15 +174,33 @@ def __init__( class PostProcessor: - """Post-processing finished Struphy runs, eithr from Simulation object or from output path. + """Post-process results from 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. Parameters ---------- - sim : Simulation - Simulation object of finished run. + 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. - path_out: str - Path to Struphy output folder (in case no sim is given). + 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 returned by :func:`setup_derham` 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__( @@ -249,27 +267,26 @@ def process( create_vtk: bool = True, verbose: bool = False, ): - """Do post processing of data in self.path_out. + """Run post-processing for fields and particle data in ``self.path_out``. Parameters ---------- step : int - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - + Interval of saved time steps to post-process (1 = every step, 2 = every second step, ...). celldivide : int - Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. - + Grid refinement factor when evaluating FEM fields (e.g. ``celldivide=2`` evaluates two + points per cell in each logical direction). physical : bool - Wether to do post-processing into push-forwarded physical (xyz) components of fields. - + If True, also compute push-forwarded physical (x,y,z) components of fields. guiding_center : bool - Compute guiding-center coordinates (only from Particles6D). - + If True, compute guiding-center coordinates for particle orbits (requires + Particles6D marker data). classify : bool - Classify guiding-center trajectories (passing, trapped or lost). - + If True, run orbit classification (passing, trapped, lost) after computing orbits. create_vtk : bool - Whether vtk files should be created. + If True, create VTK files for visualisation. + verbose : bool + Verbosity flag. """ if MPI.COMM_WORLD.Get_rank() == 0: print(f"\nPost-processing path {self.path_out}") @@ -446,20 +463,25 @@ def process_particles( ) def _create_femfields(self, step: int = 1, verbose: bool = False): - """Creates instances of :class:`~struphy.feec.psydac_derham.SplineFunction` from distributed Struphy data. + """Reconstruct FEEC spline field objects from HDF5 output files. + + The method reads the distributed HDF5 files written by Struphy, builds one + :class:`SplineFunction` per saved variable and fills their DOF vectors from the + per-rank datasets. Parameters ---------- step : int - Whether to create FEM fields at every time step (step=1, default), every second time step (step=2), etc. + Time-step stride when reading saved snapshots (default 1). + verbose : bool + Verbosity flag. Returns ------- fields : dict - Nested dictionary holding :class:`~struphy.feec.psydac_derham.SplineFunction`: fields[t][name] contains the Field with the name "name" in the hdf5 file at time t. - + Nested dictionary mapping time -> species -> variable -> ``SplineFunction``. t_grid : xp.ndarray - Time grid. + Array of times at which fields were reconstructed. """ # 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: @@ -555,36 +577,27 @@ def _eval_femfields( physical: bool = False, verbose: bool = False, ): - """Evaluate FEM fields obtained from :meth:`struphy.post_processing.post_processing_tools.create_femfields`. + """Evaluate spline fields on a regular logical grid and optionally push to physical coords. Parameters ---------- - params_in : ParamsIn - Simulation parameters. - fields : dict - Obtained from struphy.diagnostics.post_processing.create_femfields. - - celldivide : list of ints - Grid refinement in each eta direction. - - physical : bool - Wether to do post-processing into push-forwarded physical (xyz) components of fields. + Nested dictionary as returned by :meth:`_create_femfields` (time -> species -> var -> SplineFunction). + celldivide : list of int, optional + Refinement factor in each logical direction; length must be 3. + physical : bool, optional + If True, return mapped physical components (x,y,z) using the domain mapping. + verbose : bool, optional + Verbosity flag. Returns ------- point_data : dict - Nested dictionary holding values of FemFields on the grid as list of 3d xp.arrays: - point_data[name][t] contains the values of the field with name "name" in fields[t].keys() at time t. - - If physical is True, physical components of fields are saved. - Otherwise, logical components (differential n-forms) are saved. - - grids_log : 3-list - 1d logical grids in each eta-direction with Nel[i]*cell_divide[i] + 1 entries in each direction. - - grids_phy : 3-list - Mapped (physical) grids obtained by domain(*grids_log). + Nested dictionary point_data[species][var][time] -> list of arrays (scalar or per-component). + grids_log : list + Logical 1D grids for each eta direction. + grids_phy : list + Physical coordinate arrays corresponding to the logical grids (domain(*grids_log)). """ # create logical and physical grids @@ -689,24 +702,22 @@ def _create_vtk( physical: bool = False, verbose: bool = False, ): - """Creates structured virtual toolkit files (.vts) for Paraview from evaluated field data. + """Write evaluated field arrays to VTK (.vts) files for visualization. Parameters ---------- path : str - Absolute path of where to store the .vts files. Will then be in path/vtk/step_.vts. - + Directory where species subfolders and their `vtk` folders will be created. t_grid : xp.ndarray - Time grid. - - grids_phy : 3-list - Mapped (physical) grids obtained from struphy.diagnostics.post_processing.eval_femfields. - + Time grid corresponding to entries in ``point_data``. + grids_phy : list + Physical coordinate arrays returned by :meth:`_eval_femfields`. point_data : dict - Field data obtained from struphy.diagnostics.post_processing.eval_femfields. - - physical : bool - Wether to create vtk for push-forwarded physical (xyz) components of fields. + Evaluated field values as returned by :meth:`_eval_femfields`. + physical : bool, optional + If True, writes files for push-forwarded physical components (folder suffix "_phy"). + verbose : bool, optional + Verbosity flag. """ for species, vars in point_data.items(): species_path = os.path.join(path, species, "vtk" + physical * "_phy") @@ -751,53 +762,22 @@ def _post_process_markers( step: int = 1, verbose: bool = False, ): - """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation - and writes them to a .npy files and to .txt files. - Also saves the weights. - - * ``.npy`` files: - - * Particles6D: - - ===== ===== ============== ============= ====== - index | 0 | | 1 | 2 | 3 | | 4 | 5 | 6 | | 7 | - ===== ===== ============== ============= ====== - value ID position (xyz) velocities weight - ===== ===== ============== ============= ====== - - * Particles5D: - - ===== ===== ================ ========== ====== ====== ============ - index | 0 | | 1 | 2 | | 3 | 4 5 | 6 | 7 - ===== ===== ================ ========== ====== ====== ============ - value ID guiding_center v_parallel v_perp weight magn. moment - ===== ===== ================ ========== ====== ====== ============ - - * Particles3D: - - ===== ===== ============== ====== - index | 0 | | 1 | 2 | 3 | | 4 | - ===== ===== ============== ====== - value ID position (xyz) weight - ===== ===== ============== ====== - - * ``.txt`` files : - - ===== ===== ============== ====== - index | 0 | | 1 | 2 | 3 | | 4 | - ===== ===== ============== ====== - value ID position (xyz) weight - ===== ===== ============== ====== + """Compute Cartesian marker positions and write them to .npy and .txt files. - ``.txt`` files can be imported to e.g. Paraview, see `08 - Kinetic data `_ for details. + 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 kinetic data of considered species. - + Path to the per-species kinetic output directory where results will be written. step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + Time-step stride to process (default 1). + verbose : bool, optional + Verbosity flag. """ species = path_kinetic_species.split("/")[-1] @@ -893,19 +873,23 @@ def _post_process_f( compute_bckgr=False, verbose: bool = False, ): - """Computes and saves distribution functions of saved binning data during a simulation. + """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 kinetic data of considered species. - + Path to the per-species kinetic output directory. step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - compute_bckgr : bool - Whether to compute the kinetic background values and add them to the binning data. - This is used if non-standard weights are binned. + Time-step stride to process (default 1). + compute_bckgr : bool, optional + If True, compute and add background contribution to the saved binned data. + verbose : bool, optional + Verbosity flag. """ species = path_kinetic_species.split("/")[-1] species_obj: ParticleSpecies = self.model.particle_species[species] @@ -1045,15 +1029,16 @@ def _post_process_n_sph( step=1, verbose: bool = False, ): - """Computes and saves the density n of saved sph data during a simulation. + """Compute and save SPH density fields from per-rank outputs. Parameters ---------- path_kinetic_species : str - Path to kinetic data of considered species. - + Path to the per-species kinetic output directory where results will be written. step : int, optional - Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + Time-step stride to process (default 1). + verbose : bool, optional + Verbosity flag. """ species = path_kinetic_species.split("/")[-1] @@ -1104,12 +1089,45 @@ def _post_process_n_sph( class PlottingData: - """Holds post-processed plotting data as attributes. + """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 ---------- - path : str - Absolute path of simulation output folder to post-process. + 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): @@ -1135,26 +1153,76 @@ def __init__(self, sim: "Simulation" = None, path_out: str = None): @property def orbits(self) -> Orbits: - """Keys: species name. Values: 3d arrays indexed by (n, p, a), where 'n' is the time index, 'p' the particle index and 'a' the attribute index.""" + """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: - """Keys: species name. Values: dicts of slice names ('e1_v1' etc.) holding dicts of corresponding xp.arrays for plotting.""" + """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: - """Keys: species name. Values: dicts of variable names with values being 3d arrays on the grid.""" + """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: - """Keys: species name. Values: dicts of view names ('view_0' etc.) holding dicts of corresponding xp.arrays for plotting.""" + """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 def load(self, verbose: bool = False): - """Load data generated during post-processing.""" + """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). + + Parameters + ---------- + verbose : bool, optional + If True, print diagnostic information during loading (default False). + + Raises + ------ + FileNotFoundError + If expected post-processing files are missing. + NotImplementedError + If an unexpected data folder structure is encountered. + """ print("\nLoading post-processed plotting data:") print(f"Data path: {self.path_pproc}") From 4f1ecf2ae75fc99b3c696bee52059bd4ccc5df83 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 13:19:22 +0100 Subject: [PATCH 61/80] add tests for submodule struphy-parameter-files --- .github/workflows/submod-struphy-params.yml | 55 ++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/.github/workflows/submod-struphy-params.yml b/.github/workflows/submod-struphy-params.yml index 3c4fa5a75..2955121eb 100644 --- a/.github/workflows/submod-struphy-params.yml +++ b/.github/workflows/submod-struphy-params.yml @@ -28,8 +28,59 @@ jobs: uses: ./.github/actions/submodule-diff with: submod-name: struphy-parameter-files + + - name: Install prerequisites (Ubuntu) + if: env.SUBMOD_CHANGED == 'true' + uses: ./.github/actions/install/ubuntu-latest + + - name: Install Struphy + if: env.SUBMOD_CHANGED == 'true' + uses: ./.github/actions/install/install-struphy + with: + optional-deps: 'dev,doc' + + - name: Compile Struphy + if: env.SUBMOD_CHANGED == 'true' + uses: ./.github/actions/compile + with: + language: c - - name: Run workflow if submodule changed (all PR commits) + - name: Run weak Landau damping + if: env.SUBMOD_CHANGED == 'true' + run: | + echo "${{ env.SUBMOD_NAME }} has changed, running test..." + ls + cd struphy-parameter-files/VlasovAmpereOneSpecies/weak_Landau_damping + ls + mpirun -n 2 python params_weak_Landau_damping.py + python pproc_weak_Landau_damping.py + + - name: Run strong Landau damping + if: env.SUBMOD_CHANGED == 'true' + run: | + echo "${{ env.SUBMOD_NAME }} has changed, running test..." + ls + cd struphy-parameter-files/VlasovAmpereOneSpecies/strong_Landau_damping + ls + mpirun -n 2 python params_strong_Landau_damping.py + python pproc_strong_Landau_damping.py + + - name: Run two-stream instability + if: env.SUBMOD_CHANGED == 'true' + run: | + echo "${{ env.SUBMOD_NAME }} has changed, running test..." + ls + cd struphy-parameter-files/VlasovAmpereOneSpecies/two_stream + ls + mpirun -n 2 python params_two_stream.py + python pproc_two_stream.py + + - name: Run bump-on-tail instability 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 test..." + ls + cd struphy-parameter-files/VlasovAmpereOneSpecies/bump_on + ls + mpirun -n 2 python params_bump_on.py + python pproc_bump_on.py From 30995ba9252c173096711cce19435e187c1e8706 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 15:26:14 +0100 Subject: [PATCH 62/80] adapted submodeule struphy-parameter-files --- src/struphy/models/base.py | 6 +- src/struphy/models/cold_plasma_vlasov.py | 3 - src/struphy/models/species.py | 36 +++++++--- .../models/vlasov_ampere_one_species.py | 3 - src/struphy/simulation/sim.py | 71 +++++++++---------- struphy-parameter-files | 2 +- 6 files changed, 66 insertions(+), 55 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 4f32e0175..4a32efb45 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -288,7 +288,7 @@ def print_scalar_quantities(self): for key, scalar_dict in self._scalar_quantities.items(): val = scalar_dict["value"] assert not xp.isnan(val[0]), f"Scalar {key} is {val[0]}." - sq_str += f"{key}:".ljust(25) + "{:3.1e}\n".format(val[0]).rjust(26) + sq_str += f"{key}:".ljust(25) + "{:4.2e}\n".format(val[0]).rjust(26) print(sq_str) def setup_equation_params(self, units: Units, verbose=False): @@ -576,9 +576,7 @@ def generate_default_parameter_file( 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.\n\"\"\" -\nprint(f"\\nRunning {{__file__}}.") -print(description)\n""") +Users can modify this file to set up their own simulations with different parameters and initial conditions.\n\"\"\"\n""") file.write("""\n# ------------------ # Import Struphy API diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 3120c0753..0ab567065 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -141,9 +141,6 @@ def __init__(self): self.initial_poisson = propagators_fields.Poisson() self.initial_poisson.variables.phi = self.em_fields.phi - if rank == 0: - print("... Done.") - @property def bulk_species(self): return self.thermal_elec diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 514e500f4..b49038e6f 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -127,19 +127,39 @@ def set_species_properties( epsilon: float = None, kappa: float = None, ): - """Set charge- and mass number of species in parameter/launch files. - Optional: Set equation parameters (alpha, epsilon, kappa) to override units.""" + """Set physical and equation parameters for a plasma species. + + Sets the charge and mass numbers, and optionally overrides normalized equation parameters + (alpha, epsilon, kappa) that would otherwise be computed from physical units. + + Parameters + ---------- + charge_number : int, optional + Charge number in units of elementary charge (default = 1). + mass_number : int, optional + Mass number in units of proton mass (default = 1). + alpha : float, optional + Dimensionless parameter: plasma frequency / cyclotron frequency. + If None, computed from units and charge/mass numbers (default = None). + epsilon : float, optional + Normalized cyclotron period: 1 / (cyclotron frequency × time unit). + If None, computed from units and charge/mass numbers (default = None). + kappa : float, optional + Normalized plasma frequency: plasma frequency × time unit. + If None, computed from units and charge/mass numbers (default = None). + + Notes + ----- + This method should be called BEFORE instantiating a Simulation object. + For existing simulation objects, call Simulation.normalize_model() to apply changes. + A warning will be issued if this requirement is not followed.""" self._charge_number = charge_number self._mass_number = mass_number self._alpha = alpha self._epsilon = epsilon self._kappa = kappa - - if MPI.COMM_WORLD.Get_rank() == 0: - warnings.warn( - "\nSpecies.set_species_properties() should be run before instantiating a simulation.\nRun Simulation.normalize_model() for existing simulation objects." - ) + class EquationParameters: """Normalization parameters of one species, appearing in scaled equations.""" @@ -253,7 +273,7 @@ class FluidSpecies(Species): Examples -------- >>> ions = FluidSpecies() - >>> ions.set_species_properties(charge_number=1, mass_number=1836) # Protons + >>> ions.set_species_properties(charge_number=-1, mass_number=1/1836) # electrons """ diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index ec55bad24..78b64816c 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -145,9 +145,6 @@ def __init__(self, with_B0: bool = True): self.initial_poisson = propagators_fields.Poisson() self.initial_poisson.variables.phi = self.em_fields.phi - if rank == 0: - print("... Done.") - @property def bulk_species(self): return self.kinetic_ions diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 8b6745892..1e09266ad 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -276,7 +276,7 @@ def show_parameters(self): Only the MPI rank 0 prints to avoid clutter from multiple processes. """ if self.rank == 0: - print("\nSIMULATION PARAMETERS:") + print("SIMULATION PARAMETERS:") print("\nModel:") print(self.model) print("Parameter file path:") @@ -393,7 +393,8 @@ def run(self, verbose: bool = False): If True, print additional runtime information. """ - print(f"\nStarting simulation run for model {self.model_name} ...") + if self.rank == 0: + print(f"\nStarting simulation run for model {self.model_name} ...") self._remove_existing_output_files(verbose=verbose) @@ -553,16 +554,16 @@ def run(self, verbose: bool = False): if self.rank == 0 and verbose: step = str(self.time_state["index"][0]).zfill(len(total_steps)) - message = "time step: " + step + "/" + str(total_steps) + message = "time step:".ljust(25) + f"{step}/{total_steps}".rjust(25) message += ( "\n" + "normalized time:".ljust(25) - + "{0:3.1e} / {1:3.1e}".format(self.time_state["value"][0], Tend).rjust(25) + + "{0:4.2e} / {1:4.2e}".format(self.time_state["value"][0], Tend).rjust(25) ) message += ( "\n" + "physical time [s]:".ljust(25) - + "{0:3.1e} / {1:3.1e}".format( + + "{0:4.2e} / {1:4.2e}".format( self.time_state["value_sec"][0], Tend * self.units.t, ).rjust(25) @@ -824,9 +825,6 @@ def _setup_folders(self, verbose: bool = False): Setup output folders. """ if MPI.COMM_WORLD.Get_rank() == 0: - if verbose: - print("\nPREPARATION AND CLEAN-UP:") - # create output folder if it does not exit if not os.path.exists(self.env.path_out): os.makedirs(self.env.path_out, exist_ok=True) @@ -842,41 +840,42 @@ def _setup_folders(self, verbose: bool = False): def _remove_existing_output_files(self, verbose: bool = False): """Removes post_processing/, meta.txt and profile_tmp. If not restart, also removes existing hdf5 and png files in output folder.""" - # remove post_processing folder - folder = os.path.join(self.env.path_out, "post_processing") - if os.path.exists(folder): - shutil.rmtree(folder) - if verbose: - print("Removed existing folder " + folder) - - # remove meta file - file = os.path.join(self.env.path_out, "meta.txt") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove profiling file - file = os.path.join(self.env.path_out, "profile_tmp") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) + if MPI.COMM_WORLD.Get_rank() == 0: + # remove post_processing folder + folder = os.path.join(self.env.path_out, "post_processing") + if os.path.exists(folder): + shutil.rmtree(folder) + if verbose: + print("Removed existing folder " + folder) - # remove hdf5 and png files (if NOT a restart) - if not self.env.restart: - files = glob.glob(os.path.join(self.env.path_out, "data", "*.hdf5")) - for n, file in enumerate(files): + # remove meta file + file = os.path.join(self.env.path_out, "meta.txt") + if os.path.exists(file): os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes + if verbose: print("Removed existing file " + file) - files = glob.glob(os.path.join(self.env.path_out, "*.png")) - for n, file in enumerate(files): + # remove profiling file + file = os.path.join(self.env.path_out, "profile_tmp") + if os.path.exists(file): os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes + if verbose: print("Removed existing file " + file) + # remove hdf5 and png files (if NOT a restart) + if not self.env.restart: + files = glob.glob(os.path.join(self.env.path_out, "data", "*.hdf5")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + + files = glob.glob(os.path.join(self.env.path_out, "*.png")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) + def _setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium, verbose: bool = False): """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" if equil is not None: diff --git a/struphy-parameter-files b/struphy-parameter-files index 40e2845db..4f334693d 160000 --- a/struphy-parameter-files +++ b/struphy-parameter-files @@ -1 +1 @@ -Subproject commit 40e2845dbf543495a1b9f9a9c979cd13cf3dfdf7 +Subproject commit 4f334693daca3443bb571bc22b4327a1c235d754 From 470190ca7fe536628ad729fa167a3f341a7f85f6 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 15:26:45 +0100 Subject: [PATCH 63/80] formatting --- src/struphy/geometry/base.py | 2 +- src/struphy/models/species.py | 1 - src/struphy/particles/parameters.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index 30bf2c5ff..23d8514f3 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -35,7 +35,7 @@ class Domain(metaclass=ABCMeta): unit cube to physical points :math:`\mathbf x \in \Omega`. The corresponding Jacobian matrix :math:`DF:\hat\Omega \to \mathbb R^{3\times 3}`, - volume element :math:`\sqrt g: \hat\Omega \to \mathbb R`, and metric tensor + volume element :math:`\sqrt g: \hat\Omega \to \mathbb R`, and metric tensor :math:`G:\hat\Omega \to \mathbb R^{3\times 3}` are defined by .. math:: diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index b49038e6f..bbddbb197 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -159,7 +159,6 @@ def set_species_properties( self._alpha = alpha self._epsilon = epsilon self._kappa = kappa - class EquationParameters: """Normalization parameters of one species, appearing in scaled equations.""" diff --git a/src/struphy/particles/parameters.py b/src/struphy/particles/parameters.py index b2fc9662d..3decb8711 100644 --- a/src/struphy/particles/parameters.py +++ b/src/struphy/particles/parameters.py @@ -46,7 +46,7 @@ class LoadingParameters: n_quad : int, default=1 Number of quadrature points used for tesselation-based particle loading. - + set_zero_velocity: tuple Initialize velocity of Maxwellain along selected axis to be zero. From 0ad81d80bbe404cdf012cc44b634ee6935e00a65 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 20 Feb 2026 16:29:40 +0100 Subject: [PATCH 64/80] new submod commit --- struphy-parameter-files | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/struphy-parameter-files b/struphy-parameter-files index 4f334693d..5781701ed 160000 --- a/struphy-parameter-files +++ b/struphy-parameter-files @@ -1 +1 @@ -Subproject commit 4f334693daca3443bb571bc22b4327a1c235d754 +Subproject commit 5781701ed9d36997bdcdb015a310bf7d7c42ba86 From d53a1253bb9e0e37d52a34ed3a8ca646f65f45ef Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 23 Feb 2026 12:13:12 +0100 Subject: [PATCH 65/80] new commit in submodule struphy-tutorials --- struphy-tutorials | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/struphy-tutorials b/struphy-tutorials index bc9160387..e396c06c0 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit bc9160387bf5c61367eba729362bd00690235ebe +Subproject commit e396c06c083af309bf2141515495a855f41a6b12 From d8e3a09c410f03f9319e82fc74cdfcb084a74d00 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 23 Feb 2026 12:35:30 +0100 Subject: [PATCH 66/80] add noslip boundary conditions for sph --- src/struphy/io/options.py | 2 +- src/struphy/pic/base.py | 63 +++++++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index 8cacabb72..d486617e3 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -52,7 +52,7 @@ class LiteralOptions: # markers OptsPICSpace = Literal["Particles6D", "DeltaFParticles6D", "Particles5D", "Particles3D"] OptsMarkerBC = Literal["periodic", "reflect"] - OptsRecontructBC = Literal["periodic", "mirror", "fixed"] + OptsRecontructBC = Literal["periodic", "mirror", "fixed", "noslip"] OptsLoading = Literal[ "pseudo_random", "sobol_standard", diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index d976457fd..157d6521d 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -296,7 +296,7 @@ def __init__( bc_sph = [bci if bci == "periodic" else "mirror" for bci in self.bc] for bci in bc_sph: - assert bci in ("periodic", "mirror", "fixed") + assert bci in ("periodic", "mirror", "fixed", "noslip") self._bc_sph = bc_sph # particle type @@ -2319,7 +2319,7 @@ class SortingBoxes: bc_sph : list Boundary condition for sph density evaluation. - Either 'periodic', 'mirror' or 'fixed' in each direction. + Either 'periodic', 'mirror', 'fixed' or 'noslip' in each direction. is_domain_boundary: dict Has two booleans for each direction; True when the boundary of the MPI process is a domain boundary. @@ -2448,19 +2448,19 @@ def _compute_sph_index_shifts(self): self._bc_sph_index_shifts["z_m"] = flatten_index(0, 0, self.nz, self.nx, self.ny, self.nz) self._bc_sph_index_shifts["z_p"] = flatten_index(0, 0, self.nz, self.nx, self.ny, self.nz) - if self.bc_sph[0] in ("mirror", "fixed"): + if self.bc_sph[0] in ("mirror", "fixed", "noslip"): if self.is_domain_boundary["x_m"]: self._bc_sph_index_shifts["x_m"] = flatten_index(-1, 0, 0, self.nx, self.ny, self.nz) if self.is_domain_boundary["x_p"]: self._bc_sph_index_shifts["x_p"] = flatten_index(-1, 0, 0, self.nx, self.ny, self.nz) - if self.bc_sph[1] in ("mirror", "fixed"): + if self.bc_sph[1] in ("mirror", "fixed", "noslip"): if self.is_domain_boundary["y_m"]: self._bc_sph_index_shifts["y_m"] = flatten_index(0, -1, 0, self.nx, self.ny, self.nz) if self.is_domain_boundary["y_p"]: self._bc_sph_index_shifts["y_p"] = flatten_index(0, -1, 0, self.nx, self.ny, self.nz) - if self.bc_sph[2] in ("mirror", "fixed"): + if self.bc_sph[2] in ("mirror", "fixed", "noslip"): if self.is_domain_boundary["z_m"]: self._bc_sph_index_shifts["z_m"] = flatten_index(0, 0, -1, self.nx, self.ny, self.nz) if self.is_domain_boundary["z_p"]: @@ -2782,21 +2782,21 @@ def prepare_ghost_particles(self): self._markers_z_p[:, self._sorting_boxes.box_index] -= shifts["z_p"] # Mirror position for boundary condition - if self.bc_sph[0] in ("mirror", "fixed"): + if self.bc_sph[0] in ("mirror", "fixed", "noslip"): self._mirror_particles( "_markers_x_m", "_markers_x_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, ) - if self.bc_sph[1] in ("mirror", "fixed"): + if self.bc_sph[1] in ("mirror", "fixed", "noslip"): self._mirror_particles( "_markers_y_m", "_markers_y_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, ) - if self.bc_sph[2] in ("mirror", "fixed"): + if self.bc_sph[2] in ("mirror", "fixed", "noslip"): self._mirror_particles( "_markers_z_m", "_markers_z_p", @@ -2824,7 +2824,7 @@ def prepare_ghost_particles(self): self._markers_x_p_y_p[:, self._sorting_boxes.box_index] += -shifts["x_p"] - shifts["y_p"] # Mirror position for boundary condition - if self.bc_sph[0] in ("mirror", "fixed") or self.bc_sph[1] in ("mirror", "fixed"): + if self.bc_sph[0] in ("mirror", "fixed", "noslip") or self.bc_sph[1] in ("mirror", "fixed", "noslip"): self._mirror_particles( "_markers_x_m_y_m", "_markers_x_m_y_p", @@ -2854,7 +2854,7 @@ def prepare_ghost_particles(self): self._markers_x_p_z_p[:, self._sorting_boxes.box_index] += -shifts["x_p"] - shifts["z_p"] # Mirror position for boundary condition - if self.bc_sph[0] in ("mirror", "fixed") or self.bc_sph[2] in ("mirror", "fixed"): + if self.bc_sph[0] in ("mirror", "fixed", "noslip") or self.bc_sph[2] in ("mirror", "fixed", "noslip"): self._mirror_particles( "_markers_x_m_z_m", "_markers_x_m_z_p", @@ -2884,7 +2884,7 @@ def prepare_ghost_particles(self): self._markers_y_p_z_p[:, self._sorting_boxes.box_index] += -shifts["y_p"] - shifts["z_p"] # Mirror position for boundary condition - if self.bc_sph[1] in ("mirror", "fixed") or self.bc_sph[2] in ("mirror", "fixed"): + if self.bc_sph[1] in ("mirror", "fixed", "noslip") or self.bc_sph[2] in ("mirror", "fixed", "noslip"): self._mirror_particles( "_markers_y_m_z_m", "_markers_y_m_z_p", @@ -2926,7 +2926,7 @@ def prepare_ghost_particles(self): self._markers_x_p_y_p_z_p[:, self._sorting_boxes.box_index] += -shifts["x_p"] - shifts["y_p"] - shifts["z_p"] # Mirror position for boundary condition - if any([bci in ("mirror", "fixed") for bci in self.bc_sph]): + if any([bci in ("mirror", "fixed", "noslip") for bci in self.bc_sph]): self._mirror_particles( "_markers_x_m_y_m_z_m", "_markers_x_m_y_m_z_p", @@ -2950,7 +2950,7 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): continue # x-direction - if self.bc_sph[0] in ("mirror", "fixed"): + if self.bc_sph[0] in ("mirror", "fixed", "noslip"): if "x_m" in arr_name and is_domain_boundary["x_m"]: arr[:, 0] *= -1.0 if self.bc_sph[0] == "fixed" and arr_name not in self._fixed_markers_set: @@ -2964,6 +2964,12 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): remove_holes=False, ) self._fixed_markers_set[arr_name] = True + elif self.bc_sph[0] == "noslip": + # invert the velocities to have zero velocity at the boundary + arr[:, 3] *= -1.0 + arr[:, 4] *= -1.0 + arr[:, 5] *= -1.0 + elif "x_p" in arr_name and is_domain_boundary["x_p"]: arr[:, 0] = 2.0 - arr[:, 0] if self.bc_sph[0] == "fixed" and arr_name not in self._fixed_markers_set: @@ -2977,9 +2983,14 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): remove_holes=False, ) self._fixed_markers_set[arr_name] = True + elif self.bc_sph[0] == "noslip": + # invert the velocities to have zero velocity at the boundary + arr[:, 3] *= -1.0 + arr[:, 4] *= -1.0 + arr[:, 5] *= -1.0 # y-direction - if self.bc_sph[1] in ("mirror", "fixed"): + if self.bc_sph[1] in ("mirror", "fixed", "noslip"): if "y_m" in arr_name and is_domain_boundary["y_m"]: arr[:, 1] *= -1.0 if self.bc_sph[1] == "fixed" and arr_name not in self._fixed_markers_set: @@ -2993,6 +3004,12 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): remove_holes=False, ) self._fixed_markers_set[arr_name] = True + elif self.bc_sph[1] == "noslip": + # invert the velocities to have zero velocity at the boundary + arr[:, 3] *= -1.0 + arr[:, 4] *= -1.0 + arr[:, 5] *= -1.0 + elif "y_p" in arr_name and is_domain_boundary["y_p"]: arr[:, 1] = 2.0 - arr[:, 1] if self.bc_sph[1] == "fixed" and arr_name not in self._fixed_markers_set: @@ -3006,9 +3023,14 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): remove_holes=False, ) self._fixed_markers_set[arr_name] = True + elif self.bc_sph[1] == "noslip": + # invert the velocities to have zero velocity at the boundary + arr[:, 3] *= -1.0 + arr[:, 4] *= -1.0 + arr[:, 5] *= -1.0 # z-direction - if self.bc_sph[2] in ("mirror", "fixed"): + if self.bc_sph[2] in ("mirror", "fixed", "noslip"): if "z_m" in arr_name and is_domain_boundary["z_m"]: arr[:, 2] *= -1.0 if self.bc_sph[2] == "fixed" and arr_name not in self._fixed_markers_set: @@ -3022,6 +3044,12 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): remove_holes=False, ) self._fixed_markers_set[arr_name] = True + elif self.bc_sph[2] == "noslip": + # invert the velocities to have zero velocity at the boundary + arr[:, 3] *= -1.0 + arr[:, 4] *= -1.0 + arr[:, 5] *= -1.0 + elif "z_p" in arr_name and is_domain_boundary["z_p"]: arr[:, 2] = 2.0 - arr[:, 2] if self.bc_sph[2] == "fixed" and arr_name not in self._fixed_markers_set: @@ -3035,6 +3063,11 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): remove_holes=False, ) self._fixed_markers_set[arr_name] = True + elif self.bc_sph[2] == "noslip": + # invert the velocities to have zero velocity at the boundary + arr[:, 3] *= -1.0 + arr[:, 4] *= -1.0 + arr[:, 5] *= -1.0 def determine_markers_in_box(self, list_boxes): """Determine the markers that belong to a certain box (list of boxes) and put them in an array""" From 425fe553c4b30ac18bff9378348529a1f9d26523 Mon Sep 17 00:00:00 2001 From: Amin Raiessi Date: Mon, 23 Feb 2026 17:13:34 +0100 Subject: [PATCH 67/80] Test for boundary condition. work in progress --- src/struphy/pic/tests/test_sph.py | 188 ++++++++++++++++++++++++++++-- 1 file changed, 178 insertions(+), 10 deletions(-) diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index b2a384177..1ef9a3060 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1695,16 +1695,7 @@ def abs_err(num, exact): assert err_div_y < 3.5e-2 -if __name__ == "__main__": - test_sph_viscosity_evaluation_2d( - (12, 12, 1), - "gaussian_2d", - "periodic", - "periodic", - 101, - tesselation=True, - show_plot=False, - ) + # test_sph_velocity_evaluation_2d( # (12, 12, 1), "gaussian_2d", 1, "periodic", "periodic", 101, tesselation=False, show_plot=True # ) @@ -1764,3 +1755,180 @@ def abs_err(num, exact): # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "fixed", "periodic", tesselation=True, show_plot=True) # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "fixed", "fixed", tesselation=True, show_plot=True) # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "mirror", "mirror", tesselation=True, show_plot=True) + + +@pytest.mark.parametrize("boxes_per_dim", [(12, 1, 1)]) +@pytest.mark.parametrize("kernel", ["gaussian_1d", "linear_1d"]) +@pytest.mark.parametrize("tesselation", [False, True]) +@pytest.mark.parametrize("direction", ["x", "y", "z"]) +def test_sph_no_slip_boundary_1d( + boxes_per_dim, + kernel, + tesselation, + direction, + show_plot=False, +): + + if isinstance(MPI.COMM_WORLD, MockComm): + comm = None + rank = 0 + else: + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + + + dom_type = "Cuboid" + dom_params = {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0} + domain_class = getattr(domains, dom_type) + domain = domain_class(**dom_params) + + if tesselation: + ppb = 20 + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") + else: + ppb = 2000 + loading_params = LoadingParameters(ppb=ppb, seed=223) + + + if direction == "x": + def u_xyz(x, y, z): + return (xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) + elif direction == "y": + def u_xyz(x, y, z): + return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) + else: # direction == "z" + def u_xyz(x, y, z): + return (xp.zeros_like(x), xp.zeros_like(x), xp.ones_like(x)) + + background = equils.GenericCartesianFluidEquilibrium(u_xyz=u_xyz) + background.domain = domain + + + boundary_params = BoundaryParameters(bc_sph=("noslip", "periodic", "periodic")) + + particles = ParticlesSPH( + comm_world=comm, + loading_params=loading_params, + boundary_params=boundary_params, + boxes_per_dim=boxes_per_dim, + bufsize=1.0, + box_bufsize=2.0, + domain=domain, + background=background, + n_as_volume_form=True, + verbose=False, + ) + + particles.draw_markers(sort=False, verbose=False) + if comm is not None: + particles.mpi_sort_markers() + particles.initialize_weights() + + # Evaluation points: walls (eta=0, eta=1) and a few interior points + #This yields the order: left wall (0.0), right wall (1.0), then interior points.So the right wall is at index 1, not at -1. + eta_walls = xp.array([0.0, 1.0]) + eta_interior = xp.linspace(0.1, 0.9, 50) + eta1 = xp.concatenate([eta_walls, eta_interior]) + eta2 = xp.array([0.5]) + eta3 = xp.array([0.5]) + + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") + + + h1 = 1 / boxes_per_dim[0] + h2 = 1 / boxes_per_dim[1] + h3 = 1 / boxes_per_dim[2] + + v1, v2, v3 = particles.eval_velocity( + ee1, ee2, ee3, + h1=h1, h2=h2, h3=h3, + kernel_type=kernel, + derivative=0, + ) + + + if comm is not None: + all_v1 = xp.zeros_like(v1) + all_v2 = xp.zeros_like(v2) + all_v3 = xp.zeros_like(v3) + comm.Allreduce(v1, all_v1, op=MPI.SUM) + comm.Allreduce(v2, all_v2, op=MPI.SUM) + comm.Allreduce(v3, all_v3, op=MPI.SUM) + else: + all_v1, all_v2, all_v3 = v1, v2, v3 + + # Extract values at walls (first two points) and interior (remaining points) + # ee1 has shape (len(eta1), 1, 1) – we squeeze to 1D + v1_squeezed = all_v1.squeeze() + v2_squeezed = all_v2.squeeze() + v3_squeezed = all_v3.squeeze() + + + v_wall_left = (v1_squeezed[0], v2_squeezed[0], v3_squeezed[0]) + v_wall_right = (v1_squeezed[1], v2_squeezed[1], v3_squeezed[1]) + + v_interior = (v1_squeezed[2:], v2_squeezed[2:], v3_squeezed[2:]) + + #gonna fix this later + if tesselation: + tol_wall = 1e-5 + tol_interior = 5e-2 + else: + tol_wall = 1e-3 + tol_interior = 1.5e-1 + + + for comp, name in zip([0, 1, 2], ["x", "y", "z"]): + val_left = [v_wall_left[0], v_wall_left[1], v_wall_left[2]][comp] + val_right = [v_wall_right[0], v_wall_right[1], v_wall_right[2]][comp] + assert xp.abs(val_left) < tol_wall, f"Left wall {name}-velocity not zero: {val_left}" + assert xp.abs(val_right) < tol_wall, f"Right wall {name}-velocity not zero: {val_right}" + + # The component in the chosen direction should be ~1, + # the other two should be near zero. + if direction == "x": + interior_vals = v_interior[0] + other1 = v_interior[1] + other2 = v_interior[2] + elif direction == "y": + interior_vals = v_interior[1] + other1 = v_interior[0] + other2 = v_interior[2] + else: + interior_vals = v_interior[2] + other1 = v_interior[0] + other2 = v_interior[1] + + # The main component should be close to 1 + rel_error = xp.max(xp.abs(interior_vals - 1.0)) / 1.0 + assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" + + # The other components should be near zero + assert xp.max(xp.abs(other1)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other1))}" + assert xp.max(xp.abs(other2)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other2))}" + + if rank == 0 and show_plot: + + plt.figure(figsize=(8, 5)) + plt.plot(eta1, v1_squeezed, 'o-', label='v_x') + plt.plot(eta1, v2_squeezed, 's-', label='v_y') + plt.plot(eta1, v3_squeezed, 'd-', label='v_z') + plt.axhline(0, color='k', linestyle='--', linewidth=0.5) + plt.axhline(1, color='gray', linestyle='--', linewidth=0.5) + plt.xlabel('eta1') + plt.ylabel('velocity') + plt.title(f'No-slip test ({direction}-direction, {kernel}, tesselation={tesselation})') + plt.legend() + plt.grid(True) + plt.show() + plt.savefig("bc_sph") + +if __name__ == "__main__": + test_sph_no_slip_boundary_1d( + (4, 1, 1), + "gaussian_1d", + tesselation= True, + direction = "x", + show_plot=False, + ) + From 39ea742077a89bc6e8846cbd9ff74ef3ff3e464a Mon Sep 17 00:00:00 2001 From: Amin Raiessi Date: Tue, 24 Feb 2026 16:49:04 +0100 Subject: [PATCH 68/80] unit test for no slip boundary conditions finished --- src/struphy/pic/pushing/eval_kernels_gc.py | 5 ++- src/struphy/pic/tests/test_sph.py | 51 +++++++++++++--------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/struphy/pic/pushing/eval_kernels_gc.py b/src/struphy/pic/pushing/eval_kernels_gc.py index 1c39361f1..d6e27bab4 100644 --- a/src/struphy/pic/pushing/eval_kernels_gc.py +++ b/src/struphy/pic/pushing/eval_kernels_gc.py @@ -651,9 +651,10 @@ def sph_mean_velocity_coeffs( for ip in range(n_markers): # only do something if particle is a "true" particle - if not valid_mks[ip]: + #if not valid_mks[ip]: + # continue + if holes[ip]: continue - eta1 = markers[ip, 0] eta2 = markers[ip, 1] eta3 = markers[ip, 2] diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 1ef9a3060..fd3136de6 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1783,7 +1783,7 @@ def test_sph_no_slip_boundary_1d( domain = domain_class(**dom_params) if tesselation: - ppb = 20 + ppb = 2000 loading_params = LoadingParameters(ppb=ppb, loading="tesselation") else: ppb = 2000 @@ -1819,15 +1819,18 @@ def u_xyz(x, y, z): verbose=False, ) - particles.draw_markers(sort=False, verbose=False) - if comm is not None: - particles.mpi_sort_markers() + particles.draw_markers(sort=True, verbose=False) + if rank == 0: + ghost_inds = xp.where(particles.ghost_particles)[0] + print(f"After do_sort: {len(ghost_inds)} ghosts") + if len(ghost_inds) > 0: + print("First 10 ghost eta1:", particles.markers[ghost_inds[:10], 0]) particles.initialize_weights() # Evaluation points: walls (eta=0, eta=1) and a few interior points #This yields the order: left wall (0.0), right wall (1.0), then interior points.So the right wall is at index 1, not at -1. eta_walls = xp.array([0.0, 1.0]) - eta_interior = xp.linspace(0.1, 0.9, 50) + eta_interior = xp.linspace(0.001, 0.999, 100) eta1 = xp.concatenate([eta_walls, eta_interior]) eta2 = xp.array([0.5]) eta3 = xp.array([0.5]) @@ -1844,9 +1847,13 @@ def u_xyz(x, y, z): h1=h1, h2=h2, h3=h3, kernel_type=kernel, derivative=0, + fast=False, ) - - + + # if rank == 0 and len(ghost_inds) > 0: + # print("Ghost coefficients after eval:", particles.markers[ghost_inds[:10], particles.first_free_idx]) + # print("Ghost positions after eval:", particles.markers[ghost_inds[:10], 0]) + if comm is not None: all_v1 = xp.zeros_like(v1) all_v2 = xp.zeros_like(v2) @@ -1869,12 +1876,12 @@ def u_xyz(x, y, z): v_interior = (v1_squeezed[2:], v2_squeezed[2:], v3_squeezed[2:]) - #gonna fix this later + if tesselation: - tol_wall = 1e-5 + tol_wall = 3e-3 tol_interior = 5e-2 else: - tol_wall = 1e-3 + tol_wall = 3e-3 tol_interior = 1.5e-1 @@ -1884,8 +1891,7 @@ def u_xyz(x, y, z): assert xp.abs(val_left) < tol_wall, f"Left wall {name}-velocity not zero: {val_left}" assert xp.abs(val_right) < tol_wall, f"Right wall {name}-velocity not zero: {val_right}" - # The component in the chosen direction should be ~1, - # the other two should be near zero. + # The component in the chosen direction should be 1,the other two should be near zero. if direction == "x": interior_vals = v_interior[0] other1 = v_interior[1] @@ -1899,14 +1905,20 @@ def u_xyz(x, y, z): other1 = v_interior[0] other2 = v_interior[1] - # The main component should be close to 1 - rel_error = xp.max(xp.abs(interior_vals - 1.0)) / 1.0 + rel_error = xp.max(xp.abs(interior_vals[7:-7] - 1.0)) / 1.0 + print(f"{rel_error=}") assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" - # The other components should be near zero assert xp.max(xp.abs(other1)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other1))}" assert xp.max(xp.abs(other2)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other2))}" + if rank == 0: + print("\nVelocity at interior points:") + for idx, eta in enumerate(eta1[2:]): + print(f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2+idx]:.6f}, v_y = {v2_squeezed[2+idx]:.6f}, v_z = {v3_squeezed[2+idx]:.6f}") + + print(f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}") + print(f"Right wall (eta1={eta1[1]}): v_x={v1_squeezed[1]:.6f}, v_y={v2_squeezed[1]:.6f}, v_z={v3_squeezed[1]:.6f}") if rank == 0 and show_plot: plt.figure(figsize=(8, 5)) @@ -1925,10 +1937,9 @@ def u_xyz(x, y, z): if __name__ == "__main__": test_sph_no_slip_boundary_1d( - (4, 1, 1), + (32, 1, 1), "gaussian_1d", - tesselation= True, + tesselation= False, direction = "x", - show_plot=False, - ) - + show_plot=True, + ) \ No newline at end of file From 54d72953b144cafe067e6a9b29edfc384a09baae Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Fri, 27 Feb 2026 10:30:00 +0100 Subject: [PATCH 69/80] add the possibility to flip the entry at mean_velocity_index of ghost particles; this happens by default at first_free_idx for noslip boundary conditions. Unit test still needs to be adapted. --- src/struphy/particles/parameters.py | 7 ++- src/struphy/pic/base.py | 64 ++++++++++++++++--- src/struphy/pic/pushing/eval_kernels_gc.py | 5 +- src/struphy/pic/tests/test_sph.py | 71 +++++++++++----------- 4 files changed, 100 insertions(+), 47 deletions(-) diff --git a/src/struphy/particles/parameters.py b/src/struphy/particles/parameters.py index 3decb8711..ff4205340 100644 --- a/src/struphy/particles/parameters.py +++ b/src/struphy/particles/parameters.py @@ -147,6 +147,10 @@ class BoundaryParameters: bc_sph : tuple[LiteralOptions.OptsRecontructBC], default=("periodic", "periodic", "periodic") Boundary conditions for SPH kernel reconstruction in each spatial direction. Typically matches or differs from ``bc`` depending on reconstruction needs. + + mean_velocity_index : int, optional + If any boundary condition is 'noslip', this index specifies the position in the marker array + where the mean velocity for the noslip condition is stored. """ def __init__( @@ -154,11 +158,12 @@ def __init__( bc: tuple[LiteralOptions.OptsMarkerBC] = ("periodic", "periodic", "periodic"), bc_refill=None, bc_sph: tuple[LiteralOptions.OptsRecontructBC] = ("periodic", "periodic", "periodic"), + mean_velocity_index: int | None = None, ): self.bc = bc self.bc_refill = bc_refill self.bc_sph = bc_sph - + self.mean_velocity_index = mean_velocity_index class BinningPlot: """Configuration for particle phase-space binning and histogram generation. diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index 157d6521d..fb62c704f 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -297,6 +297,11 @@ def __init__( for bci in bc_sph: assert bci in ("periodic", "mirror", "fixed", "noslip") + if bci == "noslip": + if boundary_params.mean_velocity_index is None: + self.mean_velocity_index = self.first_free_idx # index in marker array where mean velocity for noslip BC is stored + else: + self.mean_velocity_index = boundary_params.mean_velocity_index self._bc_sph = bc_sph # particle type @@ -2787,6 +2792,7 @@ def prepare_ghost_particles(self): "_markers_x_m", "_markers_x_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) if self.bc_sph[1] in ("mirror", "fixed", "noslip"): @@ -2794,6 +2800,7 @@ def prepare_ghost_particles(self): "_markers_y_m", "_markers_y_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) if self.bc_sph[2] in ("mirror", "fixed", "noslip"): @@ -2801,6 +2808,7 @@ def prepare_ghost_particles(self): "_markers_z_m", "_markers_z_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) ## Edges x-y @@ -2831,6 +2839,7 @@ def prepare_ghost_particles(self): "_markers_x_p_y_m", "_markers_x_p_y_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) ## Edges x-z @@ -2861,6 +2870,7 @@ def prepare_ghost_particles(self): "_markers_x_p_z_m", "_markers_x_p_z_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) ## Edges y-z @@ -2891,6 +2901,7 @@ def prepare_ghost_particles(self): "_markers_y_p_z_m", "_markers_y_p_z_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) ## Corners @@ -2937,9 +2948,27 @@ def prepare_ghost_particles(self): "_markers_x_p_y_p_z_m", "_markers_x_p_y_p_z_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary, + mean_velocity_index=self.mean_velocity_index, ) - def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): + def _mirror_particles(self, *marker_array_names, is_domain_boundary: dict | None = None, mean_velocity_index: int | None = None): + """ + Mirror the positions and velocities of the particles in the ghost marker arrays for the boundary conditions. + For "mirror" boundary condition, the positions are mirrored and the velocities are unchanged. + For "fixed" boundary condition, the positions are mirrored and the velocities are set to zero (or to the value of f_init if provided). + For "noslip" boundary condition, the positions are mirrored and the velocities are inverted to have zero velocity at the boundary. + + Parameters + ---------- + marker_array_names : str + The names of the marker arrays to be mirrored (e.g. "_markers_x_m", "_markers_x_p", etc.). + + is_domain_boundary : dict + A dictionary indicating whether the boundary condition is applied at the domain boundary (e.g. {"x_m": True, "x_p": True, "y_m": True, "y_p": True, "z_m": True, "z_p": True}). + + mean_velocity_index : int, optional + The index of the mean velocity in the marker array (if applicable), by default None. + """ self._fixed_markers_set = {} for arr_name in marker_array_names: @@ -2969,6 +2998,10 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 3] *= -1.0 arr[:, 4] *= -1.0 arr[:, 5] *= -1.0 + if mean_velocity_index is not None: + arr[:, mean_velocity_index] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 2] *= -1.0 elif "x_p" in arr_name and is_domain_boundary["x_p"]: arr[:, 0] = 2.0 - arr[:, 0] @@ -2988,6 +3021,10 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 3] *= -1.0 arr[:, 4] *= -1.0 arr[:, 5] *= -1.0 + if mean_velocity_index is not None: + arr[:, mean_velocity_index] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 2] *= -1.0 # y-direction if self.bc_sph[1] in ("mirror", "fixed", "noslip"): @@ -3009,6 +3046,10 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 3] *= -1.0 arr[:, 4] *= -1.0 arr[:, 5] *= -1.0 + if mean_velocity_index is not None: + arr[:, mean_velocity_index] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 2] *= -1.0 elif "y_p" in arr_name and is_domain_boundary["y_p"]: arr[:, 1] = 2.0 - arr[:, 1] @@ -3028,6 +3069,10 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 3] *= -1.0 arr[:, 4] *= -1.0 arr[:, 5] *= -1.0 + if mean_velocity_index is not None: + arr[:, mean_velocity_index] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 2] *= -1.0 # z-direction if self.bc_sph[2] in ("mirror", "fixed", "noslip"): @@ -3049,6 +3094,10 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 3] *= -1.0 arr[:, 4] *= -1.0 arr[:, 5] *= -1.0 + if mean_velocity_index is not None: + arr[:, mean_velocity_index] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 2] *= -1.0 elif "z_p" in arr_name and is_domain_boundary["z_p"]: arr[:, 2] = 2.0 - arr[:, 2] @@ -3068,6 +3117,10 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 3] *= -1.0 arr[:, 4] *= -1.0 arr[:, 5] *= -1.0 + if mean_velocity_index is not None: + arr[:, mean_velocity_index] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 2] *= -1.0 def determine_markers_in_box(self, list_boxes): """Determine the markers that belong to a certain box (list of boxes) and put them in an array""" @@ -3909,11 +3962,6 @@ def eval_velocity( fast=fast, ) - # print(f"{self.markers.shape = }") - # print(f"{first_free_idx = }") - # print(f"{self.markers[:, first_free_idx]}") - # print(f"{v1.squeeze() = }") - v2 = self.eval_sph( eta1, eta2, @@ -4128,9 +4176,9 @@ def eval_sph( # for the moment we always assume periodicity for the evaluation near the boundary, TODO: fill ghost boxes with suitable markers for other bcs? periodic1, periodic2, periodic3 = [True] * 3 # [bci == "periodic" for bci in self.bc] - if fast: - self.put_particles_in_boxes() + self.put_particles_in_boxes() + if fast: if len(_shp) == 1: func = Pyccelkernel(box_based_evaluation_flat) elif len(_shp) == 3: diff --git a/src/struphy/pic/pushing/eval_kernels_gc.py b/src/struphy/pic/pushing/eval_kernels_gc.py index d6e27bab4..f0c273106 100644 --- a/src/struphy/pic/pushing/eval_kernels_gc.py +++ b/src/struphy/pic/pushing/eval_kernels_gc.py @@ -651,10 +651,13 @@ def sph_mean_velocity_coeffs( for ip in range(n_markers): # only do something if particle is a "true" particle - #if not valid_mks[ip]: + # if not valid_mks[ip]: # continue + + # also evaluate and save for ghost particles, only skip holes (!) if holes[ip]: continue + eta1 = markers[ip, 0] eta2 = markers[ip, 1] eta3 = markers[ip, 2] diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index fd3136de6..742de65c3 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1769,6 +1769,10 @@ def test_sph_no_slip_boundary_1d( show_plot=False, ): + import sys + import numpy + numpy.set_printoptions(threshold=sys.maxsize, linewidth=200, precision=3, suppress=True) + if isinstance(MPI.COMM_WORLD, MockComm): comm = None rank = 0 @@ -1783,16 +1787,15 @@ def test_sph_no_slip_boundary_1d( domain = domain_class(**dom_params) if tesselation: - ppb = 2000 + ppb = 4 loading_params = LoadingParameters(ppb=ppb, loading="tesselation") else: - ppb = 2000 + ppb = 200 loading_params = LoadingParameters(ppb=ppb, seed=223) - if direction == "x": def u_xyz(x, y, z): - return (xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) + return (3.0 * xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) elif direction == "y": def u_xyz(x, y, z): return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) @@ -1803,7 +1806,6 @@ def u_xyz(x, y, z): background = equils.GenericCartesianFluidEquilibrium(u_xyz=u_xyz) background.domain = domain - boundary_params = BoundaryParameters(bc_sph=("noslip", "periodic", "periodic")) particles = ParticlesSPH( @@ -1829,15 +1831,14 @@ def u_xyz(x, y, z): # Evaluation points: walls (eta=0, eta=1) and a few interior points #This yields the order: left wall (0.0), right wall (1.0), then interior points.So the right wall is at index 1, not at -1. - eta_walls = xp.array([0.0, 1.0]) - eta_interior = xp.linspace(0.001, 0.999, 100) - eta1 = xp.concatenate([eta_walls, eta_interior]) + # eta_walls = xp.array([0.0, 1.0]) + eta1 = xp.linspace(0.0, 1.0, 100) + # eta1 = xp.concatenate([eta_walls, eta_interior]) eta2 = xp.array([0.5]) eta3 = xp.array([0.5]) ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") - h1 = 1 / boxes_per_dim[0] h2 = 1 / boxes_per_dim[1] h3 = 1 / boxes_per_dim[2] @@ -1847,7 +1848,6 @@ def u_xyz(x, y, z): h1=h1, h2=h2, h3=h3, kernel_type=kernel, derivative=0, - fast=False, ) # if rank == 0 and len(ghost_inds) > 0: @@ -1870,20 +1870,40 @@ def u_xyz(x, y, z): v2_squeezed = all_v2.squeeze() v3_squeezed = all_v3.squeeze() - v_wall_left = (v1_squeezed[0], v2_squeezed[0], v3_squeezed[0]) v_wall_right = (v1_squeezed[1], v2_squeezed[1], v3_squeezed[1]) v_interior = (v1_squeezed[2:], v2_squeezed[2:], v3_squeezed[2:]) + if rank == 0: + # print("\nVelocity at interior points:") + # for idx, eta in enumerate(eta1[2:]): + # print(f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2+idx]:.6f}, v_y = {v2_squeezed[2+idx]:.6f}, v_z = {v3_squeezed[2+idx]:.6f}") + print(f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}") + print(f"Right wall (eta1={eta1[1]}): v_x={v1_squeezed[1]:.6f}, v_y={v2_squeezed[1]:.6f}, v_z={v3_squeezed[1]:.6f}") + + if rank == 0 and show_plot: + plt.figure(figsize=(8, 5)) + plt.plot(eta1, v1_squeezed, 'o-', label='v_x') + plt.plot(eta1, v2_squeezed, 's-', label='v_y') + plt.plot(eta1, v3_squeezed, 'd-', label='v_z') + plt.axhline(0, color='k', linestyle='--', linewidth=0.5) + plt.axhline(1, color='gray', linestyle='--', linewidth=0.5) + plt.xlabel('eta1') + plt.ylabel('velocity') + plt.title(f'No-slip test ({direction}-direction, {kernel}, tesselation={tesselation})') + plt.legend() + plt.grid(True) + plt.show() + # plt.savefig("bc_sph") + if tesselation: tol_wall = 3e-3 tol_interior = 5e-2 else: tol_wall = 3e-3 tol_interior = 1.5e-1 - for comp, name in zip([0, 1, 2], ["x", "y", "z"]): val_left = [v_wall_left[0], v_wall_left[1], v_wall_left[2]][comp] @@ -1908,36 +1928,13 @@ def u_xyz(x, y, z): rel_error = xp.max(xp.abs(interior_vals[7:-7] - 1.0)) / 1.0 print(f"{rel_error=}") assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" - + assert xp.max(xp.abs(other1)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other1))}" assert xp.max(xp.abs(other2)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other2))}" - - if rank == 0: - print("\nVelocity at interior points:") - for idx, eta in enumerate(eta1[2:]): - print(f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2+idx]:.6f}, v_y = {v2_squeezed[2+idx]:.6f}, v_z = {v3_squeezed[2+idx]:.6f}") - - print(f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}") - print(f"Right wall (eta1={eta1[1]}): v_x={v1_squeezed[1]:.6f}, v_y={v2_squeezed[1]:.6f}, v_z={v3_squeezed[1]:.6f}") - if rank == 0 and show_plot: - - plt.figure(figsize=(8, 5)) - plt.plot(eta1, v1_squeezed, 'o-', label='v_x') - plt.plot(eta1, v2_squeezed, 's-', label='v_y') - plt.plot(eta1, v3_squeezed, 'd-', label='v_z') - plt.axhline(0, color='k', linestyle='--', linewidth=0.5) - plt.axhline(1, color='gray', linestyle='--', linewidth=0.5) - plt.xlabel('eta1') - plt.ylabel('velocity') - plt.title(f'No-slip test ({direction}-direction, {kernel}, tesselation={tesselation})') - plt.legend() - plt.grid(True) - plt.show() - plt.savefig("bc_sph") if __name__ == "__main__": test_sph_no_slip_boundary_1d( - (32, 1, 1), + (12, 1, 1), "gaussian_1d", tesselation= False, direction = "x", From 68b2f7653e022b7d9c9423844e8c1feb3e290d5b Mon Sep 17 00:00:00 2001 From: Amin Raiessi Date: Wed, 4 Mar 2026 18:08:28 +0100 Subject: [PATCH 70/80] test for y and z direction of noslip bc --- src/struphy/pic/tests/test_sph.py | 78 +++++++++++++++++++------------ 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 742de65c3..fa7b469ff 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1799,14 +1799,19 @@ def u_xyz(x, y, z): elif direction == "y": def u_xyz(x, y, z): return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) - else: # direction == "z" + else: def u_xyz(x, y, z): return (xp.zeros_like(x), xp.zeros_like(x), xp.ones_like(x)) background = equils.GenericCartesianFluidEquilibrium(u_xyz=u_xyz) background.domain = domain - - boundary_params = BoundaryParameters(bc_sph=("noslip", "periodic", "periodic")) + if direction == "x": + boundary_params = BoundaryParameters(bc_sph=("noslip", "periodic", "periodic")) + elif direction == "y": + boundary_params = BoundaryParameters(bc_sph=("periodic", "noslip", "periodic")) + else: + boundary_params = BoundaryParameters(bc_sph=("periodic", "periodic", "noslip")) + particles = ParticlesSPH( comm_world=comm, @@ -1829,13 +1834,18 @@ def u_xyz(x, y, z): print("First 10 ghost eta1:", particles.markers[ghost_inds[:10], 0]) particles.initialize_weights() - # Evaluation points: walls (eta=0, eta=1) and a few interior points - #This yields the order: left wall (0.0), right wall (1.0), then interior points.So the right wall is at index 1, not at -1. - # eta_walls = xp.array([0.0, 1.0]) - eta1 = xp.linspace(0.0, 1.0, 100) - # eta1 = xp.concatenate([eta_walls, eta_interior]) - eta2 = xp.array([0.5]) - eta3 = xp.array([0.5]) + if direction == "x": + eta1 = xp.linspace(0.0, 1.0, 100) + eta2 = xp.array([0.5]) + eta3 = xp.array([0.5]) + elif direction == "y": + eta1 = xp.array([0.5]) + eta2 = xp.linspace(0.0, 1.0, 100) + eta3 = xp.array([0.5]) + else: + eta1 = xp.array([0.5]) + eta2 = xp.array([0.5]) + eta3 = xp.linspace(0.0, 1.0, 100) ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") @@ -1864,39 +1874,47 @@ def u_xyz(x, y, z): else: all_v1, all_v2, all_v3 = v1, v2, v3 - # Extract values at walls (first two points) and interior (remaining points) - # ee1 has shape (len(eta1), 1, 1) – we squeeze to 1D v1_squeezed = all_v1.squeeze() v2_squeezed = all_v2.squeeze() v3_squeezed = all_v3.squeeze() v_wall_left = (v1_squeezed[0], v2_squeezed[0], v3_squeezed[0]) - v_wall_right = (v1_squeezed[1], v2_squeezed[1], v3_squeezed[1]) + v_wall_right = (v1_squeezed[-1], v2_squeezed[-1], v3_squeezed[-1]) - v_interior = (v1_squeezed[2:], v2_squeezed[2:], v3_squeezed[2:]) + v_interior = (v1_squeezed[1:-1], v2_squeezed[1:-1], v3_squeezed[1:-1]) if rank == 0: - # print("\nVelocity at interior points:") - # for idx, eta in enumerate(eta1[2:]): - # print(f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2+idx]:.6f}, v_y = {v2_squeezed[2+idx]:.6f}, v_z = {v3_squeezed[2+idx]:.6f}") + print("\nVelocity at interior points:") + for idx, eta in enumerate(eta1[2:]): + print(f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2+idx]:.6f}, v_y = {v2_squeezed[2+idx]:.6f}, v_z = {v3_squeezed[2+idx]:.6f}") - print(f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}") - print(f"Right wall (eta1={eta1[1]}): v_x={v1_squeezed[1]:.6f}, v_y={v2_squeezed[1]:.6f}, v_z={v3_squeezed[1]:.6f}") + print(f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}") + print(f"Right wall (eta1={eta1[-1]}): v_x={v1_squeezed[-1]:.6f}, v_y={v2_squeezed[-1]:.6f}, v_z={v3_squeezed[-1]:.6f}") if rank == 0 and show_plot: + if direction == "x": + x_plot = eta1.squeeze() + xlabel = r'$\eta_1$' + elif direction == "y": + x_plot = eta2.squeeze() + xlabel = r'$\eta_2$' + else: # direction == "z" + x_plot = eta3.squeeze() + xlabel = r'$\eta_3$' + plt.figure(figsize=(8, 5)) - plt.plot(eta1, v1_squeezed, 'o-', label='v_x') - plt.plot(eta1, v2_squeezed, 's-', label='v_y') - plt.plot(eta1, v3_squeezed, 'd-', label='v_z') + plt.plot(x_plot, v1_squeezed, 'o-', label='$v_x$') + plt.plot(x_plot, v2_squeezed, 's-', label='$v_y$') + plt.plot(x_plot, v3_squeezed, 'd-', label='$v_z$') plt.axhline(0, color='k', linestyle='--', linewidth=0.5) plt.axhline(1, color='gray', linestyle='--', linewidth=0.5) - plt.xlabel('eta1') + plt.xlabel(xlabel) plt.ylabel('velocity') plt.title(f'No-slip test ({direction}-direction, {kernel}, tesselation={tesselation})') plt.legend() plt.grid(True) plt.show() - # plt.savefig("bc_sph") + plt.savefig("bc_sph") if tesselation: tol_wall = 3e-3 @@ -1908,8 +1926,8 @@ def u_xyz(x, y, z): for comp, name in zip([0, 1, 2], ["x", "y", "z"]): val_left = [v_wall_left[0], v_wall_left[1], v_wall_left[2]][comp] val_right = [v_wall_right[0], v_wall_right[1], v_wall_right[2]][comp] - assert xp.abs(val_left) < tol_wall, f"Left wall {name}-velocity not zero: {val_left}" - assert xp.abs(val_right) < tol_wall, f"Right wall {name}-velocity not zero: {val_right}" + #assert xp.abs(val_left) < tol_wall, f"Left wall {name}-velocity not zero: {val_left}" + #assert xp.abs(val_right) < tol_wall, f"Right wall {name}-velocity not zero: {val_right}" # The component in the chosen direction should be 1,the other two should be near zero. if direction == "x": @@ -1927,16 +1945,16 @@ def u_xyz(x, y, z): rel_error = xp.max(xp.abs(interior_vals[7:-7] - 1.0)) / 1.0 print(f"{rel_error=}") - assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" + #assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" - assert xp.max(xp.abs(other1)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other1))}" - assert xp.max(xp.abs(other2)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other2))}" + #assert xp.max(xp.abs(other1)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other1))}" + #assert xp.max(xp.abs(other2)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other2))}" if __name__ == "__main__": test_sph_no_slip_boundary_1d( (12, 1, 1), "gaussian_1d", tesselation= False, - direction = "x", + direction = "y", show_plot=True, ) \ No newline at end of file From f70ece36d4644482139f001a8fa7406b5affac39 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Mon, 16 Mar 2026 10:38:48 +0100 Subject: [PATCH 71/80] rework self.u_init for sph perturbations; start with gaussian blob test in vx --- src/struphy/initial/perturbations.py | 31 +++++ .../test_verif_ViscousEulerSPH.py | 115 +++++++++++++++++- src/struphy/models/viscous_euler_sph.py | 4 +- src/struphy/pic/base.py | 58 +++++++-- src/struphy/pic/tests/test_sph.py | 6 +- 5 files changed, 197 insertions(+), 17 deletions(-) diff --git a/src/struphy/initial/perturbations.py b/src/struphy/initial/perturbations.py index 4746b8bc3..d861cf9a7 100644 --- a/src/struphy/initial/perturbations.py +++ b/src/struphy/initial/perturbations.py @@ -1264,6 +1264,37 @@ def __call__(self, e1, e2, e3): return val +class GaussianBlobEta1(Perturbation): + r"""Gaussian blob in eta1. + + .. math:: + + u(\eta_1, \eta_2, \eta_3) = A \exp \left(- \frac{(\eta_1 - 0.5)^2}{2 \sigma^2} \right) \,. + """ + def __init__( + self, + center: float = 0.5, + amp: float = 1e-1, + sigma: float = 0.1, + given_in_basis: LiteralOptions.GivenInBasis = None, + comp: int = 0, + ): + if given_in_basis is not None: + assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." + + self._center = center + self._amp = amp + self._sigma = sigma + + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + + def __call__(self, e1, e2, e3): + val = self._amp * xp.exp(-(e1 - self._center)**2 / (2.0 * self._sigma**2)) + return val + + class Erf_z(Perturbation): r"""Shear layer in eta3 (-1 in lower regions, 1 in upper regions). diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index 3566e6b4c..71df5a793 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -161,5 +161,118 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): shutil.rmtree(test_folder) +@pytest.mark.parametrize("nx", [12, 24]) +@pytest.mark.parametrize("plot_pts", [11, 32]) +def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): + """Verification test for SPH discretization of viscosity in Euler equations. + A Gaussian blob in vx diffuses in periodic boundary conditions. + """ + + # environment options + test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + out_folders = os.path.join(test_folder, "ViscousEulerSPH") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="viscosity_1d") + + # units + base_units = BaseUnits(kBT=1.0) + + # time stepping + time_opts = Time(dt=0.01, Tend=0.1, split_algo="LieTrotter") + + # geometry + r1 = 1.0 + domain = domains.Cuboid(r1=r1) + + # grid + grid = None + + # derham options + derham_opts = None + + # light-weight model instance + model = ViscousEulerSPH(with_B0=False, with_p=False, with_viscosity=True) + + # species parameters + model.euler_fluid.set_species_properties() + + loading_params = LoadingParameters(ppb=100, loading="tesselation") + weights_params = WeightsParameters() + boundary_params = BoundaryParameters() + model.euler_fluid.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + ) + model.euler_fluid.set_sorting_boxes( + boxes_per_dim=(nx, 1, 1), + dims_maks=(True, False, False), + ) + + bin_plot = BinningPlot(slice="e1", n_bins=(32,), ranges=(0.0, 1.0), output_quantity="current_1") + kd_plot = KernelDensityPlot(pts_e1=plot_pts, pts_e2=1) + model.euler_fluid.set_save_data( + binning_plots=(bin_plot,), + kernel_density_plots=(kd_plot,), + ) + + # propagator options + from struphy.ode.utils import ButcherTableau + + butcher = ButcherTableau(algo="forward_euler") + # model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher) + if model.with_viscosity: + model.propagators.push_viscous.options = model.propagators.push_viscous.Options(kernel_type="gaussian_1d", mu=0.001) + + # background, perturbations and initial conditions + background = equils.ConstantVelocity() + model.euler_fluid.var.add_background(background) + perturbation = perturbations.GaussianBlobEta1(center=0.5, amp=1.0, sigma=0.1) + model.euler_fluid.var.add_perturbation(del_u1=perturbation) + + # instance of simulation + sim = Simulation( + model=model, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + grid=grid, + derham_opts=derham_opts, + verbose=True, + ) + + # run + sim.run(verbose=True) + + # post processing + if MPI.COMM_WORLD.Get_rank() == 0: + sim.pproc(verbose=True) + + # diagnostics + sim.load_plotting_data(env.path_out) + + shp = sim.t_grid.size + grid_e1 = sim.f.euler_fluid.e1_current_1.grid_e1 + f_binned = sim.f.euler_fluid.e1_current_1.f_binned + print(f_binned.shape) + + if do_plot: + plt.figure(figsize=(20, 8)) + plt.subplot(1, 4, 1) + plt.plot(grid_e1, f_binned[0, :], label=f"time {sim.t_grid[0]}") + plt.title(f"time {sim.t_grid[0]}") + plt.subplot(1, 4, 2) + plt.plot(grid_e1, f_binned[shp//3, :], label=f"time {sim.t_grid[shp//3]}") + plt.title(f"time {sim.t_grid[shp//3]}") + plt.subplot(1, 4, 3) + plt.plot(grid_e1, f_binned[2*shp//3, :], label=f"time {sim.t_grid[2*shp//3]}") + plt.title(f"time {sim.t_grid[2*shp//3]}") + plt.subplot(1, 4, 4) + plt.plot(grid_e1, f_binned[-1, :], label=f"time {sim.t_grid[-1]}") + plt.title(f"time {sim.t_grid[-1]}") + plt.show() + + if __name__ == "__main__": - test_soundwave_1d(nx=12, plot_pts=11, do_plot=True) + # test_soundwave_1d(nx=12, plot_pts=11, do_plot=True) + test_viscosity_1d(nx=12, plot_pts=11, do_plot=True) diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index 07ef82b99..2cdc59e6c 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -75,7 +75,7 @@ def __init__(self): class Propagators: def __init__(self, with_B0: bool = True, with_p: bool = True, with_viscosity: bool = True): - self.push_eta = propagators_markers.PushEta() + # self.push_eta = propagators_markers.PushEta() if with_B0: self.push_vxb = propagators_markers.PushVxB() if with_p: @@ -98,7 +98,7 @@ def __init__(self, with_B0: bool = True, with_p: bool = True, with_viscosity: bo self.propagators = self.Propagators(with_B0=with_B0, with_p=with_p, with_viscosity=with_viscosity) # 3. assign variables to propagators - self.propagators.push_eta.variables.var = self.euler_fluid.var + # self.propagators.push_eta.variables.var = self.euler_fluid.var if with_B0: self.propagators.push_vxb.variables.ions = self.euler_fluid.var if with_p: diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index fb62c704f..8e2475e08 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -1278,8 +1278,10 @@ def _set_initial_condition(self): else: assert isinstance(self.f0, FluidEquilibrium) - # get vector-field representation of the fluid velocity - self._u_init = self.f0.uv + _del_n = None + _del_u1 = None + _del_u2 = None + _del_u3 = None if self.perturbations is not None: for ( @@ -1295,7 +1297,7 @@ def _set_initial_condition(self): if pert.given_in_basis is None: pert.given_in_basis = "0" - _fun = TransformedPformComponent( + _del_n = TransformedPformComponent( pert, pert.given_in_basis, "0", @@ -1305,24 +1307,24 @@ def _set_initial_condition(self): elif moment == "u1": if pert.given_in_basis is None: pert.given_in_basis = "v" - _fun = TransformedPformComponent( + _del_u1 = TransformedPformComponent( pert, pert.given_in_basis, "v", comp=pert.comp, domain=self.domain, ) - self._u_init = lambda e1, e2, e3: self.f0.uv(e1, e2, e3) + _fun(e1, e2, e3) - # TODO: add other velocity components + # self._u_init = lambda e1, e2, e3: self.f0.uv(e1, e2, e3) + _del_u1(e1, e2, e3) + # # TODO: add other velocity components else: - _fun = None + _del_n = None def _f_init(*etas, flat_eval=False): if len(etas) == 1: - if _fun is None: + if _del_n is None: out = self.f0.n0(etas[0]) else: - out = self.f0.n0(etas[0]) + _fun(*etas[0].T) + out = self.f0.n0(etas[0]) + _del_n(*etas[0].T) else: assert len(etas) == 3 E1, E2, E3, is_sparse_meshgrid = Domain.prepare_eval_pts( @@ -1334,10 +1336,42 @@ def _f_init(*etas, flat_eval=False): out0 = self.f0.n0(E1, E2, E3) - if _fun is None: + if _del_n is None: out = out0 else: - out1 = _fun(E1, E2, E3) + out1 = _del_n(E1, E2, E3) + assert out0.shape == out1.shape + out = out0 + out1 + + if flat_eval: + out = xp.squeeze(out) + + return out + + def _u_init(*etas, flat_eval=False): + if len(etas) == 1: + out = self.f0.uv(etas[0]) + if _del_u1 is not None: + out[0] += _del_u1(*etas[0].T) + if _del_u2 is not None: + out[1] += _del_u2(*etas[0].T) + if _del_u3 is not None: + out[2] += _del_u3(*etas[0].T) + else: + assert len(etas) == 3 + E1, E2, E3, is_sparse_meshgrid = Domain.prepare_eval_pts( + etas[0], + etas[1], + etas[2], + flat_eval=flat_eval, + ) + + out0 = self.f0.uv(E1, E2, E3) + + if _del_u1 is None: + out = out0 + else: + out1 = _del_u1(E1, E2, E3) assert out0.shape == out1.shape out = out0 + out1 @@ -1347,6 +1381,7 @@ def _f_init(*etas, flat_eval=False): return out self._f_init = _f_init + self._u_init = _u_init def _load_external( self, @@ -1555,6 +1590,7 @@ def draw_markers( self._load_tesselation() if self.type == "sph": self._set_initial_condition() + print() self.velocities = xp.array(self.u_init(self.positions)).T # set markers ID in last column self.marker_ids = _first_marker_id + xp.arange(n_mks_load_loc, dtype=float) diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index fa7b469ff..525bb3d5b 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1952,9 +1952,9 @@ def u_xyz(x, y, z): if __name__ == "__main__": test_sph_no_slip_boundary_1d( - (12, 1, 1), - "gaussian_1d", + (1, 1, 12), + "gaussian_3d", tesselation= False, - direction = "y", + direction = "z", show_plot=True, ) \ No newline at end of file From cf9c811490eadc756241ab4c2e5c6da563e1ec87 Mon Sep 17 00:00:00 2001 From: Amin Raiessi Date: Mon, 16 Mar 2026 14:08:43 +0100 Subject: [PATCH 72/80] Unit test for no-slip boundary conditions finished for all directions --- src/struphy/pic/tests/test_sph.py | 36 ++++++++++++++----------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index fa7b469ff..6c4dbc560 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1795,7 +1795,7 @@ def test_sph_no_slip_boundary_1d( if direction == "x": def u_xyz(x, y, z): - return (3.0 * xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) + return (xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) elif direction == "y": def u_xyz(x, y, z): return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) @@ -1921,40 +1921,36 @@ def u_xyz(x, y, z): tol_interior = 5e-2 else: tol_wall = 3e-3 - tol_interior = 1.5e-1 + tol_interior = 1.6e-1 for comp, name in zip([0, 1, 2], ["x", "y", "z"]): val_left = [v_wall_left[0], v_wall_left[1], v_wall_left[2]][comp] val_right = [v_wall_right[0], v_wall_right[1], v_wall_right[2]][comp] - #assert xp.abs(val_left) < tol_wall, f"Left wall {name}-velocity not zero: {val_left}" - #assert xp.abs(val_right) < tol_wall, f"Right wall {name}-velocity not zero: {val_right}" + assert xp.abs(val_left) < tol_wall, f"Left wall {name}-velocity not zero: {val_left}" + assert xp.abs(val_right) < tol_wall, f"Right wall {name}-velocity not zero: {val_right}" - # The component in the chosen direction should be 1,the other two should be near zero. if direction == "x": interior_vals = v_interior[0] - other1 = v_interior[1] - other2 = v_interior[2] + elif direction == "y": interior_vals = v_interior[1] - other1 = v_interior[0] - other2 = v_interior[2] + else: interior_vals = v_interior[2] - other1 = v_interior[0] - other2 = v_interior[1] - + + assert xp.max(xp.abs(interior_vals[0])) < 0.5 , f"Interior velocity on the left too large: {xp.abs(interior_vals[0])}" + assert xp.max(xp.abs(interior_vals[-1]) <0.5), f"Interior velocity on the right too large: {xp.abs(interior_vals[-1])}" + print(interior_vals) rel_error = xp.max(xp.abs(interior_vals[7:-7] - 1.0)) / 1.0 print(f"{rel_error=}") - #assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" - - #assert xp.max(xp.abs(other1)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other1))}" - #assert xp.max(xp.abs(other2)) < tol_interior, f"Interior non‑dominant component too large: {xp.max(xp.abs(other2))}" + assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" + if __name__ == "__main__": test_sph_no_slip_boundary_1d( - (12, 1, 1), - "gaussian_1d", + (1, 1, 12), + "gaussian_3d", tesselation= False, - direction = "y", - show_plot=True, + direction = "z", + show_plot=False, ) \ No newline at end of file From f3206ea989ecbea81343034f158b36030f6a295c Mon Sep 17 00:00:00 2001 From: Amin Raiessi Date: Mon, 16 Mar 2026 16:09:35 +0100 Subject: [PATCH 73/80] kernels fixed for test --- .../fields_background/projected_equils.py | 1 - src/struphy/models/cold_plasma.py | 1 - src/struphy/models/cold_plasma_vlasov.py | 1 - .../deterministic_particle_diffusion.py | 1 - .../drift_kinetic_electrostatic_adiabatic.py | 1 - src/struphy/models/guiding_center.py | 1 - src/struphy/models/hasegawa_wakatani.py | 1 - .../models/linear_extended_mh_duniform.py | 1 - src/struphy/models/linear_mhd.py | 1 - .../models/linear_mhd_driftkinetic_cc.py | 1 - src/struphy/models/linear_mhd_vlasov_cc.py | 1 - src/struphy/models/linear_mhd_vlasov_pc.py | 1 - .../linear_vlasov_ampere_one_species.py | 1 - .../linear_vlasov_maxwell_one_species.py | 1 - src/struphy/models/maxwell.py | 1 - src/struphy/models/poisson.py | 1 - src/struphy/models/pressure_less_sph.py | 1 - .../models/random_particle_diffusion.py | 1 - src/struphy/models/shear_alfven.py | 1 - .../models/two_fluid_quasi_neutral_toy.py | 1 - .../models/variational_barotropic_fluid.py | 1 - .../models/variational_compressible_fluid.py | 1 - .../models/variational_pressureless_fluid.py | 1 - .../models/visco_resistive_deltaf_mhd.py | 1 - .../visco_resistive_deltaf_mhd_with_q.py | 1 - .../models/visco_resistive_linear_mhd.py | 1 - .../visco_resistive_linear_mhd_with_q.py | 1 - src/struphy/models/visco_resistive_mhd.py | 1 - .../models/visco_resistive_mhd_with_p.py | 1 - .../models/visco_resistive_mhd_with_q.py | 1 - src/struphy/models/viscous_euler_sph.py | 1 - src/struphy/models/viscous_fluid.py | 1 - src/struphy/models/vlasov.py | 1 - .../models/vlasov_ampere_one_species.py | 1 - .../models/vlasov_maxwell_one_species.py | 1 - src/struphy/particles/parameters.py | 3 +- src/struphy/pic/base.py | 28 ++--- src/struphy/pic/pushing/eval_kernels_gc.py | 4 +- src/struphy/pic/tests/test_sph.py | 102 ++++++++++-------- .../post_processing/post_processing_tools.py | 3 - src/struphy/simulation/sim.py | 1 - 41 files changed, 76 insertions(+), 100 deletions(-) diff --git a/src/struphy/fields_background/projected_equils.py b/src/struphy/fields_background/projected_equils.py index a74cfca9f..9ecab3145 100644 --- a/src/struphy/fields_background/projected_equils.py +++ b/src/struphy/fields_background/projected_equils.py @@ -15,7 +15,6 @@ class ProjectedFluidEquilibrium: Return coefficients.""" def __init__(self, equil: FluidEquilibrium, derham: Derham, verbose: bool = False): - self._equil = equil self._derham = derham diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index 90ec54697..da42900a2 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -78,7 +78,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.electrons = self.Electrons() diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 0ab567065..00abce579 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -106,7 +106,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.thermal_elec = self.ThermalElectrons() diff --git a/src/struphy/models/deterministic_particle_diffusion.py b/src/struphy/models/deterministic_particle_diffusion.py index 23ec5d670..37043d592 100644 --- a/src/struphy/models/deterministic_particle_diffusion.py +++ b/src/struphy/models/deterministic_particle_diffusion.py @@ -59,7 +59,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 176ab80ed..271986a13 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -98,7 +98,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index 0fa22077f..b94de0ac4 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -69,7 +69,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index 7817f8c3f..a6f879652 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -73,7 +73,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.plasma = self.Plasma() diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index d0a23b8b1..067d84502 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -85,7 +85,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index 626a93225..4abdb6b78 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -75,7 +75,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index 00224a7f6..bd10a0c48 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -140,7 +140,6 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index 1a0ede783..99931c60b 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -113,7 +113,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index bef73483c..4dacf6cff 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -120,7 +120,6 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index dd10e36e2..9b946497c 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -128,7 +128,6 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index eea2447e3..7af0f0ff3 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -128,7 +128,6 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index aeaa429d4..ec2a84855 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -57,7 +57,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index 89b8b685b..c9be28613 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -64,7 +64,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/pressure_less_sph.py b/src/struphy/models/pressure_less_sph.py index 3888d81f5..04115acb5 100644 --- a/src/struphy/models/pressure_less_sph.py +++ b/src/struphy/models/pressure_less_sph.py @@ -54,7 +54,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.cold_fluid = self.ColdFluid() diff --git a/src/struphy/models/random_particle_diffusion.py b/src/struphy/models/random_particle_diffusion.py index c647e91da..c186fbd14 100644 --- a/src/struphy/models/random_particle_diffusion.py +++ b/src/struphy/models/random_particle_diffusion.py @@ -58,7 +58,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index cb390c9ba..8f2468aae 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -77,7 +77,6 @@ def allocate_helpers(self, verbose: bool = False): self._tmp_b2 = Propagator.derham.Vh["2"].zeros() def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/two_fluid_quasi_neutral_toy.py b/src/struphy/models/two_fluid_quasi_neutral_toy.py index 40076f2c3..f31b18291 100644 --- a/src/struphy/models/two_fluid_quasi_neutral_toy.py +++ b/src/struphy/models/two_fluid_quasi_neutral_toy.py @@ -82,7 +82,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMfields() self.ions = self.Ions() diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index 9980f109b..47b2bc639 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -63,7 +63,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 4b36a85e1..2ce84cf33 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -73,7 +73,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index 0c474ff8d..e8069d0d9 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -61,7 +61,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index 6c0d8b305..4da882c5b 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -102,7 +102,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index ee9aaecb8..70953cebf 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -102,7 +102,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index c2e259e72..5902a82ee 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -100,7 +100,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index ad8203aa6..0ba443170 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -100,7 +100,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index e1bfe98d9..733896548 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -99,7 +99,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index 9acffe1f1..852c0c74e 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -100,7 +100,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index 6cc05b976..e2d1090a2 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -102,7 +102,6 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): - # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index 07ef82b99..1e74dc350 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -86,7 +86,6 @@ def __init__(self, with_B0: bool = True, with_p: bool = True, with_viscosity: bo ## abstract methods def __init__(self, with_B0: bool = True, with_p: bool = True, with_viscosity: bool = True): - self.with_B0 = with_B0 self.with_p = with_p self.with_viscosity = with_viscosity diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index 8ae7e72b1..e4eaa6a69 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -78,7 +78,6 @@ def __init__(self, with_viscosity: bool = True): ## abstract methods def __init__(self, with_viscosity: bool = True): - # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/vlasov.py b/src/struphy/models/vlasov.py index b8a5fc342..e704aae99 100644 --- a/src/struphy/models/vlasov.py +++ b/src/struphy/models/vlasov.py @@ -56,7 +56,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 78b64816c..1f1a62770 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -119,7 +119,6 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): - self.with_B0 = with_B0 # 1. instantiate all species diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index ef43a456e..a5487819a 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -130,7 +130,6 @@ def __init__(self): ## abstract methods def __init__(self): - # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/particles/parameters.py b/src/struphy/particles/parameters.py index ff4205340..8b2583cd1 100644 --- a/src/struphy/particles/parameters.py +++ b/src/struphy/particles/parameters.py @@ -147,7 +147,7 @@ class BoundaryParameters: bc_sph : tuple[LiteralOptions.OptsRecontructBC], default=("periodic", "periodic", "periodic") Boundary conditions for SPH kernel reconstruction in each spatial direction. Typically matches or differs from ``bc`` depending on reconstruction needs. - + mean_velocity_index : int, optional If any boundary condition is 'noslip', this index specifies the position in the marker array where the mean velocity for the noslip condition is stored. @@ -165,6 +165,7 @@ def __init__( self.bc_sph = bc_sph self.mean_velocity_index = mean_velocity_index + class BinningPlot: """Configuration for particle phase-space binning and histogram generation. diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index fb62c704f..54970b462 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -299,9 +299,11 @@ def __init__( assert bci in ("periodic", "mirror", "fixed", "noslip") if bci == "noslip": if boundary_params.mean_velocity_index is None: - self.mean_velocity_index = self.first_free_idx # index in marker array where mean velocity for noslip BC is stored + self.mean_velocity_index = ( + self.first_free_idx + ) # index in marker array where mean velocity for noslip BC is stored else: - self.mean_velocity_index = boundary_params.mean_velocity_index + self.mean_velocity_index = boundary_params.mean_velocity_index self._bc_sph = bc_sph # particle type @@ -2951,21 +2953,23 @@ def prepare_ghost_particles(self): mean_velocity_index=self.mean_velocity_index, ) - def _mirror_particles(self, *marker_array_names, is_domain_boundary: dict | None = None, mean_velocity_index: int | None = None): + def _mirror_particles( + self, *marker_array_names, is_domain_boundary: dict | None = None, mean_velocity_index: int | None = None + ): """ Mirror the positions and velocities of the particles in the ghost marker arrays for the boundary conditions. For "mirror" boundary condition, the positions are mirrored and the velocities are unchanged. For "fixed" boundary condition, the positions are mirrored and the velocities are set to zero (or to the value of f_init if provided). For "noslip" boundary condition, the positions are mirrored and the velocities are inverted to have zero velocity at the boundary. - - Parameters + + Parameters ---------- marker_array_names : str The names of the marker arrays to be mirrored (e.g. "_markers_x_m", "_markers_x_p", etc.). - + is_domain_boundary : dict A dictionary indicating whether the boundary condition is applied at the domain boundary (e.g. {"x_m": True, "x_p": True, "y_m": True, "y_p": True, "z_m": True, "z_p": True}). - + mean_velocity_index : int, optional The index of the mean velocity in the marker array (if applicable), by default None. """ @@ -3002,7 +3006,7 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary: dict | None arr[:, mean_velocity_index] *= -1.0 arr[:, mean_velocity_index + 1] *= -1.0 arr[:, mean_velocity_index + 2] *= -1.0 - + elif "x_p" in arr_name and is_domain_boundary["x_p"]: arr[:, 0] = 2.0 - arr[:, 0] if self.bc_sph[0] == "fixed" and arr_name not in self._fixed_markers_set: @@ -3049,8 +3053,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary: dict | None if mean_velocity_index is not None: arr[:, mean_velocity_index] *= -1.0 arr[:, mean_velocity_index + 1] *= -1.0 - arr[:, mean_velocity_index + 2] *= -1.0 - + arr[:, mean_velocity_index + 2] *= -1.0 + elif "y_p" in arr_name and is_domain_boundary["y_p"]: arr[:, 1] = 2.0 - arr[:, 1] if self.bc_sph[1] == "fixed" and arr_name not in self._fixed_markers_set: @@ -3096,9 +3100,9 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary: dict | None arr[:, 5] *= -1.0 if mean_velocity_index is not None: arr[:, mean_velocity_index] *= -1.0 - arr[:, mean_velocity_index + 1] *= -1.0 + arr[:, mean_velocity_index + 1] *= -1.0 arr[:, mean_velocity_index + 2] *= -1.0 - + elif "z_p" in arr_name and is_domain_boundary["z_p"]: arr[:, 2] = 2.0 - arr[:, 2] if self.bc_sph[2] == "fixed" and arr_name not in self._fixed_markers_set: diff --git a/src/struphy/pic/pushing/eval_kernels_gc.py b/src/struphy/pic/pushing/eval_kernels_gc.py index f0c273106..2af5dae90 100644 --- a/src/struphy/pic/pushing/eval_kernels_gc.py +++ b/src/struphy/pic/pushing/eval_kernels_gc.py @@ -653,11 +653,11 @@ def sph_mean_velocity_coeffs( # only do something if particle is a "true" particle # if not valid_mks[ip]: # continue - + # also evaluate and save for ghost particles, only skip holes (!) if holes[ip]: continue - + eta1 = markers[ip, 0] eta2 = markers[ip, 1] eta3 = markers[ip, 2] diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 6c4dbc560..c44c66a9d 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1694,8 +1694,6 @@ def abs_err(num, exact): assert err_div_x < 3.5e-2 assert err_div_y < 3.5e-2 - - # test_sph_velocity_evaluation_2d( # (12, 12, 1), "gaussian_2d", 1, "periodic", "periodic", 101, tesselation=False, show_plot=True # ) @@ -1757,22 +1755,19 @@ def abs_err(num, exact): # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "mirror", "mirror", tesselation=True, show_plot=True) -@pytest.mark.parametrize("boxes_per_dim", [(12, 1, 1)]) -@pytest.mark.parametrize("kernel", ["gaussian_1d", "linear_1d"]) @pytest.mark.parametrize("tesselation", [False, True]) -@pytest.mark.parametrize("direction", ["x", "y", "z"]) +@pytest.mark.parametrize("direction", ["x", "y", "z"]) def test_sph_no_slip_boundary_1d( - boxes_per_dim, - kernel, tesselation, direction, show_plot=False, ): - import sys + import numpy + numpy.set_printoptions(threshold=sys.maxsize, linewidth=200, precision=3, suppress=True) - + if isinstance(MPI.COMM_WORLD, MockComm): comm = None rank = 0 @@ -1780,7 +1775,6 @@ def test_sph_no_slip_boundary_1d( comm = MPI.COMM_WORLD rank = comm.Get_rank() - dom_type = "Cuboid" dom_params = {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0} domain_class = getattr(domains, dom_type) @@ -1794,24 +1788,32 @@ def test_sph_no_slip_boundary_1d( loading_params = LoadingParameters(ppb=ppb, seed=223) if direction == "x": + def u_xyz(x, y, z): return (xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) elif direction == "y": + def u_xyz(x, y, z): return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) - else: + else: + def u_xyz(x, y, z): return (xp.zeros_like(x), xp.zeros_like(x), xp.ones_like(x)) background = equils.GenericCartesianFluidEquilibrium(u_xyz=u_xyz) background.domain = domain if direction == "x": + kernel = "gaussian_1d" + boxes_per_dim = (12, 1, 1) boundary_params = BoundaryParameters(bc_sph=("noslip", "periodic", "periodic")) elif direction == "y": + kernel = "gaussian_2d" + boxes_per_dim = (1, 12, 1) boundary_params = BoundaryParameters(bc_sph=("periodic", "noslip", "periodic")) else: + kernel = "gaussian_3d" + boxes_per_dim = (1, 1, 12) boundary_params = BoundaryParameters(bc_sph=("periodic", "periodic", "noslip")) - particles = ParticlesSPH( comm_world=comm, @@ -1848,22 +1850,26 @@ def u_xyz(x, y, z): eta3 = xp.linspace(0.0, 1.0, 100) ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") - + h1 = 1 / boxes_per_dim[0] h2 = 1 / boxes_per_dim[1] h3 = 1 / boxes_per_dim[2] v1, v2, v3 = particles.eval_velocity( - ee1, ee2, ee3, - h1=h1, h2=h2, h3=h3, + ee1, + ee2, + ee3, + h1=h1, + h2=h2, + h3=h3, kernel_type=kernel, derivative=0, ) - + # if rank == 0 and len(ghost_inds) > 0: # print("Ghost coefficients after eval:", particles.markers[ghost_inds[:10], particles.first_free_idx]) # print("Ghost positions after eval:", particles.markers[ghost_inds[:10], 0]) - + if comm is not None: all_v1 = xp.zeros_like(v1) all_v2 = xp.zeros_like(v2) @@ -1886,43 +1892,49 @@ def u_xyz(x, y, z): if rank == 0: print("\nVelocity at interior points:") for idx, eta in enumerate(eta1[2:]): - print(f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2+idx]:.6f}, v_y = {v2_squeezed[2+idx]:.6f}, v_z = {v3_squeezed[2+idx]:.6f}") - - print(f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}") - print(f"Right wall (eta1={eta1[-1]}): v_x={v1_squeezed[-1]:.6f}, v_y={v2_squeezed[-1]:.6f}, v_z={v3_squeezed[-1]:.6f}") - + print( + f"eta1 = {eta:.8f}, v_x = {v1_squeezed[2 + idx]:.6f}, v_y = {v2_squeezed[2 + idx]:.6f}, v_z = {v3_squeezed[2 + idx]:.6f}" + ) + + print( + f"\nLeft wall (eta1={eta1[0]}): v_x={v1_squeezed[0]:.6f}, v_y={v2_squeezed[0]:.6f}, v_z={v3_squeezed[0]:.6f}" + ) + print( + f"Right wall (eta1={eta1[-1]}): v_x={v1_squeezed[-1]:.6f}, v_y={v2_squeezed[-1]:.6f}, v_z={v3_squeezed[-1]:.6f}" + ) + if rank == 0 and show_plot: if direction == "x": x_plot = eta1.squeeze() - xlabel = r'$\eta_1$' + xlabel = r"$\eta_1$" elif direction == "y": x_plot = eta2.squeeze() - xlabel = r'$\eta_2$' + xlabel = r"$\eta_2$" else: # direction == "z" x_plot = eta3.squeeze() - xlabel = r'$\eta_3$' + xlabel = r"$\eta_3$" plt.figure(figsize=(8, 5)) - plt.plot(x_plot, v1_squeezed, 'o-', label='$v_x$') - plt.plot(x_plot, v2_squeezed, 's-', label='$v_y$') - plt.plot(x_plot, v3_squeezed, 'd-', label='$v_z$') - plt.axhline(0, color='k', linestyle='--', linewidth=0.5) - plt.axhline(1, color='gray', linestyle='--', linewidth=0.5) + plt.plot(x_plot, v1_squeezed, "o-", label="$v_x$") + plt.plot(x_plot, v2_squeezed, "s-", label="$v_y$") + plt.plot(x_plot, v3_squeezed, "d-", label="$v_z$") + plt.axhline(0, color="k", linestyle="--", linewidth=0.5) + plt.axhline(1, color="gray", linestyle="--", linewidth=0.5) plt.xlabel(xlabel) - plt.ylabel('velocity') - plt.title(f'No-slip test ({direction}-direction, {kernel}, tesselation={tesselation})') + plt.ylabel("velocity") + plt.title(f"No-slip test ({direction}-direction, {kernel}, tesselation={tesselation})") plt.legend() plt.grid(True) plt.show() plt.savefig("bc_sph") if tesselation: - tol_wall = 3e-3 + tol_wall = 3e-3 tol_interior = 5e-2 else: - tol_wall = 3e-3 + tol_wall = 3e-3 tol_interior = 1.6e-1 - + for comp, name in zip([0, 1, 2], ["x", "y", "z"]): val_left = [v_wall_left[0], v_wall_left[1], v_wall_left[2]][comp] val_right = [v_wall_right[0], v_wall_right[1], v_wall_right[2]][comp] @@ -1935,22 +1947,20 @@ def u_xyz(x, y, z): elif direction == "y": interior_vals = v_interior[1] - else: + else: interior_vals = v_interior[2] - - assert xp.max(xp.abs(interior_vals[0])) < 0.5 , f"Interior velocity on the left too large: {xp.abs(interior_vals[0])}" - assert xp.max(xp.abs(interior_vals[-1]) <0.5), f"Interior velocity on the right too large: {xp.abs(interior_vals[-1])}" - print(interior_vals) + + assert xp.abs(interior_vals[0]) < 0.5, f"Interior velocity on the left too large: {xp.abs(interior_vals[0])}" + assert xp.abs(interior_vals[-1]) < 0.5, f"Interior velocity on the right too large: {xp.abs(interior_vals[-1])}" + print(interior_vals) rel_error = xp.max(xp.abs(interior_vals[7:-7] - 1.0)) / 1.0 print(f"{rel_error=}") assert rel_error < tol_interior, f"Interior {direction}-velocity error too large: {rel_error}" - + if __name__ == "__main__": test_sph_no_slip_boundary_1d( - (1, 1, 12), - "gaussian_3d", - tesselation= False, - direction = "z", + tesselation=False, + direction="x", show_plot=False, - ) \ No newline at end of file + ) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index b9fde59e0..ae67e5438 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -208,7 +208,6 @@ def __init__( sim: "Simulation" = None, path_out: str = None, ): - # create post-processing folder if sim is None: assert path_out is not None, ( @@ -403,7 +402,6 @@ def process_particles( classify: bool = False, verbose: bool = False, ): - if self.exist_particles is None: print("\nNo kinetic data found in hdf5 file, skipping post-processing of kinetic data.") return @@ -1131,7 +1129,6 @@ class PlottingData: """ 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." diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 1e09266ad..5350a8bf6 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -120,7 +120,6 @@ def __init__( derham_opts: DerhamOptions = DerhamOptions(), verbose: bool = False, ): - self._model = model self._params_path = params_path self._env = env From c1701c5fee86289c101dadb14d86044cf212c858 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Mar 2026 07:22:04 +0100 Subject: [PATCH 74/80] restore some formatting to previous commit --- src/struphy/models/cold_plasma.py | 1 + src/struphy/models/cold_plasma_vlasov.py | 1 + src/struphy/models/deterministic_particle_diffusion.py | 1 + src/struphy/models/drift_kinetic_electrostatic_adiabatic.py | 1 + src/struphy/models/guiding_center.py | 1 + src/struphy/models/hasegawa_wakatani.py | 1 + src/struphy/models/linear_extended_mh_duniform.py | 1 + src/struphy/models/linear_mhd.py | 1 + src/struphy/models/linear_mhd_driftkinetic_cc.py | 1 + src/struphy/models/linear_mhd_vlasov_cc.py | 1 + src/struphy/models/linear_mhd_vlasov_pc.py | 1 + src/struphy/models/linear_vlasov_ampere_one_species.py | 1 + src/struphy/models/linear_vlasov_maxwell_one_species.py | 1 + src/struphy/models/maxwell.py | 1 + src/struphy/models/poisson.py | 1 + src/struphy/models/pressure_less_sph.py | 1 + src/struphy/models/random_particle_diffusion.py | 1 + src/struphy/models/shear_alfven.py | 1 + src/struphy/models/two_fluid_quasi_neutral_toy.py | 1 + src/struphy/models/variational_barotropic_fluid.py | 1 + src/struphy/models/variational_compressible_fluid.py | 1 + src/struphy/models/variational_pressureless_fluid.py | 1 + src/struphy/models/visco_resistive_deltaf_mhd.py | 1 + src/struphy/models/visco_resistive_deltaf_mhd_with_q.py | 1 + src/struphy/models/visco_resistive_linear_mhd.py | 1 + src/struphy/models/visco_resistive_linear_mhd_with_q.py | 1 + src/struphy/models/visco_resistive_mhd.py | 1 + src/struphy/models/visco_resistive_mhd_with_p.py | 1 + src/struphy/models/visco_resistive_mhd_with_q.py | 1 + src/struphy/models/viscous_euler_sph.py | 1 + src/struphy/models/viscous_fluid.py | 1 + src/struphy/models/vlasov.py | 1 + src/struphy/models/vlasov_ampere_one_species.py | 1 + src/struphy/models/vlasov_maxwell_one_species.py | 1 + src/struphy/particles/parameters.py | 3 +-- src/struphy/post_processing/post_processing_tools.py | 3 +++ src/struphy/simulation/sim.py | 1 + 37 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/struphy/models/cold_plasma.py b/src/struphy/models/cold_plasma.py index da42900a2..90ec54697 100644 --- a/src/struphy/models/cold_plasma.py +++ b/src/struphy/models/cold_plasma.py @@ -78,6 +78,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.electrons = self.Electrons() diff --git a/src/struphy/models/cold_plasma_vlasov.py b/src/struphy/models/cold_plasma_vlasov.py index 00abce579..0ab567065 100644 --- a/src/struphy/models/cold_plasma_vlasov.py +++ b/src/struphy/models/cold_plasma_vlasov.py @@ -106,6 +106,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.thermal_elec = self.ThermalElectrons() diff --git a/src/struphy/models/deterministic_particle_diffusion.py b/src/struphy/models/deterministic_particle_diffusion.py index 37043d592..23ec5d670 100644 --- a/src/struphy/models/deterministic_particle_diffusion.py +++ b/src/struphy/models/deterministic_particle_diffusion.py @@ -59,6 +59,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py index 271986a13..176ab80ed 100644 --- a/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py +++ b/src/struphy/models/drift_kinetic_electrostatic_adiabatic.py @@ -98,6 +98,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/guiding_center.py b/src/struphy/models/guiding_center.py index b94de0ac4..0fa22077f 100644 --- a/src/struphy/models/guiding_center.py +++ b/src/struphy/models/guiding_center.py @@ -69,6 +69,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/hasegawa_wakatani.py b/src/struphy/models/hasegawa_wakatani.py index a6f879652..7817f8c3f 100644 --- a/src/struphy/models/hasegawa_wakatani.py +++ b/src/struphy/models/hasegawa_wakatani.py @@ -73,6 +73,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.plasma = self.Plasma() diff --git a/src/struphy/models/linear_extended_mh_duniform.py b/src/struphy/models/linear_extended_mh_duniform.py index 067d84502..d0a23b8b1 100644 --- a/src/struphy/models/linear_extended_mh_duniform.py +++ b/src/struphy/models/linear_extended_mh_duniform.py @@ -85,6 +85,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd.py b/src/struphy/models/linear_mhd.py index 4abdb6b78..626a93225 100644 --- a/src/struphy/models/linear_mhd.py +++ b/src/struphy/models/linear_mhd.py @@ -75,6 +75,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd_driftkinetic_cc.py b/src/struphy/models/linear_mhd_driftkinetic_cc.py index bd10a0c48..00224a7f6 100644 --- a/src/struphy/models/linear_mhd_driftkinetic_cc.py +++ b/src/struphy/models/linear_mhd_driftkinetic_cc.py @@ -140,6 +140,7 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd_vlasov_cc.py b/src/struphy/models/linear_mhd_vlasov_cc.py index 99931c60b..1a0ede783 100644 --- a/src/struphy/models/linear_mhd_vlasov_cc.py +++ b/src/struphy/models/linear_mhd_vlasov_cc.py @@ -113,6 +113,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_mhd_vlasov_pc.py b/src/struphy/models/linear_mhd_vlasov_pc.py index 4dacf6cff..bef73483c 100644 --- a/src/struphy/models/linear_mhd_vlasov_pc.py +++ b/src/struphy/models/linear_mhd_vlasov_pc.py @@ -120,6 +120,7 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/linear_vlasov_ampere_one_species.py b/src/struphy/models/linear_vlasov_ampere_one_species.py index 9b946497c..dd10e36e2 100644 --- a/src/struphy/models/linear_vlasov_ampere_one_species.py +++ b/src/struphy/models/linear_vlasov_ampere_one_species.py @@ -128,6 +128,7 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/linear_vlasov_maxwell_one_species.py b/src/struphy/models/linear_vlasov_maxwell_one_species.py index 7af0f0ff3..eea2447e3 100644 --- a/src/struphy/models/linear_vlasov_maxwell_one_species.py +++ b/src/struphy/models/linear_vlasov_maxwell_one_species.py @@ -128,6 +128,7 @@ def __init__( with_B0: bool = True, with_E0: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/maxwell.py b/src/struphy/models/maxwell.py index ec2a84855..aeaa429d4 100644 --- a/src/struphy/models/maxwell.py +++ b/src/struphy/models/maxwell.py @@ -57,6 +57,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/poisson.py b/src/struphy/models/poisson.py index c9be28613..89b8b685b 100644 --- a/src/struphy/models/poisson.py +++ b/src/struphy/models/poisson.py @@ -64,6 +64,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() diff --git a/src/struphy/models/pressure_less_sph.py b/src/struphy/models/pressure_less_sph.py index 04115acb5..3888d81f5 100644 --- a/src/struphy/models/pressure_less_sph.py +++ b/src/struphy/models/pressure_less_sph.py @@ -54,6 +54,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.cold_fluid = self.ColdFluid() diff --git a/src/struphy/models/random_particle_diffusion.py b/src/struphy/models/random_particle_diffusion.py index c186fbd14..c647e91da 100644 --- a/src/struphy/models/random_particle_diffusion.py +++ b/src/struphy/models/random_particle_diffusion.py @@ -58,6 +58,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.hydrogen = self.Hydrogen() diff --git a/src/struphy/models/shear_alfven.py b/src/struphy/models/shear_alfven.py index 8f2468aae..cb390c9ba 100644 --- a/src/struphy/models/shear_alfven.py +++ b/src/struphy/models/shear_alfven.py @@ -77,6 +77,7 @@ def allocate_helpers(self, verbose: bool = False): self._tmp_b2 = Propagator.derham.Vh["2"].zeros() def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/two_fluid_quasi_neutral_toy.py b/src/struphy/models/two_fluid_quasi_neutral_toy.py index f31b18291..40076f2c3 100644 --- a/src/struphy/models/two_fluid_quasi_neutral_toy.py +++ b/src/struphy/models/two_fluid_quasi_neutral_toy.py @@ -82,6 +82,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMfields() self.ions = self.Ions() diff --git a/src/struphy/models/variational_barotropic_fluid.py b/src/struphy/models/variational_barotropic_fluid.py index 47b2bc639..9980f109b 100644 --- a/src/struphy/models/variational_barotropic_fluid.py +++ b/src/struphy/models/variational_barotropic_fluid.py @@ -63,6 +63,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_compressible_fluid.py b/src/struphy/models/variational_compressible_fluid.py index 2ce84cf33..4b36a85e1 100644 --- a/src/struphy/models/variational_compressible_fluid.py +++ b/src/struphy/models/variational_compressible_fluid.py @@ -73,6 +73,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/variational_pressureless_fluid.py b/src/struphy/models/variational_pressureless_fluid.py index e8069d0d9..0c474ff8d 100644 --- a/src/struphy/models/variational_pressureless_fluid.py +++ b/src/struphy/models/variational_pressureless_fluid.py @@ -61,6 +61,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd.py b/src/struphy/models/visco_resistive_deltaf_mhd.py index 4da882c5b..6c0d8b305 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd.py @@ -102,6 +102,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py index 70953cebf..ee9aaecb8 100644 --- a/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_deltaf_mhd_with_q.py @@ -102,6 +102,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_linear_mhd.py b/src/struphy/models/visco_resistive_linear_mhd.py index 5902a82ee..c2e259e72 100644 --- a/src/struphy/models/visco_resistive_linear_mhd.py +++ b/src/struphy/models/visco_resistive_linear_mhd.py @@ -100,6 +100,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_linear_mhd_with_q.py b/src/struphy/models/visco_resistive_linear_mhd_with_q.py index 0ba443170..ad8203aa6 100644 --- a/src/struphy/models/visco_resistive_linear_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_linear_mhd_with_q.py @@ -100,6 +100,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_mhd.py b/src/struphy/models/visco_resistive_mhd.py index 733896548..e1bfe98d9 100644 --- a/src/struphy/models/visco_resistive_mhd.py +++ b/src/struphy/models/visco_resistive_mhd.py @@ -99,6 +99,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_mhd_with_p.py b/src/struphy/models/visco_resistive_mhd_with_p.py index 852c0c74e..9acffe1f1 100644 --- a/src/struphy/models/visco_resistive_mhd_with_p.py +++ b/src/struphy/models/visco_resistive_mhd_with_p.py @@ -100,6 +100,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/visco_resistive_mhd_with_q.py b/src/struphy/models/visco_resistive_mhd_with_q.py index e2d1090a2..6cc05b976 100644 --- a/src/struphy/models/visco_resistive_mhd_with_q.py +++ b/src/struphy/models/visco_resistive_mhd_with_q.py @@ -102,6 +102,7 @@ def __init__( with_viscosity: bool = True, with_resistivity: bool = True, ): + # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD() diff --git a/src/struphy/models/viscous_euler_sph.py b/src/struphy/models/viscous_euler_sph.py index 1e74dc350..07ef82b99 100644 --- a/src/struphy/models/viscous_euler_sph.py +++ b/src/struphy/models/viscous_euler_sph.py @@ -86,6 +86,7 @@ def __init__(self, with_B0: bool = True, with_p: bool = True, with_viscosity: bo ## abstract methods def __init__(self, with_B0: bool = True, with_p: bool = True, with_viscosity: bool = True): + self.with_B0 = with_B0 self.with_p = with_p self.with_viscosity = with_viscosity diff --git a/src/struphy/models/viscous_fluid.py b/src/struphy/models/viscous_fluid.py index e4eaa6a69..8ae7e72b1 100644 --- a/src/struphy/models/viscous_fluid.py +++ b/src/struphy/models/viscous_fluid.py @@ -78,6 +78,7 @@ def __init__(self, with_viscosity: bool = True): ## abstract methods def __init__(self, with_viscosity: bool = True): + # 1. instantiate all species self.fluid = self.Fluid() diff --git a/src/struphy/models/vlasov.py b/src/struphy/models/vlasov.py index e704aae99..b8a5fc342 100644 --- a/src/struphy/models/vlasov.py +++ b/src/struphy/models/vlasov.py @@ -56,6 +56,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 1f1a62770..78b64816c 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -119,6 +119,7 @@ def __init__(self, with_B0: bool = True): ## abstract methods def __init__(self, with_B0: bool = True): + self.with_B0 = with_B0 # 1. instantiate all species diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index a5487819a..ef43a456e 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -130,6 +130,7 @@ def __init__(self): ## abstract methods def __init__(self): + # 1. instantiate all species self.em_fields = self.EMFields() self.kinetic_ions = self.KineticIons() diff --git a/src/struphy/particles/parameters.py b/src/struphy/particles/parameters.py index 8b2583cd1..ff4205340 100644 --- a/src/struphy/particles/parameters.py +++ b/src/struphy/particles/parameters.py @@ -147,7 +147,7 @@ class BoundaryParameters: bc_sph : tuple[LiteralOptions.OptsRecontructBC], default=("periodic", "periodic", "periodic") Boundary conditions for SPH kernel reconstruction in each spatial direction. Typically matches or differs from ``bc`` depending on reconstruction needs. - + mean_velocity_index : int, optional If any boundary condition is 'noslip', this index specifies the position in the marker array where the mean velocity for the noslip condition is stored. @@ -165,7 +165,6 @@ def __init__( self.bc_sph = bc_sph self.mean_velocity_index = mean_velocity_index - class BinningPlot: """Configuration for particle phase-space binning and histogram generation. diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index ae67e5438..b9fde59e0 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -208,6 +208,7 @@ def __init__( sim: "Simulation" = None, path_out: str = None, ): + # create post-processing folder if sim is None: assert path_out is not None, ( @@ -402,6 +403,7 @@ def process_particles( classify: bool = False, verbose: bool = False, ): + if self.exist_particles is None: print("\nNo kinetic data found in hdf5 file, skipping post-processing of kinetic data.") return @@ -1129,6 +1131,7 @@ class PlottingData: """ 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." diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 5350a8bf6..1e09266ad 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -120,6 +120,7 @@ def __init__( derham_opts: DerhamOptions = DerhamOptions(), verbose: bool = False, ): + self._model = model self._params_path = params_path self._env = env From 24f207d389214c892fa710a4a2ce503806f4f627 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Mar 2026 07:22:42 +0100 Subject: [PATCH 75/80] formatting --- src/struphy/particles/parameters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/struphy/particles/parameters.py b/src/struphy/particles/parameters.py index ff4205340..8b2583cd1 100644 --- a/src/struphy/particles/parameters.py +++ b/src/struphy/particles/parameters.py @@ -147,7 +147,7 @@ class BoundaryParameters: bc_sph : tuple[LiteralOptions.OptsRecontructBC], default=("periodic", "periodic", "periodic") Boundary conditions for SPH kernel reconstruction in each spatial direction. Typically matches or differs from ``bc`` depending on reconstruction needs. - + mean_velocity_index : int, optional If any boundary condition is 'noslip', this index specifies the position in the marker array where the mean velocity for the noslip condition is stored. @@ -165,6 +165,7 @@ def __init__( self.bc_sph = bc_sph self.mean_velocity_index = mean_velocity_index + class BinningPlot: """Configuration for particle phase-space binning and histogram generation. From 6799b9e5c86ca4c8f9f3a0aae98714594d160d6b Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 17 Mar 2026 08:55:18 +0100 Subject: [PATCH 76/80] add def mean_velocity_index --- src/struphy/pic/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index e4e3bf42c..36bb6b18e 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -295,15 +295,16 @@ def __init__( if bc_sph is None: bc_sph = [bci if bci == "periodic" else "mirror" for bci in self.bc] + self._mean_velocity_index = None for bci in bc_sph: assert bci in ("periodic", "mirror", "fixed", "noslip") if bci == "noslip": if boundary_params.mean_velocity_index is None: - self.mean_velocity_index = ( + self._mean_velocity_index = ( self.first_free_idx ) # index in marker array where mean velocity for noslip BC is stored else: - self.mean_velocity_index = boundary_params.mean_velocity_index + self._mean_velocity_index = boundary_params.mean_velocity_index self._bc_sph = bc_sph # particle type @@ -495,6 +496,11 @@ def bc_sph(self): """List of boundary conditions for sph evaluation in each direction.""" return self._bc_sph + @property + def mean_velocity_index(self): + """Index in marker array where mean velocity for noslip BC is stored.""" + return self._mean_velocity_index + @property def Np(self): """Total number of markers/particles, from user input.""" From a5ac3798ef0d92269be5c5a490197cec48f6b678 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 24 Mar 2026 08:43:44 +0100 Subject: [PATCH 77/80] in eval_kernels_gc, skip for not valid markers (instead of holes) --- src/struphy/pic/pushing/eval_kernels_gc.py | 8 ++++---- src/struphy/pic/tests/test_sph.py | 21 +++++++++------------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/struphy/pic/pushing/eval_kernels_gc.py b/src/struphy/pic/pushing/eval_kernels_gc.py index 2af5dae90..1115e73cb 100644 --- a/src/struphy/pic/pushing/eval_kernels_gc.py +++ b/src/struphy/pic/pushing/eval_kernels_gc.py @@ -651,12 +651,12 @@ def sph_mean_velocity_coeffs( for ip in range(n_markers): # only do something if particle is a "true" particle - # if not valid_mks[ip]: - # continue + if not valid_mks[ip]: + continue # also evaluate and save for ghost particles, only skip holes (!) - if holes[ip]: - continue + # if holes[ip]: + # continue eta1 = markers[ip, 0] eta2 = markers[ip, 1] diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index c44c66a9d..0b72e1558 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1286,7 +1286,7 @@ def abs_err(num, exact): plt.colorbar() plt.tight_layout() - plt.savefig("image_test_2d.png") + # plt.savefig("image_test_2d.png") plt.show() plt.figure(figsize=(8, 8)) @@ -1304,7 +1304,7 @@ def abs_err(num, exact): plt.ylabel("y") plt.axis("equal") plt.tight_layout() - plt.savefig("image_test_2d_quiver.png") + # plt.savefig("image_test_2d_quiver.png") plt.show() # tolerances: conservative values aligned with your 2D density thresholds @@ -1763,7 +1763,6 @@ def test_sph_no_slip_boundary_1d( show_plot=False, ): import sys - import numpy numpy.set_printoptions(threshold=sys.maxsize, linewidth=200, precision=3, suppress=True) @@ -1788,15 +1787,12 @@ def test_sph_no_slip_boundary_1d( loading_params = LoadingParameters(ppb=ppb, seed=223) if direction == "x": - def u_xyz(x, y, z): return (xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) elif direction == "y": - def u_xyz(x, y, z): return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) else: - def u_xyz(x, y, z): return (xp.zeros_like(x), xp.zeros_like(x), xp.ones_like(x)) @@ -1926,7 +1922,7 @@ def u_xyz(x, y, z): plt.legend() plt.grid(True) plt.show() - plt.savefig("bc_sph") + # plt.savefig("bc_sph") if tesselation: tol_wall = 3e-3 @@ -1959,8 +1955,9 @@ def u_xyz(x, y, z): if __name__ == "__main__": - test_sph_no_slip_boundary_1d( - tesselation=False, - direction="x", - show_plot=False, - ) + # test_sph_no_slip_boundary_1d( + # tesselation=False, + # direction="x", + # show_plot=True, + # ) + test_sph_velocity_evaluation_2d((12, 12, 1), "gaussian_2d", 1, "periodic", "periodic", 11, tesselation=False, show_plot=True) From 29e766b3972c4fecc42dea22523a0bed3d2d641f Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 24 Mar 2026 08:50:52 +0100 Subject: [PATCH 78/80] formatting --- src/struphy/pic/pushing/eval_kernels_gc.py | 2 +- src/struphy/pic/tests/test_sph.py | 8 +++++++- src/struphy/simulation/sim.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/struphy/pic/pushing/eval_kernels_gc.py b/src/struphy/pic/pushing/eval_kernels_gc.py index 1115e73cb..921dfae6e 100644 --- a/src/struphy/pic/pushing/eval_kernels_gc.py +++ b/src/struphy/pic/pushing/eval_kernels_gc.py @@ -652,7 +652,7 @@ def sph_mean_velocity_coeffs( for ip in range(n_markers): # only do something if particle is a "true" particle if not valid_mks[ip]: - continue + continue # also evaluate and save for ghost particles, only skip holes (!) # if holes[ip]: diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 0b72e1558..8a741a2c4 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1763,6 +1763,7 @@ def test_sph_no_slip_boundary_1d( show_plot=False, ): import sys + import numpy numpy.set_printoptions(threshold=sys.maxsize, linewidth=200, precision=3, suppress=True) @@ -1787,12 +1788,15 @@ def test_sph_no_slip_boundary_1d( loading_params = LoadingParameters(ppb=ppb, seed=223) if direction == "x": + def u_xyz(x, y, z): return (xp.ones_like(x), xp.zeros_like(x), xp.zeros_like(x)) elif direction == "y": + def u_xyz(x, y, z): return (xp.zeros_like(x), xp.ones_like(x), xp.zeros_like(x)) else: + def u_xyz(x, y, z): return (xp.zeros_like(x), xp.zeros_like(x), xp.ones_like(x)) @@ -1960,4 +1964,6 @@ def u_xyz(x, y, z): # direction="x", # show_plot=True, # ) - test_sph_velocity_evaluation_2d((12, 12, 1), "gaussian_2d", 1, "periodic", "periodic", 11, tesselation=False, show_plot=True) + test_sph_velocity_evaluation_2d( + (12, 12, 1), "gaussian_2d", 1, "periodic", "periodic", 11, tesselation=False, show_plot=True + ) diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 465bda3ab..3eb5ad6b4 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1494,7 +1494,7 @@ def generate_script( if include_defaults: sim_setup += f"model = {self.model.__repr__()}\n" sim_class_def += "model=model," - + sim_setup += f"env = {self.env.__repr__()}\n" sim_class_def += "env=env," @@ -1518,7 +1518,7 @@ def generate_script( sim_setup += f"model = {self.model.__repr_no_defaults__()}\n" sim_class_def += "model=model," - + if not self.env.is_default: sim_setup += f"env = {self.env.__repr_no_defaults__()}\n" sim_class_def += "env=env," From 05e7237611c16c724eeff0529f712cd3778e39a4 Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Tue, 24 Mar 2026 10:32:57 +0100 Subject: [PATCH 79/80] set f_visc[:]=0 at the start of the viscous kernel sph evaluation --- .../test_verif_ViscousEulerSPH.py | 30 +++++++++++++++---- src/struphy/pic/pushing/pusher_kernels.py | 1 + 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index 71df5a793..e9c1af449 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -1,5 +1,6 @@ import os import shutil +import h5py import cunumpy as xp import pytest @@ -177,7 +178,7 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): base_units = BaseUnits(kBT=1.0) # time stepping - time_opts = Time(dt=0.01, Tend=0.1, split_algo="LieTrotter") + time_opts = Time(dt=0.01, Tend=1.0, split_algo="LieTrotter") # geometry r1 = 1.0 @@ -197,7 +198,7 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): loading_params = LoadingParameters(ppb=100, loading="tesselation") weights_params = WeightsParameters() - boundary_params = BoundaryParameters() + boundary_params = BoundaryParameters(bc_sph=("periodic", "periodic", "periodic")) model.euler_fluid.set_markers( loading_params=loading_params, weights_params=weights_params, @@ -221,12 +222,13 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): butcher = ButcherTableau(algo="forward_euler") # model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher) if model.with_viscosity: - model.propagators.push_viscous.options = model.propagators.push_viscous.Options(kernel_type="gaussian_1d", mu=0.001) + model.propagators.push_viscous.options = model.propagators.push_viscous.Options(kernel_type="gaussian_1d", mu=0.1) # background, perturbations and initial conditions background = equils.ConstantVelocity() model.euler_fluid.var.add_background(background) - perturbation = perturbations.GaussianBlobEta1(center=0.5, amp=1.0, sigma=0.1) + # perturbation = perturbations.GaussianBlobEta1(center=0.5, amp=1.0, sigma=0.1) + perturbation = perturbations.ModesSin(ls=(1,), amps=(1.0,)) model.euler_fluid.var.add_perturbation(del_u1=perturbation) # instance of simulation @@ -244,6 +246,20 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): # run sim.run(verbose=True) + # get 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"][()] + en_kin = f["scalar"]["en_kin"][()] + + # plot + if do_plot: + plt.figure(figsize=(18, 12)) + plt.plot(time, en_kin, label="numerical") + plt.legend() + plt.show() + # post processing if MPI.COMM_WORLD.Get_rank() == 0: sim.pproc(verbose=True) @@ -261,15 +277,19 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): plt.subplot(1, 4, 1) plt.plot(grid_e1, f_binned[0, :], label=f"time {sim.t_grid[0]}") plt.title(f"time {sim.t_grid[0]}") + plt.ylim([-1, 1]) plt.subplot(1, 4, 2) plt.plot(grid_e1, f_binned[shp//3, :], label=f"time {sim.t_grid[shp//3]}") plt.title(f"time {sim.t_grid[shp//3]}") + plt.ylim([-1, 1]) plt.subplot(1, 4, 3) plt.plot(grid_e1, f_binned[2*shp//3, :], label=f"time {sim.t_grid[2*shp//3]}") - plt.title(f"time {sim.t_grid[2*shp//3]}") + plt.title(f"time {sim.t_grid[2*shp//3]}") + plt.ylim([-1, 1]) plt.subplot(1, 4, 4) plt.plot(grid_e1, f_binned[-1, :], label=f"time {sim.t_grid[-1]}") plt.title(f"time {sim.t_grid[-1]}") + plt.ylim([-1, 1]) plt.show() diff --git a/src/struphy/pic/pushing/pusher_kernels.py b/src/struphy/pic/pushing/pusher_kernels.py index b770b1151..a32c6e0f3 100644 --- a/src/struphy/pic/pushing/pusher_kernels.py +++ b/src/struphy/pic/pushing/pusher_kernels.py @@ -3127,6 +3127,7 @@ def push_v_viscosity( # n_at_eta = markers[ip, first_free_idx] loc_box = int(markers[ip, n_cols - 2]) + f_visc[:] = 0.0 for j in range(3): # row of viscosity tensor for k in range(3): # column = derivative direction coeff_idx = first_free_idx + 3 * (j + 1) + k From bd9f738ff4fa1b8c4fc1dfd25a0f598d63b6cb2d Mon Sep 17 00:00:00 2001 From: Stefan Possanner Date: Wed, 25 Mar 2026 13:47:51 +0100 Subject: [PATCH 80/80] formatting --- src/struphy/initial/perturbations.py | 3 ++- .../test_verif_ViscousEulerSPH.py | 20 ++++++++++--------- src/struphy/pic/base.py | 2 +- src/struphy/pic/tests/test_sph.py | 4 ++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/struphy/initial/perturbations.py b/src/struphy/initial/perturbations.py index d861cf9a7..60de28f11 100644 --- a/src/struphy/initial/perturbations.py +++ b/src/struphy/initial/perturbations.py @@ -1271,6 +1271,7 @@ class GaussianBlobEta1(Perturbation): u(\eta_1, \eta_2, \eta_3) = A \exp \left(- \frac{(\eta_1 - 0.5)^2}{2 \sigma^2} \right) \,. """ + def __init__( self, center: float = 0.5, @@ -1291,7 +1292,7 @@ def __init__( self.comp = comp def __call__(self, e1, e2, e3): - val = self._amp * xp.exp(-(e1 - self._center)**2 / (2.0 * self._sigma**2)) + val = self._amp * xp.exp(-((e1 - self._center) ** 2) / (2.0 * self._sigma**2)) return val diff --git a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py index e9c1af449..cea5ff898 100644 --- a/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py +++ b/src/struphy/models/tests/verification/test_verif_ViscousEulerSPH.py @@ -1,8 +1,8 @@ import os import shutil -import h5py import cunumpy as xp +import h5py import pytest from feectools.ddm.mpi import mpi as MPI from matplotlib import pyplot as plt @@ -222,7 +222,9 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): butcher = ButcherTableau(algo="forward_euler") # model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher) if model.with_viscosity: - model.propagators.push_viscous.options = model.propagators.push_viscous.Options(kernel_type="gaussian_1d", mu=0.1) + model.propagators.push_viscous.options = model.propagators.push_viscous.Options( + kernel_type="gaussian_1d", mu=0.1 + ) # background, perturbations and initial conditions background = equils.ConstantVelocity() @@ -245,7 +247,7 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): # run sim.run(verbose=True) - + # get scalar data if MPI.COMM_WORLD.Get_rank() == 0: pa_data = os.path.join(env.path_out, "data") @@ -259,7 +261,7 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): plt.plot(time, en_kin, label="numerical") plt.legend() plt.show() - + # post processing if MPI.COMM_WORLD.Get_rank() == 0: sim.pproc(verbose=True) @@ -279,13 +281,13 @@ def test_viscosity_1d(nx: int, plot_pts: int, do_plot: bool = False): plt.title(f"time {sim.t_grid[0]}") plt.ylim([-1, 1]) plt.subplot(1, 4, 2) - plt.plot(grid_e1, f_binned[shp//3, :], label=f"time {sim.t_grid[shp//3]}") - plt.title(f"time {sim.t_grid[shp//3]}") + plt.plot(grid_e1, f_binned[shp // 3, :], label=f"time {sim.t_grid[shp // 3]}") + plt.title(f"time {sim.t_grid[shp // 3]}") plt.ylim([-1, 1]) plt.subplot(1, 4, 3) - plt.plot(grid_e1, f_binned[2*shp//3, :], label=f"time {sim.t_grid[2*shp//3]}") - plt.title(f"time {sim.t_grid[2*shp//3]}") - plt.ylim([-1, 1]) + plt.plot(grid_e1, f_binned[2 * shp // 3, :], label=f"time {sim.t_grid[2 * shp // 3]}") + plt.title(f"time {sim.t_grid[2 * shp // 3]}") + plt.ylim([-1, 1]) plt.subplot(1, 4, 4) plt.plot(grid_e1, f_binned[-1, :], label=f"time {sim.t_grid[-1]}") plt.title(f"time {sim.t_grid[-1]}") diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index 000df66ed..77e79004e 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -1360,7 +1360,7 @@ def _f_init(*etas, flat_eval=False): out = xp.squeeze(out) return out - + def _u_init(*etas, flat_eval=False): if len(etas) == 1: out = self.f0.uv(etas[0]) diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 4c3c5de46..cfcb3f769 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1962,7 +1962,7 @@ def u_xyz(x, y, z): test_sph_no_slip_boundary_1d( (1, 1, 12), "gaussian_3d", - tesselation= False, - direction = "z", + tesselation=False, + direction="z", show_plot=True, )