Source code for struphy.models.hasegawa_wakatani

import copy
import logging

from feectools.ddm.mpi import mpi as MPI
from feectools.linalg.stencil import StencilVector

from struphy.io.options import BaseUnits, LiteralOptions
from struphy.models.base import StruphyModel
from struphy.models.species import (
    FieldSpecies,
    FluidSpecies,
)
from struphy.models.variables import FEECVariable
from struphy.propagators.base import Propagator
from struphy.propagators.hasegawa_wakatani_step import HasegawaWakataniStep
from struphy.propagators.poisson_solve import PoissonSolve

logger = logging.getLogger("struphy")
rank = MPI.COMM_WORLD.Get_rank()


[docs] class HasegawaWakatani(StruphyModel): """Hasegawa-Wakatani equations in 2D for drift-wave turbulence. Parameters ---------- base_units: BaseUnits Base units for normalization (default: BaseUnits()) mass_number: float Mass number (in units of Proton mass) of the species (default: 1.0) """ @classmethod def model_type(cls) -> LiteralOptions.ModelTypes: return "Fluid" ## species class EMFields(FieldSpecies): def __init__(self): self.phi = FEECVariable(space="H1") self.init_variables() class Plasma(FluidSpecies): def __init__(self, mass_number: float = 1.0): self.density = FEECVariable(space="H1") self.vorticity = FEECVariable(space="H1") self.init_variables(mass_number=mass_number) ## propagators class Propagators: def __init__(self, phi: FEECVariable = None): self.poisson = PoissonSolve() self.hw = HasegawaWakataniStep(phi=phi) ## abstract methods def __init__(self, base_units: BaseUnits = BaseUnits(), mass_number: float = 1.0): # 0. store input parameters self.params = copy.deepcopy(locals()) # 1. instantiate all species self.em_fields = self.EMFields() self.plasma = self.Plasma(mass_number=mass_number) # 2. derive units (must be done after instantiating species to access charge and mass numbers) self.setup_equation_params(base_units=base_units) # 3. instantiate all propagators self.propagators = self.Propagators(phi=self.em_fields.phi) # 4. assign variables to propagators self.propagators.poisson.variables.phi = self.em_fields.phi self.propagators.hw.variables.n = self.plasma.density self.propagators.hw.variables.omega = self.plasma.vorticity # 5. define scalars to be tracked during simulation @property def bulk_species(self): return self.plasma @property def velocity_scale(self): return "alfvén" def update_rho(self): omega = self.plasma.vorticity.spline.vector self._rho = Propagator.mass_ops.M0.dot(omega, out=self._rho) self._rho.update_ghost_regions() return self._rho def allocate_helpers(self): """Solve initial Poisson equation. :meta private: """ self._rho: StencilVector = Propagator.derham.V0.zeros() self.update_rho() logger.info("\nINITIAL POISSON SOLVE:") self.update_rho() self.propagators.poisson(1.0) logger.info("Done.") # default parameters def generate_default_parameter_file(self, path=None, prompt=True): params_path = super().generate_default_parameter_file(path=path, prompt=prompt) new_file = [] with open(params_path, "r") as f: for line in f: if "vorticity.add_background" in line: new_file += ["model.plasma.density.add_background(FieldsBackground())\n"] else: new_file += [line] with open(params_path, "w") as f: for line in new_file: f.write(line)
[docs] @classmethod def doc_pde(cls): r"""**PDEs solved by model:** Density equation: .. math:: \frac{\partial n}{\partial t} = C (\phi - n) - [\phi, n] - \kappa \, \partial_y \phi + \nu \, \nabla^{2N} n Vorticity equation: .. math:: \frac{\partial \omega}{\partial t} = C (\phi - n) - [\phi, \omega] + \nu \, \nabla^{2N} \omega Potential equation: .. math:: \Delta \phi = \omega where :math:`[\phi, n] = \partial_x \phi \partial_y n - \partial_y \phi \partial_x n`, :math:`C = C(x, y)` and :math:`\kappa` and :math:`\nu` are constants (at the moment only :math:`N=1` is available). """
[docs] @classmethod def doc_normalization(cls): r"""The electrostatic potential is scaled by the thermal-speed unit, .. math:: \hat u = \hat v_\mathrm{th},\qquad \hat\phi = \hat u \hat x. """
[docs] @classmethod def doc_scalar_quantities(cls): r"""**The following scalars are tracked during simulation:** - No default scalar diagnostics are defined by this model."""
[docs] @classmethod def doc_discretization(cls): """Time integration is performed by the following propagators (in sequence): 1. :class:`~struphy.propagators.poisson_solve.PoissonSolve` 2. :class:`~struphy.propagators.hasegawa_wakatani_step.HasegawaWakataniStep` """ doc = rf"""**1. PoissonFieldSolve:** {PoissonSolve.__doc__} **2. HasegawaWakataniStep:** {HasegawaWakataniStep.__doc__} """ return doc
[docs] @classmethod def doc_long_description(cls): r"""This is a reduced 2D drift-wave turbulence model. The electrostatic potential is recovered from the vorticity at each step, and the coupled density-vorticity dynamics capture resistive drift-wave behavior without resolving full kinetic physics."""
[docs] @classmethod def doc_examples(cls): r"""Create and initialize a Hasegawa-Wakatani model: .. code-block:: python from struphy.models import HasegawaWakatani model = HasegawaWakatani() model.em_fields.phi model.plasma.density model.plasma.vorticity """
[docs] @classmethod def doc_use_cases(cls): r"""This model is appropriate for: - 2D resistive drift-wave turbulence studies - reduced electrostatic edge-plasma benchmarks - testing Poisson-coupled advection-diffusion solvers"""
[docs] @classmethod def doc_cannot_be_used_for(cls): r"""This model is not suitable for: - three-dimensional electromagnetic turbulence - full kinetic Landau or cyclotron physics - multi-species warm-fluid closures beyond the reduced HW system - self-consistent magnetic-field evolution"""