From 4055983ac5405f9bb5d5bb409156d074ee31a6a7 Mon Sep 17 00:00:00 2001 From: DavidA Date: Sat, 15 Aug 2026 14:41:26 +0100 Subject: [PATCH 1/5] 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 8a77a2764c..ab01eb2d4c 100644 --- a/process/core/init.py +++ b/process/core/init.py @@ -289,6 +289,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 65cb364b80..962802174f 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): From a89dcb289c0bc08689915f20b379b6085a497d58 Mon Sep 17 00:00:00 2001 From: DavidA Date: Tue, 25 Aug 2026 16:27:13 +0100 Subject: [PATCH 2/5] Address review: check all conductor models, no ixc 13 exemption, parser tests skip validation - The check no longer exempts ixc = 13: the input value seeds the first model evaluation either way; an exactly-zero value was only caught later by the generic iteration-variable check, and a negative value was not caught at all (the 1/value scaling in load_iteration_variables inverts the variable's bounds). Rejecting at input validation covers both with an actionable message. - The error message drops "or use ixc = 13" accordingly. - The conductor-model condition is removed: dr_tf_inboard is only derived under ixc = 140 or for stellarators, neither of which depends on i_tf_sup, so resistive TF coils had the same silent zero-thickness path. - test_input.py keeps its scaffold unchanged and instead disables check_process with an autouse monkeypatch fixture, as suggested; the parser tests no longer carry TF geometry. - Tests updated: resistive zero-thickness now rejected (plus an accepted positive-thickness case), and ixc = 13 with zero or negative input is rejected. Unit suite 855 passed / 4 skipped; integration 21 passed / 1 skipped (all shipped regression inputs pass the widened check). Rebased onto main (includes #4553). Co-Authored-By: Claude Fable 5 --- process/core/init.py | 26 +++++++++++++++----------- tests/unit/core/test_init.py | 31 ++++++++++++++++++++++++++++++- tests/unit/core/test_input.py | 13 +++++++------ 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/process/core/init.py b/process/core/init.py index ab01eb2d4c..1c3b5aff67 100644 --- a/process/core/init.py +++ b/process/core/init.py @@ -289,24 +289,28 @@ 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. + # An inboard TF coil must have a positive 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); otherwise the input-file value is what + # the radial build uses, and the default of 0 silently removes the TF + # coil. This applies to superconducting and resistive conductors alike, + # and also when dr_tf_inboard itself is the iteration variable + # (ixc = 13): the input value seeds the first model evaluation, an + # exactly-zero value is only caught later by the generic + # iteration-variable check, and a negative value is not caught at all + # (the 1/value scaling in load_iteration_variables inverts the + # variable's bounds). 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_inboard is not positive: the inboard TF coil has no" + " thickness. Set a positive dr_tf_inboard, 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, diff --git a/tests/unit/core/test_init.py b/tests/unit/core/test_init.py index 729336a8fd..1cd132eae9 100644 --- a/tests/unit/core/test_init.py +++ b/tests/unit/core/test_init.py @@ -56,14 +56,43 @@ def test_wp_thickness_iteration_variable_is_accepted(): assert "dr_tf_inboard" not in _validation_error_message(data) -def test_resistive_tf_is_not_checked(): +def test_zero_thickness_resistive_tf_is_rejected(): + """Resistive TF coils use dr_tf_inboard through the same radial-build + path as superconducting ones, so a zero thickness is equally invalid. + """ data = DataStructure() data.tfcoil.i_tf_sup = TFConductorModel.WATER_COOLED_COPPER data.build.dr_tf_inboard = 0.0 + with pytest.raises(ProcessValidationError, match="dr_tf_inboard"): + check_process(None, data) + + +def test_explicit_thickness_resistive_tf_is_accepted(): + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.WATER_COOLED_COPPER + data.build.dr_tf_inboard = 0.5 + assert "dr_tf_inboard" not in _validation_error_message(data) +def test_thickness_iteration_variable_does_not_exempt(): + """ixc = 13 does not exempt a non-positive input value: it seeds the + first model evaluation, an exactly-zero value is only rejected later by + the generic iteration-variable check, and a negative value is not + rejected at all (the 1/value scaling inverts the variable's bounds). + """ + for bad_value in (0.0, -0.5): + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING + data.build.dr_tf_inboard = bad_value + data.numerics.n_iteration_variables = 1 + data.numerics.ixc[0] = 13 + + with pytest.raises(ProcessValidationError, match="dr_tf_inboard"): + check_process(None, data) + + def test_stellarator_is_not_checked(): """Stellarators calculate dr_tf_inboard during the model run.""" data = DataStructure() diff --git a/tests/unit/core/test_input.py b/tests/unit/core/test_input.py index 962802174f..34703efe5c 100644 --- a/tests/unit/core/test_input.py +++ b/tests/unit/core/test_input.py @@ -10,14 +10,15 @@ from process.data_structure.numerics import PROCESSRunMode +@pytest.fixture(autouse=True) +def turn_off_check_process(monkeypatch): + """These are parser tests; configuration validation is not under test.""" + monkeypatch.setattr(init, "check_process", lambda *_: None) + + @pytest.fixture def data_structure_obj(): - 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 + return DataStructure() def _create_input_file(directory, content: str): From 91fb3175e6713c154466092819f2f93e8b588ca4 Mon Sep 17 00:00:00 2001 From: DavidA Date: Thu, 27 Aug 2026 04:15:09 +0100 Subject: [PATCH 3/5] Parametrize non-positive thickness test; apply fixture docstring suggestion Also corrects the test docstring: a negative dr_tf_inboard cannot come from an input file (the parser bounds it to [0, 10]); check_process guards the data structure however it was populated. Co-Authored-By: Claude Fable 5 --- tests/unit/core/test_init.py | 28 +++++++++++++++------------- tests/unit/core/test_input.py | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/unit/core/test_init.py b/tests/unit/core/test_init.py index 1cd132eae9..d08edd1713 100644 --- a/tests/unit/core/test_init.py +++ b/tests/unit/core/test_init.py @@ -76,21 +76,23 @@ def test_explicit_thickness_resistive_tf_is_accepted(): assert "dr_tf_inboard" not in _validation_error_message(data) -def test_thickness_iteration_variable_does_not_exempt(): +@pytest.mark.parametrize("bad_value", [0.0, -0.5]) +def test_thickness_iteration_variable_does_not_exempt(bad_value): """ixc = 13 does not exempt a non-positive input value: it seeds the - first model evaluation, an exactly-zero value is only rejected later by - the generic iteration-variable check, and a negative value is not - rejected at all (the 1/value scaling inverts the variable's bounds). + first model evaluation, and an exactly-zero value is only rejected + later by the generic iteration-variable check. A negative value + cannot come from an input file (the parser bounds dr_tf_inboard to + [0, 10]), but check_process guards the data structure however it was + populated. """ - for bad_value in (0.0, -0.5): - data = DataStructure() - data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING - data.build.dr_tf_inboard = bad_value - data.numerics.n_iteration_variables = 1 - data.numerics.ixc[0] = 13 - - with pytest.raises(ProcessValidationError, match="dr_tf_inboard"): - check_process(None, data) + data = DataStructure() + data.tfcoil.i_tf_sup = TFConductorModel.SUPERCONDUCTING + data.build.dr_tf_inboard = bad_value + data.numerics.n_iteration_variables = 1 + data.numerics.ixc[0] = 13 + + with pytest.raises(ProcessValidationError, match="dr_tf_inboard"): + check_process(None, data) def test_stellarator_is_not_checked(): diff --git a/tests/unit/core/test_input.py b/tests/unit/core/test_input.py index 34703efe5c..4af9b60a11 100644 --- a/tests/unit/core/test_input.py +++ b/tests/unit/core/test_input.py @@ -12,7 +12,7 @@ @pytest.fixture(autouse=True) def turn_off_check_process(monkeypatch): - """These are parser tests; configuration validation is not under test.""" + """These are parser tests; configuration validation is not a part of test.""" monkeypatch.setattr(init, "check_process", lambda *_: None) From 49c45cf7cfa7adae068f02580c2a2510f6d4c1f0 Mon Sep 17 00:00:00 2001 From: Timothy <75321887+timothy-nunn@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:45:25 +0100 Subject: [PATCH 4/5] DataStructure module changed --- tests/unit/core/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/core/test_init.py b/tests/unit/core/test_init.py index d08edd1713..a9af4e7308 100644 --- a/tests/unit/core/test_init.py +++ b/tests/unit/core/test_init.py @@ -4,7 +4,7 @@ from process.core.exceptions import ProcessValidationError from process.core.init import check_process -from process.core.model import DataStructure +from process.core.data_structure.base import DataStructure from process.models.tfcoil.base import TFConductorModel From 1409e503c5baebce8575e4d08c0e4898195d17f0 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Thu, 27 Aug 2026 10:33:36 +0100 Subject: [PATCH 5/5] Re-order imports --- tests/unit/core/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/core/test_init.py b/tests/unit/core/test_init.py index a9af4e7308..491612932e 100644 --- a/tests/unit/core/test_init.py +++ b/tests/unit/core/test_init.py @@ -2,9 +2,9 @@ import pytest +from process.core.data_structure.base import DataStructure from process.core.exceptions import ProcessValidationError from process.core.init import check_process -from process.core.data_structure.base import DataStructure from process.models.tfcoil.base import TFConductorModel