Skip to content

Commit ac76e04

Browse files
author
Torax team
committed
Merge pull request #1769 from Mani212005:feature/low-temperature-exit
PiperOrigin-RevId: 852282855
2 parents 8d38dd5 + 541fc63 commit ac76e04

File tree

7 files changed

+147
-3
lines changed

7 files changed

+147
-3
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ _build
2929

3030
# venv
3131
venv
32+
.venv
33+
34+
# Python version file
35+
.python-version

torax/_src/config/numerics.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class Numerics(torax_pydantic.BaseModelFrozen):
126126
adaptive_T_source_prefactor: pydantic.PositiveFloat = 2.0e10
127127
adaptive_n_source_prefactor: pydantic.PositiveFloat = 2.0e8
128128

129+
T_minimum_eV: pydantic.PositiveFloat = 5.0
130+
129131
@pydantic.model_validator(mode='after')
130132
def model_validation(self) -> Self:
131133
if self.t_initial > self.t_final:

torax/_src/orchestration/step_function.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Logic which controls the stepping over time of the simulation."""
16+
1617
import dataclasses
1718
import functools
1819

@@ -151,6 +152,13 @@ def check_for_errors(
151152
< self._runtime_params_provider.numerics.min_dt
152153
):
153154
return state.SimError.REACHED_MIN_DT
155+
156+
# Low-temperature collapse check
157+
if output_state.core_profiles.below_minimum_temperature(
158+
self._runtime_params_provider.numerics.T_minimum_eV
159+
):
160+
return state.SimError.LOW_TEMPERATURE_COLLAPSE
161+
154162
state_error = output_state.check_for_errors()
155163
if state_error != state.SimError.NO_ERROR:
156164
return state_error

torax/_src/state.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Classes defining the TORAX state that evolves over time."""
16+
1617
import dataclasses
1718
import enum
1819
import functools
@@ -103,9 +104,7 @@ class CoreProfiles:
103104
def pressure_thermal_e(self) -> cell_variable.CellVariable:
104105
"""Electron thermal pressure [Pa]."""
105106
return cell_variable.CellVariable(
106-
value=self.n_e.value
107-
* self.T_e.value
108-
* constants.CONSTANTS.keV_to_J,
107+
value=self.n_e.value * self.T_e.value * constants.CONSTANTS.keV_to_J,
109108
dr=self.n_e.dr,
110109
right_face_constraint=self.n_e.right_face_constraint
111110
* self.T_e.right_face_constraint
@@ -167,6 +166,17 @@ def negative_temperature_or_density(self) -> jax.Array:
167166
])
168167
)
169168

169+
def below_minimum_temperature(self, T_minimum_eV: float) -> bool:
170+
"""Return True if T_e or T_i is below the minimum temperature threshold."""
171+
# Convert eV -> keV since internal storage is keV
172+
T_minimum_keV = T_minimum_eV / 1000.0
173+
174+
is_low_te = jnp.any(self.T_e.value < T_minimum_keV)
175+
is_low_ti = jnp.any(self.T_i.value < T_minimum_keV)
176+
177+
# Use .item() to return a concrete Python boolean
178+
return (is_low_te | is_low_ti).item()
179+
170180
def __str__(self) -> str:
171181
return f"""
172182
CoreProfiles(
@@ -292,6 +302,7 @@ class SimError(enum.Enum):
292302
QUASINEUTRALITY_BROKEN = 2
293303
NEGATIVE_CORE_PROFILES = 3
294304
REACHED_MIN_DT = 4
305+
LOW_TEMPERATURE_COLLAPSE = 5
295306

296307
def log_error(self):
297308
match self:
@@ -320,6 +331,12 @@ def log_error(self):
320331
quasineutrality. Check the output file for near-zero temperatures or
321332
densities at the last valid step.
322333
""")
334+
case SimError.LOW_TEMPERATURE_COLLAPSE:
335+
logging.error("""
336+
Simulation stopped because ion or electron temperature fell below the
337+
configured minimum threshold. This is usually caused by radiative
338+
collapse. Output file contains all profiles up to the last valid step.
339+
""")
323340
case SimError.NO_ERROR:
324341
pass
325342
case _:

torax/_src/tests/state_test.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,54 @@ def test_core_profiles_negative_values_check(self):
127127
self.assertFalse(new_core_profiles.negative_temperature_or_density())
128128

129129

