diff --git a/process/availability.py b/process/availability.py index e40d2e15ec..4c1dc06a9c 100644 --- a/process/availability.py +++ b/process/availability.py @@ -356,11 +356,46 @@ def avail_2(self, output: bool): 1.0e0 - (u_planned + u_unplanned + u_planned * u_unplanned), 0.0e0 ) + # Modify lifetimes to take account of the availability + if ifev.ife != 1: + # First wall / blanket + if fwbsv.bktlife < cv.tlife: + fwbsv.bktlife = min(fwbsv.bktlife / cv.cfactr, cv.tlife) + # Current drive system lifetime (assumed equal to first wall and blanket lifetime) + cv.cdrlife = fwbsv.bktlife + + # Divertor + if cv.divlife < cv.tlife: + cv.divlife = min(cv.divlife / cv.cfactr, cv.tlife) + + # Centrepost + if pv.itart == 1 and cv.cplife < cv.tlife: + cv.cplife = min(cv.cplife / cv.cfactr, cv.tlife) + # Capacity factor - cpfact = cv.cfactr * (tv.tburn / tv.tcycle) + cv.cpfact = cv.cfactr * (tv.tburn / tv.tcycle) # Output if output: + po.ovarre( + self.outfile, + "First wall / blanket lifetime (FPY)", + "(bktlife)", + fwbsv.bktlife, + "OP ", + ) + po.ovarre( + self.outfile, "Divertor lifetime (FPY)", "(divlife)", cv.divlife, "OP " + ) + if pv.itart == 1: + po.ovarre( + self.outfile, + "Centrepost lifetime (FPY)", + "(cplife)", + cv.cplife, + "OP ", + ) + po.oblnkl(self.outfile) po.ocmmnt(self.outfile, "Total unavailability:") po.oblnkl(self.outfile) po.ovarre( @@ -397,7 +432,7 @@ def avail_2(self, output: bool): self.outfile, "Capacity factor: total lifetime elec. energy output / output power", "(cpfact)", - cpfact, + cv.cpfact, "OP ", ) @@ -501,17 +536,6 @@ def calc_u_planned(self, output: bool) -> float: "(adivflnc)", cv.adivflnc, ) - po.ovarre( - self.outfile, - "First wall / blanket lifetime (FPY)", - "(bktlife)", - fwbsv.bktlife, - "OP ", - ) - po.ovarre( - self.outfile, "Divertor lifetime (FPY)", "(divlife)", cv.divlife, "OP " - ) - po.ovarin( self.outfile, "Number of remote handling systems", diff --git a/process/costs.py b/process/costs.py index ec40d1298c..bf83620a06 100644 --- a/process/costs.py +++ b/process/costs.py @@ -78,6 +78,10 @@ def run(self): to account for Nth-of-a-kind cost reductions.

