Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple impurity radiation model (P_imp as a fraction of P_tot_in) #572

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions torax/examples/prad.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Not sure there's much point in making an example config out of a single feature?

  2. Our standard pattern is to make a new integration test in tests/tests_data with new features for regression and integration testing.

Suggest to remove it here, make a new integration test (to add to tests/sim.py also) based on one of the standard cases like test_iterhybrid_predictor_corrector

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll definitely do this. I hadn't actually meant to leave this config in, I'd forgotten that I hadn't got round to turning it into an integration test yet!

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CONFIG = {
'runtime_params': {},
'geometry': {
'geometry_type': 'circular',
},
'sources': {
'j_bootstrap': {},
'generic_current_source': {},
'generic_particle_source': {},
'gas_puff_source': {},
'pellet_source': {},
'generic_ion_el_heat_source': {},
'fusion_heat_source': {},
'qei_source': {},
'ohmic_heat_source': {},
'radiation_heat_sink': {},
},
'transport': {
'transport_model': 'constant',
},
'stepper': {
'stepper_type': 'linear',
},
'time_step_calculator': {
'calculator_type': 'chi',
},
}
4 changes: 2 additions & 2 deletions torax/plotting/configs/default_plot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@
suppress_zero_values=True, # Do not plot all-zero data
),
plotruns_lib.PlotProperties(
attrs=('q_brems',),
labels=(r'$Q_\mathrm{brems}$',),
attrs=('q_brems', 'q_rad'),
labels=(r'$Q_\mathrm{brems}$', r'$Q_\mathrm{rad}$'),
ylabel=r'Heat sink density $[MW~m^{-3}]$',
suppress_zero_values=True, # Do not plot all-zero data
),
Expand Down
4 changes: 2 additions & 2 deletions torax/plotting/configs/sources_plot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@
suppress_zero_values=True, # Do not plot all-zero data
),
plotruns_lib.PlotProperties(
attrs=('q_brems',),
labels=(r'$Q_\mathrm{brems}$',),
attrs=('q_brems', 'q_rad'),
labels=(r'$Q_\mathrm{brems}$', r'$Q_\mathrm{rad}$'),
ylabel=r'Heat sink density $[MW~m^{-3}]$',
suppress_zero_values=True, # Do not plot all-zero data
),
Expand Down
8 changes: 7 additions & 1 deletion torax/plotting/plotruns_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class PlotData:
q_ohmic: np.ndarray # [MW/m^3]
q_brems: np.ndarray # [MW/m^3]
q_ei: np.ndarray # [MW/m^3]
q_rad: np.ndarray # [MW/m^3]
Q_fusion: np.ndarray # pylint: disable=invalid-name # Dimensionless
s_puff: np.ndarray # [10^20 m^-3 s^-1]
s_generic: np.ndarray # [10^20 m^-3 s^-1]
Expand Down Expand Up @@ -171,12 +172,14 @@ def _transform_data(ds: xr.Dataset):
'fusion_heat_source_el': 1e6, # W/m^3 to MW/m^3
'ohmic_heat_source': 1e6, # W/m^3 to MW/m^3
'bremsstrahlung_heat_sink': 1e6, # W/m^3 to MW/m^3
'radiation_heat_sink': 1e6, # W/m^3 to MW/m^3
'qei_source': 1e6, # W/m^3 to MW/m^3
'P_ohmic': 1e6, # W to MW
'P_external_tot': 1e6, # W to MW
'P_alpha_tot': 1e6, # W to MW
'P_brems': 1e6, # W to MW
'P_ecrh': 1e6, # W to MW
'P_rad': 1e6, # W to MW
'I_ecrh': 1e6, # A to MA
'I_generic': 1e6, # A to MA
}
Expand Down Expand Up @@ -246,6 +249,9 @@ def _transform_data(ds: xr.Dataset):
q_brems=get_optional_data(
core_sources_dataset, 'bremsstrahlung_heat_sink', 'cell'
),
q_rad=get_optional_data(
core_sources_dataset, 'radiation_heat_sink', 'cell'
),
q_ei=core_sources_dataset['qei_source'].to_numpy(), # ion heating/sink
Q_fusion=post_processed_outputs_dataset['Q_fusion'].to_numpy(), # pylint: disable=invalid-name
s_puff=get_optional_data(core_sources_dataset, 'gas_puff_source', 'cell'),
Expand All @@ -263,7 +269,7 @@ def _transform_data(ds: xr.Dataset):
- post_processed_outputs_dataset['P_ohmic']
).to_numpy(),
p_alpha=post_processed_outputs_dataset['P_alpha_tot'].to_numpy(),
p_sink=post_processed_outputs_dataset['P_brems'].to_numpy(),
p_sink=post_processed_outputs_dataset['P_brems'].to_numpy() + post_processed_outputs_dataset['P_rad'].to_numpy(),
t=time,
)

