Skip to content

Commit

Permalink
Implement average charge state equilibria.
Browse files Browse the repository at this point in the history
Electron temperature dependent. Uses a polynomial fit for a selection of common impurities based on Mavrin 2018, including heavy impurities.

* Adds a new routine to calculate average impurity charge state for an IonMixture.

* Modifies the DynamicIonMixture dataclass to hold a separate Z_override attribute instead of calculating the average itself. This is used in the charge state calculation function.

* Updates core_profile_setters to calculate and return the Zimp/Zi profiles according to the new routines

* Better separation of ne and ion density + charge state setting

* Updates Qualikiz to use the new Zimp/Zi arrays

* Adds average Zimp to default output plots

* Updates boundary condition calculations to calculate boundary Zimp according to the boundary temperature.

* New sim integration test with tungsten added.

* Some sim tests which did not have a Z_override now have spatially varying Zimp, which leads to small O(1e-4) changes. The small impact is because the default impurity is Neon.

Small <5% runtime increase. This was ascertained to not be due to the (jitted) charge state calculations themselves, but due to surrounding code in non-jitted functions in the sim loop which call the charge state calculations. This will be rectified in an upcoming PR.

~10% compilation time increase due to added dependency of nimp and zimp on electron temperature.

PiperOrigin-RevId: 715335274
  • Loading branch information
jcitrin authored and Torax team committed Jan 15, 2025
1 parent a475745 commit 7e81ff8
Show file tree
Hide file tree
Showing 62 changed files with 1,469 additions and 366 deletions.
122 changes: 103 additions & 19 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,19 @@ The following inputs are valid for **time-varying-scalar** parameters:

Examples:

1. Define a time-dependent :math:`Z_{eff}` with piecewise linear interpolation, from :math:`t=2` to :math:`t=15`.
:math:`Z_{eff}` drops from 2.8 to 1.5, and then stays flat.
1. Define a time-dependent total current :math:`Ip_{tot}` with piecewise linear interpolation,
from :math:`t=10` to :math:`t=100`. :math:`Ip_{tot}` rises from 2 to 15, and then stays flat
due to constant extrapolation beyond the last time value.

.. code-block:: python
Zeff = ({2: 2.8, 5: 2.0, 8: 1.5, 15: 1.5}, 'PIECEWISE_LINEAR')
Ip_tot = ({10: 2.0, 100: 15.0}, 'PIECEWISE_LINEAR')
or more simply, taking advantage of the default.

.. code-block:: python
Zeff = {2: 2.8, 5: 2.0, 8: 1.5, 15: 1.5}
Ip_tot = {10: 2.0, 100: 15.0}
2. Define a time dependent internal boundary condition for ion temperature, ``Tiped``, with stepwise changes,
starting at :math:`1~keV`` at :math:`t=2s`, transitioning to :math:`3~keV`` at :math:`t=8s`, and back down
Expand Down Expand Up @@ -189,24 +190,107 @@ runtime_params
plasma_composition
^^^^^^^^^^^^^^^^^^

Defines the distribution of ion species. Currently restricted to a single main ion, a single impurity and a flat :math:`Z_{eff}`.
Defines the distribution of ion species. The keys and their meanings are as follows:

``Ai`` (float = 2.5)
Mass of main ion in amu units. For multiple-isotope plasmas, make an effective average.
``main_ion`` (str or dict = ``{'D': 0.5, 'T': 0.5}``)
Specifies the main ion species.

``Zi`` (float = 1.0):
Charge of main ion in units of electron charge.
* If a string, it represents a single ion species (e.g., ``'D'`` for deuterium, ``'T'`` for tritium, ``'H'`` for hydrogen). See below for the full list of supported ions.
* If a dict, it represents an ``IonMixture``, a mixture of ion species with given fractions.
The keys of the dict are the ion symbols, and the values are their fractional concentrations
(which must sum to 1 within a tolerance of 1e-6). The fractions can be time-dependent, i.e. are **time-varying-scalar**,
which supports variations of the ion mixture (e.g. mix of hydrogenic isotopes).

``Zimp`` (float = 10.0), **time-varying-scalar**
Impurity charge state.
``impurity`` (str or dict = ``'Ne'``), **time-varying-scalar**
Specifies the impurity species, following the same syntax as ``main_ion``. A single effective impurity species
is currently supported, although multiple impurities can still be defined through the IonMixture API.

``Aimp`` (float = 20.18), **time-varying-scalar**
Impurity mass in amu units.

