diff --git a/process/core/scan.py b/process/core/scan.py index 535eae7f36..6693aa4bfa 100644 --- a/process/core/scan.py +++ b/process/core/scan.py @@ -17,6 +17,7 @@ from process.core.solver.solver_handler import SolverHandler from process.data_structure.numerics import FiguresOfMerit, PROCESSRunMode from process.data_structure.scan_variables import IPNSCNS, NOUTVARS, ScanData +from process.models.availability import AvailabilityModel if TYPE_CHECKING: from process.core.model import DataStructure, Model @@ -1145,7 +1146,10 @@ def scan_select(self, nwp, swp, iscn): case 20: self.data.constraints.t_burn_min = swp[iscn - 1] case 22: - if self.data.costs.i_plant_availability == 1: + if ( + AvailabilityModel(self.data.costs.i_plant_availability) + != AvailabilityModel.USER_INPUT + ): raise ProcessValueError( "Do not scan f_t_plant_available if i_plant_availability=1" ) diff --git a/process/core/solver/objectives.py b/process/core/solver/objectives.py index 52fcc96b88..6e17276db1 100644 --- a/process/core/solver/objectives.py +++ b/process/core/solver/objectives.py @@ -3,6 +3,7 @@ from process.core.exceptions import ProcessValueError from process.core.model import DataStructure from process.data_structure.numerics import FiguresOfMerit +from process.models.availability import AvailabilityModel def objective_function(minmax: int, data: DataStructure) -> float: @@ -70,8 +71,14 @@ def objective_function(minmax: int, data: DataStructure) -> float: elif figure_of_merit == FiguresOfMerit.PULSE_LENGTH: objective_metric = data.times.t_plant_pulse_burn / 2.0e4 elif figure_of_merit == FiguresOfMerit.PLANT_AVAILABILITY_FACTOR: - if data.costs.i_plant_availability != 1: - raise ProcessValueError("minmax=15 requires i_plant_availability=1") + if ( + AvailabilityModel(data.costs.i_plant_availability) + == AvailabilityModel.USER_INPUT + ): + raise ProcessValueError( + "minmax=15 requires `f_t_plant_available` to be calculated, not user " + "input" + ) objective_metric = data.costs.f_t_plant_available elif figure_of_merit == FiguresOfMerit.MIN_R0_MAX_TAU_BURN: objective_metric = 0.95 * (data.physics.rmajor / 9.0) - 0.05 * ( diff --git a/process/models/availability.py b/process/models/availability.py index 06d953b928..cb42afa40b 100644 --- a/process/models/availability.py +++ b/process/models/availability.py @@ -1,5 +1,7 @@ import logging import math +from enum import IntEnum +from types import DynamicClassAttribute from scipy.special import comb as combinations @@ -11,6 +13,41 @@ logger = logging.getLogger(__name__) + +class AvailabilityModel(IntEnum): + """Enum for availability models""" + + USER_INPUT = (0, "Input value for `f_t_plant_available`") + WARD_TAYLOR = (1, "Ward and Taylor model (1999)") + MORRIS = (2, "Morris model (2015)") + ST = (3, "ST model (2023)") + + def __new__(cls, value: int, full_name: str): + """Create a new AvailabilityModel enum instance. + + Parameters + ---------- + value : int + The integer value of the enum member. + full_name : str + The full name/description of the availability model. + + Returns + ------- + AvailabilityModel + A new enum instance with the specified value and full_name. + """ + obj = int.__new__(cls, value) + obj._value_ = value + obj._full_name_ = full_name + return obj + + @DynamicClassAttribute + def full_name(self): + """The full name of the availability model.""" + return self._full_name_ + + DAY_SECONDS = 60 * 60 * 24 # Number of seconds in a day [s] @@ -20,13 +57,6 @@ YEAR_SECONDS = DAY_SECONDS * DAYS_IN_YEAR # Number of seconds in a year [s] -AVAILABILITY_MODELS = { - 0: "Input value for f_t_plant_available", - 1: "Ward and Taylor model (1999)", - 2: "Morris model (2015)", - 3: "ST model (2023)", -} - class Availability(Model): """Module containing plant availability routines @@ -58,7 +88,10 @@ def run(self, output: bool = False): output : indicate whether output should be written to the output file, or not """ - if self.data.costs.i_plant_availability == 3: + if ( + AvailabilityModel(self.data.costs.i_plant_availability) + == AvailabilityModel.ST + ): if self.data.physics.itart != 1: raise ProcessValueError( f"{self.data.costs.i_plant_availability=}" @@ -66,7 +99,10 @@ def run(self, output: bool = False): " Please set itart=1 to use this model." ) self.avail_st(output) # ST model (2023) - elif self.data.costs.i_plant_availability == 2: + elif ( + AvailabilityModel(self.data.costs.i_plant_availability) + == AvailabilityModel.MORRIS + ): self.avail_2(output) # Morris model (2015) else: self.avail(output) # Taylor and Ward model (1999) @@ -167,7 +203,10 @@ def avail(self, output: bool): # if i_plant_availability = 0 use input value for f_t_plant_available # Taylor and Ward 1999 model (i_plant_availability=1) - if self.data.costs.i_plant_availability == 1: + if ( + AvailabilityModel(self.data.costs.i_plant_availability) + == AvailabilityModel.WARD_TAYLOR + ): # Which component has the shorter life? if self.data.costs.life_div_fpy < self.data.fwbs.life_blkt_fpy: ld = self.data.costs.life_div_fpy @@ -294,7 +333,10 @@ def avail(self, output: bool): self.data.costs.life_plant, ) - if self.data.costs.i_plant_availability == 1: + if ( + AvailabilityModel(self.data.costs.i_plant_availability) + == AvailabilityModel.WARD_TAYLOR + ): if self.data.costs.life_div_fpy < self.data.fwbs.life_blkt_fpy: po.ovarre( self.outfile, @@ -331,7 +373,10 @@ def avail(self, output: bool): "OP ", ) - if self.data.costs.i_plant_availability == 0: + if ( + AvailabilityModel(self.data.costs.i_plant_availability) + == AvailabilityModel.USER_INPUT + ): po.ovarre( self.outfile, "Total plant availability fraction",