Skip to content

Commit bf225f6

Browse files
committed
Add Shaing corrections to Angioni-Sauter model
- Add smooth transition between models - Fix conversion from psi with 2pi factor - Tune default parameters for an 'ok' match with NCLASS on STEP and ITER cases
1 parent f9f79d0 commit bf225f6

File tree

3 files changed

+213
-14
lines changed

3 files changed

+213
-14
lines changed

torax/_src/neoclassical/transport/angioni_sauter.py

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
https://gitlab.epfl.ch/spc/public/neos [O. Sauter et al]
2222
"""
2323

24+
import dataclasses
2425
from typing import Annotated, Literal
2526

27+
import jax
2628
from jax import numpy as jnp
2729
from torax._src import array_typing
2830
from torax._src import constants
@@ -39,20 +41,42 @@
3941
# pylint: disable=invalid-name
4042

4143

44+
@jax.tree_util.register_dataclass
45+
@dataclasses.dataclass(frozen=True)
46+
class RuntimeParams(transport_runtime_params.RuntimeParams):
47+
"""RuntimeParams for the Angioni-Sauter neoclassical transport model."""
48+
49+
use_shaing_ion_correction: array_typing.BoolScalar
50+
shaing_ion_multiplier: array_typing.FloatScalar
51+
shaing_blend_start: array_typing.FloatScalar
52+
shaing_blend_rate: array_typing.FloatScalar
53+
54+
4255
class AngioniSauterModelConfig(base.NeoclassicalTransportModelConfig):
4356
"""Pydantic model for the Angioni-Sauter neoclassical transport model."""
4457

4558
model_name: Annotated[
4659
Literal['angioni_sauter'], torax_pydantic.JAX_STATIC
4760
] = 'angioni_sauter'
61+
use_shaing_ion_correction: bool = False
62+
shaing_ion_multiplier: float = 1.8
63+
shaing_blend_start: float = 0.2
64+
shaing_blend_rate: float = 5.0
4865

4966
@override
5067
def build_model(self) -> 'AngioniSauterModel':
5168
return AngioniSauterModel()
5269

5370
@override
54-
def build_runtime_params(self) -> transport_runtime_params.RuntimeParams:
55-
return super().build_runtime_params()
71+
def build_runtime_params(self) -> RuntimeParams:
72+
base_kwargs = dataclasses.asdict(super().build_runtime_params())
73+
return RuntimeParams(
74+
use_shaing_ion_correction=self.use_shaing_ion_correction,
75+
shaing_ion_multiplier=self.shaing_ion_multiplier,
76+
shaing_blend_start=self.shaing_blend_start,
77+
shaing_blend_rate=self.shaing_blend_rate,
78+
**base_kwargs
79+
)
5680

5781

5882
class AngioniSauterModel(base.NeoclassicalTransportModel):
@@ -65,13 +89,48 @@ def _call_implementation(
6589
geometry: geometry_lib.Geometry,
6690
core_profiles: state.CoreProfiles,
6791
) -> base.NeoclassicalTransport:
68-
"""Calculates neoclassical transport coefficients."""
69-
return _calculate_angioni_sauter_transport(
92+
"""Calculates neoclassical transport coefficients with smooth blend.
93+
94+
When use_shaing_correction is enabled, chi_ion is smoothly blended between
95+
Shaing (near axis) and Angioni-Sauter (far from axis) models using an
96+
exponential transition function.
97+
"""
98+
angioni_sauter = _calculate_angioni_sauter_transport(
99+
runtime_params=runtime_params,
100+
geometry=geometry,
101+
core_profiles=core_profiles,
102+
)
103+
shaing = _calculate_shaing_transport(
70104
runtime_params=runtime_params,
71105
geometry=geometry,
72106
core_profiles=core_profiles,
73107
)
74108

109+
110+
# Calculate sigmoid blend weight for Angioni-Sauter (alpha)
111+
# If correction disabled: alpha = 1 (pure Angioni-Sauter)
112+
# If correction enabled: alpha varies smoothly with rho_norm
113+
alpha = jnp.where(
114+
runtime_params.neoclassical.transport.use_shaing_correction,
115+
_calculate_blend_alpha(
116+
rho_norm=geometry.rho_face_norm,
117+
start=runtime_params.neoclassical.transport.shaing_blend_start,
118+
rate=runtime_params.neoclassical.transport.shaing_blend_rate,
119+
),
120+
1.0, # Pure Angioni-Sauter when correction disabled
121+
)
122+
123+
return base.NeoclassicalTransport(
124+
# Ion transport blend: (1-alpha)*Shaing + alpha*Angioni-Sauter
125+
chi_neo_i=(1.0 - alpha) * shaing.chi_neo_i
126+
+ alpha * angioni_sauter.chi_neo_i,
127+
# Electron transport: pure Angioni-Sauter
128+
chi_neo_e=angioni_sauter.chi_neo_e,
129+
D_neo_e=angioni_sauter.D_neo_e,
130+
V_neo_e=angioni_sauter.V_neo_e,
131+
V_neo_ware_e=angioni_sauter.V_neo_ware_e,
132+
)
133+
75134
def __hash__(self) -> int:
76135
return hash(self.__class__.__name__)
77136

@@ -587,3 +646,111 @@ def _calculate_Lmn(
587646
)
588647

589648
return Lmn_e, Lmn_i
649+
650+
651+
def _calculate_shaing_transport(
652+
runtime_params: runtime_params_lib.RuntimeParams,
653+
geometry: geometry_lib.Geometry,
654+
core_profiles: state.CoreProfiles,
655+
) -> base.NeoclassicalTransport:
656+
"""JIT-compatible implementation of the Shaing transport model.
657+
658+
Currently only implements ion thermal transport. Other contributions are
659+
negligible.
660+
661+
Args:
662+
runtime_params: Runtime parameters.
663+
geometry: Geometry object.
664+
core_profiles: Core profiles object.
665+
666+
Returns:
667+
- Neoclassical transport coefficients.
668+
- Radius of validity (in terms of rho_norm) of Shaing model for electrons.
669+
- Radius of validity (in terms of rho_norm) of Shaing model for ions.
670+
"""
671+
# Aliases for readability
672+
m_ion = core_profiles.A_i * constants.CONSTANTS.m_amu
673+
q = core_profiles.q_face
674+
kappa = geometry.elongation_face # Note: denoted delta in Shaing
675+
F = geometry.F_face # Note: denoted I in Shaing
676+
R = geometry.R_major_profile_face
677+
T_i_J = core_profiles.T_i.face_value() * constants.CONSTANTS.keV_to_J
678+
679+
# Collisionality
680+
ln_Lambda_ii = collisions.calculate_log_lambda_ii(
681+
core_profiles.T_i.face_value(),
682+
core_profiles.n_i.face_value(),
683+
core_profiles.Z_i_face,
684+
)
685+
tau_ii = collisions.calculate_tau_ii(
686+
A_i=core_profiles.A_i,
687+
Z_i=core_profiles.Z_i_face,
688+
T_i=core_profiles.T_i.face_value(),
689+
n_i=core_profiles.n_i.face_value(),
690+
ln_Lambda_ii=ln_Lambda_ii,
691+
)
692+
nu_ii = 1 / tau_ii # Ion-ion collision frequency
693+
694+
# Thermal velocity
695+
v_t_ion = jnp.sqrt(2 * T_i_J / m_ion)
696+
697+
# Larmor (gyro)frequency
698+
Omega_0_ion = (
699+
constants.CONSTANTS.q_e * core_profiles.Z_i_face * geometry.B_0 / m_ion
700+
)
701+
702+
# Large aspect ratio approximation (Equation 3, Shaing March 1997)
703+
C_1 = (2 * q / (kappa * F * R)) ** (1 / 2)
704+
705+
# Conversion from flux^2/s -> m^2/s
706+
# TODO: make a more informed choice for dpsi_drhon near the axis (currently
707+
# we simply copy the value at i=1). This is ok as chi[0] is never used.
708+
dpsi_drhon = core_profiles.psi.face_grad()
709+
dpsi_drhon = dpsi_drhon.at[0].set(dpsi_drhon[1])
710+
conversion_factor = 1 / (dpsi_drhon / (2*jnp.pi*geometry.rho_b)) ** 2
711+
712+
# Trapped particle fraction (Equation 46, Shaing March 1997)
713+
f_t_ion = (F * v_t_ion * C_1**2 / Omega_0_ion) ** (1 / 3)
714+
715+
# Orbit width in psi coordinates (Equation 73, Shaing March 1997)
716+
Delta_psi_ion = (F**2 * v_t_ion**2 * C_1 / Omega_0_ion**2) ** (2 / 3)
717+
718+
# Chi i term (Equation 74, Shaing March 1997)
719+
# psi normalization difference accounted for in conversion_factor
720+
chi_i = (nu_ii * Delta_psi_ion**2 / f_t_ion) * conversion_factor
721+
722+
return base.NeoclassicalTransport(
723+
chi_neo_i=runtime_params.neoclassical.transport.shaing_ion_multiplier
724+
* chi_i,
725+
chi_neo_e=jnp.zeros_like(geometry.rho_face),
726+
D_neo_e=jnp.zeros_like(geometry.rho_face),
727+
V_neo_e=jnp.zeros_like(geometry.rho_face),
728+
V_neo_ware_e=jnp.zeros_like(geometry.rho_face),
729+
)
730+
731+
732+
def _calculate_blend_alpha(
733+
rho_norm: array_typing.FloatVectorFace,
734+
start: array_typing.FloatScalar,
735+
rate: array_typing.FloatScalar,
736+
) -> array_typing.FloatVectorFace:
737+
"""Calculate blending weight between Angioni-Sauter and Shaing models.
738+
739+
The blend is:
740+
result = (1-alpha)*Shaing + alpha*Angioni-Sauter
741+
where alpha = 1 / (1 + exp(-2*rate*(rho_norm - start))).
742+
743+
This gives:
744+
- At axis (rho_norm = 0 << start): alpha ~ 0 (pure Shaing)
745+
- At start: alpha = 0.5 (equal blend)
746+
- Far from axis (rho_norm >> start): alpha ~ 1 (pure Angioni-Sauter)
747+
748+
Args:
749+
rho_norm: Normalized toroidal flux coordinate (face grid)
750+
start: Rho norm value where blend transition is centered
751+
rate: Controls transition steepness (higher = sharper transition)
752+
753+
Returns:
754+
Blend factor alpha in range [0, 1]
755+
"""
756+
return 1.0 / (1.0 + jnp.exp(-2.0 * rate * (rho_norm - start)))

torax/_src/physics/collisions.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
electron-ion collisions.
2828
- calculate_log_lambda_ii: Calculates the Coulomb logarithm for ion-ion
2929
collisions.
30+
- calculate_tau_ii: Calculates the ion-ion collision time.
3031
- _calculate_weighted_Z_eff: Calculates ion mass weighted Z_eff used in
3132
the equipartion calculation.
3233
- _calculate_log_tau_e_Z1: Calculates log of electron-ion collision time for
@@ -244,6 +245,41 @@ def calculate_log_lambda_ii(
244245
return 30.0 - 0.5 * jnp.log(n_i) + 1.5 * jnp.log(T_i_ev) - 3.0 * jnp.log(Z_i)
245246

246247

248+
def calculate_tau_ii(
249+
A_i: jax.Array,
250+
Z_i: jax.Array,
251+
T_i: jax.Array,
252+
n_i: jax.Array,
253+
ln_Lambda_ii: jax.Array,
254+
) -> jax.Array:
255+
"""Calculates ion-ion (self) collision time for a single ion species.
256+
257+
See Wesson 3rd edition p730.
258+
259+
Args:
260+
A_i: Ion mass in amu.
261+
Z_i: Ion charge.
262+
T_i: Ion temperature in keV.
263+
n_i: Ion density in m^-3.
264+
ln_Lambda_ii: Coulomb logarithm for ion-ion collisions.
265+
266+
Returns:
267+
Ion-ion collision time in seconds.
268+
"""
269+
log_tau_ii = (
270+
jnp.log(12)
271+
+ 1.5 * jnp.log(jnp.pi)
272+
+ 2 * jnp.log(constants.CONSTANTS.epsilon_0)
273+
+ 0.5 * (jnp.log(A_i) + jnp.log(constants.CONSTANTS.m_amu))
274+
+ 1.5 * (jnp.log(T_i) + jnp.log(constants.CONSTANTS.keV_to_J))
275+
- jnp.log(n_i)
276+
- 4 * jnp.log(Z_i)
277+
- 4 * jnp.log(constants.CONSTANTS.q_e)
278+
- jnp.log(ln_Lambda_ii)
279+
)
280+
return jnp.exp(log_tau_ii)
281+
282+
247283
# TODO(b/377225415): generalize to arbitrary number of ions.
248284
def _calculate_weighted_Z_eff(
249285
core_profiles: state.CoreProfiles,

torax/examples/step_flattop_bgb.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232

3333
import imas
3434
import numpy as np
35+
3536
from torax._src import path_utils
36-
from torax._src.imas_tools.input import core_profiles
37-
from torax._src.imas_tools.input import loader
37+
from torax._src.imas_tools.input import core_profiles, loader
3838

3939
# Load IDSs
4040
path = (
@@ -145,20 +145,16 @@
145145
"D_e_max": 100.0,
146146
"V_e_min": -50.0,
147147
"V_e_max": 50.0,
148-
# Patching
149-
# Replaces neoclassical in the core, pending potato orbit correction
150-
# https://github.com/google-deepmind/torax/issues/1406
151-
"apply_inner_patch": True,
152-
"rho_inner": 0.05,
153-
"chi_e_inner": 1.0,
154-
"chi_i_inner": 15.0,
155148
# Smoothing
156149
"smooth_everywhere": True,
157150
"smoothing_width": 0.05,
158151
},
159152
"neoclassical": {
160153
"bootstrap_current": {"model_name": "sauter"},
161-
"transport": {"model_name": "angioni_sauter"},
154+
"transport": {
155+
"model_name": "angioni_sauter",
156+
"use_shaing_ion_correction": True,
157+
},
162158
},
163159
"numerics": {
164160
"t_initial": 0.0,

0 commit comments

Comments
 (0)