``Zeff`` (float = 1.0), **time-varying-scalar**
``Zeff`` (float = 1.0), **time-varying-array**
Plasma effective charge, defined as :math:`Z_{eff}=\sum_i Z_i^2 \hat{n}_i`, where :math:`\hat{n}_i` is
the normalized ion density :math:`n_i/n_e`. For a given :math:`Z_{eff}` and :math:`Z_{imp}`, a consistent :math:`\hat{n}_i` is calculated,
with the appropriate degree of main ion dilution.
the normalized ion density :math:`n_i/n_e`. For a given :math:`Z_{eff}` and impurity charge states,
a consistent :math:`\hat{n}_i` is calculated, with the appropriate degree of main ion dilution.

``Zi_override`` (float, optional = None), **time-varying-scalar**
An optional override for the main ion's charge (Z) or average charge of an IonMixture.
If provided, this value will be used instead of the Z calculated from the ``main_ion`` specification.

``Ai_override`` (float, optional = None), **time-varying-scalar**
An optional override for the main ion's mass (A) in amu units or average mass of an IonMixture.
If provided, this value will be used instead of the A calculated from the ``main_ion`` specification.

``Zimp_override`` (float, optional), **time-varying-scalar**
As ``Zi_override``, but for the impurity ion. If provided, this value will be used instead of the Z calculated
from the ``impurity`` specification.

``Aimp_override`` (float, optional), **time-varying-scalar**
As ``Ai_override``, but for the impurity ion. If provided, this value will be used instead of the A calculated
from the ``impurity`` specification.

The average charge state of each ion in each mixture is determined by `Mavrin polynomials <https://doi.org/10.1080/10420150.2018.1462361>`_,
which are fitted to atomic data, and in the temperature ranges of interest in the tokamak core,
are well approximated as 1D functions of electron temperature. All ions with atomic numbers below
Carbon are assumed to be fully ionized.

Examples
--------

We remind that for all cases below, the impurity density is solely constrained by
the input ``Zeff`` value and the impurity charge state, presently assumed to be fully ionized.
Imminent development will support temperature-dependent impurity average charge states,

* Pure deuterium plasma:

.. code-block:: python
'plasma_composition': {
'main_ion': 'D',
'impurity': 'Ne', # Neon
'Zeff': 1.5,
}
* 50-50 DT mixture:

.. code-block:: python
'plasma_composition': {
'main_ion': {'D': 0.5, 'T': 0.5},
'impurity': 'Be', # Beryllium
'Zeff': 1.8,
}
* Time-varying DT mixture:

.. code-block:: python
'plasma_composition': {
'main_ion': {
'D': {0.0: 0.1, 5.0: 0.9}, # D fraction from 0.1 to 0.9
'T': {0.0: 0.9, 5.0: 0.1}, # T fraction from 0.9 to 0.1
},
'impurity': 'W', # Tungsten
'Zeff': 2.0,
}
Allowed ion symbols
-------------------

The following ion symbols are recognized for ``main_ion`` and ``impurity`` input fields.

* H (Hydrogen)
* D (Deuterium)
* T (Tritium)
* He3 (Helium-3)
* He4 (Helium-4)
* Li (Lithium)
* Be (Beryllium)
* C (Carbon)
* N (Nitrogen)
* O (Oxygen)
* Ne (Neon)
* Ar (Argon)
* Kr (Krypton)
* Xe (Xenon)
* W (Tungsten)

