diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 7de5a3300..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'], @@ -1796,6 +1799,94 @@ 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 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): + 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 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): + 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 +2019,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 @@ -1980,6 +2073,11 @@ 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 + 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/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" 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']) diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 0374f6f1a..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) @@ -356,7 +358,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 +405,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 +423,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))