Skip to content

Reject zero-thickness superconducting TF at input validation - #4535

Merged
timothy-nunn merged 5 commits into
ukaea:mainfrom
dallonby:fix/zero-thickness-tf-validation
Aug 27, 2026
Merged

Reject zero-thickness superconducting TF at input validation#4535
timothy-nunn merged 5 commits into
ukaea:mainfrom
dallonby:fix/zero-thickness-tf-validation

Conversation

@dallonby

Copy link
Copy Markdown
Contributor

Closes #4534

Overview

Adds a check_process validation so that the silent zero-thickness-TF failure mode described in the issue becomes an immediate, actionable input error instead of an unexplained downstream solver failure.

Changes

  • process/core/init.py: after the existing ixc 13/140 mutual-exclusion check, raise ProcessValidationError when i_tf_sup is superconducting, neither ixc 13 nor ixc 140 is active, and dr_tf_inboard <= 0. The message tells the user the three ways to fix their file. Stellarators (istell != 0, which calculate dr_tf_inboard during the model run) and IFE are excluded.
  • New tests/unit/core/test_init.py: five tests — the rejected configuration, plus accepted configurations for explicit thickness, ixc 140 active, resistive TF, and stellarator.
  • tests/unit/core/test_input.py (test-suite change, per CONTRIBUTING): the parser tests run init_process on minimal input snippets (e.g. just epsvmc = 1.0) and relied on config validation never examining the TF geometry; their shared fixture scaffold now sets a valid dr_tf_inboard so the parser tests keep testing parsing. No expected values change.

Behavioural impact

Valid configurations are unaffected (all seven shipped regression inputs pass: they each set ixc 13/140 or dr_tf_inboard, or are stellarator/IFE). The only newly-rejected configurations are ones that previously produced a machine with no inboard TF coil.

Verification

  • Full unit suite: 847 passed / 4 skipped; integration suite: 21 passed / 1 skipped (Python 3.12, local).

Found during an independent audit of v3.4.2.

🤖 Generated with Claude Code

@dallonby
dallonby requested a review from a team as a code owner August 15, 2026 13:43
@codecov-commenter

codecov-commenter commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 49.47%. Comparing base (986a8f0) to head (1409e50).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4535      +/-   ##
==========================================
- Coverage   49.47%   49.47%   -0.01%     
==========================================
  Files         150      151       +1     
  Lines       30069    30078       +9     
==========================================
+ Hits        14878    14882       +4     
- Misses      15191    15196       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@je-cook

je-cook commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

@ukaea/process-model-review this may adversely effect the optimiser as it is currently allowed to search invalid regions of space I think?

I suppose this is only checking init

@je-cook
je-cook requested a review from a team August 17, 2026 12:52
@mkovari

mkovari commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Obviously this needs to be fixed - maybe someone who has looked at this code more recently than me - @chris-ashe or someone?

@dallonby

Copy link
Copy Markdown
Contributor Author

Init only. check_process is called from exactly one place, init_process (init.py:79), which SingleRun.__init__ runs once (main.py:339 then :432) before the Models objects are even built (main.py:340), so before Scan, SolverHandler or VMCON get anywhere near it. Scans don't re-init between points (scan.py just sets the swept variable), VaryRun spawns a fresh SingleRun per iteration, and dr_tf_inboard only gets written during model evaluation when ixc 140 is active (build.py:1684) or for stellarators, both of which the check excludes.

To be sure I wasn't reasoning my way past something, I instrumented it on the branch: 1 call to check_process vs 26 model evaluations (spherical_tokamak_eval, fsolve), 2029 (large_tokamak_nof, VMCON), 4286 (low_aspect_ratio_DEMO) and 16175 (a 3-point scan). One call every time, before the solve. So the optimiser's freedom to wander through invalid regions is untouched; the check only looks at inputs that are constant for the whole run.

Two things I should say explicitly though. It is a deliberate behaviour change for scaffold inputs that rely on the dr_tf_inboard = 0 default, which is why the test_input.py fixture needed a value. For what it's worth, on main those inputs currently either hit an uncaught ZeroDivisionError (tfcoil/base.py:424, circular case) or, with i_tf_case_geom = 1, report a "feasible solution" with a winding pack of -0.34 m and a negative turn count. And I only covered the superconducting case that was reported; a resistive TF with the same zero goes down the same build.py path, so it could be extended in a follow-up if you'd like.

