Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/output.rst
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,9 @@ These are called out in the list of profiles below, and generally relate to:
Derivative of plasma surface area enclosed by each flux surface, with respect
to the normalized toroidal flux coordinate rho_norm [:math:`m^2`].

``tau_ei`` (time, rho_face_norm)
Electron-ion collision time [:math:`s`].

``T_e`` (time, rho_norm)
Electron temperature [:math:`keV`].

Expand Down Expand Up @@ -1003,4 +1006,3 @@ purposes or to rerun the simulation.
# We can also use ToraxConfig to run the simulation again.
torax_config = torax.ToraxConfig.from_dict(config_dict)
new_output = torax.run_simulation(torax_config)

2 changes: 2 additions & 0 deletions torax/_src/output_tools/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

"""Module containing functions for saving and loading simulation output."""

from collections.abc import Mapping, Sequence
import dataclasses
import functools
Expand Down Expand Up @@ -112,6 +113,7 @@

# Post processed outputs
Q_FUSION = "Q_fusion"
TAU_EI = "tau_ei"

# Edge model outputs
SEED_IMPURITY_CONCENTRATIONS = "seed_impurity_concentrations"
Expand Down
10 changes: 10 additions & 0 deletions torax/_src/output_tools/post_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from torax._src.orchestration import sim_state as sim_state_lib
from torax._src.output_tools import impurity_radiation
from torax._src.output_tools import safety_factor_fit
from torax._src.physics import collisions
from torax._src.physics import formulas
from torax._src.physics import psi_calculations
from torax._src.physics import rotation
Expand Down Expand Up @@ -65,6 +66,7 @@ class PostProcessedOutputs:
law derived from the updated (2020) ITER H-mode confinement database
FFprime: FF' on the face grid, where F is the toroidal flux function
psi_norm: Normalized poloidal flux on the face grid [Wb]
tau_ei: Electron-ion collision time on the face grid [s]
P_heat_i: Total ion heating power: all sources - sinks. i.e. auxiliary
heating + ion-electron exchange + fusion + (negative) radiation sinks [W].
P_heat_e: Total electron heating power: all sources - sinks. i.e. auxiliary
Expand Down Expand Up @@ -219,6 +221,7 @@ class PostProcessedOutputs:
H20: array_typing.FloatScalar
FFprime: array_typing.FloatVector
psi_norm: array_typing.FloatVector
tau_ei: array_typing.FloatVector
# Integrated heat sources
P_SOL_i: array_typing.FloatScalar
P_SOL_e: array_typing.FloatScalar
Expand Down Expand Up @@ -337,6 +340,7 @@ def zeros(cls, geo: geometry.Geometry) -> typing_extensions.Self:
H20=jnp.array(0.0, dtype=jax_utils.get_dtype()),
FFprime=jnp.zeros(geo.rho_face.shape),
psi_norm=jnp.zeros(geo.rho_face.shape),
tau_ei=jnp.zeros(geo.rho_face.shape),
P_SOL_i=jnp.array(0.0, dtype=jax_utils.get_dtype()),
P_SOL_e=jnp.array(0.0, dtype=jax_utils.get_dtype()),
P_SOL_total=jnp.array(0.0, dtype=jax_utils.get_dtype()),
Expand Down Expand Up @@ -677,6 +681,11 @@ def make_post_processed_outputs(
# Calculate normalized poloidal flux.
psi_face = sim_state.core_profiles.psi.face_value()
psi_norm_face = (psi_face - psi_face[0]) / (psi_face[-1] - psi_face[0])
tau_ei_face = collisions.calculate_tau_ei(
T_e=sim_state.core_profiles.T_e.face_value(),
n_e=sim_state.core_profiles.n_e.face_value(),
Z_eff=sim_state.core_profiles.Z_eff_face,
)
integrated_sources = _calculate_integrated_sources(
sim_state.geometry,
sim_state.core_profiles,
Expand Down Expand Up @@ -956,6 +965,7 @@ def cumulative_values():
H20=H20,
FFprime=FFprime_face,
psi_norm=psi_norm_face,
tau_ei=tau_ei_face,
**integrated_sources,
Q_fusion=Q_fusion,
P_LH=P_LH_martin,
Expand Down
15 changes: 15 additions & 0 deletions torax/_src/output_tools/tests/output_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ def test_core_transport_is_saved(self):
(1, len(self.geo.rho_face_norm)),
)

def test_tau_ei_is_saved(self):
"""Tests that electron-ion collision time is saved as a face profile."""
output_xr = self.history.simulation_output_to_xr()
profiles_dataset = output_xr.children[output.PROFILES].dataset

self.assertIn(output.TAU_EI, profiles_dataset.data_vars)
self.assertEqual(
profiles_dataset[output.TAU_EI].dims,
(output.TIME, output.RHO_FACE_NORM),
)
np.testing.assert_allclose(
profiles_dataset[output.TAU_EI].values[0],
self._output_state.tau_ei,
)

def test_geometry_is_saved(self):
"""Tests that the geometry is saved correctly."""
# Construct a second state with a slightly different geometry.
Expand Down
37 changes: 37 additions & 0 deletions torax/_src/output_tools/tests/post_processing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from torax._src.orchestration import run_simulation
from torax._src.orchestration import sim_state
from torax._src.output_tools import post_processing
from torax._src.physics import collisions
from torax._src.sources import source_profiles as source_profiles_lib
from torax._src.test_utils import default_configs
from torax._src.test_utils import default_sources
Expand Down Expand Up @@ -240,6 +241,42 @@ def test_zero_sources_do_not_make_nans(self):
post_processed_outputs.check_for_errors(), state.SimError.NO_ERROR
)

