From f5a2a517c35e66822767fa156c4c8fa139d5a322 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 5 Mar 2024 12:12:27 -0700 Subject: [PATCH 1/6] `scaled_capital_cost` now reported in SC + tests --- reV/supply_curve/points.py | 1 + tests/test_econ_of_scale.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 7de5a3300..170f4a555 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1980,6 +1980,7 @@ def economies_of_scale(cap_cost_scale, summary): summary['raw_lcoe'] = eos.raw_lcoe summary['mean_lcoe'] = eos.scaled_lcoe summary['capital_cost_scalar'] = eos.capital_cost_scalar + summary['scaled_capital_cost'] = eos.scaled_capital_cost return summary diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index ef9f352e2..272df05ed 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -162,6 +162,8 @@ def test_econ_of_scale_baseline(): sc_df = pd.read_csv(out_fp_sc + ".csv") assert np.allclose(base_df['mean_lcoe'], sc_df['mean_lcoe']) assert (sc_df['capital_cost_scalar'] == 1).all() + assert np.allclose(sc_df['mean_capital_cost'], + sc_df['scaled_capital_cost']) def test_sc_agg_econ_scale(): @@ -225,6 +227,8 @@ def test_sc_agg_econ_scale(): / aep + data['variable_operating_cost']) assert np.allclose(scalars, sc_df['capital_cost_scalar']) + assert np.allclose(scalars * sc_df['mean_capital_cost'], + sc_df['scaled_capital_cost']) assert np.allclose(true_scaled_lcoe, sc_df['mean_lcoe']) assert np.allclose(true_raw_lcoe, sc_df['raw_lcoe']) From bc921a88e7050e69810bd3030597646a4b958f0c Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 5 Mar 2024 12:12:35 -0700 Subject: [PATCH 2/6] Bump version for new output --- reV/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/version.py b/reV/version.py index 60eae1ab6..06d3b84ec 100644 --- a/reV/version.py +++ b/reV/version.py @@ -2,4 +2,4 @@ reV Version number """ -__version__ = "0.8.6" +__version__ = "0.8.7" From 7f635ee66dd65ae50eaead34994b5535b661ae40 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 6 Mar 2024 13:30:17 -0700 Subject: [PATCH 3/6] Add `sc_point_xxx` outputs that are based on sc point capacity --- reV/supply_curve/points.py | 99 +++++++++++++++++++++-- tests/test_supply_curve_sc_aggregation.py | 24 ++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 170f4a555..9a7ba7742 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1796,6 +1796,89 @@ def capacity_ac(self): return self.area * self.power_density_ac + @property + def sc_point_capital_cost(self): + """Get the capital cost for the entire SC point. + + This method scales the capital cost based on the included-area + capacity. The calculation requires 'capital_cost' and + 'system_capacity' in the generation file, otherwise it returns + `None`. + + Returns + ------- + sc_point_capital_cost : float | None + Total supply curve point capital cost ($). + """ + + required = ('capital_cost', 'system_capacity') + if not all(k in self.mean_h5_dsets_data for k in required): + return None + + cap_cost_per_mw = (self.mean_h5_dsets_data['capital_cost'] + / self.mean_h5_dsets_data['system_capacity']) + return cap_cost_per_mw * self.capacity + + @property + def sc_point_fixed_operating_cost(self): + """Get the fixed operating cost for the entire SC point. + + This method scales the fixed operating cost based on the + included-area capacity. The calculation requires + 'fixed_operating_cost' and 'system_capacity' in the generation + file, otherwise it returns `None`. + + Returns + ------- + sc_point_fixed_operating_cost : float | None + Total supply curve point fixed operating cost ($). + """ + + required = ('fixed_operating_cost', 'system_capacity') + if not all(k in self.mean_h5_dsets_data for k in required): + return None + + fixed_cost_per_mw = (self.mean_h5_dsets_data['fixed_operating_cost'] + / self.mean_h5_dsets_data['system_capacity']) + return fixed_cost_per_mw * self.capacity + + @property + def sc_point_annual_energy(self): + """Get the total annual energy (MWh) for the entire SC point. + + This value is computed using the capacity of the supply curve + point as well as the mean capacity factor. If the mean capacity + factor is `None`, this value will also be `None`. + + Returns + ------- + sc_point_annual_energy : float | None + Total annual energy (MWh) for the entire SC point. + """ + if self.mean_cf is None: + return None + + return self.mean_cf * self.capacity * 8760 + + @property + def sc_point_annual_energy_ac(self): + """Get the total AC annual energy (MWh) for the entire SC point. + + This value is computed using the AC capacity of the supply curve + point as well as the mean capacity factor. If either the mean + capacity factor or the AC capacity value is `None`, this value + will also be `None`. + + Returns + ------- + sc_point_annual_energy_ac : float | None + Total AC annual energy (MWh) for the entire SC point. + """ + if self.mean_cf is None or self.capacity_ac is None: + return None + + return self.mean_cf * self.capacity_ac * 8760 + @property def h5_dsets_data(self): """Get any additional/supplemental h5 dataset data to summarize. @@ -1928,11 +2011,13 @@ def point_summary(self, args=None): 'timezone': self.timezone, } - if self.capacity_ac is not None: - ARGS['capacity_ac'] = self.capacity_ac - - if self.offshore is not None: - ARGS['offshore'] = self.offshore + extra_atts = ['capacity_ac', 'offshore', 'sc_point_capital_cost', + 'sc_point_fixed_operating_cost', + 'sc_point_annual_energy', 'sc_point_annual_energy_ac'] + for attr in extra_atts: + value = getattr(self, attr) + if value is not None: + ARGS[attr] = value if self._friction_layer is not None: ARGS['mean_friction'] = self.mean_friction @@ -1981,6 +2066,10 @@ def economies_of_scale(cap_cost_scale, summary): summary['mean_lcoe'] = eos.scaled_lcoe summary['capital_cost_scalar'] = eos.capital_cost_scalar summary['scaled_capital_cost'] = eos.scaled_capital_cost + if "sc_point_capital_cost" in summary: + scaled_costs = (summary["sc_point_capital_cost"] + * eos.capital_cost_scalar) + summary['scaled_sc_point_capital_cost'] = scaled_costs return summary diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 0374f6f1a..3930b34d6 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -356,7 +356,9 @@ def test_data_layer_methods(): assert slope_min <= slope_mean <= slope_max -def test_recalc_lcoe(): +@pytest.mark.parametrize("cap_cost_scale", + ['1', '2 * np.multiply(1000, capacity) ** -0.3']) +def test_recalc_lcoe(cap_cost_scale): """Test supply curve aggregation with the re-calculation of lcoe using the multi-year mean capacity factor""" @@ -401,16 +403,17 @@ def test_recalc_lcoe(): res.create_dataset('lcoe_fcr-means', res['meta'].shape, data=lcoe_arr) - h5_dsets = ('capital_cost', 'fixed_operating_cost', + h5_dsets = ['capital_cost', 'fixed_operating_cost', 'fixed_charge_rate', 'variable_operating_cost', - 'system_capacity') + 'system_capacity'] base = SupplyCurveAggregation(EXCL, TM_DSET, excl_dict=EXCL_DICT, res_class_dset=None, res_class_bins=None, data_layers=DATA_LAYERS, h5_dsets=h5_dsets, gids=list(np.arange(10)), - recalc_lcoe=False) + recalc_lcoe=False, + cap_cost_scale=cap_cost_scale) summary_base = base.summarize(gen_temp, max_workers=1) sca = SupplyCurveAggregation(EXCL, TM_DSET, excl_dict=EXCL_DICT, @@ -418,11 +421,22 @@ def test_recalc_lcoe(): data_layers=DATA_LAYERS, h5_dsets=h5_dsets, gids=list(np.arange(10)), - recalc_lcoe=True) + recalc_lcoe=True, + cap_cost_scale=cap_cost_scale) summary = sca.summarize(gen_temp, max_workers=1) assert not np.allclose(summary_base['mean_lcoe'], summary['mean_lcoe']) + if cap_cost_scale == '1': + cc_dset = 'sc_point_capital_cost' + else: + cc_dset = 'scaled_sc_point_capital_cost' + lcoe = lcoe_fcr(summary['mean_fixed_charge_rate'], summary[cc_dset], + summary['sc_point_fixed_operating_cost'], + summary['sc_point_annual_energy'], + summary['mean_variable_operating_cost']) + assert np.allclose(lcoe, summary['mean_lcoe']) + @pytest.mark.parametrize('tm_dset', ("techmap_ri", "techmap_ri_new")) @pytest.mark.parametrize('pre_extract', (True, False)) From 92e1f48ce5b3b77c31e6776d10b8e46016d516cc Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 6 Mar 2024 15:26:19 -0700 Subject: [PATCH 4/6] Fix new values if `mean_h5_dsets_data` is `None` --- reV/supply_curve/points.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 9a7ba7742..b7d51eadd 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1802,14 +1802,16 @@ def sc_point_capital_cost(self): This method scales the capital cost based on the included-area capacity. The calculation requires 'capital_cost' and - 'system_capacity' in the generation file, otherwise it returns - `None`. + 'system_capacity' in the generation file and passed through as + `h5_dsets`, otherwise it returns `None`. Returns ------- sc_point_capital_cost : float | None Total supply curve point capital cost ($). """ + if self.mean_h5_dsets_data is None: + return None required = ('capital_cost', 'system_capacity') if not all(k in self.mean_h5_dsets_data for k in required): @@ -1826,13 +1828,16 @@ def sc_point_fixed_operating_cost(self): This method scales the fixed operating cost based on the included-area capacity. The calculation requires 'fixed_operating_cost' and 'system_capacity' in the generation - file, otherwise it returns `None`. + file and passed through as `h5_dsets`, otherwise it returns + `None`. Returns ------- sc_point_fixed_operating_cost : float | None Total supply curve point fixed operating cost ($). """ + if self.mean_h5_dsets_data is None: + return None required = ('fixed_operating_cost', 'system_capacity') if not all(k in self.mean_h5_dsets_data for k in required): From ae6378a60adfb8cef55e9a78eb654b45cf6f5fe9 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 6 Mar 2024 17:57:27 -0700 Subject: [PATCH 5/6] Add clarifying comment --- reV/supply_curve/points.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index b7d51eadd..7f229b1e3 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1585,6 +1585,9 @@ def mean_lcoe(self): if all(k in self.mean_h5_dsets_data for k in required): aep = (self.mean_h5_dsets_data['system_capacity'] * self.mean_cf * 8760) + # Note the AEP computation uses the SAM config + # `system_capacity`, so no need to scale `capital_cost` + # or `fixed_operating_cost` by anything mean_lcoe = lcoe_fcr( self.mean_h5_dsets_data['fixed_charge_rate'], self.mean_h5_dsets_data['capital_cost'], From a8b55a00f4d1603c782aad05846f1dbe73ffc01a Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 6 Mar 2024 18:01:46 -0700 Subject: [PATCH 6/6] Fix failing regression tests --- tests/test_supply_curve_sc_aggregation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 3930b34d6..277843e2e 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -110,6 +110,7 @@ def test_agg_summary(): summary = summary.fillna('None') s_baseline = s_baseline.fillna('None') + summary = summary[list(s_baseline.columns)] assert_frame_equal(summary, s_baseline, check_dtype=False, rtol=0.0001) @@ -205,6 +206,7 @@ def test_pre_extract_inclusions(pre_extract): summary = summary.fillna('None') s_baseline = s_baseline.fillna('None') + summary = summary[list(s_baseline.columns)] assert_frame_equal(summary, s_baseline, check_dtype=False, rtol=0.0001)