The code is arranged in the order of the standard accounts. """ + # Convert FPY component lifetimes to calendar years + # for replacment components + self.convert_fpy_to_calendar() + self.acc21() # Account 22 : Fusion power island @@ -125,23 +129,23 @@ def output(self): po.ovarrf( self.outfile, "First wall / blanket life (years)", - "(fwbllife)", - fwbs_variables.bktlife, + "(bktlife_cal)", + fwbs_variables.bktlife_cal, ) if ife_variables.ife != 1: po.ovarrf( self.outfile, "Divertor life (years)", - "(divlife.)", - cost_variables.divlife, + "(divlife_cal)", + cost_variables.divlife_cal, ) if physics_variables.itart == 1: po.ovarrf( self.outfile, "Centrepost life (years)", - "(cplife.)", - cost_variables.cplife, + "(cplife_cal)", + cost_variables.cplife_cal, ) po.ovarrf( @@ -2620,13 +2624,9 @@ def coelc(self): # Costs due to first wall and blanket renewal # =========================================== - # Operational life - - fwbllife = fwbs_variables.bktlife - # Compound interest factor - feffwbl = (1.0e0 + cost_variables.discount_rate) ** fwbllife + feffwbl = (1.0e0 + cost_variables.discount_rate) ** fwbs_variables.bktlife_cal # Capital recovery factor @@ -2642,7 +2642,7 @@ def coelc(self): ) if cost_variables.ifueltyp == 2: - annfwbl = annfwbl * (1.0e0 - fwbllife / cost_variables.tlife) + annfwbl = annfwbl * (1.0e0 - fwbs_variables.bktlife / cost_variables.tlife) # Cost of electricity due to first wall/blanket replacements @@ -2657,7 +2657,9 @@ def coelc(self): else: # Compound interest factor - fefdiv = (1.0e0 + cost_variables.discount_rate) ** cost_variables.divlife + fefdiv = ( + 1.0e0 + cost_variables.discount_rate + ) ** cost_variables.divlife_cal # Capital recovery factor @@ -2687,7 +2689,7 @@ def coelc(self): if (physics_variables.itart == 1) and (ife_variables.ife != 1): # Compound interest factor - fefcp = (1.0e0 + cost_variables.discount_rate) ** cost_variables.cplife + fefcp = (1.0e0 + cost_variables.discount_rate) ** cost_variables.cplife_cal # Capital recovery factor @@ -2717,7 +2719,7 @@ def coelc(self): # Compound interest factor - fefcdr = (1.0e0 + cost_variables.discount_rate) ** cost_variables.cdrlife + fefcdr = (1.0e0 + cost_variables.discount_rate) ** cost_variables.cdrlife_cal # Capital recovery factor @@ -2870,3 +2872,27 @@ def coelc(self): + cost_variables.coeoam + coedecom ) + + @staticmethod + def convert_fpy_to_calendar() -> None: + """ + Routine to convert component lifetimes in FPY to calendar years. + Required for replacement component costs. + Author: J Foster, CCFE, Culham Campus + """ + # FW/Blanket and HCD + if fwbs_variables.bktlife < cost_variables.tlife: + fwbs_variables.bktlife_cal = fwbs_variables.bktlife * cost_variables.cfactr + # Current drive system lifetime (assumed equal to first wall and blanket lifetime) + cost_variables.cdrlife_cal = fwbs_variables.bktlife_cal + + # Divertor + if cost_variables.divlife < cost_variables.tlife: + cost_variables.divlife_cal = cost_variables.divlife * cost_variables.cfactr + + # Centrepost + if ( + physics_variables.itart == 1 + and cost_variables.cplife < cost_variables.tlife + ): + cost_variables.cplife_cal = cost_variables.cplife * cost_variables.cfactr diff --git a/source/fortran/cost_variables.f90 b/source/fortran/cost_variables.f90 index 6db88ed56f..a33579b9ae 100644 --- a/source/fortran/cost_variables.f90 +++ b/source/fortran/cost_variables.f90 @@ -48,7 +48,10 @@ module cost_variables !! total plant direct cost (M$) real(dp) :: cdrlife - !! lifetime of heating/current drive system (y) + !! Full power year lifetime of heating/current drive system (y) + + real(dp) :: cdrlife_cal + !! Calendar year lifetime of heating/current drive system (y) real(dp) :: cfactr !! Total plant availability fraction; input if `iavail=0` @@ -140,6 +143,9 @@ module cost_variables real(dp) :: cplife !! Calculated full power year lifetime of centrepost (years) + real(dp) :: cplife_cal + !! Calculated calendar year lifetime of centrepost (years) + real(dp) :: cpstcst !! ST centrepost direct cost (M$) @@ -167,6 +173,9 @@ module cost_variables real(dp) :: divlife !! Full power lifetime of divertor (y) + real(dp) :: divlife_cal + !! Calendar year lifetime of divertor (y) + real(dp) :: dtlife !! period prior to the end of the plant life that the decommissioning fund is used (years) @@ -627,6 +636,7 @@ subroutine init_cost_variables cdcost = 0.0D0 cdirt = 0.0D0 cdrlife = 0.0D0 + cdrlife_cal = 0.0D0 cfactr = 0.75D0 cpfact = 0.0D0 cfind = (/0.244D0, 0.244D0, 0.244D0, 0.29D0/) @@ -652,6 +662,7 @@ subroutine init_cost_variables cost_model = 1 cowner = 0.15D0 cplife = 0.0D0 + cplife_cal = 0.0D0 cpstcst = 0.0D0 cpstflnc = 10.0D0 crctcore = 0.0D0 @@ -661,6 +672,7 @@ subroutine init_cost_variables dintrt = 0.0D0 divcst = 0.0D0 divlife = 0.0D0 + divlife_cal = 0.0D0 dtlife = 0.0D0 fcap0 = 1.165D0 fcap0cp = 1.08D0 diff --git a/source/fortran/fwbs_variables.f90 b/source/fortran/fwbs_variables.f90 index e427777766..6479992aba 100644 --- a/source/fortran/fwbs_variables.f90 +++ b/source/fortran/fwbs_variables.f90 @@ -18,6 +18,9 @@ module fwbs_variables real(dp) :: bktlife !! Full power blanket lifetime (years) + real(dp) :: bktlife_cal + !! Calendar year blanket lifetime (years) + real(dp) :: coolmass !! mass of water coolant (in shield, blanket, first wall, divertor) [kg] @@ -663,6 +666,7 @@ subroutine init_fwbs_variables implicit none bktlife = 0.0D0 + bktlife_cal = 0.0D0 coolmass = 0.0D0 vvmass = 0.0D0 denstl = 7800.0D0 diff --git a/tests/unit/test_availability.py b/tests/unit/test_availability.py index 117c293ee4..1be9df2d72 100644 --- a/tests/unit/test_availability.py +++ b/tests/unit/test_availability.py @@ -477,6 +477,86 @@ def test_calc_u_unplanned_fwbs(calc_u_unplanned_fwbs_fix, availability): assert result == calc_u_unplanned_fwbs_fix +def test_avail_2(monkeypatch, availability): + """Test avail_2 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) + """ + # Mock return values for for functions called in avail_2 + def mock_calc_u_planned(*args, **kwargs): + return 0.01 + + def mock_calc_u_unplanned_magnets(*args, **kwargs): + return 0.02 + + def mock_calc_u_unplanned_divertor(*args, **kwargs): + return 0.03 + + def mock_calc_u_unplanned_fwbs(*args, **kwargs): + return 0.04 + + def mock_calc_u_unplanned_bop(*args, **kwargs): + return 0.05 + + def mock_calc_u_unplanned_hcd(*args, **kwargs): + return 0.06 + + def mock_calc_u_unplanned_vacuum(*args, **kwargs): + return 0.07 + + # Mock module functions + monkeypatch.setattr(availability, "calc_u_planned", mock_calc_u_planned) + monkeypatch.setattr( + availability, "calc_u_unplanned_magnets", mock_calc_u_unplanned_magnets + ) + monkeypatch.setattr( + availability, "calc_u_unplanned_divertor", mock_calc_u_unplanned_divertor + ) + monkeypatch.setattr( + availability, "calc_u_unplanned_fwbs", mock_calc_u_unplanned_fwbs + ) + monkeypatch.setattr(availability, "calc_u_unplanned_bop", mock_calc_u_unplanned_bop) + monkeypatch.setattr(availability, "calc_u_unplanned_hcd", mock_calc_u_unplanned_hcd) + monkeypatch.setattr( + availability, "calc_u_unplanned_vacuum", mock_calc_u_unplanned_vacuum + ) + + # Mock module variables + monkeypatch.setattr(tv, "tburn", 5.0) + monkeypatch.setattr(tv, "tcycle", 50.0) + monkeypatch.setattr(ifev, "ife", 0) + monkeypatch.setattr(pv, "itart", 1) + monkeypatch.setattr(fwbsv, "bktlife", 5.0) + monkeypatch.setattr(cv, "divlife", 10.0) + monkeypatch.setattr(cv, "cplife", 15.0) + + availability.avail_2(False) + + cfactr_obs = cv.cfactr + cfactr_exp = 0.7173 + assert pytest.approx(cfactr_obs) == cfactr_exp + + cpfact_obs = cv.cpfact + cpfact_exp = 0.07173 + assert pytest.approx(cpfact_obs) == cpfact_exp + + bktlife_obs = fwbsv.bktlife + bktlife_exp = 6.97058413 + assert pytest.approx(bktlife_obs) == bktlife_exp + + divlife_obs = cv.divlife + divlife_exp = 13.94116827 + assert pytest.approx(divlife_obs) == divlife_exp + + cplife_obs = cv.cplife + cplife_exp = 20.9117524 + assert pytest.approx(cplife_obs) == cplife_exp + + def test_avail_st(monkeypatch, availability): """Test avail_st routine diff --git a/tests/unit/test_costs_1990.py b/tests/unit/test_costs_1990.py index 104de02499..50544566ed 100644 --- a/tests/unit/test_costs_1990.py +++ b/tests/unit/test_costs_1990.py @@ -5426,16 +5426,22 @@ class CoelcParam(NamedTuple): divlife: Any = None + divlife_cal: Any = None + coefuelt: Any = None moneyint: Any = None cdrlife: Any = None + cdrlife_cal: Any = None + capcost: Any = None cplife: Any = None + cplife_cal: Any = None + fwallcst: Any = None fcr0: Any = None @@ -5466,6 +5472,8 @@ class CoelcParam(NamedTuple): bktlife: Any = None + bktlife_cal: Any = None + uctarg: Any = None ife: Any = None @@ -5517,11 +5525,14 @@ class CoelcParam(NamedTuple): divcst=88.904644548525795, ucfuel=3.4500000000000002, divlife=6.1337250397740126, + divlife_cal=6.1337250397740126, coefuelt=0, moneyint=0, cdrlife=19.216116010620578, + cdrlife_cal=19.216116010620578, capcost=0, cplife=0, + cplife_cal=0, fwallcst=143.19827300247195, fcr0=0.065000000000000016, discount_rate=0.060000000000000012, @@ -5565,6 +5576,7 @@ class CoelcParam(NamedTuple): order="F", ).transpose(), bktlife=19.216116010620578, + bktlife_cal=19.216116010620578, uctarg=0.29999999999999999, ife=0, reprat=0, @@ -5577,8 +5589,8 @@ class CoelcParam(NamedTuple): outfile=11, expected_coeoam=4.4099029328740929e20, expected_coecap=4.9891775218979061e21, - expected_coe=6.9525339143363677e21, - expected_coefuelt=1.4801870771036603e21, + expected_coe=6.95253391e21, + expected_coefuelt=1.48018708e21, expected_moneyint=1001.1727468691442, expected_capcost=7675.6577259967762, ), @@ -5597,11 +5609,14 @@ class CoelcParam(NamedTuple): divcst=88.904644548525795, ucfuel=3.4500000000000002, divlife=6.145510750914414, + divlife_cal=6.145510750914414, coefuelt=1.4801870771036603e21, moneyint=1001.1727468691442, cdrlife=19.222115557991025, + cdrlife_cal=19.222115557991025, capcost=7675.6577259967762, cplife=0, + cplife_cal=0, fwallcst=167.7865317453867, fcr0=0.065000000000000016, discount_rate=0.060000000000000012, @@ -5645,6 +5660,7 @@ class CoelcParam(NamedTuple): order="F", ).transpose(), bktlife=19.222115557991025, + bktlife_cal=19.222115557991025, uctarg=0.29999999999999999, ife=0, reprat=0, @@ -5657,8 +5673,8 @@ class CoelcParam(NamedTuple): outfile=11, expected_coeoam=1.2419424614419636, expected_coecap=15.547404530833255, - expected_coe=21.504209731681467, - expected_coefuelt=4.5834233757821812, + expected_coe=21.50420973, + expected_coefuelt=4.58342338, expected_moneyint=1025.4310038198375, expected_capcost=7861.6376959520912, ), @@ -5705,16 +5721,22 @@ def test_coelc(coelcparam, monkeypatch, costs): monkeypatch.setattr(cost_variables, "divlife", coelcparam.divlife) + monkeypatch.setattr(cost_variables, "divlife_cal", coelcparam.divlife_cal) + monkeypatch.setattr(cost_variables, "coefuelt", coelcparam.coefuelt) monkeypatch.setattr(cost_variables, "moneyint", coelcparam.moneyint) monkeypatch.setattr(cost_variables, "cdrlife", coelcparam.cdrlife) + monkeypatch.setattr(cost_variables, "cdrlife_cal", coelcparam.cdrlife_cal) + monkeypatch.setattr(cost_variables, "capcost", coelcparam.capcost) monkeypatch.setattr(cost_variables, "cplife", coelcparam.cplife) + monkeypatch.setattr(cost_variables, "cplife_cal", coelcparam.cplife_cal) + monkeypatch.setattr(cost_variables, "fwallcst", coelcparam.fwallcst) monkeypatch.setattr(cost_variables, "fcr0", coelcparam.fcr0) @@ -5745,6 +5767,8 @@ def test_coelc(coelcparam, monkeypatch, costs): monkeypatch.setattr(fwbs_variables, "bktlife", coelcparam.bktlife) + monkeypatch.setattr(fwbs_variables, "bktlife_cal", coelcparam.bktlife_cal) + monkeypatch.setattr(ife_variables, "uctarg", coelcparam.uctarg) monkeypatch.setattr(ife_variables, "ife", coelcparam.ife)