From 12648b3f187a125845585b89b6216320be96f6cb Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 15 Oct 2024 12:26:09 +0100 Subject: [PATCH 1/5] Added adjusted component lifetimes and test for avail_2 --- process/availability.py | 19 ++++++++- tests/unit/test_availability.py | 75 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/process/availability.py b/process/availability.py index e40d2e15ec..ee64025f2e 100644 --- a/process/availability.py +++ b/process/availability.py @@ -356,8 +356,23 @@ 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) + 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: @@ -397,7 +412,7 @@ def avail_2(self, output: bool): self.outfile, "Capacity factor: total lifetime elec. energy output / output power", "(cpfact)", - cpfact, + cv.cpfact, "OP ", ) diff --git a/tests/unit/test_availability.py b/tests/unit/test_availability.py index 117c293ee4..d46f0b98f8 100644 --- a/tests/unit/test_availability.py +++ b/tests/unit/test_availability.py @@ -477,6 +477,81 @@ 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", 500.0) + monkeypatch.setattr(tv, "tcycle", 5.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 = 71.73 + assert pytest.approx(cpfact_obs) == cpfact_exp + + bktlife_obs = fwbsv.bktlife + bktlife_exp = 11.849993029415865 + assert pytest.approx(bktlife_obs) == bktlife_exp + + divlife_obs = cv.divlife + divlife_exp = 4.182350480970305 + assert pytest.approx(divlife_obs) == divlife_exp + + cplife_obs = cv.cplife + cplife_exp = 30.0 + assert pytest.approx(cplife_obs) == cplife_exp + + def test_avail_st(monkeypatch, availability): """Test avail_st routine From be7e70fd1399e5cf0f2a93a6218b50d5997bac6b Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 17 Oct 2024 09:38:55 +0100 Subject: [PATCH 2/5] Costs now use calendar years for replacement costs --- process/costs.py | 54 ++++++++++++++++++++++--------- source/fortran/cost_variables.f90 | 11 ++++++- source/fortran/fwbs_variables.f90 | 3 ++ tests/unit/test_costs_1990.py | 8 ++--- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/process/costs.py b/process/costs.py index ec40d1298c..ae80f933fe 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, + "(fwbllife_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,25 @@ def coelc(self): + cost_variables.coeoam + coedecom ) + + def convert_fpy_to_calendar(self): + """ + 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 + 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..e082153f71 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) diff --git a/source/fortran/fwbs_variables.f90 b/source/fortran/fwbs_variables.f90 index e427777766..8121bc13bb 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] diff --git a/tests/unit/test_costs_1990.py b/tests/unit/test_costs_1990.py index 104de02499..bb67e953f7 100644 --- a/tests/unit/test_costs_1990.py +++ b/tests/unit/test_costs_1990.py @@ -5577,8 +5577,8 @@ class CoelcParam(NamedTuple): outfile=11, expected_coeoam=4.4099029328740929e20, expected_coecap=4.9891775218979061e21, - expected_coe=6.9525339143363677e21, - expected_coefuelt=1.4801870771036603e21, + expected_coe=9.25536206e21, + expected_coefuelt=3.78301522e21, expected_moneyint=1001.1727468691442, expected_capcost=7675.6577259967762, ), @@ -5657,8 +5657,8 @@ class CoelcParam(NamedTuple): outfile=11, expected_coeoam=1.2419424614419636, expected_coecap=15.547404530833255, - expected_coe=21.504209731681467, - expected_coefuelt=4.5834233757821812, + expected_coe=28.66823363, + expected_coefuelt=11.74744727, expected_moneyint=1025.4310038198375, expected_capcost=7861.6376959520912, ), From 9f076d041dc31991c8ec23c35f0e346eea2e2909 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 21 Oct 2024 11:45:58 +0100 Subject: [PATCH 3/5] Fix test failures on CI --- source/fortran/cost_variables.f90 | 3 +++ source/fortran/fwbs_variables.f90 | 1 + tests/unit/test_availability.py | 17 ++++++++++------ tests/unit/test_costs_1990.py | 32 +++++++++++++++++++++++++++---- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/source/fortran/cost_variables.f90 b/source/fortran/cost_variables.f90 index e082153f71..a33579b9ae 100644 --- a/source/fortran/cost_variables.f90 +++ b/source/fortran/cost_variables.f90 @@ -636,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/) @@ -661,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 @@ -670,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 8121bc13bb..6479992aba 100644 --- a/source/fortran/fwbs_variables.f90 +++ b/source/fortran/fwbs_variables.f90 @@ -666,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 d46f0b98f8..1be9df2d72 100644 --- a/tests/unit/test_availability.py +++ b/tests/unit/test_availability.py @@ -526,8 +526,13 @@ def mock_calc_u_unplanned_vacuum(*args, **kwargs): ) # Mock module variables - monkeypatch.setattr(tv, "tburn", 500.0) - monkeypatch.setattr(tv, "tcycle", 5.0) + 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) @@ -536,19 +541,19 @@ def mock_calc_u_unplanned_vacuum(*args, **kwargs): assert pytest.approx(cfactr_obs) == cfactr_exp cpfact_obs = cv.cpfact - cpfact_exp = 71.73 + cpfact_exp = 0.07173 assert pytest.approx(cpfact_obs) == cpfact_exp bktlife_obs = fwbsv.bktlife - bktlife_exp = 11.849993029415865 + bktlife_exp = 6.97058413 assert pytest.approx(bktlife_obs) == bktlife_exp divlife_obs = cv.divlife - divlife_exp = 4.182350480970305 + divlife_exp = 13.94116827 assert pytest.approx(divlife_obs) == divlife_exp cplife_obs = cv.cplife - cplife_exp = 30.0 + cplife_exp = 20.9117524 assert pytest.approx(cplife_obs) == cplife_exp diff --git a/tests/unit/test_costs_1990.py b/tests/unit/test_costs_1990.py index bb67e953f7..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=9.25536206e21, - expected_coefuelt=3.78301522e21, + 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=28.66823363, - expected_coefuelt=11.74744727, + 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) From ff38073de02f89aabe9bb25801750a188c4d4bf9 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 21 Oct 2024 13:43:38 +0100 Subject: [PATCH 4/5] Added cplife to output for avail_2 --- process/availability.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/process/availability.py b/process/availability.py index ee64025f2e..a7c6256a01 100644 --- a/process/availability.py +++ b/process/availability.py @@ -376,6 +376,25 @@ def avail_2(self, output: bool): # 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( @@ -516,17 +535,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", From 2a09f3a119e1eb9345adea9aab39ccade48a7fbe Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 21 Oct 2024 15:05:07 +0100 Subject: [PATCH 5/5] Addressing PR review requests --- process/availability.py | 1 + process/costs.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/process/availability.py b/process/availability.py index a7c6256a01..4c1dc06a9c 100644 --- a/process/availability.py +++ b/process/availability.py @@ -361,6 +361,7 @@ def avail_2(self, output: bool): # 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 diff --git a/process/costs.py b/process/costs.py index ae80f933fe..bf83620a06 100644 --- a/process/costs.py +++ b/process/costs.py @@ -129,7 +129,7 @@ def output(self): po.ovarrf( self.outfile, "First wall / blanket life (years)", - "(fwbllife_cal)", + "(bktlife_cal)", fwbs_variables.bktlife_cal, ) @@ -2873,7 +2873,8 @@ def coelc(self): + coedecom ) - def convert_fpy_to_calendar(self): + @staticmethod + def convert_fpy_to_calendar() -> None: """ Routine to convert component lifetimes in FPY to calendar years. Required for replacement component costs. @@ -2882,6 +2883,7 @@ def convert_fpy_to_calendar(self): # 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