From 8f9b4f6eea922e9c73861a8a53dc4c177e5f6eea Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Tue, 31 Mar 2026 09:22:46 +0100 Subject: [PATCH 1/6] Support input's being set on new data structure dataclass --- process/core/init.py | 10 ++++++-- process/core/input.py | 33 ++++++++++++++++---------- process/main.py | 8 +++---- tests/unit/test_input.py | 51 ++++++++++++++++++++++++++-------------- 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/process/core/init.py b/process/core/init.py index 78e1bc47b9..3c7f355c5e 100644 --- a/process/core/init.py +++ b/process/core/init.py @@ -1,8 +1,11 @@ +from __future__ import annotations + import datetime import getpass import socket import subprocess from pathlib import Path +from typing import TYPE_CHECKING from warnings import warn import process @@ -62,8 +65,11 @@ from process.models.stellarator.initialization import st_init from process.models.tfcoil.base import TFCoilShapeModel +if TYPE_CHECKING: + from process.main import DataStructure + -def init_process(): +def init_process(data_structure: DataStructure): """Routine that calls the initialisation routines This routine calls the main initialisation routines that set @@ -77,7 +83,7 @@ def init_process(): process_output.OutputFileManager.open_files() # Input any desired new initial values - inputs = parse_input_file() + inputs = parse_input_file(data_structure) # Set active constraints set_active_constraints() diff --git a/process/core/input.py b/process/core/input.py index 7cb7b76577..279c89061b 100644 --- a/process/core/input.py +++ b/process/core/input.py @@ -1,11 +1,13 @@ """Handle parsing, validation, and actioning of a PROCESS input file (*IN.DAT).""" +from __future__ import annotations + import copy import re from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from warnings import warn import process @@ -13,6 +15,9 @@ from process.core.exceptions import ProcessValidationError, ProcessValueError from process.core.solver.constraints import ConstraintManager +if TYPE_CHECKING: + from process.main import DataStructure + NumberType = int | float ValidInputTypes = NumberType | str @@ -45,7 +50,7 @@ class InputVariable: array: bool = False """Is this input assigning values to an array?""" additional_validation: ( - Callable[[str, ValidInputTypes, int | None, "InputVariable"], ValidInputTypes] + Callable[[str, ValidInputTypes, int | None, InputVariable], ValidInputTypes] | None ) = None """A function that takes the input variable: name, value, array index, and config (this dataclass) @@ -56,7 +61,7 @@ class InputVariable: been cast to the specified `type`. """ additional_actions: ( - Callable[[str, ValidInputTypes, int | None, "InputVariable"], None] | None + Callable[[str, ValidInputTypes, int | None, InputVariable], None] | None ) = None """A function that takes the input variable: name, value, array index, and config (this dataclass) as input and performs some additional action in addition to the default actions prescribed by the variables @@ -184,9 +189,7 @@ def __post_init__(self): "admv": InputVariable( data_structure.buildings_variables, float, range=(1.0e4, 1.0e6) ), - "airtemp": InputVariable( - data_structure.water_usage_variables, float, range=(-15.0, 40.0) - ), + "airtemp": InputVariable("water_use", float, range=(-15.0, 40.0)), "alfapf": InputVariable(data_structure.pfcoil_variables, float, range=(1e-12, 1.0)), "alstroh": InputVariable( data_structure.pfcoil_variables, float, range=(1000000.0, 100000000000.0) @@ -1770,18 +1773,14 @@ def __post_init__(self): "water_buildings_w": InputVariable( data_structure.buildings_variables, float, range=(10.0, 1000.0) ), - "watertemp": InputVariable( - data_structure.water_usage_variables, float, range=(0.0, 25.0) - ), + "watertemp": InputVariable("water_use", float, range=(0.0, 25.0)), "wgt": InputVariable( data_structure.buildings_variables, float, range=(10000.0, 1000000.0) ), "wgt2": InputVariable( data_structure.buildings_variables, float, range=(10000.0, 1000000.0) ), - "windspeed": InputVariable( - data_structure.water_usage_variables, float, range=(0.0, 10.0) - ), + "windspeed": InputVariable("water_use", float, range=(0.0, 10.0)), "workshop_h": InputVariable( data_structure.buildings_variables, float, range=(1.0, 100.0) ), @@ -2148,7 +2147,7 @@ def __post_init__(self): } -def parse_input_file(): +def parse_input_file(data_structure_obj: DataStructure): input_file = data_structure.global_variables.fileprefix input_file_path = Path("IN.DAT") @@ -2186,6 +2185,14 @@ def parse_input_file(): variable_config = INPUT_VARIABLES.get(variable_name) + # string indicates it should be set on the new object data structure + if isinstance(variable_config.module, str): + module = data_structure_obj + for name in variable_config.module.split("."): + module = getattr(module, name) + + variable_config.module = module + if variable_config is None: error_msg = ( f"Unrecognised input '{variable_name}' at line {line_no} of input file." diff --git a/process/main.py b/process/main.py index c37e24dadb..d4327ea6ab 100644 --- a/process/main.py +++ b/process/main.py @@ -405,8 +405,8 @@ def __init__( self.validate_input(update_obsolete) self.init_module_vars() self.set_filenames() - self.initialise() self.models = Models() + self.initialise() self.solver = solver def run(self): @@ -479,7 +479,7 @@ def initialise(self): initialise_imprad() # Reads in input file - init.init_process() + init.init_process(self.models) # Order optimisation parameters (arbitrary order in input file) # Ensures consistency and makes output comparisons more straightforward @@ -778,9 +778,9 @@ def setup_data_structure(self): # This Models class should be replaced with a dataclass so we can # iterate over the `fields`. # This can be a disgusting temporary measure :( - data = DataStructure() + self.data = DataStructure() for model in self.models: - model.data = data + model.data = self.data # setup handlers for writing to terminal (on warnings+) diff --git a/tests/unit/test_input.py b/tests/unit/test_input.py index 863610a58e..40830b94fb 100644 --- a/tests/unit/test_input.py +++ b/tests/unit/test_input.py @@ -7,6 +7,12 @@ import process.core.input as process_input import process.data_structure as data_structure from process.core.exceptions import ProcessValidationError +from process.main import DataStructure + + +@pytest.fixture +def data_structure_obj(): + return DataStructure() def _create_input_file(directory, content: str): @@ -54,7 +60,7 @@ def _create_input_file(directory, content: str): ] + [("0.546816593988753", 0.546816593988753)], ) -def test_parse_real(epsvmc, expected, tmp_path): +def test_parse_real(epsvmc, expected, tmp_path, data_structure_obj): """Tests the parsing of real numbers into PROCESS. Program to get the expected value for 0.008 provided at https://github.com/ukaea/PROCESS/pull/3067 @@ -62,7 +68,7 @@ def test_parse_real(epsvmc, expected, tmp_path): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, f"epsvmc = {epsvmc}" ) - init.init_process() + init.init_process(data_structure_obj) assert data_structure.numerics.epsvmc == expected @@ -78,7 +84,7 @@ def test_parse_real(epsvmc, expected, tmp_path): [0.1293140904093427], ], ) -def test_exact_parsing(value, tmp_path): +def test_exact_parsing(value, tmp_path, data_structure_obj): """Tests the parsing of real numbers into PROCESS. These tests failed using the old input parser and serve to show that the Python parser generally @@ -87,17 +93,17 @@ def test_exact_parsing(value, tmp_path): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, f"epsvmc = {value}" ) - init.init_process() + init.init_process(data_structure_obj) assert data_structure.numerics.epsvmc == value -def test_parse_input(tmp_path): +def test_parse_input(tmp_path, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, ("runtitle = my run title\nioptimz = -2\nepsvmc = 0.6\nboundl(1) = 0.5"), ) - init.init_process() + init.init_process(data_structure_obj) assert data_structure.global_variables.runtitle == "my run title" assert data_structure.numerics.ioptimz == -2 @@ -105,19 +111,19 @@ def test_parse_input(tmp_path): assert pytest.approx(data_structure.numerics.boundl[0]) == 0.5 -def test_input_choices(tmp_path): +def test_input_choices(tmp_path, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, ("ioptimz = -1") ) with pytest.raises(ProcessValidationError): - init.init_process() + init.init_process(data_structure_obj) @pytest.mark.parametrize( ("input_file_value"), ((-0.01,), (1.1,)), ids=("violate lower", "violate upper") ) -def test_input_range(tmp_path, input_file_value): +def test_input_range(tmp_path, input_file_value, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, (f"epsvmc = {input_file_value}") ) @@ -126,42 +132,51 @@ def test_input_range(tmp_path, input_file_value): assert process_input.INPUT_VARIABLES["epsvmc"].range == (0.0, 1.0) with pytest.raises(ProcessValidationError): - init.init_process() + init.init_process(data_structure_obj) -def test_input_array_when_not(tmp_path): +def test_input_array_when_not(tmp_path, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, ("epsvmc(1) = 0.5") ) with pytest.raises(ProcessValidationError): - init.init_process() + init.init_process(data_structure_obj) -def test_input_not_array_when_is(tmp_path): +def test_input_not_array_when_is(tmp_path, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, ("boundl = 0.5") ) with pytest.raises(ProcessValidationError): - init.init_process() + init.init_process(data_structure_obj) -def test_input_float_when_int(tmp_path): +def test_input_float_when_int(tmp_path, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, ("ioptimz = 0.5") ) with pytest.raises(ProcessValidationError): - init.init_process() + init.init_process(data_structure_obj) -def test_input_array(tmp_path): +def test_input_array(tmp_path, data_structure_obj): data_structure.global_variables.fileprefix = _create_input_file( tmp_path, ("boundl = 0.1, 0.2, 1.0, 0.0, 1.0e2") ) - init.init_process() + init.init_process(data_structure_obj) np.testing.assert_array_equal( data_structure.numerics.boundl[:6], [0.1, 0.2, 1.0, 0.0, 1.0e2, 0] ) + + +def test_input_on_new_data_structure(tmp_path, data_structure_obj): + data_structure.global_variables.fileprefix = _create_input_file( + tmp_path, ("windspeed = 1.22") + ) + + init.init_process(data_structure_obj) + assert data_structure_obj.water_use.windspeed == 1.22 From 6c88e91ba85afb6bd786c3a6939f1427c4946c7f Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Tue, 31 Mar 2026 11:47:35 +0100 Subject: [PATCH 2/6] Add Models/data structure to VaryRun --- process/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/process/main.py b/process/main.py index d4327ea6ab..722f14bbdc 100644 --- a/process/main.py +++ b/process/main.py @@ -308,6 +308,7 @@ def __init__(self, config_file: str, solver: str = "vmcon"): # dir changes happen in old run_process code self.config_file = Path(config_file).resolve() self.solver = solver + self.models = Models() def run(self): """Perform a VaryRun by running multiple SingleRuns. @@ -328,7 +329,7 @@ def run(self): setup_loggers(Path(config.wdir) / "process.log") init.init_all_module_vars() - init.init_process() + init.init_process(self.models.data) _neqns, itervars = get_neqns_itervars() lbs, ubs = get_variable_range(itervars, config.factor) From 9f06d16273dd3cc0362ca42c0bc7986de6c944c9 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Tue, 31 Mar 2026 13:11:06 +0100 Subject: [PATCH 3/6] Create data structure on run object rather than models --- process/main.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/process/main.py b/process/main.py index 722f14bbdc..4412584878 100644 --- a/process/main.py +++ b/process/main.py @@ -308,7 +308,8 @@ def __init__(self, config_file: str, solver: str = "vmcon"): # dir changes happen in old run_process code self.config_file = Path(config_file).resolve() self.solver = solver - self.models = Models() + self.data = DataStructure() + self.models = Models(self.data) def run(self): """Perform a VaryRun by running multiple SingleRuns. @@ -329,7 +330,7 @@ def run(self): setup_loggers(Path(config.wdir) / "process.log") init.init_all_module_vars() - init.init_process(self.models.data) + init.init_process(self.data) _neqns, itervars = get_neqns_itervars() lbs, ubs = get_variable_range(itervars, config.factor) @@ -406,8 +407,9 @@ def __init__( self.validate_input(update_obsolete) self.init_module_vars() self.set_filenames() - self.models = Models() + self.data = DataStructure() self.initialise() + self.models = Models(self.data) self.solver = solver def run(self): @@ -480,7 +482,7 @@ def initialise(self): initialise_imprad() # Reads in input file - init.init_process(self.models) + init.init_process(self.data) # Order optimisation parameters (arbitrary order in input file) # Ensures consistency and makes output comparisons more straightforward @@ -665,11 +667,13 @@ class Models: engineering modules. """ - def __init__(self): + def __init__(self, data: DataStructure): """Create physics and engineering model objects. This also initialises module variables in the Fortran for that module. """ + self.data = data + self._costs_custom = None self._costs_1990 = Costs() self._costs_2015 = Costs2015() @@ -732,21 +736,20 @@ def __init__(self): plasma_profile=self.plasma_profile, ) self.neoclassics = Neoclassics() - if data_structure.stellarator_variables.istell != 0: - self.stellarator = Stellarator( - availability=self.availability, - buildings=self.buildings, - vacuum=self.vacuum, - costs=self.costs, - power=self.power, - plasma_profile=self.plasma_profile, - hcpb=self.ccfe_hcpb, - current_drive=self.current_drive, - physics=self.physics, - neoclassics=self.neoclassics, - plasma_beta=self.plasma_beta, - plasma_bootstrap=self.plasma_bootstrap_current, - ) + self.stellarator = Stellarator( + availability=self.availability, + buildings=self.buildings, + vacuum=self.vacuum, + costs=self.costs, + power=self.power, + plasma_profile=self.plasma_profile, + hcpb=self.ccfe_hcpb, + current_drive=self.current_drive, + physics=self.physics, + neoclassics=self.neoclassics, + plasma_beta=self.plasma_beta, + plasma_bootstrap=self.plasma_bootstrap_current, + ) self.dcll = DCLL(fw=self.fw) @@ -779,7 +782,6 @@ def setup_data_structure(self): # This Models class should be replaced with a dataclass so we can # iterate over the `fields`. # This can be a disgusting temporary measure :( - self.data = DataStructure() for model in self.models: model.data = self.data From 09b22a12f37363c92fd9e18c80f2d21fd6c61591 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Tue, 31 Mar 2026 13:21:02 +0100 Subject: [PATCH 4/6] Setup new data structure in unit tests --- process/core/init.py | 2 +- process/core/input.py | 2 +- tests/conftest.py | 3 ++- tests/unit/test_input.py | 2 +- tests/unit/test_main.py | 2 ++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/process/core/init.py b/process/core/init.py index 3c7f355c5e..d8a2a49c4b 100644 --- a/process/core/init.py +++ b/process/core/init.py @@ -66,7 +66,7 @@ from process.models.tfcoil.base import TFCoilShapeModel if TYPE_CHECKING: - from process.main import DataStructure + from process.core.model import DataStructure def init_process(data_structure: DataStructure): diff --git a/process/core/input.py b/process/core/input.py index 279c89061b..c0f6036b9e 100644 --- a/process/core/input.py +++ b/process/core/input.py @@ -16,7 +16,7 @@ from process.core.solver.constraints import ConstraintManager if TYPE_CHECKING: - from process.main import DataStructure + from process.core.model import DataStructure NumberType = int | float ValidInputTypes = NumberType | str diff --git a/tests/conftest.py b/tests/conftest.py index 1123481eaf..b205191780 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ from process import main from process.core.log import logging_model_handler +from process.core.model import DataStructure from process.main import Models @@ -235,4 +236,4 @@ def _plot_show_and_close_class(request): @pytest.fixture def process_models(): - return Models() + return Models(DataStructure()) diff --git a/tests/unit/test_input.py b/tests/unit/test_input.py index 40830b94fb..767f881511 100644 --- a/tests/unit/test_input.py +++ b/tests/unit/test_input.py @@ -7,7 +7,7 @@ import process.core.input as process_input import process.data_structure as data_structure from process.core.exceptions import ProcessValidationError -from process.main import DataStructure +from process.core.model import DataStructure @pytest.fixture diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ecc04b4ed4..1bdd4859c4 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -7,6 +7,7 @@ import pytest from process import data_structure, main +from process.core.model import DataStructure from process.main import Process, SingleRun, VaryRun @@ -136,6 +137,7 @@ def single_run(monkeypatch, input_file, tmp_path): single_run.input_file = str(temp_input_file) single_run.models = None + single_run.data = DataStructure() single_run.set_filenames() single_run.initialise() return single_run From 3039dbc563a988d217dde1832bfbf633de9c6f7d Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Wed, 1 Apr 2026 09:23:02 +0100 Subject: [PATCH 5/6] Remove models from VaryRun --- process/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/process/main.py b/process/main.py index 4412584878..2548099ca7 100644 --- a/process/main.py +++ b/process/main.py @@ -309,7 +309,6 @@ def __init__(self, config_file: str, solver: str = "vmcon"): self.config_file = Path(config_file).resolve() self.solver = solver self.data = DataStructure() - self.models = Models(self.data) def run(self): """Perform a VaryRun by running multiple SingleRuns. From ea07fba3854b214865cea28f227cb3fd0f44f9b2 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Wed, 1 Apr 2026 13:52:43 +0100 Subject: [PATCH 6/6] Re-add Stellarator model init condition --- process/main.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/process/main.py b/process/main.py index 2548099ca7..0e6fd15519 100644 --- a/process/main.py +++ b/process/main.py @@ -735,20 +735,21 @@ def __init__(self, data: DataStructure): plasma_profile=self.plasma_profile, ) self.neoclassics = Neoclassics() - self.stellarator = Stellarator( - availability=self.availability, - buildings=self.buildings, - vacuum=self.vacuum, - costs=self.costs, - power=self.power, - plasma_profile=self.plasma_profile, - hcpb=self.ccfe_hcpb, - current_drive=self.current_drive, - physics=self.physics, - neoclassics=self.neoclassics, - plasma_beta=self.plasma_beta, - plasma_bootstrap=self.plasma_bootstrap_current, - ) + if data_structure.stellarator_variables.istell != 0: + self.stellarator = Stellarator( + availability=self.availability, + buildings=self.buildings, + vacuum=self.vacuum, + costs=self.costs, + power=self.power, + plasma_profile=self.plasma_profile, + hcpb=self.ccfe_hcpb, + current_drive=self.current_drive, + physics=self.physics, + neoclassics=self.neoclassics, + plasma_beta=self.plasma_beta, + plasma_bootstrap=self.plasma_bootstrap_current, + ) self.dcll = DCLL(fw=self.fw)