From caea8b006903b68368496cc76525dec53d546085 Mon Sep 17 00:00:00 2001 From: Tamara Norman Date: Tue, 4 Aug 2026 03:13:57 -0700 Subject: [PATCH] Remove re-export aliases from combined.py and update runtime_params callers. Removed legacy runtime parameter re-exports from combined.py and obsolete isinstance assertions on transport runtime parameters. Standardized on jax.tree.map. PiperOrigin-RevId: 958916672 --- docs/configuration.rst | 64 ---- torax/_src/config/runtime_params.py | 2 +- torax/_src/models.py | 4 +- torax/_src/transport_model/combined.py | 91 ++--- torax/_src/transport_model/pydantic_model.py | 97 +++-- .../transport_model/pydantic_model_base.py | 127 +------ torax/_src/transport_model/runtime_params.py | 47 ++- .../transport_model/tests/combined_test.py | 54 +-- .../tests/pydantic_model_test.py | 58 --- .../qualikiz_based_transport_model_test.py | 17 +- .../tests/tglf_based_transport_model_test.py | 17 +- .../tests/transport_model_test.py | 15 +- .../transport_coefficients_builder.py | 4 +- torax/_src/transport_model/transport_model.py | 354 ------------------ torax/benchmarks/tokagrad_benchmark.py | 30 +- .../test_data/test_combined_transport.nc | Bin 592410 -> 584258 bytes .../tutorials/torax_tutorial_exercises.ipynb | 230 ++++++------ ...ax_tutorial_exercises_with_solutions.ipynb | 234 ++++++------ 18 files changed, 448 insertions(+), 997 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 6b29f10b6..b6cc54312 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1469,58 +1469,6 @@ transport model. Width of HWHM Gaussian smoothing kernel operating on transport model outputs. If using the ``QLKNN_7_11`` transport model, the default is set to 0.1. -``smooth_everywhere`` (bool [default = False]) - Smooth across entire radial domain regardless of inner and outer patches. - -``apply_inner_patch`` (**time-varying-scalar** [default = False]) - If ``True``, set a patch for inner core transport coefficients below - ``rho_inner``. Typically used as an ad-hoc measure for MHD (e.g. sawteeth) or - EM (e.g. KBM) transport in the inner-core. If using a - `CombinedTransportModel`, ensure that the inner patch is only set on the - global model rather than its component models to avoid conflicts. - -``D_e_inner`` (**time-varying-scalar** [default = 0.2]) - Particle diffusivity value for inner transport patch. - -``V_e_inner`` (**time-varying-scalar** [default = 0.0]) - Particle convection value for inner transport patch. - -``chi_i_inner`` (**time-varying-scalar** [default = 1.0]) - Ion heat conduction value for inner transport patch. - -``chi_e_inner`` (**time-varying-scalar** [default = 1.0]) - Electron heat conduction value for inner transport patch. - -``rho_inner`` (**time-varying-scalar** [default = 0.3]) - :math:`\hat{\rho}` below which inner patch is applied. - Note that ``rho_inner`` and ``rho_outer`` must have the same interpolation - mode to simplify the validation test ``rho_inner < rho_outer`` at all times. - -``apply_outer_patch`` (**time-varying-scalar** [default = False]) - If ``True``, set a patch for outer core transport coefficients above - ``rho_outer``. Useful for the L-mode near-edge region where models like - QLKNN10D are not applicable. Only used if ``set_pedestal==False``. - If using a `CombinedTransportModel`, ensure that the outer patch is - only set on the global model rather than its component models to avoid - conflicts. - -``D_e_outer`` (**time-varying-scalar** [default = 0.2]) - Particle diffusivity value for outer transport patch. - -``V_e_outer`` (**time-varying-scalar** [default = 0.0]) - Particle convection value for outer transport patch. - -``chi_i_outer`` (**time-varying-scalar** [default = 1.0]) - Ion heat conduction value for outer transport patch. - -``chi_e_outer`` (**time-varying-scalar** [default = 1.0]) - Electron heat conduction value for outer transport patch. - -``rho_outer`` (**time-varying-scalar** [default = 0.9]) - :math:`\hat{\rho}` above which outer patch is applied. - Note that ``rho_inner`` and ``rho_outer`` must have the same interpolation - mode to simplify the validation test ``rho_inner < rho_outer`` at all times. - ``fast_ion_stabilization`` (**time-varying-scalar** [default = False]) If ``True``, apply a fast ion stabilization correction to the :math:`R/L_{Ti}` input of quasilinear transport models (QLKNN, TGLFNN, QuaLiKiz). The fast ion @@ -3003,18 +2951,6 @@ CHEASE geometry), is shown below. The configuration file is also available in }, 'transport': { 'model_name': 'qlknn', - 'apply_inner_patch': True, - 'D_e_inner': 0.25, - 'V_e_inner': 0.0, - 'chi_i_inner': 1.5, - 'chi_e_inner': 1.5, - 'rho_inner': 0.3, - 'apply_outer_patch': True, - 'D_e_outer': 0.1, - 'V_e_outer': 0.0, - 'chi_i_outer': 2.0, - 'chi_e_outer': 2.0, - 'rho_outer': 0.9, 'chi_min': 0.05, 'chi_max': 100, 'D_e_min': 0.05, diff --git a/torax/_src/config/runtime_params.py b/torax/_src/config/runtime_params.py index 72aae9548..b7a289109 100644 --- a/torax/_src/config/runtime_params.py +++ b/torax/_src/config/runtime_params.py @@ -86,7 +86,7 @@ class RuntimeParams: profile_conditions: profile_conditions.RuntimeParams solver: solver_params.RuntimeParams sources: Mapping[str, sources_params.RuntimeParams] - transport: transport_model_params.RuntimeParams + transport: transport_model_params.CombinedRuntimeParams time_step_calculator: time_step_calculator_runtime_params.RuntimeParams diff --git a/torax/_src/models.py b/torax/_src/models.py index 440612c35..25213c33f 100644 --- a/torax/_src/models.py +++ b/torax/_src/models.py @@ -22,7 +22,7 @@ from torax._src.pedestal_model import pedestal_model as pedestal_model_lib from torax._src.sources import source_models as source_models_lib from torax._src.time_step_calculator.time_step_calculator import TimeStepCalculator -from torax._src.transport_model import transport_model as transport_model_lib +from torax._src.transport_model import combined as combined_lib @dataclasses.dataclass(frozen=True, eq=False) @@ -36,7 +36,7 @@ class Models(static_dataclass.StaticDataclass): """ source_models: source_models_lib.SourceModels - transport_model: transport_model_lib.TransportModel + transport_model: combined_lib.CombinedTransportModel pedestal_model: pedestal_model_lib.PedestalModel neoclassical_models: neoclassical_models_lib.NeoclassicalModels mhd_models: mhd_model_lib.MHDModels diff --git a/torax/_src/transport_model/combined.py b/torax/_src/transport_model/combined.py index f45dd448d..d4dba4d20 100644 --- a/torax/_src/transport_model/combined.py +++ b/torax/_src/transport_model/combined.py @@ -17,15 +17,16 @@ A class for combining transport models. """ +# pylint: disable=invalid-name + import dataclasses -from typing import Callable, Sequence, Tuple -import chex +from typing import Callable, Sequence import jax import jax.numpy as jnp -from torax._src import array_typing from torax._src import constants from torax._src import jax_utils from torax._src import state +from torax._src import static_dataclass from torax._src.config import runtime_params as runtime_params_lib from torax._src.geometry import geometry from torax._src.pedestal_model import pedestal_model_output as pedestal_model_output_lib @@ -37,27 +38,8 @@ MIN_SMOOTHING_WIDTH = 1e-5 -@chex.dataclass -class SmoothingZoneParams: - rho_min: array_typing.FloatScalar - rho_max: array_typing.FloatScalar - smoothing_width: array_typing.FloatScalar - - -@jax.tree_util.register_dataclass -@dataclasses.dataclass(frozen=True) -class RuntimeParams(transport_runtime_params_lib.RuntimeParams): - """Runtime parameters for the CombinedTransportModel.""" - - transport_model_params: Tuple[transport_runtime_params_lib.RuntimeParams, ...] - pedestal_transport_model_params: Tuple[ - transport_runtime_params_lib.RuntimeParams, ... - ] - smoothing_zones: Tuple[SmoothingZoneParams, ...] - - @dataclasses.dataclass(frozen=True, eq=False) -class CombinedTransportModel(transport_model_lib.TransportModel): +class CombinedTransportModel(static_dataclass.StaticDataclass): """Combines coefficients from a tuple of transport models.""" transport_models: tuple[transport_model_lib.TransportModel, ...] @@ -82,23 +64,12 @@ def __call__( core_profiles, pedestal_model_output, ) - - # In contrast to the base TransportModel, we do not apply domain restriction - # or output masking (enabled/disabled channels) as these are handled at the - # component model level in call_implementation here. - # Apply min/max clipping transport_coeffs = self._apply_clipping( transport_runtime_params, transport_coeffs, ) - # In contrast to the base TransportModel, we do not apply patches, as these - # should be handled by instantiating constant component models instead. - # However, the rho_inner and rho_outer arguments are currently required - # in the case where the inner/outer region are to be excluded from - # smoothing. - transport_coeffs = self._smooth_coeffs( runtime_params, geo, @@ -110,7 +81,7 @@ def __call__( def call_implementation( self, - transport_runtime_params: transport_runtime_params_lib.RuntimeParams, + transport_runtime_params: transport_runtime_params_lib.CombinedRuntimeParams, runtime_params: runtime_params_lib.RuntimeParams, geo: geometry.Geometry, core_profiles: state.CoreProfiles, @@ -120,7 +91,8 @@ def call_implementation( Args: transport_runtime_params: Input runtime parameters for this transport - model. Can change without triggering a JAX recompilation. + model (expected to be an instance of CombinedRuntimeParams at runtime). + Can change without triggering a JAX recompilation. runtime_params: Runtime parameters for the simulation at the current time. geo: Geometry of the torus at the current time. core_profiles: Core plasma profiles. @@ -129,9 +101,6 @@ def call_implementation( Returns: coeffs: The transport coefficients """ - # Required for pytype - assert isinstance(transport_runtime_params, RuntimeParams) - core_coeffs = self._combine( self.transport_models, transport_runtime_params.transport_model_params, @@ -255,6 +224,41 @@ def _combine( return transport_model_lib.TurbulentTransport(**accumulators) + def _apply_clipping( + self, + transport_runtime_params: transport_runtime_params_lib.CombinedRuntimeParams, + transport_coeffs: transport_model_lib.TurbulentTransport, + ) -> transport_model_lib.TurbulentTransport: + """Applies min/max clipping to transport coefficients for PDE stability.""" + chi_face_ion = jnp.clip( + transport_coeffs.chi_face_ion, + transport_runtime_params.chi_min, + transport_runtime_params.chi_max, + ) + chi_face_el = jnp.clip( + transport_coeffs.chi_face_el, + transport_runtime_params.chi_min, + transport_runtime_params.chi_max, + ) + d_face_el = jnp.clip( + transport_coeffs.d_face_el, + transport_runtime_params.D_e_min, + transport_runtime_params.D_e_max, + ) + v_face_el = jnp.clip( + transport_coeffs.v_face_el, + transport_runtime_params.V_e_min, + transport_runtime_params.V_e_max, + ) + + return dataclasses.replace( + transport_coeffs, + chi_face_ion=chi_face_ion, + chi_face_el=chi_face_el, + d_face_el=d_face_el, + v_face_el=v_face_el, + ) + def _smooth_coeffs( self, runtime_params: runtime_params_lib.RuntimeParams, @@ -263,7 +267,6 @@ def _smooth_coeffs( pedestal_model_output: pedestal_model_output_lib.PedestalModelOutput, ) -> transport_model_lib.TurbulentTransport: """Gaussian smoothing of turbulent transport coefficients.""" - assert isinstance(runtime_params.transport, RuntimeParams) smoothing_matrix = _build_smoothing_matrix( runtime_params.transport, runtime_params, @@ -280,7 +283,7 @@ def smooth_single_coeff(coeff): lambda: jnp.dot(smoothing_matrix, coeff), ) - return jax.tree_util.tree_map(smooth_single_coeff, transport_coeffs) + return jax.tree.map(smooth_single_coeff, transport_coeffs) def _add_optional( @@ -305,14 +308,16 @@ def _pedestal_domain_mask( def _build_smoothing_matrix( - transport_runtime_params: RuntimeParams, + transport_runtime_params: ( + transport_runtime_params_lib.CombinedRuntimeParams + ), runtime_params: runtime_params_lib.RuntimeParams, geo: geometry.Geometry, pedestal_model_output: pedestal_model_output_lib.PedestalModelOutput, ) -> jax.Array: """Builds a smoothing matrix for the combined transport model.""" # To reduce the range of the convolution, weights under lower_cutoff are - # clipped to zero + # clipped to zero. lower_cutoff = 0.01 # used for eps, small number to avoid divisions by zero for sigma = 0 consts = constants.CONSTANTS diff --git a/torax/_src/transport_model/pydantic_model.py b/torax/_src/transport_model/pydantic_model.py index 631232e12..0029c0ee0 100644 --- a/torax/_src/transport_model/pydantic_model.py +++ b/torax/_src/transport_model/pydantic_model.py @@ -16,7 +16,6 @@ import copy import dataclasses -import itertools from typing import Annotated, Any, Literal, Sequence from absl import logging import chex @@ -34,6 +33,7 @@ from torax._src.transport_model import qlknn_10d from torax._src.transport_model import qlknn_transport_model from torax._src.transport_model import qualikiz_based_transport_model +from torax._src.transport_model import runtime_params from torax._src.transport_model import tglfnn_ukaea_transport_model from torax._src.transport_model.tglf import tglf_transport_model import typing_extensions @@ -170,9 +170,6 @@ def _conform_data(cls, data: Any) -> Any: # The QLK version this specific QLKNN was trained on tends to # underpredict ITG electron heat flux in shaped, high-beta scenarios. data['ITG_flux_ratio_correction'] = 2.0 - else: - if 'smoothing_width' not in data: - data['smoothing_width'] = 0.1 return data def build_transport_model(self) -> qlknn_transport_model.QLKNNTransportModel: @@ -457,14 +454,19 @@ class SmoothingZone(torax_pydantic.BaseModelFrozen): smoothing_width: pydantic.NonNegativeFloat -class CombinedTransportModel(pydantic_model_base.TransportBase): +class CombinedTransportModel(torax_pydantic.BaseModelFrozen): """Model for the Combined transport model. - Note: smoothing and patches should be applied on the combined model, not the - individual component models. - Attributes: model_name: The transport model to use. Hardcoded to 'combined'. + chi_min: Lower bound on heat conductivity. + chi_max: Upper bound on heat conductivity (can be helpful for stability). + D_e_min: minimum electron density diffusivity. + D_e_max: maximum electron density diffusivity. + V_e_min: minimum electron density convection. + V_e_max: maximum electron density convection. + smoothing_width: Width of HWHM Gaussian smoothing kernel operating on + transport model outputs. transport_models: A sequence of transport models, whose outputs will be summed to give the combined core transport coefficients. pedestal_transport_models: A sequence of models that will be combined for @@ -475,6 +477,13 @@ class CombinedTransportModel(pydantic_model_base.TransportBase): means that zone will not be used for the smoothing of other zones. """ + chi_min: torax_pydantic.MeterSquaredPerSecond = 0.05 + chi_max: torax_pydantic.MeterSquaredPerSecond = 100.0 + D_e_min: torax_pydantic.MeterSquaredPerSecond = 0.05 + D_e_max: torax_pydantic.MeterSquaredPerSecond = 100.0 + V_e_min: torax_pydantic.MeterPerSecond = -50.0 + V_e_max: torax_pydantic.MeterPerSecond = 50.0 + smoothing_width: pydantic.NonNegativeFloat = 0.0 # TODO(b/434175938) V2: rename `transport_models` to `core_transport_models` transport_models: Sequence[CombinedCompatibleTransportModel] = pydantic.Field( default_factory=list @@ -505,8 +514,9 @@ def build_transport_model(self) -> combined.CombinedTransportModel: pedestal_transport_models=pedestal_transport_models, ) - def build_runtime_params(self, t: chex.Numeric) -> combined.RuntimeParams: - base_kwargs = dataclasses.asdict(super().build_runtime_params(t)) + def build_runtime_params( + self, t: chex.Numeric + ) -> runtime_params.CombinedRuntimeParams: transport_model_params = tuple( model.build_runtime_params(t) for model in self.transport_models ) @@ -518,35 +528,25 @@ def build_runtime_params(self, t: chex.Numeric) -> combined.RuntimeParams: smoothing_zones = [] for zone in self.smoothing_zones: smoothing_zones.append( - combined.SmoothingZoneParams( + runtime_params.SmoothingZoneParams( rho_min=zone.rho_min, rho_max=zone.rho_max, smoothing_width=zone.smoothing_width, ) ) - return combined.RuntimeParams( + return runtime_params.CombinedRuntimeParams( + chi_min=self.chi_min, + chi_max=self.chi_max, + D_e_min=self.D_e_min, + D_e_max=self.D_e_max, + V_e_min=self.V_e_min, + V_e_max=self.V_e_max, + smoothing_width=self.smoothing_width, transport_model_params=transport_model_params, pedestal_transport_model_params=pedestal_transport_model_params, smoothing_zones=tuple(smoothing_zones), - **base_kwargs, ) - @pydantic.model_validator(mode='after') - def _check_no_smoothing_in_components(self) -> typing_extensions.Self: - for model_list in ['transport_models', 'pedestal_transport_models']: - for i, model in enumerate(getattr(self, model_list)): - if model.smoothing_width > 0.0: - logging.warning( - 'smoothing_width > 0.0 is not supported for component models of' - ' CombinedTransportModel; instead, smoothing_width should be set' - ' on the CombinedTransportModel itself. Smoothing width set on %s' - ' component %i (%s) will be ignored.', - model_list, - i, - model.model_name, - ) - return self - @pydantic.model_validator(mode='after') def _check_smoothing_width_minimum(self) -> typing_extensions.Self: smoothing_widths = [ @@ -566,29 +566,24 @@ def _check_smoothing_width_minimum(self) -> typing_extensions.Self: @pydantic.model_validator(mode='after') def _check_fields(self) -> typing_extensions.Self: - super()._check_fields() # pyrefly: ignore[not-callable] - if ( - any([ - np.any(model.apply_inner_patch.value) - or np.any(model.apply_outer_patch.value) - # Use itertools.chain to iterate over both lists of models without - # needing to make a new list. - for model in itertools.chain( - self.transport_models, self.pedestal_transport_models - ) - ]) - or np.any(self.apply_inner_patch.value) - or np.any(self.apply_outer_patch.value) - ): - raise ValueError( - 'apply_inner_patch and apply_outer_patch not supported for' - ' CombinedTransportModel or its component models.' - ) - if np.any(self.rho_min.value != 0.0) or np.any(self.rho_max.value != 1.0): - raise ValueError( - 'rho_min and rho_max should not be set for CombinedTransportModel, as' - ' it should be applied across the whole rho domain.' + if not self.chi_min < self.chi_max: + raise ValueError('chi_min must be less than chi_max.') + if not self.D_e_min < self.D_e_max: + raise ValueError('D_e_min must be less than D_e_max.') + if not self.V_e_min < self.V_e_max: + raise ValueError('V_e_min must be less than V_e_max.') + if self.smoothing_width == 0.0: + has_qlknn = any( + isinstance(m, qlknn_transport_model.QLKNNTransportModel) + for m in list(self.transport_models) + + list(self.pedestal_transport_models) ) + if has_qlknn: + logging.warning( + 'QLKNN transport model is configured in CombinedTransportModel' + ' with smoothing_width=0. Stiff QLKNN transport coefficients' + ' without spatial smoothing may degrade solver convergence.' + ) if any([ np.any(model.rho_min.value != 0.0) or np.any(model.rho_max.value != 1.0) for model in self.pedestal_transport_models diff --git a/torax/_src/transport_model/pydantic_model_base.py b/torax/_src/transport_model/pydantic_model_base.py index a5da19131..20a1ad7aa 100644 --- a/torax/_src/transport_model/pydantic_model_base.py +++ b/torax/_src/transport_model/pydantic_model_base.py @@ -32,96 +32,28 @@ class TransportBase(torax_pydantic.BaseModelFrozen, abc.ABC): """Base model holding parameters common to all transport models. Attributes: - chi_min: Lower bound on heat conductivity. - chi_max: Upper bound on heat conductivity (can be helpful for stability). - D_e_min: minimum electron density diffusivity. - D_e_max: maximum electron density diffusivity. - V_e_min: minimum electron density convection. - V_e_max: minimum electron density convection. rho_min: normalized radius above which this model is applied. rho_max: normalized radius below which this model is applied. - apply_inner_patch: set inner core transport coefficients (ad-hoc MHD/EM - transport). - D_e_inner: inner core electron density diffusivity. - V_e_inner: inner core electron density convection. - chi_i_inner: inner core ion heat equation diffusion term. - chi_e_inner: inner core electron heat equation diffusion term. - rho_inner: normalized radius below which inner patch is applied. - apply_outer_patch: set outer core transport coefficients (ad-hoc MHD/EM - transport). Only used when pedestal.set_pedestal = False Useful for L-mode - near-edge region where QLKNN10D is not applicable. - D_e_outer: outer core electron density diffusivity. - V_e_outer: outer core electron density convection. - chi_i_outer: outer core ion heat equation diffusion term. - chi_e_outer: outer core electron heat equation diffusion term. - rho_outer: normalized radius above which outer patch is applied. - smoothing_width: Width of HWHM Gaussian smoothing kernel operating on - transport model outputs. - smooth_everywhere: Smooth across entire radial domain regardless of inner - and outer patches. disable_chi_i: If True, sets the ion heat conductivity output to zero. disable_chi_e: If True, sets the electron heat conductivity output to zero. disable_D_e: If True, sets the electron diffusivity output to zero. disable_V_e: If True, sets the electron convection output to zero. - merge_mode: Defines how this model is combined with previous models in a + fast_ion_stabilization: If True, applies fast ion stabilization. + fast_ion_stabilization_model: Fast ion stabilization model config. + fast_ion_stabilization_multiplier: Fast ion stabilization multiplier. + merge_mode: Defines how transport coefficients are combined within a CombinedTransportModel. 'add' (default) adds to the accumulated value. 'overwrite' overwrites the previous value in this model's valid domain and prevents subsequent 'add' models in the sequence from modifying this region. """ - chi_min: torax_pydantic.MeterSquaredPerSecond = 0.05 - chi_max: torax_pydantic.MeterSquaredPerSecond = 100.0 - D_e_min: torax_pydantic.MeterSquaredPerSecond = 0.05 - D_e_max: torax_pydantic.MeterSquaredPerSecond = 100.0 - V_e_min: torax_pydantic.MeterPerSecond = -50.0 - V_e_max: torax_pydantic.MeterPerSecond = 50.0 rho_min: torax_pydantic.UnitIntervalTimeVaryingScalar = ( torax_pydantic.ValidatedDefault(0.0) ) rho_max: torax_pydantic.UnitIntervalTimeVaryingScalar = ( torax_pydantic.ValidatedDefault(1.0) ) - # TODO(b/434175938): Remove patch mechanism in V2 due to duplication with - # combined transport model framework. - apply_inner_patch: interpolated_param_1d.TimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(False) - ) - D_e_inner: torax_pydantic.PositiveTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(0.2) - ) - V_e_inner: interpolated_param_1d.TimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(0.0) - ) - chi_i_inner: torax_pydantic.PositiveTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(1.0) - ) - chi_e_inner: torax_pydantic.PositiveTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(1.0) - ) - rho_inner: torax_pydantic.UnitIntervalTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(0.3) - ) - apply_outer_patch: interpolated_param_1d.TimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(False) - ) - D_e_outer: interpolated_param_1d.PositiveTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(0.2) - ) - V_e_outer: interpolated_param_1d.TimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(0.0) - ) - chi_i_outer: interpolated_param_1d.PositiveTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(1.0) - ) - chi_e_outer: interpolated_param_1d.PositiveTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(1.0) - ) - rho_outer: torax_pydantic.UnitIntervalTimeVaryingScalar = ( - torax_pydantic.ValidatedDefault(0.9) - ) - smoothing_width: pydantic.NonNegativeFloat = 0.0 - smooth_everywhere: bool = False disable_chi_i: interpolated_param_1d.TimeVaryingScalar = ( torax_pydantic.ValidatedDefault(False) ) @@ -148,34 +80,17 @@ class TransportBase(torax_pydantic.BaseModelFrozen, abc.ABC): @pydantic.model_validator(mode='after') def _check_fields(self) -> typing_extensions.Self: - if not self.chi_max > self.chi_min: - raise ValueError('chi_min must be less than chi_max.') - if not self.D_e_min < self.D_e_max: - raise ValueError('D_e_min must be less than D_e_max.') - if not self.V_e_min < self.V_e_max: - raise ValueError('V_e_min must be less than V_e_max.') - # For the time-varying parameter pairs (rho_min, rho_max), - # (rho_inner, rho_outer), we have relative magnitude constraints. These need - # to be held at all times. We validate this by checking the inequality at - # the combined time points (knots) of each pair. This is sufficient - # both for STEP and PIECEWISE_LINEAR interpolation modes. However, if the - # interpolation modes are mixed, the constraint check becomes more - # complicated. For now, we only support the same interpolation mode for - # these pairs. - if self.rho_outer.interpolation_mode != self.rho_inner.interpolation_mode: - raise ValueError( - 'rho_outer and rho_inner must have the same interpolation mode.' - ) + # For the time-varying parameter pair (rho_min, rho_max), we have relative + # magnitude constraints that must hold at all times. We validate this by + # checking the inequality at the combined time points (knots) of the pair. + # This check is sufficient to guarantee the inequality at all times + # provided that both parameters use the same interpolation mode (either step + # or linear interpolation). We therefore require their interpolation modes + # to match. if self.rho_max.interpolation_mode != self.rho_min.interpolation_mode: raise ValueError( 'rho_max and rho_min must have the same interpolation mode.' ) - all_times_inner_outer = np.union1d(self.rho_inner.time, self.rho_outer.time) - if not np.all( - self.rho_outer.get_value(all_times_inner_outer) - > self.rho_inner.get_value(all_times_inner_outer) - ): - raise ValueError('rho_outer must be greater than rho_inner for all time.') all_times_min_max = np.union1d(self.rho_min.time, self.rho_max.time) if not np.all( self.rho_max.get_value(all_times_min_max) @@ -195,28 +110,8 @@ def build_runtime_params( ) ), fast_ion_stabilization_multiplier=self.fast_ion_stabilization_multiplier, - chi_min=self.chi_min, - chi_max=self.chi_max, - D_e_min=self.D_e_min, - D_e_max=self.D_e_max, - V_e_min=self.V_e_min, - V_e_max=self.V_e_max, rho_min=self.rho_min.get_value(t), rho_max=self.rho_max.get_value(t), - apply_inner_patch=self.apply_inner_patch.get_value(t), - D_e_inner=self.D_e_inner.get_value(t), - V_e_inner=self.V_e_inner.get_value(t), - chi_i_inner=self.chi_i_inner.get_value(t), - chi_e_inner=self.chi_e_inner.get_value(t), - rho_inner=self.rho_inner.get_value(t), - apply_outer_patch=self.apply_outer_patch.get_value(t), - D_e_outer=self.D_e_outer.get_value(t), - V_e_outer=self.V_e_outer.get_value(t), - chi_i_outer=self.chi_i_outer.get_value(t), - chi_e_outer=self.chi_e_outer.get_value(t), - rho_outer=self.rho_outer.get_value(t), - smoothing_width=self.smoothing_width, - smooth_everywhere=self.smooth_everywhere, disable_chi_i=self.disable_chi_i.get_value(t), disable_chi_e=self.disable_chi_e.get_value(t), disable_D_e=self.disable_D_e.get_value(t), diff --git a/torax/_src/transport_model/runtime_params.py b/torax/_src/transport_model/runtime_params.py index 381e516f0..662672a4f 100644 --- a/torax/_src/transport_model/runtime_params.py +++ b/torax/_src/transport_model/runtime_params.py @@ -30,28 +30,8 @@ class RuntimeParams: """Input params for the transport model which can be used as compiled args.""" - chi_min: float - chi_max: float - D_e_min: float - D_e_max: float - V_e_min: float - V_e_max: float rho_min: array_typing.FloatScalar rho_max: array_typing.FloatScalar - apply_inner_patch: array_typing.BoolScalar - D_e_inner: array_typing.FloatScalar - V_e_inner: array_typing.FloatScalar - chi_i_inner: array_typing.FloatScalar - chi_e_inner: array_typing.FloatScalar - rho_inner: array_typing.FloatScalar - apply_outer_patch: array_typing.BoolScalar - D_e_outer: array_typing.FloatScalar - V_e_outer: array_typing.FloatScalar - chi_i_outer: array_typing.FloatScalar - chi_e_outer: array_typing.FloatScalar - rho_outer: array_typing.FloatScalar - smoothing_width: float - smooth_everywhere: bool disable_chi_i: array_typing.BoolScalar disable_chi_e: array_typing.BoolScalar disable_D_e: array_typing.BoolScalar @@ -62,3 +42,30 @@ class RuntimeParams: ) fast_ion_stabilization_multiplier: float merge_mode: enums.MergeMode = dataclasses.field(metadata={'static': True}) + + +@jax.tree_util.register_dataclass +@dataclasses.dataclass(frozen=True) +class SmoothingZoneParams: + """Runtime parameters for a radial smoothing zone.""" + + rho_min: array_typing.FloatScalar + rho_max: array_typing.FloatScalar + smoothing_width: float + + +@jax.tree_util.register_dataclass +@dataclasses.dataclass(frozen=True) +class CombinedRuntimeParams: + """Runtime parameters for the CombinedTransportModel.""" + + chi_min: float + chi_max: float + D_e_min: float + D_e_max: float + V_e_min: float + V_e_max: float + smoothing_width: float + transport_model_params: tuple[RuntimeParams, ...] + pedestal_transport_model_params: tuple[RuntimeParams, ...] + smoothing_zones: tuple[SmoothingZoneParams, ...] diff --git a/torax/_src/transport_model/tests/combined_test.py b/torax/_src/transport_model/tests/combined_test.py index a7aeeff76..05162575c 100644 --- a/torax/_src/transport_model/tests/combined_test.py +++ b/torax/_src/transport_model/tests/combined_test.py @@ -24,6 +24,7 @@ from torax._src.torax_pydantic import model_config from torax._src.transport_model import combined from torax._src.transport_model import enums +from torax._src.transport_model import runtime_params as transport_runtime_params_lib from torax._src.transport_model import transport_model as transport_model_lib @@ -151,7 +152,10 @@ def test_build_smoothing_matrix_zero_width_is_identity(self): instance=True, rho_norm_ped_top=0.91, ) - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) matrix = combined._build_smoothing_matrix( runtime_params.transport, runtime_params, @@ -175,7 +179,10 @@ def test_build_smoothing_matrix_row_sums_and_constant_invariance(self): instance=True, rho_norm_ped_top=0.91, ) - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) matrix = combined._build_smoothing_matrix( runtime_params.transport, runtime_params, @@ -204,7 +211,10 @@ def test_build_smoothing_matrix_zone_isolation(self): instance=True, rho_norm_ped_top=0.91, ) - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) matrix = combined._build_smoothing_matrix( runtime_params.transport, runtime_params, @@ -243,7 +253,10 @@ def test_build_smoothing_matrix_pedestal_boundary_isolation(self): instance=True, rho_norm_ped_top=0.8, ) - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) matrix = combined._build_smoothing_matrix( runtime_params.transport, runtime_params, @@ -379,37 +392,6 @@ def test_smoothing_width_shortcut(self): ) ) - def test_error_if_patches_set_on_children(self): - config = default_configs.get_default_config_dict() - config['transport'] = { - 'model_name': 'combined', - 'transport_models': [ - {'model_name': 'constant', 'apply_inner_patch': True}, - {'model_name': 'constant'}, - ], - 'pedestal_transport_models': [{'model_name': 'constant'}], - } - with self.assertRaisesRegex( - ValueError, '(?=.*patch)(?=.*CombinedTransportModel)' - ): - model_config.ToraxConfig.from_dict(config) - - def test_error_if_patches_set_on_self(self): - config = default_configs.get_default_config_dict() - config['transport'] = { - 'model_name': 'combined', - 'transport_models': [ - {'model_name': 'constant'}, - {'model_name': 'constant'}, - ], - 'pedestal_transport_models': [{'model_name': 'constant'}], - 'apply_inner_patch': True, - } - with self.assertRaisesRegex( - ValueError, '(?=.*patch)(?=.*CombinedTransportModel)' - ): - model_config.ToraxConfig.from_dict(config) - def test_error_if_pedestal_model_defines_rho_min(self): config = default_configs.get_default_config_dict() config['transport'] = { @@ -610,7 +592,7 @@ def test_none_handling_in_combine(self): # We need a RuntimeParams for combined model combined_params = mock.create_autospec( - combined.RuntimeParams, instance=True + transport_runtime_params_lib.CombinedRuntimeParams, instance=True ) combined_params.transport_model_params = [mock_params] combined_params.pedestal_transport_model_params = [] diff --git a/torax/_src/transport_model/tests/pydantic_model_test.py b/torax/_src/transport_model/tests/pydantic_model_test.py index cdaf9890c..27aeab0dd 100644 --- a/torax/_src/transport_model/tests/pydantic_model_test.py +++ b/torax/_src/transport_model/tests/pydantic_model_test.py @@ -97,7 +97,6 @@ def test_qlknn_defaults(self): self.assertIsInstance( transport, transport_pydantic_model.QLKNNTransportModel ) - self.assertEqual(transport.smoothing_width, 0.1) self.assertEqual(transport.ETG_correction_factor, 1.0 / 3.0) @parameterized.parameters( @@ -150,39 +149,18 @@ def test_qlknn_model_errors(self, model_name, model_path): ) @parameterized.named_parameters( - ( - 'mixed_modes_inner_outer_fails', - {'rho_inner': 0.3, 'rho_outer': ({0: 0.9}, 'step')}, - 'rho_outer and rho_inner must have the same interpolation mode.', - True, - ), ( 'mixed_modes_min_max_fails', {'rho_min': 0.0, 'rho_max': ({0: 1.0}, 'step')}, 'rho_max and rho_min must have the same interpolation mode.', True, ), - ( - 'inner_greater_than_outer_fails', - {'rho_inner': 0.9, 'rho_outer': 0.3}, - 'rho_outer must be greater than rho_inner for all time.', - True, - ), ( 'min_greater_than_max_fails', {'rho_min': 0.9, 'rho_max': 0.3}, 'rho_max must be greater than rho_min for all time.', True, ), - ( - 'time_varying_inner_gt_outer_fails', - { - 'rho_inner': {0: 0.3, 1: 0.95}, - 'rho_outer': {0: 0.9, 1: 0.9}, - }, - 'rho_outer must be greater than rho_inner for all time.', - True, - ), ( 'time_varying_min_gt_max_fails', { @@ -195,8 +173,6 @@ def test_qlknn_model_errors(self, model_name, model_path): ( 'time_varying_linear_succeeds', { - 'rho_inner': {0: 0.2, 1: 0.3}, - 'rho_outer': {0: 0.8, 1: 0.9}, 'rho_min': {0: 0.0, 1: 0.1}, 'rho_max': {0: 1.0, 1: 0.95}, }, @@ -206,8 +182,6 @@ def test_qlknn_model_errors(self, model_name, model_path): ( 'time_varying_step_succeeds', { - 'rho_inner': ({0: 0.2, 1: 0.3}, 'step'), - 'rho_outer': ({0: 0.8, 1: 0.9}, 'step'), 'rho_min': ({0: 0.0, 1: 0.1}, 'step'), 'rho_max': ({0: 1.0, 1: 0.95}, 'step'), }, @@ -465,38 +439,6 @@ def test_invalid_overlapping_overwrite_part_time(self): ): transport_pydantic_model.CombinedTransportModel(transport_models=[m1, m2]) - def test_smoothing_in_combined_core_component_logs_warning(self): - component_model = transport_pydantic_model.ConstantTransportModel( - smoothing_width=0.1 - ) - with self.assertLogs(level='WARNING') as log_watcher: - transport_pydantic_model.CombinedTransportModel( - transport_models=[component_model] - ) - self.assertLen(log_watcher.output, 1) - self.assertIn( - 'smoothing_width > 0.0 is not supported for component models of' - ' CombinedTransportModel', - log_watcher.output[0], - ) - - def test_smoothing_in_combined_pedestal_component_logs_warning( - self, - ): - component_model = transport_pydantic_model.ConstantTransportModel( - smoothing_width=0.1 - ) - with self.assertLogs(level='WARNING') as log_watcher: - transport_pydantic_model.CombinedTransportModel( - pedestal_transport_models=[component_model] - ) - self.assertLen(log_watcher.output, 1) - self.assertIn( - 'smoothing_width > 0.0 is not supported for component models of' - ' CombinedTransportModel', - log_watcher.output[0], - ) - if __name__ == '__main__': absltest.main() diff --git a/torax/_src/transport_model/tests/qualikiz_based_transport_model_test.py b/torax/_src/transport_model/tests/qualikiz_based_transport_model_test.py index b0e752877..71684d310 100644 --- a/torax/_src/transport_model/tests/qualikiz_based_transport_model_test.py +++ b/torax/_src/transport_model/tests/qualikiz_based_transport_model_test.py @@ -32,10 +32,10 @@ from torax._src.test_utils import default_configs from torax._src.torax_pydantic import model_config from torax._src.torax_pydantic import torax_pydantic -from torax._src.transport_model import combined from torax._src.transport_model import pydantic_model_base as transport_pydantic_model_base from torax._src.transport_model import qualikiz_based_transport_model from torax._src.transport_model import register_model +from torax._src.transport_model import runtime_params as transport_runtime_params_lib from torax._src.transport_model import transport_model as transport_model_lib @@ -130,7 +130,10 @@ def test_qualikiz_based_transport_model_prepare_qualikiz_inputs_shapes(self): qualikiz_based_transport_model.QualikizBasedTransportModel, ) runtime_params, geo, core_profiles, _ = model_inputs - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) qualikiz_params = runtime_params.transport.transport_model_params[0] assert isinstance( qualikiz_params, qualikiz_based_transport_model.RuntimeParams @@ -195,8 +198,14 @@ def test_max_normalized_collisionality_caps_nu_star(self): ) runtime_params_uncapped, geo, core_profiles, _ = uncapped_inputs runtime_params_capped, _, _, _ = capped_inputs - assert isinstance(runtime_params_uncapped.transport, combined.RuntimeParams) - assert isinstance(runtime_params_capped.transport, combined.RuntimeParams) + assert isinstance( + runtime_params_uncapped.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) + assert isinstance( + runtime_params_capped.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) qualikiz_params_uncapped = ( runtime_params_uncapped.transport.transport_model_params[0] diff --git a/torax/_src/transport_model/tests/tglf_based_transport_model_test.py b/torax/_src/transport_model/tests/tglf_based_transport_model_test.py index 712613bec..bb82e9998 100644 --- a/torax/_src/transport_model/tests/tglf_based_transport_model_test.py +++ b/torax/_src/transport_model/tests/tglf_based_transport_model_test.py @@ -32,9 +32,9 @@ from torax._src.test_utils import default_configs from torax._src.torax_pydantic import model_config from torax._src.torax_pydantic import torax_pydantic -from torax._src.transport_model import combined from torax._src.transport_model import pydantic_model_base as transport_pydantic_model_base from torax._src.transport_model import register_model +from torax._src.transport_model import runtime_params as transport_runtime_params_lib from torax._src.transport_model import tglf_based_transport_model from torax._src.transport_model import transport_model as transport_model_lib from torax._src.transport_model.tglf import tglf2py @@ -128,7 +128,10 @@ def test_tglf_based_transport_model_prepare_tglf_inputs_shapes(self): transport_model, tglf_based_transport_model.TGLFBasedTransportModel ) runtime_params, geo, core_profiles, _ = model_inputs - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) tglf_params = runtime_params.transport.transport_model_params[0] assert isinstance( tglf_params, tglf_based_transport_model.RuntimeParams @@ -173,8 +176,14 @@ def test_max_normalized_collisionality_caps_xnue(self): ) runtime_uncapped, geo, core_profiles, _ = uncapped_inputs runtime_capped, _, _, _ = capped_inputs - assert isinstance(runtime_uncapped.transport, combined.RuntimeParams) - assert isinstance(runtime_capped.transport, combined.RuntimeParams) + assert isinstance( + runtime_uncapped.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) + assert isinstance( + runtime_capped.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) tglf_params_uncapped = ( runtime_uncapped.transport.transport_model_params[0] diff --git a/torax/_src/transport_model/tests/transport_model_test.py b/torax/_src/transport_model/tests/transport_model_test.py index 6bbcc5b57..2d81fe77f 100644 --- a/torax/_src/transport_model/tests/transport_model_test.py +++ b/torax/_src/transport_model/tests/transport_model_test.py @@ -30,7 +30,6 @@ from torax._src.test_utils import default_configs from torax._src.torax_pydantic import model_config from torax._src.torax_pydantic import torax_pydantic -from torax._src.transport_model import combined from torax._src.transport_model import pydantic_model_base as transport_pydantic_model_base from torax._src.transport_model import register_model from torax._src.transport_model import runtime_params as transport_runtime_params_lib @@ -245,9 +244,12 @@ def test_preserves_none_enabled(self): chi_face_ion_bohm=None, ) - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) # Test preservation when enabled - new_coeffs = model.zero_out_disabled_channels( + new_coeffs = model.transport_models[0].zero_out_disabled_channels( runtime_params.transport.transport_model_params[0], coeffs ) self.assertIsNone(new_coeffs.chi_face_ion_bohm) @@ -264,7 +266,10 @@ def test_preserves_none_disabled(self): runtime_params = build_runtime_params.RuntimeParamsProvider.from_config( torax_config )(t=0.0) - assert isinstance(runtime_params.transport, combined.RuntimeParams) + assert isinstance( + runtime_params.transport, + transport_runtime_params_lib.CombinedRuntimeParams, + ) coeffs = transport_model_lib.TurbulentTransport( chi_face_ion=jnp.array([1.0]), @@ -279,7 +284,7 @@ def test_preserves_none_disabled(self): runtime_params.transport.transport_model_params[0], disable_chi_i=True, ) - new_coeffs_disabled = model.zero_out_disabled_channels( + new_coeffs_disabled = model.transport_models[0].zero_out_disabled_channels( disabled_params, coeffs ) self.assertIsNone(new_coeffs_disabled.chi_face_ion_bohm) diff --git a/torax/_src/transport_model/transport_coefficients_builder.py b/torax/_src/transport_model/transport_coefficients_builder.py index 569cfef40..c8060861a 100644 --- a/torax/_src/transport_model/transport_coefficients_builder.py +++ b/torax/_src/transport_model/transport_coefficients_builder.py @@ -24,8 +24,8 @@ from torax._src.neoclassical import neoclassical_models as neoclassical_models_lib from torax._src.pedestal_model import pedestal_transition_state as pedestal_transition_state_lib from torax._src.pedestal_model import runtime_params as pedestal_runtime_params_lib +from torax._src.transport_model import combined from torax._src.transport_model import pereverzev as pereverzev_lib -from torax._src.transport_model import transport_model as transport_model_lib # pylint: disable=invalid-name @@ -37,7 +37,7 @@ ) ) def calculate_all_transport_coeffs( - transport_model: transport_model_lib.TransportModel, + transport_model: combined.CombinedTransportModel, neoclassical_models: neoclassical_models_lib.NeoclassicalModels, runtime_params: runtime_params_lib.RuntimeParams, geo: geometry.Geometry, diff --git a/torax/_src/transport_model/transport_model.py b/torax/_src/transport_model/transport_model.py index ccb63feb8..9fa87409a 100644 --- a/torax/_src/transport_model/transport_model.py +++ b/torax/_src/transport_model/transport_model.py @@ -25,7 +25,6 @@ import immutabledict import jax from jax import numpy as jnp -from torax._src import constants from torax._src import state from torax._src import static_dataclass from torax._src.config import runtime_params as runtime_params_lib @@ -122,61 +121,6 @@ class TurbulentTransport: class TransportModel(static_dataclass.StaticDataclass, abc.ABC): """Calculates various coefficients related to heat and particle transport.""" - def __call__( - self, - runtime_params: runtime_params_lib.RuntimeParams, - geo: geometry.Geometry, - core_profiles: state.CoreProfiles, - pedestal_model_output: pedestal_model_output_lib.PedestalModelOutput, - ) -> TurbulentTransport: - transport_runtime_params = runtime_params.transport - - # Calculate the transport coefficients - transport_coeffs = self.call_implementation( - transport_runtime_params, - runtime_params, - geo, - core_profiles, - pedestal_model_output, - ) - - # Apply masking to selectively enable/disable specific channels - transport_coeffs = self.zero_out_disabled_channels( - transport_runtime_params, transport_coeffs - ) - - # Restrict the model to operating in its permissible rho domain - transport_coeffs = self._apply_domain_restriction( - transport_runtime_params, - runtime_params, - geo, - transport_coeffs, - pedestal_model_output, - ) - - # Apply min/max clipping - transport_coeffs = self._apply_clipping( - transport_runtime_params, - transport_coeffs, - ) - - # Apply inner and outer transport patch - transport_coeffs = self._apply_transport_patches( - transport_runtime_params, - runtime_params, - geo, - transport_coeffs, - ) - - transport_coeffs = self._smooth_coeffs( - runtime_params, - geo, - transport_coeffs, - pedestal_model_output, - ) - - return transport_coeffs - @abc.abstractmethod def call_implementation( self, @@ -212,304 +156,6 @@ def zero_out_disabled_channels( return dataclasses.replace(transport_coeffs, **to_replace) - def _apply_domain_restriction( - self, - transport_runtime_params: transport_runtime_params_lib.RuntimeParams, - runtime_params: runtime_params_lib.RuntimeParams, - geo: geometry.Geometry, - transport_coeffs: TurbulentTransport, - pedestal_model_output: pedestal_model_output_lib.PedestalModelOutput, - ) -> TurbulentTransport: - """Sets transport coefficients to zero outside the model's domain.""" - active_mask = compute_core_domain_mask( - transport_runtime_params, runtime_params, geo, pedestal_model_output - ) - - coeffs_dict = dataclasses.asdict(transport_coeffs) - to_replace = {} - - for channel_name, config in CHANNEL_CONFIG_STRUCT.items(): - # Mask main channel - val = coeffs_dict[channel_name] - to_replace[channel_name] = jnp.where(active_mask, val, 0.0) # pyrefly: ignore[bad-argument-type] - - # Mask sub-channels - for sub_channel in config['sub_channels']: - sub_val = coeffs_dict[sub_channel] - if sub_val is not None: - to_replace[sub_channel] = jnp.where(active_mask, sub_val, 0.0) - - return dataclasses.replace(transport_coeffs, **to_replace) - - def _apply_clipping( - self, - transport_runtime_params: transport_runtime_params_lib.RuntimeParams, - transport_coeffs: TurbulentTransport, - ) -> TurbulentTransport: - """Applies min/max clipping to transport coefficients for PDE stability.""" - chi_face_ion = jnp.clip( - transport_coeffs.chi_face_ion, - transport_runtime_params.chi_min, - transport_runtime_params.chi_max, - ) - chi_face_el = jnp.clip( - transport_coeffs.chi_face_el, - transport_runtime_params.chi_min, - transport_runtime_params.chi_max, - ) - d_face_el = jnp.clip( - transport_coeffs.d_face_el, - transport_runtime_params.D_e_min, - transport_runtime_params.D_e_max, - ) - v_face_el = jnp.clip( - transport_coeffs.v_face_el, - transport_runtime_params.V_e_min, - transport_runtime_params.V_e_max, - ) - - return dataclasses.replace( - transport_coeffs, - chi_face_ion=chi_face_ion, - chi_face_el=chi_face_el, - d_face_el=d_face_el, - v_face_el=v_face_el, - ) - - def _apply_transport_patches( - self, - transport_runtime_params: transport_runtime_params_lib.RuntimeParams, - runtime_params: runtime_params_lib.RuntimeParams, - geo: geometry.Geometry, - transport_coeffs: TurbulentTransport, - ) -> TurbulentTransport: - """Applies inner and outer transport patches to transport coefficients.""" - consts = constants.CONSTANTS - - # Apply inner and outer patch constant transport coefficients. rho_inner and - # rho_outer are shifted by consts.eps (1e-7) to avoid ambiguities if their - # values are close to and geo.rho_face_norm values. - chi_face_ion = jnp.where( - jnp.logical_and( - transport_runtime_params.apply_inner_patch, - geo.rho_face_norm < transport_runtime_params.rho_inner + consts.eps, - ), - transport_runtime_params.chi_i_inner, - transport_coeffs.chi_face_ion, - ) - chi_face_el = jnp.where( - jnp.logical_and( - transport_runtime_params.apply_inner_patch, - geo.rho_face_norm < transport_runtime_params.rho_inner + consts.eps, - ), - transport_runtime_params.chi_e_inner, - transport_coeffs.chi_face_el, - ) - d_face_el = jnp.where( - jnp.logical_and( - transport_runtime_params.apply_inner_patch, - geo.rho_face_norm < transport_runtime_params.rho_inner + consts.eps, - ), - transport_runtime_params.D_e_inner, - transport_coeffs.d_face_el, - ) - v_face_el = jnp.where( - jnp.logical_and( - transport_runtime_params.apply_inner_patch, - geo.rho_face_norm < transport_runtime_params.rho_inner + consts.eps, - ), - transport_runtime_params.V_e_inner, - transport_coeffs.v_face_el, - ) - - # Apply outer patch constant transport coefficients. - # Due to Pereverzev-Corrigan convection, it is required - # for the convection modes to be 'ghost' to avoid numerical instability - chi_face_ion = jnp.where( - jnp.logical_and( - jnp.logical_and( - transport_runtime_params.apply_outer_patch, - jnp.logical_not(runtime_params.pedestal.set_pedestal), - ), - geo.rho_face_norm > transport_runtime_params.rho_outer - consts.eps, - ), - transport_runtime_params.chi_i_outer, - chi_face_ion, - ) - chi_face_el = jnp.where( - jnp.logical_and( - jnp.logical_and( - transport_runtime_params.apply_outer_patch, - jnp.logical_not(runtime_params.pedestal.set_pedestal), - ), - geo.rho_face_norm > transport_runtime_params.rho_outer - consts.eps, - ), - transport_runtime_params.chi_e_outer, - chi_face_el, - ) - d_face_el = jnp.where( - jnp.logical_and( - jnp.logical_and( - transport_runtime_params.apply_outer_patch, - jnp.logical_not(runtime_params.pedestal.set_pedestal), - ), - geo.rho_face_norm > transport_runtime_params.rho_outer - consts.eps, - ), - transport_runtime_params.D_e_outer, - d_face_el, - ) - v_face_el = jnp.where( - jnp.logical_and( - jnp.logical_and( - transport_runtime_params.apply_outer_patch, - jnp.logical_not(runtime_params.pedestal.set_pedestal), - ), - geo.rho_face_norm > transport_runtime_params.rho_outer - consts.eps, - ), - transport_runtime_params.V_e_outer, - v_face_el, - ) - - return dataclasses.replace( - transport_coeffs, - chi_face_ion=chi_face_ion, - chi_face_el=chi_face_el, - d_face_el=d_face_el, - v_face_el=v_face_el, - ) - - def _smooth_coeffs( - self, - runtime_params: runtime_params_lib.RuntimeParams, - geo: geometry.Geometry, - transport_coeffs: TurbulentTransport, - pedestal_model_output: pedestal_model_output_lib.PedestalModelOutput, - ) -> TurbulentTransport: - """Gaussian smoothing of turbulent transport coefficients.""" - smoothing_matrix = _build_smoothing_matrix( - runtime_params, - geo, - pedestal_model_output, - ) - - # Iterate over fields of the CoreTransport dataclass. - # Ignore optional fields that are made all zero in post_init. - def smooth_single_coeff(coeff): - return jax.lax.cond( - jnp.all(coeff == 0.0), - lambda: coeff, - lambda: jnp.dot(smoothing_matrix, coeff), - ) - - return jax.tree_util.tree_map(smooth_single_coeff, transport_coeffs) - - -def _build_smoothing_matrix( - runtime_params: runtime_params_lib.RuntimeParams, - geo: geometry.Geometry, - pedestal_model_output: pedestal_model_output_lib.PedestalModelOutput, -) -> jax.Array: - """Builds a smoothing matrix for the turbulent transport model. - - Uses a Gaussian kernel of HWHM defined in the transport config. - - Args: - runtime_params: Input runtime parameters of the simulation. - geo: Geometry of the torus. - pedestal_model_output: Output of the pedestal model. - - Returns: - kernel: A smoothing matrix for convolution with the transport outputs. - """ - - # To reduce the range of the convolution, weights under lower_cutoff are - # clipped to zero - lower_cutoff = 0.01 - - # used for eps, small number to avoid divisions by zero for sigma = 0 - consts = constants.CONSTANTS - - # 1. Kernel matrix - kernel = jnp.exp( - -jnp.log(2) - * (geo.rho_face_norm[:, jnp.newaxis] - geo.rho_face_norm) ** 2 - / (runtime_params.transport.smoothing_width**2 + consts.eps) - ) - - # 2. Masking: we do not want transport coefficients calculated in pedestal - # region or in inner and outer transport patch regions to impact - # transport_model calculated coefficients - if ( - runtime_params.pedestal.mode - == pedestal_runtime_params_lib.Mode.INTERNAL_BOUNDARY_CONDITION - ): - # If in INTERNAL_BOUNDARY_CONDITION mode: if set_pedestal is True, mask - # according to the pedestal top. Otherwise, mask according to the outer - # patch, if set. - mask_outer_edge = jnp.where( - runtime_params.pedestal.set_pedestal, - pedestal_model_output.rho_norm_ped_top - consts.eps, - jnp.where( - runtime_params.transport.apply_outer_patch, - runtime_params.transport.rho_outer - consts.eps, - jnp.inf, - ), - ) - else: - # If in ADAPTIVE_TRANSPORT mode, only mask according to the outer patch. - mask_outer_edge = jnp.where( - runtime_params.transport.apply_outer_patch, - runtime_params.transport.rho_outer - consts.eps, - jnp.inf, - ) - - mask_inner_edge = jax.lax.cond( - runtime_params.transport.apply_inner_patch, - lambda: runtime_params.transport.rho_inner + consts.eps, - lambda: -consts.eps, - ) - - mask = jnp.where( - jnp.logical_or( - runtime_params.transport.smooth_everywhere, - jnp.logical_and( - geo.rho_face_norm > mask_inner_edge, - geo.rho_face_norm < mask_outer_edge, - ), - ), - 1.0, - 0.0, - ) - - # remove impact of smoothing on inner and outer patch, or pedestal zone - - # first zero out all rows corresponding to grid points not to be impacted - diag_mask = jnp.diag(mask) - kernel = jnp.dot(diag_mask, kernel) - # now zero out all columns corresponding to grid points not to be impacted, - # such that they don't impact the smoothing of the other grid points - num_rows = len(mask) - mask_mat = jnp.tile(mask, (num_rows, 1)) - kernel *= mask_mat - # now restore identity to the zero rows, such that smoothing is a no-op for - # on the grid points where it shouldn't impact - zero_row_mask = jnp.all(kernel == 0, axis=1) - kernel = jnp.where( - zero_row_mask[:, jnp.newaxis], jnp.eye(kernel.shape[0]), kernel - ) - - # 3. Normalization - row_sums = jnp.sum(kernel, axis=1) - kernel /= row_sums[:, jnp.newaxis] - - # 4. Remove small numbers - kernel = jnp.where(kernel < lower_cutoff, 0.0, kernel) - - # 5. Final Normalization following removal of small numbers - row_sums = jnp.sum(kernel, axis=1) - kernel /= row_sums[:, jnp.newaxis] - return kernel - def compute_core_domain_mask( transport_runtime_params: transport_runtime_params_lib.RuntimeParams, diff --git a/torax/benchmarks/tokagrad_benchmark.py b/torax/benchmarks/tokagrad_benchmark.py index f1b4cf6a2..d446226bf 100644 --- a/torax/benchmarks/tokagrad_benchmark.py +++ b/torax/benchmarks/tokagrad_benchmark.py @@ -237,26 +237,30 @@ "pellet": {"S_total": 0.0}, }, "transport": { - "model_name": "bohm-gyrobohm", - "chi_e_bohm_multiplier": 1.0, - "chi_i_bohm_multiplier": 1.0, - "chi_e_gyrobohm_multiplier": 1.0, - "chi_i_gyrobohm_multiplier": 1.0, - "chi_e_bohm_coeff": 8e-5, - "chi_e_gyrobohm_coeff": 5e-6, - "chi_i_bohm_coeff": 8e-5, - "chi_i_gyrobohm_coeff": 5e-6, - "D_face_c1": 1.0, - "D_face_c2": 0.3, - "V_face_coeff": 0.0, + "model_name": "combined", "chi_min": 0.05, "chi_max": 100.0, "D_e_min": 0.05, "D_e_max": 50.0, "V_e_min": -10.0, "V_e_max": 10.0, - "smooth_everywhere": False, "smoothing_width": 0.05, + "transport_models": [ + { + "model_name": "bohm-gyrobohm", + "chi_e_bohm_multiplier": 1.0, + "chi_i_bohm_multiplier": 1.0, + "chi_e_gyrobohm_multiplier": 1.0, + "chi_i_gyrobohm_multiplier": 1.0, + "chi_e_bohm_coeff": 8e-5, + "chi_e_gyrobohm_coeff": 5e-6, + "chi_i_bohm_coeff": 8e-5, + "chi_i_gyrobohm_coeff": 5e-6, + "D_face_c1": 1.0, + "D_face_c2": 0.3, + "V_face_coeff": 0.0, + } + ], }, "solver": { "solver_type": "linear", diff --git a/torax/tests/test_data/test_combined_transport.nc b/torax/tests/test_data/test_combined_transport.nc index 2a59e77a2a4d8a8306e5b2730615ac6b643c81f1..592b402dbcfc9f958a81fe78b8bb88219a3561c0 100644 GIT binary patch delta 32477 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 delta 30331 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\"Open\n" - ] + ], + "metadata": { + "id": "wH4_0fmxGQL8" + } }, { - "metadata": { - "id": "m5geWYHBKDd6" - }, "cell_type": "code", "source": [ "#@title LICENSE\n", @@ -31,14 +28,13 @@ "# See the License for the specific language governing permissions and\n", "# limitations under the License." ], - "outputs": [], - "execution_count": 153 - }, - { "metadata": { - "id": "9UYOakVOJ_zM", - "cellView": "form" + "id": "m5geWYHBKDd6" }, + "execution_count": 153, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Install TORAX\n", @@ -47,14 +43,14 @@ "# To install the current version on GitHub, use `%pip install git+https://github.com/google-deepmind/torax.git`.\n", "%pip install torax" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "I4TZNOJJGBR_", - "cellView": "form" + "cellView": "form", + "id": "9UYOakVOJ_zM" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Imports\n", @@ -68,14 +64,14 @@ "\n", "jax.config.update('jax_enable_x64', True)" ], - "outputs": [], - "execution_count": 154 - }, - { "metadata": { - "id": "A7MMV9poKDd6", - "cellView": "form" + "cellView": "form", + "id": "I4TZNOJJGBR_" }, + "execution_count": 154, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Base config\n", @@ -188,34 +184,42 @@ " 'rho_norm_ped_top': 0.95, # set ped top location in normalized radius\n", " },\n", " 'transport': {\n", - " 'model_name': 'qlknn', # Using QLKNN_7_11 default\n", - " # set inner core transport coefficients (ad-hoc MHD/EM transport)\n", - " 'apply_inner_patch': True,\n", - " 'D_e_inner': 0.15,\n", - " 'V_e_inner': 0.0,\n", - " 'chi_i_inner': 0.3,\n", - " 'chi_e_inner': 0.3,\n", - " 'rho_inner': 0.1, # radius below which patch transport is applied\n", - " # set outer core transport coefficients (L-mode near edge region)\n", - " 'apply_outer_patch': True,\n", - " 'D_e_outer': 0.1,\n", - " 'V_e_outer': 0.0,\n", - " 'chi_i_outer': 2.0,\n", - " 'chi_e_outer': 2.0,\n", - " 'rho_outer': 0.95, # radius above which patch transport is applied\n", - " # allowed chi and diffusivity bounds\n", - " 'chi_min': 0.05, # minimum chi\n", - " 'chi_max': 100, # maximum chi (can be helpful for stability)\n", - " 'D_e_min': 0.05, # minimum electron diffusivity\n", - " 'D_e_max': 50, # maximum electron diffusivity\n", - " 'V_e_min': -10, # minimum electron convection\n", - " 'V_e_max': 10, # minimum electron convection\n", + " 'model_name': 'combined',\n", + " 'chi_min': 0.05,\n", + " 'chi_max': 100,\n", + " 'D_e_min': 0.05,\n", + " 'D_e_max': 50,\n", + " 'V_e_min': -10,\n", + " 'V_e_max': 10,\n", " 'smoothing_width': 0.1,\n", - " 'DV_effective': True,\n", - " 'include_ITG': True, # to toggle ITG modes on or off\n", - " 'include_TEM': True, # to toggle TEM modes on or off\n", - " 'include_ETG': True, # to toggle ETG modes on or off\n", - " 'avoid_big_negative_s': False,\n", + " 'transport_models': [\n", + " {\n", + " 'model_name': 'constant',\n", + " 'rho_max': 0.1,\n", + " 'chi_i': 0.3,\n", + " 'chi_e': 0.3,\n", + " 'D_e': 0.15,\n", + " 'V_e': 0.0,\n", + " },\n", + " {\n", + " 'model_name': 'qlknn',\n", + " 'rho_min': 0.1,\n", + " 'rho_max': 0.95,\n", + " 'DV_effective': True,\n", + " 'include_ITG': True,\n", + " 'include_TEM': True,\n", + " 'include_ETG': True,\n", + " 'avoid_big_negative_s': False,\n", + " },\n", + " {\n", + " 'model_name': 'constant',\n", + " 'rho_min': 0.95,\n", + " 'chi_i': 2.0,\n", + " 'chi_e': 2.0,\n", + " 'D_e': 0.1,\n", + " 'V_e': 0.0,\n", + " },\n", + " ],\n", " },\n", " 'solver': {\n", " 'solver_type': 'linear', # linear solver with picard iteration\n", @@ -229,16 +233,16 @@ " 'time_step_calculator': {\n", " 'calculator_type': 'fixed',\n", " },\n", - "}\n" + "}" ], - "outputs": [], - "execution_count": 155 - }, - { "metadata": { - "id": "xSzvOYW_KDd6", - "cellView": "form" + "cellView": "form", + "id": "A7MMV9poKDd6" }, + "execution_count": 155, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Detailed summary plot function for a single simulation\n", @@ -449,14 +453,14 @@ "\n", " plt.show()\n" ], - "outputs": [], - "execution_count": 176 - }, - { "metadata": { - "id": "qmq9COuoKDd6", - "cellView": "form" + "cellView": "form", + "id": "xSzvOYW_KDd6" }, + "execution_count": 176, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Comparison timetrace summary plot for multiple simulations\n", @@ -578,14 +582,14 @@ " plt.tight_layout()\n", " plt.show()" ], - "outputs": [], - "execution_count": 157 - }, - { "metadata": { - "id": "FL-_GqQaGBSA", - "cellView": "form" + "cellView": "form", + "id": "qmq9COuoKDd6" }, + "execution_count": 157, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Comparison profile summary plot for multiple simulations\n", @@ -690,14 +694,14 @@ " plt.tight_layout()\n", " plt.show()" ], - "outputs": [], - "execution_count": 158 - }, - { "metadata": { - "id": "do-4obv1GBSA", - "cellView": "form" + "cellView": "form", + "id": "FL-_GqQaGBSA" }, + "execution_count": 158, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title set_LH_transition_time_function\n", @@ -725,14 +729,14 @@ " if not (allowed_range[0] <= input_obj <= allowed_range[1]):\n", " raise ValueError(f\"Input value must be between {allowed_range[0]} and {allowed_range[1]}.\")\n" ], - "outputs": [], - "execution_count": 159 - }, - { "metadata": { - "id": "O4B4jZmAGBSA", - "cellView": "form" + "cellView": "form", + "id": "do-4obv1GBSA" }, + "execution_count": 159, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Modify config function\n", @@ -848,14 +852,14 @@ " if not (allowed_range[0] <= input_obj <= allowed_range[1]):\n", " raise ValueError(f\"{variable_name} values must be between {allowed_range[0]} and {allowed_range[1]}.\")\n" ], - "outputs": [], - "execution_count": 160 - }, - { "metadata": { - "id": "HRKQQkpJGBSA", - "cellView": "form" + "cellView": "form", + "id": "O4B4jZmAGBSA" }, + "execution_count": 160, + "outputs": [] + }, + { "cell_type": "code", "source": [ "# @title Function to build and launch TORAX sim\n", @@ -864,13 +868,14 @@ " data_tree, _ = torax.run_simulation(torax_config, log_timestep_info=False)\n", " return data_tree" ], - "outputs": [], - "execution_count": 161 - }, - { "metadata": { - "id": "FDC7xuNLKDd7" + "cellView": "form", + "id": "HRKQQkpJGBSA" }, + "execution_count": 161, + "outputs": [] + }, + { "cell_type": "markdown", "source": [ "# TORAX exercise\n", @@ -970,17 +975,21 @@ "* Maintain q_min > 1\n", "* Total input power at t=100s must be > 50MW.\n", "* Ip cannot exceed 18MA during the current rampup. Note that the current ramp does not need to be monotonic.\n" - ] + ], + "metadata": { + "id": "FDC7xuNLKDd7" + } }, { - "metadata": { - "id": "3OPtAiy4KDd7" - }, "cell_type": "code", "source": [ "config0 = set_LH_transition_time(LH_transition_time = 80)\n", "out0 = run_sim(config0)" ], + "metadata": { + "id": "3OPtAiy4KDd7" + }, + "execution_count": 162, "outputs": [ { "name": "stderr", @@ -989,17 +998,17 @@ "Simulating (t=150.00000): 100%|██████████| 100/100 [00:08<00:00, 11.76it/s]\n" ] } - ], - "execution_count": 162 + ] }, { - "metadata": { - "id": "P1DAzLDAKDd7" - }, "cell_type": "code", "source": [ "detailed_plot_single_sim(out0, time = 150)" ], + "metadata": { + "id": "P1DAzLDAKDd7" + }, + "execution_count": 177, "outputs": [ { "name": "stdout", @@ -1026,17 +1035,16 @@ }, "output_type": "display_data" } - ], - "execution_count": 177 + ] }, { + "cell_type": "code", + "source": [], "metadata": { "id": "ksx5Hd5SKDd7" }, - "cell_type": "code", - "source": [], - "outputs": [], - "execution_count": 163 + "execution_count": 163, + "outputs": [] } ], "metadata": { @@ -1065,6 +1073,6 @@ "version": "3.12.8" } }, - "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 0, + "nbformat": 4 } diff --git a/torax/tutorials/torax_tutorial_exercises_with_solutions.ipynb b/torax/tutorials/torax_tutorial_exercises_with_solutions.ipynb index a79d4be61..fa62f8752 100644 --- a/torax/tutorials/torax_tutorial_exercises_with_solutions.ipynb +++ b/torax/tutorials/torax_tutorial_exercises_with_solutions.ipynb @@ -1,19 +1,16 @@ { "cells": [ { - "metadata": { - "id": "wAoJBvlGGJte" - }, "cell_type": "markdown", "source": [ "# TORAX Tutorial with Solutions\n", "\"Open\n" - ] + ], + "metadata": { + "id": "wAoJBvlGGJte" + } }, { - "metadata": { - "id": "m5geWYHBKDd6" - }, "cell_type": "code", "source": [ "#@title LICENSE\n", @@ -31,13 +28,13 @@ "# See the License for the specific language governing permissions and\n", "# limitations under the License." ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "vflmKg8bKNTl" + "id": "m5geWYHBKDd6" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Install TORAX\n", @@ -45,13 +42,13 @@ "# To install the current version on GitHub, use `%pip install git+https://github.com/google-deepmind/torax.git`.\n", "%pip install torax" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "cufUHoEGKDd6" + "id": "vflmKg8bKNTl" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Imports\n", @@ -65,13 +62,13 @@ "\n", "jax.config.update('jax_enable_x64', True)" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "A7MMV9poKDd6" + "id": "cufUHoEGKDd6" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Base config\n", @@ -184,34 +181,42 @@ " 'rho_norm_ped_top': 0.95, # set ped top location in normalized radius\n", " },\n", " 'transport': {\n", - " 'model_name': 'qlknn', # Using QLKNN_7_11 default\n", - " # set inner core transport coefficients (ad-hoc MHD/EM transport)\n", - " 'apply_inner_patch': True,\n", - " 'D_e_inner': 0.15,\n", - " 'V_e_inner': 0.0,\n", - " 'chi_i_inner': 0.3,\n", - " 'chi_e_inner': 0.3,\n", - " 'rho_inner': 0.1, # radius below which patch transport is applied\n", - " # set outer core transport coefficients (L-mode near edge region)\n", - " 'apply_outer_patch': True,\n", - " 'D_e_outer': 0.1,\n", - " 'V_e_outer': 0.0,\n", - " 'chi_i_outer': 2.0,\n", - " 'chi_e_outer': 2.0,\n", - " 'rho_outer': 0.95, # radius above which patch transport is applied\n", - " # allowed chi and diffusivity bounds\n", - " 'chi_min': 0.05, # minimum chi\n", - " 'chi_max': 100, # maximum chi (can be helpful for stability)\n", - " 'D_e_min': 0.05, # minimum electron diffusivity\n", - " 'D_e_max': 50, # maximum electron diffusivity\n", - " 'V_e_min': -10, # minimum electron convection\n", - " 'V_e_max': 10, # minimum electron convection\n", + " 'model_name': 'combined',\n", + " 'chi_min': 0.05,\n", + " 'chi_max': 100,\n", + " 'D_e_min': 0.05,\n", + " 'D_e_max': 50,\n", + " 'V_e_min': -10,\n", + " 'V_e_max': 10,\n", " 'smoothing_width': 0.1,\n", - " 'DV_effective': True,\n", - " 'include_ITG': True, # to toggle ITG modes on or off\n", - " 'include_TEM': True, # to toggle TEM modes on or off\n", - " 'include_ETG': True, # to toggle ETG modes on or off\n", - " 'avoid_big_negative_s': False,\n", + " 'transport_models': [\n", + " {\n", + " 'model_name': 'constant',\n", + " 'rho_max': 0.1,\n", + " 'chi_i': 0.3,\n", + " 'chi_e': 0.3,\n", + " 'D_e': 0.15,\n", + " 'V_e': 0.0,\n", + " },\n", + " {\n", + " 'model_name': 'qlknn',\n", + " 'rho_min': 0.1,\n", + " 'rho_max': 0.95,\n", + " 'DV_effective': True,\n", + " 'include_ITG': True,\n", + " 'include_TEM': True,\n", + " 'include_ETG': True,\n", + " 'avoid_big_negative_s': False,\n", + " },\n", + " {\n", + " 'model_name': 'constant',\n", + " 'rho_min': 0.95,\n", + " 'chi_i': 2.0,\n", + " 'chi_e': 2.0,\n", + " 'D_e': 0.1,\n", + " 'V_e': 0.0,\n", + " },\n", + " ],\n", " },\n", " 'solver': {\n", " 'solver_type': 'linear', # linear solver with picard iteration\n", @@ -225,15 +230,15 @@ " 'time_step_calculator': {\n", " 'calculator_type': 'fixed',\n", " },\n", - "}\n" + "}" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "xSzvOYW_KDd6" + "id": "A7MMV9poKDd6" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Detailed summary plot function for a single simulation\n", @@ -444,13 +449,13 @@ "\n", " plt.show()\n" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "32fJVuAbQhc1" + "id": "xSzvOYW_KDd6" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Comparison timetrace summary plot for multiple simulations\n", @@ -572,13 +577,13 @@ " plt.tight_layout()\n", " plt.show()" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "1_eekusPKDd6" + "id": "32fJVuAbQhc1" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Comparison profile summary plot for multiple simulations\n", @@ -683,13 +688,13 @@ " plt.tight_layout()\n", " plt.show()" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "Xz6YJLQAKDd6" + "id": "1_eekusPKDd6" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title set_LH_transition_time_function\n", @@ -717,13 +722,13 @@ " if not (allowed_range[0] <= input_obj <= allowed_range[1]):\n", " raise ValueError(f\"Input value must be between {allowed_range[0]} and {allowed_range[1]}.\")\n" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "4-S6_hbNKDd7" + "id": "Xz6YJLQAKDd6" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "#@title Modify config function\n", @@ -839,13 +844,13 @@ " if not (allowed_range[0] <= input_obj <= allowed_range[1]):\n", " raise ValueError(f\"{variable_name} values must be between {allowed_range[0]} and {allowed_range[1]}.\")\n" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "gPm-tYA5KDd7" + "id": "4-S6_hbNKDd7" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "# @title Function to build and launch TORAX sim\n", @@ -854,13 +859,13 @@ " data_tree, _ = torax.run_simulation(torax_config, log_timestep_info=False)\n", " return data_tree" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "w15Nr5w1CgzC" + "id": "gPm-tYA5KDd7" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "markdown", "source": [ "# TORAX exercise\n", @@ -960,12 +965,12 @@ "* Maintain q_min > 1\n", "* Total input power at t=100s must be > 50MW.\n", "* Ip cannot exceed 18MA during the current rampup. Note that the current ramp does not need to be monotonic.\n" - ] + ], + "metadata": { + "id": "w15Nr5w1CgzC" + } }, { - "metadata": { - "id": "xdwIAbOBVer2" - }, "cell_type": "code", "source": [ "# @title SOLUTION: question 1\n", @@ -997,26 +1002,26 @@ "# 6. On the other hand, q_min > 1 for the faster rampup rate, whereas q_min<1 for the slower rampup rate since a lot more current has diffused inwards. In some scenarios, q_min > 1 is a constraint to avoid sawteeth instabilities.\n", "\n" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "OuDaKHE1Ver2" + "id": "xdwIAbOBVer2" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "# Optional, can investigate the more detailed plots\n", "# detailed_plot_single_sim(out0, time = 60)\n", "# detailed_plot_single_sim(out3, time = 120)" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "_5BQpHCSVer2" + "id": "OuDaKHE1Ver2" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "# @title SOLUTION: question 2\n", @@ -1048,13 +1053,13 @@ "# This leads to a higher q-profile when there is heating during ramp-up. The NBI current also contributes here, since it displaces some of the Ohmic current\n", "# (which in stationary state peaks on axis), with a broader current profile.\n" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "tpcEG586Ver2" + "id": "_5BQpHCSVer2" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "# @title SOLUTION: question 3\n", @@ -1131,13 +1136,13 @@ "\n", "# To achieve qmin>1.5, it is necessary to reduce the total current while ensuring that the ECCD deposition is sufficiently off-axis and not too strong" ], - "outputs": [], - "execution_count": null - }, - { "metadata": { - "id": "HwWO7cJ9Ver2" + "id": "tpcEG586Ver2" }, + "execution_count": null, + "outputs": [] + }, + { "cell_type": "code", "source": [ "# @title SOLUTION: question 4\n", @@ -1168,8 +1173,11 @@ "# 2. Increasing EC power during the current ramp maintains a higher current diffusion time and avoids q<1 on-axis\n", "# 3. The location and power of the ECCD makes sure that q>1 in the deposition region" ], - "outputs": [], - "execution_count": null + "metadata": { + "id": "HwWO7cJ9Ver2" + }, + "execution_count": null, + "outputs": [] } ], "metadata": { @@ -1199,6 +1207,6 @@ "version": "3.12.8" } }, - "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 0, + "nbformat": 4 }