Source code for struphy.models.vlasov_pauli

import logging

from maybempi import MPI

from struphy import BaseUnits
from struphy.models.scalars import FunctionScalarFEEC, FunctionScalarPIC
from struphy.models.vlasov import Vlasov
from struphy.propagators.base import Propagator
from struphy.propagators.push_v_in_force_field import PushVinForceField

logger = logging.getLogger("struphy")


class PauliMixin:
    """Scalars and parameter-file defaults shared by the models with classical Pauli particles
    (:class:`~struphy.models.VlasovPauli`, :class:`~struphy.models.VlasovPoissonAdiabaticPauli`)."""

    def _pauli_scalars(self, scalars: dict) -> dict:
        """Add the magnetic-moment energy ``en_mu`` (also to ``en_tot``) and ``mu_prime_max`` to the scalars of the
        parent model."""
        scalars.pop("en_tot", None)
        scalars["en_mu"] = FunctionScalarPIC(self._compute_en_mu, self.kinetic_ions.var)
        terms = [scalars[name] for name in ("en_phi", "en_f", "en_mu") if name in scalars]
        en_tot = terms[0]
        for term in terms[1:]:
            en_tot = en_tot + term
        scalars["en_tot"] = en_tot
        scalars["mu_prime_max"] = FunctionScalarFEEC(self._compute_mu_prime_max)
        return scalars

    def _compute_en_mu(self):
        r"""Local part of the magnetic-moment energy :math:`\sum_p w_p \mu_p |\mathbf B_0(\mathbf x_p)|`."""
        particles = self.kinetic_ions.var.particles
        if particles.n_mks_loc == 0:
            return 0.0
        _, absB = particles._unit_b_and_absB(particles.positions)
        return float((particles.weights * particles.magnetic_moments * absB).sum())

    def _compute_mu_prime_max(self):
        r"""Maximum over all markers of :math:`\mu'_p = |\mathbf v_p \times \mathbf b_0|^2 / (2 |\mathbf B_0|)`."""
        particles = self.kinetic_ions.var.particles
        mu_prime = particles.mu_prime()
        value = float(mu_prime.max()) if mu_prime.size > 0 else 0.0
        # maximum within the clone and between clones
        comm = Propagator.derham.comm
        if comm is not None and comm.Get_size() > 1:
            value = comm.allreduce(value, op=MPI.MAX)
        if particles.clone_config is not None:
            value = particles.clone_config.inter_comm.allreduce(value, op=MPI.MAX)
        return value

    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 "BaseUnits(" in line:
                    new_file += ["base_units = BaseUnits(kBT=1.0)\n"]
                elif "push_vxb.Options" in line:
                    new_file += [
                        "model.propagators.push_vxb.options = model.propagators.push_vxb.Options(algo='implicit')\n"
                    ]
                elif "time_opts = Time(" in line:
                    new_file += ["time_opts = Time(split_algo='Strang')\n"]
                else:
                    new_file += [line]

        with open(params_path, "w") as f:
            for line in new_file:
                f.write(line)
        return params_path


