Skip to content

Commit f191ebe

Browse files
hamelphiTorax team
authored andcommitted
Combine Waltz and Victor rules for E×B shear suppression
Refactors the rotation/shear suppression in the QLKNN transport model to simultaneously apply both the Waltz rule and the Victor rule (Van de Plassche 2020) to different physical components of the E×B shear, rather than treating them as mutually exclusive options. The E×B shearing rate is decomposed into: Poloidal/pressure contributions (from ∇Pᵢ and v_θB_φ): Waltz rule applied, giving pure suppression f_waltz = -α. Toroidal contribution (from -v_φB_θ): Victor rule applied, a fitted model that can produce both suppression and enhancement depending on local plasma parameters. The combined scaling factor becomes: f_rot = clip(1 + f_waltz * |γ_pp| / γ_max + f_victor * |γ_tor| / γ_max, 0) Changes: rotation.py: Introduce RotationOutput dataclass. Split _calculate_radial_electric_field to return separate Er components for poloidal/pressure and toroidal contributions. qualikiz_based_transport_model.py: Propagate separate v_ExB components through to QualikizInputs with individual gamma_E_GB_poloidal_pressure and gamma_E_GB_toroidal shearing rates. qlknn_transport_model.py: Apply both rules simultaneously. Remove ShearSuppressionModel enum. pydantic_model.py: Remove shear_suppression_model config field. Retain shear_suppression_alpha for Waltz rule strength. tglf_based_transport_model.py, post_processing.py: Update to new RotationOutput API. Docs: Update physics_models.rst to describe combined model; add Waltz 1998 reference to links.rst. Tests: Remove separate Waltz suppression sim test. Update unit tests and regenerate reference .nc data. PiperOrigin-RevId: 868764762
1 parent 08dddaf commit f191ebe

16 files changed

+256
-226
lines changed

docs/configuration.rst

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,19 +1365,9 @@ It is recommended to not set ``qlknn_model_name``, or
13651365

13661366
* ``full_radius``: The rotation correction is applied everywhere.
13671367