Expand Down
1 change: 1 addition & 0 deletions torax/post_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'ohmic_heat_source': 'P_ohmic',
'bremsstrahlung_heat_sink': 'P_brems',
'electron_cyclotron_source': 'P_ecrh',
'radiation_heat_sink': 'P_rad',
}
EXTERNAL_HEATING_SOURCES = [
'generic_ion_el_heat_source',
Expand Down
122 changes: 122 additions & 0 deletions torax/sources/radiation_heat_sink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Basic radiation heat sink for electron heat equation.."""

import dataclasses

import chex
import jax
import jax.numpy as jnp

from torax import array_typing
from torax import geometry
from torax import state
from torax.config import runtime_params_slice
from torax.sources import bremsstrahlung_heat_sink
from torax.sources import qei_source
from torax.sources import runtime_params as runtime_params_lib
from torax.sources import source as source_lib
from torax.sources import source_models as source_models_lib

SOURCE_NAME = "radiation_heat_sink"


def _Qrad_as_fraction_of_Qtot_in(
dynamic_runtime_params_slice: runtime_params_slice.DynamicRuntimeParamsSlice,
dynamic_source_runtime_params: runtime_params_lib.DynamicRuntimeParams,
geo: geometry.Geometry,
core_profiles: state.CoreProfiles,
source_models: source_models_lib.SourceModels,
) -> jax.Array:
"""Model function for radiation heat sink.

In this model, a fixed % of the total power input to the temp_el equation is lost."""
theo-brown marked this conversation as resolved.
Show resolved Hide resolved
# Based on source_models.sum_sources_temp_el and source_models.calc_and_sum_sources_psi,
# but only summing over heating *input* sources (Pohm + Paux + Palpha)
# and summing over *both* ion and electron heating

def get_temp_el_profile(source_name: str, source: source_lib.Source) -> jax.Array:
# TODO: Currently this recomputes the profile for each source, which is inefficient
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree. We will prioritize changing the pattern such that this won't happen.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK for the time being though?

# (and will be a problem if sources are slow/non-jittable)
# A similar TODO is noted in source_models.calc_and_sum_sources_psi
profile = source.get_value(
dynamic_runtime_params_slice,
dynamic_runtime_params_slice.sources[source_name],
geo,
core_profiles,
)
return source.get_source_profile_for_affected_core_profile(
profile, source_lib.AffectedCoreProfile.TEMP_EL.value, geo
) + source.get_source_profile_for_affected_core_profile(
profile, source_lib.AffectedCoreProfile.TEMP_ION.value, geo
)

# Manually remove sources that will not be summed
sources_to_sum = source_models.temp_el_sources | source_models.temp_ion_sources
sources_to_sum.pop(SOURCE_NAME, None)
sources_to_sum.pop(bremsstrahlung_heat_sink.SOURCE_NAME, None)
theo-brown marked this conversation as resolved.
Show resolved Hide resolved
sources_to_sum.pop(qei_source.SOURCE_NAME, None)

source_profiles = jax.tree.map(
get_temp_el_profile,
list(sources_to_sum.keys()),
list(sources_to_sum.values())
)

Qtot_in = jnp.sum(jnp.stack(source_profiles), axis=0)

# Calculate the radiation heat sink
return (
-dynamic_source_runtime_params.fraction_of_total_power_density
* Qtot_in
* jnp.ones_like(geo.rho)
)


@dataclasses.dataclass(kw_only=True)
class RuntimeParams(runtime_params_lib.RuntimeParams):
fraction_of_total_power_density: runtime_params_lib.TimeInterpolatedInput = 0.1
mode: runtime_params_lib.Mode = runtime_params_lib.Mode.MODEL_BASED

def make_provider(
self,
torax_mesh: geometry.Grid1D | None = None,
) -> "RuntimeParamsProvider":
return RuntimeParamsProvider(**self.get_provider_kwargs(torax_mesh))


@chex.dataclass
class RuntimeParamsProvider(runtime_params_lib.RuntimeParamsProvider):
"""Provides runtime parameters for a given time and geometry."""

runtime_params_config: RuntimeParams

def build_dynamic_params(
self,
t: chex.Numeric,
) -> "DynamicRuntimeParams":
return DynamicRuntimeParams(**self.get_dynamic_params_kwargs(t))


@chex.dataclass(frozen=True)
class DynamicRuntimeParams(runtime_params_lib.DynamicRuntimeParams):
fraction_of_total_power_density: array_typing.ScalarFloat


@dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
class RadiationHeatSink(source_lib.Source):
"""Radiation heat sink for electron heat equation."""

source_models: source_models_lib.SourceModels
model_func: source_lib.SourceProfileFunction = _Qrad_as_fraction_of_Qtot_in

@property
def supported_modes(self) -> tuple[runtime_params_lib.Mode, ...]:
"""Returns the modes supported by this source."""
return (
runtime_params_lib.Mode.ZERO,
runtime_params_lib.Mode.MODEL_BASED,
runtime_params_lib.Mode.PRESCRIBED,
)

@property
def affected_core_profiles(self) -> tuple[source_lib.AffectedCoreProfile, ...]:
return (source_lib.AffectedCoreProfile.TEMP_EL,)
7 changes: 6 additions & 1 deletion torax/sources/register_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class to build, the runtime associated with that source and (optionally) the
from torax.sources import ion_cyclotron_source
from torax.sources import ohmic_heat_source
from torax.sources import qei_source
from torax.sources import radiation_heat_sink
from torax.sources import runtime_params
from torax.sources import source

Expand Down Expand Up @@ -146,6 +147,11 @@ def _register_new_source(
source_class=ion_cyclotron_source.IonCyclotronSource,
default_runtime_params_class=ion_cyclotron_source.RuntimeParams,
),
radiation_heat_sink.SOURCE_NAME: _register_new_source(
source_class=radiation_heat_sink.RadiationHeatSink,
default_runtime_params_class=radiation_heat_sink.RuntimeParams,
links_back=True,
)
}


Expand All @@ -155,4 +161,3 @@ def get_registered_source(source_name: str) -> RegisteredSource:
return _REGISTERED_SOURCES[source_name]
else:
raise RuntimeError(f'Source:{source_name} has not been registered.')

9 changes: 9 additions & 0 deletions torax/sources/tests/bootstrap_current_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def setUpClass(cls):
expected_affected_core_profiles=(source_lib.AffectedCoreProfile.PSI,),
)

def test_expected_mesh_states(self):
# This function is reimplemented here as BootstrapCurrentSource does not
# appear in source_models, which the parent class uses to build the source
source = bootstrap_current_source.BootstrapCurrentSource()
self.assertSameElements(
source.affected_core_profiles,
self._expected_affected_core_profiles,
)

def test_extraction_of_relevant_profile_from_output(self):
"""Tests that the relevant profile is extracted from the output."""
source = bootstrap_current_source.BootstrapCurrentSource()
Expand Down
10 changes: 9 additions & 1 deletion torax/sources/tests/electron_cyclotron_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,15 @@ def test_invalid_source_types_raise_errors(self):
def test_extraction_of_relevant_profile_from_output(self):
"""Tests that the relevant profile is extracted from the output."""
geo = geometry.build_circular_geometry()
source = self._source_class()
# pylint: disable=missing-kwoa
source_builder = self._source_class_builder() # pytype: disable=missing-parameter
# pylint: enable=missing-kwoa
source_models_builder = source_models_lib.SourceModelsBuilder(
{'foo': source_builder},
)
source_models = source_models_builder()
source = source_models.sources['foo']
self.assertIsInstance(source, source_lib.Source)
cell = source_lib.ProfileType.CELL.get_profile_shape(geo)
fake_profile = jnp.stack((jnp.ones(cell), 2 * jnp.ones(cell)))
# Check TEMP_EL and PSI are modified
Expand Down
14 changes: 9 additions & 5 deletions torax/sources/tests/ion_cyclotron_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,16 @@ def test_source_value(self, mock_path):
return_value=_DUMMY_MODEL_PATH,
)
def test_expected_mesh_states(self, mock_path):
# This function is reimplemented from the parent class to properly handle
# the mock_path, without which an error is raised.
del mock_path
# Most Source subclasses should have default names and be instantiable
# without any __init__ arguments.
# pylint: disable=missing-kwoa
source = self._source_class() # pytype: disable=missing-parameter
# pylint: enable=missing-kwoa
source_builder = self._source_class_builder()
source_models_builder = source_models_lib.SourceModelsBuilder(
{'foo': source_builder},
)
source_models = source_models_builder()
source = source_models.sources['foo']
self.assertIsInstance(source, source_lib.Source)
self.assertSameElements(
source.affected_core_profiles,
self._expected_affected_core_profiles,
Expand Down
9 changes: 9 additions & 0 deletions torax/sources/tests/qei_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ def setUpClass(cls):
),
)

def test_expected_mesh_states(self):
# This function is reimplemented here as QeiSource does not appear in
# source_models, which the parent class uses to build the source
source = qei_source.QeiSource()
self.assertSameElements(
source.affected_core_profiles,
self._expected_affected_core_profiles,
)

def test_source_value(self):
"""Checks that the default implementation from Sources gives values."""
source_builder = self._source_class_builder()
Expand Down
Loading