def test_tau_ei_output(self):
"""Checks electron-ion collision time is calculated on the face grid."""
input_state = sim_state.SimState(
t=jnp.array(0.0),
dt=jnp.array(1e-3),
core_profiles=self.core_profiles,
core_transport=state.CoreTransport.zeros(self.geo),
core_sources=self.source_profiles,
geometry=self.geo,
solver_numeric_outputs=state.SolverNumericOutputs(
solver_error_state=np.array(0, jax_utils.get_int_dtype()),
outer_solver_iterations=np.array(0, jax_utils.get_int_dtype()),
inner_solver_iterations=np.array(0, jax_utils.get_int_dtype()),
sawtooth_crash=False,
),
edge_outputs=None,
time_step_calculator_state=(
self.models.time_step_calculator.initial_state(self.runtime_params)
),
)

outputs = post_processing.make_post_processed_outputs(
sim_state=input_state,
runtime_params=self.runtime_params,
previous_post_processed_outputs=post_processing.PostProcessedOutputs.zeros(
self.geo
),
)

expected_tau_ei = collisions.calculate_tau_ei(
T_e=self.core_profiles.T_e.face_value(),
n_e=self.core_profiles.n_e.face_value(),
Z_eff=self.core_profiles.Z_eff_face,
)
np.testing.assert_allclose(outputs.tau_ei, expected_tau_ei)

def test_current_outputs(self):
"""Checks calculation of current-related outputs."""
# Setup non-zero bootstrap current
Expand Down
49 changes: 30 additions & 19 deletions torax/_src/physics/collisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
electron-ion collisions.
- calculate_log_lambda_ii: Calculates the Coulomb logarithm for ion-ion
collisions.
- calculate_tau_ei: Calculates the electron-ion collision time.
- calculate_tau_ii: Calculates the ion-ion collision time.
- _calculate_weighted_Z_eff: Calculates ion mass weighted Z_eff used in
the equipartion calculation.
Expand Down Expand Up @@ -101,26 +102,12 @@ def calc_nu_star(
Returns:
nu_star: on face grid.
"""

# Calculate Coulomb logarithm
log_lambda_ei_face = calculate_log_lambda_ei(
core_profiles.T_e.face_value(),
core_profiles.n_e.face_value(),
)

# ion_electron collisionality
log_tau_e_Z1 = _calculate_log_tau_e_Z1(
core_profiles.T_e.face_value(),
core_profiles.n_e.face_value(),
log_lambda_ei_face,
)

nu_e = (
1
/ jnp.exp(log_tau_e_Z1)
* core_profiles.Z_eff_face
* collisionality_multiplier
tau_ei = calculate_tau_ei(
T_e=core_profiles.T_e.face_value(),
n_e=core_profiles.n_e.face_value(),
Z_eff=core_profiles.Z_eff_face,
)
nu_e = 1 / tau_ei * collisionality_multiplier

# calculate bounce time
tau_bounce = (
Expand All @@ -144,6 +131,30 @@ def calc_nu_star(
return nustar


def calculate_tau_ei(
T_e: jax.Array,
n_e: jax.Array,
Z_eff: jax.Array,
) -> jax.Array:
"""Calculates electron-ion collision time.

The Z=1 collision time is based on Wesson 3rd edition p729. For multi-species
plasmas this returns the effective electron-ion collision time used by
`calc_nu_star`, scaling the collision frequency by Z_eff.

Args:
T_e: Electron temperature [keV].
n_e: Electron density [m^-3].
Z_eff: Effective ion charge [dimensionless].

Returns:
Electron-ion collision time [s].
"""
log_lambda_ei = calculate_log_lambda_ei(T_e, n_e)
log_tau_e_Z1 = _calculate_log_tau_e_Z1(T_e, n_e, log_lambda_ei)
return jnp.exp(log_tau_e_Z1) / Z_eff


def fast_ion_fractional_heating_formula(
birth_energy: float | array_typing.FloatVector,
T_e: array_typing.FloatVector,
Expand Down
8 changes: 8 additions & 0 deletions torax/_src/physics/tests/collisions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ def test_calculate_log_lambda_ii(self, T_i_ev, n_i, Z_i, expected):
result = collisions.calculate_log_lambda_ii(T_i_kev, n_i, Z_i)
np.testing.assert_allclose(result, expected, atol=1e-6)

def test_calculate_tau_ei_scales_with_zeff(self):
T_e = jnp.array([1.0, 2.0])
n_e = jnp.array([1e20, 2e20])
tau_z1 = collisions.calculate_tau_ei(T_e, n_e, Z_eff=jnp.ones_like(T_e))
tau_z2 = collisions.calculate_tau_ei(T_e, n_e, Z_eff=2 * jnp.ones_like(T_e))

np.testing.assert_allclose(tau_z2, tau_z1 / 2.0)

# TODO(b/377225415): generalize to arbitrary number of ions.
@parameterized.parameters([
dict(
Expand Down
Loading