[docs] class VlasovPauli(PauliMixin, Vlasov): r"""Guiding-center orbits in a static magnetic field, computed from classical Pauli particles. The particles are full-orbit (6D) markers with a magnetic moment (classical Pauli particles, Xiao and Qin 2021), loaded on the slow manifold without gyration. Extends :class:`~struphy.models.Vlasov` by the magnetic-moment (mirror) force :math:`-\mu \nabla |\mathbf B_0|`. The markers follow guiding-center orbits with full-orbit pushers, with time steps larger than the cyclotron period. With the self-consistent electrostatic potential and adiabatic electrons, see :class:`~struphy.models.VlasovPoissonAdiabaticPauli`. Parameters ---------- base_units : BaseUnits Base units for normalization (default: ``BaseUnits(kBT=1.0)``). charge_number : int Charge number (in units of the positive elementary charge) of the species (default: 1). mass_number : float Mass number (in units of the proton mass) of the species (default: 1.0). epsilon : float, optional Normalized inverse cyclotron frequency :math:`1 / (\hat{\Omega}_\mathrm{c} \hat{t})`. If ``None``, computed from ``base_units`` and the charge/mass numbers. """ ## changed species class KineticIons(Vlasov.KineticIons): space = "PauliParticles6D" ## additional propagators class Propagators(Vlasov.Propagators): """Propagators of :class:`~struphy.models.Vlasov` plus ``push_v_mu`` (magnetic-moment force). Parameters ---------- **kwargs Passed to :class:`Vlasov.Propagators <struphy.models.Vlasov.Propagators>`. """ # with Strang splitting the last propagator (push_vxb) makes one full step between the two half kicks, # i.e. the Boris scheme for push_vxb with algo="implicit" splitting_order = ("push_eta", "push_v_mu", "push_vxb") def __init__(self, **kwargs): super().__init__(**kwargs) self.push_v_mu = PushVinForceField(magnetic_moment=True) # attribute types for IDEs and type checkers: the classes of this model, not those of the parent kinetic_ions: KineticIons propagators: Propagators ## abstract methods def __init__( self, base_units: BaseUnits = BaseUnits(kBT=1.0), charge_number: int = 1, mass_number: float = 1.0, epsilon: float = None, ): # 0. store input parameters self._store_params(locals()) # 1.-5. see Vlasov super().__init__(base_units, charge_number, mass_number) def _link_propagator_variables(self): """:meta private:""" super()._link_propagator_variables() self.propagators.push_v_mu.variables.var = self.kinetic_ions.var def _create_scalars(self): """:meta private:""" return self._pauli_scalars(super()._create_scalars()) @property def velocity_scale(self): return "thermal" ## abstract methods for documentation
[docs] @classmethod def doc_pde(cls): r"""**PDEs solved by model:** Vlasov equation for classical Pauli particles in a static magnetic field :math:`\mathbf B_0`, with the magnetic moment :math:`\mu \geq 0` as a parameter of the distribution function :math:`f(\mathbf{x}, \mathbf{v}, \mu, t)`: .. math:: \frac{\partial f}{\partial t} + \mathbf{v} \cdot \nabla f + \left( \frac{1}{\varepsilon} \mathbf{v} \times \mathbf{B}_0 - \mu \nabla |\mathbf{B}_0| \right) \cdot \frac{\partial f}{\partial \mathbf{v}} = 0\,, with :math:`f` concentrated on the slow manifold :math:`\mathbf v \times \mathbf b_0 \approx 0`. **Conserved quantities:** the number of particles and the energy .. math:: \frac{\textrm d}{\textrm d t} \int_0^\infty \int_{\mathbb{R}^3} \int_\Omega \left( \frac{1}{2} |\mathbf v|^2 + \mu |\mathbf B_0| \right) f \, \textrm d \mathbf x \, \textrm d^3 \mathbf v \, \textrm d \mu = 0\,, for periodic boundary conditions or particles that do not leave the domain. """
[docs] @classmethod def doc_normalization(cls): r"""The reference speed is the thermal speed, .. math:: \hat v = \sqrt{k_B \hat T / m}\,,\qquad \hat \mu = \frac{m \hat v^2}{\hat B}\,, with the dimensionless parameter :math:`\varepsilon = 1/(\hat\Omega_c\hat t)`. The normalized magnetic moment is :math:`\mu = v_\perp^2 / (2 |\mathbf B_0|)`, as in the guiding-center models."""
[docs] @classmethod def doc_scalar_quantities(cls): r"""**The following scalars are tracked during simulation** (the sum of the energies is conserved by the model equations): - Kinetic energy: ``en_f`` :math:`= \frac{1}{2} \sum_p w_p |\mathbf{v}_p|^2` - Magnetic-moment energy: ``en_mu`` :math:`= \sum_p w_p \mu_p |\mathbf B_0(\mathbf x_p)|` - Total energy: ``en_tot`` = ``en_f + en_mu`` - Distance from the slow manifold: ``mu_prime_max`` :math:`= \max_p |\mathbf v_p \times \mathbf b_0|^2 / (2 |\mathbf B_0|)`, Eq. (7) of Xiao and Qin; it must stay small compared to the magnetic moments."""
[docs] @classmethod def doc_discretization(cls): """Time integration is performed by the following propagators (in sequence). With Strang splitting, a time step is a drift by dt/2, a kick by dt/2 with the magnetic-moment force -mu grad |B0|, the rotation by v x B0 over the full dt, a second kick by dt/2 and a drift by dt/2. With ``PushVxB(algo="implicit")`` (Crank-Nicolson, i.e. the Cayley rotation) this is the Boris scheme of the classical Pauli particle (BAP2 in Xiao and Qin 2021), which gives the grad-B and curvature drifts for any dt / epsilon. The exact rotation (``algo="analytic"``) and Lie-Trotter splitting do not; they need dt < epsilon. The markers are loaded with :class:`~struphy.pic.particles.PauliParticles6D`: the magnetic moment of each marker is computed from its drawn perpendicular velocity, which is then set to zero.""" return cls.propagators_doc()
[docs] @classmethod def doc_long_description(cls): r"""Guiding-center orbits computed with the slow manifold of the classical Pauli particle (J. Xiao and H. Qin, "Slow manifolds of classical Pauli particle enable structure-preserving geometric algorithms for guiding center dynamics", Comput. Phys. Commun. 265, 107981 (2021)). A classical Pauli particle is a charged particle with an intrinsic magnetic moment :math:`\mu`, with the Lagrangian :math:`L = \frac{1}{2}|\dot{\mathbf x}|^2 + \dot{\mathbf x} \cdot \mathbf A - \mu B`. When its perpendicular velocity vanishes initially, its effective magnetic moment :math:`\mu' = |\dot{\mathbf x} \times \mathbf b|^2 / (2B)` is an adiabatic invariant and stays close to zero: the particle moves on a slow manifold, along which its position follows the guiding-center dynamics of a particle with magnetic moment :math:`\mu`. Unlike the guiding-center Lagrangian, the Lagrangian of the classical Pauli particle is regular, so the full-orbit Boris scheme can be used with time steps larger than the cyclotron period."""
[docs] @classmethod def doc_examples(cls): r"""Guiding-center orbits in the equilibrium magnetic field: .. code-block:: python from struphy.models import VlasovPauli model = VlasovPauli(epsilon=0.01) model.kinetic_ions.var # Boris rotation (Cayley transform) for time steps larger than the cyclotron period model.propagators.push_vxb.options = model.propagators.push_vxb.Options(algo="implicit") """
[docs] @classmethod def doc_use_cases(cls): r"""This model is appropriate for: - guiding-center orbits (banana orbits, grad-B and curvature drifts) in general magnetic fields, with a full-orbit pusher and time steps larger than the cyclotron period - verification of guiding-center pushers"""
[docs] @classmethod def doc_cannot_be_used_for(cls): r"""This model is not suitable for: - self-consistent fields (use :class:`~struphy.models.VlasovPoissonAdiabaticPauli` for the electrostatic case) - finite-Larmor-radius effects (use :class:`~struphy.models.Vlasov`) - magnetic fields without scale separation, where the magnetic moment is not an adiabatic invariant - time-dependent magnetic fields"""