Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 process/core/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,33 @@ def check_process(inputs, data): # noqa: ARG001
"Iteration variables 13 and 140 cannot be used simultaneously",
)

# 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 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 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,
)

# Can't use c_tf_turn as iteration var, constraint or
# input if i_tf_turns_integer == 1
if (
Expand Down
105 changes: 105 additions & 0 deletions tests/unit/core/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Unit tests for input sanity checks in process.core.init.check_process."""

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.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_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)


@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, 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.
"""
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()
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)
6 changes: 6 additions & 0 deletions tests/unit/core/test_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
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 a part of test."""
monkeypatch.setattr(init, "check_process", lambda *_: None)


@pytest.fixture
def data_structure_obj():
return DataStructure()
Expand Down
Loading