|
| 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