Skip to content

Commit b078770

Browse files
jcitrinTorax team
authored andcommitted
Add a "scaled_profile" ICRH model to TORAX.
This new model allows users to provide reference ion and electron heating profiles. These profiles are then radially shifted based on the ratio of the current on-axis toroidal magnetic field (B0) to a reference B0, reflecting the change in the ICRH resonance location. The profiles are also rescaled to match a specified total absorbed power. This model is useful for approximating the adaptation of pre-computed RF heating profiles from higher-fidelity source calculations, to different magnetic field scenarios and powers. The model does not include a fast-ion calculation which are expected to be prescribed separately. PiperOrigin-RevId: 906940622
1 parent d7b3088 commit b078770

21 files changed

Lines changed: 560 additions & 141 deletions

docs/configuration.rst

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,46 +2053,91 @@ implemented in TORAX.
20532053
20542054
icrh
20552055
^^^^
2056-
Ion cyclotron heating using a surrogate model of the TORIC ICRH spectrum
2057-
solver simulation https://meetings.aps.org/Meeting/DPP24/Session/NP12.106.
2058-
This source is currently SPARC specific.
2059-
2060-
Weights and configuration for the surrogate model are needed to use this source.
2061-
By default these are expected to be found under
2062-
``'~/toric_surrogate/TORIC_MLP_v1/toricnn.json'``. To use a different file path
2063-
an alternative path can be provided using the ``TORIC_NN_MODEL_PATH``
2064-
environment variable which should point to a compatible JSON file.
2056+
Ion cyclotron resonance heating (ICRH) source. Two model implementations are
2057+
available, selected via the ``model_name`` discriminator.
20652058
20662059
``mode`` (str [default = 'model'])
20672060
2068-
``model_path`` (str | None [default = None])
2069-
Path to the JSON file containing the weights and configuration for the
2070-
surrogate model. If None, the default path
2071-
``'~/toric_surrogate/TORIC_MLP_v1/toricnn.json'`` is used.
2072-
2073-
``wall_inner`` (float [default = 1.24])
2074-
Inner radial location of first wall at plasma midplane level [m].
2075-
2076-
``wall_outer`` (float [default = 2.43])
2077-
Outer radial location of first wall at plasma midplane level [m].
2078-
2079-
``frequency`` (**time-varying-scalar** [default = 120e6])
2080-
ICRF wave frequency in Hz.
2081-
2082-
``minority_concentration`` (**time-varying-scalar** [default = 0.03])
2083-
Helium-3 minority fractional concentration relative to the electron density.
2084-
This is a deprecated legacy option, used only when ``minority_species`` is
2085-
not specified. It is recommended to instead set ``minority_species`` to
2086-
'He3' and use the plasma composition to set the minority concentration.
2087-
2088-
``minority_species`` (str | None [default = None])
2089-
Symbol of the minority species (e.g. 'He3'). If specified, the minority
2090-
concentration is automatically extracted from the plasma composition.
2091-
Presently, only 'He3' is supported.
2092-
20932061
``P_total`` (**time-varying-scalar** [default = 10e6])
20942062
Total injected source power in W.
20952063
2064+
``absorption_fraction`` (**time-varying-scalar** [default = 1.0])
2065+
Fraction of ``P_total`` absorbed by the plasma.
2066+
2067+
``model_name`` (str)
2068+
Selects the ICRH model. Available options:
2069+
2070+
* ``'toric_nn'`` (default)
2071+
Surrogate model of the TORIC ICRH spectrum solver simulation
2072+
https://meetings.aps.org/Meeting/DPP24/Session/NP12.106.
2073+
This source is currently SPARC specific.
2074+
2075+
Weights and configuration for the surrogate model are needed to use this
2076+
source. By default these are expected to be found under
2077+
``'~/toric_surrogate/TORIC_MLP_v1/toricnn.json'``. To use a different file
2078+
path an alternative path can be provided using the ``TORIC_NN_MODEL_PATH``
2079+
environment variable which should point to a compatible JSON file.
2080+
2081+
``model_path`` (str | None [default = None])
2082+
Path to the JSON file containing the weights and configuration for the
2083+
surrogate model. If None, the default path
2084+
``'~/toric_surrogate/TORIC_MLP_v1/toricnn.json'`` is used.
2085+
2086+
``wall_inner`` (float [default = 1.24])
2087+
Inner radial location of first wall at plasma midplane level [m].
2088+
2089+
``wall_outer`` (float [default = 2.43])
2090+
Outer radial location of first wall at plasma midplane level [m].
2091+
2092+
``frequency`` (**time-varying-scalar** [default = 120e6])
2093+
ICRF wave frequency in Hz.
2094+
2095+
``minority_concentration`` (**time-varying-scalar** [default = 0.03])
2096+
Helium-3 minority fractional concentration relative to the electron
2097+
density. This is a deprecated legacy option, used only when
2098+
``minority_species`` is not specified. It is recommended to instead set
2099+
``minority_species`` to 'He3' and use the plasma composition to set the
2100+
minority concentration.
2101+
2102+
``minority_species`` (str | None [default = None])
2103+
Symbol of the minority species (e.g. 'He3'). If specified, the minority
2104+
concentration is automatically extracted from the plasma composition.
2105+
Presently, only 'He3' is supported.
2106+
2107+
* ``'scaled_profile'``
2108+
Prescribed-profile model with magnetic-field-dependent resonance shifting.
2109+
Adapts reference heating profiles to different magnetic field operating points
2110+
and normalises them to the target power. See :ref:`physics_models` for
2111+
detailed physics description.
2112+
2113+
``heat_profile_ion`` (**time-varying-array** [default = {0: {0: 0, 1: 0}}])
2114+
Reference ion heating power density shape [W/m³], provided on the
2115+
normalised radius grid. The shape is rescaled during normalisation; only
2116+
the relative profile matters.
2117+
2118+
``heat_profile_electron`` (**time-varying-array** [default = {0: {0: 0, 1: 0}}])
2119+
Reference electron heating power density shape [W/m³], provided on the
2120+
normalised radius grid.
2121+
2122+
``reference_B0`` (**time-varying-scalar** [default = 12.2])
2123+
Vacuum toroidal magnetic field at which the reference profiles were
2124+
computed [T].
2125+
2126+
Example:
2127+
2128+
.. code-block:: python
2129+
2130+
'sources': {
2131+
'icrh': {
2132+
'model_name': 'scaled_profile',
2133+
'P_total': 25e6,
2134+
'absorption_fraction': 0.9,
2135+
'heat_profile_ion': {0: {0.0: 0, 0.3: 1.0, 0.6: 0.2, 1.0: 0}},
2136+
'heat_profile_electron': {0: {0.0: 0, 0.35: 0.8, 0.65: 0.1, 1.0: 0}},
2137+
'reference_B0': 12.2,
2138+
},
2139+
}
2140+
20962141
See :ref:`physics_models` for more detail.
20972142
20982143
impurity_radiation

