diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 0e6ebadbea..2898a04fb7 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -35,6 +35,7 @@ jobs: # to the CI runners python ./scripts/run_examples.py \ -e 'plasmod_example' \ + -e 'process_example' \ -e 'solver_example' \ -e 'equilibria/fem_fixed_boundary' \ -e 'codes/ext_code_script' diff --git a/bluemira/base/parameter_frame/_frame.py b/bluemira/base/parameter_frame/_frame.py index 01b4bf9362..555600ec1d 100644 --- a/bluemira/base/parameter_frame/_frame.py +++ b/bluemira/base/parameter_frame/_frame.py @@ -405,6 +405,20 @@ def _validate_parameter_field(field, member_type: Type) -> Tuple[Type, ...]: def _validate_units(param_data: Dict, value_type: Iterable[Type]): try: quantity = pint.Quantity(param_data["value"], param_data["unit"]) + except ValueError: + try: + quantity = pint.Quantity(f'{param_data["value"]}*{param_data["unit"]}') + except pint.errors.PintError as pe: + if param_data["value"] is None: + quantity = pint.Quantity( + 1 if param_data["unit"] in (None, "") else param_data["unit"] + ) + param_data["source"] = f"{param_data.get('source', '')}\nMAD UNIT 🤯 😭:" + else: + raise ValueError("Unit conversion failed") from pe + else: + param_data["value"] = quantity.magnitude + param_data["unit"] = quantity.units except KeyError as ke: raise ValueError("Parameters need a value and a unit") from ke except TypeError: @@ -434,6 +448,9 @@ def _validate_units(param_data: Dict, value_type: Iterable[Type]): param_data["unit"] = f"{unit:~P}" + if "MAD UNIT" in param_data.get("source", ""): + param_data["source"] += f"{quantity.magnitude}{param_data['unit']}" + def _remake_units(dimensionality: Union[Dict, pint.util.UnitsContainer]) -> pint.Unit: """Reconstruct unit from its dimensionality""" diff --git a/bluemira/builders/coil_supports.py b/bluemira/builders/coil_supports.py index c7eaeab91a..ac0ac0cded 100644 --- a/bluemira/builders/coil_supports.py +++ b/bluemira/builders/coil_supports.py @@ -28,6 +28,7 @@ from typing import Dict, List, Optional, Tuple, Type, Union import numpy as np +import numpy.typing as npt from bluemira.base.builder import Builder from bluemira.base.components import Component, PhysicalComponent @@ -38,7 +39,7 @@ from bluemira.builders.tools import apply_component_display_options from bluemira.display.palettes import BLUE_PALETTE from bluemira.geometry.compound import BluemiraCompound -from bluemira.geometry.constants import VERY_BIG +from bluemira.geometry.constants import D_TOLERANCE, VERY_BIG from bluemira.geometry.coordinates import Coordinates, get_intersect from bluemira.geometry.error import GeometryError from bluemira.geometry.face import BluemiraFace @@ -621,7 +622,9 @@ def bounds() -> Tuple[np.ndarray, np.ndarray]: return np.array([0, 0]), np.array([1, 1]) @staticmethod - def f_L_to_wire(wire: BluemiraWire, x_norm: List[float]): # noqa: N802 + def f_L_to_wire( # noqa: N802 + wire: BluemiraWire, x_norm: Union[List[float], npt.NDArray] + ): """ Convert a pair of normalised L values to a wire """ @@ -667,6 +670,9 @@ def constrain_koz(self, x_norm: np.ndarray) -> np.ndarray: ------- KOZ constraint array """ + if np.isnan(x_norm).any(): + bluemira_warn(f"NaN in x_norm {x_norm}") + x_norm = np.array([0, D_TOLERANCE]) straight_line = self.f_L_to_wire(self.wire, x_norm) straight_points = straight_line.discretize(ndiscr=self.n_koz_discr).xz.T return signed_distance_2D_polygon(straight_points, self.koz_points) diff --git a/bluemira/codes/_freecadapi.py b/bluemira/codes/_freecadapi.py index 60bc7cee2a..34a68dc28a 100644 --- a/bluemira/codes/_freecadapi.py +++ b/bluemira/codes/_freecadapi.py @@ -595,6 +595,8 @@ def offset_wire( raise FreeCADError(msg) from None fix_wire(wire) + if not wire.isClosed() and not open_wire: + raise FreeCADError("offset failed to close wire") return wire diff --git a/bluemira/codes/interface.py b/bluemira/codes/interface.py index 47e148b42e..bb3ade871a 100644 --- a/bluemira/codes/interface.py +++ b/bluemira/codes/interface.py @@ -236,9 +236,11 @@ def _map_external_outputs_to_bluemira_params( for bm_name, mapping in self.params.mappings.items(): if not (mapping.recv or recv_all): continue - output_value = self._get_output_or_raise(external_outputs, mapping.name) + output_value = self._get_output_or_raise(external_outputs, mapping.out_name) if mapping.unit is None: - bluemira_warn(f"{mapping.name} from code {self._name} has no known unit") + bluemira_warn( + f"{mapping.out_name} from code {self._name} has no known unit" + ) value = output_value elif output_value is None: value = output_value diff --git a/bluemira/codes/params.py b/bluemira/codes/params.py index 70cb0654a4..b81f4cef4d 100644 --- a/bluemira/codes/params.py +++ b/bluemira/codes/params.py @@ -47,7 +47,9 @@ def defaults(self) -> Dict: """ @classmethod - def from_defaults(cls, data: Dict) -> MappedParameterFrame: + def from_defaults( + cls, data: Dict, source: str = "bluemira codes default" + ) -> MappedParameterFrame: """ Create ParameterFrame with default values for external codes. @@ -62,7 +64,7 @@ def from_defaults(cls, data: Dict) -> MappedParameterFrame: new_param_dict[bm_map_name] = { "value": data.get(param_map.name, None), "unit": param_map.unit, - "source": "bluemira codes default", + "source": source, } return cls.from_dict(new_param_dict) @@ -130,6 +132,7 @@ class ParameterMapping: """ name: str + out_name: Optional[str] = None send: bool = True recv: bool = True unit: Optional[str] = None @@ -140,7 +143,9 @@ def __post_init__(self): """ Freeze the dataclass """ - self._frozen = ("name", "unit", "_frozen") + if self.out_name is None: + self.out_name = self.name + self._frozen = ("name", "out_name", "unit", "_frozen") def to_dict(self) -> Dict: """ @@ -148,6 +153,7 @@ def to_dict(self) -> Dict: """ return { "name": self.name, + "out_name": self.out_name, "send": self.send, "recv": self.recv, "unit": self.unit, @@ -179,10 +185,10 @@ def __setattr__(self, attr: str, value: Union[bool, str]): Value of attribute """ if ( - attr not in ["send", "recv", "name", "unit", "_frozen"] + attr not in ["send", "recv", "name", "out_name", "unit", "_frozen"] or attr in self._frozen ): - raise KeyError(f"{attr} cannot be set for a {self.__class__.__name__}") + raise KeyError(f"{attr} cannot be set for a {type(self).__name__}") if attr in ["send", "recv"] and not isinstance(value, bool): raise ValueError(f"{attr} must be a bool") super().__setattr__(attr, value) diff --git a/bluemira/codes/plasmod/params.py b/bluemira/codes/plasmod/params.py index 68b147e12f..62aea546a9 100644 --- a/bluemira/codes/plasmod/params.py +++ b/bluemira/codes/plasmod/params.py @@ -25,7 +25,7 @@ from copy import deepcopy from dataclasses import asdict, dataclass from enum import Enum -from typing import ClassVar, Dict, Union +from typing import Dict, Union from bluemira.base.parameter_frame import Parameter from bluemira.codes.params import MappedParameterFrame @@ -126,7 +126,7 @@ class PlasmodSolverParams(MappedParameterFrame): v_burn: Parameter[float] """Target loop voltage (if lower than -1e-3, ignored)-> plasma loop voltage [V].""" - _mappings: ClassVar = deepcopy(mappings) + _mappings = deepcopy(mappings) _defaults = PlasmodInputs() @property diff --git a/bluemira/codes/process/_equation_variable_mapping.py b/bluemira/codes/process/_equation_variable_mapping.py new file mode 100644 index 0000000000..d900cc92a9 --- /dev/null +++ b/bluemira/codes/process/_equation_variable_mapping.py @@ -0,0 +1,731 @@ +# bluemira is an integrated inter-disciplinary design tool for future fusion +# reactors. It incorporates several modules, some of which rely on other +# codes, to carry out a range of typical conceptual fusion reactor design +# activities. +# +# Copyright (C) 2021-2023 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh, +# J. Morris, D. Short +# +# bluemira is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# bluemira is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with bluemira; if not, see . +""" +Death to PROCESS integers +""" +from dataclasses import dataclass, field +from typing import Tuple + +from bluemira.codes.utilities import Model + + +class Objective(Model): + """ + Enum for PROCESS optimisation objective + """ + + MAJOR_RADIUS = 1 + # 2 NOT USED + NEUTRON_WALL_LOAD = 3 + MAX_COIL_POWER = 4 + FUSION_GAIN = 5 + ELECTRICITY_COST = 6 + CAPITAL_COST = 7 + ASPECT_RATIO = 8 + DIVERTOR_HEAT_LOAD = 9 + TOROIDAL_FIELD = 10 + INJECTED_POWER = 11 + # 12, 13 NOT USED + PULSE_LENGTH = 14 + AVAILABILITY = 15 + MAJOR_RADIUS_PULSE_LENGTH = 16 + NET_ELECTRICITY = 17 + NULL = 18 + FUSION_GAIN_PULSE_LENGTH = 19 + + +OBJECTIVE_MIN_ONLY = (16, 19) + + +@dataclass +class ConstraintSelection: + """ + Mixin dataclass for a Constraint selection in PROCESSModel + + Parameters + ---------- + _value_: + Integer value of the constraint + requires_variables: + List of required iteration variables for the constraint + requires_values: + List of required inputs for the constraint + description: + Short description of the model constraint + """ + + _value_: int + requires_variables: Tuple[int] = field(default_factory=tuple) + requires_values: Tuple[str] = field(default_factory=tuple) + description: str = "" + + +class Constraint(ConstraintSelection, Model): + """ + Enum for PROCESS constraints + """ + + BETA_CONSISTENCY = 1, (5,), (), "Beta consistency" + GLOBAL_POWER_CONSISTENCY = ( + 2, + (1, 2, 3, 4, 6, 10, 11), + (), + "Global Power Balance Consistency", + ) + ION_POWER_CONSISTENCY = ( + 3, + (1, 2, 3, 4, 6, 10, 11), + (), + "DEPRECATED - Ion Power Balance Consistency", + ) + ELECTRON_POWER_CONSISTENCY = ( + 4, + (1, 2, 3, 4, 6, 10, 11), + (), + "DEPRECATED - Electron Power Balance Consistency", + ) + DENSITY_UPPER_LIMIT = ( + 5, + (1, 2, 3, 4, 6, 9), + (), + "Density Upper Limit (Greenwald)", + ) + EPS_BETA_POL_UPPER_LIMIT = ( + 6, + (1, 2, 3, 4, 6, 8), + ("epbetmax",), + "Equation for epsilon beta-poloidal upper limit", + ) + HOT_BEAM_ION_DENSITY = 7, (7,), (), "Equation for hot beam ion density" + NWL_UPPER_LIMIT = ( + 8, + (1, 2, 3, 4, 6, 14), + ("walalw",), + "Neutron wall load upper limit", + ) + FUSION_POWER_UPPER_LIMIT = ( + 9, + (1, 2, 3, 4, 6, 26), + ("powfmax",), + "Equation for fusion power upper limit", + ) + # 10 NOT USED + RADIAL_BUILD_CONSISTENCY = ( + 11, + (1, 3, 13, 16, 29, 42, 61), + (), + "Radial Build Consistency", + ) + VS_LOWER_LIMIT = ( + 12, + (1, 2, 3, 15), + (), + "Equation for volt-second capability lower limit", + ) + BURN_TIME_LOWER_LIMIT = ( + 13, + (1, 2, 3, 6, 16, 17, 19, 29, 42, 44, 61), + (), + "Burn time lower limit", + ) + NBI_LAMBDA_CENTRE = ( + 14, + (), + (), + "Equation to fix number of NBI decay lengths to plasma centre", + ) + LH_THRESHHOLD_LIMIT = 15, (103,), (), "L-H Power ThresHhold Limit" + NET_ELEC_LOWER_LIMIT = ( + 16, + (1, 2, 3, 25), + ("pnetelin",), + "Net electric power lower limit", + ) + RAD_POWER_UPPER_LIMIT = 17, (28,), (), "Equation for radiation power upper limit" + DIVERTOR_HEAT_UPPER_LIMIT = ( + 18, + (27), + (), + "Equation for divertor heat load upper limit", + ) + MVA_UPPER_LIMIT = 19, (30,), ("mvalim",), "Equation for MVA upper limit" + NBI_TANGENCY_UPPER_LIMIT = ( + 20, + (3, 13, 31, 33), + (), + "Equation for neutral beam tangency radius upper limit", + ) + AMINOR_LOWER_LIMIT = 21, (32,), (), "Equation for minor radius lower limit" + DIV_COLL_CONN_UPPER_LIMIT = ( + 22, + (34,), + (), + "Equation for divertor collision/connection length ratio upper limit", + ) + COND_SHELL_R_RATIO_UPPER_LIMIT = ( + 23, + (1, 74, 104), + ("cwrmax",), + "Equation for conducting shell radius / rminor upper limit", + ) + BETA_UPPER_LIMIT = 24, (1, 2, 3, 4, 6, 18, 36), (), "Beta Upper Limit" + PEAK_TF_UPPER_LIMIT = ( + 25, + (3, 13, 29, 35), + ("bmxlim",), + "Peak toroidal field upper limit", + ) + CS_EOF_DENSITY_LIMIT = ( + 26, + (37, 38, 41), + (), + "Central solenoid EOF current density upper limit", + ) + CS_BOP_DENSITY_LIMIT = ( + 27, + (37, 38, 41), + (), + "Central solenoid bop current density upper limit", + ) + Q_LOWER_LIMIT = ( + 28, + (40, 45, 47), + ("bigqmin",), + "Equation for fusion gain (big Q) lower limit", + ) + IB_RADIAL_BUILD_CONSISTENCY = ( + 29, + (1, 3, 13, 16, 29, 42, 61), + (), + "Equation for minor radius lower limit OR Inboard radial build consistency", + ) + PINJ_UPPER_LIMIT = 30, (11, 46, 47), ("pinjalw",), "Injection Power Upper Limit" + TF_CASE_STRESS_UPPER_LIMIT = ( + 31, + (48, 56, 57, 58, 59, 60), + ("sig_tf_case_max",), + "TF coil case stress upper limit", + ) + TF_JACKET_STRESS_UPPER_LIMIT = ( + 32, + (49, 56, 57, 58, 59, 60), + ("sig_tf_wp_max",), + "TF WP steel jacket/conduit stress upper limit", + ) + TF_JCRIT_RATIO_UPPER_LIMIT = ( + 33, + (50, 56, 57, 58, 59, 60), + (), + "TF superconductor operating current / critical current density", + ) + TF_DUMP_VOLTAGE_UPPER_LIMIT = ( + 34, + (51, 52, 56, 57, 58, 59, 60), + ("vdalw",), + "TF dump voltage upper limit", + ) + TF_CURRENT_DENSITY_UPPER_LIMIT = ( + 35, + (53, 56, 57, 58, 59, 60), + (), + "TF winding pack current density upper limit", + ) + TF_T_MARGIN_LOWER_LIMIT = ( + 36, + (54, 56, 57, 58, 59, 60), + ("tftmp",), + "TF temperature margin upper limit", + ) + CD_GAMMA_UPPER_LIMIT = ( + 37, + (40, 47), + ("gammax",), + "Equation for current drive gamma upper limit", + ) + # 38 NOT USED + FW_TEMP_UPPER_LIMIT = 39, (63,), (), "First wall peak temperature upper limit" + PAUX_LOWER_LIMIT = ( + 40, + (64,), + ("auxmin",), + "Start-up injection power upper limit (PULSE)", + ) + IP_RAMP_LOWER_LIMIT = ( + 41, + (65, 66), + ("tohsmn",), + "Plasma ramp-up time lower limit (PULSE)", + ) + CYCLE_TIME_LOWER_LIMIT = ( + 42, + (17, 65, 67), + ("tcycmn",), + "Cycle time lower limit (PULSE)", + ) + CENTREPOST_TEMP_AVERAGE = ( + 43, + (13, 20, 69, 70), + (), + "Average centrepost temperature (TART) consistency equation", + ) + CENTREPOST_TEMP_UPPER_LIMIT = ( + 44, + (68, 69, 70), + ("ptempalw",), + "Peak centrepost temperature upper limit (TART)", + ) + QEDGE_LOWER_LIMIT = 45, (1, 2, 3, 70), (), "Edge safety factor lower limit (TART)" + IP_IROD_UPPER_LIMIT = 46, (2, 60, 72), (), "Equation for Ip/Irod upper limit (TART)" + # 47 NOT USED (or maybe it is, WTF?!) + BETAPOL_UPPER_LIMIT = 48, (2, 3, 18, 79), ("betpmax",), "Poloidal beta upper limit" + # 49 NOT USED + REP_RATE_UPPER_LIMIT = 50, (86,), (), "IFE repetition rate upper limit (IFE)" + CS_FLUX_CONSISTENCY = ( + 51, + (1, 3, 16, 29), + (), + "Startup volt-seconds consistency (PULSE)", + ) + TBR_LOWER_LIMIT = 52, (89, 90, 91), ("tbrmin",), "Tritium breeding ratio lower limit" + NFLUENCE_TF_UPPER_LIMIT = ( + 53, + (92, 93, 94), + ("nflutfmax",), + "Neutron fluence on TF coil upper limit", + ) + PNUCL_TF_UPPER_LIMIT = ( + 54, + (93, 94, 95), + ("ptfnucmax",), + "Peak TF coil nuclear heating upper limit", + ) + HE_VV_UPPER_LIMIT = ( + 55, + (93, 94, 96), + ("vvhealw",), + "Vacuum vessel helium concentration upper limit iblanket=2", + ) + PSEPR_UPPER_LIMIT = ( + 56, + (1, 3, 97, 102), + ("pseprmax",), + "Pseparatrix/Rmajor upper limit", + ) + # 57, 58 NOT USED + NBI_SHINETHROUGH_UPPER_LIMIT = ( + 59, + (4, 6, 19, 105), + ("nbshinefmax",), + "Neutral beam shinethrough fraction upper limit (NBI)", + ) + CS_T_MARGIN_LOWER_LIMIT = ( + 60, + (106,), + (), + "Central solenoid temperature margin lower limit (SCTF)[sic.." + " I guess they mean SCCS]", + ) + AVAIL_LOWER_LIMIT = 61, (107,), ("avail_min",), "Minimum availability value" + CONFINEMENT_RATIO_LOWER_LIMIT = ( + 62, + (110,), + ("taulimit",), + "taup/taueff the ratio of particle to energy confinement times", + ) + NITERPUMP_UPPER_LIMIT = ( + 63, + (111,), + (), + "The number of ITER-like vacuum pumps niterpump < tfno", + ) + ZEFF_UPPER_LIMIT = 64, (112,), ("zeffmax",), "Zeff less than or equal to zeffmax" + DUMP_TIME_LOWER_LIMIT = ( + 65, + (56, 113), + ("max_vv_stress",), + "Dump time set by VV loads", + ) + PF_ENERGY_RATE_UPPER_LIMIT = ( + 66, + (65, 113), + ("tohs",), + "Limit on rate of change of energy in poloidal field", + ) + WALL_RADIATION_UPPER_LIMIT = ( + 67, + (4, 6, 102, 116), + ("peakfactrad", "peakradwallload"), + "Simple radiation wall load limit", + ) + PSEPB_QAR_UPPER_LIMIT = ( + 68, + (117,), + ("psepbqarmax",), + "P_separatrix Bt / q A R upper limit", + ) + PSEP_KALLENBACH_UPPER_LIMIT = ( + 69, + (118,), + (), + "ensure the separatrix power = the value from Kallenbach divertor", + ) + TSEP_CONSISTENCY = ( + 70, + (119,), + (), + "ensure that temp = separatrix in the pedestal profile", + ) + NSEP_CONSISTENCY = ( + 71, + (), + (), + "ensure that neomp = separatrix density (nesep) x neratio", + ) + CS_STRESS_UPPER_LIMIT = ( + 72, + (123,), + (), + "Central solenoid shear stress limit (Tresca yield criterion)", + ) + PSEP_LH_AUX_CONSISTENCY = 73, (137,), (), "Psep >= Plh + Paux" + TF_CROCO_T_UPPER_LIMIT = 74, (141,), ("tmax_croco",), "TFC quench" + TF_CROCO_CU_AREA_CONSTRAINT = ( + 75, + (143,), + ("coppera_m2_max",), + "TFC current / copper area < maximum", + ) + EICH_SEP_DENSITY_CONSTRAINT = 76, (144,), (), "Eich critical separatrix density" + TF_TURN_CURRENT_UPPER_LIMIT = ( + 77, + (146,), + ("cpttf_max",), + "TF coil current per turn upper limit", + ) + REINKE_IMP_FRAC_LOWER_LIMIT = ( + 78, + (147,), + (), + "Reinke criterion impurity fraction lower limit", + ) + BMAX_CS_UPPER_LIMIT = 79, (149,), ("bmaxcs_lim",), "Peak CS field upper limit" + PDIVT_LOWER_LIMIT = 80, (153,), ("pdivtlim",), "Divertor power lower limit" + DENSITY_PROFILE_CONSISTENCY = 81, (154,), (), "Ne(0) > ne(ped) constraint" + STELLARATOR_COIL_CONSISTENCY = ( + 82, + (171,), + ("toroidalgap",), + ) + STELLARATOR_RADIAL_BUILD_CONSISTENCY = ( + 83, + (172,), + (), + "Radial build consistency for stellarators", + ) + BETA_LOWER_LIMIT = 84, (173,), (), "Lower limit for beta" + CP_LIFETIME_LOWER_LIMIT = ( + 85, + (), + ("nflutfmax",), + "Constraint for centrepost lifetime", + ) + TURN_SIZE_UPPER_LIMIT = ( + 86, + (), + ("t_turn_tf_max",), + "Constraint for TF coil turn dimension", + ) + CRYOPOWER_UPPER_LIMIT = 87, (), (), "Constraint for cryogenic power" + TF_STRAIN_UPPER_LIMIT = ( + 88, + (), + ("str_wp_max",), + "Constraint for TF coil strain absolute value", + ) + OH_CROCO_CU_AREA_CONSTRAINT = ( + 89, + (166,), + ("copperaoh_m2_max",), + "Constraint for CS coil quench protection", + ) + CS_FATIGUE = ( + 90, + (167,), + ( + "residual_sig_hoop", + "n_cycle_min", + "t_crack_radial", + "t_crack_vertical", + "t_structural_radial", + "t_structural_vertical", + "sf_vertical_crack", + "sf_radial_crack", + "sf_fast_fracture", + "paris_coefficient", + "paris_power_law", + "walker_coefficient", + "fracture_toughness", + ), + "CS fatigue constraints", + ) + ECRH_IGNITABILITY = 91, (168,), (), "Checking if the design point is ECRH ignitable" + + +# The dreaded f-values +FV_CONSTRAINT_ITVAR_MAPPING = { + 5: 9, + 6: 8, + 8: 14, + 9: 26, + 12: 15, + 13: 21, + 15: 103, + 16: 25, + 17: 28, + 18: 27, + 19: 30, + 20: 33, + 21: 32, + 22: 34, + 23: 104, + 24: 36, + 25: 35, + 26: 38, + 27: 39, + 28: 45, + # 30: 46, # Keep this as an equality constraint by default + 31: 48, + 32: 49, + 33: 50, + 34: 51, + 35: 53, + 36: 54, + 37: 40, + 38: 62, + 39: 63, + 40: 64, + 41: 66, + 42: 67, + 44: 68, + 45: 71, + 46: 72, + 48: 79, + 50: 86, + 52: 89, + 53: 92, + 54: 95, + 55: 96, + 56: 97, + 59: 105, + 60: 106, + 61: 107, + 62: 110, + 63: 111, + 64: 112, + 65: 113, + 66: 115, + 67: 116, + 68: 117, + 69: 118, + 72: 123, + 73: 137, + 74: 141, + 75: 143, + 76: 144, + 77: 146, + 78: 146, + 81: 154, + 83: 160, # OR 172?! + 84: 161, # OR 173?! + 89: 166, + 90: 167, + 91: 168, +} + +ITERATION_VAR_MAPPING = { + "aspect": 1, + "bt": 2, + "rmajor": 3, + "te": 4, + "beta": 5, + "dene": 6, + "rnbeam": 7, + "fbeta": 8, + "fdene": 9, + "hfact": 10, + "pheat": 11, + # NO LONGER USED "oacdp": 12, + "tfcth": 13, + "fwalld": 14, + "fvs": 15, + "ohcth": 16, + "tdwell": 17, + "q": 18, + "enbeam": 19, + "tcpav": 20, + "ftburn": 21, + # 22 NOT USED + "fcoolcp": 23, + # 24 NOT USED + "fpnetel": 25, + "ffuspow": 26, + "fhldiv": 27, + "fradpwr": 28, + "bore": 29, + "fmva": 30, + "gapomin": 31, + "frminor": 32, + "fportsz": 33, + "fdivcol": 34, + "fpeakb": 35, + "fbetatry": 36, + "coheof": 37, + "fjohc": 38, + "fjohc0": 39, + "fgamcd": 40, + "fcohbop": 41, + "gapoh": 42, + # 43 NOT USED + "fvsbrnni": 44, + "fqval": 45, + "fpinj": 46, + "feffcd": 47, + "fstrcase": 48, + "fstrcond": 49, + "fiooic": 50, + "fvdump": 51, + "vdalw": 52, + "fjprot": 53, + "ftmargtf": 54, + # 55 NOT USED + "tdmptf": 56, + "thkcas": 57, + "thwcndut": 58, + "fcutfsu": 59, + "cpttf": 60, + "gapds": 61, + "fdtmp": 62, + "ftpeak": 63, + "fauxmn": 64, + "tohs": 65, + "ftohs": 66, + "ftcycl": 67, + "fptemp": 68, + "rcool": 69, + "vcool": 70, + "fq": 71, + "fipir": 72, + "scrapli": 73, + "scraplo": 74, + "tfootfi": 75, + # 76, 77, 78 NOT USED + "fbetap": 79, + # 80 NOT USED + "edrive": 81, + "drveff": 82, + "tgain": 83, + "chrad": 84, + "pdrive": 85, + "frrmax": 86, + # 87, 88 NOT USED + "ftbr": 89, + "blbuith": 90, + "blbuoth": 91, + "fflutf": 92, + "shldith": 93, + "shldoth": 94, + "fptfnuc": 95, + "fvvhe": 96, + "fpsepr": 97, + "li6enrich": 98, + # 99, 100, 101 NOT USED + "fimpvar": 102, + "flhthresh": 103, + "fcwr": 104, + "fnbshinef": 105, + "ftmargoh": 106, + "favail": 107, + "breeder_f": 108, + "ralpne": 109, + "ftaulimit": 110, + "fniterpump": 111, + "fzeffmax": 112, + "fmaxvvstress": 113, # OR IS IT fmaxvvstress ?! ftaucq + "fw_channel_length": 114, + "fpoloidalpower": 115, + "fradwall": 116, + "fpsepbqar": 117, + "fpsep": 118, + "tesep": 119, + "ttarget": 120, + "neratio": 121, + "oh_steel_frac": 122, + "foh_stress": 123, + "qtargettotal": 124, + "fimp(3)": 125, # Beryllium + "fimp(4)": 126, # Carbon + "fimp(5)": 127, # Nitrogen + "fimp(6)": 128, # Oxygen + "fimp(7)": 129, # Neon + "fimp(8)": 130, # Silicon + "fimp(9)": 131, # Argon + "fimp(10)": 132, # Iron + "fimp(11)": 133, # Nickel + "fimp(12)": 134, # Krypton + "fimp(13)": 135, # Xenon + "fimp(14)": 136, # Tungsten + "fplhsep": 137, + "rebco_thickness": 138, + "copper_thick": 139, + "dr_tf_wp": 140, # TODO: WTF + "fcqt": 141, + "nesep": 142, + "f_coppera_m2": 143, + "fnesep": 144, + "fgwped": 145, + "fcpttf": 146, + "freinke": 147, + "fzactual": 148, + "fbmaxcs": 149, + # 150, 151 NOT USED + "fgwsep": 152, + "fpdivlim": 153, + "fne0": 154, + "pfusife": 155, + "rrin": 156, + "fvssu": 157, + "croco_thick": 158, + "ftoroidalgap": 159, + "f_avspace": 160, + "fbetatry_lower": 161, + "r_cp_top": 162, + "f_t_turn_tf": 163, + "f_crypmw": 164, + "fstr_wp": 165, + "f_copperaoh_m2": 166, + "fncycle": 167, + "fecrh_ignition": 168, + "te0_ecrh_achievable": 169, + "beta_div": 170, +} + + +VAR_ITERATION_MAPPING = {v: k for k, v in ITERATION_VAR_MAPPING.items()} diff --git a/bluemira/codes/process/_inputs.py b/bluemira/codes/process/_inputs.py index f46ec00f89..3013941da4 100644 --- a/bluemira/codes/process/_inputs.py +++ b/bluemira/codes/process/_inputs.py @@ -22,8 +22,8 @@ Parameter classes/structures for Process """ -from dataclasses import dataclass, field, fields -from typing import Dict, Generator, List, Tuple, Union +from dataclasses import dataclass, fields +from typing import Dict, Generator, List, Optional, Tuple, Union from bluemira.codes.process.api import _INVariable @@ -42,230 +42,670 @@ class ProcessInputs: `process.io.python_fortran_dicts.get_dicts()["DICT_DESCRIPTIONS"]` """ - bounds: Dict[str, Dict[str, str]] = field( - default_factory=lambda: { - "2": {"u": "20.0"}, - "3": {"u": "13"}, - "4": {"u": "150.0"}, - "9": {"u": "1.2"}, - "18": {"l": "3.5"}, - "29": {"l": "0.1"}, - "38": {"u": "1.0"}, - "39": {"u": "1.0"}, - "42": {"l": "0.05", "u": "0.1"}, - "50": {"u": "1.0"}, - "52": {"u": "10.0"}, - "61": {"l": "0.02"}, - "103": {"u": "10.0"}, - "60": {"l": "6.0e4", "u": "9.0e4"}, - "59": {"l": "0.50", "u": "0.94"}, - } - ) - # fmt: off - icc: List[int] = field(default_factory=lambda: [1, 2, 5, 8, 11, 13, 15, 16, 24, 25, - 26, 27, 30, 31, 32, 33, 34, 35, 36, - 60, 62, 65, 68, 72]) - ixc: List[int] = field(default_factory=lambda: [2, 3, 4, 5, 6, 9, 13, 14, 16, 18, - 29, 36, 37, 38, 39, 41, 42, 44, 48, - 49, 50, 51, 52, 53, 54, 56, 57, 58, - 59, 60, 61, 102, 103, 106, 109, 110, - 113, 117, 122, 123]) - # fmt: on - abktflnc: float = 15.0 - adivflnc: float = 20.0 - alphan: float = 1.0 - alphat: float = 1.45 - alstroh: float = 660000000.0 - aspect: float = 3.1 - beta: float = 0.031421 - blnkith: float = 0.755 - blnkoth: float = 0.982 - bmxlim: float = 11.2 - bore: float = 2.3322 - bscfmax: float = 0.99 - bt: float = 5.3292 - casths: float = 0.05 - cfactr: float = 0.75 - coheof: float = 20726000.0 - coreradiationfraction: float = 0.6 - coreradius: float = 0.75 - cost_model: int = 0 - cptdin: List[float] = field( - default_factory=lambda: [*([42200.0] * 4), *([43000.0] * 4)] - ) - cpttf: float = 65000.0 - d_vv_bot: float = 0.6 - d_vv_in: float = 0.6 - d_vv_out: float = 1.1 - d_vv_top: float = 0.6 - ddwex: float = 0.15 - dene: float = 7.4321e19 - dhecoil: float = 0.01 - dintrt: float = 0.0 - discount_rate: float = 0.06 - divdum: int = 1 - divfix: float = 0.621 - dnbeta: float = 3.0 - dr_tf_case_in: float = 0.52465 - dr_tf_case_out: float = 0.06 - emult: float = 1.35 - enbeam: float = 1e3 - epsvmc: float = 1e-08 - etaech: float = 0.4 - etahtp: float = 0.87 - etaiso: float = 0.9 - etanbi: float = 0.3 - etath: float = 0.375 - fbetatry: float = 0.48251 - fcap0: float = 1.15 - fcap0cp: float = 1.06 - fcohbop: float = 0.93176 - fcontng: float = 0.15 - fcr0: float = 0.065 - fcuohsu: float = 0.7 - fcutfsu: float = 0.80884 - fdene: float = 1.2 - ffuspow: float = 1.0 - fgwped: float = 0.85 - fimp: List[float] = field( - default_factory=lambda: [1.0, 0.1, *([0.0] * 10), 0.00044, 5e-05] - ) - fimpvar: float = 0.00037786 - fiooic: float = 0.63437 - fjohc0: float = 0.53923 - fjohc: float = 0.57941 - fjprot: float = 1.0 - fkind: float = 1.0 - fkzohm: float = 1.0245 - flhthresh: float = 1.4972 - foh_stress: float = 1.0 - fpeakb: float = 1.0 - fpinj: float = 1.0 - fpnetel: float = 1.0 - fpsepbqar: float = 1.0 - fstrcase: float = 1.0 - fstrcond: float = 0.92007 - ftaucq: float = 0.91874 - ftaulimit: float = 1.0 - ftburn: float = 1.0 - ftmargoh: float = 1.0 - ftmargtf: float = 1.0 - fvdump: float = 1.0 - fvsbrnni: float = 0.39566 - fwalld: float = 0.131 - gamma: float = 0.3 - gamma_ecrh: float = 0.3 - gapds: float = 0.02 - gapoh: float = 0.05 - gapomin: float = 0.2 - hfact: float = 1.1 - hldivlim: float = 10.0 - i_single_null: int = 1 - i_tf_sc_mat: int = 5 - i_tf_turns_integer: int = 1 - iavail: int = 0 - ibss: int = 4 - iculbl: int = 1 - icurr: int = 4 - idensl: int = 7 - iefrf: int = 10 - ieped: int = 1 - ifalphap: int = 1 - ifispact: int = 0 - ifueltyp: int = 1 - iinvqd: int = 1 - impvar: int = 13 - inuclear: int = 1 - iohcl: int = 1 - ioptimz: int = 1 - ipedestal: int = 1 - ipfloc: List[int] = field(default_factory=lambda: [2, 2, 3, 3]) - ipowerflow: int = 0 - iprimshld: int = 1 - iprofile: int = 1 - isc: int = 34 - ishape: int = 0 - isumatoh: int = 5 - isumatpf: int = 3 - kappa: float = 1.848 - ksic: float = 1.4 - lpulse: int = 1 - lsa: int = 2 - minmax: int = 1 - n_layer: int = 10 - n_pancake: int = 20 - n_tf: int = 16 - ncls: List[int] = field(default_factory=lambda: [1, 1, 2, 2]) - neped: float = 6.78e19 - nesep: float = 2e19 - ngrp: int = 4 - oacdcp: float = 8673900.0 - oh_steel_frac: float = 0.57875 - ohcth: float = 0.55242 - ohhghf: float = 0.9 - output_costs: int = 0 - pheat: float = 50.0 - pinjalw: float = 51.0 - plasma_res_factor: float = 0.66 - pnetelin: float = 500.0 - primary_pumping: int = 3 - prn1: float = 0.4 - psepbqarmax: float = 9.2 - pulsetimings: float = 0.0 - q0: float = 1.0 - q: float = 3.5 - qnuc: float = 12920.0 - ralpne: float = 0.06894 - rhopedn: float = 0.94 - rhopedt: float = 0.94 - ripmax: float = 0.6 - rjconpf: List[float] = field( - default_factory=lambda: [1.1e7, 1.1e7, 6e6, 6e6, 8e6, 8e6, 8e6, 8e6] - ) - rmajor: float = 8.8901 - rpf2: float = -1.825 - scrapli: float = 0.225 - scraplo: float = 0.225 - secondary_cycle: int = 2 - shldith: float = 1e-06 - shldlth: float = 1e-06 - shldoth: float = 1e-06 - shldtth: float = 1e-06 - sig_tf_case_max: float = 580000000.0 - sig_tf_wp_max: float = 580000000.0 - ssync: float = 0.6 - tbeta: float = 2.0 - tbrnmn: float = 7200.0 - tburn: float = 10000.0 - tdmptf: float = 25.829 - tdwell: float = 0.0 - te: float = 12.33 - teped: float = 5.5 - tesep: float = 0.1 - tfcth: float = 1.208 - tftmp: float = 4.75 - tftsgap: float = 0.05 - thicndut: float = 0.002 - thshield: float = 0 - thwcndut: float = 0.008 - tinstf: float = 0.008 - tlife: float = 40.0 - tmargmin: float = 1.5 - tramp: float = 500.0 - triang: float = 0.5 - ucblvd: float = 280.0 - ucdiv: float = 500000.0 - ucme: float = 300000000.0 - vdalw: float = 10.0 - vfshld: float = 0.6 - vftf: float = 0.3 - vgap2: float = 0.05 - vvblgap: float = 0.02 - walalw: float = 8.0 - zeffdiv: float = 3.5 - zref: List[float] = field( - default_factory=lambda: [3.6, 1.2, 1.0, 2.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] - ) + runtitle: Optional[str] = None + + # Optimisation problem setup + bounds: Optional[Dict[str, Dict[str, str]]] = None + icc: Optional[List[int]] = None + ixc: Optional[List[int]] = None + + # Settings + maxcal: Optional[int] = None + minmax: Optional[int] = None + epsvmc: Optional[float] = None + ioptimz: Optional[int] = None + output_costs: Optional[int] = None + isweep: Optional[int] = None + nsweep: Optional[int] = None + sweep: List[float] = None + pulsetimings: Optional[int] = None + # Top down of PROCESS variables list + + # Times + tburn: Optional[float] = None + tdwell: Optional[float] = None + theat: Optional[float] = None + tohs: Optional[float] = None + tqnch: Optional[float] = None + tramp: Optional[float] = None + + # FWBS + ibkt_life: Optional[int] = None + denstl: Optional[float] = None + denw: Optional[float] = None + emult: Optional[float] = None + fblss: Optional[float] = None + fdiv: Optional[float] = None + fwbsshape: Optional[int] = None + fw_armour_thickness: Optional[float] = None + iblanket: Optional[int] = None + iblnkith: Optional[int] = None + li6enrich: Optional[float] = None + breeder_f: Optional[float] = None + breeder_multiplier: Optional[float] = None + vfcblkt: Optional[float] = None + vfpblkt: Optional[float] = None + blktmodel: Optional[int] = None # Listed as an output... + # f_neut_shield: float = # -1.0 the documentation defaults cannot be right... + breedmat: Optional[int] = None + fblbe: Optional[float] = None + fblbreed: Optional[float] = None + fblhebmi: Optional[float] = None + fblhebmo: Optional[float] = None + fblhebpi: Optional[float] = None + fblhebpo: Optional[float] = None + hcdportsize: Optional[int] = None + npdiv: Optional[int] = None + nphcdin: Optional[int] = None + nphcdout: Optional[int] = None + wallpf: Optional[float] = None + iblanket_thickness: Optional[int] = None + secondary_cycle: Optional[int] = None # Listed as an output... + secondary_cycle_liq: Optional[int] = None + afwi: Optional[float] = None + afwo: Optional[float] = None + fw_wall: Optional[float] = None + afw: Optional[float] = None + pitch: Optional[float] = None + fwinlet: Optional[float] = None + fwoutlet: Optional[float] = None + fwpressure: Optional[float] = None + roughness: Optional[float] = None + fw_channel_length: Optional[float] = None + peaking_factor: Optional[float] = None + blpressure: Optional[float] = None + inlet_temp: Optional[float] = None + outlet_temp: Optional[float] = None + coolp: Optional[float] = None + nblktmodpo: Optional[int] = None + nblktmodpi: Optional[int] = None + nblktmodto: Optional[int] = None + nblktmodti: Optional[int] = None + tfwmatmax: Optional[float] = None + fw_th_conductivity: Optional[float] = None + fvoldw: Optional[float] = None + fvolsi: Optional[float] = None + fvolso: Optional[float] = None + fwclfr: Optional[float] = None + rpf2dewar: Optional[float] = None + vfshld: Optional[float] = None + irefprop: Optional[int] = None + fblli2o: Optional[float] = None + fbllipb: Optional[float] = None + vfblkt: Optional[float] = None + declblkt: Optional[float] = None + declfw: Optional[float] = None + declshld: Optional[float] = None + blkttype: Optional[int] = None + etaiso: Optional[float] = None + etahtp: Optional[float] = None + n_liq_recirc: Optional[int] = None + bz_channel_conduct_liq: Optional[float] = None + blpressure_liq: Optional[float] = None + inlet_temp_liq: Optional[float] = None + outlet_temp_liq: Optional[float] = None + f_nuc_pow_bz_struct: Optional[float] = None + pnuc_fw_ratio_dcll: Optional[float] = None + + # TF coil + sig_tf_case_max: Optional[float] = None + sig_tf_wp_max: Optional[float] = None + bcritsc: Optional[float] = None + casthi_fraction: Optional[float] = None + casths_fraction: Optional[float] = None + f_t_turn_tf: Optional[float] = None + t_turn_tf_max: Optional[float] = None + cpttf: Optional[float] = None + cpttf_max: Optional[float] = None + dcase: Optional[float] = None + dcond: List[float] = None + dcondins: Optional[float] = None + dhecoil: Optional[float] = None + farc4tf: Optional[float] = None + b_crit_upper_nbti: Optional[float] = None + t_crit_nbti: Optional[float] = None + fcutfsu: Optional[float] = None + fhts: Optional[float] = None + i_tf_stress_model: Optional[int] = None + i_tf_wp_geom: Optional[int] = None + i_tf_case_geom: Optional[int] = None # Listed as an output + i_tf_turns_integer: Optional[int] = None # Listed as an output + i_tf_sc_mat: Optional[int] = None + i_tf_sup: Optional[int] = None + i_tf_shape: Optional[int] = None # Listed as an output + i_tf_cond_eyoung_trans: Optional[int] = None + n_pancake: Optional[int] = None + n_layer: Optional[int] = None + n_rad_per_layer: Optional[int] = None + i_tf_bucking: Optional[int] = None + n_tf_graded_layers: Optional[int] = None + jbus: Optional[float] = None + eyoung_ins: Optional[float] = None + eyoung_steel: Optional[float] = None + eyong_cond_axial: Optional[float] = None + eyoung_res_tf_buck: Optional[float] = None + # eyoung_al: Optional[float] = 69000000000.0 # defaults cannot be right + poisson_steel: Optional[float] = None + poisson_copper: Optional[float] = None + poisson_al: Optional[float] = None + str_cs_con_res: Optional[float] = None + str_pf_con_res: Optional[float] = None + str_tf_con_res: Optional[float] = None + str_wp_max: Optional[float] = None + i_str_wp: Optional[int] = None + quench_model: str = None + tcritsc: Optional[float] = None + tdmptf: Optional[float] = None + tfinsgap: Optional[float] = None + # rhotfbus: Optional[float] = -1.0 # defaults cannot be right + frhocp: Optional[float] = None + frholeg: Optional[float] = None + # i_cp_joints: Optional[int] = -1 # defaults cannot be right + rho_tf_joints: Optional[float] = None + n_tf_joints_contact: Optional[int] = None + n_tf_joints: Optional[int] = None + th_joint_contact: Optional[float] = None + # eff_tf_cryo: Optional[float] = -1.0 # defaults cannot be right + n_tf: Optional[int] = None + tftmp: Optional[float] = None + thicndut: Optional[float] = None + thkcas: Optional[float] = None + thwcndut: Optional[float] = None + tinstf: Optional[float] = None + tmaxpro: Optional[float] = None + tmax_croco: Optional[float] = None + tmpcry: Optional[float] = None + vdalw: Optional[float] = None + f_vforce_inboard: Optional[float] = None + vftf: Optional[float] = None + etapump: Optional[float] = None + fcoolcp: Optional[float] = None + fcoolleg: Optional[float] = None + ptempalw: Optional[float] = None + rcool: Optional[float] = None + tcoolin: Optional[float] = None + tcpav: Optional[float] = None + vcool: Optional[float] = None + theta1_coil: Optional[float] = None + theta1_vv: Optional[float] = None + max_vv_stress: Optional[float] = None + inuclear: Optional[int] = None + qnuc: Optional[float] = None + ripmax: Optional[float] = None + tf_in_cs: Optional[int] = None + tfcth: Optional[float] = None + tftsgap: Optional[float] = None + casthi: Optional[float] = None + casths: Optional[float] = None + tmargmin: Optional[float] = None + oacdcp: Optional[float] = None + + # PF Power + iscenr: Optional[int] = None + maxpoloidalpower: Optional[float] = None + + # Cost variables + abktflnc: Optional[float] = None + adivflnc: Optional[float] = None + cconfix: Optional[float] = None + cconshpf: Optional[float] = None + cconshtf: Optional[float] = None + cfactr: Optional[float] = None + cfind: List[float] = None + cland: Optional[float] = None + costexp: Optional[float] = None + costexp_pebbles: Optional[float] = None + cost_factor_buildings: Optional[float] = None + cost_factor_land: Optional[float] = None + cost_factor_tf_coils: Optional[float] = None + cost_factor_fwbs: Optional[float] = None + cost_factor_tf_rh: Optional[float] = None + cost_factor_tf_vv: Optional[float] = None + cost_factor_tf_bop: Optional[float] = None + cost_factor_tf_misc: Optional[float] = None + maintenance_fwbs: Optional[float] = None + maintenance_gen: Optional[float] = None + amortization: Optional[float] = None + cost_model: Optional[int] = None + cowner: Optional[float] = None + cplife_input: Optional[float] = None + cpstflnc: Optional[float] = None + csi: Optional[float] = None + # cturbb: Optional[float] = 38.0 # defaults cannot be right + decomf: Optional[float] = None + dintrt: Optional[float] = None + fcap0: Optional[float] = None + fcap0cp: Optional[float] = None + fcdfuel: Optional[float] = None + fcontng: Optional[float] = None + fcr0: Optional[float] = None + fkind: Optional[float] = None + iavail: Optional[int] = None + life_dpa: Optional[float] = None + avail_min: Optional[float] = None + favail: Optional[float] = None + num_rh_systems: Optional[int] = None + conf_mag: Optional[float] = None + div_prob_fail: Optional[float] = None + div_umain_time: Optional[float] = None + div_nref: Optional[float] = None + div_nu: Optional[float] = None + fwbs_nref: Optional[float] = None + fwbs_nu: Optional[float] = None + fwbs_prob_fail: Optional[float] = None + fwbs_umain_time: Optional[float] = None + redun_vacp: Optional[float] = None + tbktrepl: Optional[float] = None + tcomrepl: Optional[float] = None + tdivrepl: Optional[float] = None + uubop: Optional[float] = None + uucd: Optional[float] = None + uudiv: Optional[float] = None + uufuel: Optional[float] = None + uufw: Optional[float] = None + uumag: Optional[float] = None + uuves: Optional[float] = None + ifueltyp: Optional[int] = None + ucblvd: Optional[float] = None + ucdiv: Optional[float] = None + ucme: Optional[float] = None + ireactor: Optional[int] = None + lsa: Optional[int] = None + discount_rate: Optional[float] = None + startupratio: Optional[float] = None + tlife: Optional[float] = None + bkt_life_csf: Optional[int] = None + # ... + + # CS fatigue + residual_sig_hoop: Optional[float] = None + n_cycle_min: Optional[int] = None + t_crack_vertical: Optional[float] = None + t_crack_radial: Optional[float] = None + t_structural_radial: Optional[float] = None + t_structural_vertical: Optional[float] = None + sf_vertical_crack: Optional[float] = None + sf_radial_crack: Optional[float] = None + sf_fast_fracture: Optional[float] = None + paris_coefficient: Optional[float] = None + paris_power_law: Optional[float] = None + walker_coefficient: Optional[float] = None + fracture_toughness: Optional[float] = None + + # REBCO + rebco_thickness: Optional[float] = None + copper_thick: Optional[float] = None + hastelloy_thickness: Optional[float] = None + tape_width: Optional[float] = None + tape_thickness: Optional[float] = None + croco_thick: Optional[float] = None + copper_rrr: Optional[float] = None + copper_m2_max: Optional[float] = None + f_coppera_m2: Optional[float] = None + copperaoh_m2_max: Optional[float] = None + f_copperaoh_m2: Optional[float] = None + + # Primary pumping + primary_pumping: Optional[int] = None + gamma_he: Optional[float] = None + t_in_bb: Optional[float] = None + t_out_bb: Optional[float] = None + p_he: Optional[float] = None + dp_he: Optional[float] = None + + # Constraint variables + auxmin: Optional[float] = None + betpmx: Optional[float] = None + bigqmin: Optional[float] = None + bmxlim: Optional[float] = None + fauxmn: Optional[float] = None + fbeta: Optional[float] = None + fbetap: Optional[float] = None + fbetatry: Optional[float] = None + fbetatry_lower: Optional[float] = None + fcwr: Optional[float] = None + fdene: Optional[float] = None + fdivcol: Optional[float] = None + fdtmp: Optional[float] = None + fecrh_ignition: Optional[float] = None + fflutf: Optional[float] = None + ffuspow: Optional[float] = None + fgamcd: Optional[float] = None + fhldiv: Optional[float] = None + fiooic: Optional[float] = None + fipir: Optional[float] = None + fjohc: Optional[float] = None + fjohc0: Optional[float] = None + fjprot: Optional[float] = None + flhthresh: Optional[float] = None + fmva: Optional[float] = None + fnbshinef: Optional[float] = None + fncycle: Optional[float] = None + fnesep: Optional[float] = None + foh_stress: Optional[float] = None + fpeakb: Optional[float] = None + fpinj: Optional[float] = None + fpnetel: Optional[float] = None + fportsz: Optional[float] = None + fpsepbqar: Optional[float] = None + fpsepr: Optional[float] = None + fptemp: Optional[float] = None + fq: Optional[float] = None + fqval: Optional[float] = None + fradwall: Optional[float] = None + freinke: Optional[float] = None + fstrcase: Optional[float] = None + fstrcond: Optional[float] = None + fstr_wp: Optional[float] = None + fmaxvvstress: Optional[float] = None + ftbr: Optional[float] = None + ftburn: Optional[float] = None + ftcycl: Optional[float] = None + ftmargoh: Optional[float] = None + ftmargtf: Optional[float] = None + ftohs: Optional[float] = None + ftpeak: Optional[float] = None + fvdump: Optional[float] = None + fvs: Optional[float] = None + fvvhe: Optional[float] = None + fwalld: Optional[float] = None + fzeffmax: Optional[float] = None + gammax: Optional[float] = None + maxradwallload: Optional[float] = None + mvalim: Optional[float] = None + nbshinefmax: Optional[float] = None + nflutfmax: Optional[float] = None + pdivtlim: Optional[float] = None + peakfactrad: Optional[float] = None + pnetelin: Optional[float] = None + powfmax: Optional[float] = None + psepbqarmax: Optional[float] = None + pseprmax: Optional[float] = None + ptfnucmax: Optional[float] = None + tbrmin: Optional[float] = None + tbrnmn: Optional[float] = None + vvhealw: Optional[float] = None + walalw: Optional[float] = None + taulimit: Optional[float] = None + ftaulimit: Optional[float] = None + fniterpump: Optional[float] = None + zeffmax: Optional[float] = None + fpoloidalpower: Optional[float] = None + fpsep: Optional[float] = None + fcqt: Optional[float] = None + + # Build variables + aplasmin: Optional[float] = None + blbmith: Optional[float] = None + blbmoth: Optional[float] = None + blbpith: Optional[float] = None + blbpoth: Optional[float] = None + blbuith: Optional[float] = None + blbuoth: Optional[float] = None + blnkith: Optional[float] = None + blnkoth: Optional[float] = None + bore: Optional[float] = None + clhsf: Optional[float] = None + ddwex: Optional[float] = None + d_vv_in: Optional[float] = None + d_vv_out: Optional[float] = None + d_vv_top: Optional[float] = None + d_vv_bot: Optional[float] = None + f_avspace: Optional[float] = None + fcspc: Optional[float] = None + fhole: Optional[float] = None + fseppc: Optional[float] = None + gapds: Optional[float] = None + gapoh: Optional[float] = None + gapomin: Optional[float] = None + iohcl: Optional[int] = None + iprecomp: Optional[int] = None + ohcth: Optional[float] = None + rinboard: Optional[float] = None + f_r_cp: Optional[float] = None + scrapli: Optional[float] = None + scraplo: Optional[float] = None + shldith: Optional[float] = None + shldlth: Optional[float] = None + shldoth: Optional[float] = None + shldtth: Optional[float] = None + sigallpc: Optional[float] = None + tfoofti: Optional[float] = None + thshield_ib: Optional[float] = None + thshield_ob: Optional[float] = None + thshield_vb: Optional[float] = None + vgap: Optional[float] = None + vgap2: Optional[float] = None + vgaptop: Optional[float] = None + vvblgap: Optional[float] = None + plleni: Optional[float] = None + plsepi: Optional[float] = None + plsepo: Optional[float] = None + + # Buildings + + # Current drive + beamwd: Optional[float] = None + bscfmax: Optional[float] = None + cboot: Optional[float] = None + harnum: Optional[float] = None + enbeam: Optional[float] = None + etaech: Optional[float] = None + etanbi: Optional[float] = None + feffcd: Optional[float] = None + frbeam: Optional[float] = None + ftritbm: Optional[float] = None + gamma_ecrh: Optional[float] = None + rho_ecrh: Optional[float] = None + xi_ebw: Optional[float] = None + iefrf: Optional[int] = None + irfcf: Optional[int] = None + nbshield: Optional[float] = None + pheat: Optional[float] = None # Listed as an output + pinjalw: Optional[float] = None + tbeamin: Optional[float] = None + + # Impurity radiation + coreradius: Optional[float] = None + coreradiationfraction: Optional[float] = None + fimp: List[float] = None + fimpvar: Optional[float] = None + impvar: Optional[int] = None + + # Reinke + impvardiv: Optional[int] = None + lhat: Optional[float] = None + fzactual: Optional[float] = None + + # Divertor + divdum: Optional[int] = None + anginc: Optional[float] = None + beta_div: Optional[float] = None + betai: Optional[float] = None + betao: Optional[float] = None + bpsout: Optional[float] = None + c1div: Optional[float] = None + c2div: Optional[float] = None + c3div: Optional[float] = None + c4div: Optional[float] = None + c5div: Optional[float] = None + delld: Optional[float] = None + divclfr: Optional[float] = None + divdens: Optional[float] = None + divfix: Optional[float] = None + divleg_profile_inner: Optional[float] = None + divleg_profile_outer: Optional[float] = None + divplt: Optional[float] = None + fdfs: Optional[float] = None + fdiva: Optional[float] = None + fgamp: Optional[float] = None + fififi: Optional[float] = None + flux_exp: Optional[float] = None + frrp: Optional[float] = None + hldivlim: Optional[float] = None + ksic: Optional[float] = None + omegan: Optional[float] = None + prn1: Optional[float] = None + rlenmax: Optional[float] = None + tdiv: Optional[float] = None + xparain: Optional[float] = None + xpertin: Optional[float] = None + zeffdiv: Optional[float] = None + + # Pulse + bctmp: Optional[float] = None + dtstor: Optional[float] = None + istore: Optional[int] = None + itcycl: Optional[int] = None + lpulse: Optional[int] = None # Listed as an output + + # IFE + + # Heat transport + baseel: Optional[float] = None + crypw_max: Optional[float] = None + f_crypmw: Optional[float] = None + etatf: Optional[float] = None + etath: Optional[float] = None + fpumpblkt: Optional[float] = None + fpumpdiv: Optional[float] = None + fpumpfw: Optional[float] = None + fpumpshld: Optional[float] = None + ipowerflow: Optional[int] = None + iprimshld: Optional[int] = None + pinjmax: Optional[float] = None + pwpm2: Optional[float] = None + trithtmw: Optional[float] = None + vachtmw: Optional[float] = None + irfcd: Optional[int] = None + + # Water usage + + # Vacuum + ntype: Optional[int] = None + pbase: Optional[float] = None + prdiv: Optional[float] = None + pumptp: Optional[float] = None + rat: Optional[float] = None + tn: Optional[float] = None + pumpareafraction: Optional[float] = None + pumpspeedmax: Optional[float] = None + pumpspeedfactor: Optional[float] = None + initialpressure: Optional[float] = None + outgasindex: Optional[float] = None + outgasfactor: Optional[float] = None + + # PF coil + alfapf: Optional[float] = None + alstroh: Optional[float] = None + coheof: Optional[float] = None + cptdin: List[float] = None + etapsu: Optional[float] = None + fcohbop: Optional[float] = None + fcuohsu: Optional[float] = None + fcupfsu: Optional[float] = None + fvssu: Optional[float] = None + ipfloc: Optional[List[int]] = None + ipfres: Optional[int] = None # Listed as an output + isumatoh: Optional[int] = None + isumatpf: Optional[int] = None + i_pf_current: Optional[int] = None + ncls: Optional[List[int]] = None + nfxfh: Optional[int] = None + ngrp: Optional[int] = None + ohhghf: Optional[float] = None + oh_steel_frac: Optional[float] = None + pfclres: Optional[float] = None + rjconpf: List[float] = None + routr: Optional[float] = None + rpf2: Optional[float] = None + rref: List[float] = None + sigpfcalw: Optional[float] = None + sigpfcf: Optional[float] = None + vf: List[float] = None + vhohc: Optional[float] = None + zref: List[float] = None + bmaxcs_lim: Optional[float] = None + fbmaxcs: Optional[float] = None + ld_ratio_cst: Optional[float] = None + + # Physics + alphaj: Optional[float] = None + alphan: Optional[float] = None + alphat: Optional[float] = None + aspect: Optional[float] = None + beamfus0: Optional[float] = None + beta: Optional[float] = None + betbm0: Optional[float] = None + bt: Optional[float] = None + csawth: Optional[float] = None + cvol: Optional[float] = None + cwrmax: Optional[float] = None + dene: Optional[float] = None + dnbeta: Optional[float] = None + epbetmax: Optional[float] = None + falpha: Optional[float] = None + fdeut: Optional[float] = None + ftar: Optional[float] = None + ffwal: Optional[float] = None + fgwped: Optional[float] = None + fgwsep: Optional[float] = None + fkzohm: Optional[float] = None + fpdivlim: Optional[float] = None + fne0: Optional[float] = None + ftrit: Optional[float] = None + fvsbrnni: Optional[float] = None + gamma: Optional[float] = None + hfact: Optional[float] = None + taumax: Optional[float] = None + ibss: Optional[int] = None + iculbl: Optional[int] = None # listed as an output... + icurr: Optional[int] = None + idensl: Optional[int] = None + ifalphap: Optional[int] = None + ifispact: Optional[int] = None # listed as an output... + iinvqd: Optional[int] = None + ipedestal: Optional[int] = None + ieped: Optional[int] = None # listed as an output... + eped_sf: Optional[float] = None + neped: Optional[float] = None + nesep: Optional[float] = None + plasma_res_factor: Optional[float] = None + rhopedn: Optional[float] = None + rhopedt: Optional[float] = None + tbeta: Optional[float] = None + teped: Optional[float] = None + tesep: Optional[float] = None + iprofile: Optional[int] = None + iradloss: Optional[int] = None + isc: Optional[int] = None + iscrp: Optional[int] = None + ishape: Optional[int] = None # listed as an output... + itart: Optional[int] = None # listed as an output... + itartpf: Optional[int] = None # listed as an output... + iwalld: Optional[int] = None + kappa: Optional[float] = None + kappa95: Optional[float] = None + m_s_limit: Optional[float] = None + ilhthresh: Optional[int] = None + q: Optional[float] = None + q0: Optional[float] = None + tauratio: Optional[float] = None + rad_fraction_sol: Optional[float] = None + ralpne: Optional[float] = None + rli: Optional[float] = None + rmajor: Optional[float] = None + rnbeam: Optional[float] = None + i_single_null: Optional[int] = None + ssync: Optional[float] = None + te: Optional[float] = None + ti: Optional[float] = None + tratio: Optional[float] = None + triang: Optional[float] = None + triang95: Optional[float] = None + + # Stellarator + fblvd: Optional[float] = None def __iter__(self) -> Generator[Tuple[str, Union[float, List, Dict]], None, None]: """ @@ -285,25 +725,30 @@ def to_invariable(self) -> Dict[str, _INVariable]: """ out_dict = {} for name, value in self: - if name not in ["icc", "ixc", "bounds"]: + if name not in ["icc", "ixc", "bounds"] and value is not None: new_val = _INVariable(name, value, "Parameter", "", "") out_dict[name] = new_val out_dict["icc"] = _INVariable( "icc", - self.icc, + [] if self.icc is None else self.icc, "Constraint Equation", "Constraint Equation", "Constraint Equations", ) + # PROCESS iteration variables need to be sorted to converge well(!) out_dict["ixc"] = _INVariable( "ixc", - self.ixc, + [] if self.ixc is None else sorted(self.ixc), "Iteration Variable", "Iteration Variable", "Iteration Variables", ) out_dict["bounds"] = _INVariable( - "bounds", self.bounds, "Bound", "Bound", "Bounds" + "bounds", + {} if self.bounds is None else self.bounds, + "Bound", + "Bound", + "Bounds", ) return out_dict diff --git a/bluemira/codes/process/_model_mapping.py b/bluemira/codes/process/_model_mapping.py new file mode 100644 index 0000000000..c15d28eccd --- /dev/null +++ b/bluemira/codes/process/_model_mapping.py @@ -0,0 +1,1151 @@ +# bluemira is an integrated inter-disciplinary design tool for future fusion +# reactors. It incorporates several modules, some of which rely on other +# codes, to carry out a range of typical conceptual fusion reactor design +# activities. +# +# Copyright (C) 2021-2023 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh, +# J. Morris, D. Short +# +# bluemira is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# bluemira is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with bluemira; if not, see . + +""" +PROCESS model mappings +""" +from dataclasses import dataclass, field +from typing import Tuple + +from bluemira.codes.utilities import Model + + +class classproperty: # noqa: N801 + """ + Hacking for properties to work with Enums + """ + + def __init__(self, func): + self.func = func + + def __get__(self, obj, owner): + """ + Apply function to owner + """ + return self.func(owner) + + +@dataclass +class ModelSelection: + """ + Mixin dataclass for a Model selection in PROCESSModel + + Parameters + ---------- + _value_: + Integer value of the model selection + requires: + List of required inputs for the model selection + description: + Short description of the model selection + """ + + _value_: int + requires_values: Tuple[str] = field(default_factory=tuple) + description: str = "" + + +class PROCESSModel(ModelSelection, Model): + """ + Baseclass for PROCESS models + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + raise NotImplementedError(f"{self.__name__} has no 'switch_name' property.") + + +class PROCESSOptimisationAlgorithm(PROCESSModel): + """ + Switch for the optimisation algorithm to use in PROCESS + + # TODO: This switch will be used in future to support + alternative optimisation algorithms. + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ioptimz" + + NO_OPTIMISATION = 0, (), "Do not use optimisation" + VMCON = 1, (), "The traditional VMCON optimisation algorithm" + + +class PlasmaGeometryModel(PROCESSModel): + """ + Switch for plasma geometry + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ishape" + + HENDER_K_D_100 = 0, ("kappa", "triang") + GALAMBOS_K_D_95 = 1, ("kappa95", "triang95") + ZOHM_ITER = 2, ("triang", "fkzohm") + ZOHM_ITER_D_95 = 3, ("triang95", "fkzohm") + HENDER_K_D_95 = 4, ("kappa95, triang95") + MAST_95 = 5, ("kappa95, triang95") + MAST_100 = 6, ("kappa, triang") + FIESTA_95 = 7, ("kappa95, triang95") + FIESTA_100 = 8, ("kappa, triang") + A_LI3 = 9, ("triang",) + CREATE_A_M_S = ( + 10, + ("aspect", "m_s_limit", "triang"), + "A fit to CREATE data for conventional A tokamaks", + ) + MENARD = 11, ("triang", "aspect") + + +class PlasmaNullConfigurationModel(PROCESSModel): + """ + Switch for single-null / double-null + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_single_null" + + DOUBLE_NULL = 0, ("ftar",) + SINGLE_NULL = 1 + + +class PlasmaPedestalModel(PROCESSModel): + """ + Switch for plasma profile model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ipedestal" + + NO_PEDESTAL = 0, ("te",) + PEDESTAL_GW = 1, ( + "te", + "neped", + "nesep", + "rhopedn", + "rhopedt", + "tbeta", + "teped", + "tesep", + "ralpne", + ) + PLASMOD_GW = 2, ("te", "neped", "nesep", "tbeta", "teped", "tesep", "ralpne") + PLASMOD = 3, ("te", "rhopedn", "rhopedt", "teped", "tesep") + + +class PlasmaProfileModel(PROCESSModel): + """ + Switch for current profile consistency + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iprofile" + + INPUT = 0, ("alphaj", "rli") + CONSISTENT = 1, ("q", "q0") + + +class EPEDScalingModel(PROCESSModel): + """ + Switch for the pedestal scaling model + + TODO: This is largely undocumented and bound to some extent with PLASMOD + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ieped" + + UKNOWN_0 = 0, ("teped",) + SAARELMA = 1 + UNKNOWN_1 = 2 + UNKNOWN_2 = 3 + + +class BetaLimitModel(PROCESSModel): + """ + Switch for the plasma beta limit model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iculbl" + + TOTAL = 0 # Including fast ion contribution + THERMAL = 1 + THERMAL_NBI = 2 + TOTAL_TF = 3 # Calculated using only the toroidal field + + +class BetaGScalingModel(PROCESSModel): + """ + Switch for the beta g coefficient dnbeta model + + NOTE: Over-ridden if iprofile = 1 + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "gtscale" + + INPUT = 0, ("dnbeta",) + CONVENTIONAL = 1 + MENARD_ST = 2 + + +class AlphaPressureModel(PROCESSModel): + """ + Switch for the pressure contribution from fast alphas + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ifalphap" + + HENDER = 0 + WARD = 1 + + +class DensityLimitModel(PROCESSModel): + """ + Switch for the density limit model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "idensl" + + ASDEX = 1 + BORRASS_ITER_I = 2 + BORRASS_ITER_II = 3 + JET_RADIATION = 4 + JET_SIMPLE = 5 + HUGILL_MURAKAMI = 6 + GREENWALD = 7 + + +class PlasmaCurrentScalingLaw(PROCESSModel): + """ + Switch for plasma current scaling law + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "icurr" + + PENG = 1 + PENG_DN = 2 + ITER_SIMPLE = 3 + ITER_REVISED = 4 # Recommended for iprofile = 1 + TODD_I = 5 + TODD_II = 6 + CONNOR_HASTIE = 7 + SAUTER = 8 + FIESTA = 9 + + +class ConfinementTimeScalingLaw(PROCESSModel): + """ + Switch for the energy confinement time scaling law + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "isc" + + NEO_ALCATOR_OHMIC = 1 + MIRNOV_H_MODE = 2 + MEREZHKIN_MUHKOVATOV_L_MODE = 3 + SHIMOMURA_H_MODE = 4 + KAYE_GOLDSTON_L_MODE = 5 + ITER_89_P_L_MODE = 6 + ITER_89_O_L_MODE = 7 + REBUT_LALLIA_L_MODE = 8 + GOLDSTON_L_MODE = 9 + T10_L_MODE = 10 + JAERI_88_L_MODE = 11 + KAYE_BIG_COMPLEX_L_MODE = 12 + ITER_H90_P_H_MODE = 13 + ITER_MIX = 14 # Minimum of 6 and 7 + RIEDEL_L_MODE = 15 + CHRISTIANSEN_L_MODE = 16 + LACKNER_GOTTARDI_L_MODE = 17 + NEO_KAYE_L_MODE = 18 + RIEDEL_H_MODE = 19 + ITER_H90_P_H_MODE_AMENDED = 20 + LHD_STELLARATOR = 21 + GRYO_RED_BOHM_STELLARATOR = 22 + LACKNER_GOTTARDI_STELLARATOR = 23 + ITER_93H_H_MODE = 24 + TITAN_RFP = 25 + ITER_H97_P_NO_ELM_H_MODE = 26 + ITER_H97_P_ELMY_H_MODE = 27 + ITER_96P_L_MODE = 28 + VALOVIC_ELMY_H_MODE = 29 + KAYE_PPPL98_L_MODE = 30 + ITERH_PB98P_H_MODE = 31 + IPB98_Y_H_MODE = 32 + IPB98_Y1_H_MODE = 33 + IPB98_Y2_H_MODE = 34 + IPB98_Y3_H_MODE = 35 + IPB98_Y4_H_MODE = 36 + ISS95_STELLARATOR = 37 + ISS04_STELLARATOR = 38 + DS03_H_MODE = 39 + MURARI_H_MODE = 40 + PETTY_H_MODE = 41 + LANG_H_MODE = 42 + HUBBARD_NOM_I_MODE = 43 + HUBBARD_LOW_I_MODE = 44 + HUBBARD_HI_I_MODE = 45 + NSTX_H_MODE = 46 + NSTX_PETTY_H_MODE = 47 + NSTX_GB_H_MODE = 48 + INPUT = 49, ("tauee_in",) + + +class BootstrapCurrentScalingLaw(PROCESSModel): + """ + Switch for the model to calculate bootstrap fraction + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ibss" + + ITER = 1, ("cboot",) + GENERAL = 2 + NUMERICAL = 3 + SAUTER = 4 + + +class LHThreshholdScalingLaw(PROCESSModel): + """ + Switch for the model to calculate the L-H power threshhold + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ilhthresh" + + ITER_1996_NOM = 1 + ITER_1996_LOW = 2 + ITER_1996_HI = 3 + ITER_1997 = 4 + ITER_1997_K = 5 + MARTIN_NOM = 6 + MARTIN_HI = 7 + MARTIN_LOW = 8 + SNIPES_NOM = 9 + SNIPES_HI = 10 + SNIPES_LOW = 11 + SNIPES_CLOSED_DIVERTOR_NOM = 12 + SNIPES_CLOSED_DIVERTOR_HI = 13 + SNIPES_CLOSED_DIVERTOR_LOW = 14 + HUBBARD_LI_NOM = 15 + HUBBARD_LI_HI = 16 + HUBBARD_LI_LOW = 17 + HUBBARD_2017_LI = 18 + MARTIN_ACORRECT_NOM = 19 + MARTIN_ACORRECT_HI = 20 + MARTIN_ACORRECT_LOW = 21 + + +class RadiationLossModel(PROCESSModel): + """ + Switch for radiation loss term usage in power balance + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iradloss" + + SCALING_PEDSETAL = 0 # ipedestal 2, 3 + SCALING_CORE = 1 + SCALING_ONLY = 2 + + +class PlasmaWallGapModel(PROCESSModel): + """ + Switch to select plasma-wall gap model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iscrp" + + TEN_PERCENT = 0, (), "SOL thickness calculated as 10 percent of minor radius" + INPUT = 1, ("scrapli", "scraplo"), "Fixed thickness SOL values" + + +class OperationModel(PROCESSModel): + """ + Switch to set the operation mode + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "lpulse" + + STEADY_STATE = 0 + PULSED = 1 + + +class PowerFlowModel(PROCESSModel): + """ + Switch to control power flow model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ipowerflow" + + SIMPLE = 0 + STELLARATOR = 1 + + +class ThermalStorageModel(PROCESSModel): + """ + Switch to et the power cycle thermal storage model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "istore" + + INHERENT_STEAM = 1 + BOILER = 2 + STEEL = 3, ("dtstor",) # Obsolete + + +class BlanketModel(PROCESSModel): + """ + Switch to select the blanket model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "blktmodel" + + CCFE_HCPB = 1 + KIT_HCPB = 2 + CCFE_HCPB_TBR = 3 + + +class InboardBlanketSwitch(PROCESSModel): + """ + Switch to determin whether or not there is an inboard blanket + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iblktith" + + ABSENT = 0 + PRESENT = 1 + + +class InVesselGeometryModel(PROCESSModel): + """ + Switch to control the geometry of the FW, blanket, shield, and VV shape + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "fwbsshape" + + CYL_ELLIPSE = 1 + TWO_ELLIPSE = 2 + + +class TFCSTopologyModel(PROCESSModel): + """ + Switch to select the TF-CS topology + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "tf_in_cs" + + ITER = 0 + INSANITY = 1 + + +class TFCoilConductorTechnology(PROCESSModel): + """ + Switch for TF coil conductor model: + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_sup" + + COPPER = 0, ("tfootfi",) + SC = 1 + CRYO_AL = 2 + + +class TFSuperconductorModel(PROCESSModel): + """ + Switch for the TF superconductor model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_sc_mat" + + NB3SN_ITER_STD = 1 + BI_2212 = 2 + NBTI = 3 + NB3SN_ITER_INPUT = 4 # User-defined critical parameters + NB3SN_WST = 5 + REBCO_CROCO = 6 + NBTI_DGL = 7 + REBCO_DGL = 8 + REBCO_ZHAI = 9 + + +class TFCasingGeometryModel(PROCESSModel): + """ + Switch for the TF casing geometry model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_case_geom" + + CURVED = 0 + FLAT = 1 + + +class TFWindingPackGeometryModel(PROCESSModel): + """ + Switch for the TF winding pack geometry model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_wp_geom" + + RECTANGULAR = 0 + DOUBLE_RECTANGULAR = 1 + TRAPEZOIDAL = 2 + + +class TFWindingPackTurnModel(PROCESSModel): + """ + Switch for the TF winding pack turn model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_turns_integer" + + CURRENT_PER_TURN = 0, ("cpttf",) # or t_cable_tf or t_turn_tf + INTEGER_TURN = 1, ("n_layer", "n_pancake") + + +class TFCoilShapeModel(PROCESSModel): + """ + Switch for the TF coil shape model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_shape" + + PRINCETON = 1 + PICTURE_FRAME = 2 + + +class ResistiveCentrepostModel(PROCESSModel): + """ + Swtich for the resistive centrepost model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_r_cp_top" + + CALCULATED = 0 + INPUT = 1 + MID_TOP_RATIO = 2 + + +class TFCoilJointsModel(PROCESSModel): + """ + Switch for the TF coil joints + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_cp_joints" + + SC_CLAMP_RES_SLIDE = ( + -1, + (), + "Chooses clamped joints for SC magnets (i_tf_sup=1)" + " and sliding joints for resistive magnets (i_tf_sup=0,2)", + ) + NO_JOINTS = 0 + SLIDING_JOINTS = 1, ( + "tho_tf_joints", + "n_tf_joints_contact", + "n_tf_joints", + "th_joint_contact", + ) + + +class TFStressModel(PROCESSModel): + """ + Switch for the TF inboard midplane stress model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_stress_model" + + GEN_PLANE_STRAIN = 0 + PLANE_STRESS = 1 + GEN_PLANE_STRAIN_NEW = 2 + + +class TFCoilSupportModel(PROCESSModel): + """ + Switch for the TF inboard coil support model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_tf_bucking" + + NO_SUPPORT = 0 + BUCKED = 1 + BUCKED_WEDGED = 2 + + +class PFConductorModel(PROCESSModel): + """ + Switch for the PF conductor technology model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ipfres" + + SUPERCONDUCTING = 0 + RESISTIVE = 1 + + +class PFSuperconductorModel(PROCESSModel): + """ + Switch for the PF superconductor model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "isumatpf" + + NB3SN_ITER_STD = 1 + BI_2212 = 2, ("fhts",) + NBTI = 3 + NB3SN_ITER_INPUT = 4 # User-defined critical parameters + NB3SN_WST = 5 + REBCO_CROCO = 6 + NBTI_DGL = 7 + REBCO_DGL = 8 + REBCO_ZHAI = 9 + + +class PFCurrentControlModel(PROCESSModel): + """ + Switch to control the currents in the PF coils + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_pf_current" + + INPUT = 0, ("curpfb", "curpff", "curpfs") + SVD = 1 + + +class SolenoidSwitchModel(PROCESSModel): + """ + Switch to control whether or not a central solenoid should be + used. + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iohcl" + + NO_SOLENOID = 0 + SOLENOID = 1 + + +class CSSuperconductorModel(PROCESSModel): + """ + Switch for the CS superconductor model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "isumatoh" + + NB3SN_ITER_STD = 1 + BI_2212 = 2 + NBTI = 3 + NB3SN_ITER_INPUT = 4 # User-defined critical parameters + NB3SN_WST = 5 + REBCO_CROCO = 6 + NBTI_DGL = 7 + REBCO_DGL = 8 + REBCO_ZHAI = 9 + + +class CSPrecompressionModel(PROCESSModel): + """ + Switch to control the existence of pre-compression tie plates in the CS + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iprecomp" + + ABSENT = 0 + PRESENT = 1 + + +class CSStressModel(PROCESSModel): + """ + Switch for the calculation of the CS stress + + # TODO: Listed as an output?! + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_cs_stress" + + HOOP_ONLY = 0 + HOOP_AXIAL = 1 + + +class DivertorHeatFluxModel(PROCESSModel): + """ + Switch for the divertor heat flux model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "i_hldiv" + + # TODO: What about Kallenbach? + INPUT = 0 + CHAMBER = 1 + WADE = 2 + + +class DivertorThermalHeatUse(PROCESSModel): + """ + Switch to control if the divertor thermal power is used in the + power cycle + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iprimdiv" + + LOW_GRADE_HEAT = 0 + HIGH_GRADE_HEAT = 1 + + +class ShieldThermalHeatUse(PROCESSModel): + """ + Switch to control if shield (inside VV) is used in the power cycle + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iprimshld" + + NOT_USED = 0 + LOW_GRADE_HEAT = 1 + + +class TFNuclearHeatingModel(PROCESSModel): + """ + Switch to control nuclear heating in TF model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "inuclear" + + FRANCES_FOX = 0 + INPUT = 1, ("qnuc",) + + +class PrimaryPumpingModel(PROCESSModel): + """ + Switch for the calculation method of the pumping power + required for the primary coolant + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "primary_pumping" + + INPUT = 0 + FRACTION = 1 + PRESSURE_DROP = 2 + PRESSURE_DROP_INPUT = 3 + + +class SecondaryCycleModel(PROCESSModel): + """ + Switch for the calculation of thermal to electric conversion efficiency + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "secondary_cycle" + + FIXED = 0 + FIXED_W_DIVERTOR = 1 + INPUT = 2 + RANKINE = 3 + BRAYTON = 4 + + +class CurrentDriveEfficiencyModel(PROCESSModel): + """ + Switch for current drive efficiency model: + + 1 - Fenstermacher Lower Hybrid + 2 - Ion Cyclotron current drive + 3 - Fenstermacher ECH + 4 - Ehst Lower Hybrid + 5 - ITER Neutral Beam + 6 - new Culham Lower Hybrid model + 7 - new Culham ECCD model + 8 - new Culham Neutral Beam model + 10 - ECRH user input gamma + 11 - ECRH "HARE" model (E. Poli, Physics of Plasmas 2019) + 12 - EBW user scaling input. Scaling (S. Freethy) + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iefrf" + + FENSTER_LH = 1 + ICYCCD = 2 + FENSTER_ECH = 3 + EHST_LH = 4 + ITER_NB = 5 + CUL_LH = 6 + CUL_ECCD = 7 + CUL_NB = 8 + ECRH_UI_GAM = 10 + ECRH_HARE = 11 + EBW_UI = 12 + + +class PlasmaIgnitionModel(PROCESSModel): + """ + Switch to control whether or not the plasma is ignited + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ignite" + + NOT_IGNITED = 0 + IGNITED = 1 + + +class VacuumPumpingModel(PROCESSModel): + """ + Switch to control the vacuum pumping technology model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ntype" + + TURBO_PUMP = 0 + CRYO_PUMP = 1 + + +class VacuumPumpingDwellModel(PROCESSModel): + """ + Switch to control when vacuum pumping occurs + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "dwell_pump" + + T_DWELL = 0 + T_RAMP = 1 + T_DWELL_RAMP = 2 + + +class FISPACTSwitchModel(PROCESSModel): + """ + Switch to control FISPACT-II neutronics calculations + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "ifispact" + + OFF = 0 + ON = 1 # Presumably... + + +class AvailabilityModel(PROCESSModel): + """ + Switch to control the availability model + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "iavail" + + INPUT = 0 + TAYLOR_WARD = 1 + MORRIS = 2 + + +class SafetyAssuranceLevel(PROCESSModel): + """ + Switch to control the level of safety assurance + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "lsa" + + TRULY_SAFE = 1 + VERY_SAFE = 2 # In-between + SOMEWHAT_SAFE = 3 # In-between + FISSION = 4 # Not sure what this is implying... + + +class CostModel(PROCESSModel): + """ + Switch to control the cost model used + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "cost_model" + + TETRA_1990 = 0 + KOVARI_2015 = 1 + + +class OutputCostsSwitch(PROCESSModel): + """ + Switch to control whether or not cost information is output + """ + + @classproperty + def switch_name(self) -> str: + """ + PROCESS switch name + """ + return "output_costs" + + NO = 0, (), "Do not print cost information to output" + YES = 1, (), "Print cost information to output" diff --git a/bluemira/codes/process/_plotting.py b/bluemira/codes/process/_plotting.py index cb25c55ac8..4b77b49769 100644 --- a/bluemira/codes/process/_plotting.py +++ b/bluemira/codes/process/_plotting.py @@ -210,7 +210,11 @@ def read_radial_build(num): # Be careful that the numbers don't change rb = [] num += 1 while "***" not in raw[num]: - if read_rb_line(raw[num]) is None: + if "TF coil radial placement switch" in raw[num]: + # PROCESS v3.0.0 added this line to the start of the RB + # TF coil radial placement switch ... (tf_in_cs) .... 0 + pass + elif read_rb_line(raw[num]) is None: pass else: rb.append(read_rb_line(raw[num])) diff --git a/bluemira/codes/process/_run.py b/bluemira/codes/process/_run.py index c79e59aaa0..bd026fe8ca 100644 --- a/bluemira/codes/process/_run.py +++ b/bluemira/codes/process/_run.py @@ -77,7 +77,17 @@ def runinput(self): """ self._run_process() + @staticmethod + def flush_callable(line: str) -> bool: + """Callable for flushed output""" + try: + int(line.split("|")[0]) + except ValueError: + return False + else: + return True + def _run_process(self): bluemira_print(f"Running '{PROCESS_NAME}' systems code") command = [self.binary, "-i", self.in_dat_path] - self._run_subprocess(command) + self._run_subprocess(command, flush_callable=self.flush_callable) diff --git a/bluemira/codes/process/_setup.py b/bluemira/codes/process/_setup.py index 00e27da360..39abec2f6a 100644 --- a/bluemira/codes/process/_setup.py +++ b/bluemira/codes/process/_setup.py @@ -24,16 +24,16 @@ from pathlib import Path from typing import ClassVar, Dict, Optional, Union +from bluemira.base.parameter_frame import ParameterFrame from bluemira.codes.error import CodesError from bluemira.codes.interface import CodesSetup from bluemira.codes.process._inputs import ProcessInputs -from bluemira.codes.process.api import InDat, _INVariable, update_obsolete_vars -from bluemira.codes.process.constants import NAME as PROCESS_NAME -from bluemira.codes.process.mapping import ( +from bluemira.codes.process._model_mapping import ( CurrentDriveEfficiencyModel, TFCoilConductorTechnology, ) -from bluemira.codes.process.params import ProcessSolverParams +from bluemira.codes.process.api import ENABLED, InDat, _INVariable, update_obsolete_vars +from bluemira.codes.process.constants import NAME as PROCESS_NAME class Setup(CodesSetup): @@ -46,9 +46,6 @@ class Setup(CodesSetup): The bluemira parameters for this task. in_dat_path: The path to where the IN.DAT file should be written. - template_in_dat_path: - The path to a template PROCESS IN.DAT file. By default this - points to a sample one within the Bluemira repository. problem_settings: The PROCESS parameters that do not exist in Bluemira. """ @@ -60,17 +57,13 @@ class Setup(CodesSetup): def __init__( self, - params: ProcessSolverParams, + params: Union[Dict, ParameterFrame], in_dat_path: str, - template_in_dat: Union[str, ProcessInputs] = None, problem_settings: Optional[Dict[str, Union[float, str]]] = None, ): super().__init__(params, PROCESS_NAME) self.in_dat_path = in_dat_path - self.template_in_dat = ( - self.params.template_defaults if template_in_dat is None else template_in_dat - ) self.problem_settings = problem_settings if problem_settings is not None else {} def run(self): @@ -102,14 +95,16 @@ def _write_in_dat(self, use_bp_inputs: bool = True): Default, True """ # Load defaults in bluemira folder - writer = _make_writer(self.template_in_dat) + writer = _make_writer(self.params.template_defaults) if use_bp_inputs: inputs = self._get_new_inputs(remapper=update_obsolete_vars) for key, value in inputs.items(): - writer.add_parameter(key, value) + if value is not None: + writer.add_parameter(key, value) for key, value in self.problem_settings.items(): - writer.add_parameter(key, value) + if value is not None: + writer.add_parameter(key, value) self._validate_models(writer) @@ -130,12 +125,23 @@ def _validate_models(self, writer): writer.add_parameter(name, model.value) -def _make_writer(template_in_dat: Union[str, Dict[str, _INVariable]]) -> InDat: - if isinstance(template_in_dat, Dict): - indat = InDat(filename=None) - indat.data = template_in_dat - return indat - if isinstance(template_in_dat, str) and Path(template_in_dat).is_file(): +def _make_writer(template_in_dat: Dict[str, _INVariable]) -> InDat: + indat = InDat(filename=None) + indat.data = template_in_dat + return indat + + +def create_template_from_path(template_in_dat: Union[str, Path]) -> ProcessInputs: + if not ENABLED: + raise CodesError( + f"{PROCESS_NAME} is not installed cannot read template {template_in_dat}" + ) + if Path(template_in_dat).is_file(): # InDat autoloads IN.DAT without checking for existence - return InDat(filename=template_in_dat) + return ProcessInputs( + **{ + k: v.value if k == "runtitle" else v.get_value + for k, v in InDat(filename=template_in_dat).data.items() + } + ) raise CodesError(f"Template IN.DAT '{template_in_dat}' is not a file.") diff --git a/bluemira/codes/process/_solver.py b/bluemira/codes/process/_solver.py index 514656f755..e58f664b3c 100644 --- a/bluemira/codes/process/_solver.py +++ b/bluemira/codes/process/_solver.py @@ -22,16 +22,20 @@ import copy from enum import auto from pathlib import Path -from typing import Dict, List, Mapping, Tuple, Union +from typing import Dict, List, Mapping, Tuple, Type, Union import numpy as np from bluemira.base.look_and_feel import bluemira_warn from bluemira.base.parameter_frame import ParameterFrame from bluemira.codes.error import CodesError -from bluemira.codes.interface import BaseRunMode, CodesSolver +from bluemira.codes.interface import ( + BaseRunMode, + CodesSolver, +) +from bluemira.codes.process._inputs import ProcessInputs from bluemira.codes.process._run import Run -from bluemira.codes.process._setup import Setup +from bluemira.codes.process._setup import Setup, create_template_from_path from bluemira.codes.process._teardown import Teardown from bluemira.codes.process.api import Impurities from bluemira.codes.process.constants import BINARY as PROCESS_BINARY @@ -76,9 +80,21 @@ class Solver(CodesSolver): The directory in which to run PROCESS. It is also the directory in which to look for PROCESS input and output files. Default is current working directory. + * read_dir: + The directory from which data is read when running in read mode. + * template_in_dat_path: + The path to a template PROCESS IN.DAT file or and instances of + :class:`bluemira.codes.process._inputs.ProcessInputs`. + By default this is an empty instance of the class. To create a new + instance + :class:`bluemira.codes.process.template_builder.PROCESSTemplateBuilder` + should be used. * problem_settings: Any PROCESS parameters that do not correspond to a bluemira parameter. + * in_dat_path: + The path to save the IN.DAT file that is run by PROCESS. + By default this is '/IN.DAT'. Notes ----- @@ -106,11 +122,11 @@ class Solver(CodesSolver): overwriting data with PROCESS outputs would be undesirable. """ - name = PROCESS_NAME - setup_cls = Setup - run_cls = Run - teardown_cls = Teardown - run_mode_cls = RunMode + name: str = PROCESS_NAME + setup_cls: Type[Setup] = Setup + run_cls: Type[Run] = Run + teardown_cls: Type[Teardown] = Teardown + run_mode_cls: Type[RunMode] = RunMode def __init__( self, @@ -123,27 +139,22 @@ def __init__( self._run: Union[Run, None] = None self._teardown: Union[Teardown, None] = None - self.params = ProcessSolverParams.from_defaults() - - if isinstance(params, ParameterFrame): - self.params.update_from_frame(params) - else: - try: - self.params.update_from_dict(params) - except TypeError: - self.params.update_values(params) - _build_config = copy.deepcopy(build_config) self.binary = _build_config.pop("binary", PROCESS_BINARY) self.run_directory = _build_config.pop("run_dir", Path.cwd().as_posix()) self.read_directory = _build_config.pop("read_dir", Path.cwd().as_posix()) - self.template_in_dat = _build_config.pop( - "template_in_dat", self.params.template_defaults - ) + self.template_in_dat = _build_config.pop("template_in_dat", ProcessInputs()) self.problem_settings = _build_config.pop("problem_settings", {}) self.in_dat_path = _build_config.pop( "in_dat_path", Path(self.run_directory, "IN.DAT").as_posix() ) + + if isinstance(self.template_in_dat, (str, Path)): + self.template_in_dat = create_template_from_path(self.template_in_dat) + + self.params = ProcessSolverParams.from_defaults(self.template_in_dat) + self.params.update(params) + if len(_build_config) > 0: quoted_delim = "', '" bluemira_warn( @@ -164,14 +175,15 @@ def execute(self, run_mode: Union[str, RunMode]) -> ParameterFrame: """ if isinstance(run_mode, str): run_mode = self.run_mode_cls.from_string(run_mode) - self._setup = Setup( + self._setup = self.setup_cls( self.params, self.in_dat_path, - self.template_in_dat, self.problem_settings, ) - self._run = Run(self.params, self.in_dat_path, self.binary) - self._teardown = Teardown(self.params, self.run_directory, self.read_directory) + self._run = self.run_cls(self.params, self.in_dat_path, self.binary) + self._teardown = self.teardown_cls( + self.params, self.run_directory, self.read_directory + ) if setup := self._get_execution_method(self._setup, run_mode): setup() @@ -205,7 +217,9 @@ def get_raw_variables(self, params: Union[List, str]) -> List[float]: ) @staticmethod - def get_species_data(impurity: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def get_species_data( + impurity: str, confinement_time_ms: float + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Get species data from PROCESS section of OPEN-ADAS database. @@ -217,18 +231,27 @@ def get_species_data(impurity: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray] The impurity to get the species data for. This string should be one of the names in the :class:`~bluemira.codes.process.api.Impurities` Enum. + confinement_time_ms: + the confinement time to read the data for options are: + [0.1, 1.0, 10.0, 100.0, 1000.0, np.inf] Returns ------- tref: - The temperature in keV. + The temperature in eV. l_ref: The loss function value $L_z(n_e, T_e)$ in W.m3. z_ref: Average effective charge. """ - t_ref, lz_ref, z_av_ref = np.genfromtxt(Impurities[impurity].file()).T - return t_ref, lz_ref, z_av_ref + lz_ref, z_ref = Impurities[impurity].read_impurity_files(("lz", "z")) + + t_ref = filter(lambda lz: lz.content == "Te[eV]", lz_ref) + lz_ref = filter(lambda lz: f"{confinement_time_ms:.1f}" in lz.content, lz_ref) + z_av_ref = filter(lambda z: f"{confinement_time_ms:.1f}" in z.content, z_ref) + return tuple( + np.array(next(ref).data, dtype=float) for ref in (t_ref, lz_ref, z_av_ref) + ) def get_species_fraction(self, impurity: str) -> float: """ diff --git a/bluemira/codes/process/_teardown.py b/bluemira/codes/process/_teardown.py index d80b57f38f..467812c845 100644 --- a/bluemira/codes/process/_teardown.py +++ b/bluemira/codes/process/_teardown.py @@ -232,7 +232,7 @@ def _derive_radial_build_params(self, data: Dict) -> Dict[str, float]: except KeyError: # PROCESS updated their parameter names in v2.4.0, splitting # 'thshield' into 'thshield_ib', 'thshield_ob', and 'thshield_vb' - shield_th = data["thshield_ib"] + data["thshield_ib"] + shield_th = data["thshield_ib"] try: rtfin = data["bore"] + data["ohcth"] + data["precomp"] + data["gapoh"] diff --git a/bluemira/codes/process/api.py b/bluemira/codes/process/api.py index bf6d8d4fcd..ef6a11c522 100644 --- a/bluemira/codes/process/api.py +++ b/bluemira/codes/process/api.py @@ -22,10 +22,13 @@ """ PROCESS api """ +from __future__ import annotations + from dataclasses import dataclass from enum import Enum +from importlib import resources from pathlib import Path -from typing import Dict, List, TypeVar, Union +from typing import Dict, Iterable, List, Literal, Tuple, TypeVar, Union from bluemira.base.look_and_feel import bluemira_print, bluemira_warn from bluemira.codes.error import CodesError @@ -57,7 +60,7 @@ def __init__(self, filename): imp_data = None # placeholder for PROCESS module try: - import process.data.impuritydata as imp_data + from process.impurity_radiation import ImpurityDataHeader, read_impurity_file from process.io.in_dat import InDat # noqa: F401, F811 from process.io.mfile import MFile # noqa: F401, F811 from process.io.python_fortran_dicts import get_dicts @@ -141,12 +144,20 @@ class Impurities(Enum): Xe = 13 W = 14 - def file(self): + def files(self) -> Dict[str, Path]: """ Get PROCESS impurity data file path """ + with resources.path( + "process.data.lz_non_corona_14_elements", "Ar_lz_tau.dat" + ) as dp: + data_path = dp.parent + try: - return Path(Path(imp_data.__file__).parent, f"{self.name:_<2}Lzdata.dat") + return { + i: Path(data_path, f"{self.name:_<3}{i}_tau.dat") + for i in ("lz", "z", "z2") + } except NameError: raise CodesError("PROCESS impurity data directory not found") from None @@ -154,7 +165,16 @@ def id(self): # noqa: A003 """ Get variable string for impurity fraction """ - return f"fimp({self.value:02}" + return f"fimp({self.value:02})" + + def read_impurity_files( + self, filetype: Iterable[Literal["lz", "z2", "z"]] + ) -> Tuple[list[ImpurityDataHeader]]: + """Get contents of impurity data files""" + files = self.files() + return tuple( + read_impurity_file(files[file]) for file in set(filetype).intersection(files) + ) def update_obsolete_vars(process_map_name: str) -> Union[str, List[str], None]: diff --git a/bluemira/codes/process/mapping.py b/bluemira/codes/process/mapping.py index 5de53a5319..c9a513c161 100644 --- a/bluemira/codes/process/mapping.py +++ b/bluemira/codes/process/mapping.py @@ -22,59 +22,9 @@ """ PROCESS mappings """ -from bluemira.codes.utilities import Model, create_mapping - - -class CurrentDriveEfficiencyModel(Model): - """ - Switch for current drive efficiency model: - - 1 - Fenstermacher Lower Hybrid - 2 - Ion Cyclotron current drive - 3 - Fenstermacher ECH - 4 - Ehst Lower Hybrid - 5 - ITER Neutral Beam - 6 - new Culham Lower Hybrid model - 7 - new Culham ECCD model - 8 - new Culham Neutral Beam model - 10 - ECRH user input gamma - 11 - ECRH "HARE" model (E. Poli, Physics of Plasmas 2019) - 12 - EBW user scaling input. Scaling (S. Freethy) - - PROCESS variable name: "iefrf" - """ - - FENSTER_LH = 1 - ICYCCD = 2 - FENSTER_ECH = 3 - EHST_LH = 4 - ITER_NB = 5 - CUL_LH = 6 - CUL_ECCD = 7 - CUL_NB = 8 - ECRH_UI_GAM = 10 - ECRH_HARE = 11 - EBW_UI = 12 - - -class TFCoilConductorTechnology(Model): - """ - Switch for TF coil conductor model: - - 0 - copper - 1 - superconductor - 2 - Cryogenic aluminium - - PROCESS variable name: "i_tf_sup" - """ - - COPPER = 0 - SC = 1 - CRYO_AL = 2 - +from bluemira.codes.utilities import create_mapping IN_mappings = { - "P_el_net": ("pnetelin", "MW"), "n_TF": ("n_tf", "dimensionless"), "TF_ripple_limit": ("ripmax", "%"), "C_Ejima": ("gamma", "dimensionless"), @@ -82,20 +32,26 @@ class TFCoilConductorTechnology(Model): "P_hcd_ss": ("pinjalw", "MW"), "eta_nb": ("etanbi", "dimensionless"), "e_mult": ("emult", "dimensionless"), - "tk_sh_out": ("shldoth", "m"), - "tk_sh_top": ("shldtth", "m"), - "tk_sh_bot": ("shldlth", "m"), - "tk_vv_out": ("d_vv_out", "m"), - "tk_vv_top": ("d_vv_top", "m"), - "tk_vv_bot": ("d_vv_bot", "m"), "tk_cr_vv": ("ddwex", "m"), - "tk_tf_front_ib": ("dr_tf_case_out", "m"), + "tk_tf_front_ib": ("casthi", "m"), "tk_tf_side": ("casths", "m"), "PsepB_qAR_max": ("psepbqarmax", "MW.T/m"), + "q_0": ("q0", "dimensionless"), + "m_s_limit": ("m_s_limit", "dimensionless"), + "delta": ("triang", "dimensionless"), + "sigma_tf_case_max": ("sig_tf_case_max", "Pa"), + "sigma_tf_wp_max": ("sig_tf_wp_max", "Pa"), + "sigma_cs_wp_max": ("alstroh", "Pa"), + "H_star": ("hfact", "dimensionless"), + "bb_pump_eta_el": ("etahtp", "dimensionless"), + "bb_pump_eta_isen": ("etaiso", "dimensionless"), + "bb_t_inlet": ("inlet_temp", "K"), + "bb_t_outlet": ("outlet_temp", "K"), + "eta_ecrh": ("etaech", "dimensionless"), + "gamma_ecrh": ("gamma_ecrh", "1e20 A/W/m^2"), } OUT_mappings = { - "P_el_net_process": ("pnetelmw", "MW"), "R_0": ("rmajor", "m"), "B_0": ("bt", "T"), "kappa_95": ("kappa95", "dimensionless"), @@ -108,8 +64,8 @@ class TFCoilConductorTechnology(Model): "P_fus_DD": ("pdd", "MW"), "H_star": ("hfact", "dimensionless"), "P_sep": ("pdivt", "MW"), - "P_rad_core": ("pcoreradmw", "MW"), - "P_rad_edge": ("pedgeradmw", "MW"), + "P_rad_core": ("pinnerzoneradmw", "MW"), + "P_rad_edge": ("pouterzoneradmw", "MW"), "P_rad": ("pradmw", "MW"), "P_line": ("plinepv*vol", "MW"), "P_sync": ("psyncpv*vol", "MW"), @@ -122,7 +78,7 @@ class TFCoilConductorTechnology(Model): "tk_fw_in": ("fwith", "m"), "tk_fw_out": ("fwoth", "m"), "tk_tf_inboard": ("tfcth", "m"), - "tk_tf_nose": ("dr_tf_case_in", "m"), + "tk_tf_nose": ("thkcas", "m"), "tf_wp_width": ("dr_tf_wp", "m"), "tf_wp_depth": ("wwp1", "m"), "tk_tf_ins": ("tinstf", "m"), @@ -145,38 +101,52 @@ class TFCoilConductorTechnology(Model): "TF_respc_ob": ("tflegres", "ohm"), "TF_currpt_ob": ("cpttf", "A"), "P_bd_in": ("pinjmw", "MW"), - "condrad_cryo_heat": ("qss/1.0D6", "MW"), + "condrad_cryo_heat": ("qss/1.0d6", "MW"), } IO_mappings = { "A": ("aspect", "dimensionless"), + "tau_flattop": (("tbrnmn", "tburn"), "s"), + "P_el_net": (("pnetelin", "pnetelmw"), "MW"), "tk_bb_ib": ("blnkith", "m"), "tk_bb_ob": ("blnkoth", "m"), - "tk_sh_in": ("shldith", "m"), "tk_vv_in": ("d_vv_in", "m"), "tk_sol_ib": ("scrapli", "m"), "tk_sol_ob": ("scraplo", "m"), - "tk_ts": ("thshield", "m"), "g_cs_tf": ("gapoh", "m"), "g_ts_tf": ("tftsgap", "m"), "g_vv_bb": ("vvblgap", "m"), } NONE_mappings = { - "tau_flattop": ("tburn", "s"), "B_tf_peak": ("bmaxtfrp", "T"), - "q_95": ("q95", "dimensionless"), "T_e": ("te", "keV"), "Z_eff": ("zeff", "amu"), "V_p": ("vol", "m^3"), "l_i": ("rli", "dimensionless"), "f_ni": ("faccd", "dimensionless"), "tk_tf_outboard": ("tfthko", "m"), - "sigma_tf_case_max": ("sig_tf_case_max", "Pa"), - "sigma_tf_wp_max": ("sig_tf_wp_max", "Pa"), "h_cp_top": ("h_cp_top", "m"), "h_tf_max_in": ("hmax", "m"), "r_tf_inboard_out": ("r_tf_inboard_out", "m"), + # The following mappings are not 1:1 + "tk_sh_in": ("shldith", "m"), + "tk_sh_out": ("shldoth", "m"), + "tk_sh_top": ("shldtth", "m"), + "tk_sh_bot": ("shldlth", "m"), + "tk_vv_out": ("d_vv_out", "m"), + "tk_vv_top": ("d_vv_top", "m"), + "tk_vv_bot": ("d_vv_bot", "m"), + # Thermal shield thickness is a constant for us + "tk_ts": ("thshield_ib", "m"), + # "tk_ts": ("thshield_ob", "m"), + # "tk_ts": ("thshield_vb", "m"), + # TODO: q is not properly put in the MFILE output + # This should be ok OK most of the time as q_95 is input and then + # used as the lower bound of the q iteration variable, but this + # should be fixed as soon as PROCESS deal with this issue on + # their side + "q_95": ("q", "dimensionless"), } mappings = create_mapping(IN_mappings, OUT_mappings, IO_mappings, NONE_mappings) diff --git a/bluemira/codes/process/params.py b/bluemira/codes/process/params.py index a6054448fc..0fc56afe78 100644 --- a/bluemira/codes/process/params.py +++ b/bluemira/codes/process/params.py @@ -22,17 +22,21 @@ """ PROCESS's parameter definitions. """ +from __future__ import annotations from copy import deepcopy from dataclasses import dataclass -from typing import ClassVar, Dict, List, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union -from bluemira.base.parameter_frame import Parameter +from bluemira.base.parameter_frame import Parameter # noqa: TCH001 from bluemira.codes.params import MappedParameterFrame, ParameterMapping from bluemira.codes.process._inputs import ProcessInputs -from bluemira.codes.process.api import _INVariable +from bluemira.codes.process.constants import NAME from bluemira.codes.process.mapping import mappings +if TYPE_CHECKING: + from bluemira.codes.process.api import _INVariable + @dataclass class ProcessSolverParams(MappedParameterFrame): @@ -60,6 +64,9 @@ class ProcessSolverParams(MappedParameterFrame): P_el_net: Parameter[float] """Net electrical power output [megawatt].""" + tau_flattop: Parameter[float] + """Flat-top duration [second].""" + P_hcd_ss: Parameter[float] """Steady-state HCD power [megawatt].""" @@ -96,6 +103,48 @@ class ProcessSolverParams(MappedParameterFrame): PsepB_qAR_max: Parameter[float] """Maximum PsepB/q95AR vale [MW.T/m]""" + q_0: Parameter[float] + """Plasma safety factor on axis [dimensionless]""" + + q_95: Parameter[float] + """Plasma safety factor at the 95th percentile flux surface [dimensionless]""" + + m_s_limit: Parameter[float] + """Margin to vertical stability [dimensionless]""" + + delta: Parameter[float] + """Triangularity [dimensionless]""" + + sigma_tf_case_max: Parameter[float] + """Maximum von Mises stress in the TF coil case nose [pascal].""" + + sigma_tf_wp_max: Parameter[float] + """Maximum von Mises stress in the TF coil winding pack [pascal].""" + + sigma_cs_wp_max: Parameter[float] + """Maximum von Mises stress in the CS coil winding pack [pascal].""" + + H_star: Parameter[float] + """H factor (radiation corrected) [dimensionless].""" + + bb_pump_eta_el: Parameter[float] + """Breeding blanket pumping electrical efficiency [dimensionless]""" + + bb_pump_eta_isen: Parameter[float] + """Breeding blanket pumping isentropic efficiency [dimensionless]""" + + bb_t_inlet: Parameter[float] + """Breeding blanket inlet temperature [K]""" + + bb_t_outlet: Parameter[float] + """Breeding blanket outlet temperature [K]""" + + eta_ecrh: Parameter[float] + """Electron cyclotron resonce heating wallplug efficiency [dimensionless]""" + + gamma_ecrh: Parameter[float] + """Electron cyclotron resonce heating current drive efficiency [TODO: UNITS!]""" + # Out parameters B_0: Parameter[float] """Toroidal field at R_0 [tesla].""" @@ -112,7 +161,6 @@ class ProcessSolverParams(MappedParameterFrame): delta_95: Parameter[float] """95th percentile plasma triangularity [dimensionless].""" - delta: Parameter[float] """Last closed surface plasma triangularity [dimensionless].""" f_bs: Parameter[float] @@ -121,9 +169,6 @@ class ProcessSolverParams(MappedParameterFrame): g_vv_ts: Parameter[float] """Gap between VV and TS [meter].""" - H_star: Parameter[float] - """H factor (radiation corrected) [dimensionless].""" - I_p: Parameter[float] """Plasma current [megaampere].""" @@ -139,9 +184,6 @@ class ProcessSolverParams(MappedParameterFrame): P_brehms: Parameter[float] """Bremsstrahlung [megawatt].""" - P_el_net_process: Parameter[float] - """Net electrical power output as provided by PROCESS [megawatt].""" - P_fus_DD: Parameter[float] """D-D fusion power [megawatt].""" @@ -303,24 +345,12 @@ class ProcessSolverParams(MappedParameterFrame): l_i: Parameter[float] """Normalised internal plasma inductance [dimensionless].""" - q_95: Parameter[float] - """Plasma safety factor [dimensionless].""" - r_tf_inboard_out: Parameter[float] """Outboard Radius of the TF coil inboard leg tapered region [meter].""" - sigma_tf_case_max: Parameter[float] - """Maximum von Mises stress in the TF coil case nose [pascal].""" - - sigma_tf_wp_max: Parameter[float] - """Maximum von Mises stress in the TF coil winding pack nose [pascal].""" - T_e: Parameter[float] """Average plasma electron temperature [kiloelectron_volt].""" - tau_flattop: Parameter[float] - """Flat-top duration [second].""" - tk_tf_outboard: Parameter[float] """TF coil outboard thickness [meter].""" @@ -330,8 +360,19 @@ class ProcessSolverParams(MappedParameterFrame): Z_eff: Parameter[float] """Effective particle radiation atomic mass [unified_atomic_mass_unit].""" - _mappings: ClassVar = deepcopy(mappings) - _defaults = ProcessInputs() + _mappings = deepcopy(mappings) + + @property + def _defaults(self): + try: + return self.__defaults + except AttributeError: + self.__defaults = ProcessInputs() + return self.__defaults + + @_defaults.setter + def _defaults(self, value: ProcessInputs): + self.__defaults = value @property def mappings(self) -> Dict[str, ParameterMapping]: @@ -353,8 +394,18 @@ def template_defaults(self) -> Dict[str, _INVariable]: return self._defaults.to_invariable() @classmethod - def from_defaults(cls) -> MappedParameterFrame: + def from_defaults( + cls, template: Optional[ProcessInputs] = None + ) -> ProcessSolverParams: """ Initialise from defaults """ - return super().from_defaults(cls._defaults.to_dict()) + if template is None: + template = ProcessInputs() + self = super().from_defaults(template.to_dict()) + else: + self = super().from_defaults( + template.to_dict(), source=f"{NAME} user input template" + ) + self.__defaults = template + return self diff --git a/bluemira/codes/process/template_builder.py b/bluemira/codes/process/template_builder.py new file mode 100644 index 0000000000..76db50ed78 --- /dev/null +++ b/bluemira/codes/process/template_builder.py @@ -0,0 +1,321 @@ +# bluemira is an integrated inter-disciplinary design tool for future fusion +# reactors. It incorporates several modules, some of which rely on other +# codes, to carry out a range of typical conceptual fusion reactor design +# activities. +# +# Copyright (C) 2021-2023 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh, +# J. Morris, D. Short +# +# bluemira is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# bluemira is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with bluemira; if not, see . + +""" +PROCESS IN.DAT template builder +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union + +if TYPE_CHECKING: + from bluemira.codes.process._equation_variable_mapping import ( + Constraint, + ConstraintSelection, + Objective, + ) + from bluemira.codes.process._model_mapping import ( + PROCESSModel, + PROCESSOptimisationAlgorithm, + ) + +from bluemira.base.look_and_feel import bluemira_warn +from bluemira.codes.process._equation_variable_mapping import ( + FV_CONSTRAINT_ITVAR_MAPPING, + ITERATION_VAR_MAPPING, + OBJECTIVE_MIN_ONLY, + VAR_ITERATION_MAPPING, +) +from bluemira.codes.process._inputs import ProcessInputs +from bluemira.codes.process.api import Impurities + + +class PROCESSTemplateBuilder: + """ + An API patch to make PROCESS a little easier to work with before + the PROCESS team write a Python API. + """ + + def __init__(self): + self._models: Dict[str, PROCESSModel] = {} + self._constraints: List[Constraint] = [] + self.values: Dict[str, Any] = {} + self.variables: Dict[str, float] = {} + self.bounds: Dict[str, Dict[str, str]] = {} + self.ixc: List[int] = [] + self.fimp: List[float] = 14 * [0.0] + + self.minmax: int = 0 + self.ioptimiz: int = 0 + self.maxcal: int = 1000 + self.epsvmc: float = 1.0e-8 + + def set_run_title(self, run_title: str): + """ + Set the run title + """ + self.values["runtitle"] = run_title + + def set_optimisation_algorithm(self, algorithm_choice: PROCESSOptimisationAlgorithm): + """ + Set the optimisation algorithm to use + """ + self.ioptimiz = algorithm_choice.value + + def set_optimisation_numerics( + self, max_iterations: int = 1000, tolerance: float = 1e-8 + ): + """ + Set optimisation numerics + """ + self.maxcal = max_iterations + self.epsvmc = tolerance + + def set_minimisation_objective(self, objective: Objective): + """ + Set the minimisation objective equation to use when running PROCESS + """ + self.minmax = objective.value + + def set_maximisation_objective(self, objective: Objective): + """ + Set the maximisation objective equation to use when running PROCESS + """ + minmax = objective.value + if minmax in OBJECTIVE_MIN_ONLY: + raise ValueError( + f"Equation {objective} can only be used as a minimisation objective." + ) + self.minmax = -minmax + + def set_model(self, model_choice: PROCESSModel): + """ + Set a model switch to the PROCESS run + """ + if model_choice.switch_name in self._models: + bluemira_warn(f"Over-writing model choice {model_choice}.") + self._models[model_choice.switch_name] = model_choice + + def add_constraint(self, constraint: Constraint): + """ + Add a constraint to the PROCESS run + """ + if constraint in self._constraints: + bluemira_warn( + f"Constraint {constraint.name} is already in the constraint list." + ) + + if constraint.value in FV_CONSTRAINT_ITVAR_MAPPING: + # Sensible (?) defaults. bounds are standard PROCESS for f-values for _most_ + # f-value constraints. + self.add_fvalue_constraint(constraint, None, None, None) + else: + self._constraints.append(constraint) + + def add_fvalue_constraint( + self, + constraint: Constraint, + value: Optional[float] = None, + lower_bound: Optional[float] = None, + upper_bound: Optional[float] = None, + ): + """ + Add an f-value constraint to the PROCESS run + """ + if constraint.value not in FV_CONSTRAINT_ITVAR_MAPPING: + raise ValueError( + f"Constraint '{constraint.name}' is not an f-value constraint." + ) + self._constraints.append(constraint) + + itvar = FV_CONSTRAINT_ITVAR_MAPPING[constraint.value] + if itvar not in self.ixc: + self.add_variable( + VAR_ITERATION_MAPPING[itvar], value, lower_bound, upper_bound + ) + + def add_variable( + self, + name: str, + value: Optional[float] = None, + lower_bound: Optional[float] = None, + upper_bound: Optional[float] = None, + ): + """ + Add an iteration variable to the PROCESS run + """ + itvar = ITERATION_VAR_MAPPING.get(name, None) + if not itvar: + raise ValueError(f"There is no iteration variable: '{name}'") + + if itvar in self.ixc: + bluemira_warn( + f"Iteration variable '{name}' is already in the variable list." + " Updating value and bounds." + ) + self.adjust_variable(name, value, lower_bound, upper_bound) + + else: + self.ixc.append(itvar) + self._add_to_dict(self.variables, name, value) + + if lower_bound or upper_bound: + var_bounds = {} + if lower_bound: + var_bounds["l"] = str(lower_bound) + if upper_bound: + var_bounds["u"] = str(upper_bound) + if var_bounds: + self.bounds[str(itvar)] = var_bounds + + def adjust_variable( + self, + name: str, + value: Optional[float] = None, + lower_bound: Optional[float] = None, + upper_bound: Optional[float] = None, + ): + """ + Adjust an iteration variable in the PROCESS run + """ + itvar = ITERATION_VAR_MAPPING.get(name, None) + if not itvar: + raise ValueError(f"There is no iteration variable: '{name}'") + if itvar not in self.ixc: + bluemira_warn( + f"Iteration variable '{name}' is not in the variable list. Adding it." + ) + self.add_variable(name, value, lower_bound, upper_bound) + else: + self._add_to_dict(self.variables, name, value) + if (lower_bound or upper_bound) and str(itvar) not in self.bounds: + self.bounds[str(itvar)] = {} + + if lower_bound: + self.bounds[str(itvar)]["l"] = str(lower_bound) + if upper_bound: + self.bounds[str(itvar)]["u"] = str(upper_bound) + + def add_input_value(self, name: str, value: Union[float, Iterable[float]]): + """ + Add a fixed input value to the PROCESS run + """ + if name in self.values: + bluemira_warn(f"Over-writing {name} from {self.values[name]} to {value}") + self._add_to_dict(self.values, name, value) + + def add_input_values(self, mapping: Dict[str, Any]): + """ + Add a dictionary of fixed input values to the PROCESS run + """ + for name, value in mapping.items(): + self.add_input_value(name, value) + + def add_impurity(self, impurity: Impurities, value: float): + """ + Add an impurity concentration + """ + idx = impurity.value - 1 + self.fimp[idx] = value + + def _add_to_dict(self, mapping: Dict[str, Any], name: str, value: Any): + if "fimp(" in name: + num = int(name.strip("fimp(")[:2]) + impurity = Impurities(num) + self.add_impurity(impurity, value) + else: + mapping[name] = value + + def _check_model_inputs(self): + """ + Check the required inputs for models have been provided. + """ + for model in self._models.values(): + self._check_missing_inputs(model) + + def _check_constraint_inputs(self): + """ + Check the required inputs for the constraints have been provided + """ + for constraint in self._constraints: + self._check_missing_iteration_variables(constraint) + self._check_missing_inputs(constraint) + + def _check_missing_inputs(self, model: Union[PROCESSModel, ConstraintSelection]): + missing_inputs = [ + input_name + for input_name in model.requires_values + if (input_name not in self.values and input_name not in self.variables) + ] + + if missing_inputs: + model_name = f"{model.__class__.__name__}.{model.name}" + inputs = ", ".join([f"'{inp}'" for inp in missing_inputs]) + bluemira_warn( + f"{model_name} requires inputs {inputs} which have not been specified." + " Default values will be used." + ) + + def _check_missing_iteration_variables(self, constraint: ConstraintSelection): + missing_itv = [ + VAR_ITERATION_MAPPING[itv_num] + for itv_num in constraint.requires_variables + if ( + VAR_ITERATION_MAPPING[itv_num] not in self.variables + and VAR_ITERATION_MAPPING[itv_num] not in self.values + ) + ] + if missing_itv: + con_name = f"{constraint.__class__.__name__}.{constraint.name}" + inputs = ", ".join([f"'{inp}'" for inp in missing_itv]) + bluemira_warn( + f"{con_name} requires iteration variable {inputs} " + "which have not been specified. Default values will be used." + ) + + def make_inputs(self) -> ProcessInputs: + """ + Make the ProcessInputs InVariable for the specified template + """ + if self.ioptimiz != 0 and self.minmax == 0: + bluemira_warn( + "You are running in optimisation mode," + " but have not set an objective function." + ) + + self._check_constraint_inputs() + icc = [con.value for con in self._constraints] + self._check_model_inputs() + models = {k: v.value for k, v in self._models.items()} + + return ProcessInputs( + bounds=self.bounds, + icc=icc, + ixc=self.ixc, + minmax=self.minmax, + ioptimz=self.ioptimiz, + epsvmc=self.epsvmc, + maxcal=self.maxcal, + fimp=self.fimp, + **self.values, + **models, + **self.variables, + ) diff --git a/bluemira/codes/utilities.py b/bluemira/codes/utilities.py index 97ab35e6f6..1026158fb2 100644 --- a/bluemira/codes/utilities.py +++ b/bluemira/codes/utilities.py @@ -29,7 +29,7 @@ import threading from enum import Enum from types import ModuleType -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List from bluemira.base.look_and_feel import ( _bluemira_clean_flush, @@ -113,8 +113,14 @@ def create_mapping( ]: if puts is not None: for bm_key, (ec_key, unit) in puts.items(): + if isinstance(ec_key, tuple): + ec_in = ec_key[0] + ec_out = ec_key[1] + else: + ec_in = ec_out = ec_key + mappings[bm_key] = ParameterMapping( - ec_key, send=sr["send"], recv=sr["recv"], unit=unit + ec_in, ec_out, send=sr["send"], recv=sr["recv"], unit=unit ) return mappings @@ -133,13 +139,18 @@ class LogPipe(threading.Thread): """ - def __init__(self, loglevel: str): + def __init__( + self, + loglevel: str, + flush_callable: Callable[[str], bool] = lambda line: False, # noqa: ARG005 + ): super().__init__(daemon=True) self.logfunc = {"print": bluemira_print_clean, "error": bluemira_error_clean}[ loglevel ] self.logfunc_flush = _bluemira_clean_flush + self.flush_callable = flush_callable self.fd_read, self.fd_write = os.pipe() self.pipe = os.fdopen(self.fd_read, encoding="utf-8", errors="ignore") self.start() @@ -155,7 +166,7 @@ def run(self): Run the thread and pipe it all into the logger. """ for line in iter(self.pipe.readline, ""): - if line.startswith("==>"): + if self.flush_callable(line): self.logfunc_flush(line.strip("\n")) else: self.logfunc(line) @@ -169,7 +180,12 @@ def close(self): os.close(self.fd_write) -def run_subprocess(command: List[str], run_directory: str = ".", **kwargs) -> int: +def run_subprocess( + command: List[str], + run_directory: str = ".", + flush_callable: Callable[[str], bool] = lambda line: False, # noqa: ARG005 + **kwargs, +) -> int: """ Run a subprocess terminal command piping the output into bluemira's logs. @@ -189,8 +205,8 @@ def run_subprocess(command: List[str], run_directory: str = ".", **kwargs) -> in return_code: int The return code of the subprocess. """ - stdout = LogPipe("print") - stderr = LogPipe("error") + stdout = LogPipe("print", flush_callable) + stderr = LogPipe("error", flush_callable) kwargs["cwd"] = run_directory kwargs.pop("shell", None) # Protect against user input diff --git a/bluemira/equilibria/coils/_field.py b/bluemira/equilibria/coils/_field.py index f1e2ff8d24..9c8da5e668 100644 --- a/bluemira/equilibria/coils/_field.py +++ b/bluemira/equilibria/coils/_field.py @@ -632,8 +632,14 @@ def control_F(self, coil: CoilGroup) -> np.ndarray: same_pos = np.nonzero(xw == zw)[0] if same_pos.size > 0: # self inductance - xxw = xw[same_pos] - cr = self._current_radius[xxw] + # same_pos could be an array that is indexed from zw. + # This loops over zw and creates an index in xw where xw == zw + # better ways welcome! + xxw = [] + for _z in zw: + if (_pos := np.nonzero(_z == xw)[0]).size > 0: + xxw.extend(_pos) + cr = self._current_radius[np.array(xxw)] Bz = np.zeros((x.size, 1)) Bx = Bz.copy() # Should be 0 anyway mask = np.zeros_like(Bz, dtype=bool) diff --git a/bluemira/equilibria/plotting.py b/bluemira/equilibria/plotting.py index 9988f6acde..44e3b4206c 100644 --- a/bluemira/equilibria/plotting.py +++ b/bluemira/equilibria/plotting.py @@ -369,8 +369,13 @@ def _plot_coil(self, x_boundary, z_boundary, ctype, fill=True, **kwargs): x = np.append(x_boundary, x_boundary[0]) z = np.append(z_boundary, z_boundary[0]) + if all(x_boundary == x_boundary[0]) or all(z_boundary == z_boundary[0]): + self.ax.plot( + x[0], z[0], zorder=11, color="k", linewidth=linewidth, marker="+" + ) + else: + self.ax.plot(x, z, zorder=11, color=color, linewidth=linewidth) - self.ax.plot(x, z, zorder=11, color=color, linewidth=linewidth) if fill: if mask: self.ax.fill(x, z, color="w", zorder=10, alpha=1) diff --git a/bluemira/geometry/tools.py b/bluemira/geometry/tools.py index a44bae3d06..9c2666765c 100644 --- a/bluemira/geometry/tools.py +++ b/bluemira/geometry/tools.py @@ -598,6 +598,7 @@ def _offset_wire_discretised( label="", *, fallback_method="square", + fallback_force_spline=False, byedges=True, ndiscr=200, **fallback_kwargs, @@ -624,10 +625,16 @@ def _offset_wire_discretised( coordinates = wire.discretize(byedges=byedges, ndiscr=ndiscr) - result = offset_clipper( - coordinates, thickness, method=fallback_method, **fallback_kwargs + wire = make_polygon( + offset_clipper( + coordinates, thickness, method=fallback_method, **fallback_kwargs + ), + label=label, + closed=True, ) - return make_polygon(result, label=label, closed=True) + if fallback_force_spline: + return force_wire_to_spline(wire, n_edges_max=ndiscr) + return wire @fallback_to(_offset_wire_discretised, cadapi.FreeCADError) diff --git a/conda/environment.yml b/conda/environment.yml index 6a1fe22b3a..aaf52f8474 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -25,12 +25,12 @@ dependencies: - libpng=1.6.37 # malloc crash on 1.6.38 - hdf5=1.12.1 - netcdf4=1.6.0 - - numpy=1.22.4 + - numpy=1.23 - fenics=2019.1.0 - MeshPy=2020.1 - meshio=4.4.5 - mshr=2019.1.0 - - scipy=1.7.3 + - scipy=1.9 - graphviz - pip - pip: diff --git a/data/reactors/EU-DEMO/systems_code/mockPROCESS.json b/data/reactors/EU-DEMO/systems_code/mockPROCESS.json index 76869685cb..4114fdeb77 100644 --- a/data/reactors/EU-DEMO/systems_code/mockPROCESS.json +++ b/data/reactors/EU-DEMO/systems_code/mockPROCESS.json @@ -5,7 +5,7 @@ "H_star": 1.1, "I_p": 19.117, "P_brehms": 60.956, - "P_el_net_process": 500.0, + "P_el_net": 500.0, "P_fus": 1790.6, "P_fus_DD": 2.1206, "P_fus_DT": 1788.5, diff --git a/documentation/source/examples.rst b/documentation/source/examples.rst index d31a93e6b7..a5f8887435 100644 --- a/documentation/source/examples.rst +++ b/documentation/source/examples.rst @@ -63,6 +63,7 @@ External Code Examples examples/codes/external_code examples/codes/run_plasmod_example + examples/codes/run_process_example examples/codes/equilibrium_plasmod_example examples/codes/solver_example diff --git a/eudemo/config/build_config.json b/eudemo/config/build_config.json index dae712a6ad..dacd3aa4d5 100644 --- a/eudemo/config/build_config.json +++ b/eudemo/config/build_config.json @@ -4,7 +4,7 @@ "run_mode": "run", "read_dir": "config/", "run_dir": "config/", - "plot": false + "plot": true }, "Fixed boundary equilibrium": { "run_mode": "read", diff --git a/eudemo/config/mockPROCESS.json b/eudemo/config/mockPROCESS.json index 1a59577b43..ce1ae3ae06 100644 --- a/eudemo/config/mockPROCESS.json +++ b/eudemo/config/mockPROCESS.json @@ -8,7 +8,6 @@ "P_bd_in": 50.0, "P_brehms": 59.668, "P_el_net": 500, - "P_el_net_process": 500.0, "P_fus": 1994.6, "P_fus_DD": 2.4746, "P_fus_DT": 1992.1, diff --git a/eudemo/config/params.json b/eudemo/config/params.json index 07829d337c..9cc0a95976 100644 --- a/eudemo/config/params.json +++ b/eudemo/config/params.json @@ -24,7 +24,7 @@ "long_name": "Peak field inside the TF coil winding pack" }, "C_Ejima": { - "value": 0.4, + "value": 0.3, "unit": "dimensionless", "source": "Input (Ejima, et al., Volt-second analysis and consumption in Doublet III plasmas, Nuclear Fusion 22, 1313 (1982))", "long_name": "Ejima constant" @@ -59,12 +59,6 @@ "source": "Input", "long_name": "Net electrical power output" }, - "P_el_net_process": { - "value": 0.0, - "unit": "megawatt", - "source": "Input", - "long_name": "Net electrical power output as provided by PROCESS" - }, "P_fus": { "value": 2000, "unit": "megawatt", @@ -84,7 +78,7 @@ "long_name": "D-T fusion power" }, "P_hcd_ss": { - "value": 50, + "value": 51, "unit": "megawatt", "source": "Input", "long_name": "Steady-state HCD power" @@ -376,6 +370,12 @@ "source": "Input", "long_name": "95th percentile plasma elongation" }, + "m_s_limit": { + "value": 0.1, + "unit": "dimensionless", + "source": "Input", + "long_name": "Margin to vertical stability criterion" + }, "l_i": { "value": 0.8, "unit": "dimensionless", @@ -383,16 +383,22 @@ "long_name": "Normalised internal plasma inductance" }, "n_TF": { - "value": 18, + "value": 16, "unit": "dimensionless", "source": "Input", "long_name": "Number of TF coils" }, + "q_0": { + "value": 1.0, + "unit": "dimensionless", + "source": "Input", + "long_name": "Plasma safety factor on axis" + }, "q_95": { "value": 3.5, "unit": "dimensionless", "source": "Input", - "long_name": "Plasma safety factor" + "long_name": "Plasma safety factor at the 95th percentile flux surface" }, "r_cs_corner": { "value": 0, @@ -472,17 +478,23 @@ "source": "Input", "long_name": "Outboard vessel inner radius" }, + "sigma_cs_wp_max": { + "value": 660000000.0, + "unit": "pascal", + "source": "Input", + "long_name": "Maximum von Mises stress in the CS coil winding pack" + }, "sigma_tf_case_max": { - "value": 550000000.0, + "value": 580000000.0, "unit": "pascal", "source": "Input", "long_name": "Maximum von Mises stress in the TF coil case nose" }, "sigma_tf_wp_max": { - "value": 550000000.0, + "value": 580000000.0, "unit": "pascal", "source": "Input", - "long_name": "Maximum von Mises stress in the TF coil winding pack nose" + "long_name": "Maximum von Mises stress in the TF coil winding pack" }, "tau_e": { "value": 3, @@ -517,19 +529,19 @@ "long_name": "Minimum breeding blanket angle" }, "tk_bb_ib": { - "value": 0.8, + "value": 0.755, "unit": "meter", "source": "Input", "long_name": "Inboard blanket thickness" }, "tk_bb_ob": { - "value": 1.1, + "value": 0.982, "unit": "meter", "source": "Input", "long_name": "Outboard blanket thickness" }, "tk_cr_vv": { - "value": 0.3, + "value": 0.15, "unit": "meter", "source": "Input", "long_name": "Cryostat VV thickness" @@ -625,28 +637,28 @@ "long_name": "ITER-like TF gravity support base plate depth" }, "tk_sh_bot": { - "value": 1e-6, + "value": 0.0, "unit": "meter", "source": "Input", "long_name": "Lower shield thickness", "description": "DO NOT USE - PROCESS has VV = VV + shield" }, "tk_sh_in": { - "value": 1e-6, + "value": 0.0, "unit": "meter", "source": "Input", "long_name": "Inboard shield thickness", "description": "DO NOT USE - PROCESS has VV = VV + shield" }, "tk_sh_out": { - "value": 1e-6, + "value": 0.0, "unit": "meter", "source": "Input", "long_name": "Outboard shield thickness", "description": "DO NOT USE - PROCESS has VV = VV + shield" }, "tk_sh_top": { - "value": 1e-6, + "value": 0.0, "unit": "meter", "source": "Input", "long_name": "Upper shield thickness", @@ -665,7 +677,7 @@ "long_name": "Outboard SOL thickness" }, "tk_tf_front_ib": { - "value": 0.04, + "value": 0.06, "unit": "meter", "source": "Input", "long_name": "TF coil inboard steel front plasma-facing" @@ -702,7 +714,7 @@ "long_name": "TF coil outboard thickness" }, "tk_tf_side": { - "value": 0.1, + "value": 0.05, "unit": "meter", "source": "Input", "long_name": "TF coil inboard case minimum side wall thickness" @@ -778,7 +790,7 @@ "long_name": "Maximum peak field to use in CS modules" }, "CS_jmax": { - "value": 16, + "value": 16.5, "unit": "megaampere / meter ** 2", "source": "Input", "long_name": "Maximum current density to use in CS modules" @@ -832,7 +844,7 @@ "long_name": "Shafranov shift of plasma (geometric=>magnetic)" }, "tk_cs_casing": { - "value": 0.07, + "value": 0.02, "unit": "meter", "source": "Input", "long_name": "Thickness of the CS coil casing" @@ -1021,5 +1033,17 @@ "value": 150, "unit": "MW", "long_name": "Steady-state heating and current drive electrical power" + }, + "eta_ecrh": { + "value": 0.4, + "unit": "dimensionless", + "source": "Input", + "long_name": "Electron cyclotron resonce heating wallplug efficiency" + }, + "gamma_ecrh": { + "value": 0.3e20, + "unit": "A/W/m^2", + "source": "Input", + "long_name": "Electron cyclotron resonce heating current drive efficiency" } } diff --git a/eudemo/eudemo/params.py b/eudemo/eudemo/params.py index c5be839128..c32065a9d3 100644 --- a/eudemo/eudemo/params.py +++ b/eudemo/eudemo/params.py @@ -64,6 +64,7 @@ class EUDEMOReactorParams(ParameterFrame): ib_offset_angle: Parameter[float] kappa_95: Parameter[float] kappa: Parameter[float] + m_s_limit: Parameter[float] l_i: Parameter[float] n_CS: Parameter[int] n_PF: Parameter[int] @@ -71,7 +72,6 @@ class EUDEMOReactorParams(ParameterFrame): ob_offset_angle: Parameter[float] P_bd_in: Parameter[float] P_brehms: Parameter[float] - P_el_net_process: Parameter[float] P_el_net: Parameter[float] P_fus_DD: Parameter[float] P_fus_DT: Parameter[float] @@ -85,6 +85,7 @@ class EUDEMOReactorParams(ParameterFrame): P_sync: Parameter[float] PF_bmax: Parameter[float] PF_jmax: Parameter[float] + q_0: Parameter[float] q_95: Parameter[float] R_0: Parameter[float] r_cs_corner: Parameter[float] @@ -100,6 +101,7 @@ class EUDEMOReactorParams(ParameterFrame): r_ts_ib_in: Parameter[float] r_vv_ib_in: Parameter[float] r_vv_ob_in: Parameter[float] + sigma_cs_wp_max: Parameter[float] sigma_tf_case_max: Parameter[float] sigma_tf_wp_max: Parameter[float] T_e: Parameter[float] @@ -151,6 +153,10 @@ class EUDEMOReactorParams(ParameterFrame): T_e_ped: Parameter[float] q_control: Parameter[float] + # Heating and current drive + eta_ecrh: Parameter[float] + gamma_ecrh: Parameter[float] + # Equilibrium div_L2D_ib: Parameter[float] div_L2D_ob: Parameter[float] @@ -164,7 +170,7 @@ class EUDEMOReactorParams(ParameterFrame): div_Ltarg: Parameter[float] # noqa: N815 div_open: Parameter[bool] - # Plasma face + # Remote maintenance c_rm: Parameter[float] # Vacuum vessel diff --git a/eudemo/eudemo/pf_coils/tools.py b/eudemo/eudemo/pf_coils/tools.py index c814839ce7..720eeb87ec 100644 --- a/eudemo/eudemo/pf_coils/tools.py +++ b/eudemo/eudemo/pf_coils/tools.py @@ -204,7 +204,10 @@ def make_coilset( for s in solenoid: s.fix_size() - tf_track = offset_wire(tf_boundary, 1) + tf_track = offset_wire( + tf_boundary, 1, fallback_method="miter", fallback_force_spline=True + ) + x_c, z_c = make_PF_coil_positions( tf_track, n_PF, @@ -329,12 +332,10 @@ def make_coil_mapper( if len(_bin) < 1: bluemira_warn("There is a segment of the track which has no coils on it.") elif len(_bin) == 1: - coil = _bin[0] - interpolator_dict[coil.name] = PathInterpolator(segment) + interpolator_dict[_bin[0].name] = PathInterpolator(segment) else: - coils = _bin l_values = np.array( - [segment.parameter_at([c.x, 0, c.z], tolerance=VERY_BIG) for c in coils] + [segment.parameter_at([c.x, 0, c.z], tolerance=VERY_BIG) for c in _bin] ) idx = np.argsort(l_values) l_values = l_values[idx] @@ -344,7 +345,7 @@ def make_coil_mapper( sub_segs = _split_segment(segment, split_positions) # Sorted coils - for coil, sub_seg in zip([coils[i] for i in idx], sub_segs): + for coil, sub_seg in zip([_bin[i] for i in idx], sub_segs): interpolator_dict[coil.name] = PathInterpolator(sub_seg) return PositionMapper(interpolator_dict) @@ -383,7 +384,9 @@ def make_pf_coil_path(tf_boundary: BluemiraWire, offset_value: float) -> Bluemir ------- Path along which the PF coil centroids should be positioned """ - tf_offset = offset_wire(tf_boundary, offset_value) + tf_offset = offset_wire( + tf_boundary, offset_value, fallback_method="miter", fallback_force_spline=True + ) # Find top-left and bottom-left "corners" coordinates = tf_offset.discretize(byedges=True, ndiscr=200) diff --git a/eudemo/eudemo/radial_build.py b/eudemo/eudemo/radial_build.py index bb10cbd417..7960423eb4 100644 --- a/eudemo/eudemo/radial_build.py +++ b/eudemo/eudemo/radial_build.py @@ -24,10 +24,344 @@ from bluemira.base.parameter_frame import ParameterFrame from bluemira.codes import plot_radial_build, systems_code_solver +from bluemira.codes.process._equation_variable_mapping import Constraint, Objective +from bluemira.codes.process._model_mapping import ( + AlphaPressureModel, + AvailabilityModel, + BetaLimitModel, + BootstrapCurrentScalingLaw, + CSSuperconductorModel, + ConfinementTimeScalingLaw, + CostModel, + CurrentDriveEfficiencyModel, + DensityLimitModel, + EPEDScalingModel, + FISPACTSwitchModel, + OperationModel, + OutputCostsSwitch, + PFSuperconductorModel, + PROCESSOptimisationAlgorithm, + PlasmaCurrentScalingLaw, + PlasmaGeometryModel, + PlasmaNullConfigurationModel, + PlasmaPedestalModel, + PlasmaProfileModel, + PowerFlowModel, + PrimaryPumpingModel, + SecondaryCycleModel, + ShieldThermalHeatUse, + SolenoidSwitchModel, + TFNuclearHeatingModel, + TFSuperconductorModel, + TFWindingPackTurnModel, +) +from bluemira.codes.process.api import Impurities +from bluemira.codes.process.template_builder import PROCESSTemplateBuilder _PfT = TypeVar("_PfT", bound=ParameterFrame) +template_builder = PROCESSTemplateBuilder() +template_builder.set_optimisation_algorithm(PROCESSOptimisationAlgorithm.VMCON) +template_builder.set_optimisation_numerics(max_iterations=1000, tolerance=1e-8) + +template_builder.set_minimisation_objective(Objective.MAJOR_RADIUS) + +for constraint in ( + Constraint.BETA_CONSISTENCY, + Constraint.GLOBAL_POWER_CONSISTENCY, + Constraint.DENSITY_UPPER_LIMIT, + Constraint.NWL_UPPER_LIMIT, + Constraint.RADIAL_BUILD_CONSISTENCY, + Constraint.BURN_TIME_LOWER_LIMIT, + Constraint.LH_THRESHHOLD_LIMIT, + Constraint.NET_ELEC_LOWER_LIMIT, + Constraint.BETA_UPPER_LIMIT, + Constraint.CS_EOF_DENSITY_LIMIT, + Constraint.CS_BOP_DENSITY_LIMIT, + Constraint.PINJ_UPPER_LIMIT, + Constraint.TF_CASE_STRESS_UPPER_LIMIT, + Constraint.TF_JACKET_STRESS_UPPER_LIMIT, + Constraint.TF_JCRIT_RATIO_UPPER_LIMIT, + Constraint.TF_DUMP_VOLTAGE_UPPER_LIMIT, + Constraint.TF_CURRENT_DENSITY_UPPER_LIMIT, + Constraint.TF_T_MARGIN_LOWER_LIMIT, + Constraint.CS_T_MARGIN_LOWER_LIMIT, + Constraint.CONFINEMENT_RATIO_LOWER_LIMIT, + Constraint.DUMP_TIME_LOWER_LIMIT, + Constraint.PSEPB_QAR_UPPER_LIMIT, + Constraint.CS_STRESS_UPPER_LIMIT, + Constraint.DENSITY_PROFILE_CONSISTENCY, + Constraint.CS_FATIGUE, +): + template_builder.add_constraint(constraint) + +# Variable vector values and bounds +template_builder.add_variable("bt", 5.3292, upper_bound=20.0) +template_builder.add_variable("rmajor", 9.2901, upper_bound=13.0) +template_builder.add_variable("te", 12.33, upper_bound=150.0) +template_builder.add_variable("beta", 3.4421e-2) +template_builder.add_variable("dene", 7.4321e19) +template_builder.add_variable("q", 3.5, lower_bound=3.5) +template_builder.add_variable("pheat", 50.0) +template_builder.add_variable("ralpne", 6.8940e-02) +template_builder.add_variable("bore", 2.3322, lower_bound=0.1) +template_builder.add_variable("ohcth", 0.55242, lower_bound=0.1) +template_builder.add_variable("thwcndut", 8.0e-3, lower_bound=8.0e-3) +template_builder.add_variable("thkcas", 0.52465) +template_builder.add_variable("tfcth", 1.2080) +template_builder.add_variable("gapoh", 0.05, lower_bound=0.05, upper_bound=0.1) +template_builder.add_variable("gapds", 0.02, lower_bound=0.02) +template_builder.add_variable("oh_steel_frac", 0.57875) +template_builder.add_variable("coheof", 2.0726e07) +template_builder.add_variable("cpttf", 6.5e4, lower_bound=6.0e4, upper_bound=9.0e4) +template_builder.add_variable("tdmptf", 2.5829e01) +template_builder.add_variable("vdalw", 10.0, upper_bound=10.0) +template_builder.add_variable("fimp(13)", 3.573e-04) + +# Some constraints require multiple f-values, but they are getting ridding of those, +# so no fancy mechanics for now... +template_builder.add_variable("fcutfsu", 0.80884, lower_bound=0.5, upper_bound=0.94) +template_builder.add_variable("fcohbop", 0.93176) +template_builder.add_variable("fvsbrnni", 0.39566) +template_builder.add_variable("fncycle", 1.0) +# template_builder.add_variable("feffcd", 1.0, lower_bound=0.001, upper_bound=1.0) + +# Modified f-values and bounds w.r.t. defaults +template_builder.adjust_variable("fne0", 0.6, upper_bound=0.95) +template_builder.adjust_variable("fdene", 1.2, upper_bound=1.2) +template_builder.adjust_variable("flhthresh", 1.2, lower_bound=1.1, upper_bound=1.2) +template_builder.adjust_variable("ftburn", 1.0, upper_bound=1.0) + +# Modifying the initial variable vector to improve convergence +template_builder.adjust_variable("fpnetel", 1.0) +template_builder.adjust_variable("fstrcase", 1.0) +template_builder.adjust_variable("ftmargtf", 1.0) +template_builder.adjust_variable("ftmargoh", 1.0) +template_builder.adjust_variable("ftaulimit", 1.0) +template_builder.adjust_variable("fjohc", 0.57941, upper_bound=1.0) +template_builder.adjust_variable("fjohc0", 0.53923, upper_bound=1.0) +template_builder.adjust_variable("foh_stress", 1.0) +template_builder.adjust_variable("fbetatry", 0.48251) +template_builder.adjust_variable("fwalld", 0.131) +template_builder.adjust_variable("fmaxvvstress", 1.0) +template_builder.adjust_variable("fpsepbqar", 1.0) +template_builder.adjust_variable("fvdump", 1.0) +template_builder.adjust_variable("fstrcond", 0.92007) +template_builder.adjust_variable("fiooic", 0.63437, upper_bound=1.0) +template_builder.adjust_variable("fjprot", 1.0) + +# Set model switches +for model_choice in ( + BootstrapCurrentScalingLaw.SAUTER, + ConfinementTimeScalingLaw.IPB98_Y2_H_MODE, + PlasmaCurrentScalingLaw.ITER_REVISED, + PlasmaProfileModel.CONSISTENT, + PlasmaPedestalModel.PEDESTAL_GW, + PlasmaNullConfigurationModel.SINGLE_NULL, + EPEDScalingModel.SAARELMA, + BetaLimitModel.THERMAL, + DensityLimitModel.GREENWALD, + AlphaPressureModel.WARD, + PlasmaGeometryModel.CREATE_A_M_S, + PowerFlowModel.SIMPLE, + ShieldThermalHeatUse.LOW_GRADE_HEAT, + SecondaryCycleModel.INPUT, + CurrentDriveEfficiencyModel.ECRH_UI_GAM, + OperationModel.PULSED, + PFSuperconductorModel.NBTI, + SolenoidSwitchModel.SOLENOID, + CSSuperconductorModel.NB3SN_WST, + TFSuperconductorModel.NB3SN_WST, + TFWindingPackTurnModel.INTEGER_TURN, + FISPACTSwitchModel.OFF, + PrimaryPumpingModel.PRESSURE_DROP_INPUT, + TFNuclearHeatingModel.INPUT, + CostModel.TETRA_1990, + AvailabilityModel.INPUT, + OutputCostsSwitch.NO, +): + template_builder.set_model(model_choice) + +template_builder.add_impurity(Impurities.H, 1.0) +template_builder.add_impurity(Impurities.He, 0.1) +template_builder.add_impurity(Impurities.W, 5.0e-5) + +# Set fixed input values +template_builder.add_input_values( + { + # CS fatigue variables + "residual_sig_hoop": 150.0e6, + # "n_cycle_min": , + # "t_crack_radial": , + # "t_structural_radial": , + "t_crack_vertical": 0.649e-3, + "sf_vertical_crack": 1.0, + "sf_radial_crack": 1.0, + "sf_fast_fracture": 1.0, + "paris_coefficient": 3.86e-11, + "paris_power_law": 2.394, + "walker_coefficient": 0.5, + "fracture_toughness": 150.0, + # Undocumented danger stuff + "iblanket": 1, + "lsa": 2, + # Profile parameterisation inputs + "alphan": 1.0, + "alphat": 1.45, + "rhopedn": 0.94, + "rhopedt": 0.94, + "tbeta": 2.0, + "teped": 5.5, + "tesep": 0.1, + "fgwped": 0.85, + "neped": 0.678e20, + "nesep": 0.2e20, + "dnbeta": 3.0, + # Plasma impurity stuff + "coreradius": 0.75, + "coreradiationfraction": 0.6, + # Important stuff + "pnetelin": 500.0, + "tbrnmn": 7.2e3, + "sig_tf_case_max": 5.8e8, + "sig_tf_wp_max": 5.8e8, + "alstroh": 6.6e8, + "psepbqarmax": 9.2, + "aspect": 3.1, + "m_s_limit": 0.1, + "triang": 0.5, + "q0": 1.0, + "ssync": 0.6, + "plasma_res_factor": 0.66, + "gamma": 0.3, + "hfact": 1.1, + "life_dpa": 70.0, + # Radial build inputs + "tftsgap": 0.05, + "vvblgap": 0.02, + "blnkith": 0.755, + "scrapli": 0.225, + "scraplo": 0.225, + "blnkoth": 0.982, + "ddwex": 0.15, + "gapomin": 0.2, + # Vertical build inputs + "vgap2": 0.05, + "divfix": 0.621, + # HCD inputs + "pinjalw": 51.0, + "gamma_ecrh": 0.3, + "etaech": 0.4, + "bscfmax": 0.99, + # BOP inputs + "etath": 0.375, + "etahtp": 0.87, + "etaiso": 0.9, + "vfshld": 0.6, + "tdwell": 0.0, + "tramp": 500.0, + # CS / PF coil inputs + "fcuohsu": 0.7, + "ohhghf": 0.9, + "rpf2": -1.825, + "cptdin": [4.22e4, 4.22e4, 4.22e4, 4.22e4, 4.3e4, 4.3e4, 4.3e4, 4.3e4], + "ipfloc": [2, 2, 3, 3], + "ncls": [1, 1, 2, 2], + "ngrp": 4, + "rjconpf": [1.1e7, 1.1e7, 6.0e6, 6.0e6, 8.0e6, 8.0e6, 8.0e6, 8.0e6], + # TF coil inputs + "n_tf": 16, + "casthi": 0.06, + "casths": 0.05, + "ripmax": 0.6, + "dhecoil": 0.01, + "tftmp": 4.75, + "thicndut": 2.0e-3, + "tinstf": 0.008, + # "tfinsgap": 0.01, + "tmargmin": 1.5, + "vftf": 0.3, + "n_pancake": 20, + "n_layer": 10, + "qnuc": 1.292e4, + # Inputs we don't care about but must specify + "cfactr": 0.75, # Ha! + "kappa": 1.848, # Should be overwritten + "walalw": 8.0, # Should never get even close to this + "tlife": 40.0, + "abktflnc": 15.0, + "adivflnc": 20.0, + # For sanity... + "hldivlim": 10, + "ksic": 1.4, + "prn1": 0.4, + "zeffdiv": 3.5, + "bmxlim": 11.2, + "ffuspow": 1.0, + "fpeakb": 1.0, + "divdum": 1, + "ibkt_life": 1, + "fkzohm": 1.0245, + "iinvqd": 1, + "dintrt": 0.0, + "fcap0": 1.15, + "fcap0cp": 1.06, + "fcontng": 0.15, + "fcr0": 0.065, + "fkind": 1.0, + "ifueltyp": 1, + "discount_rate": 0.06, + "bkt_life_csf": 1, + "ucblvd": 280.0, + "ucdiv": 5e5, + "ucme": 3.0e8, + # Suspicous stuff + "zref": [3.6, 1.2, 1.0, 2.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + "fpinj": 1.0, + } +) + + +def apply_specific_interface_rules(params: _PfT): + """ + Apply specific rules for the interface between PROCESS and BLUEMIRA + that relate to the EU-DEMO design parameterisation + """ + # Apply q_95 as a boundary on the iteration vector rather than a fixed input + q_95_min = params.q_95.value + template_builder.adjust_variable("q", value=q_95_min, lower_bound=q_95_min) + + # Apply thermal shield thickness to all values in PROCESS + tk_ts = params.tk_ts.value + template_builder.add_input_values( + { + "thshield_ib": tk_ts, + "thshield_ob": tk_ts, + "thshield_vb": tk_ts, + } + ) + + # Apply the summation of "shield" and "VV" thicknesses in PROCESS + default_vv_tk = 0.3 + tk_vv_ib = params.tk_vv_in.value + tk_vv_ob = params.tk_vv_out.value + tk_sh_ib = tk_vv_ib - default_vv_tk + tk_sh_ob = tk_vv_ob - default_vv_tk + template_builder.add_input_values( + { + "shldith": tk_sh_ib, + "shldoth": tk_sh_ob, + "shldtth": tk_sh_ib, + "shldlth": tk_sh_ib, + "d_vv_in": default_vv_tk, + "d_vv_out": default_vv_tk, + "d_vv_top": default_vv_tk, + "d_vv_bot": default_vv_tk, + } + ) + + def radial_build(params: _PfT, build_config: Dict) -> _PfT: """ Update parameters after a radial build is run/read/mocked using PROCESS. @@ -45,10 +379,17 @@ def radial_build(params: _PfT, build_config: Dict) -> _PfT: """ run_mode = build_config.pop("run_mode", "mock") plot = build_config.pop("plot", False) + if run_mode == "run": + template_builder.set_run_title( + build_config.pop("PROCESS_runtitle", "Bluemira EUDEMO") + ) + apply_specific_interface_rules(params) + build_config["template_in_dat"] = template_builder.make_inputs() solver = systems_code_solver(params, build_config) new_params = solver.execute(run_mode) if plot: plot_radial_build(solver.read_directory) + params.update_from_frame(new_params) return params diff --git a/eudemo/eudemo_tests/template.json b/eudemo/eudemo_tests/template.json index 94cb7d7fb9..ffd771a437 100644 --- a/eudemo/eudemo_tests/template.json +++ b/eudemo/eudemo_tests/template.json @@ -233,21 +233,8 @@ "mapping": { "PROCESS": { "name": "pnetelin", + "out_name": "pnetelmw", "send": true, - "recv": false, - "unit": "MW" - } - } - }, - "P_el_net_process": { - "name": "Net electrical power output as provided by PROCESS", - "value": null, - "unit": "MW", - "source": "Input", - "mapping": { - "PROCESS": { - "name": "pnetelmw", - "send": false, "recv": true, "unit": "MW" } diff --git a/examples/codes/run_process_example.ex.py b/examples/codes/run_process_example.ex.py new file mode 100644 index 0000000000..39cbf2f400 --- /dev/null +++ b/examples/codes/run_process_example.ex.py @@ -0,0 +1,380 @@ +# --- +# jupyter: +# jupytext: +# cell_metadata_filter: tags,-all +# notebook_metadata_filter: -jupytext.text_representation.jupytext_version +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% tags=["remove-cell"] +# bluemira is an integrated inter-disciplinary design tool for future fusion +# reactors. It incorporates several modules, some of which rely on other +# codes, to carry out a range of typical conceptual fusion reactor design +# activities. +# +# Copyright (C) 2021-2023 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh, +# J. Morris, D. Short +# +# bluemira is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# bluemira is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with bluemira; if not, see . + +""" +Run PROCESS using the PROCESSTemplateBuilder +""" + +# %% [markdown] +# # Running PROCESS from "scratch" +# This example shows how to build a PROCESS template IN.DAT file + + +# %% +from bluemira.codes import systems_code_solver +from bluemira.codes.process._equation_variable_mapping import Constraint, Objective +from bluemira.codes.process._model_mapping import ( + AlphaPressureModel, + AvailabilityModel, + BetaLimitModel, + BootstrapCurrentScalingLaw, + CSSuperconductorModel, + ConfinementTimeScalingLaw, + CostModel, + CurrentDriveEfficiencyModel, + DensityLimitModel, + EPEDScalingModel, + FISPACTSwitchModel, + OperationModel, + OutputCostsSwitch, + PFSuperconductorModel, + PROCESSOptimisationAlgorithm, + PlasmaCurrentScalingLaw, + PlasmaGeometryModel, + PlasmaNullConfigurationModel, + PlasmaPedestalModel, + PlasmaProfileModel, + PowerFlowModel, + PrimaryPumpingModel, + SecondaryCycleModel, + ShieldThermalHeatUse, + SolenoidSwitchModel, + TFNuclearHeatingModel, + TFSuperconductorModel, + TFWindingPackTurnModel, +) +from bluemira.codes.process.api import Impurities +from bluemira.codes.process.template_builder import PROCESSTemplateBuilder + +# %%[markdown] +# First we are going to build a template using the :py:class:`PROCESSTemplateBuilder`, +# without interacting with any of PROCESS' integers. + +# %% + +template_builder = PROCESSTemplateBuilder() + + +# %%[markdown] +# Now we're going to specify which optimisation algorithm we want to use, and the +# number of iterations and tolerance. + +# %% +template_builder.set_run_title("Example that won't converge") +template_builder.set_optimisation_algorithm(PROCESSOptimisationAlgorithm.VMCON) +template_builder.set_optimisation_numerics(max_iterations=1000, tolerance=1e-8) + + +# %%[markdown] +# Let's select the optimisation objective as the major radius: + +# %% +template_builder.set_minimisation_objective(Objective.MAJOR_RADIUS) + +# %%[markdown] +# You can inspect what options are available by taking a look at the +# :py:class:`Objective` Enum. The options are hopefully self-explanatory. +# The values of the options correspond to the PROCESS integers. + +# %% +print("\n".join(str(o) for o in list(Objective))) + + +# %%[markdown] +# Now we will add a series of constraint equations to the PROCESS problem +# we wish to solve. You can read more about these constraints an what +# they mean in the PROCESS documentation + +# %% +for constraint in ( + Constraint.BETA_CONSISTENCY, + Constraint.GLOBAL_POWER_CONSISTENCY, + Constraint.DENSITY_UPPER_LIMIT, + Constraint.RADIAL_BUILD_CONSISTENCY, + Constraint.BURN_TIME_LOWER_LIMIT, + Constraint.LH_THRESHHOLD_LIMIT, + Constraint.NET_ELEC_LOWER_LIMIT, + Constraint.TF_CASE_STRESS_UPPER_LIMIT, + Constraint.TF_JACKET_STRESS_UPPER_LIMIT, + Constraint.TF_JCRIT_RATIO_UPPER_LIMIT, + Constraint.TF_CURRENT_DENSITY_UPPER_LIMIT, + Constraint.TF_T_MARGIN_LOWER_LIMIT, + Constraint.PSEPB_QAR_UPPER_LIMIT, +): + template_builder.add_constraint(constraint) + + +# %%[markdown] +# Many of these constraints require certain iteration variables to have been +# specified, or certain input values. The novice user can easily not be +# aware that this is the case, or simply forget to specify these. + +# The :py:class:`PROCESSTemplateBuilder` will warn the user if certain +# values have not been specified. For example, if we try to make a set of +# inputs for an IN.DAT now, we will get many warning messages: + +# %% +inputs = template_builder.make_inputs() + + +# %%[markdown] +# So let's go ahead and add the iteration variables we want to the problem: + +# %% +template_builder.add_variable("bt", 5.3292, upper_bound=20.0) +template_builder.add_variable("rmajor", 8.8901, upper_bound=13.0) +template_builder.add_variable("te", 12.33, upper_bound=150.0) +template_builder.add_variable("beta", 3.1421e-2) +template_builder.add_variable("dene", 7.4321e19) +template_builder.add_variable("q", 3.5, lower_bound=3.5) +template_builder.add_variable("pheat", 50.0) +template_builder.add_variable("ralpne", 6.8940e-02) +template_builder.add_variable("bore", 2.3322, lower_bound=0.1) +template_builder.add_variable("ohcth", 0.55242, lower_bound=0.1) +template_builder.add_variable("thwcndut", 8.0e-3, lower_bound=8.0e-3) +template_builder.add_variable("thkcas", 0.52465) +template_builder.add_variable("tfcth", 1.2080) +template_builder.add_variable("gapoh", 0.05, lower_bound=0.05, upper_bound=0.1) +template_builder.add_variable("gapds", 0.02, lower_bound=0.02) +template_builder.add_variable("cpttf", 6.5e4, lower_bound=6.0e4, upper_bound=9.0e4) +template_builder.add_variable("tdmptf", 2.5829e01) +template_builder.add_variable("fcutfsu", 0.80884, lower_bound=0.5, upper_bound=0.94) +template_builder.add_variable("fvsbrnni", 0.39566) + +# %%[markdown] +# Many of the PROCESS constraints use so-called 'f-values', which are automatically +# added to the iteration variables using this API. However, often one wants to modify +# the defaults of these f-values, which one can do as such: + +# %% +# Modified f-values and bounds w.r.t. defaults +template_builder.adjust_variable("fne0", 0.6, upper_bound=0.95) +template_builder.adjust_variable("fdene", 1.2, upper_bound=1.2) + + +# %%[markdown] +# Often one wants to specify certain impurity concentrations, and even use +# one of these as an iteration variable. + +# %% +template_builder.add_impurity(Impurities.H, 1.0) +template_builder.add_impurity(Impurities.He, 0.1) +template_builder.add_impurity(Impurities.W, 5.0e-5) +template_builder.add_variable(Impurities.Xe.id(), 3.573e-04) + + +# %%[markdown] +# We also want to specify some input values that are not variables: + +# %% +template_builder.add_input_values( + { + # Profile parameterisation inputs + "alphan": 1.0, + "alphat": 1.45, + "rhopedn": 0.94, + "rhopedt": 0.94, + "tbeta": 2.0, + "teped": 5.5, + "tesep": 0.1, + "fgwped": 0.85, + "neped": 0.678e20, + "nesep": 0.2e20, + "dnbeta": 3.0, + # Plasma impurity stuff + "coreradius": 0.75, + "coreradiationfraction": 0.6, + # Important stuff + "pnetelin": 500.0, + "tbrnmn": 7.2e3, + "sig_tf_case_max": 5.8e8, + "sig_tf_wp_max": 5.8e8, + "alstroh": 6.6e8, + "psepbqarmax": 9.2, + "aspect": 3.1, + "m_s_limit": 0.1, + "triang": 0.5, + "q0": 1.0, + "ssync": 0.6, + "plasma_res_factor": 0.66, + "gamma": 0.3, + "hfact": 1.1, + "life_dpa": 70.0, + # Radial build inputs + "tftsgap": 0.05, + "d_vv_in": 0.3, + "shldith": 0.3, + "vvblgap": 0.02, + "blnkith": 0.755, + "scrapli": 0.225, + "scraplo": 0.225, + "blnkoth": 0.982, + "d_vv_out": 0.3, + "shldoth": 0.8, + "ddwex": 0.15, + "gapomin": 0.2, + # Vertical build inputs + "d_vv_top": 0.3, + "vgap2": 0.05, + "shldtth": 0.3, + "divfix": 0.621, + "d_vv_bot": 0.3, + # HCD inputs + "pinjalw": 51.0, + "gamma_ecrh": 0.3, + "etaech": 0.4, + "bscfmax": 0.99, + # BOP inputs + "etath": 0.375, + "etahtp": 0.87, + "etaiso": 0.9, + "vfshld": 0.6, + "tdwell": 0.0, + "tramp": 500.0, + # CS / PF coil inputs + "t_crack_vertical": 0.4e-3, + "fcuohsu": 0.7, + "ohhghf": 0.9, + "rpf2": -1.825, + "cptdin": [4.22e4, 4.22e4, 4.22e4, 4.22e4, 4.3e4, 4.3e4, 4.3e4, 4.3e4], + "ipfloc": [2, 2, 3, 3], + "ncls": [1, 1, 2, 2], + "ngrp": 4, + "rjconpf": [1.1e7, 1.1e7, 6.0e6, 6.0e6, 8.0e6, 8.0e6, 8.0e6, 8.0e6], + # TF coil inputs + "n_tf": 16, + "casthi": 0.06, + "casths": 0.05, + "ripmax": 0.6, + "dhecoil": 0.01, + "tftmp": 4.75, + "thicndut": 2.0e-3, + "tinstf": 0.008, + # "tfinsgap": 0.01, + "tmargmin": 1.5, + "vftf": 0.3, + } +) + +# %%[markdown] +# PROCESS has many different models with integer-value 'switches'. We can specify +# these choices as follows: + +# %% +for model_choice in ( + BootstrapCurrentScalingLaw.SAUTER, + ConfinementTimeScalingLaw.IPB98_Y2_H_MODE, + PlasmaCurrentScalingLaw.ITER_REVISED, + PlasmaProfileModel.CONSISTENT, + PlasmaPedestalModel.PEDESTAL_GW, + PlasmaNullConfigurationModel.SINGLE_NULL, + EPEDScalingModel.SAARELMA, + BetaLimitModel.THERMAL, + DensityLimitModel.GREENWALD, + AlphaPressureModel.WARD, + PlasmaGeometryModel.CREATE_A_M_S, + PowerFlowModel.SIMPLE, + ShieldThermalHeatUse.LOW_GRADE_HEAT, + SecondaryCycleModel.INPUT, + CurrentDriveEfficiencyModel.ECRH_UI_GAM, + OperationModel.PULSED, + PFSuperconductorModel.NBTI, + SolenoidSwitchModel.SOLENOID, + CSSuperconductorModel.NB3SN_WST, + TFSuperconductorModel.NB3SN_WST, + TFWindingPackTurnModel.INTEGER_TURN, + FISPACTSwitchModel.OFF, + PrimaryPumpingModel.PRESSURE_DROP_INPUT, + TFNuclearHeatingModel.INPUT, + CostModel.TETRA_1990, + AvailabilityModel.INPUT, + OutputCostsSwitch.NO, +): + template_builder.set_model(model_choice) + +# %%[markdown] +# Some of these model choices also require certain input values +# to be specified. If these are not specified by the user, default +# values are used, which may not be desirable. Let us see what +# we're still missing: + +# %% +inputs = template_builder.make_inputs() + +# %%[markdown] +# And now let's add those missing inputs: + +# %% +template_builder.add_input_value("qnuc", 1.3e4) +template_builder.add_input_value("n_layer", 20) +template_builder.add_input_value("n_pancake", 20) + + +# %%[markdown] +# Finally, let us run PROCESS with our inputs. In this case, we're just running +# PROCESS as an external code (see e.g. [External code example](../external_code.ex.py)) +# So we are not interesed in passing any parameters into it. In future, once the +# input template has been refined to something desirable, one can pass in parameters +# in mapped names to PROCESS, and not need to explicitly know all the PROCESS +# parameter names. + +# %% +solver = systems_code_solver( + params={}, build_config={"template_in_dat": template_builder.make_inputs()} +) + +result = solver.execute("run") + +# %% +# Great, so it runs! All we need to do now is make sure we have properly +# specified our design problem, and perhaps adjust the initial values +# of the iteration variables to give the optimisation algorithm a better +# chance of finding a feasible point. + +# %% +template_builder.adjust_variable("fpnetel", 1.0) +template_builder.adjust_variable("fstrcase", 1.0) +template_builder.adjust_variable("ftmargtf", 1.0) +template_builder.adjust_variable("ftmargoh", 1.0) +template_builder.adjust_variable("ftaulimit", 1.0) +template_builder.adjust_variable("fbetatry", 0.48251) +template_builder.adjust_variable("fpsepbqar", 1.0) +template_builder.adjust_variable("fvdump", 1.0) +template_builder.adjust_variable("fstrcond", 0.92007) +template_builder.adjust_variable("fjprot", 1.0) + +# %% diff --git a/pyproject.toml b/pyproject.toml index e55f5bed3a..f511bc85c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,9 +87,6 @@ openmc = [ "OpenMC @git+https://github.com/openmc-dev/openmc.git", "parametric-plasma-source @git+https://github.com/open-radiation-sources/parametric-plasma-source.git", ] -process = [ - "cmake>=3.13.0", -] polyscope = ["polyscope"] [build-system] @@ -326,6 +323,7 @@ ignore-names = [ ] "bluemira/__init__.py" = ["TID252"] "bluemira/codes/__init__.py" = ["E402"] +"bluemira/codes/process/_model_mapping.py" = ["PLR6301"] "bluemira/geometry/parameterisations.py" = ["E731"] "bluemira/codes/plasmod/api/_outputs.py" = ["N815"] "bluemira/codes/plasmod/api/_inputs.py" = ["N815"] diff --git a/requirements.txt b/requirements.txt index 775fe67a0d..0ff9e1f652 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,9 +17,9 @@ neutronics-material-maker==0.1.11 nlopt==2.7.1 meshio==4.4.5 numba==0.58.0 -numba-scipy==0.3.1 +numba-scipy @ git+https://github.com/numba/numba-scipy@1e2f244 numexpr==2.8.6 -numpy==1.22.4 +numpy==1.23.5 packaging==23.2 pandas==2.0.3 periodictable==1.6.1 @@ -34,7 +34,7 @@ python-dateutil==2.8.2 pytz==2023.3.post1 rich==13.6.0 scikit-learn==1.3.1 -scipy==1.7.3 +scipy==1.10.1 seaborn==0.13.0 six==1.16.0 tables==3.8.0 diff --git a/scripts/install-process.sh b/scripts/install-process.sh index a0c2abbc80..76d902b38a 100644 --- a/scripts/install-process.sh +++ b/scripts/install-process.sh @@ -14,18 +14,18 @@ if [[ $(basename $PWD) == *"bluemira"* ]]; then fi if [ ! -d process ]; then - git clone git@git.ccfe.ac.uk:process/process.git + git clone git@github.com:ukaea/process.git fi cd process -git checkout develop +git checkout main git pull if [ "$1" ] then git checkout "$1" else - git checkout v2.3.0-hotfix + git checkout v3.0.1 fi @@ -35,34 +35,10 @@ if [ -d build ]; then rm -rf build fi -# Come out of bluemira conda and make a new environment for our build -conda deactivate -conda env remove -n bluemira-process-build || true -conda create -y -n bluemira-process-build python=3.8 numpy=1.21.5 -conda activate bluemira-process-build - -# Install requirements into the build environment -pip install -r ../bluemira/requirements.txt -pip install -r requirements.txt --no-cache-dir -pip install --upgrade --no-cache-dir -e ../bluemira/'[process]' - # Do the PROCESS build -cmake -S . -B build +cmake -S . -B build -DRELEASE=TRUE cmake --build build -# Deactivate build environment and reactivate bluemira -conda deactivate && conda activate bluemira - -# Install PROCESS dependencies into bluemira environment -pip install -r requirements.txt --no-cache-dir -pip install --upgrade --no-cache-dir -e ../bluemira/'[process]' - -# Install PROCESS into bluemira environment -pip install . - -# Clean up our build environment -conda env remove -n bluemira-process-build || true - # The following suggests how to install PROCESS via a manylinux wheel, if you have docker # installed. diff --git a/tests/base/parameterframe/test_parameterframe.py b/tests/base/parameterframe/test_parameterframe.py index 51ca4a9de9..5f71fdf769 100644 --- a/tests/base/parameterframe/test_parameterframe.py +++ b/tests/base/parameterframe/test_parameterframe.py @@ -131,6 +131,15 @@ class GenericFrame(ParameterFrame): assert frame.x.value == 10 + def test_horrible_scalar_unit(self): + @dataclass + class GenericFrame(ParameterFrame): + x: Parameter + + frame = GenericFrame.from_dict({"x": {"value": 10, "unit": "1e50 m"}}) + + assert frame.x.value == pytest.approx(1e51) + @pytest.mark.parametrize("value", ["OK", ["OK"]]) def test_TypeError_given_field_has_Union_Parameter_type(self, value): @dataclass diff --git a/tests/codes/process/test_api.py b/tests/codes/process/test_api.py index 73d71e825d..a3e6a0eb61 100644 --- a/tests/codes/process/test_api.py +++ b/tests/codes/process/test_api.py @@ -19,9 +19,10 @@ # You should have received a copy of the GNU Lesser General Public # License along with bluemira; if not, see . -from pathlib import Path from unittest.mock import patch +import pytest + from bluemira.codes.process import api PROCESS_OBS_VAR = { @@ -39,15 +40,16 @@ def test_update_obsolete_vars(): assert str2 == "shrubbery" +@pytest.mark.skipif(not api.ENABLED, reason="PROCESS is not installed on the system.") @patch.object(api, "imp_data") def test_impurities(imp_data_mock): imp_data_mock.__file__ = "./__init__.py" assert api.Impurities["H"] == api.Impurities.H assert api.Impurities(1) == api.Impurities.H - assert api.Impurities(1).id() == "fimp(01" - assert api.Impurities(10).id() == "fimp(10" - assert api.Impurities(1).file() == Path("./H_Lzdata.dat") - assert api.Impurities(10).file() == Path("./FeLzdata.dat") + assert api.Impurities(1).id() == "fimp(01)" + assert api.Impurities(10).id() == "fimp(10)" + assert api.Impurities(1).file().parts[-1] == "H__lz_tau.dat" + assert api.Impurities(10).file().parts[-1] == "Fe_lz_tau.dat" def test_INVariable_works_with_floats(): diff --git a/tests/codes/process/test_data/MFILE.DAT b/tests/codes/process/test_data/MFILE.DAT index 260e055032..2218ef718b 100644 --- a/tests/codes/process/test_data/MFILE.DAT +++ b/tests/codes/process/test_data/MFILE.DAT @@ -977,7 +977,7 @@ Heat_extracted_from_divertor_(MW)_______________________________________ (pthermdiv)___________________ 5.2392E+02 Nuclear_and_photon_power_lost_to_H/CD_system_(MW)_______________________ (psechcd)_____________________ 0.0000E+00 Total_(MW)______________________________________________________________ ______________________________ 4.2155E+03 - Net_electric_power_output(MW)___________________________________________ (pnetelmw.)___________________ 5.0000E+02 + Net_electric_power_output(MW)___________________________________________ (pnetelmw.)___________________ 6.0000E+02 Required_Net_electric_power_output(MW)__________________________________ (pnetelin)____________________ 5.0000E+02 Electric_power_for_heating_and_current_drive_(MW)_______________________ (pinjwp)______________________ 5.6250E+02 Electric_power_for_primary_coolant_pumps_(MW)___________________________ (htpmw)_______________________ 3.8236E+02 @@ -992,7 +992,7 @@ Fusion_power_(MW)_______________________________________________________ (powfmw.)_____________________ 3.0729E+03 Power_from_energy_multiplication_in_blanket_and_shield_(MW)_____________ (emultmw)_____________________ 5.8473E+02 Total_(MW)______________________________________________________________ ______________________________ 3.6576E+03 - Net_electrical_output_(MW) _____________________________________________ (pnetelmw)____________________ 5.0000E+02 + Net_electrical_output_(MW) _____________________________________________ (pnetelmw)____________________ 6.0000E+02 Heat_rejected_by_main_power_conversion_circuit_(MW)_____________________ (rejected_main)_______________ 2.6347E+03 Heat_rejected_by_other_cooling_circuits_(MW)____________________________ (psechtmw)____________________ 5.2323E+02 Total_(MW)______________________________________________________________ ______________________________ 3.6579E+03 diff --git a/tests/codes/process/test_data/mfile_data.json b/tests/codes/process/test_data/mfile_data.json index 53e049fe4b..8ae55eb637 100644 --- a/tests/codes/process/test_data/mfile_data.json +++ b/tests/codes/process/test_data/mfile_data.json @@ -931,59 +931,59 @@ "var_mod": "Plasma", "scan01": 0.094054 }, - "fimp(01": { + "fimp(01)": { "var_mod": "Plasma", "scan01": 0.74267 }, - "fimp(02": { + "fimp(02)": { "var_mod": "Plasma", "scan01": 0.094054 }, - "fimp(03": { + "fimp(03)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(04": { + "fimp(04)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(05": { + "fimp(05)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(06": { + "fimp(06)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(07": { + "fimp(07)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(08": { + "fimp(08)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(09": { + "fimp(09)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(10": { + "fimp(10)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(11": { + "fimp(11)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(12": { + "fimp(12)": { "var_mod": "Plasma", "scan01": 0.0 }, - "fimp(13": { + "fimp(13)": { "var_mod": "Plasma", "scan01": 0.0013085 }, - "fimp(14": { + "fimp(14)": { "var_mod": "Plasma", "scan01": 5e-5 }, @@ -3717,7 +3717,7 @@ }, "pnetelmw.": { "var_mod": "Plant Power / Heat Transport Balance", - "scan01": 500.0 + "scan01": 600.0 }, "pnetelin": { "var_mod": "Plant Power / Heat Transport Balance", @@ -3737,7 +3737,7 @@ }, "pnetelmw": { "var_mod": "Plant Power / Heat Transport Balance", - "scan01": 500.0 + "scan01": 600.0 }, "rejected_main": { "var_mod": "Plant Power / Heat Transport Balance", diff --git a/tests/codes/process/test_data/params.json b/tests/codes/process/test_data/params.json index c46fc27ff1..d15569ff62 100644 --- a/tests/codes/process/test_data/params.json +++ b/tests/codes/process/test_data/params.json @@ -196,12 +196,6 @@ "source": "Input", "long_name": "Bremsstrahlung" }, - "P_el_net_process": { - "value": 0.0, - "unit": "megawatt", - "source": "Input", - "long_name": "Net electrical power output as provided by PROCESS" - }, "P_fus_DD": { "value": 5, "unit": "megawatt", @@ -530,6 +524,12 @@ "source": "Input", "long_name": "Plasma side TF coil maximum height" }, + "m_s_limit": { + "value": 0.1, + "unit": "dimensionless", + "source": "Input", + "long_name": "Margin to vertical stability criterion" + }, "l_i": { "value": 0.8, "unit": "dimensionless", @@ -542,12 +542,24 @@ "source": "Input", "long_name": "Plasma safety factor" }, + "q_0": { + "value": 1.0, + "unit": "dimensionless", + "source": "Input", + "long_name": "Plasma safety factor on axis" + }, "r_tf_inboard_out": { "value": 0.6265, "unit": "meter", "source": "Input", "long_name": "Outboard Radius of the TF coil inboard leg tapered region" }, + "sigma_cs_wp_max": { + "value": 660000000.0, + "unit": "pascal", + "source": "Input", + "long_name": "Maximum von Mises stress in the CS coil winding pack" + }, "sigma_tf_case_max": { "value": 550000000.0, "unit": "pascal", @@ -571,5 +583,37 @@ "unit": "meter", "source": "Input", "long_name": "TF coil outboard thickness" + }, + "bb_pump_eta_el": { + "value": 0.87, + "unit": "", + "long_name": "Breeding blanket pumping electrical efficiency" + }, + "bb_pump_eta_isen": { + "value": 0.9, + "unit": "", + "long_name": "Breeding blanket pumping isentropic efficiency" + }, + "bb_t_inlet": { + "value": 573.15, + "unit": "K", + "long_name": "Breeding blanket inlet temperature" + }, + "bb_t_outlet": { + "value": 773.15, + "unit": "K", + "long_name": "Breeding blanket outlet temperature" + }, + "eta_ecrh": { + "value": 0.4, + "unit": "dimensionless", + "source": "Input", + "long_name": "Electron cyclotron resonce heating wallplug efficiency" + }, + "gamma_ecrh": { + "value": 0.3e20, + "unit": "A/W/m^2", + "source": "Input", + "long_name": "Electron cyclotron resonce heating current drive efficiency" } } diff --git a/tests/codes/process/test_data/read/mockPROCESS.json b/tests/codes/process/test_data/read/mockPROCESS.json index 69c50edb80..1a6b06c083 100644 --- a/tests/codes/process/test_data/read/mockPROCESS.json +++ b/tests/codes/process/test_data/read/mockPROCESS.json @@ -8,7 +8,6 @@ "P_bd_in": 50.0, "P_brehms": 59.668, "P_el_net": 500, - "P_el_net_process": 500.0, "P_fus": 1994.6, "P_fus_DD": 2.4746, "P_fus_DT": 1992.1, diff --git a/tests/codes/process/test_run.py b/tests/codes/process/test_run.py index 46e797d195..b885891515 100644 --- a/tests/codes/process/test_run.py +++ b/tests/codes/process/test_run.py @@ -46,6 +46,8 @@ def test_run_func_calls_subprocess_with_in_dat_path(self, run_func): getattr(run, run_func)() - self.run_subprocess_mock.assert_called_once_with( - [process.BINARY, "-i", "input/path_IN.DAT"] - ) + assert self.run_subprocess_mock.call_args[0][0] == [ + process.BINARY, + "-i", + "input/path_IN.DAT", + ] diff --git a/tests/codes/process/test_setup.py b/tests/codes/process/test_setup.py index cb8d6a15d0..2ccfa459ed 100644 --- a/tests/codes/process/test_setup.py +++ b/tests/codes/process/test_setup.py @@ -24,11 +24,10 @@ import pytest from bluemira.codes import process -from bluemira.codes.error import CodesError +from bluemira.codes.params import ParameterMapping from bluemira.codes.process._setup import Setup from bluemira.codes.process.mapping import mappings as process_mappings from bluemira.codes.process.params import ProcessSolverParams -from tests._helpers import file_exists from tests.codes.process.utilities import PARAM_FILE MODULE_REF = "bluemira.codes.process._setup" @@ -71,30 +70,6 @@ def test_run_adds_problem_setting_params_to_InDat_writer(self): assert writer.add_parameter.call_count > 0 assert mock.call("input0", 0.0) in writer.add_parameter.call_args_list - def test_run_inits_writer_with_template_file_if_file_exists(self): - with self._indat_patch as indat_cls_mock: - setup = Setup(self.default_pf, "", template_in_dat="template/path/in.dat") - indat_cls_mock.return_value.data = {"input": 0.0} - - with file_exists("template/path/in.dat", f"{MODULE_REF}.Path.is_file"): - setup.run() - - indat_cls_mock.assert_called_once_with(filename="template/path/in.dat") - - def test_run_inits_writer_without_template_returns_default_filled_data(self): - with self._indat_patch as indat_cls_mock: - setup = Setup(self.default_pf, "", template_in_dat=None) - setup.run() - - assert indat_cls_mock.return_value.data == self.default_pf.template_defaults - - @pytest.mark.parametrize("run_func", ["run", "runinput"]) - def test_run_raises_CodesError_given_no_data_in_template_file(self, run_func): - setup = Setup(self.default_pf, "", template_in_dat="template/path/in.dat") - - with pytest.raises(CodesError): - getattr(setup, run_func)() - def test_runinput_does_not_write_bluemira_outputs_to_in_dat(self): with self._writer_patch as writer_cls_mock: setup = Setup(self.default_pf, "") @@ -130,11 +105,14 @@ class TestSetupIntegration: @mock.patch(f"{MODULE_REF}.InDat") def test_obsolete_parameter_names_are_updated(self, writer_cls_mock): pf = ProcessSolverParams.from_json(PARAM_FILE) + pf.mappings["tk_tf_front_ib"] = ParameterMapping( + "dr_tf_case_out", send=True, recv=False, unit="m" + ) setup = Setup(pf, "") writer_cls_mock.return_value.data = {"x": 0} setup.run() writer = writer_cls_mock.return_value - # 'dr_tf_case_out' is new name for 'casthi' - assert mock.call("dr_tf_case_out", 0.04) in writer.add_parameter.call_args_list + # 'dr_tf_case_out' is old name for 'casthi' + assert mock.call("casthi", 0.04) in writer.add_parameter.call_args_list diff --git a/tests/codes/process/test_solver.py b/tests/codes/process/test_solver.py index 7dfbb45ce6..99a1442905 100644 --- a/tests/codes/process/test_solver.py +++ b/tests/codes/process/test_solver.py @@ -19,9 +19,10 @@ # You should have received a copy of the GNU Lesser General Public # License along with bluemira; if not, see . +import contextlib import filecmp +import json import re -import tempfile from pathlib import Path from unittest import mock @@ -29,8 +30,10 @@ from bluemira.codes.error import CodesError from bluemira.codes.process import ENABLED +from bluemira.codes.process._inputs import ProcessInputs from bluemira.codes.process._solver import RunMode, Solver from bluemira.codes.process.params import ProcessSolverParams +from tests._helpers import file_exists from tests.codes.process import utilities as utils @@ -96,28 +99,32 @@ def test_get_species_fraction_retrieves_parameter_value(self): @pytest.mark.skipif(not ENABLED, reason="PROCESS is not installed on the system.") class TestSolverIntegration: DATA_DIR = Path(Path(__file__).parent, "test_data") + MODULE_REF = "bluemira.codes.process._setup" def setup_method(self): self.params = ProcessSolverParams.from_json(utils.PARAM_FILE) + self._indat_patch = mock.patch(f"{self.MODULE_REF}.InDat") + + def teardown_method(self): + self._indat_patch.stop() + @pytest.mark.longrun - def test_run_mode_outputs_process_files(self): - run_dir = tempfile.TemporaryDirectory() - build_config = {"run_dir": run_dir.name} - solver = Solver(self.params, build_config) + def test_run_mode_outputs_process_files(self, tmp_path): + solver = Solver(self.params, {"run_dir": tmp_path}) - solver.execute(RunMode.RUN) + with contextlib.suppress(CodesError): + solver.execute(RunMode.RUNINPUT) - assert Path(run_dir.name, "IN.DAT").exists() - assert Path(run_dir.name, "MFILE.DAT").exists() + assert Path(tmp_path, "IN.DAT").exists() + assert Path(tmp_path, "MFILE.DAT").exists() @pytest.mark.parametrize("run_mode", [RunMode.READ, RunMode.READALL]) def test_read_mode_updates_params_from_mfile(self, run_mode): # Assert here to check the parameter is actually changing assert self.params.r_tf_in_centre.value != pytest.approx(2.6354) - build_config = {"read_dir": self.DATA_DIR} - solver = Solver(self.params, build_config) + solver = Solver(self.params, {"read_dir": self.DATA_DIR}) solver.execute(run_mode) # Expected value comes from ./test_data/MFILE.DAT @@ -125,9 +132,7 @@ def test_read_mode_updates_params_from_mfile(self, run_mode): @pytest.mark.parametrize("run_mode", [RunMode.READ, RunMode.READALL]) def test_derived_radial_build_params_are_updated(self, run_mode): - build_config = {"read_dir": self.DATA_DIR} - - solver = Solver(self.params, build_config) + solver = Solver(self.params, {"read_dir": self.DATA_DIR}) solver.execute(run_mode) # Expected values come from derivation (I added the numbers up by hand) @@ -139,23 +144,22 @@ def test_derived_radial_build_params_are_updated(self, run_mode): assert solver.params.r_vv_ob_in.value == pytest.approx(13.69696) @pytest.mark.longrun - def test_runinput_mode_does_not_edit_template(self): - run_dir = tempfile.TemporaryDirectory() + def test_runinput_mode_does_not_edit_template(self, tmp_path): template_path = Path(self.DATA_DIR, "IN.DAT") build_config = { - "run_dir": run_dir.name, + "run_dir": tmp_path, "template_in_dat": template_path, } solver = Solver(self.params, build_config) - solver.execute(RunMode.RUN) - - assert Path(run_dir.name, "IN.DAT").is_file() - filecmp.cmp(Path(run_dir.name, "IN.DAT"), template_path) - assert Path(run_dir.name, "MFILE.DAT").is_file() + with contextlib.suppress(CodesError): + solver.execute(RunMode.RUN) + assert Path(tmp_path, "IN.DAT").is_file() + filecmp.cmp(Path(tmp_path, "IN.DAT"), template_path) + assert Path(tmp_path, "MFILE.DAT").is_file() def test_get_species_data_returns_row_vectors(self): - temp, loss_f, z_eff = Solver.get_species_data("H") + temp, loss_f, z_eff = Solver.get_species_data("H", confinement_time_ms=1.0) assert isinstance(temp.size, int) == 1 assert temp.size > 0 @@ -163,3 +167,67 @@ def test_get_species_data_returns_row_vectors(self): assert loss_f.size > 0 assert isinstance(z_eff.size, int) == 1 assert z_eff.size > 0 + + def test_run_inits_writer_with_template_file_if_file_exists(self, tmp_path): + build_config = { + "run_dir": tmp_path, + "template_in_dat": "template/path/in.dat", + } + + class BLANK: + get_value = 0.0 + + with self._indat_patch as indat_cls_mock, file_exists( + "template/path/in.dat", f"{self.MODULE_REF}.Path.is_file" + ): + indat_cls_mock.return_value.data = {"casthi": BLANK} + Solver(self.params, build_config) + + indat_cls_mock.assert_called_once_with(filename="template/path/in.dat") + + def test_run_inits_writer_without_template_returns_default_filled_data(self): + with self._indat_patch as indat_cls_mock: + solver = Solver(self.params, {}) + solver.run_cls = lambda *_, **_kw: None + solver.teardown_cls = lambda *_, **_kw: None + solver.execute("run") + assert indat_cls_mock.return_value.data == self.params.template_defaults + + def test_run_raises_CodesError_given_no_data_in_template_file(self): + build_config = { + "template_in_dat": "template/path/in.dat", + } + + with pytest.raises(CodesError): + Solver(self.params, build_config) + + @pytest.mark.parametrize(("pf_n", "pf_v"), [(None, None), ("tk_sh_in", 3)]) + @pytest.mark.parametrize( + ("template", "result"), + [ + (ProcessInputs(bore=5, shldith=5, i_tf_wp_geom=2), (5, 5, 2)), + ], + ) + def test_indat_creation_with_template(self, template, result, pf_n, pf_v, tmp_path): + if pf_n is None: + pf = {} + else: + with open(utils.PARAM_FILE) as pf_h: + pf = {pf_n: json.load(pf_h)[pf_n]} + pf[pf_n]["value"] = pf_v + result = (result[0], pf_v, result[2]) + path = tmp_path / "IN.DAT" + build_config = { + "in_dat_path": path, + "template_in_dat": template, + } + + solver = Solver(pf, build_config) + solver.params.mappings["tk_sh_in"].send = True + solver.run_cls = lambda *_, **_kw: None + solver.teardown_cls = lambda *_, **_kw: None + solver.execute("run") + + assert f"bore = {result[0]}" in open(path).read() # noqa: SIM115 + assert f"shldith = {result[1]}" in open(path).read() # noqa: SIM115 + assert f"i_tf_wp_geom = {result[2]}" in open(path).read() # noqa: SIM115 diff --git a/tests/codes/process/test_teardown.py b/tests/codes/process/test_teardown.py index ae3d9a020c..c401cd5139 100644 --- a/tests/codes/process/test_teardown.py +++ b/tests/codes/process/test_teardown.py @@ -59,6 +59,7 @@ def test_run_func_updates_bluemira_params_from_mfile(self, run_func): # Expected value comes from ./test_data/mfile_data.json assert teardown.params.tau_e.value == pytest.approx(4.3196) + assert teardown.params.P_el_net.value == pytest.approx(6e8) @pytest.mark.parametrize("run_func", ["read", "readall"]) def test_read_func_updates_bluemira_params_from_mfile(self, run_func): @@ -70,7 +71,7 @@ def test_read_func_updates_bluemira_params_from_mfile(self, run_func): # Expected value comes from ./test_data/mfile_data.json assert teardown.params.tau_e.value == pytest.approx(4.3196) # auto unit conversion - assert teardown.params.P_el_net.value == pytest.approx(5e8) + assert teardown.params.P_el_net.value == pytest.approx(6e8) def test_read_unknown_outputs_set_to_nan(self): """ diff --git a/tests/codes/process/test_template_builder.py b/tests/codes/process/test_template_builder.py new file mode 100644 index 0000000000..918e200835 --- /dev/null +++ b/tests/codes/process/test_template_builder.py @@ -0,0 +1,547 @@ +# bluemira is an integrated inter-disciplinary design tool for future fusion +# reactors. It incorporates several modules, some of which rely on other +# codes, to carry out a range of typical conceptual fusion reactor design +# activities. +# +# Copyright (C) 2021-2023 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh, +# J. Morris, D. Short +# +# bluemira is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# bluemira is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with bluemira; if not, see . + +""" +Test PROCESS template builder +""" +import os +from pathlib import Path + +import numpy as np +import pytest + +from bluemira.base.constants import EPS +from bluemira.base.file import try_get_bluemira_private_data_root +from bluemira.codes.process._equation_variable_mapping import Constraint, Objective +from bluemira.codes.process._model_mapping import ( + AlphaPressureModel, + AvailabilityModel, + BetaLimitModel, + BootstrapCurrentScalingLaw, + CSSuperconductorModel, + ConfinementTimeScalingLaw, + CostModel, + CurrentDriveEfficiencyModel, + DensityLimitModel, + EPEDScalingModel, + FISPACTSwitchModel, + OperationModel, + OutputCostsSwitch, + PFSuperconductorModel, + PROCESSOptimisationAlgorithm, + PlasmaCurrentScalingLaw, + PlasmaGeometryModel, + PlasmaNullConfigurationModel, + PlasmaPedestalModel, + PlasmaProfileModel, + PowerFlowModel, + PrimaryPumpingModel, + SecondaryCycleModel, + ShieldThermalHeatUse, + SolenoidSwitchModel, + TFNuclearHeatingModel, + TFSuperconductorModel, + TFWindingPackTurnModel, +) +from bluemira.codes.process.api import ENABLED, Impurities +from bluemira.codes.process.template_builder import PROCESSTemplateBuilder +from bluemira.utilities.tools import compare_dicts + + +def extract_warning(caplog): + result = [line for message in caplog.messages for line in message.split(os.linesep)] + result = " ".join(result) + caplog.clear() + return result + + +class TestPROCESSTemplateBuilder: + def test_no_error_on_nothing(self, caplog): + t = PROCESSTemplateBuilder() + _ = t.make_inputs() + assert len(caplog.messages) == 0 + + def test_warn_on_optimisation_with_no_objective(self, caplog): + t = PROCESSTemplateBuilder() + t.set_optimisation_algorithm(PROCESSOptimisationAlgorithm.VMCON) + _ = t.make_inputs() + assert len(caplog.messages) == 1 + warning = extract_warning(caplog) + assert "You are running in optimisation mode, but" in warning + + @pytest.mark.parametrize( + "objective", + [Objective.FUSION_GAIN_PULSE_LENGTH, Objective.MAJOR_RADIUS_PULSE_LENGTH], + ) + def test_error_on_maxmin_objective(self, objective): + t = PROCESSTemplateBuilder() + with pytest.raises( + ValueError, match="can only be used as a minimisation objective" + ): + t.set_maximisation_objective(objective) + + @pytest.mark.parametrize("bad_name", ["spoon", "aaaaaaaaaaaaa"]) + def test_error_on_bad_itv_name(self, bad_name): + t = PROCESSTemplateBuilder() + with pytest.raises(ValueError, match="There is no iteration variable:"): + t.add_variable(bad_name, 3.14159) + + @pytest.mark.parametrize("bad_name", ["spoon", "aaaaaaaaaaaaa"]) + def test_error_on_adjusting_bad_variable(self, bad_name): + t = PROCESSTemplateBuilder() + with pytest.raises(ValueError, match="There is no iteration variable:"): + t.adjust_variable(bad_name, 3.14159) + + def test_warn_on_repeated_constraint(self, caplog): + t = PROCESSTemplateBuilder() + t.add_constraint(Constraint.BETA_CONSISTENCY) + t.add_constraint(Constraint.BETA_CONSISTENCY) + assert len(caplog.messages) == 1 + warning = extract_warning(caplog) + assert "is already in" in warning + + def test_warn_on_repeated_itv(self, caplog): + t = PROCESSTemplateBuilder() + t.add_variable("bore", 2.0) + t.add_variable("bore", 3.0) + assert len(caplog.messages) == 1 + warning = extract_warning(caplog) + assert "Iteration variable 'bore' is already" in warning + + def test_warn_on_adjusting_nonexistent_variable(self, caplog): + t = PROCESSTemplateBuilder() + t.adjust_variable("bore", 2.0) + assert len(caplog.messages) == 1 + warning = extract_warning(caplog) + assert "Iteration variable 'bore' is not in" in warning + assert "bore" in t.variables + + def test_warn_on_missing_input_constraint(self, caplog): + t = PROCESSTemplateBuilder() + t.add_constraint(Constraint.NWL_UPPER_LIMIT) + t.add_variable("aspect", 3.1) + t.add_variable("bt", 5.0) + t.add_variable("rmajor", 9.0) + t.add_variable("te", 12.0) + t.add_variable("dene", 8.0e19) + _ = t.make_inputs() + assert len(caplog.messages) == 1 + warning = extract_warning(caplog) + assert "requires inputs 'walalw'" in warning + + def test_warn_on_missing_itv_constraint(self, caplog): + t = PROCESSTemplateBuilder() + t.add_constraint(Constraint.RADIAL_BUILD_CONSISTENCY) + _ = t.make_inputs() + assert len(caplog.messages) == 1 + assert "requires iteration" in extract_warning(caplog) + + def test_no_warn_on_missing_itv_constraint_but_as_input(self, caplog): + t = PROCESSTemplateBuilder() + t.add_constraint(Constraint.NWL_UPPER_LIMIT) + t.add_variable("bt", 5.0) + t.add_variable("rmajor", 9.0) + t.add_variable("te", 12.0) + t.add_variable("dene", 8.0e19) + t.add_input_value("walalw", 8.0) + t.add_input_value("aspect", 3.1) + _ = t.make_inputs() + assert len(caplog.messages) == 0 + + def test_warn_on_missing_input_model(self, caplog): + t = PROCESSTemplateBuilder() + t.set_model(PlasmaGeometryModel.CREATE_A_M_S) + _ = t.make_inputs() + assert len(caplog.messages) == 1 + assert "requires inputs" in extract_warning(caplog) + + def test_automatic_fvalue_itv(self): + t = PROCESSTemplateBuilder() + t.set_minimisation_objective(Objective.MAJOR_RADIUS) + t.add_constraint(Constraint.NET_ELEC_LOWER_LIMIT) + assert "fpnetel" in t.variables + + def test_warn_on_overwrite_value(self, caplog): + t = PROCESSTemplateBuilder() + t.add_input_value("dummy", 1.0) + t.add_input_value("dummy", 2.0) + assert len(caplog.messages) == 1 + assert "Over-writing" in extract_warning(caplog) + + def test_warn_on_overwrite_model(self, caplog): + t = PROCESSTemplateBuilder() + t.set_model(PlasmaGeometryModel.CREATE_A_M_S) + t.set_model(PlasmaGeometryModel.FIESTA_100) + assert len(caplog.messages) == 1 + assert "Over-writing" in extract_warning(caplog) + + def test_impurity_shenanigans(self): + t = PROCESSTemplateBuilder() + t.add_impurity(Impurities.Xe, 0.5) + assert t.fimp[12] == pytest.approx(0.5, rel=0, abs=EPS) + t.add_variable("fimp(13)", 0.6) + assert t.fimp[12] == pytest.approx(0.6, rel=0, abs=EPS) + t.add_impurity(Impurities.Xe, 0.4) + assert t.fimp[12] == pytest.approx(0.4, rel=0, abs=EPS) + + def test_input_appears_in_dat(self): + t = PROCESSTemplateBuilder() + t.add_input_value("tinstf", 1000.0) + assert t.values["tinstf"] == pytest.approx(1000.0, rel=0, abs=EPS) + data = t.make_inputs() + assert data.to_invariable()["tinstf"]._value == pytest.approx( + 1000.0, rel=0, abs=EPS + ) + + def test_inputs_appear_in_dat(self): + t = PROCESSTemplateBuilder() + t.add_input_values({"tinstf": 1000.0, "bore": 1000}) + assert t.values["tinstf"] == pytest.approx(1000.0, rel=0, abs=EPS) + assert t.values["bore"] == pytest.approx(1000.0, rel=0, abs=EPS) + data = t.make_inputs().to_invariable() + assert data["tinstf"]._value == pytest.approx(1000.0, rel=0, abs=EPS) + assert data["bore"]._value == pytest.approx(1000.0, rel=0, abs=EPS) + + +def read_indat(filename): + from process.io.in_dat import InDat + + naughties = ["runtitle", "pulsetimings"] + data = InDat(filename=filename).data + return {k: v for k, v in data.items() if k not in naughties} + + +@pytest.mark.private +@pytest.mark.skipif(not ENABLED, reason="PROCESS is not installed on the system.") +class TestInDatOneForOne: + @classmethod + def setup_class(cls): + fp = Path( + try_get_bluemira_private_data_root(), + "process/DEMO_2023_TEMPLATE_TEST_IN.DAT", + ) + + cls.true_data = read_indat(fp) + + template_builder = PROCESSTemplateBuilder() + template_builder.set_optimisation_algorithm(PROCESSOptimisationAlgorithm.VMCON) + template_builder.set_optimisation_numerics(max_iterations=1000, tolerance=1e-8) + + template_builder.set_minimisation_objective(Objective.MAJOR_RADIUS) + + for constraint in ( + Constraint.BETA_CONSISTENCY, + Constraint.GLOBAL_POWER_CONSISTENCY, + Constraint.DENSITY_UPPER_LIMIT, + Constraint.NWL_UPPER_LIMIT, + Constraint.RADIAL_BUILD_CONSISTENCY, + Constraint.BURN_TIME_LOWER_LIMIT, + Constraint.LH_THRESHHOLD_LIMIT, + Constraint.NET_ELEC_LOWER_LIMIT, + Constraint.BETA_UPPER_LIMIT, + Constraint.CS_EOF_DENSITY_LIMIT, + Constraint.CS_BOP_DENSITY_LIMIT, + Constraint.PINJ_UPPER_LIMIT, + Constraint.TF_CASE_STRESS_UPPER_LIMIT, + Constraint.TF_JACKET_STRESS_UPPER_LIMIT, + Constraint.TF_JCRIT_RATIO_UPPER_LIMIT, + Constraint.TF_DUMP_VOLTAGE_UPPER_LIMIT, + Constraint.TF_CURRENT_DENSITY_UPPER_LIMIT, + Constraint.TF_T_MARGIN_LOWER_LIMIT, + Constraint.CS_T_MARGIN_LOWER_LIMIT, + Constraint.CONFINEMENT_RATIO_LOWER_LIMIT, + Constraint.DUMP_TIME_LOWER_LIMIT, + Constraint.PSEPB_QAR_UPPER_LIMIT, + Constraint.CS_STRESS_UPPER_LIMIT, + Constraint.DENSITY_PROFILE_CONSISTENCY, + Constraint.CS_FATIGUE, + ): + template_builder.add_constraint(constraint) + + # Variable vector values and bounds + template_builder.add_variable("bt", 5.3292, upper_bound=20.0) + template_builder.add_variable("rmajor", 8.8901, upper_bound=13) + template_builder.add_variable("te", 12.33, upper_bound=150.0) + template_builder.add_variable("beta", 3.1421e-2) + template_builder.add_variable("dene", 7.4321e19) + template_builder.add_variable("q", 3.5, lower_bound=3.5) + template_builder.add_variable("pheat", 50.0) + template_builder.add_variable("ralpne", 6.8940e-02) + template_builder.add_variable("bore", 2.3322, lower_bound=0.1) + template_builder.add_variable("ohcth", 0.55242, lower_bound=0.1) + template_builder.add_variable("thwcndut", 8.0e-3, lower_bound=8.0e-3) + template_builder.add_variable("thkcas", 0.52465) + template_builder.add_variable("tfcth", 1.2080) + template_builder.add_variable("gapoh", 0.05, lower_bound=0.05, upper_bound=0.1) + template_builder.add_variable("gapds", 0.02, lower_bound=0.02) + template_builder.add_variable("oh_steel_frac", 0.57875) + template_builder.add_variable("coheof", 2.0726e07) + template_builder.add_variable( + "cpttf", 6.5e4, lower_bound=6.0e4, upper_bound=9.0e4 + ) + template_builder.add_variable("tdmptf", 2.5829e01) + template_builder.add_variable("vdalw", 10.0, upper_bound=10.0) + template_builder.add_variable("fimp(13)", 3.573e-04) + + # Some constraints require multiple f-values, but they are getting + # ridding of those, so no fancy mechanics for now... + template_builder.add_variable( + "fcutfsu", 0.80884, lower_bound=0.5, upper_bound=0.94 + ) + template_builder.add_variable("fcohbop", 0.93176) + template_builder.add_variable("fvsbrnni", 0.39566) + template_builder.add_variable("fncycle", 1.0) + + # Modified f-values and bounds w.r.t. defaults + template_builder.adjust_variable("fne0", 0.6, upper_bound=0.95) + template_builder.adjust_variable("fdene", 1.2, upper_bound=1.2) + template_builder.adjust_variable( + "flhthresh", 1.2, lower_bound=1.1, upper_bound=1.2 + ) + template_builder.adjust_variable("ftburn", 1.0, upper_bound=1.0) + + # Modifying the initial variable vector to improve convergence + template_builder.adjust_variable("fpnetel", 1.0) + template_builder.adjust_variable("fstrcase", 1.0) + template_builder.adjust_variable("ftmargtf", 1.0) + template_builder.adjust_variable("ftmargoh", 1.0) + template_builder.adjust_variable("ftaulimit", 1.0) + template_builder.adjust_variable("fjohc", 0.57941, upper_bound=1.0) + template_builder.adjust_variable("fjohc0", 0.53923, upper_bound=1.0) + template_builder.adjust_variable("foh_stress", 1.0) + template_builder.adjust_variable("fbetatry", 0.48251) + template_builder.adjust_variable("fwalld", 0.131) + template_builder.adjust_variable("fmaxvvstress", 1.0) + template_builder.adjust_variable("fpsepbqar", 1.0) + template_builder.adjust_variable("fvdump", 1.0) + template_builder.adjust_variable("fstrcond", 0.92007) + template_builder.adjust_variable("fiooic", 0.63437, upper_bound=1.0) + template_builder.adjust_variable("fjprot", 1.0) + + # Set model switches + for model_choice in ( + BootstrapCurrentScalingLaw.SAUTER, + ConfinementTimeScalingLaw.IPB98_Y2_H_MODE, + PlasmaCurrentScalingLaw.ITER_REVISED, + PlasmaProfileModel.CONSISTENT, + PlasmaPedestalModel.PEDESTAL_GW, + PlasmaNullConfigurationModel.SINGLE_NULL, + EPEDScalingModel.SAARELMA, + BetaLimitModel.THERMAL, + DensityLimitModel.GREENWALD, + AlphaPressureModel.WARD, + PlasmaGeometryModel.CREATE_A_M_S, + PowerFlowModel.SIMPLE, + ShieldThermalHeatUse.LOW_GRADE_HEAT, + SecondaryCycleModel.INPUT, + CurrentDriveEfficiencyModel.ECRH_UI_GAM, + OperationModel.PULSED, + PFSuperconductorModel.NBTI, + SolenoidSwitchModel.SOLENOID, + CSSuperconductorModel.NB3SN_WST, + TFSuperconductorModel.NB3SN_WST, + TFWindingPackTurnModel.INTEGER_TURN, + FISPACTSwitchModel.OFF, + PrimaryPumpingModel.PRESSURE_DROP_INPUT, + TFNuclearHeatingModel.INPUT, + CostModel.TETRA_1990, + AvailabilityModel.INPUT, + OutputCostsSwitch.NO, + ): + template_builder.set_model(model_choice) + + template_builder.add_impurity(Impurities.H, 1.0) + template_builder.add_impurity(Impurities.He, 0.1) + template_builder.add_impurity(Impurities.W, 5.0e-5) + + # Set fixed input values + template_builder.add_input_values( + { + # Undocumented danger stuff + "iblanket": 1, + "lsa": 2, + # Profile parameterisation inputs + "alphan": 1.0, + "alphat": 1.45, + "rhopedn": 0.94, + "rhopedt": 0.94, + "tbeta": 2.0, + "teped": 5.5, + "tesep": 0.1, + "fgwped": 0.85, + "neped": 0.678e20, + "nesep": 0.2e20, + "dnbeta": 3.0, + # Plasma impurity stuff + "coreradius": 0.75, + "coreradiationfraction": 0.6, + # Important stuff + "pnetelin": 500.0, + "tbrnmn": 7.2e3, + "sig_tf_case_max": 5.8e8, + "sig_tf_wp_max": 5.8e8, + "alstroh": 6.6e8, + "psepbqarmax": 9.2, + "aspect": 3.1, + "m_s_limit": 0.1, + "triang": 0.5, + "q0": 1.0, + "ssync": 0.6, + "plasma_res_factor": 0.66, + "gamma": 0.3, + "hfact": 1.1, + "life_dpa": 70.0, + # Radial build inputs + "tftsgap": 0.05, + "d_vv_in": 0.3, + "shldith": 0.3, + "vvblgap": 0.02, + "blnkith": 0.755, + "scrapli": 0.225, + "scraplo": 0.225, + "blnkoth": 0.982, + "d_vv_out": 0.3, + "shldoth": 0.8, + "ddwex": 0.15, + "gapomin": 0.2, + # Vertical build inputs + "d_vv_top": 0.3, + "vgap2": 0.05, + "shldtth": 0.3, + "divfix": 0.621, + "d_vv_bot": 0.3, + # HCD inputs + "pinjalw": 51.0, + "gamma_ecrh": 0.3, + "etaech": 0.4, + "bscfmax": 0.99, + # BOP inputs + "etath": 0.375, + "etahtp": 0.87, + "etaiso": 0.9, + "vfshld": 0.6, + "tdwell": 0.0, + "tramp": 500.0, + # CS / PF coil inputs + "t_crack_vertical": 0.4e-3, + "fcuohsu": 0.7, + "ohhghf": 0.9, + "rpf2": -1.825, + "cptdin": [4.22e4, 4.22e4, 4.22e4, 4.22e4, 4.3e4, 4.3e4, 4.3e4, 4.3e4], + "ipfloc": [2, 2, 3, 3], + "ncls": [1, 1, 2, 2], + "ngrp": 4, + "rjconpf": [1.1e7, 1.1e7, 6.0e6, 6.0e6, 8.0e6, 8.0e6, 8.0e6, 8.0e6], + # TF coil inputs + "n_tf": 16, + "casthi": 0.06, + "casths": 0.05, + "ripmax": 0.6, + "dhecoil": 0.01, + "tftmp": 4.75, + "thicndut": 2.0e-3, + "tinstf": 0.008, + # "tfinsgap": 0.01, + "tmargmin": 1.5, + "vftf": 0.3, + "n_pancake": 20, + "n_layer": 10, + "qnuc": 1.292e4, + # Inputs we don't care about but must specify + "cfactr": 0.75, # Ha! + "kappa": 1.848, # Should be overwritten + "walalw": 8.0, # Should never get even close to this + "tlife": 40.0, + "abktflnc": 15.0, + "adivflnc": 20.0, + # For sanity... + "hldivlim": 10, + "ksic": 1.4, + "prn1": 0.4, + "zeffdiv": 3.5, + "bmxlim": 11.2, + "ffuspow": 1.0, + "fpeakb": 1.0, + "divdum": 1, + "ibkt_life": 1, + "fkzohm": 1.0245, + "iinvqd": 1, + "dintrt": 0.0, + "fcap0": 1.15, + "fcap0cp": 1.06, + "fcontng": 0.15, + "fcr0": 0.065, + "fkind": 1.0, + "ifueltyp": 1, + "discount_rate": 0.06, + "bkt_life_csf": 1, + "ucblvd": 280.0, + "ucdiv": 5e5, + "ucme": 3.0e8, + # Suspicous stuff + "zref": [3.6, 1.2, 1.0, 2.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + "fpinj": 1.0, + } + ) + + cls.template = template_builder.make_inputs().to_invariable() + + def test_indat_bounds_the_same(self): + true_bounds = self.true_data.pop("bounds").get_value + new_bounds = self.template.pop("bounds").get_value + + # Make everything floats for easier comparison + for k in true_bounds: + for kk in true_bounds[k]: + true_bounds[k][kk] = float(true_bounds[k][kk]) + for k in new_bounds: + for kk in new_bounds[k]: + new_bounds[k][kk] = float(new_bounds[k][kk]) + assert compare_dicts(true_bounds, new_bounds) + + def test_indat_constraints(self): + true_cons = self.true_data.pop("icc").get_value + new_cons = self.template.pop("icc").get_value + + np.testing.assert_allclose(sorted(true_cons), sorted(new_cons)) + + def test_indat_variables(self): + true_vars = self.true_data.pop("ixc").get_value + new_vars = self.template.pop("ixc").get_value + + np.testing.assert_allclose(sorted(true_vars), sorted(new_vars)) + + def test_inputs_same(self): + for k in self.true_data: + if not isinstance(self.true_data[k].get_value, (list, dict)): + assert np.allclose( + self.true_data[k].get_value, self.template[k].get_value + ) + elif isinstance(self.true_data[k].get_value, dict): + compare_dicts(self.true_data[k].get_value, self.template[k]._value) + else: + assert not set(self.true_data[k].get_value) - set( + self.template[k].get_value + ) + + def test_no_extra_inputs(self): + for k in self.template: + assert k in self.true_data diff --git a/tests/codes/test_interface.py b/tests/codes/test_interface.py index 5457d26558..e621dbf505 100644 --- a/tests/codes/test_interface.py +++ b/tests/codes/test_interface.py @@ -47,7 +47,7 @@ class Params(MappedParameterFrame): _mappings: ClassVar = { "param1": ParameterMapping("ext1", send=True, recv=True, unit="MW"), - "param2": ParameterMapping("ext2", send=False, recv=False), + "param2": ParameterMapping("ext2", "ext3", send=False, recv=False), } @property @@ -87,3 +87,7 @@ def test_no_defaults_are_set_to_None(self): assert params.param2.value is None assert params.param1.unit == "W" assert params.param2.unit == "" + assert params.mappings["param1"].name == "ext1" + assert params.mappings["param1"].out_name == "ext1" + assert params.mappings["param2"].name == "ext2" + assert params.mappings["param2"].out_name == "ext3" diff --git a/tests/test_data/reactors/BLUEPRINT-INTEGRATION-TEST/systems_code/mockPROCESS.json b/tests/test_data/reactors/BLUEPRINT-INTEGRATION-TEST/systems_code/mockPROCESS.json index 69c50edb80..1a6b06c083 100644 --- a/tests/test_data/reactors/BLUEPRINT-INTEGRATION-TEST/systems_code/mockPROCESS.json +++ b/tests/test_data/reactors/BLUEPRINT-INTEGRATION-TEST/systems_code/mockPROCESS.json @@ -8,7 +8,6 @@ "P_bd_in": 50.0, "P_brehms": 59.668, "P_el_net": 500, - "P_el_net_process": 500.0, "P_fus": 1994.6, "P_fus_DD": 2.4746, "P_fus_DT": 1992.1, diff --git a/tests/test_data/reactors/SMOKE-TEST-EU-DEMO/systems_code/mockPROCESS.json b/tests/test_data/reactors/SMOKE-TEST-EU-DEMO/systems_code/mockPROCESS.json index daefd7f2eb..58be6e21eb 100644 --- a/tests/test_data/reactors/SMOKE-TEST-EU-DEMO/systems_code/mockPROCESS.json +++ b/tests/test_data/reactors/SMOKE-TEST-EU-DEMO/systems_code/mockPROCESS.json @@ -5,7 +5,7 @@ "H_star": 1.1, "I_p": 19.117, "P_brehms": 60.956, - "P_el_net_process": 500.0, + "P_el_net": 500.0, "P_fus": 1790.6, "P_fus_DD": 2.1206, "P_fus_DT": 1788.5,