Skip to content

Commit 8c1b325

Browse files
theo-brownTorax team
authored andcommitted
Add a gas puff source with feedback on line averaged density.
PiperOrigin-RevId: 906928786
1 parent 713e770 commit 8c1b325

3 files changed

Lines changed: 154 additions & 4 deletions

File tree

torax/_src/sources/gas_puff_source.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
import chex
2020
import jax
21+
import jax.numpy as jnp
2122
from torax._src import array_typing
23+
from torax._src import math_utils
2224
from torax._src import state
2325
from torax._src.config import runtime_params as runtime_params_lib
2426
from torax._src.geometry import geometry
@@ -42,6 +44,8 @@
4244
class RuntimeParams(sources_runtime_params_lib.RuntimeParams):
4345
puff_decay_length: array_typing.FloatScalar
4446
S_total: array_typing.FloatScalar
47+
target_line_average_n_e: array_typing.FloatScalar
48+
feedback_gain: array_typing.FloatScalar
4549

4650

4751
# Default formula: exponential
@@ -66,6 +70,35 @@ def calc_puff_source(
6670
)
6771

6872

73+
# Gas puff with feedback on line averaged density
74+
def calc_puff_feedback_source(
75+
runtime_params: runtime_params_lib.RuntimeParams,
76+
geo: geometry.Geometry,
77+
source_name: str,
78+
core_profiles: state.CoreProfiles,
79+
unused_calculated_source_profiles: source_profiles.SourceProfiles | None,
80+
unused_conductivity: conductivity_base.Conductivity | None,
81+
) -> tuple[array_typing.FloatVectorCell, ...]:
82+
"""Calculates external source term for n from puffs with feedback."""
83+
source_params = runtime_params.sources[source_name]
84+
assert isinstance(source_params, RuntimeParams)
85+
86+
current_line_avg_n_e = math_utils.line_average(core_profiles.n_e.value, geo)
87+
error = source_params.target_line_average_n_e - current_line_avg_n_e
88+
89+
S_total = source_params.feedback_gain * error
90+
S_total = jnp.clip(S_total, 0.0, jnp.inf)
91+
92+
return (
93+
formulas.exponential_profile(
94+
decay_start=1.0,
95+
width=source_params.puff_decay_length,
96+
total=S_total,
97+
geo=geo,
98+
),
99+
)
100+
101+
69102
@dataclasses.dataclass(kw_only=True, frozen=True, eq=False)
70103
class GasPuffSource(source.Source):
71104
"""Gas puff source for the n_e equation."""
@@ -86,35 +119,50 @@ class GasPuffSourceConfig(base.SourceModelBase):
86119
S_total: total gas puff particles/s
87120
"""
88121

89-
model_name: Annotated[Literal['exponential'], torax_pydantic.JAX_STATIC] = (
90-
'exponential'
91-
)
122+
model_name: Annotated[
123+
Literal['exponential', 'feedback'], torax_pydantic.JAX_STATIC
124+
] = 'exponential'
92125
puff_decay_length: torax_pydantic.TimeVaryingScalar = (
93126
torax_pydantic.ValidatedDefault(0.05)
94127
)
95128
S_total: torax_pydantic.TimeVaryingScalar = torax_pydantic.ValidatedDefault(
96129
1e22
97130
)
131+
target_line_average_n_e: torax_pydantic.TimeVaryingScalar = (
132+
torax_pydantic.ValidatedDefault(0.0)
133+
)
134+
feedback_gain: torax_pydantic.TimeVaryingScalar = (
135+
torax_pydantic.ValidatedDefault(0.0)
136+
)
98137
mode: Annotated[
99138
sources_runtime_params_lib.Mode, torax_pydantic.JAX_STATIC
100139
] = sources_runtime_params_lib.Mode.MODEL_BASED
101140

102141
@property
103142
def model_func(self) -> source.SourceProfileFunction:
143+
if self.model_name == 'feedback':
144+
return calc_puff_feedback_source
104145
return calc_puff_source
105146

106147
def build_runtime_params(
107148
self,
108149
t: chex.Numeric,
109150
) -> RuntimeParams:
151+
if self.model_name == 'feedback':
152+
is_explicit = True
153+
else:
154+
is_explicit = self.is_explicit
155+
110156
return RuntimeParams(
111157
prescribed_values=tuple(
112158
[v.get_value(t) for v in self.prescribed_values]
113159
),
114160
mode=self.mode,
115-
is_explicit=self.is_explicit,
161+
is_explicit=is_explicit,
116162
puff_decay_length=self.puff_decay_length.get_value(t),
117163
S_total=self.S_total.get_value(t),
164+
target_line_average_n_e=self.target_line_average_n_e.get_value(t),
165+
feedback_gain=self.feedback_gain.get_value(t),
118166
)
119167

120168
def build_source(self) -> GasPuffSource:

torax/_src/sources/tests/gas_puff_source_test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
from absl.testing import absltest
15+
import chex
16+
from torax._src import math_utils
17+
from torax._src.config import build_runtime_params
18+
from torax._src.core_profiles import initialization
1519
from torax._src.sources import gas_puff_source
1620
from torax._src.sources.tests import test_lib
21+
from torax._src.test_utils import default_configs
22+
from torax._src.torax_pydantic import model_config
1723

1824

1925
class GasPuffSourceTest(test_lib.SingleProfileSourceTestCase):
@@ -25,6 +31,66 @@ def setUp(self):
2531
source_name=gas_puff_source.GasPuffSource.SOURCE_NAME,
2632
)
2733

34+
def test_feedback_mode(self):
35+
"""Tests calc_puff_feedback_source with real objects."""
36+
config = default_configs.get_default_config_dict()
37+
config['sources'] = {
38+
'gas_puff': {
39+
'model_name': 'feedback',
40+
'feedback_gain': 10.0,
41+
}
42+
}
43+
torax_config = model_config.ToraxConfig.from_dict(config)
44+
geo = torax_config.geometry.build_provider(torax_config.numerics.t_initial)
45+
46+
runtime_params_provider = (
47+
build_runtime_params.RuntimeParamsProvider.from_config(torax_config)
48+
)
49+
runtime_params = runtime_params_provider(t=torax_config.numerics.t_initial)
50+
51+
source_models = torax_config.sources.build_models()
52+
neoclassical_models = torax_config.neoclassical.build_models()
53+
core_profiles = initialization.initial_core_profiles(
54+
runtime_params=runtime_params,
55+
geo=geo,
56+
source_models=source_models,
57+
neoclassical_models=neoclassical_models,
58+
)
59+
60+
initial_line_avg = math_utils.line_average(core_profiles.n_e.value, geo)
61+
62+
# Rebuild with specific requested value
63+
config['sources']['gas_puff']['model_name'] = 'feedback'
64+
config['sources']['gas_puff']['target_line_average_n_e'] = float(
65+
initial_line_avg + 1e19
66+
)
67+
config['sources']['gas_puff']['feedback_gain'] = 10.0
68+
torax_config = model_config.ToraxConfig.from_dict(config)
69+
runtime_params = build_runtime_params.RuntimeParamsProvider.from_config(
70+
torax_config
71+
)(t=torax_config.numerics.t_initial)
72+
73+
gas_puff_params = runtime_params.sources['gas_puff']
74+
chex.assert_trees_all_close(
75+
gas_puff_params.target_line_average_n_e, float(initial_line_avg + 1e19)
76+
)
77+
chex.assert_trees_all_close(gas_puff_params.feedback_gain, 10.0)
78+
79+
profile = gas_puff_source.calc_puff_feedback_source(
80+
runtime_params=runtime_params,
81+
geo=geo,
82+
source_name='gas_puff',
83+
core_profiles=core_profiles,
84+
unused_calculated_source_profiles=None,
85+
unused_conductivity=None,
86+
)[0]
87+
88+
total_particles = math_utils.volume_integration(profile, geo)
89+
90+
# Expected error = (initial_line_avg + 1e19) - initial_line_avg = 1e19
91+
# Expected S_total = 10.0 * 1e19 = 1e20
92+
chex.assert_trees_all_close(total_particles, 1e20, rtol=1e-5)
93+
2894

2995
if __name__ == '__main__':
3096
absltest.main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
15+
"""ITER hybrid scenario with gas puff feedback control."""
16+
17+
import copy
18+
from torax.tests.test_data import test_iterhybrid_predictor_corrector
19+
20+
CONFIG = copy.deepcopy(test_iterhybrid_predictor_corrector.CONFIG)
21+
22+
CONFIG['numerics']['t_final'] = 20.0
23+
24+
# Configure gas puff to use feedback
25+
CONFIG['sources']['gas_puff'] = {
26+
'model_name': 'feedback',
27+
'puff_decay_length': 0.3,
28+
'feedback_gain': 1e5,
29+
'target_line_average_n_e': (
30+
{
31+
0.0: 8e19,
32+
10.0: 9e19,
33+
},
34+
'STEP',
35+
),
36+
}

0 commit comments

Comments
 (0)