@timothy-nunn timothy-nunn self-assigned this Aug 25, 2026
Comment thread process/core/init.py Outdated
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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this condition should be removed because on the first iteration with ixc = 13 the IN.DAT-defined value of dr_tf_inboard is used which would cause the model errors this condition is designed to protect against.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This would also require a slight change to the error message below.

Comment thread process/core/init.py Outdated
if (
data.stellarator.istell == 0
and data.ife.ife == 0
and data.tfcoil.i_tf_sup == TFConductorModel.SUPERCONDUCTING

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hopefully someone from @ukaea/process-model-review can confirm, but I think we can probably test this condition for superconducting and resistive TF coils. Resistive TF still uses dr_tf_inboard and it would not surprise me if it being 0 or negative would be equally problematic here too.

Comment thread tests/unit/core/test_input.py Outdated
Comment on lines +14 to +20
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I want to avoid having to do specific setup in this test wherever possible. A better option for me is to turn off check_process since its not actually useful for this test.

@pytest.fixture(autouse=True)
def turn_off_check_process(monkeypatch):
    monkeypatch.setattr(init, "check_process", lambda *_: None)

Rebasing to include #4553 (now merged onto main) will also be useful because that will check none of these tests fail because of the above change.

@timothy-nunn

timothy-nunn commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

@je-cook the only impact on the "solver" (on the solution) will be that some users may need to update their initial point if they currently have dr_tf_inboard /= 0 which could change the path/solution.

dallonby and others added 2 commits August 25, 2026 16:23
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 <noreply@anthropic.com>
…er 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 ukaea#4553).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dallonby

Copy link
Copy Markdown
Contributor Author

Thanks — all four points taken, in a89dcb2 (rebased onto main, so #4553's match= checks run against the fixture change).

One precision on the ixc = 13 case, because it slightly strengthens your change: with dr_tf_inboard = 0 and ixc = 13, the run already failed before any model evaluation — load_iteration_variables computes scale = 1/value, and check_iteration_variable rejects an exactly-zero value with the generic "iteration variable is 0" error. What the exemption really left open is a negative input value: that passes check_iteration_variable (it only tests |v| ≤ 1e-12/NaN/inf), the negative scale inverts the variable's scaled bounds, and the first model evaluation runs with a negative thickness. The check now rejects both at input validation with the actionable message; the message no longer suggests ixc = 13 as a remedy.

The conductor-model condition is removed rather than widened: dr_tf_inboard is only derived under ixc = 140 (Build.calculate_radial_build) or for stellarators, neither of which depends on i_tf_sup, so resistive coils had the identical path. Your check_process monkeypatch is in as written.

Unit 855 passed / 4 skipped; integration 21 passed / 1 skipped — all shipped inputs pass the widened check.

@dallonby
dallonby force-pushed the fix/zero-thickness-tf-validation branch from 2fbe8a8 to a89dcb2 Compare August 25, 2026 17:10
Comment thread tests/unit/core/test_init.py Outdated
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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry that I missed this in my first review, I would prefer that this was parametrize'd rather than using a for loop over bad values

Comment thread tests/unit/core/test_input.py Outdated

@pytest.fixture(autouse=True)
def turn_off_check_process(monkeypatch):
"""These are parser tests; configuration validation is not under test."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"""These are parser tests; configuration validation is not under test."""
"""These are parser tests; configuration validation is not a part of test."""

@timothy-nunn

Copy link
Copy Markdown
Collaborator

Thanks @dallonby, I think I was actually wrong about the ixc = 13 case because the input parser would reject a negative value. Nevertheless I still think keeping the validation logic as is makes sense as it does no harm.

…estion

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 <noreply@anthropic.com>

@timothy-nunn timothy-nunn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a change we made yesterday that has not quite filtered through. Otherwise all looks good, thank you for finding this bug and providing a fix!

Comment thread tests/unit/core/test_init.py Outdated
@timothy-nunn
timothy-nunn merged commit 040a561 into ukaea:main Aug 27, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Superconducting TF silently loses all inboard thickness when dr_tf_wp_with_insulation is not an iteration variable

5 participants