Profile conditions
^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -1240,9 +1324,9 @@ The configuration file is also available in ``torax/examples/iterhybrid_rampup.p
CONFIG = {
'runtime_params': {
'plasma_composition': {
'Ai': 2.5,
'main_ion': {'D': 0.5, 'T': 0.5},
'impurity': 'Ne',
'Zeff': 1.6,
'Zimp': 10,
},
'profile_conditions': {
'Ip_tot': {0: 3, 80: 10.5},
Expand Down
3 changes: 3 additions & 0 deletions docs/equation_summary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ TORAX solves coupled 1D PDEs in normalized toroidal flux coordinates,
g_0q_i^{\mathrm{conv}}T_i\right] + Q_i
\end{multline}
If multiple main ion species are present (e.g., a D-T mix), then :math:`n_i` represents the sum of all
main ions, and ion attributes like charge and mass are averaged values for the mixture, weighted by fractional abundance.

Electron heat transport, governing the evolution of the electron temperature :math:`T_e`.

.. math::
Expand Down
27 changes: 20 additions & 7 deletions docs/physics_models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,29 @@ Generalization to geometry data beyond CHEASE is also planned.
Plasma composition, initial and prescribed conditions
=====================================================

Presently, TORAX only accommodates a single main ion species,configured with its
atomic mass number (:math:`A_i`) and charge state (:math:`Z_i`). The plasma effective
charge, :math:`Z_\textit{eff}`, is assumed to be radially flat and is also
user-configurable. A single impurity with charge state :math:`Z_\textit{imp}` is
specified to accommodate :math:`Z_\textit{eff} > 1`. The main ion density dilution
is then calculated as follows:
Presently, TORAX accommodates a single main ion species and single impurity species,
which can be comprised of time-dependent mixtures of ions with fractional abundances
summing to 1. This is useful for example for simulating isotope mixes. Based on the
ion symbols and fractional abundances, the average mass of each species is determined.
The average charge state of each ion in each mixture is determined by `Mavrin polynomials <https://doi.org/10.1080/10420150.2018.1462361>`_,
which are fitted to atomic data, and in the temperature ranges of interest in the tokamak core,
are well approximated as 1D functions of electron temperature. All ions with atomic numbers below
Carbon are assumed to be fully ionized.

The impurity and main ion densities are constrained by the plasma effective
charge, :math:`Z_\mathrm{eff}`, which is a user-provided 2D array in both time and space,
as well as quasineutrality.

:math:`n_i`, and :math:`n_{imp}`, are solved from the
following system of equations, where :math:`Z_\mathrm{eff}` and the electron density are
known, and :math:`Z_\mathrm{imp}` is the average impurity charge of the impurity mixture,
with the average charge state for each ion determined from the Mavrin polynomials.

.. math::
n_i=(Z_\textit{imp}-Z_\textit{eff})/(Z_\textit{imp}-1)n_e
n_\mathrm{i}Z_\mathrm{i}^2 + n_\mathrm{imp}Z_\mathrm{imp}^2 = n_\mathrm{e}Z_\mathrm{eff}
n_\mathrm{i}Z_\mathrm{i} + n_\mathrm{imp}Z_\mathrm{imp} = n_\mathrm{e}
Initial conditions for the evolving profiles :math:`T_i`, :math:`T_e`, :math:`n_e`,
and :math:`\psi` are user-configurable. For :math:`T_{i,e}`, both the initial core
Expand Down
1 change: 1 addition & 0 deletions torax/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from torax.config.build_sim import build_sim_from_config
from torax.config.config_loader import import_module
from torax.interpolated_param import InterpolatedVarSingleAxis
from torax.interpolated_param import InterpolationMode
from torax.output import ToraxSimOutputs
from torax.sim import Sim
from torax.state import SimError
Expand Down
162 changes: 162 additions & 0 deletions torax/charge_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Copyright 2024 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Routines for calculating impurity charge states."""

import typing
from jax import numpy as jnp
import numpy as np
from torax import array_typing
from torax import constants
from torax.config import plasma_composition

# Polynomial fit coefficients from A. A. Mavrin (2018):
# Improved fits of coronal radiative cooling rates for high-temperature plasmas,
# Radiation Effects and Defects in Solids, 173:5-6, 388-398,
# DOI: 10.1080/10420150.2018.1462361
MAVRIN_Z_COEFFS = {
'C': np.array([ # Carbon
[5.8588e00, -1.7632e00, -7.3521e00, -1.2217e01, -7.2007e00],
[6.0000e00, 0.0000e00, 0.0000e00, 0.0000e00, 0.0000e00],
]),
'N': np.array([ # Nitrogen
[6.9728e00, 1.5668e-01, 1.8861e00, 3.3818e00, 0.0000e00],
[7.0000e00, 0.0000e00, 0.0000e00, 0.0000e00, 0.0000e00],
]),
'O': np.array([ # Oxygen
[4.0451e00, -2.2093e01, -3.8664e01, -1.8560e01, 0.0000e00],
[7.9878e00, 8.0180e-02, -3.7050e-02, -4.6261e-01, -4.3092e00],
[8.0000e00, 0.0000e00, 0.0000e00, 0.0000e00, 0.0000e00],
]),
'Ne': np.array([ # Neon
[8.9737e00, -1.3242e01, -5.3631e01, -6.4696e01, -2.5303e01],
[9.9532e00, 2.1413e-01, -8.0723e-01, 3.6868e00, -7.0678e00],
[1.0000e01, 0.0000e00, 0.0000e00, 0.0000e00, 0.0000e00],
]),
'Ar': np.array([ # Argon
[1.3171e01, -2.0781e01, -4.3776e01, -1.1595e01, 6.8717e00],
[1.5986e01, 1.1413e00, 2.5023e00, 1.8455e00, -4.8830e-02],
[1.4948e01, 7.9986e00, -8.0048e00, 3.5667e00, -5.9213e-01],
]),
'Kr': np.array([ # Krypton
[7.7040e01, 3.0638e02, 5.6890e02, 4.6320e02, 1.3630e02],
[2.4728e01, 1.5186e00, 1.5744e01, 6.8446e01, -1.0279e02],
[2.5368e01, 2.3443e01, -2.5703e01, 1.3215e01, -2.4682e00],
]),
'Xe': np.array([ # Xenon
[3.0532e02, 1.3973e03, 2.5189e03, 1.9967e03, 5.8178e02],
[3.2616e01, 1.6271e01, -4.8384e01, -2.9061e01, 8.6824e01],
[4.8066e01, -1.7259e02, 6.6739e02, -9.0008e02, 4.0756e02],
[-5.7527e01, 2.4056e02, -1.9931e02, 7.3261e01, -1.0019e01],
]),
'W': np.array([ # Tungsten
[2.6703e01, 1.6518e01, 2.1027e01, 3.4582e01, 1.6823e01],
[3.6902e01, -7.9611e01, 2.5532e02, -1.0577e01, -2.5887e02],
[6.3795e01, -1.0011e02, 1.5985e02, -8.4207e01, 1.5119e01],
]),
}

# Temperature boundaries in keV, separating the rows for the fit coefficients.
TEMPERATURE_INTERVALS = {
'C': np.array([0.7]),
'N': np.array([0.7]),
'O': np.array([0.3, 1.5]),
'Ne': np.array([0.5, 2.0]),
'Ar': np.array([0.6, 3.0]),
'Kr': np.array([0.447, 4.117]),
'Xe': np.array([0.3, 1.5, 8.0]),
'W': np.array([1.5, 4.0]),
}


# pylint: disable=invalid-name
def calculate_average_charge_state_single_species(
Te: array_typing.ArrayFloat,
ion_symbol: str,
) -> array_typing.ArrayFloat:
"""Calculates the average charge state of an impurity based on the Marvin 2018 polynomial fit.
The polynomial fit range is 0.1-100 keV, which is well within the typical
bounds of core tokamak modelling. For safety, inputs are clipped to avoid
extrapolation outside this range.
Args:
Te: Electron temperature [keV].
ion_symbol: Species to calculate average charge state for. Concrete values.
Returns:
Z: Average charge state [amu].
"""

allowed_symbols = typing.get_args(constants.ION_SYMBOLS)
if ion_symbol not in allowed_symbols:
raise ValueError(
f'Invalid ion symbol: {ion_symbol}. Allowed symbols are :'
f' {allowed_symbols}'
)
# Return the Z value for light ions that are fully ionized for T > 0.1 keV.
if ion_symbol not in MAVRIN_Z_COEFFS:
return jnp.ones_like(Te) * constants.ION_PROPERTIES_DICT[ion_symbol].Z

# Avoid extrapolating fitted polynomial out of bounds.
Te_allowed_range = (0.1, 100.0)
Te = jnp.clip(Te, *Te_allowed_range)

# Gather coefficients for each temperature
interval_indices = jnp.searchsorted(TEMPERATURE_INTERVALS[ion_symbol], Te)
Zavg_coeffs_in_range = jnp.take(
MAVRIN_Z_COEFFS[ion_symbol], interval_indices, axis=0
).transpose()

def _calculate_in_range(X, coeffs):
"""Return Mavrin 2018 Zavg polynomial."""
A0, A1, A2, A3, A4 = coeffs
return A0 + A1 * X + A2 * X**2 + A3 * X**3 + A4 * X**4

X = jnp.log10(Te)
Zavg = _calculate_in_range(X, Zavg_coeffs_in_range)

return Zavg


def get_average_charge_state(
ion_symbols: tuple[str, ...],
ion_mixture: plasma_composition.DynamicIonMixture,
Te: array_typing.ArrayFloat,
) -> array_typing.ArrayFloat:
"""Calculates or prescribes average impurity charge state profile (JAX-compatible).
Args:
ion_symbols: Species to calculate average charge state for. Concrete values.
ion_mixture: DynamicIonMixture object containing impurity information. The
index of the ion_mixture.fractions array corresponds to the index of the
ion_symbols array.
Te: Electron temperature [keV]. Can be any sized array, e.g. on cell grid,
face grid, or a single scalar.
Returns:
Zimp: Average charge state profile on cell grid [amu].
Zimp_face: Average charge state profile on face grid [amu].
"""

if ion_mixture.Z_override is not None:
return jnp.ones_like(Te) * ion_mixture.Z_override

avg_Z = jnp.zeros_like(Te)
for ion_symbol, fraction in zip(ion_symbols, ion_mixture.fractions):
avg_Z += fraction * calculate_average_charge_state_single_species(
Te, ion_symbol
)

return avg_Z
Loading

0 comments on commit 7e81ff8

Please sign in to comment.