Source code for struphy.models.base

import logging
import os
from abc import ABCMeta, abstractmethod
from textwrap import indent

import cunumpy as xp
from feectools.ddm.mpi import MockMPI
from feectools.ddm.mpi import mpi as MPI

try:
    from IPython.display import HTML, Markdown, display
except ImportError:

    def HTML(data):
        return data

    def Markdown(data):
        return data

    def display(*objects, **kwargs):
        return objects[0] if objects else None


from line_profiler import profile
from scope_profiler import ProfileManager

from struphy import BaseUnits
from struphy.io.options import LiteralOptions
from struphy.models.scalars import Scalars
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.docstring_converter import rst_to_html, rst_to_markdown
from struphy.utils.utils import __class_with_params_repr_no_defaults__, all_class_params_are_default, all_subclasses

logger = logging.getLogger("struphy")


class StruphyModelMeta(ABCMeta):
    def __iter__(cls):
        return iter(all_subclasses(cls))


[docs] class StruphyModel(metaclass=StruphyModelMeta): """ 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. scalars : Scalars or None 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. 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): # Initialize helper arrays pass class Propagators: # Define propagators pass """ # ---------------- # Abstract methods # ----------------
[docs] @classmethod @abstractmethod def model_type(cls) -> LiteralOptions.ModelTypes: """Model type (Fluid, Kinetic, Hybrid, or Toy)""" pass
[docs] @abstractmethod class Propagators: pass
@abstractmethod def __init__(self): pass @property @abstractmethod def bulk_species() -> Species: """Bulk species of the plasma. Must be an attribute of species_static().""" @property @abstractmethod def velocity_scale() -> str: """Velocity unit scale of the model. Must be one of "alfvén", "cyclotron", "light" or "thermal"."""
[docs] @abstractmethod def allocate_helpers(self): """Allocate helper arrays and perform initial solves if needed."""
# -------------- # Common methods # -------------- def __repr__(self): out = f"{self.__class__.__name__}(\n" for k, v in self.params.items(): out += " " * 4 out += f"{k}={v},\n" out += ")" return out def __repr_no_defaults__(self) -> str: return __class_with_params_repr_no_defaults__(self) @property def is_default(self): return all_class_params_are_default(self) # def __str__(self): # for k, v in self.__dict__.items(): # logger.info(f"{k + ':':<20}{v}") # return "" # def __str__(self): # out = f"{self.__class__.__name__}\n" # for k, v in self.species.items(): # out += f" {k}:\n" # out += f"{v}" # return out forced_heading_level = 5
[docs] @classmethod def info(cls): summary = ( rst_to_html(cls.__doc__).split("Parameters")[0].split("<")[0] if cls.__doc__ else """Description not available for this model.""" ) summary = " ".join(summary.split()) doc = f"**{summary}**\n" doc += rf"""To see detailed information on the model, run the following methods: .. code-block:: python {cls.name()}.pde() {cls.name()}.normalization() {cls.name()}.scalar_quantities() {cls.name()}.discretization() {cls.name()}.long_description() {cls.name()}.examples() {cls.name()}.use_cases() {cls.name()}.cannot_be_used_for() """ return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def pde(cls): doc_pde = getattr(cls, "doc_pde", None) doc = doc_pde.__doc__ if doc_pde else """PDE description not available for this model.""" return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def normalization(cls): doc_normalization = getattr(cls, "doc_normalization", None) doc = "**Normalization:**\n" doc += ( doc_normalization.__doc__ if doc_normalization else """Description of normalization not available for this model.""" ) return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def scalar_quantities(cls): doc_scalar_quantities = getattr(cls, "doc_scalar_quantities", None) doc = ( doc_scalar_quantities.__doc__ if doc_scalar_quantities else """Description of scalar quantities not available for this model.""" ) return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def discretization(cls): doc_discretization = getattr(cls, "doc_discretization", None) doc = "**Discretization (Propagators called in sequence):**\n" doc += ( doc_discretization() if doc_discretization else """Description of discretization not available for this model.""" ) return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def long_description(cls): doc_long_description = getattr(cls, "doc_long_description", None) doc = "**Long description:**\n" doc += ( doc_long_description.__doc__ if doc_long_description else """Long description not available for this model.""" ) return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def examples(cls): doc_examples = getattr(cls, "doc_examples", None) doc = "**Examples:**\n" doc += doc_examples.__doc__ if doc_examples else """Examples not available for this model.""" return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def use_cases(cls): doc_use_cases = getattr(cls, "doc_use_cases", None) doc = "**Use cases:**\n" doc += doc_use_cases.__doc__ if doc_use_cases else """Description of use cases not available for this model.""" return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def cannot_be_used_for(cls): doc_cannot_be_used_for = getattr(cls, "doc_cannot_be_used_for", None) doc = "**Cannot be used for:**\n" doc += ( doc_cannot_be_used_for.__doc__ if doc_cannot_be_used_for else """Information on scenarios for which the model is not suitable is not available.""" ) return display(HTML(rst_to_html(doc, forced_heading_level=cls.forced_heading_level)))
[docs] @classmethod def name(cls) -> str: return cls.__name__
[docs] @classmethod def create_doc(cls) -> "Documentation": doc = Documentation(cls) return doc
@property def scalars(self) -> Scalars | None: """Scalars to be updated and saved during the simulation.""" return getattr(self, "_scalars", Scalars()) @scalars.setter def scalars(self, value: Scalars): assert isinstance(value, Scalars) self._scalars = value
[docs] @profile def update_scalar_quantities(self): """Update scalar quantities by calling their .update() method. This should be called at the end of each time step in the simulation loop.""" if self.scalars is not None: self.scalars.update()
[docs] def print_scalar_quantities(self): """ Check if scalars are not "nan" and print to screen. """ sq_str = "" for key, scalar in self.scalars.dct.items(): val = scalar.value[0] assert not xp.isnan(val), f"Scalar {key} is {val}." sq_str += f"{key}:".ljust(25) + "{:4.2e}\n".format(val).rjust(26) print(sq_str)
[docs] def setup_equation_params(self, base_units: BaseUnits): """Compute units and set equation parameters for each fluid and kinetic species.""" self.base_units = base_units self.units = Units(base_units) if self.bulk_species is None: A_bulk = None Z_bulk = None else: A_bulk = self.bulk_species.mass_number Z_bulk = self.bulk_species.charge_number self.units.derive_units( velocity_scale=self.velocity_scale, A_bulk=A_bulk, Z_bulk=Z_bulk, ) for _, species in self.fluid_species.items(): assert isinstance(species, FluidSpecies) species.setup_equation_params(units=self.units) for _, species in self.particle_species.items(): assert isinstance(species, ParticleSpecies) species.setup_equation_params(units=self.units)
[docs] def show_equation_params(self): """Print the equation parameters for each species to screen.""" for _, species in self.fluid_species.items(): assert isinstance(species, FluidSpecies) species.equation_params.show() for _, species in self.particle_species.items(): assert isinstance(species, ParticleSpecies) species.equation_params.show()
[docs] @profile def integrate(self, dt, split_algo="LieTrotter"): """ Advance the model by a time step ``dt`` by sequentially calling its Propagators. Parameters ---------- dt : float Time step of time integration. split_algo : str Splitting algorithm. Currently available: "LieTrotter" and "Strang". """ # first order in time if split_algo == "LieTrotter": for propagator in self.prop_list: prop_name = propagator.__class__.__name__ with ProfileManager.profile_region("prop: " + prop_name): propagator(dt) # second order in time elif split_algo == "Strang": assert len(self.prop_list) > 1 for propagator in self.prop_list[:-1]: prop_name = type(propagator).__name__ with ProfileManager.profile_region("prop: " + prop_name): propagator(dt / 2) propagator = self.prop_list[-1] prop_name = type(propagator).__name__ with ProfileManager.profile_region("prop: " + prop_name): propagator(dt) for propagator in self.prop_list[:-1][::-1]: prop_name = type(propagator).__name__ with ProfileManager.profile_region("prop: " + prop_name): propagator(dt / 2) else: raise NotImplementedError( f"Splitting scheme {split_algo} not available.", )
[docs] @profile def update_markers_to_be_saved(self): """ Writes markers with IDs that are supposed to be saved into corresponding array. """ 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 _, var in species.variables.items(): assert isinstance(var, PICVariable | SPHVariable) obj = var.particles assert isinstance(obj, Particles) if var.n_to_save > 0: markers_on_proc = xp.logical_and( obj.markers[:, -1] >= 0.0, obj.markers[:, -1] < var.n_to_save, ) n_markers_on_proc = xp.count_nonzero(markers_on_proc) var.saved_markers[:] = -1.0 var.saved_markers[:n_markers_on_proc] = obj.markers[markers_on_proc]
[docs] @profile def update_distr_functions(self): """ Writes distribution functions slices that are supposed to be saved into corresponding array. """ dim_to_int = {"e1": 0, "e2": 1, "e3": 2, "v1": 3, "v2": 4, "v3": 5} 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 _, var in species.variables.items(): assert isinstance(var, PICVariable | SPHVariable) obj = var.particles assert isinstance(obj, Particles) if obj.n_cols_diagnostics > 0: for i in range(obj.n_cols_diagnostics): str_dn = f"d{i + 1}" dim_to_int[str_dn] = 3 + obj.vdim + 3 + i for bin_plot in species.saving_params.binning_plots: comps = bin_plot.slice.split("_") components = [False] * (3 + obj.vdim + 3 + obj.n_cols_diagnostics) for comp in comps: components[dim_to_int[comp]] = True edges = bin_plot.bin_edges binning_quantity = bin_plot.output_quantity divide_by_jac = bin_plot.divide_by_jac f_slice, df_slice = obj.binning( components, edges, output_quantity=binning_quantity, divide_by_jac=divide_by_jac ) bin_plot.f[:] = f_slice bin_plot.df[:] = df_slice for kd_plot in species.saving_params.kernel_density_plots: h1 = 1 / obj.boxes_per_dim[0] h2 = 1 / obj.boxes_per_dim[1] h3 = 1 / obj.boxes_per_dim[2] ndim = xp.count_nonzero([d > 1 for d in obj.boxes_per_dim]) if ndim == 0: kernel_type = "gaussian_3d" else: kernel_type = "gaussian_" + str(ndim) + "d" pts = kd_plot.plot_pts n_sph = obj.eval_density( *pts, h1=h1, h2=h2, h3=h3, kernel_type=kernel_type, fast=True, ) kd_plot.n_sph[:] = n_sph
[docs] def generate_default_parameter_file( self, path: str = None, prompt: bool = True, ): """Generate a parameter file with default options for each species, and save it to the current input path. The default name is params_<model_name>.yml. Parameters ---------- path : str Alternative path to getcwd()/params_MODEL.py. prompt : bool Whether to prompt for overwriting the specified .yml file. Returns ------- params_path : str The path of the parameter file. """ if path is None: path = os.path.join(os.getcwd(), f"params_{self.__class__.__name__}.py") # create new default file try: file = open(path, "x") except FileExistsError: if not prompt: yn = "Y" else: yn = input(f"\nFile {path} exists, overwrite (Y/n)? ") if yn in ("", "Y", "y", "yes", "Yes"): file = open(path, "w") else: logger.info("exiting ...") exit() except FileNotFoundError: folder = os.path.join("/", *path.split("/")[:-1]) if not prompt: yn = "Y" else: yn = input(f"\nFolder {folder} does not exist, create (Y/n)? ") if yn in ("", "Y", "y", "yes", "Yes"): os.makedirs(folder) file = open(path, "x") else: logger.info("exiting ...") exit() # loop over species to create parameter snippets variables_params = "" particle_params = """\n# ------------------- # Particle parameters # -------------------\n""" init_bckgr_pic = "\n# Background for kinetic species\n" has_feec = False has_pic = False has_sph = False for sn, species in self.species.items(): assert isinstance(species, Species) if isinstance(species, ParticleSpecies): particle_params += "\nloading_params = LoadingParameters()\n" particle_params += "weights_params = WeightsParameters()\n" particle_params += "boundary_params = BoundaryParameters()\n" particle_params += "sorting_params = SortingParameters()\n" particle_params += "saving_params = SavingParameters()\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 = "sorting_params=sorting_params,\n" particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) txt = "saving_params=saving_params,\n" particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) txt = ")\n" particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) 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" else: 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" ) elif isinstance(var, PICVariable): has_pic = True 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_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_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None), equil=equil)\n" init_pert_pic += ( "maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil)\n" ) init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" if "3D" in var.space: 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" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" init_bckgr_pic += "background = maxwellian_1 + maxwellian_2\n" init_bckgr_pic += f"model.{sn}.{vn}.add_background(background)\n" elif isinstance(var, SPHVariable): has_sph = True 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 += f"model.{sn}.{vn}.add_perturbation(del_n=perturbation)\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. name = \"Default {self.__class__.__name__}\" description = \"\"\"\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\"\"\"\n""") file.write(""" import logging from struphy import set_logging_level set_logging_level(logging.WARNING)\n""") file.write("""\n# ------------------ # Import Struphy API # ------------------\n""") file.write("""\nfrom struphy import ( BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Simulation, Time, domains, equils, grids, perturbations, )\n""") if has_pic or has_sph: file.write("""\n# For particles:\nfrom struphy import ( BinningPlot, BoundaryParameters, KernelDensityPlot, LoadingParameters, WeightsParameters, SortingParameters, SavingParameters, maxwellians, )\n""") file.write("""\n# --------------------- # Instance of the model # ---------------------\n""") file.write(f"\nfrom struphy.models import {self.__class__.__name__}\n") file.write("\n# Units\n") file.write("base_units = BaseUnits()\n") file.write("\n# Model instance\n") file.write(f"model = {self.__class__.__name__}(base_units=base_units)\n") 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("\n# Environment options\n") file.write("env = EnvironmentOptions()\n") file.write("\n# Time stepping\n") file.write("time_opts = Time()\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("equil = equils.HomogenSlab()\n") # if has_feec: grid = "grid = grids.TensorProductGrid()\n" derham = "derham_opts = DerhamOptions()\n" # else: # grid = "grid = None\n" # derham = "derham_opts = None\n" file.write("\n# Grid\n") file.write(grid) file.write("\n# Derham options\n") file.write(derham) file.write("\n# Simulation object\n") file.write("""sim = Simulation( model=model, name=name, description=description, params_path=__file__, env=env, time_opts=time_opts, domain=domain, equil=equil, grid=grid, derham_opts=derham_opts, )\n""") if has_pic or has_sph: file.write(particle_params) 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# ------------------ # Initial conditions # ------------------ # Initial conditions are the sum of the background(s) and the perturbation(s). # If backgrounds or perturbations are not specified, they are assumed to be zero.\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: file.write(init_bckgr_sph) file.write(init_pert_sph) file.write('\nif __name__ == "__main__":\n') file.write(" sim.run()") file.close() logger.info( f"\nDefault parameter file for '{self.__class__.__name__}' has been created in the cwd ({path}).\n\ You can now launch a simulation with 'python params_{self.__class__.__name__}.py'", ) return path
[docs] def to_dict(self) -> dict: """Serialize the model configuration to a dictionary.""" dct = {"model": self.__class__.__name__} return dct
[docs] @classmethod def from_dict(cls, dct) -> "StruphyModel": """Deserialize a model configuration from a dictionary.""" model_name = dct["model"] return cls.from_name(model_name)
[docs] @classmethod def from_name(cls, name: str) -> "StruphyModel": """Instantiate a model from its name.""" from struphy.models.utils import get_model_by_name model_cls = get_model_by_name(name) return model_cls()
# ------------- # Model 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 # ----------------- # 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 base_units(self) -> BaseUnits: """Base units of the model.""" return self._base_units @base_units.setter def base_units(self, new_units): assert isinstance(new_units, BaseUnits) self._base_units = new_units @property def units(self) -> Units: """Units of the model.""" return self._units @units.setter def units(self, new_units): assert isinstance(new_units, Units) self._units = new_units @property def params(self) -> dict: """Model parameters passed to __init__() of the class, as dictionary.""" if not hasattr(self, "_params"): self._params = {} return self._params @params.setter def params(self, new): assert isinstance(new, dict) if "self" in new: new.pop("self") if "__class__" in new: new.pop("__class__") self._params = new
class Documentation: def __init__( self, cls: StruphyModel, ): self.description = self.Content(cls.__doc__ if cls.__doc__ else "Description not available for this model.") self.pde = self.Content(cls.doc_pde.__doc__ if cls.doc_pde else "PDE description not available for this model.") self.normalization = self.Content( cls.doc_normalization.__doc__ if cls.doc_normalization else "Description of normalization not available for this model." ) self.scalar_quantities = self.Content( cls.doc_scalar_quantities.__doc__ if cls.doc_scalar_quantities else "Description of scalar quantities not available for this model." ) self.discretization = self.Content( cls.doc_discretization() if cls.doc_discretization else "Description of discretization not available for this model." ) self.long_description = self.Content( cls.doc_long_description.__doc__ if cls.doc_long_description else "Long description not available for this model." ) self.examples = self.Content( cls.doc_examples.__doc__ if cls.doc_examples else "Examples not available for this model." ) self.use_cases = self.Content( cls.doc_use_cases.__doc__ if cls.doc_use_cases else "Use cases not available for this model." ) self.cannot_be_used_for = self.Content( cls.doc_cannot_be_used_for.__doc__ if cls.doc_cannot_be_used_for else "Information on scenarios for which the model is not suitable is not available." ) self.model_properties = self.Content( f"**Model type:** {cls.model_type()}\n- **Velocity scale:** {cls.velocity_scale}\n- **Bulk species:** {cls.bulk_species}" ) class Content: def __init__(self, rst, md=None, html=None): self.rst = rst self.md = md or rst_to_markdown(rst) self.html = html or rst_to_html(rst)