docs/physics_models.rst

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,17 @@ The following models are available:
712712
Ion Cyclotron Resonance Heating
713713
-------------------------------
714714

715-
Presently this source is implemented for a SPARC specific ICRH scenario.
715+
TORAX supports ICRH through two models, selectable via the ``model_name``
716+
discriminator in the ``icrh`` source configuration.
717+
718+
**Common parameters** shared by all ICRH models are the total injected power
719+
:math:`P_\mathrm{total}` and the absorption fraction :math:`\alpha`. The
720+
absorbed power is :math:`P_\mathrm{abs} = P_\mathrm{total} \times \alpha`.
721+
722+
ToricNN Surrogate (``toric_nn``)
723+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
724+
725+
This is the default ICRH model, currently specific to the SPARC tokamak.
716726

717727
A core Ion Cyclotron Range of Frequencies (ICRF) heating surrogate model trained
718728
on TORIC ICRH spectrum solver simulations is used to provide power profiles for
@@ -728,6 +738,42 @@ parameterized model of Mikkelsen, as for Fusion Power.
728738
It is assumed that all tritium heating goes to ions and all electron heating
729739
goes to electrons.
730740

741+
Scaled Profile (``scaled_profile``)
742+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
743+
744+
A lightweight model that takes user-prescribed reference heating profiles
745+
and adapts them to different magnetic field operating points using a
746+
physically-motivated radial shift.
747+
748+
The model performs two operations:
749+
750+
1. **Resonance shift:** The ICRH resonance occurs where
751+
:math:`\omega = n \omega_{ci}(R)`, and since the vacuum toroidal field scales
752+
as :math:`B_t \propto 1/R`, the resonance radius scales linearly with
753+
the on-axis field: :math:`R_\mathrm{res} \propto B_0`. When the actual
754+
:math:`B_0` differs from the reference field :math:`B_{0,\mathrm{ref}}`, the
755+
heating profile shifts in normalised radius space.
756+
757+
For each grid point, the shifted outboard midplane radius is:
758+
759+
.. math::
760+
761+
R'_\mathrm{out}(\rho) = R_\mathrm{out}(\rho) \cdot \frac{B_0}{B_{0,\mathrm{ref}}}
762+
763+
where :math:`R_\mathrm{out}(\rho) = R_\mathrm{major} + r(\rho)` is the
764+
outboard midplane radius, which is monotonically increasing with
765+
:math:`\rho`. The reference profiles are then evaluated at the :math:`\rho`
766+
coordinate that corresponds to :math:`R'_\mathrm{out}` on the original
767+
:math:`R_\mathrm{out}(\rho)` mapping.
768+
769+
2. **Power normalisation:** The shifted ion and electron heating profiles are
770+
rescaled so that the volume-integrated total heating equals
771+
:math:`P_\mathrm{total} \times \alpha`.
772+
773+
This model does not include a fast-ion calculation; all fast-ion outputs are
774+
zero. It is useful when reference profiles from a full-wave RF code are
775+
available and need to be quickly approximated for different field scenarios.
776+
731777
Fast Ion Physics
732778
================
733779

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Copyright 2026 DeepMind Technologies Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Scaled-profile ICRH model with magnetic-field-dependent resonance shift."""
15+
16+
import dataclasses
17+
from typing import Annotated, Literal
18+
19+
import chex
20+
import jax
21+
from jax import numpy as jnp
22+
from torax._src import array_typing
23+
from torax._src import math_utils
24+
from torax._src import state
25+
from torax._src.config import runtime_params as runtime_params_lib
26+
from torax._src.geometry import geometry
27+
from torax._src.neoclassical.conductivity import base as conductivity_base
28+
from torax._src.physics import fast_ion as fast_ion_lib
29+
from torax._src.sources import runtime_params as source_runtime_params_lib
30+
from torax._src.sources import source
31+
from torax._src.sources import source_profiles
32+
from torax._src.sources.ion_cyclotron_source import base
33+
from torax._src.torax_pydantic import torax_pydantic
34+
35+
# pylint: disable=invalid-name
36+
37+
38+
@jax.tree_util.register_dataclass
39+
@dataclasses.dataclass(frozen=True)
40+
class RuntimeParams(source_runtime_params_lib.RuntimeParams):
41+
"""Runtime parameters for the scaled-profile ICRH model."""
42+
43+
P_total: array_typing.FloatScalar
44+
absorption_fraction: array_typing.FloatScalar
45+
heat_profile_ion: array_typing.FloatVector
46+
heat_profile_electron: array_typing.FloatVector
47+
reference_B0: array_typing.FloatScalar
48+
49+
50+
def scaled_profile_model_func(
51+
runtime_params: runtime_params_lib.RuntimeParams,
52+
geo: geometry.Geometry,
53+
source_name: str,
54+
core_profiles: state.CoreProfiles,
55+
unused_calculated_source_profiles: source_profiles.SourceProfiles | None,
56+
unused_conductivity: conductivity_base.Conductivity | None,
57+
) -> tuple[
58+
array_typing.FloatVectorCell,
59+
array_typing.FloatVectorCell,
60+
tuple[fast_ion_lib.FastIon, ...],
61+
]:
62+
"""Compute ICRH heating from prescribed profiles with B-field shift.
63+
64+
The model performs two operations on the reference profiles:
65+
1. **Radial shift**: The ICRH resonance location in major radius scales as
66+
R_res ∝ B₀. When B₀ differs from the reference field, the resonance
67+
moves, and the heating profile shifts accordingly in normalised radius.
68+
2. **Power normalisation**: The shifted profiles are rescaled so the
69+
volume-integrated total heating equals ``P_total * absorption_fraction``.
70+
71+
Args:
72+
runtime_params: Full simulation runtime parameters.
73+
geo: Magnetic geometry.
74+
source_name: Name of this source (used to look up params).
75+
core_profiles: Core plasma profiles (unused by this model).
76+
unused_calculated_source_profiles: Not used.
77+
unused_conductivity: Not used.
78+
79+
Returns:
80+
Tuple of (ion_heating, electron_heating, fast_ions) where fast_ions are
81+
all zeros (no fast-ion model in this mode).
82+
"""
83+
del core_profiles # Unused.
84+
source_params = runtime_params.sources[source_name]
85+
assert isinstance(source_params, RuntimeParams)
86+
87+
ref_ion = source_params.heat_profile_ion
88+
ref_el = source_params.heat_profile_electron
89+
90+
# --- 1. Compute resonance shift ---
91+
# The ICRH resonance occurs where ω = n·ω_ci(R) and since B_t ∝ 1/R,
92+
# the resonance major radius scales linearly with B₀.
93+
# B_ratio > 1 means stronger field → resonance moves outward in R.
94+
B_ratio = geo.B_0 / source_params.reference_B0
95+
96+
# Outboard midplane major radius on the cell grid: R_out(ρ) = R_major + r(ρ).
97+
# This is monotonically increasing with ρ, unlike the flux-surface-averaged
98+
# R_major_profile which can be constant (e.g. circular geometry).
99+
R_out = geo.R_out
100+
rho = geo.torax_mesh.cell_centers
101+
102+
# Shifted major radius for each grid point.
103+
R_shifted = R_out * B_ratio
104+
105+
# Map back to normalised radius: for each shifted R, find the
106+
# corresponding ρ on the original R_out(ρ) curve.
107+
rho_shifted = jnp.interp(R_shifted, R_out, rho)
108+
109+
# Evaluate reference profiles at the shifted ρ positions.
110+
shifted_ion = jnp.interp(rho_shifted, rho, ref_ion)
111+
shifted_el = jnp.interp(rho_shifted, rho, ref_el)
112+
113+
# --- 2. Normalise to target power ---
114+
absorbed_power = source_params.P_total * source_params.absorption_fraction
115+
total_shape = shifted_ion + shifted_el
116+
integrated = math_utils.volume_integration(total_shape, geo)
117+
# Guard against zero integrated power (e.g. if profiles are all zero).
118+
# Use safe denominator to avoid NaN gradients in dead jnp.where branches.
119+
safe_integrated = jnp.where(integrated > 0, integrated, 1.0)
120+
scale = jnp.where(integrated > 0, absorbed_power / safe_integrated, 0.0)
121+
122+
source_ion = shifted_ion * scale
123+
source_el = shifted_el * scale
124+
125+
# --- 3. Default zero fast ions in build_fast_ions ---
126+
fast_ions = base.build_fast_ions(source_name=source_name, geo=geo)
127+
128+
return (source_ion, source_el, fast_ions)
129+
130+
131+
class ScaledProfileIonCyclotronSourceConfig(base.IonCyclotronSourceConfig):
132+
"""Configuration for ICRH with prescribed, B-field-shiftable profiles.
133+
134+
This model takes reference ion and electron heating profiles and:
135+
1. Shifts them radially based on the ratio of the actual vacuum toroidal
136+
magnetic field to a reference field (``B₀ / reference_B0``).
137+
2. Rescales the amplitude so that the volume-integrated total heating
138+
equals ``P_total * absorption_fraction``.
139+
140+
This is useful when computed reference heating profiles are available at a
141+
specific magnetic field, and you need to approximate them to different
142+
operating points without re-running the full RF solver.
143+
144+
Attributes:
145+
model_name: Discriminator literal for Pydantic.
146+
heat_profile_ion: Reference ion heating power density shape [W/m³],
147+
provided on the normalised radius grid.
148+
heat_profile_electron: Reference electron heating power density shape
149+
[W/m³], provided on the normalised radius grid.
150+
reference_B0: Vacuum toroidal magnetic field at which the reference
151+
profiles were computed [T].
152+
"""
153+
154+
model_name: Annotated[
155+
Literal['scaled_profile'], torax_pydantic.JAX_STATIC
156+
] = 'scaled_profile'
157+
heat_profile_ion: torax_pydantic.TimeVaryingArray = (
158+
torax_pydantic.ValidatedDefault({0: {0: 0, 1: 0}})
159+
)
160+
heat_profile_electron: torax_pydantic.TimeVaryingArray = (
161+
torax_pydantic.ValidatedDefault({0: {0: 0, 1: 0}})
162+
)
163+
reference_B0: torax_pydantic.TimeVaryingScalar = (
164+
torax_pydantic.ValidatedDefault(12.2)
165+
)
166+
167+
@property
168+
def model_func(self) -> source.SourceProfileFunction:
169+
return scaled_profile_model_func
170+
171+
def build_runtime_params(
172+
self,
173+
t: chex.Numeric,
174+
) -> RuntimeParams:
175+
return RuntimeParams(
176+
prescribed_values=tuple(
177+
[v.get_value(t) for v in self.prescribed_values]
178+
),
179+
mode=self.mode,
180+
is_explicit=self.is_explicit,
181+
P_total=self.P_total.get_value(t),
182+
absorption_fraction=self.absorption_fraction.get_value(t),
183+
heat_profile_ion=self.heat_profile_ion.get_value(t),
184+
heat_profile_electron=self.heat_profile_electron.get_value(t),
185+
reference_B0=self.reference_B0.get_value(t),
186+
)

0 commit comments

Comments
 (0)