From 2fbe8a8666abb52184a6ecf104a92860d737f495 Mon Sep 17 00:00:00 2001 From: DavidA Date: Sat, 15 Aug 2026 14:41:26 +0100 Subject: [PATCH] Reject zero-thickness superconducting TF at input validation Build.calculate_radial_build only derives dr_tf_inboard from the winding pack and case thicknesses when dr_tf_wp_with_insulation (ixc = 140) is an iteration variable. If a user supplies the winding pack thickness as a plain input instead, dr_tf_inboard silently stays at its default of 0: the TF coil vanishes from the radial build and the run fails far downstream with unexplained radial-build inconsistency and multi-GPa TF stresses. Add a check_process validation that a superconducting TF has a positive dr_tf_inboard when neither ixc = 13 nor ixc = 140 is active, with an actionable message. Stellarators (which calculate dr_tf_inboard during the model run) and IFE are excluded. Test-suite change, per CONTRIBUTING: the parser tests in tests/unit/core/test_input.py run init_process on minimal input snippets and relied on config validation not examining the TF geometry; their fixture scaffold now sets a valid dr_tf_inboard. Co-Authored-By: Claude Fable 5 --- process/core/init.py | 23 +++++++++++ tests/unit/core/test_init.py | 74 +++++++++++++++++++++++++++++++++++ tests/unit/core/test_input.py | 7 +++- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/unit/core/test_init.py diff --git a/process/core/init.py b/process/core/init.py index 7704fd05e8..619235202e 100644 --- a/process/core/init.py +++ b/process/core/init.py @@ -287,6 +287,29 @@ def check_process(inputs, data): # noqa: ARG001 "Iteration variables 13 and 140 cannot be used simultaneously", ) + # A superconducting TF coil must have a non-zero inboard thickness. + # dr_tf_inboard is only derived from the winding pack and case thicknesses + # when dr_tf_wp_with_insulation (ixc = 140) is an iteration variable (see + # Build.calculate_radial_build); with neither ixc = 13 nor ixc = 140 active + # it stays at its input value, and the default of 0 silently removes the + # TF coil from the radial build. Stellarators calculate dr_tf_inboard + # during the model run, so are excluded from this check. + if ( + data.stellarator.istell == 0 + and data.ife.ife == 0 + and data.tfcoil.i_tf_sup == TFConductorModel.SUPERCONDUCTING + and not (data.numerics.ixc[: data.numerics.n_iteration_variables] == 13).any() + and not (data.numerics.ixc[: data.numerics.n_iteration_variables] == 140).any() + and data.build.dr_tf_inboard <= 0.0 + ): + raise ProcessValidationError( + "dr_tf_inboard is not positive: the superconducting inboard TF coil" + " has no thickness. Set dr_tf_inboard (or use ixc = 13), or make" + " dr_tf_wp_with_insulation an iteration variable (ixc = 140) so that" + " dr_tf_inboard is derived from the winding pack and case thicknesses", + dr_tf_inboard=data.build.dr_tf_inboard, + ) + # Can't use c_tf_turn as iteration var, constraint or # input if i_tf_turns_integer == 1 if ( diff --git a/tests/unit/core/test_init.py b/tests/unit/core/test_init.py new file mode 100644 index 0000000000..729336a8fd --- /dev/null +++ b/tests/unit/core/test_init.py @@ -0,0 +1,74 @@ +"""Unit tests for input sanity checks in process.core.init.check_process.""" + +import pytest + +from process.core.exceptions import ProcessValidationError +from process.core.init import check_process +from process.core.model import DataStructure +from process.models.tfcoil.base import TFConductorModel + + +def _validation_error_message(data): + """Run check_process and return any validation error message. + + Later, unrelated checks may still fire on an otherwise-default + DataStructure, so callers assert on the message content rather than + on whether an error was raised. + """ + try: + check_process(None, data) + except ProcessValidationError as error: + return str(error) + return "" + + +def test_zero_thickness_superconducting_tf_is_rejected(): + """SC TF with dr_tf_inboard left at 0 and neither ixc=13 nor ixc=140 + active must fail validation instead of silently building a machine + with no inboard TF coil. + """ + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING + data.build.dr_tf_inboard = 0.0 + + with pytest.raises(ProcessValidationError, match="dr_tf_inboard"): + check_process(None, data) + + +def test_explicit_tf_thickness_is_accepted(): + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING + data.build.dr_tf_inboard = 0.5 + + assert "dr_tf_inboard" not in _validation_error_message(data) + + +def test_wp_thickness_iteration_variable_is_accepted(): + """With ixc = 140 active, dr_tf_inboard is derived in the build model, + so a zero input value is legitimate. + """ + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING + data.build.dr_tf_inboard = 0.0 + data.numerics.n_iteration_variables = 1 + data.numerics.ixc[0] = 140 + + assert "dr_tf_inboard" not in _validation_error_message(data) + + +def test_resistive_tf_is_not_checked(): + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.WATER_COOLED_COPPER + data.build.dr_tf_inboard = 0.0 + + assert "dr_tf_inboard" not in _validation_error_message(data) + + +def test_stellarator_is_not_checked(): + """Stellarators calculate dr_tf_inboard during the model run.""" + data = DataStructure() + data.stellarator.istell = 1 + data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING + data.build.dr_tf_inboard = 0.0 + + assert "dr_tf_inboard" not in _validation_error_message(data) diff --git a/tests/unit/core/test_input.py b/tests/unit/core/test_input.py index b716e7a170..8efa1609a6 100644 --- a/tests/unit/core/test_input.py +++ b/tests/unit/core/test_input.py @@ -12,7 +12,12 @@ @pytest.fixture def data_structure_obj(): - return DataStructure() + data = DataStructure() + # These parser tests run init_process on minimal input snippets; give the + # scaffold a valid TF thickness so configuration validation (which rejects + # a zero-thickness superconducting TF) does not reject the scaffold. + data.build.dr_tf_inboard = 1.0 + return data def _create_input_file(directory, content: str):