Source code for struphy.models.linear_mhd_vlasov_pc

import copy
import logging

from struphy import BaseUnits
from struphy.io.options import LiteralOptions
from struphy.models.base import StruphyModel
from struphy.models.scalars import BilinearEnergyFEEC, KineticEnergyPIC, LostMarkersPIC, Scalars, VolumeFormEnergyFEEC
from struphy.models.species import (
    FieldSpecies,
    FluidSpecies,
    ParticleSpecies,
)
from struphy.models.variables import FEECVariable, PICVariable
from struphy.polar.basic import PolarVector
from struphy.propagators.base import Propagator
from struphy.propagators.magnetosonic import Magnetosonic
from struphy.propagators.pressure_coupling_6d import PressureCoupling6D
from struphy.propagators.push_eta_pc import PushEtaPC
from struphy.propagators.push_vxb import PushVxB
from struphy.propagators.shear_alfven_propagator import ShearAlfvenPropagator

logger = logging.getLogger("struphy")


[docs] class LinearMHDVlasovPC(StruphyModel): """Hybrid linear MHD coupled with energetic ions (6D Vlasov) via the pressure-coupling scheme. Parameters ---------- base_units: BaseUnits Base units for normalization (default: BaseUnits()) mhd_mass_number: float Mass number (in units of Proton mass) of the MHD bulk species (default: 1.0) hot_charge_number: int Charge number (in units of the positive elementary charge) of the energetic ion species (default: 1) hot_mass_number: float Mass number (in units of Proton mass) of the energetic ion species (default: 1.0) hot_epsilon: float, optional Normalized cyclotron period of the energetic ion species. If None, computed from units and charge/mass numbers. turn_off: tuple[str, ...] Names of coupling terms to turn off (default: (None,)) """ @classmethod def model_type(cls) -> LiteralOptions.ModelTypes: return "Hybrid" ## species class EnergeticIons(ParticleSpecies): def __init__( self, charge_number: int = 1, mass_number: float = 1.0, epsilon: float = None, ): self.var = PICVariable(space="Particles6D") self.init_variables( charge_number=charge_number, mass_number=mass_number, epsilon=epsilon, ) class EMFields(FieldSpecies): def __init__(self): self.b_field = FEECVariable(space="Hdiv") self.init_variables() class MHD(FluidSpecies): def __init__(self, mass_number: float = 1.0): self.density = FEECVariable(space="L2") self.pressure = FEECVariable(space="L2") self.velocity = FEECVariable(space="Hdiv") self.init_variables(mass_number=mass_number) ## propagators class Propagators: def __init__( self, turn_off: tuple[str, ...] = (None,), b2_var: FEECVariable = None, b_field: FEECVariable = None, u_tilde: FEECVariable = None, ): if "PushEtaPC" not in turn_off: self.push_eta_pc = PushEtaPC(u_tilde=u_tilde) if "PushVxB" not in turn_off: self.push_vxb = PushVxB(b2_var=b2_var) if "PressureCoupling6D" not in turn_off: self.pc6d = PressureCoupling6D() if "ShearAlfven" not in turn_off: self.shearalfven = ShearAlfvenPropagator() if "Magnetosonic" not in turn_off: self.magnetosonic = Magnetosonic(b_field=b_field) def __init__( self, base_units: BaseUnits = BaseUnits(), mhd_mass_number: float = 1.0, hot_charge_number: int = 1, hot_mass_number: float = 1.0, hot_epsilon: float = None, turn_off: tuple[str, ...] = (None,), ): # 0. store input parameters self.params = copy.deepcopy(locals()) # 1. instantiate all species self.em_fields = self.EMFields() self.mhd = self.MHD(mhd_mass_number) self.energetic_ions = self.EnergeticIons( hot_charge_number, hot_mass_number, hot_epsilon, ) # 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( turn_off, b2_var=self.em_fields.b_field, b_field=self.em_fields.b_field, u_tilde=self.mhd.velocity, ) # 4. assign variables to propagators if "ShearAlfven" not in turn_off: self.propagators.shearalfven.variables.u = self.mhd.velocity self.propagators.shearalfven.variables.b = self.em_fields.b_field if "Magnetosonic" not in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure if "PressureCoupling6D" not in turn_off: self.propagators.pc6d.variables.u = self.mhd.velocity self.propagators.pc6d.variables.energetic_ions = self.energetic_ions.var if "PushEtaPC" not in turn_off: self.propagators.push_eta_pc.variables.var = self.energetic_ions.var if "PushVxB" not in turn_off: self.propagators.push_vxb.variables.ions = self.energetic_ions.var # 5. define scalars to be tracked during simulation kinetic_energy = BilinearEnergyFEEC(self.mhd.velocity, bilinear_form_name="M2n") pressure_energy = VolumeFormEnergyFEEC(self.mhd.pressure, normalization=1.0 / (5 / 3 - 1)) magnetic_energy = BilinearEnergyFEEC(self.em_fields.b_field) Ab = self.mhd.mass_number Ah = self.energetic_ions.var.species.mass_number particle_energy = KineticEnergyPIC(self.energetic_ions.var, normalization=Ah / Ab) self.scalars = Scalars( en_U=kinetic_energy, en_p=pressure_energy, en_B=magnetic_energy, en_f=particle_energy, en_tot=kinetic_energy + pressure_energy + magnetic_energy + particle_energy, n_lost_particles=LostMarkersPIC(self.energetic_ions.var), ) @property def bulk_species(self): return self.mhd @property def velocity_scale(self): return "alfvén" def post_allocate(self): self._ones = Propagator.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0
[docs] @classmethod def doc_pde(cls): r"""**PDEs solved by model:** MHD continuity: .. math:: \frac{\partial \tilde{\rho}}{\partial t} + \nabla \cdot (\rho_0 \tilde{\mathbf{U}}) = 0 MHD momentum: .. math:: \rho_0 \frac{\partial \tilde{\mathbf{U}}}{\partial t} + \nabla \tilde{p} + \frac{A_\textnormal{h}}{A_\textnormal{b}} \nabla \cdot \tilde{\mathbb{P}}_{\textnormal{h},\perp} = (\nabla \times \tilde{\mathbf{B}}) \times \mathbf{B}_0 + \mathbf{J}_0 \times \tilde{\mathbf{B}} .. math:: \mathbf{J}_0 = \nabla \times \mathbf{B}_0 MHD pressure: .. math:: \frac{\partial \tilde{p}}{\partial t} + \nabla \cdot (p_0 \tilde{\mathbf{U}}) + \frac{2}{3} p_0 \nabla \cdot \tilde{\mathbf{U}} = 0 MHD induction: .. math:: \frac{\partial \tilde{\mathbf{B}}}{\partial t} - \nabla \times (\tilde{\mathbf{U}} \times \mathbf{B}_0) = 0 Energetic-particle Vlasov equation: .. math:: \frac{\partial f_\textnormal{h}}{\partial t} + (\mathbf{v} + \tilde{\mathbf{U}}_\perp) \cdot \nabla f_\textnormal{h} + \left[ \frac{1}{\epsilon} \mathbf{v} \times (\mathbf{B}_0 + \tilde{\mathbf{B}}) - \nabla \tilde{\mathbf{U}}_\perp \cdot \mathbf{v} \right] \cdot \frac{\partial f_\textnormal{h}}{\partial \mathbf{v}} = 0 Perpendicular pressure tensor: .. math:: \tilde{\mathbb{P}}_{\textnormal{h},\perp} = \int \mathbf{v}_\perp \mathbf{v}_\perp^\top f_\textnormal{h} \, d \mathbf{v} """
[docs] @classmethod def doc_normalization(cls): r"""Fluid and hot-particle velocities are normalized with the bulk Alfvén speed. The kinetic pressure tensor is scaled consistently with :math:`A_h m_H \hat n \hat v_A^2`, and the hot cyclotron parameter is :math:`\varepsilon`."""
[docs] @classmethod def doc_scalar_quantities(cls): r"""**The following scalars are tracked during simulation:** - MHD kinetic energy: ``en_U`` - Thermal pressure energy: ``en_p`` - Magnetic energy: ``en_B`` - Energetic-particle kinetic energy: ``en_f`` - Total energy: ``en_tot`` - Lost particles: ``n_lost_particles``"""
[docs] @classmethod def doc_discretization(cls): """Time integration is performed by the following propagators (in sequence): 1. :class:`~struphy.propagators.push_eta_pc.PushEtaPC` 2. :class:`~struphy.propagators.push_vxb.PushVxB` 3. :class:`~struphy.propagators.pressure_coupling_6d.PressureCoupling6D` 4. :class:`~struphy.propagators.shear_alfven_propagator.ShearAlfvenPropagator` 5. :class:`~struphy.propagators.magnetosonic.Magnetosonic` """ doc = rf"""**1. push_eta_pc.PushEtaPC:** {PushEtaPC.__doc__} **2. push_vxb.PushVxB:** {PushVxB.__doc__} **3. pressure_coupling_6d.PressureCoupling6D:** {PressureCoupling6D.__doc__} **4. ShearAlfvenPropagator:** {ShearAlfvenPropagator.__doc__} **5. Magnetosonic:** {Magnetosonic.__doc__} """ return doc
[docs] @classmethod def doc_long_description(cls): r"""LinearMHDVlasovPC is the pressure-coupling counterpart to the current-coupling hybrid model. It is targeted at linear problems where energetic-particle pressure anisotropy is the relevant feedback channel on the bulk MHD dynamics."""
[docs] @classmethod def doc_examples(cls): r"""Create and initialize the linear MHD-Vlasov pressure-coupling model: .. code-block:: python from struphy.models import LinearMHDVlasovPC model = LinearMHDVlasovPC() model.em_fields.b_field model.mhd.velocity model.energetic_ions.var """
[docs] @classmethod def doc_use_cases(cls): r"""This model is appropriate for: - linear pressure-coupling hybrid studies - energetic-particle pressure feedback on MHD modes - verification of PushEtaPC and pressure-coupling operators"""
[docs] @classmethod def doc_cannot_be_used_for(cls): r"""This model is not suitable for: - current-coupling closure studies - nonlinear hybrid turbulence - dissipative/resistive MHD - fully kinetic treatment of the bulk plasma"""