1368-
``shear_suppression_model`` (str [default = 'waltz_rule'])
1369-
Selects the shear suppression model used for rotation effects on transport.
1370-
Options are:
1371-
1372-
* ``waltz_rule``: Simple model from Waltz et al., PoP 1998
1373-
(https://doi.org/10.1063/1.872847): :math:`f_{rot} = -\alpha`.
1374-
1375-
* ``vandeplassche2020``: Fitted rotation rule from Van de Plassche et al.,
1376-
PoP 2020 (https://doi.org/10.1063/1.5134126).
1377-
13781368
``shear_suppression_alpha`` (float [default = 1.0])
1379-
Alpha parameter for the Waltz rule shear suppression model. Only used when
1380-
``shear_suppression_model`` is ``'waltz_rule'``.
1369+
Alpha parameter for the Waltz rule shear suppression applied to
1370+
poloidal/pressure contributions.
13811371

13821372
``output_mode_contributions`` (bool [default = False])
13831373
If ``True``, output individual ITG/TEM/ETG contributions to transport

docs/links.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
.. _mavrin2017_link: https://doi.org/10.1007/s10894-017-0136-z
2727
.. _kim1991_link: https://doi.org/10.1063/1.859671
2828
.. _hinton1976_link: https://doi.org/10.1103/RevModPhys.48.239
29+
.. _waltz1998_link: https://doi.org/10.1063/1.872847
2930

3031
.. Define substitutions using link targets
3132
.. |qlknn10d| replace:: `[van de Plassche et al, Phys. Plasmas 2020] <qlknn10d_link_>`_
@@ -55,3 +56,4 @@
5556
.. |mavrin2017| replace:: `[Mavrin, J Fusion Energ 2017] <mavrin2017_link_>`_
5657
.. |kim1991| replace:: `[Kim et al, Phys. Fluids B 1991] <kim1991_link_>`_
5758
.. |hinton1976| replace:: `[Hinton & Hazeltine, Rev. Mod. Phys. 1976] <hinton1976_link_>`_
59+
.. |waltz1998| replace:: `[Waltz et al, Phys. Plasmas 1998] <waltz1998_link_>`_

docs/physics_models.rst

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -386,42 +386,39 @@ be enabled through the transport model configuration.
386386
`True` in the transport model configuration.
387387

388388
* **QLKNN:** The QLKNN transport model
389-
incorporates a "rotation rule" that reduces turbulent fluxes (specifically
390-
for ITG and TEM modes), see |qlknn10d|. This reduction is based on the
391-
ratio of the :math:`E \times B` shearing rate (:math:`\gamma_{E \times B}`)
392-
to the maximum linear growth rate (:math:`\gamma_{max}`). The scaling
393-
factor is calculated as:
389+
incorporates a "rotation rule" that modifies turbulent fluxes (specifically
390+
for ITG and TEM modes) based on E×B shear. The modification combines two
391+
physical effects:
392+
393+
* **Waltz rule** applied to poloidal/pressure contributions (from
394+
:math:`\nabla P_i` and :math:`v_\theta B_\phi`): Simple suppression
395+
:math:`f_{waltz} = -\alpha`, where :math:`\alpha` is set by the
396+
``shear_suppression_alpha`` parameter (default 1.0). See |waltz1998|.
397+
398+
* **Victor rule** applied to toroidal contributions (from
399+
:math:`-v_\phi B_\theta`): A fitted model that can produce both
400+
suppression and enhancement depending on local plasma parameters,
401+
due to competing stabilizing (E×B shear) and destabilizing (parallel
402+
velocity shear) effects. See |qlknn10d|.
403+
404+
The combined scaling factor is:
394405

395406
.. math::
396-
f_{rot} = 1 + f_{rule} \frac{\gamma_{E \times B}}{\gamma_{max}}
397-
398-
The factor :math:`f_{rule}` determines the strength of shear suppression
399-
and can be computed using one of two models, controlled by the
400-
``shear_suppression_model`` configuration parameter:
401-
402-
* ``waltz_rule``: Simple model from Waltz et al., PoP 1998:
403-
:math:`f_{rule} = -\alpha`, where :math:`\alpha` is set by the
404-
``shear_suppression_alpha`` parameter (default 1.0). This model provides
405-
pure suppression of turbulent transport.
406-
407-
* ``vandeplassche2020``: Fitted rotation rule from Van de Plassche et al.,
408-
PoP 2020. This model calculates :math:`f_{rule}` based on the safety
409-
factor, magnetic shear, and inverse aspect ratio. Note that this model
410-
can produce both suppression and enhancement of transport depending on
411-
local plasma parameters. This is because it was fitted for purely
412-
toroidal rotation, which has both stabilizing (ExB shear) and
413-
destabilizing (parallel velocity shear) effects, with a geometry
414-
dependence setting the relative impact.
407+
f_{rot} = \text{clip}\left(1 + f_{waltz}\frac{|\gamma_{pp}|}{\gamma_{max}}
408+
+ f_{victor}\frac{|\gamma_{tor}|}{\gamma_{max}}, 0\right)
409+
410+
where :math:`\gamma_{pp}` and :math:`\gamma_{tor}` are the E×B shearing
411+
rates from poloidal/pressure and toroidal contributions respectively.
415412

416413
The application of the rotation rule is controlled by the
417414
``rotation_mode`` configuration parameter. Options for ``rotation_mode`` are:
418415

419-
* ``off``: No rotation correction is applied.
416+
* ``off``: No rotation correction is applied.
420417

421-
* ``half_radius``: The rotation correction is only applied to the outer
422-
half of the radius (:math:`\hat{\rho} > 0.5`).
418+
* ``half_radius``: The rotation correction is only applied to the outer
419+
half of the radius (:math:`\hat{\rho} > 0.5`).
423420

424-
* ``full_radius``: The rotation correction is applied everywhere.
421+
* ``full_radius``: The rotation correction is applied everywhere.
425422

426423
**Tuning Rotation Effects:**
427424

torax/_src/output_tools/post_processing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ def cumulative_values():
956956
sim_state.core_profiles, sim_state.geometry
957957
)
958958

959-
_, radial_electric_field, poloidal_velocity = rotation.calculate_rotation(
959+
rotation_output = rotation.calculate_rotation(
960960
T_i=sim_state.core_profiles.T_i,
961961
psi=sim_state.core_profiles.psi,
962962
n_i=sim_state.core_profiles.n_i,
@@ -1039,8 +1039,8 @@ def cumulative_values():
10391039
beta_pol=beta_pol,
10401040
beta_N=beta_N,
10411041
impurity_species=impurity_radiation_outputs,
1042-
poloidal_velocity=poloidal_velocity.face_value(),
1043-
radial_electric_field=radial_electric_field.face_value(),
1042+
poloidal_velocity=rotation_output.poloidal_velocity.face_value(),
1043+
radial_electric_field=rotation_output.Er.face_value(),
10441044
first_step=jnp.array(False),
10451045
)
10461046

torax/_src/physics/rotation.py

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""Calculations related to the rotation of the plasma."""
15+
import dataclasses
1516

1617
from jax import numpy as jnp
1718
from torax._src import array_typing
@@ -24,6 +25,27 @@
2425

2526

2627
# pylint: disable=invalid-name
28+
@dataclasses.dataclass(frozen=True)
29+
class RotationOutput:
30+
"""Structured output from rotation calculations.
31+
32+
Attributes:
33+
v_ExB: Total ExB velocity profile on the face grid [m/s].
34+
v_ExB_poloidal_pressure: v_ExB from poloidal rotation + pressure gradient
35+
contributions: (dpi/dr / (Zi * e * ni) + v_theta * B_phi) / B_total.
36+
v_ExB_toroidal: v_ExB from toroidal velocity contribution: -v_phi * B_theta
37+
/ B_total.
38+
Er: Total radial electric field as a cell variable [V/m].
39+
poloidal_velocity: Poloidal velocity as a cell variable [m/s].
40+
"""
41+
42+
v_ExB: array_typing.FloatVectorFace
43+
v_ExB_poloidal_pressure: array_typing.FloatVectorFace
44+
v_ExB_toroidal: array_typing.FloatVectorFace
45+
Er: cell_variable.CellVariable
46+
poloidal_velocity: cell_variable.CellVariable
47+
48+
2749
def _calculate_radial_electric_field(
2850
pressure_thermal_i: cell_variable.CellVariable,
2951
toroidal_angular_velocity: cell_variable.CellVariable,
@@ -33,11 +55,21 @@ def _calculate_radial_electric_field(
3355
B_pol_face: array_typing.FloatVector,
3456
B_tor_face: array_typing.FloatVector,
3557
geo: geometry.Geometry,
36-
) -> cell_variable.CellVariable:
37-
"""Calculates the radial electric field Er.
58+
) -> tuple[
59+
cell_variable.CellVariable,
60+
array_typing.FloatVectorFace,
61+
array_typing.FloatVectorFace,
62+
]:
63+
"""Calculates the radial electric field Er with separate components.
3864
3965
Er = (1 / (Zi * e * ni)) * dpi/dr - v_phi * B_theta + v_theta * B_phi
4066
67+
We split Er into:
68+
- Er_poloidal_pressure: (1 / (Zi * e * ni)) * dpi/dr + v_theta * B_phi
69+
This contribution is from the pressure gradient and poloidal rotation.
70+
- Er_toroidal: -v_phi * B_theta
71+
This contribution is from toroidal velocity.
72+
4173
Args:
4274
pressure_thermal_i: Pressure profile as a cell variable.
4375
toroidal_angular_velocity: Toroidal velocity profile as a cell variable.
@@ -49,7 +81,10 @@ def _calculate_radial_electric_field(
4981
geo: Geometry object.
5082
5183
Returns:
52-
Er: Radial electric field [V/m] on the cell grid.
84+
Er: Radial electric field [V/m] as a CellVariable.
85+
Er_poloidal_pressure_face: Er contribution from pressure gradient and
86+
poloidal rotation [V/m] on face grid.
87+
Er_toroidal_face: Er contribution from toroidal velocity [V/m] on face grid.
5388
"""
5489
# Calculate dpi/dr with respect to a midplane-averaged radial coordinate.
5590
dpi_dr = pressure_thermal_i.face_grad(
@@ -58,19 +93,27 @@ def _calculate_radial_electric_field(
5893

5994
# Calculate Er
6095
denominator = Z_i_face * constants.CONSTANTS.q_e * n_i.face_value()
61-
Er = (
96+
97+
Er_poloidal_pressure_face = (
6298
math_utils.safe_divide(jnp.array(1.0), denominator) * dpi_dr
63-
- toroidal_angular_velocity.face_value()
99+
+ poloidal_velocity.face_value() * B_tor_face
100+
)
101+
102+
Er_toroidal_face = (
103+
-toroidal_angular_velocity.face_value()
64104
* geo.R_major_profile_face
65105
* B_pol_face
66-
+ poloidal_velocity.face_value() * B_tor_face
67106
)
68-
return cell_variable.CellVariable(
69-
value=geometry.face_to_cell(Er),
107+
108+
Er_face = Er_poloidal_pressure_face + Er_toroidal_face
109+
110+
Er = cell_variable.CellVariable(
111+
value=geometry.face_to_cell(Er_face),
70112
face_centers=geo.rho_face_norm,
71-
right_face_constraint=Er[-1],
113+
right_face_constraint=Er_face[-1],
72114
right_face_grad_constraint=None,
73115
)
116+
return Er, Er_poloidal_pressure_face, Er_toroidal_face
74117

75118

76119
def _calculate_v_ExB(
@@ -93,11 +136,7 @@ def calculate_rotation(
93136
pressure_thermal_i: cell_variable.CellVariable,
94137
geo: geometry.Geometry,
95138
poloidal_velocity_multiplier: array_typing.FloatScalar = 1.0,
96-
) -> tuple[
97-
array_typing.FloatVectorFace,
98-
cell_variable.CellVariable,
99-
cell_variable.CellVariable,
100-
]:
139+
) -> RotationOutput:
101140
"""Calculates quantities related to the rotation of the plasma.
102141
103142
Args:
@@ -114,9 +153,8 @@ def calculate_rotation(
114153
velocity.
115154
116155
Returns:
117-
v_ExB: ExB velocity profile on the face grid [m/s].
118-
Er: Radial electric field as a cell variable [V/m] .
119-
poloidal_velocity: Poloidal velocity as a cell variable [m/s].
156+
RotationOutput with v_ExB, separated v_ExB components, Er, and
157+
poloidal_velocity.
120158
"""
121159

122160
# Flux surface average of `B_phi = F/R`.
@@ -142,17 +180,29 @@ def calculate_rotation(
142180
poloidal_velocity_multiplier=poloidal_velocity_multiplier,
143181
)
144182

145-
Er = _calculate_radial_electric_field(
146-
pressure_thermal_i=pressure_thermal_i,
147-
toroidal_angular_velocity=toroidal_angular_velocity,
148-
poloidal_velocity=poloidal_velocity,
149-
n_i=n_i,
150-
Z_i_face=Z_i_face,
151-
B_pol_face=B_pol_face,
152-
B_tor_face=B_tor_face,
153-
geo=geo,
183+
Er, Er_poloidal_pressure_face, Er_toroidal_face = (
184+
_calculate_radial_electric_field(
185+
pressure_thermal_i=pressure_thermal_i,
186+
toroidal_angular_velocity=toroidal_angular_velocity,
187+
poloidal_velocity=poloidal_velocity,
188+
n_i=n_i,
189+
Z_i_face=Z_i_face,
190+
B_pol_face=B_pol_face,
191+
B_tor_face=B_tor_face,
192+
geo=geo,
193+
)
154194
)
155195

156196
v_ExB = _calculate_v_ExB(Er.face_value(), B_total_face)
197+
v_ExB_poloidal_pressure = _calculate_v_ExB(
198+
Er_poloidal_pressure_face, B_total_face
199+
)
200+
v_ExB_toroidal = _calculate_v_ExB(Er_toroidal_face, B_total_face)
157201

158-
return v_ExB, Er, poloidal_velocity
202+
return RotationOutput(
203+
v_ExB=v_ExB,
204+
v_ExB_poloidal_pressure=v_ExB_poloidal_pressure,
205+
v_ExB_toroidal=v_ExB_toroidal,
206+
Er=Er,
207+
poloidal_velocity=poloidal_velocity,
208+
)

torax/_src/physics/tests/rotation_test.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def setUp(self):
3131
).build_geometry()
3232

3333
def test_calculate_rotation_shapes_are_correct(self):
34-
v_ExB, Er, poloidal_velocity = rotation.calculate_rotation(
34+
rotation_output = rotation.calculate_rotation(
3535
T_i=core_profile_helpers.make_constant_core_profile(
3636
geo=self.geo, value=1.0
3737
),
@@ -50,16 +50,19 @@ def test_calculate_rotation_shapes_are_correct(self):
5050
),
5151
geo=self.geo,
5252
)
53-
self.assertEqual(v_ExB.shape, self.geo.rho_face_norm.shape)
54-
self.assertEqual(Er.face_value().shape, self.geo.rho_face_norm.shape)
53+
self.assertEqual(rotation_output.v_ExB.shape, self.geo.rho_face_norm.shape)
5554
self.assertEqual(
56-
poloidal_velocity.face_value().shape, self.geo.rho_face_norm.shape
55+
rotation_output.Er.face_value().shape, self.geo.rho_face_norm.shape
56+
)
57+
self.assertEqual(
58+
rotation_output.poloidal_velocity.face_value().shape,
59+
self.geo.rho_face_norm.shape,
5760
)
5861

5962
def test_electric_field_is_zero_for_zero_velocities_and_constant_pressure(
6063
self,
6164
):
62-
E_r = rotation._calculate_radial_electric_field(
65+
E_r, _, _ = rotation._calculate_radial_electric_field(
6366
pressure_thermal_i=core_profile_helpers.make_constant_core_profile(
6467
geo=self.geo, value=1.0
6568
),
@@ -75,13 +78,12 @@ def test_electric_field_is_zero_for_zero_velocities_and_constant_pressure(
7578
B_tor_face=np.ones_like(self.geo.rho_face_norm),
7679
geo=self.geo,
7780
)
78-
# For constant profiles and zero velocities, E_r should be zero.
7981
np.testing.assert_allclose(
8082
E_r.value, np.zeros_like(self.geo.rho_norm), atol=1e-12
8183
)
8284

8385
def test_electric_field_is_not_zero_for_toroidal_velocity(self):
84-
E_r = rotation._calculate_radial_electric_field(
86+
E_r, _, _ = rotation._calculate_radial_electric_field(
8587
pressure_thermal_i=core_profile_helpers.make_constant_core_profile(
8688
geo=self.geo, value=1.0
8789
),
@@ -97,12 +99,11 @@ def test_electric_field_is_not_zero_for_toroidal_velocity(self):
9799
B_tor_face=np.ones_like(self.geo.rho_face_norm),
98100
geo=self.geo,
99101
)
100-
# E_r should not be all zeros when toroidal velocity is non-zero.
101102
self.assertTrue(np.any(np.abs(E_r.value) > 1e-12))
102103

103104
def test_electric_field_is_not_zero_for_poloidal_velocity(self):
104105
"""Test that radial electric field is not zero for non-zero poloidal velocity."""
105-
E_r = rotation._calculate_radial_electric_field(
106+
E_r, _, _ = rotation._calculate_radial_electric_field(
106107
pressure_thermal_i=core_profile_helpers.make_constant_core_profile(
107108
geo=self.geo, value=1.0
108109
),
@@ -118,12 +119,11 @@ def test_electric_field_is_not_zero_for_poloidal_velocity(self):
118119
B_tor_face=np.ones_like(self.geo.rho_face_norm),
119120
geo=self.geo,
120121
)
121-
# E_r should not be all zeros when poloidal velocity is non-zero.
122122
self.assertTrue(np.any(np.abs(E_r.value) > 1e-12))
123123

124124
def test_electric_field_is_not_zero_for_non_constant_pressure(self):
125125
"""Test that radial electric field is not zero for non-zero poloidal velocity."""
126-
E_r = rotation._calculate_radial_electric_field(
126+
E_r, _, _ = rotation._calculate_radial_electric_field(
127127
pressure_thermal_i=cell_variable.CellVariable(
128128
value=np.linspace(1.0, 2.0, self.geo.rho_norm.size),
129129
face_centers=self.geo.rho_face_norm,
@@ -140,7 +140,6 @@ def test_electric_field_is_not_zero_for_non_constant_pressure(self):
140140
B_tor_face=np.ones_like(self.geo.rho_face_norm),
141141
geo=self.geo,
142142
)
143-
# E_r should not be all zeros when poloidal velocity is non-zero.
144143
self.assertTrue(np.any(np.abs(E_r.value) > 1e-12))
145144

146145
def test_v_ExB_is_zero_for_zero_electric_field(self):

0 commit comments

Comments
 (0)