130+
class CoreProfilesTemperatureCheckTest(parameterized.TestCase):
131+
"""Tests for the below_minimum_temperature method in CoreProfiles."""
132+
133+
def setUp(self):
134+
super().setUp()
135+
self.geo = circular_geometry.CircularConfig(n_rho=5).build_geometry()
136+
self.base_profiles = core_profile_helpers.make_zero_core_profiles(self.geo)
137+
138+
@parameterized.named_parameters(
139+
('all_above', 0.2, 0.2, 100.0, False), # 0.2 keV = 200 eV > 100 eV
140+
('te_below', 0.05, 0.2, 100.0, True), # 0.05 keV = 50 eV < 100 eV
141+
('ti_below', 0.2, 0.05, 100.0, True),
142+
('both_below', 0.05, 0.05, 100.0, True),
143+
# 0.1 keV = 100 eV. Logic is strictly <, so this should pass (False).
144+
('exact_boundary', 0.1, 0.1, 100.0, False),
145+
)
146+
def test_below_minimum_temperature(
147+
self, te_val, ti_val, threshold_ev, expected
148+
):
149+
"""Verifies below_minimum_temperature returns correct boolean flag."""
150+
# Create profiles with constant values across the radius
151+
core_profiles = dataclasses.replace(
152+
self.base_profiles,
153+
T_e=core_profile_helpers.make_constant_core_profile(self.geo, te_val),
154+
T_i=core_profile_helpers.make_constant_core_profile(self.geo, ti_val),
155+
)
156+
157+
result = core_profiles.below_minimum_temperature(threshold_ev)
158+
159+
self.assertIsInstance(result, bool)
160+
self.assertEqual(result, expected)
161+
162+
def test_below_minimum_temperature_mixed_profile(self):
163+
"""Tests detection when only part of the profile is below threshold."""
164+
threshold_ev = 100.0 # 0.1 keV
165+
166+
te_values = jnp.array([0.2, 0.2, 0.05, 0.2, 0.2]) # 0.05 is below threshold
167+
ti_values = jnp.array([0.2, 0.2, 0.2, 0.2, 0.2])
168+
169+
core_profiles = dataclasses.replace(
170+
self.base_profiles,
171+
T_e=dataclasses.replace(self.base_profiles.T_e, value=te_values),
172+
T_i=dataclasses.replace(self.base_profiles.T_i, value=ti_values),
173+
)
174+
175+
self.assertTrue(core_profiles.below_minimum_temperature(threshold_ev))
176+
177+
130178
class ImpurityFractionsTest(parameterized.TestCase):
131179
"""Tests for the impurity_fractions attribute in CoreProfiles."""
132180

torax/tests/sim_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,18 @@ def test_nans_trigger_error(self):
590590
self.assertEqual(state_history.sim_error, state.SimError.NAN_DETECTED)
591591
self.assertLess(state_history.times[-1], torax_config.numerics.t_final)
592592

593+
def test_low_temperature_error(self):
594+
"""Verify that a config with radiation collapse triggers early stopping and an error."""
595+
torax_config = self._get_torax_config(
596+
'test_iterhybrid_radiation_collapse.py'
597+
)
598+
_, state_history = run_simulation.run_simulation(torax_config)
599+
600+
self.assertEqual(
601+
state_history.sim_error, state.SimError.LOW_TEMPERATURE_COLLAPSE
602+
)
603+
self.assertLess(state_history.times[-1], torax_config.numerics.t_final)
604+
593605
def test_full_output_matches_reference(self):
594606
"""Check for complete output match with reference."""
595607
torax_config = self._get_torax_config('test_iterhybrid_rampup.py')
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2024 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+
15+
"""Based on test_iterhybrid_predictor_corrector but with radiation collapse."""
16+
17+
# pylint: disable=invalid-name
18+
19+
import copy
20+
21+
from torax.tests.test_data import test_iterhybrid_predictor_corrector
22+
23+
W_frac = 1e-3
24+
CONFIG = copy.deepcopy(test_iterhybrid_predictor_corrector.CONFIG)
25+
26+
assert isinstance(CONFIG['plasma_composition'], dict)
27+
# increasing density to eventually trigger radiation collapse
28+
CONFIG['profile_conditions']['nbar'] = {0: 0.8, 5.0: 1.2}
29+
CONFIG['plasma_composition']['impurity'] = {
30+
'Ne': 1 - W_frac,
31+
'W': W_frac,
32+
}
33+
CONFIG['plasma_composition']['Z_eff'] = 3.0
34+
CONFIG['sources']['impurity_radiation'] = {
35+
'model_name': 'mavrin_fit',
36+
}
37+
# Remove fusion source to make collapse easier to attain
38+
CONFIG['sources'].pop('fusion')
39+
# Reduce the pedestal temperature to make collapse easier to attain
40+
CONFIG['pedestal']['T_i_ped'] = 1.0
41+
CONFIG['pedestal']['T_e_ped'] = 1.0
42+
# nonlinear solver to enable adaptive dt such that small temperature is reached
43+
# during the collapse.
44+
CONFIG['solver'] = (
45+
{
46+
'solver_type': 'newton_raphson',
47+
'use_predictor_corrector': True,
48+
'n_corrector_steps': 5,
49+
'chi_pereverzev': 30,
50+
'D_pereverzev': 15,
51+
'use_pereverzev': True,
52+
}
53+
)

0 commit comments

Comments
 (0)