diff --git a/documentation/proc-pages/eng-models/plant.md b/documentation/proc-pages/eng-models/plant-availability.md similarity index 59% rename from documentation/proc-pages/eng-models/plant.md rename to documentation/proc-pages/eng-models/plant-availability.md index 6a529a05..c07da428 100644 --- a/documentation/proc-pages/eng-models/plant.md +++ b/documentation/proc-pages/eng-models/plant-availability.md @@ -6,8 +6,8 @@ If `iavail = 0`, the input value of `cfactr` is used. If `iavail = 1`, a model by N. Taylor and D. Ward[^1] is used instead, in which `cfactr` is calculated taking into account the time taken to replace certain components of the fusion power core, and various unplanned unavailability fractions which may be set by the user, as summerised in Table 1. -| Input parameter | description | -| --- | --- | --- | +| Input parameter | Description | +| :-: | - | | `tbktrepl` | time needed to replace blanket (years) | | `tdivrepl` | time needed to replace divertor (years) | | `tcomrepl` | time needed to replace both blanket and divertor (years) | @@ -33,6 +33,60 @@ The unplanned downtime for the blanket is based on the number of cycles it exper It is assumed that the vacuum system can be maintained in parallel with blanket replacement, so it does not contribute to the planned downtime. The unplanned downtime is baed on an assumed failure rate for a cryo-pump, and a specified total number pumps, with some of them being redundant. The resulting downtime can be reduced to a negligible level if there are several redundant pumps, but in addition, there is a fixed unavailability to allow for common mode failures affecting several pumps. +If `iavail = 3`, the availability model for Spherical Tokamaks (ST) is implemented. + +!!! Warning "Warning" + Currently, this model only uses the centrepost to calculate the availability of an ST plant. Other systems/components will be added in the future. + +This model takes the user-specified time to replace a centrepost `tmain` and the centrepost lifetime `cplife` (calculated, see below) and calculates the number of maintenance cycles + +$$ t_{\text{main}} + t_{\text{CP,life}} = t_{\text{maint cycle}}. $$ + +The number of maintenance cycles over the lifetime of the plant is calculated and then the ceiling of this value is taken as the number of centreposts required over the lifetime of the plant + +$$ n_{\text{cycles}} = t_{\text{life}} / t_{\text{maint cycle}}, $$ + +$$ n_{\text{CP}} = \lceil n_{\text{cycles}} \rceil. $$ + +The planned unavailability is then what percent of a maintenance cycle is taken up by the user-specified maintenance time + +$$ U_{\text{planned}} = t_{\text{main}} / t_{\text{maint cycle}} $$ + +and the total operational time is given by + +$$ t_{\text{op}} = t_{\text{life}} (1 - U_{\text{planned}}). $$ + +The total availability of the plant is then given by + +$$ A_{\text{tot}} = 1 - (U_{\text{planned}} + U_{\text{unplanned}} + U_{\text{planned}}U_{\text{unplanned}}) $$ + +where $U_{unplanned}$ is unplanned unavailability which is provided by the user i.e. how often do you expect the centrepost to break over its lifetime. The cross term takes account of overlap between planned and unplanned unavailability. + +Finally, the capcity factor is given by + +$$ C = A_{\text{tot}} (t_{\text{burn}} / t_{\text{cycle}}) $$ + +where $t_{\text{burn}}$ is the burn time and $t_{\text{cycle}}$ is the full cycle time. + +## Centrepost Lifetime + +All availability models in PROCESS require the calculation of the centerpost lifetime, which is detailed here. + +!!! Note "Note" + The centrepost lifetime is calculated in full-power years (FPY). + +For superconducting magnets (`i_tf_sup = 1`), the centrepost lifetime is calculated as + +$$ t_{\text{CP,life}} = min(f_{\text{TF,max}}/(\phi_{\text{CP,max}}t_{\text{year}}),t_{\text{life}}) $$ + +where $f_{\text{TF,max}}$ is the max fast neutron fluence on the TF coil ($\mathrm{m}^{-2} \mathrm{s}$), $\phi_{\text{CP,max}}$ is the centrepost TF fast neutron flux ($\mathrm{m}^{-2}$ $\mathrm{s}^{-1}$) and $t_{\text{year}}$ is the number of seconds in a year. + +For copper or cryogenic aluminium magnets (`i_tf_sup = 0 or 2`), the centrepost lifetime is + +$$ t_{\text{CP,life}} = min(f_{\text{CP, allowable}}/P_{\text{wall}}, t_{\text{life}}) $$ + +where $f_{\text{CP, allowable}}$ is the allowable centrepost neutron fluence and $P_{\text{wall}}$ is the average neutron wall load ($\mathrm{MW} \mathrm{m}^{-2}$). + [^1]: P. J. Knight, *"PROCESS 3020: Plant Availability Model"*, Work File Note F/PL/PJK/PROCESS/CODE/
[^2]: M. Kovari, F. Fox, C. Harrington, R. Kembleton, P. Knight, H. Lux, J. Morris *"PROCESS: a systems code for fusion power plants - Part 2: Engineering"*, Fus. Eng. & Des. 104, 9-20 (2016) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index bbacff8a..0ce7a72a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,7 +80,7 @@ nav: - ITER Model: eng-models/heating_and_current_drive/NBI/iter_nb.md - Culham Model: eng-models/heating_and_current_drive/NBI/culham_nb.md - Cryostat and vacuum system: eng-models/cryostat-and-vacuum-system.md - - Plant Availability: eng-models/plant.md + - Plant Availability: eng-models/plant-availability.md - Power Requirements: eng-models/power-requirements.md - Vacuum Vessel: eng-models/vacuum-vessel.md - Unique Models: diff --git a/process/availability.py b/process/availability.py index ea2e7c7a..78e5ceef 100644 --- a/process/availability.py +++ b/process/availability.py @@ -47,12 +47,19 @@ def run(self, output: bool = False): 0 | Input value for cfactr 1 | Ward and Taylor model (1999) 2 | Morris model (2015) + 3 | ST model (2023) :param output: indicate whether output should be written to the output file, or not (default = False) :type output: boolean """ - if cv.iavail > 1: + if cv.iavail == 3: + if pv.itart != 1: + raise ValueError( + f"{cv.iavail=} is for a Spherical Tokamak. Please set itart=1 to use this model." + ) + self.avail_st(output) # ST model (2023) + elif cv.iavail == 2: self.avail_2(output) # Morris model (2015) else: self.avail(output) # Taylor and Ward model (1999) @@ -115,17 +122,7 @@ def avail(self, output: bool): # Centrepost lifetime (years) (ST machines only) if pv.itart == 1: - # SC magnets CP lifetime - # Rem : only the TF maximum fluence is considered for now - if tfv.i_tf_sup == 1: - cv.cplife = min( - ctv.nflutfmax / (fwbsv.neut_flux_cp * YEAR_SECONDS), cv.tlife - ) - - # Aluminium/Copper magnets CP lifetime - # For now, we keep the original def, developped for GLIDCOP magnets ... - else: - cv.cplife = min(cv.cpstflnc / pv.wallmw, cv.tlife) + cv.cplife = self.cp_lifetime() # Plant Availability (iavail=0,1) @@ -450,17 +447,7 @@ def calc_u_planned(self, output: bool) -> float: # Centrepost lifetime (years) (ST only) if pv.itart == 1: - # SC magnets CP lifetime - # Rem : only the TF maximum fluence is considered for now - if tfv.i_tf_sup == 1: - cv.cplife = min( - ctv.nflutfmax / (fwbsv.neut_flux_cp * YEAR_SECONDS), cv.tlife - ) - - # Aluminium/Copper magnets CP lifetime - # For now, we keep the original def, developped for GLIDCOP magnets ... - else: - cv.cplife = min(cv.cpstflnc / pv.wallmw, cv.tlife) + cv.cplife = self.cp_lifetime() # Current drive lifetime (assumed equal to first wall and blanket lifetime) cv.cdrlife = fwbsv.bktlife @@ -499,7 +486,6 @@ def calc_u_planned(self, output: bool) -> float: # Output if output: - po.oheadr(self.outfile, "Plant Availability (2014 Model)") po.ocmmnt(self.outfile, "Planned unavailability:") @@ -610,7 +596,6 @@ def calc_u_unplanned_magnets(self, output: bool) -> float: # !!!!!!!!! if output: - po.ocmmnt(self.outfile, "Magnets:") po.oblnkl(self.outfile) po.ovarre( @@ -944,7 +929,6 @@ def calc_u_unplanned_vacuum(self, output: bool) -> float: sum_prob = 0.0e0 for n in range(cv.redun_vac + 1, total_pumps + 1): - # Probability for n failures in the operational period, n > number of redundant pumps # vac_fail_p.append(maths_library.binomial(total_pumps,n) * (cryo_nfailure_rate**(total_pumps-n)) *(cryo_failure_rate**n)) @@ -994,3 +978,158 @@ def calc_u_unplanned_vacuum(self, output: bool) -> float: po.oblnkl(self.outfile) return u_unplanned_vacuum + + def avail_st(self, output: bool): + """Routine to calculate availability for plant with a Spherical Tokamak + + :param output: indicate whether output should be written to the output file, or not + :type output: boolean + """ + # CP lifetime + cv.cplife = self.cp_lifetime() + + # Time for a maintenance cycle (years) + # Lifetime of CP + time to replace + maint_cycle = cv.cplife + cv.tmain + + # Number of maintenance cycles over plant lifetime + n_cycles_main = cv.tlife / maint_cycle + + # Number of centre columns over plant lifetime + n_centre_cols = math.ceil(n_cycles_main) + + # Planned unavailability + u_planned = cv.tmain / maint_cycle + + # Operational time (years) + cv.t_operation = cv.tlife * (1.0e0 - u_planned) + + # Total availability + cv.cfactr = max( + 1.0e0 - (u_planned + cv.u_unplanned + u_planned * cv.u_unplanned), 0.0e0 + ) + + # Capacity factor + cv.cpfact = cv.cfactr * (tv.tburn / tv.tcycle) + + if output: + po.oheadr(self.outfile, "Plant Availability") + if tfv.i_tf_sup == 1: + po.ovarre( + self.outfile, + "Max fast neutron fluence on TF coil (n/m2)", + "(nflutfmax)", + ctv.nflutfmax, + "OP ", + ) + po.ovarre( + self.outfile, + "Centrepost TF fast neutron flux (E > 0.1 MeV) (m^(-2).^(-1))", + "(neut_flux_cp)", + fwbsv.neut_flux_cp, + "OP ", + ) + else: + po.ovarre( + self.outfile, + "Allowable ST centrepost neutron fluence (MW-yr/m2)", + "(cpstflnc)", + cv.cpstflnc, + "OP ", + ) + po.ovarre( + self.outfile, + "Average neutron wall load (MW/m2)", + "(wallmw)", + pv.wallmw, + "OP ", + ) + po.ovarre( + self.outfile, + "Centrepost lifetime (years)", + "(cplife)", + cv.cplife, + "OP ", + ) + po.oblnkl(self.outfile) + po.ovarre( + self.outfile, + "Length of maintenance cycle (years)", + "(maint_cycle)", + maint_cycle, + "OP ", + ) + po.ovarre( + self.outfile, + "Number of maintenance cycles over lifetime", + "(n_cycles_main)", + n_cycles_main, + "OP ", + ) + po.ovarre( + self.outfile, + "Number of centre columns over lifetime", + "(n_centre_cols)", + n_centre_cols, + "OP ", + ) + po.oblnkl(self.outfile) + po.ovarre( + self.outfile, + "Total planned unavailability", + "(u_planned)", + u_planned, + "OP ", + ) + po.ovarre( + self.outfile, + "Total unplanned unavailability", + "(u_unplanned)", + cv.u_unplanned, + "IP ", + ) + po.ovarre( + self.outfile, + "Total plant availability fraction", + "(cfactr)", + cv.cfactr, + "OP ", + ) + po.ovarre( + self.outfile, + "Capacity factor: total lifetime elec. energy output / output power", + "(cpfact)", + cv.cpfact, + "OP ", + ) + po.ovarre( + self.outfile, + "Total DT operational time (years)", + "(t_operation)", + cv.t_operation, + "OP ", + ) + po.ovarre( + self.outfile, "Total plant lifetime (years)", "(tlife)", cv.tlife, "OP" + ) + + def cp_lifetime(self): + """Calculate Centrepost Lifetime + + This routine calculates the lifetime of the centrepost, + either for superconducting or aluminium/resistive magnets. + + :returns: CP lifetime + :rtype: float + """ + # SC magnets CP lifetime + # Rem : only the TF maximum fluence is considered for now + if tfv.i_tf_sup == 1: + cplife = min(ctv.nflutfmax / (fwbsv.neut_flux_cp * YEAR_SECONDS), cv.tlife) + + # Aluminium/Copper magnets CP lifetime + # For now, we keep the original def, developped for GLIDCOP magnets ... + else: + cplife = min(cv.cpstflnc / pv.wallmw, cv.tlife) + + return cplife diff --git a/source/fortran/cost_variables.f90 b/source/fortran/cost_variables.f90 index 1eeadd9b..f677f61b 100644 --- a/source/fortran/cost_variables.f90 +++ b/source/fortran/cost_variables.f90 @@ -198,6 +198,7 @@ module cost_variables !! - =0 use input value for cfactr !! - =1 calculate cfactr using Taylor and Ward 1999 model !! - =2 calculate cfactr using new (2015) model + !! - =3 calculate cfactr using ST model integer :: ibkt_life !! Switch for fw/blanket lifetime calculation in availability module: @@ -342,6 +343,12 @@ module cost_variables real(dp) :: tlife !! Full power year plant lifetime (years) + real(dp) :: tmain + !! Maintenance time for replacing CP (years) (iavail = 3) + + real(dp) :: u_unplanned + !! User-input CP unplanned unavailability (iavail = 3) + real(dp), parameter :: ucad = 180.0D0 !! unit cost for administration buildings (M$/m3) diff --git a/source/fortran/input.f90 b/source/fortran/input.f90 index f7f4eafe..b3c0d68e 100644 --- a/source/fortran/input.f90 +++ b/source/fortran/input.f90 @@ -256,7 +256,7 @@ subroutine parse_input_file(in_file,out_file,show_changes) ucblli, ucpfcb, tlife, ipnet, fcdfuel, ucbus, ucpfb, uchts, & maintenance_fwbs, fwbs_prob_fail, uclh, ucblss, ucblvd, ucsc, ucturb, & ucpens, cland, ucwindpf, i_cp_lifetime, cplife_input, & - startupratio + startupratio, tmain, u_unplanned use current_drive_variables, only: pinjfixmw, etaech, pinjalw, etanbi, & ftritbm, gamma_ecrh, pheat, beamwd, enbeam, pheatfix, bscfmax, & forbitloss, nbshield, tbeamin, feffcd, iefrf, iefrffix, irfcd, cboot, & @@ -2563,6 +2563,12 @@ subroutine parse_input_file(in_file,out_file,show_changes) case ('startupratio') call parse_real_variable('startupratio', startupratio, 0.0D0, 10.0D0, & 'Ratio (additional HCD power for start-up) / (flat-top operational requirements)') + case ('tmain') + call parse_real_variable('tmain', tmain, 0.0D0, 100.0D0, & + 'Maintenance time for replacing CP (years) (iavail = 3)') + case ('u_unplanned') + call parse_real_variable('u_unplanned', u_unplanned, 0.0D0, 1.0D0, & + 'User-input CP unplanned unavailability (iavail = 3)') ! Unit cost settings @@ -2781,7 +2787,7 @@ subroutine parse_input_file(in_file,out_file,show_changes) ! Availability settings case ('iavail') - call parse_int_variable('iavail', iavail, 0, 2, & + call parse_int_variable('iavail', iavail, 0, 3, & 'Switch for plant availability model') case ('ibkt_life') call parse_int_variable('ibkt_life', ibkt_life, 0, 2, & diff --git a/tests/unit/test_availability.py b/tests/unit/test_availability.py index e711c193..2a6b76fb 100644 --- a/tests/unit/test_availability.py +++ b/tests/unit/test_availability.py @@ -2,7 +2,11 @@ from process import fortran from process.availability import Availability from process.fortran import cost_variables as cv +from process.fortran import physics_variables as pv from process.fortran import tfcoil_variables as tfv +from process.fortran import constraint_variables as ctv +from process.fortran import fwbs_variables as fwbsv +from process.fortran import times_variables as tv import pytest from pytest import approx @@ -385,3 +389,49 @@ def test_calc_u_unplanned_fwbs(calc_u_unplanned_fwbs_fix, availability): # then assert the result is the expected one result = availability.calc_u_unplanned_fwbs(output=False) assert result == calc_u_unplanned_fwbs_fix + + +def test_avail_st(monkeypatch, availability): + """Test avail_st routine + + :param monkeypatch: Mock fixture + :type monkeypatch: object + + :param availability: fixture containing an initialised `Availability` object + :type availability: tests.unit.test_availability.availability (functional fixture) + """ + + monkeypatch.setattr(cv, "tmain", 1.0) + monkeypatch.setattr(cv, "tlife", 30.0) + monkeypatch.setattr(cv, "u_unplanned", 0.1) + monkeypatch.setattr(tv, "tburn", 5.0) + monkeypatch.setattr(tv, "tcycle", 10.0) + + availability.avail_st(output=False) + + assert pytest.approx(cv.t_operation) == 29.03225806 + assert pytest.approx(cv.cfactr) == 0.86451613 + assert pytest.approx(cv.cpfact) == 0.43225806 + + +@pytest.mark.parametrize("i_tf_sup, exp", ((1, 6.337618), (0, 4))) +def test_cp_lifetime(monkeypatch, availability, i_tf_sup, exp): + """Test cp_lifetime routine + + :param monkeypatch: Mock fixture + :type monkeypatch: object + + :param availability: fixture containing an initialised `Availability` object + :type availability: tests.unit.test_availability.availability (functional fixture) + """ + + monkeypatch.setattr(tfv, "i_tf_sup", i_tf_sup) + monkeypatch.setattr(ctv, "nflutfmax", 1.0e23) + monkeypatch.setattr(fwbsv, "neut_flux_cp", 5.0e14) + monkeypatch.setattr(cv, "cpstflnc", 20.0) + monkeypatch.setattr(pv, "wallmw", 5.0) + monkeypatch.setattr(cv, "tlife", 30.0) + + cplife = availability.cp_lifetime() + + assert pytest.approx(cplife) == exp