From e885cf3cf2489fa7136fae4984e9927bdc31a9b2 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 12:20:26 -0600 Subject: [PATCH 01/88] Fix typo --- reV/generation/generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/generation/generation.py b/reV/generation/generation.py index 5e9a2e274..7faf238df 100644 --- a/reV/generation/generation.py +++ b/reV/generation/generation.py @@ -113,7 +113,7 @@ def __init__( allowed and/or required SAM config file inputs. If economic parameters are supplied in the SAM config, then you can bundle a "follow-on" econ calculation by just adding the desired econ - output keys to the `output_request`. You can request ``reV`` to ' + output keys to the `output_request`. You can request ``reV`` to run the analysis for one or more "sites", which correspond to the meta indices in the resource data (also commonly called the ``gid's``). From 729fd2b86729e2cc827313172380fbfad19ea018 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 15:11:15 -0600 Subject: [PATCH 02/88] Fix site data not coming through if config column not given in points --- reV/config/project_points.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reV/config/project_points.py b/reV/config/project_points.py index c4b2ac637..907769817 100644 --- a/reV/config/project_points.py +++ b/reV/config/project_points.py @@ -623,9 +623,7 @@ def _parse_points(cls, points, res_file=None): # pylint: disable=no-member if SiteDataField.CONFIG not in df.columns: - df = cls._parse_sites( - df[SiteDataField.GID].values, res_file=res_file - ) + df[SiteDataField.CONFIG] = None gids = df[SiteDataField.GID].values if not np.array_equal(np.sort(gids), gids): From f5e8d5b258132db1e3200dada713538ccb0d02f5 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 15:20:05 -0600 Subject: [PATCH 03/88] Add required lcoe outputs --- reV/generation/base.py | 21 +++++++++++- .../output_attributes/lcoe_fcr_inputs.json | 34 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/reV/generation/base.py b/reV/generation/base.py index f45ee25bc..0a5744fe8 100644 --- a/reV/generation/base.py +++ b/reV/generation/base.py @@ -45,6 +45,20 @@ with open(os.path.join(ATTR_DIR, 'lcoe_fcr_inputs.json')) as f: LCOE_IN_ATTRS = json.load(f) +LCOE_REQUIRED_OUTPUTS = ("multiplier_regional", "capital_cost", + "fixed_operating_cost", "variable_operating_cost", + "base_capital_cost", "base_fixed_operating_cost", + "base_variable_operating_cost", "fixed_charge_rate") +"""Required econ outputs in generation file.""" + + +def _add_lcoe_outputs(output_request): + """Add required lcoe outputs to output request. """ + for out_var in LCOE_REQUIRED_OUTPUTS: + if out_var not in output_request: + output_request.append(out_var) + return output_request + class BaseGen(ABC): """Base class for reV gen and econ classes to run SAM simulations.""" @@ -859,7 +873,6 @@ def add_site_data_to_pp(self, site_data): """ self.project_points.join_df(site_data, key=self.site_data.index.name) - @abstractmethod def _parse_output_request(self, req): """Set the output variables requested from the user. @@ -873,6 +886,12 @@ def _parse_output_request(self, req): output_request : list Output variables requested from SAM. """ + output_request = self._output_request_type_check(req) + + if "lcoe_fcr" in output_request: + output_request = _add_lcoe_outputs(output_request) + + return output_request def _get_data_shape(self, dset, n_sites): """Get the output array shape based on OUT_ATTRS or PySAM.Outputs. diff --git a/reV/generation/output_attributes/lcoe_fcr_inputs.json b/reV/generation/output_attributes/lcoe_fcr_inputs.json index 32da77a92..d88b6d195 100644 --- a/reV/generation/output_attributes/lcoe_fcr_inputs.json +++ b/reV/generation/output_attributes/lcoe_fcr_inputs.json @@ -4,7 +4,7 @@ "dtype": "float32", "scale_factor": 1, "type": "scalar", - "units": "dollars" + "units": "usd" }, "fixed_charge_rate": { "chunks": null, @@ -18,13 +18,41 @@ "dtype": "float32", "scale_factor": 1, "type": "scalar", - "units": "dollars" + "units": "usd" }, "variable_operating_cost": { "chunks": null, "dtype": "float32", "scale_factor": 1, "type": "scalar", - "units": "dol/kWh" + "units": "usd/kWh" + }, + "base_capital_cost": { + "chunks": null, + "dtype": "float32", + "scale_factor": 1, + "type": "scalar", + "units": "usd" + }, + "base_fixed_operating_cost": { + "chunks": null, + "dtype": "float32", + "scale_factor": 1, + "type": "scalar", + "units": "usd" + }, + "base_variable_operating_cost": { + "chunks": null, + "dtype": "float32", + "scale_factor": 1, + "type": "scalar", + "units": "usd/kWh" + }, + "multiplier_regional": { + "chunks": null, + "dtype": "float32", + "scale_factor": 1, + "type": "scalar", + "units": "unitless" } } From 6c2aee3d190f233f602a2789622427b018a2c0c4 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 15:21:13 -0600 Subject: [PATCH 04/88] User superclass `_parse_output_request` --- reV/econ/econ.py | 2 +- reV/generation/generation.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/reV/econ/econ.py b/reV/econ/econ.py index 1987657a5..7e4097f75 100644 --- a/reV/econ/econ.py +++ b/reV/econ/econ.py @@ -405,7 +405,7 @@ def _parse_output_request(self, req): Output variables requested from SAM. """ - output_request = self._output_request_type_check(req) + output_request = super()._parse_output_request(req) for request in output_request: if request not in self.OUT_ATTRS: diff --git a/reV/generation/generation.py b/reV/generation/generation.py index 7faf238df..4a9093ccb 100644 --- a/reV/generation/generation.py +++ b/reV/generation/generation.py @@ -128,7 +128,7 @@ def __init__( >>> import os >>> from reV import Gen, TESTDATADIR >>> - >>> sam_tech = 'pvwattsv7' + >>> sam_tech = 'pvwattsv8' >>> sites = 0 >>> fp_sam = os.path.join(TESTDATADIR, 'SAM/naris_pv_1axis_inv13.json') >>> fp_res = os.path.join(TESTDATADIR, 'nsrdb/ri_100_nsrdb_2013.h5') @@ -145,15 +145,16 @@ def __init__( >>> gen.run() >>> >>> gen.out - {'lcoe_fcr': array([131.39166, 131.31221, 127.54539, 125.49656]), - 'cf_mean': array([0.17713654, 0.17724372, 0.1824783 , 0.1854574 ]), - : array([[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.], - ..., - [0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]])} + {'fixed_charge_rate': array([0.096, 0.096, 0.096, 0.096], + 'base_capital_cost': array([39767200, 39767200, 39767200, 39767200], + 'base_variable_operating_cost': array([0, 0, 0, 0], + 'base_fixed_operating_cost': array([260000, 260000, 260000, 260000], + 'capital_cost': array([39767200, 39767200, 39767200, 39767200], + 'fixed_operating_cost': array([260000, 260000, 260000, 260000], + 'variable_operating_cost': array([0, 0, 0, 0], + 'multiplier_regional': array([1, 1, 1, 1], + 'cf_mean': array([0.17859147, 0.17869979, 0.1834818 , 0.18646291], + 'lcoe_fcr': array([130.32126, 130.24226, 126.84782, 124.81981]} Parameters ---------- @@ -947,7 +948,7 @@ def _parse_output_request(self, req): Output variables requested from SAM. """ - output_request = self._output_request_type_check(req) + output_request = super()._parse_output_request(req) # ensure that cf_mean is requested from output if "cf_mean" not in output_request: From b41ba7c7b22b5d115f440918beb9b500960e13ab Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 15:21:53 -0600 Subject: [PATCH 05/88] Add cost defaults as needed --- reV/SAM/SAM.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/reV/SAM/SAM.py b/reV/SAM/SAM.py index 9ae66fbdd..15176a35f 100644 --- a/reV/SAM/SAM.py +++ b/reV/SAM/SAM.py @@ -827,6 +827,8 @@ def _parse_site_sys_inputs(self, site_sys_inputs): else: self.sam_sys_inputs[k] = v + _add_cost_defaults(self.sam_sys_inputs) + @staticmethod def _is_arr_like(val): """Returns true if SAM data is array-like. False if scalar.""" @@ -916,3 +918,19 @@ def execute(self): msg += " for site {}".format(self.site) logger.exception(msg) raise SAMExecutionError(msg) from e + + +def _add_cost_defaults(sam_inputs): + """Add default values for required cost outputs if they are missing. """ + sam_inputs.setdefault("fixed_charge_rate", 0) + + reg_mult = sam_inputs.setdefault("multiplier_regional", 1) + capital_cost = sam_inputs.setdefault("capital_cost", 0) + fixed_operating_cost = sam_inputs.setdefault("fixed_operating_cost", 0) + variable_operating_cost = sam_inputs.setdefault( + "variable_operating_cost", 0) + + sam_inputs["base_capital_cost"] = capital_cost + sam_inputs["base_fixed_operating_cost"] = fixed_operating_cost + sam_inputs["base_variable_operating_cost"] = variable_operating_cost + sam_inputs["capital_cost"] = capital_cost * reg_mult From 437a7685baa8eec4eca573c10e7400da297fceda Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 15:22:26 -0600 Subject: [PATCH 06/88] Tests for new cost outputs --- tests/test_econ_lcoe.py | 49 ++++++++++++++++++++++++++++++++++++ tests/test_gen_config.py | 7 ++++++ tests/test_gen_geothermal.py | 27 ++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/tests/test_econ_lcoe.py b/tests/test_econ_lcoe.py index 4a84023de..6a89839f9 100644 --- a/tests/test_econ_lcoe.py +++ b/tests/test_econ_lcoe.py @@ -22,6 +22,7 @@ from reV import TESTDATADIR from reV.cli import main from reV.econ.econ import Econ +from reV.generation.base import LCOE_REQUIRED_OUTPUTS from reV.handlers.outputs import Outputs from reV.utilities import ModuleName @@ -60,6 +61,9 @@ def test_lcoe(year, max_workers, spw): assert result + for output in LCOE_REQUIRED_OUTPUTS: + assert output in obj.out + @pytest.mark.parametrize('year', ('2012', '2013')) def test_fout(year): @@ -80,6 +84,8 @@ def test_fout(year): econ.run(max_workers=1, out_fpath=fpath) with Outputs(fpath) as f: lcoe = f['lcoe_fcr'] + for output in LCOE_REQUIRED_OUTPUTS: + assert output in f.datasets with h5py.File(r1f, mode='r') as f: year_rows = {'2012': 0, '2013': 1} @@ -116,6 +122,8 @@ def test_append_data(year): lcoe = f['lcoe_fcr'] meta = f.meta ti = f.time_index + for output in LCOE_REQUIRED_OUTPUTS: + assert output in f.datasets with Outputs(original_file) as f: og_dsets = f.dsets @@ -163,6 +171,8 @@ def test_append_multi_node(node): meta = out.meta data_test = out['lcoe_fcr'] test_cap_cost = out['capital_cost'] + for output in LCOE_REQUIRED_OUTPUTS: + assert output in out.datasets assert np.allclose(data_baseline, data_test) @@ -218,6 +228,8 @@ def test_econ_from_config(runner, clear_loggers): out_fpath = os.path.join(td, fn_out) with Outputs(out_fpath, 'r') as f: lcoe = f['lcoe_fcr'] + for output in LCOE_REQUIRED_OUTPUTS: + assert output in f.datasets with h5py.File(r1f, mode='r') as f: r1_lcoe = f['pv']['lcoefcr'][0, 0:10] * 1000 @@ -229,6 +241,43 @@ def test_econ_from_config(runner, clear_loggers): clear_loggers() +def test_multiplier_regional(clear_loggers): + """Gen PV CF profiles with write to disk and compare against rev1.""" + with tempfile.TemporaryDirectory() as dirout: + cf_file = os.path.join(TESTDATADIR, + 'gen_out/gen_ri_pv_2012_x000.h5') + sam_files = os.path.join(TESTDATADIR, 'SAM', + 'i_lcoe_naris_pv_1axis_inv13.json') + fpath = os.path.join(dirout, 'lcoe_out_econ_2012.h5') + mults = np.arange(0, 100) / 100 + points = pd.DataFrame({"gid": np.arange(0, 100), + "multiplier_regional": mults}) + econ = Econ(points, sam_files, cf_file, + output_request='lcoe_fcr', + sites_per_worker=25) + econ.run(max_workers=1, out_fpath=fpath) + + with Outputs(cf_file) as f: + cf = f['cf_mean'] + + with Outputs(fpath) as f: + lcoe = f['lcoe_fcr'] + for output in LCOE_REQUIRED_OUTPUTS: + assert output in f.datasets + + with open(sam_files, "r") as fh: + sam_config = json.load(fh) + + cc = sam_config["capital_cost"] * mults + num = (cc * sam_config["fixed_charge_rate"] + + sam_config["fixed_operating_cost"]) + aep = cf * sam_config["system_capacity"] / 1000 * 8760 + lcoe_truth = num / aep + sam_config["variable_operating_cost"] + + assert np.allclose(lcoe, lcoe_truth, rtol=RTOL, atol=ATOL) + clear_loggers() + + def execute_pytest(capture='all', flags='-rapP'): """Execute module as pytest with detailed summary report. diff --git a/tests/test_gen_config.py b/tests/test_gen_config.py index 88cc2691f..bd5408d94 100644 --- a/tests/test_gen_config.py +++ b/tests/test_gen_config.py @@ -20,6 +20,7 @@ from reV import TESTDATADIR from reV.cli import main from reV.config.project_points import ProjectPoints +from reV.generation.base import LCOE_REQUIRED_OUTPUTS from reV.generation.generation import Gen from reV.handlers.outputs import Outputs from reV.utilities import SiteDataField @@ -107,6 +108,12 @@ def test_gen_from_config(runner, tech, clear_loggers): monthly = cf['monthly_energy'] assert monthly.shape == (12, 10) + for output in LCOE_REQUIRED_OUTPUTS: + if tech == 'pv': + assert output in cf.datasets + else: + assert output not in cf.datasets + break if rev2_profiles is None: diff --git a/tests/test_gen_geothermal.py b/tests/test_gen_geothermal.py index 4f27550c7..3acf23b5a 100644 --- a/tests/test_gen_geothermal.py +++ b/tests/test_gen_geothermal.py @@ -15,6 +15,7 @@ from rex import Outputs from reV import TESTDATADIR +from reV.generation.base import LCOE_REQUIRED_OUTPUTS from reV.generation.generation import Gen from reV.SAM.generation import Geothermal from reV.utilities import ResourceMetaField @@ -115,6 +116,8 @@ def test_gen_geothermal(depth, sample_resource_data): ) assert np.allclose(truth, test, rtol=RTOL, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output in gen.out @pytest.mark.parametrize( "sample_resource_data", [{"temp": 60, "potential": 200}], indirect=True @@ -165,6 +168,9 @@ def test_gen_geothermal_temp_too_low(sample_resource_data): ) assert np.allclose(truth, test, rtol=RTOL, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output in gen.out + @pytest.mark.parametrize( "sample_resource_data", [{"temp": 150, "potential": 100}], indirect=True @@ -211,6 +217,9 @@ def test_per_kw_cost_inputs(sample_resource_data): ) assert np.allclose(truth, test, rtol=1e-6, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output in gen.out + @pytest.mark.parametrize( "sample_resource_data", [{"temp": 150, "potential": 100}], indirect=True @@ -258,6 +267,9 @@ def test_drill_cost_inputs(sample_resource_data): ) assert np.allclose(truth, test, rtol=1e-6, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output in gen.out + @pytest.mark.parametrize( "sample_resource_data", [{"temp": 150, "potential": 20}], indirect=True @@ -315,6 +327,9 @@ def test_gen_with_nameplate_input(sample_resource_data): ) assert np.allclose(truth, test, rtol=RTOL, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output in gen.out + @pytest.mark.parametrize( "sample_resource_data", [{"temp": 150, "potential": 20}], indirect=True @@ -359,6 +374,9 @@ def test_gen_egs_too_high_egs_plant_design_temp(sample_resource_data): ) assert np.allclose(truth, test, rtol=RTOL, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output not in gen.out + @pytest.mark.parametrize( "sample_resource_data", @@ -406,6 +424,9 @@ def test_gen_egs_too_low_egs_plant_design_temp(sample_resource_data): ) assert np.allclose(truth, test, rtol=RTOL, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output not in gen.out + @pytest.mark.parametrize( "sample_resource_data", @@ -454,6 +475,9 @@ def test_gen_egs_plant_design_temp_adjusted_from_user(sample_resource_data): ) assert np.allclose(truth, test, rtol=RTOL, atol=ATOL), msg + for output in LCOE_REQUIRED_OUTPUTS: + assert output not in gen.out + @pytest.mark.parametrize( "sample_resource_data", [{"temp": 150, "potential": 20}], indirect=True @@ -491,6 +515,9 @@ def test_gen_with_time_index_step_input(sample_resource_data): assert gen.out["cf_profile"].shape[0] == 8760 // 2 + for output in LCOE_REQUIRED_OUTPUTS: + assert output in gen.out + def execute_pytest(capture="all", flags="-rapP"): """Execute module as pytest with detailed summary report. From 97483aedd9d8d359be91a88db56edac7f5db479b Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 15:45:29 -0600 Subject: [PATCH 07/88] Linter fix --- tests/test_gen_geothermal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_gen_geothermal.py b/tests/test_gen_geothermal.py index 3acf23b5a..f1fdedab2 100644 --- a/tests/test_gen_geothermal.py +++ b/tests/test_gen_geothermal.py @@ -119,6 +119,7 @@ def test_gen_geothermal(depth, sample_resource_data): for output in LCOE_REQUIRED_OUTPUTS: assert output in gen.out + @pytest.mark.parametrize( "sample_resource_data", [{"temp": 60, "potential": 200}], indirect=True ) From 71b2fc2ca241f1b82fe9eac1faa2219e64c54654 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 16:11:14 -0600 Subject: [PATCH 08/88] `"system_capacity"` now a required lcoe output, since costs are scaled to it --- reV/SAM/SAM.py | 20 ++++++++++++++++++-- reV/generation/base.py | 7 ++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/reV/SAM/SAM.py b/reV/SAM/SAM.py index 15176a35f..40164533f 100644 --- a/reV/SAM/SAM.py +++ b/reV/SAM/SAM.py @@ -626,6 +626,8 @@ def __init__( self._meta = self._parse_meta(meta) self._parse_site_sys_inputs(site_sys_inputs) + _add_cost_defaults(self.sam_sys_inputs) + _add_sys_capacity(self.sam_sys_inputs) @property def meta(self): @@ -827,8 +829,6 @@ def _parse_site_sys_inputs(self, site_sys_inputs): else: self.sam_sys_inputs[k] = v - _add_cost_defaults(self.sam_sys_inputs) - @staticmethod def _is_arr_like(val): """Returns true if SAM data is array-like. False if scalar.""" @@ -934,3 +934,19 @@ def _add_cost_defaults(sam_inputs): sam_inputs["base_fixed_operating_cost"] = fixed_operating_cost sam_inputs["base_variable_operating_cost"] = variable_operating_cost sam_inputs["capital_cost"] = capital_cost * reg_mult + + +def _add_sys_capacity(sam_inputs): + """Add system capacity SAM input if it is missing. """ + cap = sam_inputs.get("system_capacity") + if cap is None: + cap = sam_inputs.get("turbine_capacity") + + if cap is None: + cap = sam_inputs.get("wind_turbine_powercurve_powerout") + if cap is not None: + cap = max(cap) + else: + cap = 1 + + sam_inputs["system_capacity"] = cap diff --git a/reV/generation/base.py b/reV/generation/base.py index 0a5744fe8..97e605921 100644 --- a/reV/generation/base.py +++ b/reV/generation/base.py @@ -45,9 +45,10 @@ with open(os.path.join(ATTR_DIR, 'lcoe_fcr_inputs.json')) as f: LCOE_IN_ATTRS = json.load(f) -LCOE_REQUIRED_OUTPUTS = ("multiplier_regional", "capital_cost", - "fixed_operating_cost", "variable_operating_cost", - "base_capital_cost", "base_fixed_operating_cost", +LCOE_REQUIRED_OUTPUTS = ("system_capacity", "multiplier_regional", + "capital_cost", "fixed_operating_cost", + "variable_operating_cost", "base_capital_cost", + "base_fixed_operating_cost", "base_variable_operating_cost", "fixed_charge_rate") """Required econ outputs in generation file.""" From 1cfa822afce722381d6f401c6c80a4e66b52fffe Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 16:21:05 -0600 Subject: [PATCH 09/88] Ignore base costs --- reV/bespoke/bespoke.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 5d5cbb878..0b9ff03c1 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1191,7 +1191,10 @@ def _check_sys_inputs(plant1, plant2, 'capital_cost', 'fixed_operating_cost', 'variable_operating_cost', - 'balance_of_system_cost')): + 'balance_of_system_cost', + 'base_capital_cost', + 'base_fixed_operating_cost', + 'base_variable_operating_cost')): """Check two reV-SAM models for matching system inputs. Parameters From ea5393c804e4a1aab575b0d7582c9b0233379967 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 17:37:32 -0600 Subject: [PATCH 10/88] `system_capacity` as output variable --- reV/config/output_request.py | 1 + reV/generation/output_attributes/lcoe_fcr_inputs.json | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/reV/config/output_request.py b/reV/config/output_request.py index 3cd9e7e95..8f079b224 100644 --- a/reV/config/output_request.py +++ b/reV/config/output_request.py @@ -61,6 +61,7 @@ class SAMOutputRequest(OutputRequest): # all available SAM output variables should be in the values CORRECTIONS = {'cf_means': 'cf_mean', 'cf': 'cf_mean', + 'capacity': 'system_capacity', 'capacity_factor': 'cf_mean', 'capacityfactor': 'cf_mean', 'cf_profiles': 'cf_profile', diff --git a/reV/generation/output_attributes/lcoe_fcr_inputs.json b/reV/generation/output_attributes/lcoe_fcr_inputs.json index d88b6d195..7d71c0d88 100644 --- a/reV/generation/output_attributes/lcoe_fcr_inputs.json +++ b/reV/generation/output_attributes/lcoe_fcr_inputs.json @@ -54,5 +54,12 @@ "scale_factor": 1, "type": "scalar", "units": "unitless" + }, + "system_capacity": { + "chunks": null, + "dtype": "float32", + "scale_factor": 1, + "type": "scalar", + "units": "kW" } } From fa6c18ce6257e8800bc5e86abecc9ebd346184ec Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sat, 1 Jun 2024 17:44:14 -0600 Subject: [PATCH 11/88] Linter fix --- reV/config/output_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/config/output_request.py b/reV/config/output_request.py index 8f079b224..84526c2cc 100644 --- a/reV/config/output_request.py +++ b/reV/config/output_request.py @@ -37,7 +37,7 @@ def __init__(self, inp): for request in inp: if request in self.CORRECTIONS.values(): self.append(request) - elif request in self.CORRECTIONS.keys(): + elif request in self.CORRECTIONS: self.append(self.CORRECTIONS[request]) msg = ('Correcting output request "{}" to "{}".' .format(request, self.CORRECTIONS[request])) From 1f9c697dd77e3802da8105c381b12dd9b72185a3 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 4 Jun 2024 12:13:44 -0600 Subject: [PATCH 12/88] Fix circular import --- reV/handlers/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reV/handlers/__init__.py b/reV/handlers/__init__.py index 50c9d4775..e73fcad17 100644 --- a/reV/handlers/__init__.py +++ b/reV/handlers/__init__.py @@ -3,5 +3,4 @@ Sub-package of data handlers """ from .exclusions import ExclusionLayers -from .multi_year import MultiYear from .outputs import Outputs From 51ee321dd5bbbd1dcdbab9fb0d35fc180e98ebf3 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 4 Jun 2024 12:13:57 -0600 Subject: [PATCH 13/88] Required pass-through datasets now added --- reV/handlers/multi_year.py | 59 +++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/reV/handlers/multi_year.py b/reV/handlers/multi_year.py index e8d3e52d5..3ca2e9961 100644 --- a/reV/handlers/multi_year.py +++ b/reV/handlers/multi_year.py @@ -18,6 +18,7 @@ parse_year, ) +from reV.generation.base import LCOE_REQUIRED_OUTPUTS from reV.config.output_request import SAMOutputRequest from reV.handlers.outputs import Outputs from reV.utilities import ModuleName, log_versions @@ -60,8 +61,12 @@ def __init__(self, name, out_dir, source_files=None, source files. This takes priority over `source_dir` and `source_prefix` but is not used if `source_files` are specified explicitly. By default, ``None``. - dsets : list | tuple, optional - List of datasets to collect. By default, ``('cf_mean',)``. + dsets : str | list | tuple, optional + List of datasets to collect. This can be set to + ``"PIPELINE"`` if running from the command line as part of a + reV pipeline. In this case, all the datasets from the + previous pipeline step will be collected. + By default, ``('cf_mean',)``. pass_through_dsets : list | tuple, optional Optional list of datasets that are identical in the multi-year files (e.g. input datasets that don't vary from @@ -76,10 +81,32 @@ def __init__(self, name, out_dir, source_files=None, self._source_prefix = source_prefix self._source_pattern = source_pattern self._pass_through_dsets = None - if pass_through_dsets is not None: - self._pass_through_dsets = SAMOutputRequest(pass_through_dsets) + self._dsets = None - self._dsets = self._parse_dsets(dsets) + self._parse_pass_through_dsets(dsets, pass_through_dsets or []) + self._parse_dsets(dsets) + + def _parse_pass_through_dsets(self, dsets, pass_through_dsets): + """Parse a multi-year pass-through dataset collection request. + + Parameters + ---------- + dsets : str | list + One or more datasets to collect, or "PIPELINE" + pass_through_dsets : list + List of pass through datasets. + """ + if isinstance(dsets, str) and dsets == 'PIPELINE': + files = parse_previous_status(self._dirout, ModuleName.MULTI_YEAR) + with Resource(files[0]) as res: + dsets = [d for d in res] + + if "lcoe_fcr" in dsets: + for dset in LCOE_REQUIRED_OUTPUTS: + if dset not in pass_through_dsets: + pass_through_dsets.append(dset) + + self._pass_through_dsets = SAMOutputRequest(pass_through_dsets) def _parse_dsets(self, dsets): """Parse a multi-year dataset collection request. Can handle PIPELINE @@ -90,11 +117,6 @@ def _parse_dsets(self, dsets): ---------- dsets : str | list One or more datasets to collect, or "PIPELINE" - - Returns - ------- - dsets : SAMOutputRequest - Dataset list object. """ if isinstance(dsets, str) and dsets == 'PIPELINE': files = parse_previous_status(self._dirout, ModuleName.MULTI_YEAR) @@ -104,9 +126,7 @@ def _parse_dsets(self, dsets): and d != 'meta' and d not in self.pass_through_dsets] - dsets = SAMOutputRequest(dsets) - - return dsets + self._dsets = SAMOutputRequest(dsets) @property def name(self): @@ -815,10 +835,15 @@ def my_collect_groups(out_fpath, groups, clobber=True): MultiYear.collect_means(out_fpath, group['source_files'], dset, group=group['group']) - if group.get('pass_through_dsets', None) is not None: - for dset in group['pass_through_dsets']: - MultiYear.pass_through(out_fpath, group['source_files'], - dset, group=group['group']) + pass_through_dsets = group.get('pass_through_dsets') or [] + if "lcoe_fcr" in group['dsets']: + for dset in LCOE_REQUIRED_OUTPUTS: + if dset not in pass_through_dsets: + pass_through_dsets.append(dset) + + for dset in pass_through_dsets: + MultiYear.pass_through(out_fpath, group['source_files'], + dset, group=group['group']) runtime = (time.time() - t0) / 60 logger.info('- {} collection completed in: {:.2f} min.' From 76c2fab6529e9e18af2a1336cb228fa9bf464b18 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 4 Jun 2024 13:21:35 -0600 Subject: [PATCH 14/88] Linter fix --- reV/handlers/multi_year.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/handlers/multi_year.py b/reV/handlers/multi_year.py index 3ca2e9961..5b287b035 100644 --- a/reV/handlers/multi_year.py +++ b/reV/handlers/multi_year.py @@ -99,7 +99,7 @@ def _parse_pass_through_dsets(self, dsets, pass_through_dsets): if isinstance(dsets, str) and dsets == 'PIPELINE': files = parse_previous_status(self._dirout, ModuleName.MULTI_YEAR) with Resource(files[0]) as res: - dsets = [d for d in res] + dsets = res.datasets if "lcoe_fcr" in dsets: for dset in LCOE_REQUIRED_OUTPUTS: From 1dfaa27bd8a446eb92bf46394a5b9e880acef23b Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 4 Jun 2024 14:06:18 -0600 Subject: [PATCH 15/88] Default costs to None instead of 0 --- reV/SAM/SAM.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/reV/SAM/SAM.py b/reV/SAM/SAM.py index 40164533f..f5a0b20de 100644 --- a/reV/SAM/SAM.py +++ b/reV/SAM/SAM.py @@ -922,18 +922,21 @@ def execute(self): def _add_cost_defaults(sam_inputs): """Add default values for required cost outputs if they are missing. """ - sam_inputs.setdefault("fixed_charge_rate", 0) + sam_inputs.setdefault("fixed_charge_rate", None) reg_mult = sam_inputs.setdefault("multiplier_regional", 1) - capital_cost = sam_inputs.setdefault("capital_cost", 0) - fixed_operating_cost = sam_inputs.setdefault("fixed_operating_cost", 0) + capital_cost = sam_inputs.setdefault("capital_cost", None) + fixed_operating_cost = sam_inputs.setdefault("fixed_operating_cost", None) variable_operating_cost = sam_inputs.setdefault( - "variable_operating_cost", 0) + "variable_operating_cost", None) sam_inputs["base_capital_cost"] = capital_cost sam_inputs["base_fixed_operating_cost"] = fixed_operating_cost sam_inputs["base_variable_operating_cost"] = variable_operating_cost - sam_inputs["capital_cost"] = capital_cost * reg_mult + if capital_cost is not None: + sam_inputs["capital_cost"] = capital_cost * reg_mult + else: + sam_inputs["capital_cost"] = None def _add_sys_capacity(sam_inputs): @@ -946,7 +949,5 @@ def _add_sys_capacity(sam_inputs): cap = sam_inputs.get("wind_turbine_powercurve_powerout") if cap is not None: cap = max(cap) - else: - cap = 1 sam_inputs["system_capacity"] = cap From 1e8c9d21e4477795dac4c0992bf7f73a29365e1d Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 4 Jun 2024 18:21:27 -0600 Subject: [PATCH 16/88] Don't scale `None` values --- reV/generation/generation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reV/generation/generation.py b/reV/generation/generation.py index 4a9093ccb..a2065425b 100644 --- a/reV/generation/generation.py +++ b/reV/generation/generation.py @@ -713,6 +713,8 @@ def _run_single_worker( for k in site_output.keys(): # iterate through variable names in each site's output dict if k in cls.OUT_ATTRS: + if out[site][k] is None: + continue # get dtype and scale for output variable name dtype = cls.OUT_ATTRS[k].get("dtype", "float32") scale_factor = cls.OUT_ATTRS[k].get("scale_factor", 1) From c2a4e0cc07366eb1ef763b1e52c894c6e281a200 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 5 Jun 2024 00:04:06 -0600 Subject: [PATCH 17/88] Remove unused var --- examples/bespoke_wind_plants/single_run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/bespoke_wind_plants/single_run.py b/examples/bespoke_wind_plants/single_run.py index ff53a2faf..cd45b8bc8 100644 --- a/examples/bespoke_wind_plants/single_run.py +++ b/examples/bespoke_wind_plants/single_run.py @@ -23,7 +23,6 @@ EXCL = os.path.join(TESTDATADIR, 'ri_exclusions/ri_exclusions.h5') RES = os.path.join(TESTDATADIR, 'wtk/ri_100_wtk_{}.h5') TM_DSET = 'techmap_wtk_ri_100' -AGG_DSET = ('cf_mean', 'cf_profile') # note that this differs from the EXCL_DICT = {'ri_srtm_slope': {'inclusion_range': (None, 5), From 9090f2a011c0a0e611db7847ef27fddc932f63d0 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 5 Jun 2024 01:12:19 -0600 Subject: [PATCH 18/88] Add in required gen outputs for solar --- reV/generation/generation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/reV/generation/generation.py b/reV/generation/generation.py index a2065425b..90ce6ce21 100644 --- a/reV/generation/generation.py +++ b/reV/generation/generation.py @@ -956,6 +956,14 @@ def _parse_output_request(self, req): if "cf_mean" not in output_request: output_request.append("cf_mean") + if _is_solar_run_with_ac_outputs(self.tech): + if "dc_ac_ratio" not in output_request: + output_request.append("dc_ac_ratio") + for dset in ["cf_mean", "cf_profile"]: + ac_dset = f"{dset}_ac" + if dset in output_request and ac_dset not in output_request: + output_request.append(ac_dset) + for request in output_request: if request not in self.OUT_ATTRS: msg = ( @@ -1100,3 +1108,10 @@ def run(self, out_fpath=None, max_workers=1, timeout=1800, pool_size=None): raise e return self._out_fpath + + +def _is_solar_run_with_ac_outputs(tech): + """True if tech is pvwattsv8+""" + if "pvwatts" not in tech.casefold(): + return False + return tech.casefold() not in {f"pvwattsv{i}" for i in range(8)} From e2423911860e87d1f4f6b8c57ddb4a190bb6845f Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 11:30:31 -0600 Subject: [PATCH 19/88] Add `dc_ac_ratio` as required pass through dset --- reV/handlers/multi_year.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reV/handlers/multi_year.py b/reV/handlers/multi_year.py index 5b287b035..6e3b3d418 100644 --- a/reV/handlers/multi_year.py +++ b/reV/handlers/multi_year.py @@ -105,6 +105,9 @@ def _parse_pass_through_dsets(self, dsets, pass_through_dsets): for dset in LCOE_REQUIRED_OUTPUTS: if dset not in pass_through_dsets: pass_through_dsets.append(dset) + if "dc_ac_ratio" in dsets: + if "dc_ac_ratio" not in pass_through_dsets: + pass_through_dsets.append("dc_ac_ratio") self._pass_through_dsets = SAMOutputRequest(pass_through_dsets) From e852e5e80936cfced3a7a03d25cbfcbe8eadd028 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 12:03:13 -0600 Subject: [PATCH 20/88] Adjust capacity column name --- reV/bespoke/bespoke.py | 4 +- reV/hybrids/hybrid_methods.py | 24 ++++---- reV/hybrids/hybrids.py | 8 +-- reV/qa_qc/summary.py | 8 +-- reV/supply_curve/points.py | 31 +++++++++- reV/supply_curve/supply_curve.py | 29 ++++----- reV/utilities/__init__.py | 4 +- tests/test_bespoke.py | 6 +- tests/test_econ_of_scale.py | 6 +- tests/test_hybrids.py | 71 ++++++++++++----------- tests/test_supply_curve_compute.py | 10 ++-- tests/test_supply_curve_sc_aggregation.py | 12 ++-- tests/test_supply_curve_vpd.py | 6 +- 13 files changed, 129 insertions(+), 90 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 0b9ff03c1..f2437077f 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -566,7 +566,7 @@ def _parse_prior_run(self): # {meta_column: sam_sys_input_key} required = { - SupplyCurveField.CAPACITY: "system_capacity", + SupplyCurveField.CAPACITY_AC_MW: "system_capacity", SupplyCurveField.TURBINE_X_COORDS: "wind_farm_xCoordinates", SupplyCurveField.TURBINE_Y_COORDS: "wind_farm_yCoordinates", } @@ -1356,7 +1356,7 @@ def run_plant_optimization(self): # copy dataset outputs to meta data for supply curve table summary # convert SAM system capacity in kW to reV supply curve cap in MW - self._meta[SupplyCurveField.CAPACITY] = ( + self._meta[SupplyCurveField.CAPACITY_AC_MW] = ( self.outputs["system_capacity"] / 1e3 ) diff --git a/reV/hybrids/hybrid_methods.py b/reV/hybrids/hybrid_methods.py index 5b4b1c0c2..6ff6721be 100644 --- a/reV/hybrids/hybrid_methods.py +++ b/reV/hybrids/hybrid_methods.py @@ -31,9 +31,9 @@ def aggregate_solar_capacity(h): capacity ratio and the solar capacity is copied into this new column. """ - if f'hybrid_solar_{SupplyCurveField.CAPACITY}' in h.hybrid_meta: + if f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}' in h.hybrid_meta: return None - return h.hybrid_meta[f'solar_{SupplyCurveField.CAPACITY}'] + return h.hybrid_meta[f'solar_{SupplyCurveField.CAPACITY_AC_MW}'] def aggregate_wind_capacity(h): @@ -60,9 +60,9 @@ def aggregate_wind_capacity(h): exist, it is assumed that there is no limit on the solar to wind capacity ratio and the wind capacity is copied into this new column. """ - if f'hybrid_wind_{SupplyCurveField.CAPACITY}' in h.hybrid_meta: + if f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}' in h.hybrid_meta: return None - return h.hybrid_meta[f'wind_{SupplyCurveField.CAPACITY}'] + return h.hybrid_meta[f'wind_{SupplyCurveField.CAPACITY_AC_MW}'] def aggregate_capacity(h): @@ -81,8 +81,8 @@ def aggregate_capacity(h): A series of data containing the aggregated capacity, or `None` if the capacity columns are missing. """ - sc = f'hybrid_solar_{SupplyCurveField.CAPACITY}' - wc = f'hybrid_wind_{SupplyCurveField.CAPACITY}' + sc = f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}' + wc = f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}' missing_solar_cap = sc not in h.hybrid_meta.columns missing_wind_cap = wc not in h.hybrid_meta.columns if missing_solar_cap or missing_wind_cap: @@ -109,8 +109,8 @@ def aggregate_capacity_factor(h): if the capacity and/or mean_cf columns are missing. """ - sc = f'hybrid_solar_{SupplyCurveField.CAPACITY}' - wc = f'hybrid_wind_{SupplyCurveField.CAPACITY}' + sc = f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}' + wc = f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}' scf = f'solar_{SupplyCurveField.MEAN_CF}' wcf = f'wind_{SupplyCurveField.MEAN_CF}' missing_solar_cap = sc not in h.hybrid_meta.columns @@ -130,8 +130,10 @@ def aggregate_capacity_factor(h): HYBRID_METHODS = { - f'hybrid_solar_{SupplyCurveField.CAPACITY}': aggregate_solar_capacity, - f'hybrid_wind_{SupplyCurveField.CAPACITY}': aggregate_wind_capacity, - f'hybrid_{SupplyCurveField.CAPACITY}': aggregate_capacity, + f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}': ( + aggregate_solar_capacity + ), + f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}': aggregate_wind_capacity, + f'hybrid_{SupplyCurveField.CAPACITY_AC_MW}': aggregate_capacity, f'hybrid_{SupplyCurveField.MEAN_CF}': aggregate_capacity_factor } diff --git a/reV/hybrids/hybrids.py b/reV/hybrids/hybrids.py index a579408be..911be3a32 100644 --- a/reV/hybrids/hybrids.py +++ b/reV/hybrids/hybrids.py @@ -39,8 +39,8 @@ SupplyCurveField.SC_COL_IND } DROPPED_COLUMNS = [SupplyCurveField.GID] -DEFAULT_FILL_VALUES = {f'solar_{SupplyCurveField.CAPACITY}': 0, - f'wind_{SupplyCurveField.CAPACITY}': 0, +DEFAULT_FILL_VALUES = {f'solar_{SupplyCurveField.CAPACITY_AC_MW}': 0, + f'wind_{SupplyCurveField.CAPACITY_AC_MW}': 0, f'solar_{SupplyCurveField.MEAN_CF}': 0, f'wind_{SupplyCurveField.MEAN_CF}': 0} OUTPUT_PROFILE_NAMES = ['hybrid_profile', @@ -1196,8 +1196,8 @@ def _compute_hybridized_profile_components(self): def __rep_profile_hybridization_params(self): """Zip the rep profile hybridization parameters.""" - cap_col_names = [f"hybrid_solar_{SupplyCurveField.CAPACITY}", - f"hybrid_wind_{SupplyCurveField.CAPACITY}"] + cap_col_names = [f"hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}", + f"hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}"] idx_maps = [ self.meta_hybridizer.solar_profile_indices_map, self.meta_hybridizer.wind_profile_indices_map, diff --git a/reV/qa_qc/summary.py b/reV/qa_qc/summary.py index 386caf4cb..287444a39 100644 --- a/reV/qa_qc/summary.py +++ b/reV/qa_qc/summary.py @@ -597,11 +597,11 @@ def _extract_sc_data(self, lcoe=SupplyCurveField.MEAN_LCOE): sc_df : pandas.DataFrame Supply curve data """ - values = [SupplyCurveField.CAPACITY, lcoe] + values = [SupplyCurveField.CAPACITY_AC_MW, lcoe] self._check_value(self.summary, values, scatter=False) sc_df = self.summary[values].sort_values(lcoe) sc_df['cumulative_capacity'] = ( - sc_df[SupplyCurveField.CAPACITY].cumsum() + sc_df[SupplyCurveField.CAPACITY_AC_MW].cumsum() ) return sc_df @@ -800,11 +800,11 @@ def _extract_sc_data(self, lcoe=SupplyCurveField.MEAN_LCOE): sc_df : pandas.DataFrame Supply curve data """ - values = [SupplyCurveField.CAPACITY, lcoe] + values = [SupplyCurveField.CAPACITY_AC_MW, lcoe] self._check_value(self.sc_table, values, scatter=False) sc_df = self.sc_table[values].sort_values(lcoe) sc_df['cumulative_capacity'] = ( - sc_df[SupplyCurveField.CAPACITY].cumsum() + sc_df[SupplyCurveField.CAPACITY_AC_MW].cumsum() ) return sc_df diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index a5c667193..56fbfb00c 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1978,6 +1978,28 @@ def capacity_ac(self): return self.area * self.power_density_ac + @property + def capacity_dc(self): + """Get the DC estimated capacity in MW of the supply curve point + in the current resource class with the applied exclusions. + + This values is provided only for solar inputs that have + the "dc_ac_ratio" dataset in the generation file. If these + conditions are not met, this value is `None`. + + Returns + ------- + capacity : float | None + Estimated AC capacity in MW of the supply curve point in the + current resource class with the applied exclusions. Only not + `None` for solar runs with "dc_ac_ratio" dataset in the + generation file + """ + if self.power_density_ac is None: + return None + + return self.area * self.power_density + @property def sc_point_capital_cost(self): """Get the capital cost for the entire SC point. @@ -2206,12 +2228,17 @@ def point_summary(self, args=None): SupplyCurveField.MEAN_CF: self.mean_cf, SupplyCurveField.MEAN_LCOE: self.mean_lcoe, SupplyCurveField.MEAN_RES: self.mean_res, - SupplyCurveField.CAPACITY: self.capacity, SupplyCurveField.AREA_SQ_KM: self.area, + SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, } + if self.capacity_ac is None: + ARGS[SupplyCurveField.CAPACITY_AC_MW] = self.capacity + else: + ARGS[SupplyCurveField.CAPACITY_AC_MW] = self.capacity_ac + extra_atts = { - SupplyCurveField.CAPACITY_AC: self.capacity_ac, + # SupplyCurveField.CAPACITY_AC: self.capacity_ac, SupplyCurveField.OFFSHORE: self.offshore, SupplyCurveField.SC_POINT_CAPITAL_COST: self.sc_point_capital_cost, SupplyCurveField.SC_POINT_FIXED_OPERATING_COST: ( diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index e7c361725..648eeb1f0 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -28,7 +28,7 @@ class SupplyCurve: """SupplyCurve""" def __init__(self, sc_points, trans_table, sc_features=None, - sc_capacity_col=SupplyCurveField.CAPACITY): + sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW): """ReV LCOT calculation and SupplyCurve sorting class. ``reV`` supply curve computes the transmission costs associated @@ -302,7 +302,7 @@ def _parse_trans_table(trans_table): @staticmethod def _map_trans_capacity(trans_sc_table, - sc_capacity_col=SupplyCurveField.CAPACITY): + sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW): """ Map SC gids to transmission features based on capacity. For any SC gids with capacity > the maximum transmission feature capacity, map @@ -513,10 +513,11 @@ def _check_sc_trans_table(cls, sc_points, trans_table): @classmethod def _merge_sc_trans_tables(cls, sc_points, trans_table, sc_cols=(SupplyCurveField.SC_GID, - SupplyCurveField.CAPACITY, + SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF, SupplyCurveField.MEAN_LCOE), - sc_capacity_col=SupplyCurveField.CAPACITY): + sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW + ): """ Merge the supply curve table with the transmission features table. @@ -600,10 +601,10 @@ def _merge_sc_trans_tables(cls, sc_points, trans_table, @classmethod def _map_tables(cls, sc_points, trans_table, sc_cols=(SupplyCurveField.SC_GID, - SupplyCurveField.CAPACITY, + SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF, SupplyCurveField.MEAN_LCOE), - sc_capacity_col=SupplyCurveField.CAPACITY): + sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW): """ Map supply curve points to transmission features @@ -618,8 +619,9 @@ def _map_tables(cls, sc_points, trans_table, sc_cols : tuple | list, optional List of column from sc_points to transfer into the trans table, If the `sc_capacity_col` is not included, it will get added. - by default (SupplyCurveField.SC_GID, SupplyCurveField.CAPACITY, - SupplyCurveField.MEAN_CF, SupplyCurveField.MEAN_LCOE) + by default (SupplyCurveField.SC_GID, + SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF, + SupplyCurveField.MEAN_LCOE) sc_capacity_col : str, optional Name of capacity column in `trans_sc_table`. The values in this column determine the size of transmission lines built. @@ -718,7 +720,7 @@ def _parse_sc_gids(trans_table, gid_key=SupplyCurveField.SC_GID): @staticmethod def _get_capacity(sc_gid, sc_table, connectable=True, - sc_capacity_col=SupplyCurveField.CAPACITY): + sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW): """ Get capacity of supply curve point @@ -767,7 +769,8 @@ def _get_capacity(sc_gid, sc_table, connectable=True, def _compute_trans_cap_cost(cls, trans_table, trans_costs=None, avail_cap_frac=1, max_workers=None, connectable=True, line_limited=False, - sc_capacity_col=SupplyCurveField.CAPACITY): + sc_capacity_col=( + SupplyCurveField.CAPACITY_AC_MW)): """ Compute levelized cost of transmission for all combinations of supply curve points and tranmission features in trans_table @@ -940,7 +943,7 @@ def compute_total_lcoe( cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" - cost /= self._trans_table[SupplyCurveField.CAPACITY] + cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF].values resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE] @@ -952,7 +955,7 @@ def compute_total_lcoe( .values.copy()) fr_cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" - fr_cost /= self._trans_table[SupplyCurveField.CAPACITY] + fr_cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] lcot_fr = ((cost + fr_cost) * fcr) / (cf_mean_arr * 8760) lcoe_fr = lcot_fr + resource_lcoe @@ -971,7 +974,7 @@ def compute_total_lcoe( .values.copy()) r_cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" - r_cost /= self._trans_table[SupplyCurveField.CAPACITY] + r_cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] cost += r_cost # $/MW lcot = (cost * fcr) / (cf_mean_arr * 8760) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 97afa5256..32e2bf56c 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -137,11 +137,11 @@ class SupplyCurveField(FieldEnum): MEAN_CF = "mean_cf" MEAN_LCOE = "mean_lcoe" MEAN_RES = "mean_res" - CAPACITY = "capacity" + CAPACITY_AC_MW = "capacity_ac_mw" + CAPACITY_DC_MW = "capacity_dc_mw" OFFSHORE = "offshore" SC_ROW_IND = "sc_row_ind" SC_COL_IND = "sc_col_ind" - CAPACITY_AC = "capacity_ac" CAPITAL_COST = "capital_cost" FIXED_OPERATING_COST = "fixed_operating_cost" VARIABLE_OPERATING_COST = "variable_operating_cost" diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index ddb83dbf1..d5a63e7df 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -469,7 +469,7 @@ def test_extra_outputs(gid=33): assert "lcoe_fcr-2013" in out assert "lcoe_fcr-means" in out - assert SupplyCurveField.CAPACITY in bsp.meta + assert SupplyCurveField.CAPACITY_AC_MW in bsp.meta assert SupplyCurveField.MEAN_CF in bsp.meta assert SupplyCurveField.MEAN_LCOE in bsp.meta @@ -503,7 +503,7 @@ def test_extra_outputs(gid=33): assert "lcoe_fcr-2013" in out assert "lcoe_fcr-means" in out - assert SupplyCurveField.CAPACITY in bsp.meta + assert SupplyCurveField.CAPACITY_AC_MW in bsp.meta assert SupplyCurveField.MEAN_CF in bsp.meta assert SupplyCurveField.MEAN_LCOE in bsp.meta @@ -1190,7 +1190,7 @@ def test_bespoke_prior_run(): cols = [ SupplyCurveField.TURBINE_X_COORDS, SupplyCurveField.TURBINE_Y_COORDS, - SupplyCurveField.CAPACITY, + SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.N_GIDS, SupplyCurveField.GID_COUNTS, SupplyCurveField.RES_GIDS, diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index 9532aa49e..1ef4400fb 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -211,7 +211,9 @@ def test_sc_agg_econ_scale(): res.create_dataset(k, res["meta"].shape, data=arr) res[k].attrs["scale_factor"] = 1.0 - eqn = f"2 * np.multiply(1000, {SupplyCurveField.CAPACITY}) ** -0.3" + eqn = ( + f"2 * np.multiply(1000, {SupplyCurveField.CAPACITY_AC_MW}) ** -0.3" + ) out_fp_base = os.path.join(td, "base") base = SupplyCurveAggregation( EXCL, @@ -271,7 +273,7 @@ def test_sc_agg_econ_scale(): assert np.allclose(true_scaled_lcoe, sc_df[SupplyCurveField.MEAN_LCOE]) assert np.allclose(true_raw_lcoe, sc_df[SupplyCurveField.RAW_LCOE]) - sc_df = sc_df.sort_values(SupplyCurveField.CAPACITY) + sc_df = sc_df.sort_values(SupplyCurveField.CAPACITY_AC_MW) assert all(sc_df[SupplyCurveField.MEAN_LCOE].diff()[1:] < 0) for i in sc_df.index.values: if sc_df.loc[i, 'scalars'] < 1: diff --git a/tests/test_hybrids.py b/tests/test_hybrids.py index 297ec3d4a..413cccd9f 100644 --- a/tests/test_hybrids.py +++ b/tests/test_hybrids.py @@ -91,7 +91,7 @@ def test_hybridization_profile_output_single_resource(solar_fpath, wind_fpath): res.meta[SupplyCurveField.SC_POINT_GID] == sc_point_gid )[0][0] - solar_cap = res.meta.loc[solar_idx, SupplyCurveField.CAPACITY] + solar_cap = res.meta.loc[solar_idx, SupplyCurveField.CAPACITY_AC_MW] solar_test_profile = res["rep_profiles_0", :, solar_idx] weighted_solar = solar_cap * solar_test_profile @@ -121,7 +121,7 @@ def test_hybridization_profile_output_with_ratio_none(solar_fpath, wind_fpath): res.meta[SupplyCurveField.SC_POINT_GID] == sc_point_gid )[0][0] - solar_cap = res.meta.loc[solar_idx, SupplyCurveField.CAPACITY] + solar_cap = res.meta.loc[solar_idx, SupplyCurveField.CAPACITY_AC_MW] solar_test_profile = res["rep_profiles_0", :, solar_idx] weighted_solar = solar_cap * solar_test_profile @@ -154,14 +154,14 @@ def test_hybridization_profile_output(solar_fpath, wind_fpath): solar_idx = np.where( res.meta[SupplyCurveField.SC_POINT_GID] == common_sc_point_gid )[0][0] - solar_cap = res.meta.loc[solar_idx, SupplyCurveField.CAPACITY] + solar_cap = res.meta.loc[solar_idx, SupplyCurveField.CAPACITY_AC_MW] solar_test_profile = res["rep_profiles_0", :, solar_idx] with Resource(wind_fpath) as res: wind_idx = np.where( res.meta[SupplyCurveField.SC_POINT_GID] == common_sc_point_gid )[0][0] - wind_cap = res.meta.loc[wind_idx, SupplyCurveField.CAPACITY] + wind_cap = res.meta.loc[wind_idx, SupplyCurveField.CAPACITY_AC_MW] wind_test_profile = res["rep_profiles_0", :, wind_idx] weighted_solar = solar_cap * solar_test_profile @@ -244,10 +244,10 @@ def test_meta_hybridization(input_combination, expected_shape, overlap, def test_limits_and_ratios_output_values(solar_fpath, wind_fpath): """Test that limits and ratios are properly applied in succession.""" - limits = {f"solar_{SupplyCurveField.CAPACITY}": 50, - f"wind_{SupplyCurveField.CAPACITY}": 0.5} - ratio_numerator = f"solar_{SupplyCurveField.CAPACITY}" - ratio_denominator = f"wind_{SupplyCurveField.CAPACITY}" + limits = {f"solar_{SupplyCurveField.CAPACITY_AC_MW}": 50, + f"wind_{SupplyCurveField.CAPACITY_AC_MW}": 0.5} + ratio_numerator = f"solar_{SupplyCurveField.CAPACITY_AC_MW}" + ratio_denominator = f"wind_{SupplyCurveField.CAPACITY_AC_MW}" ratio = "{}/{}".format(ratio_numerator, ratio_denominator) ratio_bounds = (0.3, 3.6) bounds = (0.3 - 1e6, 3.6 + 1e6) @@ -274,17 +274,17 @@ def test_limits_and_ratios_output_values(solar_fpath, wind_fpath): h.hybrid_meta["hybrid_{}".format(ratio_denominator)] <= h.hybrid_meta[ratio_denominator] ) - assert np.all(h.hybrid_meta[f"solar_{SupplyCurveField.CAPACITY}"] - <= limits[f"solar_{SupplyCurveField.CAPACITY}"]) - assert np.all(h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY}"] - <= limits[f"wind_{SupplyCurveField.CAPACITY}"]) + assert np.all(h.hybrid_meta[f"solar_{SupplyCurveField.CAPACITY_AC_MW}"] + <= limits[f"solar_{SupplyCurveField.CAPACITY_AC_MW}"]) + assert np.all(h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY_AC_MW}"] + <= limits[f"wind_{SupplyCurveField.CAPACITY_AC_MW}"]) @pytest.mark.parametrize( "ratio_cols", [ - (f"solar_{SupplyCurveField.CAPACITY}", - f"wind_{SupplyCurveField.CAPACITY}"), + (f"solar_{SupplyCurveField.CAPACITY_AC_MW}", + f"wind_{SupplyCurveField.CAPACITY_AC_MW}"), (f"solar_{SupplyCurveField.AREA_SQ_KM}", f"wind_{SupplyCurveField.AREA_SQ_KM}"), ], @@ -324,13 +324,13 @@ def test_ratios_input(ratio_cols, ratio_bounds, bounds, solar_fpath, ) if SupplyCurveField.CAPACITY in ratio: - col = f"hybrid_solar_{SupplyCurveField.CAPACITY}" + col = f"hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}" max_solar_capacities = h.hybrid_meta[col] max_solar_capacities = max_solar_capacities.values.reshape(1, -1) assert np.all( h.profiles["hybrid_solar_profile"] <= max_solar_capacities ) - col = f"hybrid_wind_{SupplyCurveField.CAPACITY}" + col = f"hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}" max_wind_capacities = h.hybrid_meta[col] max_wind_capacities = max_wind_capacities.values.reshape(1, -1) assert np.all(h.profiles["hybrid_wind_profile"] <= max_wind_capacities) @@ -364,23 +364,23 @@ def test_rep_profile_idx_map(solar_fpath, wind_fpath): def test_limits_values(solar_fpath, wind_fpath): """Test that column values are properly limited on user input.""" - limits = {f"solar_{SupplyCurveField.CAPACITY}": 100, - f"wind_{SupplyCurveField.CAPACITY}": 0.5} + limits = {f"solar_{SupplyCurveField.CAPACITY_AC_MW}": 100, + f"wind_{SupplyCurveField.CAPACITY_AC_MW}": 0.5} h = Hybridization(solar_fpath, wind_fpath, limits=limits) h.run() - assert np.all(h.hybrid_meta[f"solar_{SupplyCurveField.CAPACITY}"] - <= limits[f"solar_{SupplyCurveField.CAPACITY}"]) - assert np.all(h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY}"] - <= limits[f"wind_{SupplyCurveField.CAPACITY}"]) + assert np.all(h.hybrid_meta[f"solar_{SupplyCurveField.CAPACITY_AC_MW}"] + <= limits[f"solar_{SupplyCurveField.CAPACITY_AC_MW}"]) + assert np.all(h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY_AC_MW}"] + <= limits[f"wind_{SupplyCurveField.CAPACITY_AC_MW}"]) def test_invalid_limits_column_name(solar_fpath, wind_fpath): """Test invalid inputs for limits columns.""" test_limits = {"un_prefixed_col": 0, - f"wind_{SupplyCurveField.CAPACITY}": 10} + f"wind_{SupplyCurveField.CAPACITY_AC_MW}": 10} with pytest.raises(InputError) as excinfo: Hybridization(solar_fpath, wind_fpath, limits=test_limits) @@ -392,7 +392,7 @@ def test_fillna_values(solar_fpath, wind_fpath): """Test that N/A values are filled properly based on user input.""" fill_vals = {f"solar_{SupplyCurveField.N_GIDS}": 0, - f"wind_{SupplyCurveField.CAPACITY}": -1} + f"wind_{SupplyCurveField.CAPACITY_AC_MW}": -1} h = Hybridization( solar_fpath, @@ -405,19 +405,20 @@ def test_fillna_values(solar_fpath, wind_fpath): assert not np.any(h.hybrid_meta[f"solar_{SupplyCurveField.N_GIDS}"].isna()) assert not np.any( - h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY}"].isna() + h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY_AC_MW}"].isna() ) assert np.any(h.hybrid_meta[f"solar_{SupplyCurveField.N_GIDS}"].values == 0) - assert np.any(h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY}"].values - == -1) + assert np.any( + h.hybrid_meta[f"wind_{SupplyCurveField.CAPACITY_AC_MW}"].values + == -1) def test_invalid_fillna_column_name(solar_fpath, wind_fpath): """Test invalid inputs for fillna columns.""" test_fillna = {"un_prefixed_col": 0, - f"wind_{SupplyCurveField.CAPACITY}": 10} + f"wind_{SupplyCurveField.CAPACITY_AC_MW}": 10} with pytest.raises(InputError) as excinfo: Hybridization(solar_fpath, wind_fpath, fillna=test_fillna) @@ -514,7 +515,8 @@ def test_invalid_ratio_bounds_length_input(solar_fpath, wind_fpath): """Test improper ratios input.""" ratio = ( - f"solar_{SupplyCurveField.CAPACITY}/wind_{SupplyCurveField.CAPACITY}" + f"solar_{SupplyCurveField.CAPACITY_AC_MW}" + f"/wind_{SupplyCurveField.CAPACITY_AC_MW}" ) with pytest.raises(InputError) as excinfo: Hybridization( @@ -531,7 +533,7 @@ def test_invalid_ratio_bounds_length_input(solar_fpath, wind_fpath): def test_ratio_column_missing(solar_fpath, wind_fpath): """Test missing ratio column.""" - ratio = f"solar_col_dne/wind_{SupplyCurveField.CAPACITY}" + ratio = f"solar_col_dne/wind_{SupplyCurveField.CAPACITY_AC_MW}" with pytest.raises(FileInputError) as excinfo: Hybridization( solar_fpath, wind_fpath, ratio=ratio, ratio_bounds=(1, 1) @@ -576,7 +578,7 @@ def test_invalid_ratio_format(ratio, solar_fpath, wind_fpath): def test_invalid_ratio_column_name(solar_fpath, wind_fpath): """Test invalid inputs for ratio columns.""" - ratio = f"un_prefixed_col/wind_{SupplyCurveField.CAPACITY}" + ratio = f"un_prefixed_col/wind_{SupplyCurveField.CAPACITY_AC_MW}" with pytest.raises(InputError) as excinfo: Hybridization( solar_fpath, wind_fpath, ratio=ratio, ratio_bounds=(1, 1) @@ -724,7 +726,8 @@ def test_hybrids_data_contains_col(solar_fpath, wind_fpath): @pytest.mark.parametrize("half_hour", [True, False]) @pytest.mark.parametrize( "ratio", - [f"solar_{SupplyCurveField.CAPACITY}/wind_{SupplyCurveField.CAPACITY}", + [f"solar_{SupplyCurveField.CAPACITY_AC_MW}" + f"/wind_{SupplyCurveField.CAPACITY_AC_MW}", f"solar_{SupplyCurveField.AREA_SQ_KM}" f"/wind_{SupplyCurveField.AREA_SQ_KM}"], ) @@ -738,8 +741,8 @@ def test_hybrids_cli_from_config( fv = -999 allow_solar_only, allow_wind_only = input_combination fill_vals = {f"solar_{SupplyCurveField.N_GIDS}": 0, - f"wind_{SupplyCurveField.CAPACITY}": -1} - limits = {f"solar_{SupplyCurveField.CAPACITY}": 100} + f"wind_{SupplyCurveField.CAPACITY_AC_MW}": -1} + limits = {f"solar_{SupplyCurveField.CAPACITY_AC_MW}": 100} if half_hour: sfp, wfp = solar_fpath_30_min, wind_fpath diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index af80190d7..b795cd8ca 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -284,7 +284,7 @@ def test_parallel(): def verify_trans_cap(sc_table, trans_tables, - cap_col=SupplyCurveField.CAPACITY): + cap_col=SupplyCurveField.CAPACITY_AC_MW): """ Verify that sc_points are connected to features in the correct capacity bins @@ -778,13 +778,15 @@ def test_least_cost_simple_with_ac_capacity_column(): trans_tables.append(out_fp) sc = SC_POINTS.copy() - sc[SupplyCurveField.CAPACITY_AC] = sc[SupplyCurveField.CAPACITY] / 1.02 + sc[SupplyCurveField.CAPACITY_AC_MW] = ( + sc[SupplyCurveField.CAPACITY_DC_MW] / 1.02 + ) sc = SupplyCurve(sc, trans_tables, - sc_capacity_col=SupplyCurveField.CAPACITY_AC) + sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW) sc_simple_ac_cap = sc.simple_sort(fcr=0.1) verify_trans_cap(sc_simple_ac_cap, trans_tables, - cap_col=SupplyCurveField.CAPACITY_AC) + cap_col=SupplyCurveField.CAPACITY_AC_MW) assert np.allclose( sc_simple["trans_cap_cost_per_mw"] * 1.02, diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index f8d49ac15..3ee399bab 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -162,13 +162,13 @@ def test_agg_summary_solar_ac(pd): ) summary = sca.summarize(gen, max_workers=1) - assert SupplyCurveField.CAPACITY_AC in summary - assert np.allclose(summary[SupplyCurveField.CAPACITY] / 1.3, - summary[SupplyCurveField.CAPACITY_AC]) + assert SupplyCurveField.CAPACITY_AC_MW in summary + assert np.allclose(summary[SupplyCurveField.CAPACITY_DC_MW] / 1.3, + summary[SupplyCurveField.CAPACITY_AC_MW]) def test_multi_file_excl(): - """Test sc aggregation with multple exclusion file inputs.""" + """Test sc aggregation with multiple exclusion file inputs.""" excl_dict = { "ri_srtm_slope": { @@ -360,7 +360,7 @@ def test_agg_scalar_excl(): ) summary_with_weights = sca.summarize(GEN, max_workers=1) - dsets = [SupplyCurveField.AREA_SQ_KM, SupplyCurveField.CAPACITY] + dsets = [SupplyCurveField.AREA_SQ_KM, SupplyCurveField.CAPACITY_AC_MW] for dset in dsets: diff = summary_base[dset].values / summary_with_weights[dset].values msg = ("Fractional exclusions failed for {} which has values {} and {}" @@ -433,7 +433,7 @@ def test_data_layer_methods(): @pytest.mark.parametrize( "cap_cost_scale", - ["1", f"2 * np.multiply(1000, {SupplyCurveField.CAPACITY}) ** -0.3"] + ["1", f"2 * np.multiply(1000, {SupplyCurveField.CAPACITY_AC_MW}) ** -0.3"] ) def test_recalc_lcoe(cap_cost_scale): """Test supply curve aggregation with the re-calculation of lcoe using the diff --git a/tests/test_supply_curve_vpd.py b/tests/test_supply_curve_vpd.py index 6b8e334c2..e28856b98 100644 --- a/tests/test_supply_curve_vpd.py +++ b/tests/test_supply_curve_vpd.py @@ -50,7 +50,7 @@ def test_vpd(): summary = sca.summarize(GEN, max_workers=1) for i in summary.index: - capacity = summary.loc[i, SupplyCurveField.CAPACITY] + capacity = summary.loc[i, SupplyCurveField.CAPACITY_AC_MW] area = summary.loc[i, SupplyCurveField.AREA_SQ_KM] res_gids = np.array(summary.loc[i, SupplyCurveField.RES_GIDS]) gid_counts = np.array(summary.loc[i, SupplyCurveField.GID_COUNTS]) @@ -96,8 +96,8 @@ def test_vpd_fractional_excl(): summary_2 = sca_2.summarize(GEN, max_workers=1) for i in summary_1.index: - cap_full = summary_1.loc[i, SupplyCurveField.CAPACITY] - cap_half = summary_2.loc[i, SupplyCurveField.CAPACITY] + cap_full = summary_1.loc[i, SupplyCurveField.CAPACITY_AC_MW] + cap_half = summary_2.loc[i, SupplyCurveField.CAPACITY_AC_MW] msg = ('Variable power density for fractional exclusions failed! ' 'Index {} has cap full {} and cap half {}' From f45f09d8c05274fb8b84c320c4a31ba497bea700 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:02:24 -0600 Subject: [PATCH 21/88] Fix cap cost scaling --- reV/supply_curve/supply_curve.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index 648eeb1f0..ecea562cd 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -138,6 +138,7 @@ def __init__(self, sc_points, trans_table, sc_features=None, self._sc_points, trans_table, sc_capacity_col=sc_capacity_col ) self._sc_gids, self._mask = self._parse_sc_gids(self._trans_table) + self._costs_capacity_col = self._determine_cost_cap_col() def __repr__(self): msg = "{} with {} points".format(self.__class__.__name__, len(self)) @@ -157,6 +158,16 @@ def __getitem__(self, gid): return self._sc_points.iloc[i] + def _determine_cost_cap_col(self): + """Determine the cap column used to scale costs (DC for solar runs)""" + if SupplyCurveField.CAPACITY_DC_MW not in self._trans_table: + return SupplyCurveField.CAPACITY_AC_MW + + if self._trans_table[SupplyCurveField.CAPACITY_DC_MW].isna().all(): + return SupplyCurveField.CAPACITY_AC_MW + + return SupplyCurveField.CAPACITY_DC_MW + @staticmethod def _parse_sc_points(sc_points, sc_features=None): """ @@ -584,11 +595,12 @@ def _merge_sc_trans_tables(cls, sc_points, trans_table, if isinstance(sc_cols, tuple): sc_cols = list(sc_cols) - if SupplyCurveField.MEAN_LCOE_FRICTION in sc_points: - sc_cols.append(SupplyCurveField.MEAN_LCOE_FRICTION) - - if "transmission_multiplier" in sc_points: - sc_cols.append("transmission_multiplier") + extra_cols = [SupplyCurveField.CAPACITY_DC_MW, + SupplyCurveField.MEAN_LCOE_FRICTION, + "transmission_multiplier"] + for col in extra_cols: + if col in sc_points: + sc_cols.append(col) sc_cols += merge_cols sc_points = sc_points[sc_cols].copy() @@ -943,7 +955,7 @@ def compute_total_lcoe( cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" - cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] + cost /= self._trans_table[self._costs_capacity_col] cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF].values resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE] @@ -955,7 +967,7 @@ def compute_total_lcoe( .values.copy()) fr_cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" - fr_cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] + fr_cost /= self._trans_table[self._costs_capacity_col] lcot_fr = ((cost + fr_cost) * fcr) / (cf_mean_arr * 8760) lcoe_fr = lcot_fr + resource_lcoe @@ -974,7 +986,7 @@ def compute_total_lcoe( .values.copy()) r_cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" - r_cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] + r_cost /= self._trans_table[self._costs_capacity_col] cost += r_cost # $/MW lcot = (cost * fcr) / (cf_mean_arr * 8760) From fcc37d8e749889559c137d7a52d34cc82f050a19 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:02:36 -0600 Subject: [PATCH 22/88] Fix test --- tests/test_supply_curve_compute.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index b795cd8ca..ebd818766 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -762,6 +762,12 @@ def test_least_cost_simple_with_ac_capacity_column(): trans_tables.append(out_fp) sc = SupplyCurve(SC_POINTS, trans_tables) + # sc = SC_POINTS.copy() + # sc[SupplyCurveField.CAPACITY_DC_MW] = ( + # sc[SupplyCurveField.CAPACITY_AC_MW].values + # ) + # sc = SupplyCurve(sc, trans_tables, + # sc_capacity_col=SupplyCurveField.CAPACITY_DC_MW) sc_simple = sc.simple_sort(fcr=0.1) verify_trans_cap(sc_simple, trans_tables) @@ -778,10 +784,12 @@ def test_least_cost_simple_with_ac_capacity_column(): trans_tables.append(out_fp) sc = SC_POINTS.copy() + sc[SupplyCurveField.CAPACITY_DC_MW] = ( + sc[SupplyCurveField.CAPACITY_AC_MW].values + ) sc[SupplyCurveField.CAPACITY_AC_MW] = ( sc[SupplyCurveField.CAPACITY_DC_MW] / 1.02 ) - sc = SupplyCurve(sc, trans_tables, sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW) sc_simple_ac_cap = sc.simple_sort(fcr=0.1) From e31055ab801173b6f77c74e28b567fff9b571a56 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:03:18 -0600 Subject: [PATCH 23/88] New default output spec --- reV/supply_curve/points.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 56fbfb00c..07219e075 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2229,16 +2229,13 @@ def point_summary(self, args=None): SupplyCurveField.MEAN_LCOE: self.mean_lcoe, SupplyCurveField.MEAN_RES: self.mean_res, SupplyCurveField.AREA_SQ_KM: self.area, + SupplyCurveField.CAPACITY_AC_MW: ( + self.capacity if self.capacity_ac is None else self.capacity_ac + ) SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, } - if self.capacity_ac is None: - ARGS[SupplyCurveField.CAPACITY_AC_MW] = self.capacity - else: - ARGS[SupplyCurveField.CAPACITY_AC_MW] = self.capacity_ac - extra_atts = { - # SupplyCurveField.CAPACITY_AC: self.capacity_ac, SupplyCurveField.OFFSHORE: self.offshore, SupplyCurveField.SC_POINT_CAPITAL_COST: self.sc_point_capital_cost, SupplyCurveField.SC_POINT_FIXED_OPERATING_COST: ( From 08ba36ff9da43c4ff2471681fe54f05521063626 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:03:48 -0600 Subject: [PATCH 24/88] Add missing comma --- reV/supply_curve/points.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 07219e075..6273904f7 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2231,7 +2231,7 @@ def point_summary(self, args=None): SupplyCurveField.AREA_SQ_KM: self.area, SupplyCurveField.CAPACITY_AC_MW: ( self.capacity if self.capacity_ac is None else self.capacity_ac - ) + ), SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, } From f2e4fa45c55367d17ccfee1174e78fd21b1b01b4 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:04:01 -0600 Subject: [PATCH 25/88] Add alias --- reV/utilities/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 32e2bf56c..86e5f7fba 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -190,6 +190,8 @@ class _LegacySCAliases(Enum): values where each string value represents a previously known alias. """ + CAPACITY_AC_MW = "capacity" + class ModuleName(str, Enum): """A collection of the module names available in reV. From 366ea5bf090d1c1a0375b90e6a43a232b6fc9932 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:11:30 -0600 Subject: [PATCH 26/88] Linter fix --- reV/utilities/__init__.py | 1 - tests/test_econ_of_scale.py | 11 ++++++----- tests/test_hybrids.py | 5 +++++ tests/test_supply_curve_vpd.py | 3 ++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 86e5f7fba..7f798d547 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -162,7 +162,6 @@ class SupplyCurveField(FieldEnum): EOS_MULT = "eos_mult" REG_MULT = "reg_mult" - @classmethod def map_from_legacy(cls): """Map of legacy names to current values. diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index 1ef4400fb..0b0b8c27b 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -147,9 +147,10 @@ def test_econ_of_scale_baseline(): with Resource(GEN) as res: cf = res["cf_mean-means"] - lcoe = (1000 * (data['fixed_charge_rate'] * data['capital_cost'] - + data['fixed_operating_cost']) - / (cf * data['system_capacity'] * 8760)) + lcoe = (1000 + * (data['fixed_charge_rate'] * data['capital_cost'] + + data['fixed_operating_cost']) + / (cf * data['system_capacity'] * 8760)) with h5py.File(gen_temp, "a") as res: res["lcoe_fcr-means"][...] = lcoe @@ -251,11 +252,11 @@ def test_sc_agg_econ_scale(): aep = ((sc_df['mean_fixed_charge_rate'] * sc_df['mean_capital_cost'] + sc_df['mean_fixed_operating_cost']) - / sc_df[SupplyCurveField.RAW_LCOE]) + / sc_df[SupplyCurveField.RAW_LCOE]) true_raw_lcoe = ((data['fixed_charge_rate'] * data['capital_cost'] + data['fixed_operating_cost']) - / aep + data['variable_operating_cost']) + / aep + data['variable_operating_cost']) eval_inputs = {k: sc_df[k].values.flatten() for k in sc_df.columns} # pylint: disable=eval-used diff --git a/tests/test_hybrids.py b/tests/test_hybrids.py index 413cccd9f..7e2d5f745 100644 --- a/tests/test_hybrids.py +++ b/tests/test_hybrids.py @@ -45,12 +45,14 @@ def _fix_meta(fp): @pytest.fixture(scope="module") def module_td(): + """Module-level temporaty dirsctory""" with tempfile.TemporaryDirectory() as td: yield td @pytest.fixture(scope="module") def solar_fpath(module_td): + """Solar fpath with legacy columns renamed. """ new_fp = os.path.join(module_td, "solar.h5") shutil.copy(SOLAR_FPATH, new_fp) _fix_meta(new_fp) @@ -59,6 +61,7 @@ def solar_fpath(module_td): @pytest.fixture(scope="module") def wind_fpath(module_td): + """Wind fpath with legacy columns renamed. """ new_fp = os.path.join(module_td, "wind.h5") shutil.copy(WIND_FPATH, new_fp) _fix_meta(new_fp) @@ -67,6 +70,7 @@ def wind_fpath(module_td): @pytest.fixture(scope="module") def solar_fpath_30_min(module_td): + """Solar fpath (30 min data) with legacy columns renamed.""" new_fp = os.path.join(module_td, "solar_30min.h5") shutil.copy(SOLAR_FPATH_30_MIN, new_fp) _fix_meta(new_fp) @@ -75,6 +79,7 @@ def solar_fpath_30_min(module_td): @pytest.fixture(scope="module") def solar_fpath_mult(module_td): + """Solar fpath (with mult) with legacy columns renamed. """ new_fp = os.path.join(module_td, "solar_mult.h5") shutil.copy(SOLAR_FPATH_MULT, new_fp) _fix_meta(new_fp) diff --git a/tests/test_supply_curve_vpd.py b/tests/test_supply_curve_vpd.py index e28856b98..c082e6b59 100644 --- a/tests/test_supply_curve_vpd.py +++ b/tests/test_supply_curve_vpd.py @@ -83,7 +83,8 @@ def test_vpd_fractional_excl(): sca_1 = SupplyCurveAggregation(EXCL, TM_DSET, excl_dict=excl_dict_1, res_class_dset=RES_CLASS_DSET, res_class_bins=RES_CLASS_BINS, - data_layers=DATA_LAYERS, power_density=tmp_path, + data_layers=DATA_LAYERS, + power_density=tmp_path, gids=gids_subset) summary_1 = sca_1.summarize(GEN, max_workers=1) From ed7b74bf32f46ae4700a1bfff665343737fe93b5 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 15:42:17 -0600 Subject: [PATCH 27/88] Rename enum --- reV/bespoke/bespoke.py | 2 +- reV/hybrids/hybrid_methods.py | 6 +++--- reV/hybrids/hybrids.py | 4 ++-- reV/supply_curve/points.py | 2 +- reV/supply_curve/supply_curve.py | 8 ++++---- reV/utilities/__init__.py | 2 +- tests/test_bespoke.py | 4 ++-- tests/test_supply_curve_compute.py | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index f2437077f..907d51767 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1261,7 +1261,7 @@ def run_wind_plant_ts(self): # copy dataset outputs to meta data for supply curve table summary if "cf_mean-means" in self.outputs: - self._meta.loc[:, SupplyCurveField.MEAN_CF] = self.outputs[ + self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[ "cf_mean-means" ] if "lcoe_fcr-means" in self.outputs: diff --git a/reV/hybrids/hybrid_methods.py b/reV/hybrids/hybrid_methods.py index 6ff6721be..7d86a4ff6 100644 --- a/reV/hybrids/hybrid_methods.py +++ b/reV/hybrids/hybrid_methods.py @@ -111,8 +111,8 @@ def aggregate_capacity_factor(h): sc = f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}' wc = f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}' - scf = f'solar_{SupplyCurveField.MEAN_CF}' - wcf = f'wind_{SupplyCurveField.MEAN_CF}' + scf = f'solar_{SupplyCurveField.MEAN_CF_AC}' + wcf = f'wind_{SupplyCurveField.MEAN_CF_AC}' missing_solar_cap = sc not in h.hybrid_meta.columns missing_wind_cap = wc not in h.hybrid_meta.columns missing_solar_mean_cf = scf not in h.hybrid_meta.columns @@ -135,5 +135,5 @@ def aggregate_capacity_factor(h): ), f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}': aggregate_wind_capacity, f'hybrid_{SupplyCurveField.CAPACITY_AC_MW}': aggregate_capacity, - f'hybrid_{SupplyCurveField.MEAN_CF}': aggregate_capacity_factor + f'hybrid_{SupplyCurveField.MEAN_CF_AC}': aggregate_capacity_factor } diff --git a/reV/hybrids/hybrids.py b/reV/hybrids/hybrids.py index 911be3a32..25ca3fc96 100644 --- a/reV/hybrids/hybrids.py +++ b/reV/hybrids/hybrids.py @@ -41,8 +41,8 @@ DROPPED_COLUMNS = [SupplyCurveField.GID] DEFAULT_FILL_VALUES = {f'solar_{SupplyCurveField.CAPACITY_AC_MW}': 0, f'wind_{SupplyCurveField.CAPACITY_AC_MW}': 0, - f'solar_{SupplyCurveField.MEAN_CF}': 0, - f'wind_{SupplyCurveField.MEAN_CF}': 0} + f'solar_{SupplyCurveField.MEAN_CF_AC}': 0, + f'wind_{SupplyCurveField.MEAN_CF_AC}': 0} OUTPUT_PROFILE_NAMES = ['hybrid_profile', 'hybrid_solar_profile', 'hybrid_wind_profile'] diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 6273904f7..17eac9d1e 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2225,7 +2225,7 @@ def point_summary(self, args=None): SupplyCurveField.GEN_GIDS: self.gen_gid_set, SupplyCurveField.GID_COUNTS: self.gid_counts, SupplyCurveField.N_GIDS: self.n_gids, - SupplyCurveField.MEAN_CF: self.mean_cf, + SupplyCurveField.MEAN_CF_AC: self.mean_cf, SupplyCurveField.MEAN_LCOE: self.mean_lcoe, SupplyCurveField.MEAN_RES: self.mean_res, SupplyCurveField.AREA_SQ_KM: self.area, diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index ecea562cd..097c29ba2 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -525,7 +525,7 @@ def _check_sc_trans_table(cls, sc_points, trans_table): def _merge_sc_trans_tables(cls, sc_points, trans_table, sc_cols=(SupplyCurveField.SC_GID, SupplyCurveField.CAPACITY_AC_MW, - SupplyCurveField.MEAN_CF, + SupplyCurveField.MEAN_CF_AC, SupplyCurveField.MEAN_LCOE), sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW ): @@ -614,7 +614,7 @@ def _merge_sc_trans_tables(cls, sc_points, trans_table, def _map_tables(cls, sc_points, trans_table, sc_cols=(SupplyCurveField.SC_GID, SupplyCurveField.CAPACITY_AC_MW, - SupplyCurveField.MEAN_CF, + SupplyCurveField.MEAN_CF_AC, SupplyCurveField.MEAN_LCOE), sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW): """ @@ -632,7 +632,7 @@ def _map_tables(cls, sc_points, trans_table, List of column from sc_points to transfer into the trans table, If the `sc_capacity_col` is not included, it will get added. by default (SupplyCurveField.SC_GID, - SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF, + SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF_AC, SupplyCurveField.MEAN_LCOE) sc_capacity_col : str, optional Name of capacity column in `trans_sc_table`. The values in @@ -956,7 +956,7 @@ def compute_total_lcoe( cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" cost /= self._trans_table[self._costs_capacity_col] - cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF].values + cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC].values resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE] if 'reinforcement_cost_floored_per_mw' in self._trans_table: diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 7f798d547..465360b1b 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -134,7 +134,7 @@ class SupplyCurveField(FieldEnum): COUNTY = "county" STATE = "state" COUNTRY = "country" - MEAN_CF = "mean_cf" + MEAN_CF_AC = "mean_cf" MEAN_LCOE = "mean_lcoe" MEAN_RES = "mean_res" CAPACITY_AC_MW = "capacity_ac_mw" diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index d5a63e7df..08c32912c 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -470,7 +470,7 @@ def test_extra_outputs(gid=33): assert "lcoe_fcr-means" in out assert SupplyCurveField.CAPACITY_AC_MW in bsp.meta - assert SupplyCurveField.MEAN_CF in bsp.meta + assert SupplyCurveField.MEAN_CF_AC in bsp.meta assert SupplyCurveField.MEAN_LCOE in bsp.meta assert "pct_slope" in bsp.meta @@ -504,7 +504,7 @@ def test_extra_outputs(gid=33): assert "lcoe_fcr-means" in out assert SupplyCurveField.CAPACITY_AC_MW in bsp.meta - assert SupplyCurveField.MEAN_CF in bsp.meta + assert SupplyCurveField.MEAN_CF_AC in bsp.meta assert SupplyCurveField.MEAN_LCOE in bsp.meta assert "pct_slope" in bsp.meta diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index ebd818766..41ef65a7a 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -614,7 +614,7 @@ def test_least_cost_simple_with_trans_cap_cost_per_mw(r_costs): assert (sc_simple["trans_gid"] == 42445).all() if not r_costs: - lcot = 4244.5 / (sc_simple[SupplyCurveField.MEAN_CF] * 8760) + lcot = 4244.5 / (sc_simple[SupplyCurveField.MEAN_CF_AC] * 8760) assert np.allclose(lcot, sc_simple["lcot"], atol=0.001) From b3543001df35b1c1912acd9f1026f15089b49bd8 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 16:03:33 -0600 Subject: [PATCH 28/88] Fix test --- tests/test_hybrids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hybrids.py b/tests/test_hybrids.py index 7e2d5f745..61765b92c 100644 --- a/tests/test_hybrids.py +++ b/tests/test_hybrids.py @@ -328,7 +328,7 @@ def test_ratios_input(ratio_cols, ratio_bounds, bounds, solar_fpath, <= h.hybrid_meta[ratio_denominator] ) - if SupplyCurveField.CAPACITY in ratio: + if SupplyCurveField.CAPACITY_AC_MW in ratio: col = f"hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}" max_solar_capacities = h.hybrid_meta[col] max_solar_capacities = max_solar_capacities.values.reshape(1, -1) From 343a1935787736672f71347cddf182b44ee6f86c Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 17:23:40 -0600 Subject: [PATCH 29/88] Remove old code --- tests/test_supply_curve_compute.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index 41ef65a7a..ce419a648 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -762,12 +762,6 @@ def test_least_cost_simple_with_ac_capacity_column(): trans_tables.append(out_fp) sc = SupplyCurve(SC_POINTS, trans_tables) - # sc = SC_POINTS.copy() - # sc[SupplyCurveField.CAPACITY_DC_MW] = ( - # sc[SupplyCurveField.CAPACITY_AC_MW].values - # ) - # sc = SupplyCurve(sc, trans_tables, - # sc_capacity_col=SupplyCurveField.CAPACITY_DC_MW) sc_simple = sc.simple_sort(fcr=0.1) verify_trans_cap(sc_simple, trans_tables) From 11aa9bc0b18675701bd46db90460a73fcfa3ff5c Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 17:24:27 -0600 Subject: [PATCH 30/88] Add dc capacity factor as output --- reV/supply_curve/points.py | 84 ++++++++++++++++++++++- reV/utilities/__init__.py | 1 + tests/test_supply_curve_sc_aggregation.py | 16 ++++- 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 17eac9d1e..aca7fdbd9 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1467,7 +1467,11 @@ def __init__( (resource) "gid" and "power_density columns". cf_dset : str | np.ndarray Dataset name from gen containing capacity factor mean values. - Can be pre-extracted generation output data in np.ndarray. + This name is used to infer AC capacity factor dataset for + solar runs (i.e. the AC vsersion of "cf_mean-means" would + be inferred to be "cf_mean_ac-means"). This input can also + be pre-extracted generation output data in np.ndarray, in + which case all DC solar outputs are set to `None`. lcoe_dset : str | np.ndarray Dataset name from gen containing LCOE mean values. Can be pre-extracted generation output data in np.ndarray. @@ -1684,6 +1688,30 @@ def gen_data(self): return self._gen_data + @property + def gen_ac_data(self): + """Get the generation ac capacity factor data array. + + This output is only not `None` for solar runs where `cf_dset` + was specified as a string. + + Returns + ------- + gen_ac_data : np.ndarray | None + Multi-year-mean ac capacity factor data array for all sites + in the generation data output file or `None` if none + detected. + """ + + if isinstance(self._cf_dset, np.ndarray): + return None + + ac_cf_dset = _infer_cf_dset_ac(self._cf_dset) + if ac_cf_dset in self.gen.datasets: + return self.gen[ac_cf_dset] + + return None + @property def lcoe_data(self): """Get the LCOE data array. @@ -1721,6 +1749,45 @@ def mean_cf(self): return mean_cf + @property + def mean_cf_ac(self): + """Get the mean AC capacity factor for the non-excluded data. + + This output is only not `None` for solar runs. + + Capacity factor is weighted by the exclusions (usually 0 or 1, + but 0.5 exclusions will weight appropriately). + + Returns + ------- + mean_cf_ac : float | None + Mean capacity factor value for the non-excluded data. + """ + mean_cf_ac = None + if self.gen_ac_data is not None: + mean_cf_ac = self.exclusion_weighted_mean(self.gen_ac_data) + + return mean_cf_ac + + @property + def mean_cf_dc(self): + """Get the mean DC capacity factor for the non-excluded data. + + This output is only not `None` for solar runs. + + Capacity factor is weighted by the exclusions (usually 0 or 1, + but 0.5 exclusions will weight appropriately). + + Returns + ------- + mean_cf_dc : float | None + Mean capacity factor value for the non-excluded data. + """ + if self.mean_cf_ac is not None: + return self.mean_cf + + return None + @property def mean_lcoe(self): """Get the mean LCOE for the non-excluded data. @@ -2225,7 +2292,10 @@ def point_summary(self, args=None): SupplyCurveField.GEN_GIDS: self.gen_gid_set, SupplyCurveField.GID_COUNTS: self.gid_counts, SupplyCurveField.N_GIDS: self.n_gids, - SupplyCurveField.MEAN_CF_AC: self.mean_cf, + SupplyCurveField.MEAN_CF_AC: ( + self.mean_cf if self.mean_cf_ac is None else self.mean_cf_ac + ), + SupplyCurveField.MEAN_CF_DC: self.mean_cf_dc, SupplyCurveField.MEAN_LCOE: self.mean_lcoe, SupplyCurveField.MEAN_RES: self.mean_res, SupplyCurveField.AREA_SQ_KM: self.area, @@ -2454,3 +2524,13 @@ def summarize( summary = point.economies_of_scale(cap_cost_scale, summary) return summary + + +def _infer_cf_dset_ac(cf_dset): + """Infer AC dataset name from input. """ + parts = cf_dset.split("-") + if len(parts) == 1: + return f"{cf_dset}_ac" + + cf_name = "-".join(parts[:-1]) + return f"{cf_name}_ac-{parts[-1]}" diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 465360b1b..2cc4e209b 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -135,6 +135,7 @@ class SupplyCurveField(FieldEnum): STATE = "state" COUNTRY = "country" MEAN_CF_AC = "mean_cf" + MEAN_CF_DC = "capacity_factor_dc" MEAN_LCOE = "mean_lcoe" MEAN_RES = "mean_res" CAPACITY_AC_MW = "capacity_ac_mw" diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 3ee399bab..5d75ff769 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -130,11 +130,21 @@ 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) + assert SupplyCurveField.CAPACITY_AC_MW in summary + assert SupplyCurveField.CAPACITY_DC_MW in summary + assert SupplyCurveField.MEAN_CF_AC in summary + assert SupplyCurveField.MEAN_CF_DC in summary - assert "capacity_ac" not in summary + # dc outputs are `None` because old gen file does not have correct + # output dsets + assert not summary[SupplyCurveField.CAPACITY_AC_MW].isna().any() + assert not summary[SupplyCurveField.CAPACITY_DC_MW].isna().all() + assert not summary[SupplyCurveField.MEAN_CF_AC].isna().any() + assert not summary[SupplyCurveField.MEAN_CF_DC].isna().all() + + summary = summary[list(s_baseline.columns)] + assert_frame_equal(summary, s_baseline, check_dtype=False, rtol=0.0001) @pytest.mark.parametrize("pd", [None, 45]) From c3ee26c4f20eae4b158eab34f947c8a7e0c79332 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 17:26:35 -0600 Subject: [PATCH 31/88] Scale costs using DC CF column for solar --- reV/supply_curve/supply_curve.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index 097c29ba2..7a84ff0c4 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -138,7 +138,12 @@ def __init__(self, sc_points, trans_table, sc_features=None, self._sc_points, trans_table, sc_capacity_col=sc_capacity_col ) self._sc_gids, self._mask = self._parse_sc_gids(self._trans_table) - self._costs_capacity_col = self._determine_cost_cap_col() + self._costs_capacity_col = self._determine_cost_col( + SupplyCurveField.CAPACITY_DC_MW, SupplyCurveField.CAPACITY_AC_MW + ) + self._costs_cf_col = self._determine_cost_col( + SupplyCurveField.MEAN_CF_DC, SupplyCurveField.MEAN_CF_AC + ) def __repr__(self): msg = "{} with {} points".format(self.__class__.__name__, len(self)) @@ -158,15 +163,15 @@ def __getitem__(self, gid): return self._sc_points.iloc[i] - def _determine_cost_cap_col(self): - """Determine the cap column used to scale costs (DC for solar runs)""" - if SupplyCurveField.CAPACITY_DC_MW not in self._trans_table: - return SupplyCurveField.CAPACITY_AC_MW + def _determine_cost_col(self, dc_col, ac_col): + """Determine the column used to scale costs (DC for solar runs)""" + if dc_col not in self._trans_table: + return ac_col - if self._trans_table[SupplyCurveField.CAPACITY_DC_MW].isna().all(): - return SupplyCurveField.CAPACITY_AC_MW + if self._trans_table[dc_col].isna().all(): + return ac_col - return SupplyCurveField.CAPACITY_DC_MW + return dc_col @staticmethod def _parse_sc_points(sc_points, sc_features=None): @@ -596,6 +601,7 @@ def _merge_sc_trans_tables(cls, sc_points, trans_table, sc_cols = list(sc_cols) extra_cols = [SupplyCurveField.CAPACITY_DC_MW, + SupplyCurveField.MEAN_CF_DC, SupplyCurveField.MEAN_LCOE_FRICTION, "transmission_multiplier"] for col in extra_cols: @@ -956,7 +962,7 @@ def compute_total_lcoe( cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" cost /= self._trans_table[self._costs_capacity_col] - cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC].values + cf_mean_arr = self._trans_table[self._costs_cf_col].values resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE] if 'reinforcement_cost_floored_per_mw' in self._trans_table: From 4f18fbe00d7bbf5666f9a3583bd4f87b6b09cfa1 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 17:31:43 -0600 Subject: [PATCH 32/88] Remove bad output --- reV/supply_curve/points.py | 22 ---------------------- reV/utilities/__init__.py | 1 - 2 files changed, 23 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index aca7fdbd9..a58933ba3 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2140,25 +2140,6 @@ def sc_point_annual_energy(self): 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. @@ -2314,9 +2295,6 @@ def point_summary(self, args=None): SupplyCurveField.SC_POINT_ANNUAL_ENERGY: ( self.sc_point_annual_energy ), - SupplyCurveField.SC_POINT_ANNUAL_ENERGY_AC: ( - self.sc_point_annual_energy_ac - ), } for attr, value in extra_atts.items(): if value is not None: diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 2cc4e209b..25947a6ad 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -150,7 +150,6 @@ class SupplyCurveField(FieldEnum): SC_POINT_CAPITAL_COST = "sc_point_capital_cost" SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" SC_POINT_ANNUAL_ENERGY = "sc_point_annual_energy" - SC_POINT_ANNUAL_ENERGY_AC = "sc_point_annual_energy_ac" MEAN_FRICTION = "mean_friction" MEAN_LCOE_FRICTION = "mean_lcoe_friction" TOTAL_LCOE_FRICTION = "total_lcoe_friction" From e7894926da70cd3e9225fd425f641de15f4565a9 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 7 Jun 2024 18:01:31 -0600 Subject: [PATCH 33/88] Add test for new mean cf outputs --- reV/supply_curve/sc_aggregation.py | 22 +++++++++------------- tests/test_supply_curve_sc_aggregation.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/reV/supply_curve/sc_aggregation.py b/reV/supply_curve/sc_aggregation.py index 59b297ba8..ef6274a02 100644 --- a/reV/supply_curve/sc_aggregation.py +++ b/reV/supply_curve/sc_aggregation.py @@ -784,7 +784,7 @@ def _check_data_layers( @staticmethod def _get_res_gen_lcoe_data( - gen, res_class_dset, res_class_bins, cf_dset, lcoe_dset + gen, res_class_dset, res_class_bins, lcoe_dset ): """Extract the basic resource / generation / lcoe data to be used in the aggregation process. @@ -800,8 +800,6 @@ def _get_res_gen_lcoe_data( res_class_bins : list | None List of two-entry lists dictating the resource class bins. None if no resource classes. - cf_dset : str - Dataset name from f_gen containing capacity factor mean values. lcoe_dset : str Dataset name from f_gen containing LCOE mean values. @@ -811,16 +809,14 @@ def _get_res_gen_lcoe_data( Extracted resource data from res_class_dset res_class_bins : list List of resouce class bin ranges. - cf_data : np.ndarray | None - Capacity factor data extracted from cf_dset in gen lcoe_data : np.ndarray | None LCOE data extracted from lcoe_dset in gen """ - dset_list = (res_class_dset, cf_dset, lcoe_dset) + dset_list = (res_class_dset, lcoe_dset) gen_dsets = [] if gen is None else gen.datasets - labels = ("res_class_dset", "cf_dset", "lcoe_dset") - temp = [None, None, None] + labels = ("res_class_dset", "lcoe_dset") + temp = [None, None] if isinstance(gen, Resource): source_fps = [gen.h5_file] @@ -847,12 +843,12 @@ def _get_res_gen_lcoe_data( logger.warning(w) warn(w, OutputWarning) - res_data, cf_data, lcoe_data = temp + res_data, lcoe_data = temp if res_class_dset is None or res_class_bins is None: res_class_bins = [None] - return res_data, res_class_bins, cf_data, lcoe_data + return res_data, res_class_bins, lcoe_data @staticmethod def _get_extra_dsets(gen, h5_dsets): @@ -1110,9 +1106,9 @@ def run_serial( excl_fpath, gen_fpath, **file_kwargs ) as fh: temp = cls._get_res_gen_lcoe_data( - fh.gen, res_class_dset, res_class_bins, cf_dset, lcoe_dset + fh.gen, res_class_dset, res_class_bins, lcoe_dset ) - res_data, res_class_bins, cf_data, lcoe_data = temp + res_data, res_class_bins, lcoe_data = temp h5_dsets_data = cls._get_extra_dsets(fh.gen, h5_dsets) n_finished = 0 @@ -1131,7 +1127,7 @@ def run_serial( gen_index, res_class_dset=res_data, res_class_bin=res_bin, - cf_dset=cf_data, + cf_dset=cf_dset, lcoe_dset=lcoe_data, h5_dsets=h5_dsets_data, data_layers=fh.data_layers, diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 5d75ff769..78052a433 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -151,15 +151,22 @@ def test_agg_summary(): def test_agg_summary_solar_ac(pd): """Test the aggregation summary method for solar ac outputs.""" + with Outputs(GEN, "r") as out: + cf_means_dc = out["cf_mean-means"] + with tempfile.TemporaryDirectory() as td: gen = os.path.join(td, "gen.h5") shutil.copy(GEN, gen) Outputs.add_dataset( gen, "dc_ac_ratio", np.array([1.3] * 188), np.float32 ) + Outputs.add_dataset( + gen, "cf_mean_ac-means", cf_means_dc * 1.3, np.float32 + ) with Outputs(gen, "r") as out: assert "dc_ac_ratio" in out.datasets + assert "cf_mean_ac-means" in out.datasets sca = SupplyCurveAggregation( EXCL, @@ -173,8 +180,21 @@ def test_agg_summary_solar_ac(pd): summary = sca.summarize(gen, max_workers=1) assert SupplyCurveField.CAPACITY_AC_MW in summary + assert SupplyCurveField.CAPACITY_DC_MW in summary + assert SupplyCurveField.MEAN_CF_AC in summary + assert SupplyCurveField.MEAN_CF_DC in summary + + assert not summary[SupplyCurveField.CAPACITY_AC_MW].isna().any() + assert not summary[SupplyCurveField.CAPACITY_DC_MW].isna().any() + assert not summary[SupplyCurveField.MEAN_CF_AC].isna().any() + assert not summary[SupplyCurveField.MEAN_CF_DC].isna().any() + assert np.allclose(summary[SupplyCurveField.CAPACITY_DC_MW] / 1.3, summary[SupplyCurveField.CAPACITY_AC_MW]) + assert np.allclose(summary[SupplyCurveField.CAPACITY_DC_MW] + * summary[SupplyCurveField.MEAN_CF_DC], + summary[SupplyCurveField.CAPACITY_AC_MW] + * summary[SupplyCurveField.MEAN_CF_AC]) def test_multi_file_excl(): From f5992ba2e6492fb2096209c0d89c67d5b058f644 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 12:59:01 -0600 Subject: [PATCH 34/88] SC point gid, row and col inds, now part of standard output --- reV/bespoke/bespoke.py | 17 +++------ reV/supply_curve/points.py | 60 +++++++++++++++++++----------- reV/supply_curve/sc_aggregation.py | 6 --- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 907d51767..29f445e17 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -838,28 +838,23 @@ def meta(self): [float(np.round(n, 1)) for n in self.sc_point.gid_counts] ) - with SupplyCurveExtent( - self.sc_point._excl_fpath, resolution=self.sc_point.resolution - ) as sc: - row_ind, col_ind = sc.get_sc_row_col_ind(self.sc_point.gid) - self._meta = pd.DataFrame( { - SupplyCurveField.SC_POINT_GID: self.sc_point.gid, - SupplyCurveField.SC_ROW_IND: row_ind, - SupplyCurveField.SC_COL_IND: col_ind, - SupplyCurveField.GID: self.sc_point.gid, SupplyCurveField.LATITUDE: self.sc_point.latitude, SupplyCurveField.LONGITUDE: self.sc_point.longitude, - SupplyCurveField.TIMEZONE: self.sc_point.timezone, SupplyCurveField.COUNTRY: self.sc_point.country, SupplyCurveField.STATE: self.sc_point.state, SupplyCurveField.COUNTY: self.sc_point.county, SupplyCurveField.ELEVATION: self.sc_point.elevation, - SupplyCurveField.OFFSHORE: self.sc_point.offshore, + SupplyCurveField.TIMEZONE: self.sc_point.timezone, + SupplyCurveField.SC_POINT_GID: self.sc_point.sc_point_gid, + SupplyCurveField.SC_ROW_IND: self.sc_point.sc_row_ind, + SupplyCurveField.SC_COL_IND: self.sc_point.sc_col_ind, SupplyCurveField.RES_GIDS: res_gids, SupplyCurveField.GID_COUNTS: gid_counts, SupplyCurveField.N_GIDS: self.sc_point.n_gids, + SupplyCurveField.OFFSHORE: self.sc_point.offshore, + SupplyCurveField.GID: self.sc_point.gid, SupplyCurveField.AREA_SQ_KM: self.sc_point.area, }, index=[self.sc_point.gid], diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index a58933ba3..b024c255f 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -51,9 +51,9 @@ def __init__(self, gid, exclusion_shape, resolution=64): self._gid = gid self._resolution = resolution - self._rows, self._cols = self._parse_slices( - gid, resolution, exclusion_shape - ) + self._rows = self._cols = self._sc_row_ind = self._sc_col_ind = None + self._parse_sc_row_col_ind(resolution, exclusion_shape) + self._parse_slices(resolution, exclusion_shape) @staticmethod def _ordered_unique(seq): @@ -74,32 +74,33 @@ def _ordered_unique(seq): return [x for x in seq if not (x in seen or seen.add(x))] - def _parse_slices(self, gid, resolution, exclusion_shape): - """Parse inputs for the definition of this SC point. + def _parse_sc_row_col_ind(self, resolution, exclusion_shape): + """Parse SC row and column index. Parameters ---------- - gid : int | None - gid for supply curve point to analyze. resolution : int | None SC resolution, must be input in combination with gid. exclusion_shape : tuple - Shape of the exclusions extent (rows, cols). Inputing this will - speed things up considerably. - - Returns - ------- - rows : slice - Row slice to index the high-res layer (exclusions) for the gid in - the agg layer (supply curve). - cols : slice - Col slice to index the high-res layer (exclusions) for the gid in - the agg layer (supply curve). + Shape of the exclusions extent (rows, cols). """ + n_sc_cols = int(np.ceil(exclusion_shape[1] / resolution)) - rows, cols = self.get_agg_slices(gid, exclusion_shape, resolution) + self._sc_row_ind = self._gid // n_sc_cols + self._sc_col_ind = self._gid % n_sc_cols - return rows, cols + def _parse_slices(self, resolution, exclusion_shape): + """Parse row and column resource/generation grid slices. + + Parameters + ---------- + resolution : int | None + SC resolution, must be input in combination with gid. + exclusion_shape : tuple + Shape of the exclusions extent (rows, cols). + """ + inds = self.get_agg_slices(self._gid, exclusion_shape, resolution) + self._rows, self._cols = inds @property def gid(self): @@ -117,6 +118,16 @@ def sc_point_gid(self): """ return self._gid + @property + def sc_row_ind(self): + """int: Supply curve row index""" + return self._sc_row_ind + + @property + def sc_col_ind(self): + """int: Supply curve column index""" + return self._sc_col_ind + @property def resolution(self): """Get the supply curve grid aggregation resolution""" @@ -1273,6 +1284,8 @@ def summary(self): """ meta = { SupplyCurveField.SC_POINT_GID: self.sc_point_gid, + SupplyCurveField.SC_ROW_IND: self.sc_row_ind, + SupplyCurveField.SC_COL_IND: self.sc_col_ind, SupplyCurveField.SOURCE_GIDS: self.h5_gid_set, SupplyCurveField.GID_COUNTS: self.gid_counts, SupplyCurveField.N_GIDS: self.n_gids, @@ -2264,15 +2277,19 @@ def point_summary(self, args=None): ARGS = { SupplyCurveField.LATITUDE: self.latitude, SupplyCurveField.LONGITUDE: self.longitude, - SupplyCurveField.TIMEZONE: self.timezone, SupplyCurveField.COUNTRY: self.country, SupplyCurveField.STATE: self.state, SupplyCurveField.COUNTY: self.county, SupplyCurveField.ELEVATION: self.elevation, + SupplyCurveField.TIMEZONE: self.timezone, + SupplyCurveField.SC_POINT_GID: self.sc_point_gid, + SupplyCurveField.SC_ROW_IND: self.sc_row_ind, + SupplyCurveField.SC_COL_IND: self.sc_col_ind, SupplyCurveField.RES_GIDS: self.res_gid_set, SupplyCurveField.GEN_GIDS: self.gen_gid_set, SupplyCurveField.GID_COUNTS: self.gid_counts, SupplyCurveField.N_GIDS: self.n_gids, + SupplyCurveField.OFFSHORE: self.offshore, SupplyCurveField.MEAN_CF_AC: ( self.mean_cf if self.mean_cf_ac is None else self.mean_cf_ac ), @@ -2287,7 +2304,6 @@ def point_summary(self, args=None): } extra_atts = { - SupplyCurveField.OFFSHORE: self.offshore, SupplyCurveField.SC_POINT_CAPITAL_COST: self.sc_point_capital_cost, SupplyCurveField.SC_POINT_FIXED_OPERATING_COST: ( self.sc_point_fixed_operating_cost diff --git a/reV/supply_curve/sc_aggregation.py b/reV/supply_curve/sc_aggregation.py index ef6274a02..1dd3ae774 100644 --- a/reV/supply_curve/sc_aggregation.py +++ b/reV/supply_curve/sc_aggregation.py @@ -1075,7 +1075,6 @@ def run_serial( summary = [] with SupplyCurveExtent(excl_fpath, resolution=resolution) as sc: - points = sc.points exclusion_shape = sc.exclusions.shape if gids is None: gids = sc.valid_sc_points(tm_dset) @@ -1147,11 +1146,6 @@ def run_serial( except EmptySupplyCurvePointError: logger.debug("SC point {} is empty".format(gid)) else: - pointsum[SupplyCurveField.SC_POINT_GID] = gid - pointsum[SupplyCurveField.SC_ROW_IND] = \ - points.loc[gid, 'row_ind'] - pointsum[SupplyCurveField.SC_COL_IND] = \ - points.loc[gid, 'col_ind'] pointsum['res_class'] = ri summary.append(pointsum) From c9e9fab172943461cd5cfd71b2ae6fd3b5868a13 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 15:19:23 -0600 Subject: [PATCH 35/88] Test only for columns that exist in baseline --- tests/test_supply_curve_aggregation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_supply_curve_aggregation.py b/tests/test_supply_curve_aggregation.py index 06d455d5c..60b4a841a 100644 --- a/tests/test_supply_curve_aggregation.py +++ b/tests/test_supply_curve_aggregation.py @@ -56,6 +56,7 @@ def check_agg(agg_out, baseline_h5): truth = truth.fillna('none') test = test.fillna('none') + test = test[truth.columns] assert_frame_equal(truth, test, check_dtype=False, rtol=0.0001, check_index_type=False) else: From fd04812037d45955dfe108e8fd24204da38b8b67 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:11:27 -0600 Subject: [PATCH 36/88] Docstring updates --- reV/bespoke/bespoke.py | 2 +- reV/econ/econ.py | 2 +- reV/generation/base.py | 2 +- reV/generation/generation.py | 2 +- reV/hybrids/hybrids.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 29f445e17..0f6f13dc5 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1961,7 +1961,7 @@ def _parse_points(points, sam_configs): Slice or list specifying project points, string pointing to a project points csv, or a fully instantiated PointsControl object. Can also be a single site integer value. Points csv should have - `SupplyCurveField.GID` and 'config' column, the config maps to the + `SiteDataField.GID` and 'config' column, the config maps to the sam_configs dict keys. sam_configs : dict | str | SAMConfig SAM input configuration ID(s) and file path(s). Keys are the SAM diff --git a/reV/econ/econ.py b/reV/econ/econ.py index 7e4097f75..a55985cdb 100644 --- a/reV/econ/econ.py +++ b/reV/econ/econ.py @@ -352,7 +352,7 @@ def _run_single_worker(pc, econ_fun, output_request, **kwargs): pc : reV.config.project_points.PointsControl Iterable points control object from reV config module. Must have project_points with df property with all relevant - site-specific inputs and a `SupplyCurveField.GID` column. + site-specific inputs and a `SiteDataField.GID` column. By passing site-specific inputs in this dataframe, which was split using points_control, only the data relevant to the current sites is passed. diff --git a/reV/generation/base.py b/reV/generation/base.py index 97e605921..0929e78df 100644 --- a/reV/generation/base.py +++ b/reV/generation/base.py @@ -317,7 +317,7 @@ def meta(self): Meta data df for sites in project points. Column names are meta data variables, rows are different sites. The row index does not indicate the site number if the project points are - non-sequential or do not start from 0, so a `SupplyCurveField.GID` + non-sequential or do not start from 0, so a `SiteDataField.GID` column is added. """ return self._meta diff --git a/reV/generation/generation.py b/reV/generation/generation.py index 90ce6ce21..53a13c8bb 100644 --- a/reV/generation/generation.py +++ b/reV/generation/generation.py @@ -455,7 +455,7 @@ def meta(self): Meta data df for sites in project points. Column names are meta data variables, rows are different sites. The row index does not indicate the site number if the project points are - non-sequential or do not start from 0, so a `SupplyCurveField.GID` + non-sequential or do not start from 0, so a `SiteDataField.GID` column is added. """ if self._meta is None: diff --git a/reV/hybrids/hybrids.py b/reV/hybrids/hybrids.py index 25ca3fc96..dde691e8d 100644 --- a/reV/hybrids/hybrids.py +++ b/reV/hybrids/hybrids.py @@ -713,7 +713,7 @@ def _propagate_duplicate_cols(self, duplicate_cols): self._hybrid_meta.loc[null_idx, no_suffix] = non_null_vals def _drop_cols(self, duplicate_cols): - """Drop any remaning duplicate and 'DROPPED_COLUMNS' columns.""" + """Drop any remaining duplicate and 'DROPPED_COLUMNS' columns.""" self._hybrid_meta.drop( duplicate_cols + DROPPED_COLUMNS, axis=1, From 1dda07b23f6e3134c0a9b9110ca2fce178d010ea Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:23:23 -0600 Subject: [PATCH 37/88] Power density uses resource meta GID's --- reV/supply_curve/sc_aggregation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reV/supply_curve/sc_aggregation.py b/reV/supply_curve/sc_aggregation.py index 1dd3ae774..858e17b5a 100644 --- a/reV/supply_curve/sc_aggregation.py +++ b/reV/supply_curve/sc_aggregation.py @@ -28,7 +28,7 @@ from reV.supply_curve.exclusions import FrictionMask from reV.supply_curve.extent import SupplyCurveExtent from reV.supply_curve.points import GenerationSupplyCurvePoint -from reV.utilities import SupplyCurveField, log_versions +from reV.utilities import ResourceMetaField, SupplyCurveField, log_versions from reV.utilities.exceptions import ( EmptySupplyCurvePointError, FileInputError, @@ -170,14 +170,14 @@ def _parse_power_density(self): if self._pdf.endswith(".csv"): self._power_density = pd.read_csv(self._pdf) - if (SupplyCurveField.GID in self._power_density + if (ResourceMetaField.GID in self._power_density and 'power_density' in self._power_density): self._power_density = \ - self._power_density.set_index(SupplyCurveField.GID) + self._power_density.set_index(ResourceMetaField.GID) else: msg = ('Variable power density file must include "{}" ' 'and "power_density" columns, but received: {}' - .format(SupplyCurveField.GID, + .format(ResourceMetaField.GID, self._power_density.columns.values)) logger.error(msg) raise FileInputError(msg) From 3515efc2609202c08566dd8f537aeeb19b768abd Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:24:58 -0600 Subject: [PATCH 38/88] Hybrids GID column unique from supply curve --- reV/hybrids/hybrids.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reV/hybrids/hybrids.py b/reV/hybrids/hybrids.py index dde691e8d..f29b0c62a 100644 --- a/reV/hybrids/hybrids.py +++ b/reV/hybrids/hybrids.py @@ -38,7 +38,7 @@ SupplyCurveField.SC_POINT_GID, SupplyCurveField.SC_ROW_IND, SupplyCurveField.SC_COL_IND } -DROPPED_COLUMNS = [SupplyCurveField.GID] +HYBRIDS_GID_COL = "gid" DEFAULT_FILL_VALUES = {f'solar_{SupplyCurveField.CAPACITY_AC_MW}': 0, f'wind_{SupplyCurveField.CAPACITY_AC_MW}': 0, f'solar_{SupplyCurveField.MEAN_CF_AC}': 0, @@ -702,7 +702,7 @@ def _format_meta_post_merge(self): self._propagate_duplicate_cols(duplicate_cols) self._drop_cols(duplicate_cols) self._hybrid_meta.rename(self.__col_name_map, inplace=True, axis=1) - self._hybrid_meta.index.name = SupplyCurveField.GID + self._hybrid_meta.index.name = HYBRIDS_GID_COL def _propagate_duplicate_cols(self, duplicate_cols): """Fill missing column values from outer merge.""" @@ -713,9 +713,9 @@ def _propagate_duplicate_cols(self, duplicate_cols): self._hybrid_meta.loc[null_idx, no_suffix] = non_null_vals def _drop_cols(self, duplicate_cols): - """Drop any remaining duplicate and 'DROPPED_COLUMNS' columns.""" + """Drop any remaining duplicate and 'HYBRIDS_GID_COL' columns.""" self._hybrid_meta.drop( - duplicate_cols + DROPPED_COLUMNS, + duplicate_cols + [HYBRIDS_GID_COL], axis=1, inplace=True, errors="ignore", From 8e90ce3b81341488d1ada17d75824b1840d79f9d Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:36:19 -0600 Subject: [PATCH 39/88] Hard code gid where needed --- reV/qa_qc/qa_qc.py | 2 +- reV/supply_curve/extent.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reV/qa_qc/qa_qc.py b/reV/qa_qc/qa_qc.py index 739df9c63..00369c023 100644 --- a/reV/qa_qc/qa_qc.py +++ b/reV/qa_qc/qa_qc.py @@ -105,7 +105,7 @@ def create_scatter_plots( if file.endswith(".csv"): summary_csv = os.path.join(self.out_dir, file) summary = pd.read_csv(summary_csv) - has_right_cols = (SupplyCurveField.GID in summary + has_right_cols = ("gid" in summary and SupplyCurveField.LATITUDE in summary and SupplyCurveField.LONGITUDE in summary) if has_right_cols: diff --git a/reV/supply_curve/extent.py b/reV/supply_curve/extent.py index 9154e2cb1..b1d148710 100644 --- a/reV/supply_curve/extent.py +++ b/reV/supply_curve/extent.py @@ -391,7 +391,7 @@ def points(self): } ) - self._points.index.name = SupplyCurveField.GID # sc_point_gid + self._points.index.name = "gid" # sc_point_gid return self._points From 57043afc4d474ba4aad2d3de92c3a9a79fd1bfa3 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:41:36 -0600 Subject: [PATCH 40/88] Remove unused column field --- reV/bespoke/bespoke.py | 3 +-- reV/utilities/__init__.py | 1 - tests/test_bespoke.py | 22 +++++++++++----------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 0f6f13dc5..afeccc6d4 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -854,7 +854,6 @@ def meta(self): SupplyCurveField.GID_COUNTS: gid_counts, SupplyCurveField.N_GIDS: self.sc_point.n_gids, SupplyCurveField.OFFSHORE: self.sc_point.offshore, - SupplyCurveField.GID: self.sc_point.gid, SupplyCurveField.AREA_SQ_KM: self.sc_point.area, }, index=[self.sc_point.gid], @@ -2041,7 +2040,7 @@ def _get_prior_meta(self, gid): meta = None if self._prior_meta is not None: - mask = self._prior_meta[SupplyCurveField.GID] == gid + mask = self._prior_meta[SupplyCurveField.SC_POINT_GID] == gid if any(mask): meta = self._prior_meta[mask] diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 25947a6ad..239b2c0be 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -122,7 +122,6 @@ class SupplyCurveField(FieldEnum): SOURCE_GIDS = "source_gids" SC_GID = "sc_gid" GID_COUNTS = "gid_counts" - GID = "gid" N_GIDS = "n_gids" RES_GIDS = "res_gids" GEN_GIDS = "gen_gids" diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 08c32912c..0bec84f9a 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -327,7 +327,7 @@ def test_bespoke_points(): for gid in pp.gids: assert pp[gid][0] == "default" - points = pd.DataFrame({SupplyCurveField.GID: [33, 34, 35]}) + points = pd.DataFrame({SiteDataField.GID: [33, 34, 35]}) pp = BespokeWindPlants._parse_points(points, {"default": SAM}) assert len(pp) == 3 assert SiteDataField.CONFIG in pp.df.columns @@ -665,8 +665,8 @@ def test_collect_bespoke(): with Resource(h5_file) as fout: meta = fout.meta.rename(columns=SupplyCurveField.map_from_legacy()) assert all( - meta[SupplyCurveField.GID].values - == sorted(meta[SupplyCurveField.GID].values) + meta[SupplyCurveField.SC_POINT_GID].values + == sorted(meta[SupplyCurveField.SC_POINT_GID].values) ) ti = fout.time_index assert len(ti) == 8760 @@ -680,16 +680,16 @@ def test_collect_bespoke(): columns=SupplyCurveField.map_from_legacy()) assert all( np.isin( - src_meta[SupplyCurveField.GID].values, - meta[SupplyCurveField.GID].values, + src_meta[SupplyCurveField.SC_POINT_GID].values, + meta[SupplyCurveField.SC_POINT_GID].values, ) ) for isource, gid in enumerate( - src_meta[SupplyCurveField.GID].values + src_meta[SupplyCurveField.SC_POINT_GID].values ): - iout = np.where(meta[SupplyCurveField.GID].values == gid)[ - 0 - ] + gid_mask = (meta[SupplyCurveField.SC_POINT_GID].values + == gid) + iout = np.where(gid_mask)[0] truth = source["cf_profile-2012", :, isource].flatten() test = data[:, iout].flatten() assert np.allclose(truth, test) @@ -1243,7 +1243,7 @@ def test_gid_map(): ) gid_map = pd.DataFrame( - {SupplyCurveField.GID: [3, 4, 13, 12, 11, 10, 9]} + {SiteDataField.GID: [3, 4, 13, 12, 11, 10, 9]} ) new_gid = 50 gid_map["gid_map"] = new_gid @@ -1344,7 +1344,7 @@ def test_bespoke_bias_correct(): # intentionally leaving out WTK gid 13 which only has 5 included 90m # pixels in order to check that this is dynamically patched. bias_correct = pd.DataFrame( - {SupplyCurveField.GID: [3, 4, 12, 11, 10, 9]} + {SiteDataField.GID: [3, 4, 12, 11, 10, 9]} ) bias_correct["method"] = "lin_ws" bias_correct["scalar"] = 0.5 From 28202917999c172077ca11fed88ec31e5874ebb1 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:41:47 -0600 Subject: [PATCH 41/88] Add required outputs to bespoke meta --- reV/bespoke/bespoke.py | 2 ++ tests/test_bespoke.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index afeccc6d4..5ebdea68b 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1255,6 +1255,7 @@ def run_wind_plant_ts(self): # copy dataset outputs to meta data for supply curve table summary if "cf_mean-means" in self.outputs: + self._meta.loc[:, SupplyCurveField.MEAN_CF_DC] = None self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[ "cf_mean-means" ] @@ -1353,6 +1354,7 @@ def run_plant_optimization(self): self._meta[SupplyCurveField.CAPACITY_AC_MW] = ( self.outputs["system_capacity"] / 1e3 ) + self._meta[SupplyCurveField.CAPACITY_DC_MW] = None # add required ReEDS multipliers to meta baseline_cost = self.plant_optimizer.capital_cost_per_kw( diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 0bec84f9a..8cd2103f6 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -84,6 +84,12 @@ "(0.0975 * capital_cost + fixed_operating_cost) " "/ aep + variable_operating_cost" ) +EXPECTED_META_COLUMNS = [SupplyCurveField.SC_POINT_GID, + SupplyCurveField.TURBINE_X_COORDS, + SupplyCurveField.TURBINE_Y_COORDS, + SupplyCurveField.RES_GIDS, + SupplyCurveField.CAPACITY_DC_MW, + SupplyCurveField.MEAN_CF_DC] def test_turbine_placement(gid=33): @@ -598,12 +604,11 @@ def test_bespoke(): with Resource(out_fpath_truth) as f: meta = f.meta assert len(meta) <= len(points) - assert SupplyCurveField.SC_POINT_GID in meta - assert SupplyCurveField.TURBINE_X_COORDS in meta - assert SupplyCurveField.TURBINE_Y_COORDS in meta + for col in EXPECTED_META_COLUMNS: + assert col in meta + assert "possible_x_coords" in meta assert "possible_y_coords" in meta - assert SupplyCurveField.RES_GIDS in meta dsets_1d = ( "system_capacity", From 79922c080ce53278e40fcc320d01e0fedd757d33 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:43:12 -0600 Subject: [PATCH 42/88] Remove outputs that break reVX tests --- reV/supply_curve/points.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index b024c255f..5fc2407fe 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1284,8 +1284,6 @@ def summary(self): """ meta = { SupplyCurveField.SC_POINT_GID: self.sc_point_gid, - SupplyCurveField.SC_ROW_IND: self.sc_row_ind, - SupplyCurveField.SC_COL_IND: self.sc_col_ind, SupplyCurveField.SOURCE_GIDS: self.h5_gid_set, SupplyCurveField.GID_COUNTS: self.gid_counts, SupplyCurveField.N_GIDS: self.n_gids, From b4be822a783367bcb37dbd9253ecb95ab1edbebd Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 18:59:46 -0600 Subject: [PATCH 43/88] New supply curve field enumerations --- reV/bespoke/bespoke.py | 12 +++++++----- reV/utilities/__init__.py | 14 ++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 5ebdea68b..353615ae2 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1335,15 +1335,17 @@ def run_plant_optimization(self): self._meta["bespoke_balance_of_system_cost"] = ( self.plant_optimizer.balance_of_system_cost ) - self._meta["included_area"] = self.plant_optimizer.area - self._meta["included_area_capacity_density"] = ( + self._meta[SupplyCurveField.INCLUDED_AREA] = self.plant_optimizer.area + self._meta[SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY] = ( self.plant_optimizer.capacity_density ) - self._meta["convex_hull_area"] = self.plant_optimizer.convex_hull_area - self._meta["convex_hull_capacity_density"] = ( + self._meta[SupplyCurveField.CONVEX_HULL_AREA] = ( + self.plant_optimizer.convex_hull_area + ) + self._meta[SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY] = ( self.plant_optimizer.convex_hull_capacity_density ) - self._meta["full_cell_capacity_density"] = ( + self._meta[SupplyCurveField.FULL_CELL_CAPACITY_DENSITY] = ( self.plant_optimizer.full_cell_capacity_density ) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 239b2c0be..96ae070dd 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -112,10 +112,11 @@ class ResourceMetaField(FieldEnum): class SupplyCurveField(FieldEnum): """An enumerated map to supply curve summary/meta keys. - Each output name should match the name of a key in - meth:`AggregationSupplyCurvePoint.summary` or - meth:`GenerationSupplyCurvePoint.point_summary` or - meth:`BespokeSinglePlant.meta` + This is a collection of known supply curve fields that reV outputs + across aggregation, supply curve, and bespoke outputs. + + Not all of these columns are guaranteed in every supply-curve like + output (e.g. "convex_hull_area" is a bespoke-only output). """ SC_POINT_GID = "sc_point_gid" @@ -160,6 +161,11 @@ class SupplyCurveField(FieldEnum): TURBINE_Y_COORDS = "turbine_y_coords" EOS_MULT = "eos_mult" REG_MULT = "reg_mult" + INCLUDED_AREA = "included_area" + INCLUDED_AREA_CAPACITY_DENSITY = "included_area_capacity_density" + CONVEX_HULL_AREA = "convex_hull_area" + CONVEX_HULL_CAPACITY_DENSITY = "convex_hull_capacity_density" + FULL_CELL_CAPACITY_DENSITY = "full_cell_capacity_density" @classmethod def map_from_legacy(cls): From 84eba8191364360e13fa82ea3ed2132f1e2043d6 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Mon, 10 Jun 2024 19:51:59 -0600 Subject: [PATCH 44/88] Fix docs --- reV/SAM/generation.py | 6 +++--- reV/nrwal/nrwal.py | 2 +- reV/rep_profiles/rep_profiles.py | 2 +- reV/supply_curve/exclusions.py | 2 +- reV/supply_curve/supply_curve.py | 3 ++- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/reV/SAM/generation.py b/reV/SAM/generation.py index d8eaf3f59..2ef711fd9 100644 --- a/reV/SAM/generation.py +++ b/reV/SAM/generation.py @@ -887,11 +887,11 @@ def __init__( simulate reduced performance over time. - ``analysis_period`` : Integer representing the number of years to include in the lifetime of the model generator. Required if - ``system_use_lifetime_output``=1. + ``system_use_lifetime_output`` is set to 1. - ``dc_degradation`` : List of percentage values representing the annual DC degradation of capacity factors. Maybe a single value that will be compound each year or a vector of yearly rates. - Required if ``system_use_lifetime_output``=1. + Required if ``system_use_lifetime_output`` is set to 1. You may also include the following ``reV``-specific keys: @@ -1546,7 +1546,7 @@ class Geothermal(AbstractSamGenerationFromWeatherFile): - The design temperature is lower than the resource temperature by a factor of ``MAX_RT_TO_EGS_RATIO`` - If either of these conditions are true, the ``design_temp`` is a + If either of these conditions are true, the ``design_temp`` is adjusted to match the resource temperature input in order to avoid SAM errors. - ``set_EGS_PDT_to_RT`` : Boolean flag to set EGS design diff --git a/reV/nrwal/nrwal.py b/reV/nrwal/nrwal.py index 4ee73f356..4b192ace7 100644 --- a/reV/nrwal/nrwal.py +++ b/reV/nrwal/nrwal.py @@ -37,7 +37,7 @@ class RevNrwal: def __init__(self, gen_fpath, site_data, sam_files, nrwal_configs, output_request, save_raw=True, - meta_gid_col=ResourceMetaField.GID, + meta_gid_col=str(ResourceMetaField.GID), # str() to fix docs site_meta_cols=None): """Framework to handle reV-NRWAL analysis. diff --git a/reV/rep_profiles/rep_profiles.py b/reV/rep_profiles/rep_profiles.py index 9b12ee726..27704d6d6 100644 --- a/reV/rep_profiles/rep_profiles.py +++ b/reV/rep_profiles/rep_profiles.py @@ -953,7 +953,7 @@ class RepProfiles(RepProfilesBase): def __init__(self, gen_fpath, rev_summary, reg_cols, cf_dset='cf_profile', rep_method='meanoid', err_method='rmse', - weight=SupplyCurveField.GID_COUNTS, + weight=str(SupplyCurveField.GID_COUNTS), # str() to fix docs n_profiles=1, aggregate_profiles=False): """ReV rep profiles class. diff --git a/reV/supply_curve/exclusions.py b/reV/supply_curve/exclusions.py index 9055a0ad6..e47d7af7f 100644 --- a/reV/supply_curve/exclusions.py +++ b/reV/supply_curve/exclusions.py @@ -111,7 +111,7 @@ def __init__(self, layer, specifications to create a boolean mask that defines the extent to which the original mask should be applied. For example, suppose you specify the input the following - way: + way:: input_dict = { "viewsheds": { diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index 7a84ff0c4..a64ad88a8 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -28,7 +28,8 @@ class SupplyCurve: """SupplyCurve""" def __init__(self, sc_points, trans_table, sc_features=None, - sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW): + # str() to fix docs + sc_capacity_col=str(SupplyCurveField.CAPACITY_AC_MW)): """ReV LCOT calculation and SupplyCurve sorting class. ``reV`` supply curve computes the transmission costs associated From c68b2828bc6d88edd7913a2a902d800d8bb7a137 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 00:28:32 -0600 Subject: [PATCH 45/88] Rename field --- reV/supply_curve/points.py | 3 ++- reV/utilities/__init__.py | 2 +- tests/test_econ_of_scale.py | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 5fc2407fe..44990a0a4 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2299,6 +2299,7 @@ def point_summary(self, args=None): self.capacity if self.capacity_ac is None else self.capacity_ac ), SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, + SupplyCurveField.EOS_MULT: 1, # added later } extra_atts = { @@ -2362,7 +2363,7 @@ def economies_of_scale(cap_cost_scale, summary): eos = EconomiesOfScale(cap_cost_scale, summary) summary[SupplyCurveField.RAW_LCOE] = eos.raw_lcoe summary[SupplyCurveField.MEAN_LCOE] = eos.scaled_lcoe - summary[SupplyCurveField.CAPITAL_COST_SCALAR] = eos.capital_cost_scalar + summary[SupplyCurveField.EOS_MULT] = eos.capital_cost_scalar summary[SupplyCurveField.SCALED_CAPITAL_COST] = eos.scaled_capital_cost if SupplyCurveField.SC_POINT_CAPITAL_COST in summary: scaled_costs = ( diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 96ae070dd..845accff0 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -154,7 +154,6 @@ class SupplyCurveField(FieldEnum): MEAN_LCOE_FRICTION = "mean_lcoe_friction" TOTAL_LCOE_FRICTION = "total_lcoe_friction" RAW_LCOE = "raw_lcoe" - CAPITAL_COST_SCALAR = "capital_cost_scalar" SCALED_CAPITAL_COST = "scaled_capital_cost" SCALED_SC_POINT_CAPITAL_COST = "scaled_sc_point_capital_cost" TURBINE_X_COORDS = "turbine_x_coords" @@ -195,6 +194,7 @@ class _LegacySCAliases(Enum): """ CAPACITY_AC_MW = "capacity" + EOS_MULT = "capital_cost_multiplier" class ModuleName(str, Enum): diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index 0b0b8c27b..ab49dd952 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -188,7 +188,7 @@ def test_econ_of_scale_baseline(): sc_df = pd.read_csv(out_fp_sc + ".csv") assert np.allclose(base_df[SupplyCurveField.MEAN_LCOE], sc_df[SupplyCurveField.MEAN_LCOE]) - assert (sc_df[SupplyCurveField.CAPITAL_COST_SCALAR] == 1).all() + assert (sc_df[SupplyCurveField.EOS_MULT] == 1).all() assert np.allclose(sc_df['mean_capital_cost'], sc_df[SupplyCurveField.SCALED_CAPITAL_COST]) @@ -267,8 +267,7 @@ def test_sc_agg_econ_scale(): + data["fixed_operating_cost"] ) / aep + data["variable_operating_cost"] - assert np.allclose(scalars, - sc_df[SupplyCurveField.CAPITAL_COST_SCALAR]) + assert np.allclose(scalars, sc_df[SupplyCurveField.EOS_MULT]) assert np.allclose(scalars * sc_df['mean_capital_cost'], sc_df[SupplyCurveField.SCALED_CAPITAL_COST]) From 0fdfb4c413abbdea25a3967b15f3ca41a2d4bce2 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 01:14:46 -0600 Subject: [PATCH 46/88] Add annual energy as output --- reV/bespoke/bespoke.py | 9 ++++++++- reV/supply_curve/points.py | 6 +++--- tests/test_bespoke.py | 8 +++++++- tests/test_supply_curve_sc_aggregation.py | 8 ++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 353615ae2..28259f128 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1253,9 +1253,12 @@ def run_wind_plant_ts(self): self._outputs.update(means) + self._meta[SupplyCurveField.MEAN_CF_DC] = None + self._meta[SupplyCurveField.MEAN_CF_AC] = None + self._meta[SupplyCurveField.MEAN_LCOE] = None + self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY] = None # copy dataset outputs to meta data for supply curve table summary if "cf_mean-means" in self.outputs: - self._meta.loc[:, SupplyCurveField.MEAN_CF_DC] = None self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[ "cf_mean-means" ] @@ -1264,6 +1267,10 @@ def run_wind_plant_ts(self): "lcoe_fcr-means" ] self.recalc_lcoe() + if "annual_energy-means" in self.outputs: + self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY] = ( + self.outputs["annual_energy-means"] / 1000 + ) logger.debug("Timeseries analysis complete!") diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 44990a0a4..1d1f5e784 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2300,6 +2300,9 @@ def point_summary(self, args=None): ), SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, SupplyCurveField.EOS_MULT: 1, # added later + SupplyCurveField.SC_POINT_ANNUAL_ENERGY: ( + self.sc_point_annual_energy + ), } extra_atts = { @@ -2307,9 +2310,6 @@ def point_summary(self, args=None): SupplyCurveField.SC_POINT_FIXED_OPERATING_COST: ( self.sc_point_fixed_operating_cost ), - SupplyCurveField.SC_POINT_ANNUAL_ENERGY: ( - self.sc_point_annual_energy - ), } for attr, value in extra_atts.items(): if value is not None: diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 8cd2103f6..9d7b7df96 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -89,7 +89,8 @@ SupplyCurveField.TURBINE_Y_COORDS, SupplyCurveField.RES_GIDS, SupplyCurveField.CAPACITY_DC_MW, - SupplyCurveField.MEAN_CF_DC] + SupplyCurveField.MEAN_CF_DC, + SupplyCurveField.SC_POINT_ANNUAL_ENERGY] def test_turbine_placement(gid=33): @@ -625,6 +626,11 @@ def test_bespoke(): assert len(f[dset]) == len(meta) assert f[dset].any() # not all zeros + assert np.allclose( + f["annual_energy-means"] / 1000, + meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY] + ) + dsets_2d = ( "cf_profile-2012", "cf_profile-2013", diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 78052a433..7e7f514b7 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -195,6 +195,14 @@ def test_agg_summary_solar_ac(pd): * summary[SupplyCurveField.MEAN_CF_DC], summary[SupplyCurveField.CAPACITY_AC_MW] * summary[SupplyCurveField.MEAN_CF_AC]) + assert np.allclose(summary[SupplyCurveField.CAPACITY_DC_MW] + * summary[SupplyCurveField.MEAN_CF_DC] + * 8760, + summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY]) + assert np.allclose(summary[SupplyCurveField.CAPACITY_AC_MW] + * summary[SupplyCurveField.MEAN_CF_AC] + * 8760, + summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY]) def test_multi_file_excl(): From 13ae7b5f0f49ecc4db23cfd7574700c71d20b9e4 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 01:18:05 -0600 Subject: [PATCH 47/88] Rename field --- reV/bespoke/bespoke.py | 4 ++-- reV/supply_curve/points.py | 2 +- reV/utilities/__init__.py | 2 +- tests/test_bespoke.py | 4 ++-- tests/test_supply_curve_sc_aggregation.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 28259f128..7e16a93ae 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1256,7 +1256,7 @@ def run_wind_plant_ts(self): self._meta[SupplyCurveField.MEAN_CF_DC] = None self._meta[SupplyCurveField.MEAN_CF_AC] = None self._meta[SupplyCurveField.MEAN_LCOE] = None - self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY] = None + self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = None # copy dataset outputs to meta data for supply curve table summary if "cf_mean-means" in self.outputs: self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[ @@ -1268,7 +1268,7 @@ def run_wind_plant_ts(self): ] self.recalc_lcoe() if "annual_energy-means" in self.outputs: - self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY] = ( + self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = ( self.outputs["annual_energy-means"] / 1000 ) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 1d1f5e784..92db9355d 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2300,7 +2300,7 @@ def point_summary(self, args=None): ), SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, SupplyCurveField.EOS_MULT: 1, # added later - SupplyCurveField.SC_POINT_ANNUAL_ENERGY: ( + SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW: ( self.sc_point_annual_energy ), } diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 845accff0..602b5d24b 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -149,7 +149,7 @@ class SupplyCurveField(FieldEnum): FIXED_CHARGE_RATE = "fixed_charge_rate" SC_POINT_CAPITAL_COST = "sc_point_capital_cost" SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" - SC_POINT_ANNUAL_ENERGY = "sc_point_annual_energy" + SC_POINT_ANNUAL_ENERGY_MW = "sc_point_annual_energy" MEAN_FRICTION = "mean_friction" MEAN_LCOE_FRICTION = "mean_lcoe_friction" TOTAL_LCOE_FRICTION = "total_lcoe_friction" diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 9d7b7df96..7a294d0d9 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -90,7 +90,7 @@ SupplyCurveField.RES_GIDS, SupplyCurveField.CAPACITY_DC_MW, SupplyCurveField.MEAN_CF_DC, - SupplyCurveField.SC_POINT_ANNUAL_ENERGY] + SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] def test_turbine_placement(gid=33): @@ -628,7 +628,7 @@ def test_bespoke(): assert np.allclose( f["annual_energy-means"] / 1000, - meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY] + meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] ) dsets_2d = ( diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 7e7f514b7..22fa8b697 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -198,11 +198,11 @@ def test_agg_summary_solar_ac(pd): assert np.allclose(summary[SupplyCurveField.CAPACITY_DC_MW] * summary[SupplyCurveField.MEAN_CF_DC] * 8760, - summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY]) + summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW]) assert np.allclose(summary[SupplyCurveField.CAPACITY_AC_MW] * summary[SupplyCurveField.MEAN_CF_AC] * 8760, - summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY]) + summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW]) def test_multi_file_excl(): @@ -566,7 +566,7 @@ def test_recalc_lcoe(cap_cost_scale): lcoe = lcoe_fcr(summary['mean_fixed_charge_rate'], summary[cc_dset], summary[SupplyCurveField.SC_POINT_FIXED_OPERATING_COST], - summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY], + summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW], summary['mean_variable_operating_cost']) assert np.allclose(lcoe, summary[SupplyCurveField.MEAN_LCOE]) From d55cc451db598c00696457865ec38567155afc66 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 01:40:21 -0600 Subject: [PATCH 48/88] `sc_gid` now output in bespoke meta --- reV/bespoke/bespoke.py | 1 + tests/test_bespoke.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 7e16a93ae..7c190d243 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -2213,6 +2213,7 @@ def meta(self): meta = pd.concat(meta, axis=0) else: meta = meta[0] + meta.index.name = SupplyCurveField.SC_GID return meta @property diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 7a294d0d9..20002a6f2 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -84,7 +84,8 @@ "(0.0975 * capital_cost + fixed_operating_cost) " "/ aep + variable_operating_cost" ) -EXPECTED_META_COLUMNS = [SupplyCurveField.SC_POINT_GID, +EXPECTED_META_COLUMNS = [SupplyCurveField.SC_GID, + SupplyCurveField.SC_POINT_GID, SupplyCurveField.TURBINE_X_COORDS, SupplyCurveField.TURBINE_Y_COORDS, SupplyCurveField.RES_GIDS, @@ -603,7 +604,7 @@ def test_bespoke(): assert out_fpath_truth == test_fpath assert os.path.exists(out_fpath_truth) with Resource(out_fpath_truth) as f: - meta = f.meta + meta = f.meta.reset_index() assert len(meta) <= len(points) for col in EXPECTED_META_COLUMNS: assert col in meta From c74a34ecb7c62dcb73d6a4b9c84f1fcdd4c8cbb8 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 11:46:56 -0600 Subject: [PATCH 49/88] Rename mult input for consistency with bespoke --- reV/SAM/SAM.py | 2 +- reV/generation/base.py | 2 +- reV/generation/generation.py | 2 +- tests/test_econ_lcoe.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reV/SAM/SAM.py b/reV/SAM/SAM.py index f5a0b20de..fdd7040e2 100644 --- a/reV/SAM/SAM.py +++ b/reV/SAM/SAM.py @@ -924,7 +924,7 @@ def _add_cost_defaults(sam_inputs): """Add default values for required cost outputs if they are missing. """ sam_inputs.setdefault("fixed_charge_rate", None) - reg_mult = sam_inputs.setdefault("multiplier_regional", 1) + reg_mult = sam_inputs.setdefault("capital_cost_multiplier", 1) capital_cost = sam_inputs.setdefault("capital_cost", None) fixed_operating_cost = sam_inputs.setdefault("fixed_operating_cost", None) variable_operating_cost = sam_inputs.setdefault( diff --git a/reV/generation/base.py b/reV/generation/base.py index 0929e78df..2f5264b11 100644 --- a/reV/generation/base.py +++ b/reV/generation/base.py @@ -45,7 +45,7 @@ with open(os.path.join(ATTR_DIR, 'lcoe_fcr_inputs.json')) as f: LCOE_IN_ATTRS = json.load(f) -LCOE_REQUIRED_OUTPUTS = ("system_capacity", "multiplier_regional", +LCOE_REQUIRED_OUTPUTS = ("system_capacity", "capital_cost_multiplier", "capital_cost", "fixed_operating_cost", "variable_operating_cost", "base_capital_cost", "base_fixed_operating_cost", diff --git a/reV/generation/generation.py b/reV/generation/generation.py index 53a13c8bb..e5ae69094 100644 --- a/reV/generation/generation.py +++ b/reV/generation/generation.py @@ -152,7 +152,7 @@ def __init__( 'capital_cost': array([39767200, 39767200, 39767200, 39767200], 'fixed_operating_cost': array([260000, 260000, 260000, 260000], 'variable_operating_cost': array([0, 0, 0, 0], - 'multiplier_regional': array([1, 1, 1, 1], + 'capital_cost_multiplier': array([1, 1, 1, 1], 'cf_mean': array([0.17859147, 0.17869979, 0.1834818 , 0.18646291], 'lcoe_fcr': array([130.32126, 130.24226, 126.84782, 124.81981]} diff --git a/tests/test_econ_lcoe.py b/tests/test_econ_lcoe.py index 6a89839f9..84afdfc87 100644 --- a/tests/test_econ_lcoe.py +++ b/tests/test_econ_lcoe.py @@ -241,7 +241,7 @@ def test_econ_from_config(runner, clear_loggers): clear_loggers() -def test_multiplier_regional(clear_loggers): +def test_capital_cost_multiplier_regional(clear_loggers): """Gen PV CF profiles with write to disk and compare against rev1.""" with tempfile.TemporaryDirectory() as dirout: cf_file = os.path.join(TESTDATADIR, @@ -251,7 +251,7 @@ def test_multiplier_regional(clear_loggers): fpath = os.path.join(dirout, 'lcoe_out_econ_2012.h5') mults = np.arange(0, 100) / 100 points = pd.DataFrame({"gid": np.arange(0, 100), - "multiplier_regional": mults}) + "capital_cost_multiplier": mults}) econ = Econ(points, sam_files, cf_file, output_request='lcoe_fcr', sites_per_worker=25) From e73392fe033e7c28ce90d3c3eb4f0838bed14223 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 12:11:08 -0600 Subject: [PATCH 50/88] Fix tests --- reV/bespoke/bespoke.py | 1 - tests/test_bespoke.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 7c190d243..7e16a93ae 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -2213,7 +2213,6 @@ def meta(self): meta = pd.concat(meta, axis=0) else: meta = meta[0] - meta.index.name = SupplyCurveField.SC_GID return meta @property diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 20002a6f2..82dcd7b1e 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -84,8 +84,7 @@ "(0.0975 * capital_cost + fixed_operating_cost) " "/ aep + variable_operating_cost" ) -EXPECTED_META_COLUMNS = [SupplyCurveField.SC_GID, - SupplyCurveField.SC_POINT_GID, +EXPECTED_META_COLUMNS = [SupplyCurveField.SC_POINT_GID, SupplyCurveField.TURBINE_X_COORDS, SupplyCurveField.TURBINE_Y_COORDS, SupplyCurveField.RES_GIDS, @@ -786,6 +785,8 @@ def test_bespoke_supply_curve(): sc = SupplyCurve(bespoke_sc_fp, trans_tables) sc_full = sc.full_sort(fcr=0.1, avail_cap_frac=0.1) + assert SupplyCurveField.SC_GID in sc_full + assert all( gid in sc_full[SupplyCurveField.SC_GID] for gid in normal_sc_points[SupplyCurveField.SC_GID] From 0225ae79af83456fdafe0ca53d806dfcb3c9cfc5 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 12:34:26 -0600 Subject: [PATCH 51/88] Add multipliers as outputs --- reV/supply_curve/points.py | 10 ++++++++++ tests/test_supply_curve_sc_aggregation.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 92db9355d..efd12206c 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2185,6 +2185,15 @@ def h5_dsets_data(self): return _h5_dsets_data + @property + def regional_multiplier(self): + """float: Mean regional capital cost multiplier, defaults to 1.""" + if "capital_cost_multiplier" not in self.gen.datasets: + return 1 + + multipliers = self.gen["capital_cost_multiplier"] + return self.exclusion_weighted_mean(multipliers) + @property def mean_h5_dsets_data(self): """Get the mean supplemental h5 datasets data (optional) @@ -2300,6 +2309,7 @@ def point_summary(self, args=None): ), SupplyCurveField.CAPACITY_DC_MW: self.capacity_dc, SupplyCurveField.EOS_MULT: 1, # added later + SupplyCurveField.REG_MULT: self.regional_multiplier, SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW: ( self.sc_point_annual_energy ), diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 22fa8b697..ed5a2b02c 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -135,6 +135,8 @@ def test_agg_summary(): assert SupplyCurveField.CAPACITY_DC_MW in summary assert SupplyCurveField.MEAN_CF_AC in summary assert SupplyCurveField.MEAN_CF_DC in summary + assert SupplyCurveField.REG_MULT in summary + assert SupplyCurveField.EOS_MULT in summary # dc outputs are `None` because old gen file does not have correct # output dsets @@ -143,6 +145,9 @@ def test_agg_summary(): assert not summary[SupplyCurveField.MEAN_CF_AC].isna().any() assert not summary[SupplyCurveField.MEAN_CF_DC].isna().all() + assert np.allclose(summary[SupplyCurveField.REG_MULT], 1) + assert np.allclose(summary[SupplyCurveField.EOS_MULT], 1) + summary = summary[list(s_baseline.columns)] assert_frame_equal(summary, s_baseline, check_dtype=False, rtol=0.0001) @@ -559,6 +564,10 @@ def test_recalc_lcoe(cap_cost_scale): assert not np.allclose(summary_base[SupplyCurveField.MEAN_LCOE], summary[SupplyCurveField.MEAN_LCOE]) + assert np.allclose(summary[SupplyCurveField.EOS_MULT], + summary[SupplyCurveField.SCALED_SC_POINT_CAPITAL_COST] + / summary[SupplyCurveField.SC_POINT_CAPITAL_COST]) + if cap_cost_scale == '1': cc_dset = SupplyCurveField.SC_POINT_CAPITAL_COST else: From 913000e2161cb4124f12e06f9844baa034ab869d Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 13:58:59 -0600 Subject: [PATCH 52/88] Remove `CAPITAL_COST` field --- reV/econ/economies_of_scale.py | 5 +---- reV/utilities/__init__.py | 1 - tests/test_econ_of_scale.py | 16 ++++++++-------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/reV/econ/economies_of_scale.py b/reV/econ/economies_of_scale.py index f47b6e259..376d0fd15 100644 --- a/reV/econ/economies_of_scale.py +++ b/reV/econ/economies_of_scale.py @@ -199,10 +199,7 @@ def raw_capital_cost(self): out : float | np.ndarray Unscaled (raw) capital_cost found in the data input arg. """ - key_list = [ - SupplyCurveField.CAPITAL_COST, - "mean_capital_cost", - ] + key_list = ["capital_cost", "mean_capital_cost"] return self._get_prioritized_keys(self._data, key_list) @property diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 602b5d24b..25dbb2885 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -143,7 +143,6 @@ class SupplyCurveField(FieldEnum): OFFSHORE = "offshore" SC_ROW_IND = "sc_row_ind" SC_COL_IND = "sc_col_ind" - CAPITAL_COST = "capital_cost" FIXED_OPERATING_COST = "fixed_operating_cost" VARIABLE_OPERATING_COST = "variable_operating_cost" FIXED_CHARGE_RATE = "fixed_charge_rate" diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index ab49dd952..e25abe43f 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -81,47 +81,47 @@ def test_lcoe_calc_simple(): # from pvwattsv7 defaults data = { "aep": 35188456.00, - SupplyCurveField.CAPITAL_COST: 53455000.00, + "capital_cost": 53455000.00, "foc": 360000.00, "voc": 0, "fcr": 0.096, } - true_lcoe = (data["fcr"] * data[SupplyCurveField.CAPITAL_COST] + true_lcoe = (data["fcr"] * data["capital_cost"] + data["foc"]) / (data["aep"] / 1000) data[SupplyCurveField.MEAN_LCOE] = true_lcoe eos = EconomiesOfScale(eqn, data) assert eos.raw_capital_cost == eos.scaled_capital_cost - assert eos.raw_capital_cost == data[SupplyCurveField.CAPITAL_COST] + assert eos.raw_capital_cost == data["capital_cost"] assert np.allclose(eos.raw_lcoe, true_lcoe, rtol=0.001) assert np.allclose(eos.scaled_lcoe, true_lcoe, rtol=0.001) eqn = 1 eos = EconomiesOfScale(eqn, data) assert eos.raw_capital_cost == eos.scaled_capital_cost - assert eos.raw_capital_cost == data[SupplyCurveField.CAPITAL_COST] + assert eos.raw_capital_cost == data["capital_cost"] assert np.allclose(eos.raw_lcoe, true_lcoe, rtol=0.001) assert np.allclose(eos.scaled_lcoe, true_lcoe, rtol=0.001) eqn = 2 - true_scaled = ((data['fcr'] * eqn * data[SupplyCurveField.CAPITAL_COST] + true_scaled = ((data['fcr'] * eqn * data["capital_cost"] + data['foc']) / (data['aep'] / 1000)) eos = EconomiesOfScale(eqn, data) assert eqn * eos.raw_capital_cost == eos.scaled_capital_cost - assert eos.raw_capital_cost == data[SupplyCurveField.CAPITAL_COST] + assert eos.raw_capital_cost == data["capital_cost"] assert np.allclose(eos.raw_lcoe, true_lcoe, rtol=0.001) assert np.allclose(eos.scaled_lcoe, true_scaled, rtol=0.001) data['system_capacity'] = 2 eqn = '1 / system_capacity' - true_scaled = ((data['fcr'] * 0.5 * data[SupplyCurveField.CAPITAL_COST] + true_scaled = ((data['fcr'] * 0.5 * data["capital_cost"] + data['foc']) / (data['aep'] / 1000)) eos = EconomiesOfScale(eqn, data) assert 0.5 * eos.raw_capital_cost == eos.scaled_capital_cost - assert eos.raw_capital_cost == data[SupplyCurveField.CAPITAL_COST] + assert eos.raw_capital_cost == data["capital_cost"] assert np.allclose(eos.raw_lcoe, true_lcoe, rtol=0.001) assert np.allclose(eos.scaled_lcoe, true_scaled, rtol=0.001) From 9f338e2d2831e7447d234e657fc5a2d60bceb489 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 14:00:45 -0600 Subject: [PATCH 53/88] Remove other unused cost fields --- reV/econ/economies_of_scale.py | 24 ++++++------------------ reV/utilities/__init__.py | 3 --- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/reV/econ/economies_of_scale.py b/reV/econ/economies_of_scale.py index 376d0fd15..523b67024 100644 --- a/reV/econ/economies_of_scale.py +++ b/reV/econ/economies_of_scale.py @@ -238,12 +238,8 @@ def fcr(self): out : float | np.ndarray Fixed charge rate from input data arg """ - key_list = [ - SupplyCurveField.FIXED_CHARGE_RATE, - "mean_fixed_charge_rate", - "fcr", - "mean_fcr", - ] + key_list = ["fixed_charge_rate", "mean_fixed_charge_rate", "fcr", + "mean_fcr"] return self._get_prioritized_keys(self._data, key_list) @property @@ -255,12 +251,8 @@ def foc(self): out : float | np.ndarray Fixed operating cost from input data arg """ - key_list = [ - SupplyCurveField.FIXED_OPERATING_COST, - "mean_fixed_operating_cost", - "foc", - "mean_foc", - ] + key_list = ["fixed_operating_cost", "mean_fixed_operating_cost", + "foc", "mean_foc"] return self._get_prioritized_keys(self._data, key_list) @property @@ -272,12 +264,8 @@ def voc(self): out : float | np.ndarray Variable operating cost from input data arg """ - key_list = [ - SupplyCurveField.VARIABLE_OPERATING_COST, - "mean_variable_operating_cost", - "voc", - "mean_voc", - ] + key_list = ["variable_operating_cost", "mean_variable_operating_cost", + "voc", "mean_voc"] return self._get_prioritized_keys(self._data, key_list) @property diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 25dbb2885..97d9eeab5 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -143,9 +143,6 @@ class SupplyCurveField(FieldEnum): OFFSHORE = "offshore" SC_ROW_IND = "sc_row_ind" SC_COL_IND = "sc_col_ind" - FIXED_OPERATING_COST = "fixed_operating_cost" - VARIABLE_OPERATING_COST = "variable_operating_cost" - FIXED_CHARGE_RATE = "fixed_charge_rate" SC_POINT_CAPITAL_COST = "sc_point_capital_cost" SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" SC_POINT_ANNUAL_ENERGY_MW = "sc_point_annual_energy" From 1f12274f7a9a5feb6af9460d399e6972c2a427eb Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 16:23:11 -0600 Subject: [PATCH 54/88] Change output name --- reV/generation/output_attributes/lcoe_fcr_inputs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/generation/output_attributes/lcoe_fcr_inputs.json b/reV/generation/output_attributes/lcoe_fcr_inputs.json index 7d71c0d88..af402ce6d 100644 --- a/reV/generation/output_attributes/lcoe_fcr_inputs.json +++ b/reV/generation/output_attributes/lcoe_fcr_inputs.json @@ -48,7 +48,7 @@ "type": "scalar", "units": "usd/kWh" }, - "multiplier_regional": { + "capital_cost_multiplier": { "chunks": null, "dtype": "float32", "scale_factor": 1, From 371a31b9013a6d89033281516f50992355c7353a Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 17:08:06 -0600 Subject: [PATCH 55/88] [WIP] Adding new cost outputs --- reV/econ/economies_of_scale.py | 57 +++++++++++++++----- reV/supply_curve/points.py | 97 ++++++++++++++++++++++++++++------ reV/utilities/__init__.py | 16 ++++-- tests/test_econ_of_scale.py | 3 +- 4 files changed, 139 insertions(+), 34 deletions(-) diff --git a/reV/econ/economies_of_scale.py b/reV/econ/economies_of_scale.py index 523b67024..3212e8ee5 100644 --- a/reV/econ/economies_of_scale.py +++ b/reV/econ/economies_of_scale.py @@ -190,6 +190,33 @@ def capital_cost_scalar(self): """ return self._evaluate() + def _cost_from_cap(self, col_name): + """Get full cost value from cost per mw in data. + + Parameters + ---------- + col_name : str + Name of column containing the cost per mw value. + + Returns + ------- + float | None + Cost value if it was found in data, ``None`` otherwise. + """ + cap = self._data.get(SupplyCurveField.CAPACITY_AC_MW) + if cap is None: + return None + + cost_per_mw = self._data.get(col_name) + if cost_per_mw is None: + return None + + cost = cap * cost_per_mw + if cost > 0: + return cost + + return None + @property def raw_capital_cost(self): """Unscaled (raw) capital cost found in the data input arg. @@ -199,6 +226,12 @@ def raw_capital_cost(self): out : float | np.ndarray Unscaled (raw) capital_cost found in the data input arg. """ + raw_capital_cost_from_cap = self._cost_from_cap( + SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW + ) + if raw_capital_cost_from_cap is not None: + return raw_capital_cost_from_cap + key_list = ["capital_cost", "mean_capital_cost"] return self._get_prioritized_keys(self._data, key_list) @@ -217,18 +250,6 @@ def scaled_capital_cost(self): cc *= self.capital_cost_scalar return cc - @property - def system_capacity(self): - """Get the system capacity in kW (SAM input, not the reV supply - curve capacity). - - Returns - ------- - out : float | np.ndarray - """ - key_list = ["system_capacity", "mean_system_capacity"] - return self._get_prioritized_keys(self._data, key_list) - @property def fcr(self): """Fixed charge rate from input data arg @@ -251,6 +272,12 @@ def foc(self): out : float | np.ndarray Fixed operating cost from input data arg """ + foc_from_cap = self._cost_from_cap( + SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW + ) + if foc_from_cap is not None: + return foc_from_cap + key_list = ["fixed_operating_cost", "mean_fixed_operating_cost", "foc", "mean_foc"] return self._get_prioritized_keys(self._data, key_list) @@ -264,6 +291,12 @@ def voc(self): out : float | np.ndarray Variable operating cost from input data arg """ + voc_from_cap = self._cost_from_cap( + SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW + ) + if voc_from_cap is not None: + return voc_from_cap + key_list = ["variable_operating_cost", "mean_variable_operating_cost", "voc", "mean_voc"] return self._get_prioritized_keys(self._data, key_list) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index efd12206c..db08673a4 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1504,7 +1504,7 @@ def __init__( recalc_lcoe : bool Flag to re-calculate the LCOE from the multi-year mean capacity factor and annual energy production data. This requires several - datasets to be aggregated in the h5_dsets input: system_capacity, + datasets to be aggregated in the gen input: system_capacity, fixed_charge_rate, capital_cost, fixed_operating_cost, and variable_operating_cost. apply_exclusions : bool @@ -1525,6 +1525,8 @@ def __init__( self._power_density = self._power_density_ac = power_density self._friction_layer = friction_layer self._recalc_lcoe = recalc_lcoe + self._ssc = None + self._slk = {} super().__init__( gid, @@ -1816,18 +1818,12 @@ def mean_lcoe(self): # year CF, but the output should be identical to the original LCOE and # so is not consequential). if self._recalc_lcoe: - required = ( - "fixed_charge_rate", - "capital_cost", - "fixed_operating_cost", - "variable_operating_cost", - "system_capacity", - ) - if self.mean_h5_dsets_data is not None and all( - k in self.mean_h5_dsets_data for k in required - ): + required = ("fixed_charge_rate", "capital_cost", + "fixed_operating_cost", "variable_operating_cost", + "system_capacity") + if all(self._sam_lcoe_kwargs.get(k) is not None for k in required): aep = ( - self.mean_h5_dsets_data["system_capacity"] + self._sam_lcoe_kwargs["system_capacity"] * self.mean_cf * 8760 ) @@ -1835,11 +1831,11 @@ def mean_lcoe(self): # `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"], - self.mean_h5_dsets_data["fixed_operating_cost"], + self._sam_lcoe_kwargs["fixed_charge_rate"], + self._sam_lcoe_kwargs["capital_cost"], + self._sam_lcoe_kwargs["fixed_operating_cost"], aep, - self.mean_h5_dsets_data["variable_operating_cost"], + self._sam_lcoe_kwargs["variable_operating_cost"], ) # alternative if lcoe was not able to be re-calculated from @@ -2194,6 +2190,55 @@ def regional_multiplier(self): multipliers = self.gen["capital_cost_multiplier"] return self.exclusion_weighted_mean(multipliers) + @property + def _sam_system_capacity(self): + """float: Mean SAM generation system capacity input, defaults to 0. """ + if self._ssc is not None: + return self._ssc + + self._ssc = 0 + if "system_capacity" in self.gen.datasets: + self._ssc = self.exclusion_weighted_mean( + self.gen["system_capacity"] + ) + + return self._ssc + + @property + def _sam_lcoe_kwargs(self): + """dict: Mean LCOE inputs, as passed to SAM during generation.""" + if self._slk: + return self._slk + + self._slk = {"capital_cost": None, "fixed_operating_cost": None, + "variable_operating_cost": None, + "fixed_charge_rate": None, "system_capacity": None} + + for dset in self._slk: + if dset in self.gen.datasets: + self._slk[dset] = self.exclusion_weighted_mean( + self.gen[dset] + ) + + return self._slk + + def _compute_cost_per_ac_mw(self, dset): + """Compute a cost per AC MW for a given input. """ + if self._sam_system_capacity <= 0: + return 0 + + if dset not in self.gen.datasets: + return 0 + + sam_cost = self.exclusion_weighted_mean(self.gen[dset]) + sam_cost_per_mw = sam_cost / self._sam_system_capacity + sc_point_cost = sam_cost_per_mw * self.capacity + + ac_cap = (self.capacity + if self.capacity_ac is None + else self.capacity_ac) + return sc_point_cost / ac_cap + @property def mean_h5_dsets_data(self): """Get the mean supplemental h5 datasets data (optional) @@ -2313,6 +2358,24 @@ def point_summary(self, args=None): SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW: ( self.sc_point_annual_energy ), + SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW: ( + self._compute_cost_per_ac_mw("capital_cost") + ), + SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW: ( + self._compute_cost_per_ac_mw("base_capital_cost") + ), + SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW: ( + self._compute_cost_per_ac_mw("fixed_operating_cost") + ), + SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW: ( + self._compute_cost_per_ac_mw("base_fixed_operating_cost") + ), + SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW: ( + self._compute_cost_per_ac_mw("variable_operating_cost") + ), + SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW: ( + self._compute_cost_per_ac_mw("base_variable_operating_cost") + ) } extra_atts = { @@ -2489,7 +2552,7 @@ def summarize( recalc_lcoe : bool Flag to re-calculate the LCOE from the multi-year mean capacity factor and annual energy production data. This requires several - datasets to be aggregated in the h5_dsets input: system_capacity, + datasets to be aggregated in the gen input: system_capacity, fixed_charge_rate, capital_cost, fixed_operating_cost, and variable_operating_cost. diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 97d9eeab5..c74db58a7 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -143,15 +143,11 @@ class SupplyCurveField(FieldEnum): OFFSHORE = "offshore" SC_ROW_IND = "sc_row_ind" SC_COL_IND = "sc_col_ind" - SC_POINT_CAPITAL_COST = "sc_point_capital_cost" - SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" SC_POINT_ANNUAL_ENERGY_MW = "sc_point_annual_energy" MEAN_FRICTION = "mean_friction" MEAN_LCOE_FRICTION = "mean_lcoe_friction" TOTAL_LCOE_FRICTION = "total_lcoe_friction" RAW_LCOE = "raw_lcoe" - SCALED_CAPITAL_COST = "scaled_capital_cost" - SCALED_SC_POINT_CAPITAL_COST = "scaled_sc_point_capital_cost" TURBINE_X_COORDS = "turbine_x_coords" TURBINE_Y_COORDS = "turbine_y_coords" EOS_MULT = "eos_mult" @@ -162,6 +158,18 @@ class SupplyCurveField(FieldEnum): CONVEX_HULL_CAPACITY_DENSITY = "convex_hull_capacity_density" FULL_CELL_CAPACITY_DENSITY = "full_cell_capacity_density" + COST_BASE_OCC_USD_PER_AC_MW = "cost_base_occ_usd_per_ac_mw" + COST_SITE_OCC_USD_PER_AC_MW = "cost_site_occ_usd_per_ac_mw" + COST_BASE_FOC_USD_PER_AC_MW = "cost_base_foc_usd_per_ac_mw" + COST_SITE_FOC_USD_PER_AC_MW = "cost_site_foc_usd_per_ac_mw" + COST_BASE_VOC_USD_PER_AC_MW = "cost_base_voc_usd_per_ac_mw" + COST_SITE_VOC_USD_PER_AC_MW = "cost_site_voc_usd_per_ac_mw" + + SC_POINT_CAPITAL_COST = "sc_point_capital_cost" + SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" + SCALED_CAPITAL_COST = "scaled_capital_cost" + SCALED_SC_POINT_CAPITAL_COST = "scaled_sc_point_capital_cost" + @classmethod def map_from_legacy(cls): """Map of legacy names to current values. diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index e25abe43f..aec680cc6 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -190,7 +190,8 @@ def test_econ_of_scale_baseline(): sc_df[SupplyCurveField.MEAN_LCOE]) assert (sc_df[SupplyCurveField.EOS_MULT] == 1).all() assert np.allclose(sc_df['mean_capital_cost'], - sc_df[SupplyCurveField.SCALED_CAPITAL_COST]) + sc_df[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] + * 20000) def test_sc_agg_econ_scale(): From 1ee754689f44920454be24cf3dd390e2a19461c5 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 22:59:34 -0600 Subject: [PATCH 56/88] Add to test --- tests/test_econ_of_scale.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index aec680cc6..b2e2228ea 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -159,6 +159,10 @@ def test_econ_of_scale_baseline(): res.create_dataset(k, res["meta"].shape, data=arr) res[k].attrs["scale_factor"] = 1.0 + arr = np.full(res["meta"].shape, data["capital_cost"]) + res.create_dataset("base_capital_cost", + res["meta"].shape, data=arr) + out_fp_base = os.path.join(td, "base") base = SupplyCurveAggregation( EXCL, @@ -192,6 +196,8 @@ def test_econ_of_scale_baseline(): assert np.allclose(sc_df['mean_capital_cost'], sc_df[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] * 20000) + assert np.allclose(sc_df[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW], + sc_df[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW]) def test_sc_agg_econ_scale(): From 7ad4d10e55007f54cb8b6853f3711d0cf45a86c1 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 23:08:22 -0600 Subject: [PATCH 57/88] Remove unnecessary field --- reV/supply_curve/points.py | 5 ++++- reV/utilities/__init__.py | 1 - tests/test_econ_of_scale.py | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index db08673a4..6966d1eea 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2437,7 +2437,10 @@ def economies_of_scale(cap_cost_scale, summary): summary[SupplyCurveField.RAW_LCOE] = eos.raw_lcoe summary[SupplyCurveField.MEAN_LCOE] = eos.scaled_lcoe summary[SupplyCurveField.EOS_MULT] = eos.capital_cost_scalar - summary[SupplyCurveField.SCALED_CAPITAL_COST] = eos.scaled_capital_cost + summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = ( + summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] + * summary[SupplyCurveField.EOS_MULT] + ) if SupplyCurveField.SC_POINT_CAPITAL_COST in summary: scaled_costs = ( summary[SupplyCurveField.SC_POINT_CAPITAL_COST] diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index c74db58a7..5195df642 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -167,7 +167,6 @@ class SupplyCurveField(FieldEnum): SC_POINT_CAPITAL_COST = "sc_point_capital_cost" SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" - SCALED_CAPITAL_COST = "scaled_capital_cost" SCALED_SC_POINT_CAPITAL_COST = "scaled_sc_point_capital_cost" @classmethod diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index b2e2228ea..3c23aab73 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -275,8 +275,6 @@ def test_sc_agg_econ_scale(): ) / aep + data["variable_operating_cost"] assert np.allclose(scalars, sc_df[SupplyCurveField.EOS_MULT]) - assert np.allclose(scalars * sc_df['mean_capital_cost'], - sc_df[SupplyCurveField.SCALED_CAPITAL_COST]) assert np.allclose(true_scaled_lcoe, sc_df[SupplyCurveField.MEAN_LCOE]) assert np.allclose(true_raw_lcoe, sc_df[SupplyCurveField.RAW_LCOE]) From f319e9728aa2dedab2a941d9d0dd0800df157dc3 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 23:53:05 -0600 Subject: [PATCH 58/88] Fix typo bug --- reV/econ/economies_of_scale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/econ/economies_of_scale.py b/reV/econ/economies_of_scale.py index 3212e8ee5..31e48b0c9 100644 --- a/reV/econ/economies_of_scale.py +++ b/reV/econ/economies_of_scale.py @@ -292,7 +292,7 @@ def voc(self): Variable operating cost from input data arg """ voc_from_cap = self._cost_from_cap( - SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW + SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW ) if voc_from_cap is not None: return voc_from_cap From c43b8d8972e6a38f9c7a9136f3ae516f791123ea Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Tue, 11 Jun 2024 23:56:48 -0600 Subject: [PATCH 59/88] Updated cost outputs --- reV/supply_curve/points.py | 83 +++-------------------- reV/utilities/__init__.py | 6 +- tests/test_supply_curve_sc_aggregation.py | 48 ++++++++++--- 3 files changed, 47 insertions(+), 90 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index 6966d1eea..cbfa9b995 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -2074,61 +2074,6 @@ def capacity_dc(self): return self.area * self.power_density - @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. @@ -2190,6 +2135,14 @@ def regional_multiplier(self): multipliers = self.gen["capital_cost_multiplier"] return self.exclusion_weighted_mean(multipliers) + @property + def fixed_charge_rate(self): + """float: Mean fixed_charge_rate, defaults to 0.""" + if "fixed_charge_rate" not in self.gen.datasets: + return 0 + + return self.exclusion_weighted_mean(self.gen["fixed_charge_rate"]) + @property def _sam_system_capacity(self): """float: Mean SAM generation system capacity input, defaults to 0. """ @@ -2375,18 +2328,9 @@ def point_summary(self, args=None): ), SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW: ( self._compute_cost_per_ac_mw("base_variable_operating_cost") - ) - } - - extra_atts = { - SupplyCurveField.SC_POINT_CAPITAL_COST: self.sc_point_capital_cost, - SupplyCurveField.SC_POINT_FIXED_OPERATING_COST: ( - self.sc_point_fixed_operating_cost ), + SupplyCurveField.FIXED_CHARGE_RATE: self.fixed_charge_rate, } - for attr, value in extra_atts.items(): - if value is not None: - ARGS[attr] = value if self._friction_layer is not None: ARGS[SupplyCurveField.MEAN_FRICTION] = self.mean_friction @@ -2441,15 +2385,6 @@ def economies_of_scale(cap_cost_scale, summary): summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] * summary[SupplyCurveField.EOS_MULT] ) - if SupplyCurveField.SC_POINT_CAPITAL_COST in summary: - scaled_costs = ( - summary[SupplyCurveField.SC_POINT_CAPITAL_COST] - * eos.capital_cost_scalar - ) - summary[SupplyCurveField.SCALED_SC_POINT_CAPITAL_COST] = ( - scaled_costs - ) - return summary @classmethod diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 5195df642..d3d807044 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -157,17 +157,13 @@ class SupplyCurveField(FieldEnum): CONVEX_HULL_AREA = "convex_hull_area" CONVEX_HULL_CAPACITY_DENSITY = "convex_hull_capacity_density" FULL_CELL_CAPACITY_DENSITY = "full_cell_capacity_density" - COST_BASE_OCC_USD_PER_AC_MW = "cost_base_occ_usd_per_ac_mw" COST_SITE_OCC_USD_PER_AC_MW = "cost_site_occ_usd_per_ac_mw" COST_BASE_FOC_USD_PER_AC_MW = "cost_base_foc_usd_per_ac_mw" COST_SITE_FOC_USD_PER_AC_MW = "cost_site_foc_usd_per_ac_mw" COST_BASE_VOC_USD_PER_AC_MW = "cost_base_voc_usd_per_ac_mw" COST_SITE_VOC_USD_PER_AC_MW = "cost_site_voc_usd_per_ac_mw" - - SC_POINT_CAPITAL_COST = "sc_point_capital_cost" - SC_POINT_FIXED_OPERATING_COST = "sc_point_fixed_operating_cost" - SCALED_SC_POINT_CAPITAL_COST = "scaled_sc_point_capital_cost" + FIXED_CHARGE_RATE = "fixed_charge_rate" @classmethod def map_from_legacy(cls): diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index ed5a2b02c..1a3cd6610 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -501,6 +501,19 @@ def test_recalc_lcoe(cap_cost_scale): for k, v in data.items(): arr = np.full(res["meta"].shape, v) res.create_dataset(k, res["meta"].shape, data=arr) + + arr = np.full(res["meta"].shape, data["capital_cost"]) + res.create_dataset("base_capital_cost", res["meta"].shape, + data=arr) + + arr = np.full(res["meta"].shape, data["fixed_operating_cost"]) + res.create_dataset("base_fixed_operating_cost", res["meta"].shape, + data=arr) + + arr = np.full(res["meta"].shape, data["variable_operating_cost"]) + res.create_dataset("base_variable_operating_cost", + res["meta"].shape, data=arr) + for year, cf in zip(years, annual_cf): lcoe = lcoe_fcr(data["fixed_charge_rate"], data["capital_cost"], @@ -565,18 +578,31 @@ def test_recalc_lcoe(cap_cost_scale): summary[SupplyCurveField.MEAN_LCOE]) assert np.allclose(summary[SupplyCurveField.EOS_MULT], - summary[SupplyCurveField.SCALED_SC_POINT_CAPITAL_COST] - / summary[SupplyCurveField.SC_POINT_CAPITAL_COST]) + summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] + / summary[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW]) + + fcr = summary[SupplyCurveField.FIXED_CHARGE_RATE] + cap_cost = (summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] + * summary[SupplyCurveField.CAPACITY_AC_MW]) + foc = (summary[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] + * summary[SupplyCurveField.CAPACITY_AC_MW]) + voc = (summary[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] + * summary[SupplyCurveField.CAPACITY_AC_MW]) + aep = summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] + + lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) + assert np.allclose(lcoe, summary[SupplyCurveField.MEAN_LCOE]) - if cap_cost_scale == '1': - cc_dset = SupplyCurveField.SC_POINT_CAPITAL_COST - else: - cc_dset = SupplyCurveField.SCALED_SC_POINT_CAPITAL_COST - lcoe = lcoe_fcr(summary['mean_fixed_charge_rate'], - summary[cc_dset], - summary[SupplyCurveField.SC_POINT_FIXED_OPERATING_COST], - summary[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW], - summary['mean_variable_operating_cost']) + cap_cost = (summary[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] + * summary[SupplyCurveField.CAPACITY_AC_MW] + * summary[SupplyCurveField.REG_MULT] + * summary[SupplyCurveField.EOS_MULT]) + foc = (summary[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW] + * summary[SupplyCurveField.CAPACITY_AC_MW]) + voc = (summary[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] + * summary[SupplyCurveField.CAPACITY_AC_MW]) + + lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) assert np.allclose(lcoe, summary[SupplyCurveField.MEAN_LCOE]) From 5303c5998dda679ddc630e3b92c58d9caaf13e31 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 12 Jun 2024 11:03:35 -0600 Subject: [PATCH 60/88] Add meta rename for prior run --- reV/bespoke/bespoke.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 7e16a93ae..628155578 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -2033,6 +2033,7 @@ def _parse_prior_run(prior_run): if isinstance(val, str) and val[0] == "[" and val[-1] == "]": meta[col] = meta[col].apply(json.loads) + meta = meta.rename(columns=SupplyCurveField.map_from_legacy()) return meta def _get_prior_meta(self, gid): From 81cef919a26512f9284da417ba6da8887b906115 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 12 Jun 2024 11:58:04 -0600 Subject: [PATCH 61/88] Add cost outputs to bespoke --- reV/bespoke/bespoke.py | 38 +++++++++++++++++++++++++++++-------- tests/test_bespoke.py | 43 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 628155578..d39a309eb 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1360,22 +1360,44 @@ def run_plant_optimization(self): # copy dataset outputs to meta data for supply curve table summary # convert SAM system capacity in kW to reV supply curve cap in MW - self._meta[SupplyCurveField.CAPACITY_AC_MW] = ( - self.outputs["system_capacity"] / 1e3 - ) + capacity_ac_mw = self.outputs["system_capacity"] / 1e3 + self._meta[SupplyCurveField.CAPACITY_AC_MW] = capacity_ac_mw self._meta[SupplyCurveField.CAPACITY_DC_MW] = None # add required ReEDS multipliers to meta baseline_cost = self.plant_optimizer.capital_cost_per_kw( capacity_mw=self._baseline_cap_mw ) - self._meta[SupplyCurveField.EOS_MULT] = ( + eos_mult = (self.plant_optimizer.capital_cost + / self.plant_optimizer.capacity + / baseline_cost) + reg_mult = self.sam_sys_inputs.get("capital_cost_multiplier", 1) + + self._meta[SupplyCurveField.EOS_MULT] = eos_mult + self._meta[SupplyCurveField.REG_MULT] = reg_mult + + cap_cost = ( self.plant_optimizer.capital_cost - / self.plant_optimizer.capacity - / baseline_cost + + self.plant_optimizer.balance_of_system_cost + ) + self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = cap_cost + self._meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] = ( + cap_cost / eos_mult / reg_mult / capacity_ac_mw + ) + self._meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] = ( + self.plant_optimizer.fixed_operating_cost / capacity_ac_mw + ) + self._meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW] = ( + self.plant_optimizer.fixed_operating_cost / capacity_ac_mw + ) + self._meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] = ( + self.plant_optimizer.variable_operating_cost / capacity_ac_mw + ) + self._meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] = ( + self.plant_optimizer.variable_operating_cost / capacity_ac_mw ) - self._meta[SupplyCurveField.REG_MULT] = self.sam_sys_inputs.get( - "capital_cost_multiplier", 1 + self._meta[SupplyCurveField.FIXED_CHARGE_RATE] = ( + self.plant_optimizer.fixed_charge_rate ) return self.outputs diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 82dcd7b1e..cd65d5083 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -17,6 +17,7 @@ from rex import Resource from reV import TESTDATADIR +from reV.econ.utilities import lcoe_fcr from reV.bespoke.bespoke import BespokeSinglePlant, BespokeWindPlants from reV.bespoke.place_turbines import PlaceTurbines, _compute_nn_conn_dist from reV.cli import main @@ -88,9 +89,25 @@ SupplyCurveField.TURBINE_X_COORDS, SupplyCurveField.TURBINE_Y_COORDS, SupplyCurveField.RES_GIDS, + SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.CAPACITY_DC_MW, + SupplyCurveField.MEAN_CF_AC, SupplyCurveField.MEAN_CF_DC, - SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] + SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW, + SupplyCurveField.EOS_MULT, + SupplyCurveField.REG_MULT, + SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW, + SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW, + SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW, + SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW, + SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW, + SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW, + SupplyCurveField.FIXED_CHARGE_RATE, + SupplyCurveField.INCLUDED_AREA, + SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY, + SupplyCurveField.CONVEX_HULL_AREA, + SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY, + SupplyCurveField.FULL_CELL_CAPACITY_DENSITY] def test_turbine_placement(gid=33): @@ -645,6 +662,30 @@ def test_bespoke(): assert f[dset].shape[1] == len(meta) assert f[dset].any() # not all zeros + fcr = meta[SupplyCurveField.FIXED_CHARGE_RATE] + cap_cost = (meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] + * meta[SupplyCurveField.CAPACITY_AC_MW]) + foc = (meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] + * meta[SupplyCurveField.CAPACITY_AC_MW]) + voc = (meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] + * meta[SupplyCurveField.CAPACITY_AC_MW]) + aep = meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] + + lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) + assert np.allclose(lcoe, meta[SupplyCurveField.MEAN_LCOE]) + + cap_cost = (meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] + * meta[SupplyCurveField.CAPACITY_AC_MW] + * meta[SupplyCurveField.REG_MULT] + * meta[SupplyCurveField.EOS_MULT]) + foc = (meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW] + * meta[SupplyCurveField.CAPACITY_AC_MW]) + voc = (meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] + * meta[SupplyCurveField.CAPACITY_AC_MW]) + + lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) + assert np.allclose(lcoe, meta[SupplyCurveField.MEAN_LCOE]) + out_fpath_pre = os.path.join(td, 'bespoke_out_pre.h5') bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION, CAP_COST_FUN, FOC_FUN, VOC_FUN, BOS_FUN, From 24a0e17545028dc9bed1e85eff9b001136d059b6 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 12 Jun 2024 16:16:02 -0600 Subject: [PATCH 62/88] One rename columns if meta is not None --- reV/bespoke/bespoke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index d39a309eb..d92d2b133 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -2048,6 +2048,7 @@ def _parse_prior_run(prior_run): with Outputs(prior_run, mode="r") as f: meta = f.meta + meta = meta.rename(columns=SupplyCurveField.map_from_legacy()) # pylint: disable=no-member for col in meta.columns: @@ -2055,7 +2056,6 @@ def _parse_prior_run(prior_run): if isinstance(val, str) and val[0] == "[" and val[-1] == "]": meta[col] = meta[col].apply(json.loads) - meta = meta.rename(columns=SupplyCurveField.map_from_legacy()) return meta def _get_prior_meta(self, gid): From a3cd110dcbbdfaf4c9806f9d0ccbda21093a5749 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 12 Jun 2024 17:17:11 -0600 Subject: [PATCH 63/88] Finalize bespoke meta outputs --- reV/bespoke/bespoke.py | 61 ++++++++++++++++++++------------------- reV/utilities/__init__.py | 9 ++++++ tests/test_bespoke.py | 45 ++++++++++++++++------------- 3 files changed, 65 insertions(+), 50 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index d92d2b133..6211a375e 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1071,11 +1071,9 @@ def recalc_lcoe(self): cc = lcoe_kwargs['capital_cost'] foc = lcoe_kwargs['fixed_operating_cost'] voc = lcoe_kwargs['variable_operating_cost'] - bos = lcoe_kwargs['balance_of_system_cost'] aep = self.outputs['annual_energy-means'] - cap_cost = cc + bos - my_mean_lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) + my_mean_lcoe = lcoe_fcr(fcr, cc, foc, aep, voc) self._outputs["lcoe_fcr-means"] = my_mean_lcoe self._meta[SupplyCurveField.MEAN_LCOE] = my_mean_lcoe @@ -1117,9 +1115,6 @@ def get_lcoe_kwargs(self): value = float(self.meta[kwarg].values[0]) lcoe_kwargs[kwarg] = value - for k, v in lcoe_kwargs.items(): - self._meta[k] = v - missing = [k for k in kwargs_list if k not in lcoe_kwargs] if any(missing): msg = ( @@ -1131,6 +1126,8 @@ def get_lcoe_kwargs(self): logger.error(msg) raise KeyError(msg) + bos = lcoe_kwargs.pop("balance_of_system_cost") + lcoe_kwargs["capital_cost"] = lcoe_kwargs["capital_cost"] + bos return lcoe_kwargs @staticmethod @@ -1299,9 +1296,12 @@ def run_plant_optimization(self): logger.exception(msg) raise RuntimeError(msg) from e - # TODO need to add: - # total cell area - # cell capacity density + self._outputs["full_polygons"] = self.plant_optimizer.full_polygons + self._outputs["packing_polygons"] = ( + self.plant_optimizer.packing_polygons + ) + system_capacity_kw = self.plant_optimizer.capacity + self._outputs["system_capacity"] = system_capacity_kw txc = [int(np.round(c)) for c in self.plant_optimizer.turbine_x] tyc = [int(np.round(c)) for c in self.plant_optimizer.turbine_y] @@ -1315,31 +1315,31 @@ def run_plant_optimization(self): self._meta[SupplyCurveField.TURBINE_X_COORDS] = txc self._meta[SupplyCurveField.TURBINE_Y_COORDS] = tyc - self._meta["possible_x_coords"] = pxc - self._meta["possible_y_coords"] = pyc - - self._outputs["full_polygons"] = self.plant_optimizer.full_polygons - self._outputs["packing_polygons"] = ( - self.plant_optimizer.packing_polygons - ) - self._outputs["system_capacity"] = self.plant_optimizer.capacity + self._meta[SupplyCurveField.POSSIBLE_X_COORDS] = pxc + self._meta[SupplyCurveField.POSSIBLE_Y_COORDS] = pyc - self._meta["n_turbines"] = self.plant_optimizer.nturbs - self._meta["avg_sl_dist_to_center_m"] = \ + self._meta[SupplyCurveField.N_TURBINES] = self.plant_optimizer.nturbs + self._meta["avg_sl_dist_to_center_m"] = ( self.plant_optimizer.avg_sl_dist_to_center_m - self._meta["avg_sl_dist_to_medoid_m"] = \ + ) + self._meta["avg_sl_dist_to_medoid_m"] = ( self.plant_optimizer.avg_sl_dist_to_medoid_m + ) self._meta["nn_conn_dist_m"] = self.plant_optimizer.nn_conn_dist_m - self._meta["bespoke_aep"] = self.plant_optimizer.aep - self._meta["bespoke_objective"] = self.plant_optimizer.objective - self._meta["bespoke_capital_cost"] = self.plant_optimizer.capital_cost - self._meta["bespoke_fixed_operating_cost"] = ( + self._meta[SupplyCurveField.BESPOKE_AEP] = self.plant_optimizer.aep + self._meta[SupplyCurveField.BESPOKE_OBJECTIVE] = ( + self.plant_optimizer.objective + ) + self._meta[SupplyCurveField.BESPOKE_CAPITAL_COST] = ( + self.plant_optimizer.capital_cost + ) + self._meta[SupplyCurveField.BESPOKE_FIXED_OPERATING_COST] = ( self.plant_optimizer.fixed_operating_cost ) - self._meta["bespoke_variable_operating_cost"] = ( + self._meta[SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST] = ( self.plant_optimizer.variable_operating_cost ) - self._meta["bespoke_balance_of_system_cost"] = ( + self._meta[SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST] = ( self.plant_optimizer.balance_of_system_cost ) self._meta[SupplyCurveField.INCLUDED_AREA] = self.plant_optimizer.area @@ -1356,11 +1356,9 @@ def run_plant_optimization(self): self.plant_optimizer.full_cell_capacity_density ) - logger.debug("Plant layout optimization complete!") - # copy dataset outputs to meta data for supply curve table summary # convert SAM system capacity in kW to reV supply curve cap in MW - capacity_ac_mw = self.outputs["system_capacity"] / 1e3 + capacity_ac_mw = system_capacity_kw / 1e3 self._meta[SupplyCurveField.CAPACITY_AC_MW] = capacity_ac_mw self._meta[SupplyCurveField.CAPACITY_DC_MW] = None @@ -1380,7 +1378,9 @@ def run_plant_optimization(self): self.plant_optimizer.capital_cost + self.plant_optimizer.balance_of_system_cost ) - self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = cap_cost + self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = ( + cap_cost / capacity_ac_mw + ) self._meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] = ( cap_cost / eos_mult / reg_mult / capacity_ac_mw ) @@ -1400,6 +1400,7 @@ def run_plant_optimization(self): self.plant_optimizer.fixed_charge_rate ) + logger.debug("Plant layout optimization complete!") return self.outputs def agg_data_layers(self): diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index d3d807044..76560836f 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -148,8 +148,11 @@ class SupplyCurveField(FieldEnum): MEAN_LCOE_FRICTION = "mean_lcoe_friction" TOTAL_LCOE_FRICTION = "total_lcoe_friction" RAW_LCOE = "raw_lcoe" + POSSIBLE_X_COORDS = "possible_x_coords" + POSSIBLE_Y_COORDS = "possible_y_coords" TURBINE_X_COORDS = "turbine_x_coords" TURBINE_Y_COORDS = "turbine_y_coords" + N_TURBINES = "n_turbines" EOS_MULT = "eos_mult" REG_MULT = "reg_mult" INCLUDED_AREA = "included_area" @@ -164,6 +167,12 @@ class SupplyCurveField(FieldEnum): COST_BASE_VOC_USD_PER_AC_MW = "cost_base_voc_usd_per_ac_mw" COST_SITE_VOC_USD_PER_AC_MW = "cost_site_voc_usd_per_ac_mw" FIXED_CHARGE_RATE = "fixed_charge_rate" + BESPOKE_AEP = "bespoke_aep" + BESPOKE_OBJECTIVE = "bespoke_objective" + BESPOKE_CAPITAL_COST = "bespoke_capital_cost" + BESPOKE_FIXED_OPERATING_COST = "bespoke_fixed_operating_cost" + BESPOKE_VARIABLE_OPERATING_COST = "bespoke_variable_operating_cost" + BESPOKE_BALANCE_OF_SYSTEM_COST = "bespoke_balance_of_system_cost" @classmethod def map_from_legacy(cls): diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index cd65d5083..0f338872c 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -88,6 +88,9 @@ EXPECTED_META_COLUMNS = [SupplyCurveField.SC_POINT_GID, SupplyCurveField.TURBINE_X_COORDS, SupplyCurveField.TURBINE_Y_COORDS, + SupplyCurveField.POSSIBLE_X_COORDS, + SupplyCurveField.POSSIBLE_Y_COORDS, + SupplyCurveField.N_TURBINES, SupplyCurveField.RES_GIDS, SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.CAPACITY_DC_MW, @@ -107,7 +110,13 @@ SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY, SupplyCurveField.CONVEX_HULL_AREA, SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY, - SupplyCurveField.FULL_CELL_CAPACITY_DENSITY] + SupplyCurveField.FULL_CELL_CAPACITY_DENSITY, + SupplyCurveField.BESPOKE_AEP, + SupplyCurveField.BESPOKE_OBJECTIVE, + SupplyCurveField.BESPOKE_CAPITAL_COST, + SupplyCurveField.BESPOKE_FIXED_OPERATING_COST, + SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST, + SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST] def test_turbine_placement(gid=33): @@ -393,7 +402,7 @@ def test_single(gid=33): assert "annual_energy-means" in out assert ( - TURB_RATING * bsp.meta["n_turbines"].values[0] + TURB_RATING * bsp.meta[SupplyCurveField.N_TURBINES].values[0] == out["system_capacity"] ) x_coords = json.loads( @@ -402,8 +411,8 @@ def test_single(gid=33): y_coords = json.loads( bsp.meta[SupplyCurveField.TURBINE_Y_COORDS].values[0] ) - assert bsp.meta["n_turbines"].values[0] == len(x_coords) - assert bsp.meta["n_turbines"].values[0] == len(y_coords) + assert bsp.meta[SupplyCurveField.N_TURBINES].values[0] == len(x_coords) + assert bsp.meta[SupplyCurveField.N_TURBINES].values[0] == len(y_coords) for y in (2012, 2013): cf = out[f"cf_profile-{y}"] @@ -556,7 +565,7 @@ def test_extra_outputs(gid=33): bsp.close() -def test_bespoke(): +def test_bespok_kjbndkjnbdfkjne(): """Test bespoke optimization with multiple plants, parallel processing, and file output.""" output_request = ( @@ -595,6 +604,8 @@ def test_bespoke(): ) TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) + sam_configs = copy.deepcopy(SAM_CONFIGS) + sam_configs["default"]["fixed_charge_rate"] = 0.0975 # test no outputs with pytest.warns(UserWarning) as record: @@ -603,7 +614,7 @@ def test_bespoke(): OBJECTIVE_FUNCTION, CAP_COST_FUN, FOC_FUN, VOC_FUN, BOS_FUN, fully_excluded_points, - SAM_CONFIGS, ga_kwargs={'max_time': 5}, + sam_configs, ga_kwargs={'max_time': 5}, excl_dict=EXCL_DICT, output_request=output_request) test_fpath = bsp.run(max_workers=2, out_fpath=out_fpath_request) @@ -613,7 +624,7 @@ def test_bespoke(): assert not os.path.exists(out_fpath_truth) bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION, CAP_COST_FUN, FOC_FUN, VOC_FUN, BOS_FUN, - points, SAM_CONFIGS, ga_kwargs={'max_time': 5}, + points, sam_configs, ga_kwargs={'max_time': 5}, excl_dict=EXCL_DICT, output_request=output_request) test_fpath = bsp.run(max_workers=2, out_fpath=out_fpath_request) @@ -625,9 +636,6 @@ def test_bespoke(): for col in EXPECTED_META_COLUMNS: assert col in meta - assert "possible_x_coords" in meta - assert "possible_y_coords" in meta - dsets_1d = ( "system_capacity", "cf_mean-2012", @@ -670,9 +678,7 @@ def test_bespoke(): voc = (meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] * meta[SupplyCurveField.CAPACITY_AC_MW]) aep = meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] - - lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) - assert np.allclose(lcoe, meta[SupplyCurveField.MEAN_LCOE]) + lcoe_site = lcoe_fcr(fcr, cap_cost, foc, aep, voc) cap_cost = (meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] * meta[SupplyCurveField.CAPACITY_AC_MW] @@ -682,9 +688,9 @@ def test_bespoke(): * meta[SupplyCurveField.CAPACITY_AC_MW]) voc = (meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] * meta[SupplyCurveField.CAPACITY_AC_MW]) + lcoe_base = lcoe_fcr(fcr, cap_cost, foc, aep, voc) - lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) - assert np.allclose(lcoe, meta[SupplyCurveField.MEAN_LCOE]) + assert np.allclose(lcoe_site, lcoe_base) out_fpath_pre = os.path.join(td, 'bespoke_out_pre.h5') bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION, @@ -784,11 +790,10 @@ def test_consistent_eval_namespace(gid=33): ) _ = bsp.run_plant_optimization() - assert bsp.meta["bespoke_aep"].values[0] == bsp.plant_optimizer.aep - assert ( - bsp.meta["bespoke_objective"].values[0] - == bsp.plant_optimizer.objective - ) + assert (bsp.meta[SupplyCurveField.BESPOKE_AEP].values[0] + == bsp.plant_optimizer.aep) + assert (bsp.meta[SupplyCurveField.BESPOKE_OBJECTIVE].values[0] + == bsp.plant_optimizer.objective) bsp.close() From 137493714b7e2a138a39cbebf9d0a9341c79bd39 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Wed, 12 Jun 2024 18:08:12 -0600 Subject: [PATCH 64/88] Fix prior meta runs --- reV/bespoke/bespoke.py | 31 +++++++++++++++++++++---------- tests/test_bespoke.py | 23 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 6211a375e..4e5b73a16 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1094,17 +1094,23 @@ def get_lcoe_kwargs(self): plant_optimizer, original_sam_sys_inputs, meta """ - kwargs_list = [ - "fixed_charge_rate", - "system_capacity", - "capital_cost", - "fixed_operating_cost", - "variable_operating_cost", - "balance_of_system_cost", - ] + kwargs_map = { + "fixed_charge_rate": SupplyCurveField.FIXED_CHARGE_RATE, + "system_capacity": SupplyCurveField.CAPACITY_AC_MW, + "capital_cost": SupplyCurveField.BESPOKE_CAPITAL_COST, + "fixed_operating_cost": ( + SupplyCurveField.BESPOKE_FIXED_OPERATING_COST + ), + "variable_operating_cost": ( + SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST + ), + "balance_of_system_cost": ( + SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST + ), + } lcoe_kwargs = {} - for kwarg in kwargs_list: + for kwarg, meta_field in kwargs_map.items(): if kwarg in self.outputs: lcoe_kwargs[kwarg] = self.outputs[kwarg] elif getattr(self.plant_optimizer, kwarg, None) is not None: @@ -1114,8 +1120,13 @@ def get_lcoe_kwargs(self): elif kwarg in self.meta: value = float(self.meta[kwarg].values[0]) lcoe_kwargs[kwarg] = value + elif meta_field in self.meta: + value = float(self.meta[meta_field].values[0]) + if meta_field == SupplyCurveField.CAPACITY_AC_MW: + value *= 1000 # MW to kW + lcoe_kwargs[kwarg] = value - missing = [k for k in kwargs_list if k not in lcoe_kwargs] + missing = [k for k in kwargs_map if k not in lcoe_kwargs] if any(missing): msg = ( "Could not find these LCOE kwargs in outputs, " diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 0f338872c..c44120d96 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -565,7 +565,7 @@ def test_extra_outputs(gid=33): bsp.close() -def test_bespok_kjbndkjnbdfkjne(): +def test_bespoke(): """Test bespoke optimization with multiple plants, parallel processing, and file output.""" output_request = ( @@ -1253,6 +1253,27 @@ def test_bespoke_prior_run(): SupplyCurveField.N_GIDS, SupplyCurveField.GID_COUNTS, SupplyCurveField.RES_GIDS, + SupplyCurveField.N_TURBINES, + SupplyCurveField.EOS_MULT, + SupplyCurveField.REG_MULT, + SupplyCurveField.INCLUDED_AREA, + SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY, + SupplyCurveField.CONVEX_HULL_AREA, + SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY, + SupplyCurveField.FULL_CELL_CAPACITY_DENSITY, + SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW, + SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW, + SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW, + SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW, + SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW, + SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW, + SupplyCurveField.FIXED_CHARGE_RATE, + SupplyCurveField.BESPOKE_AEP, + SupplyCurveField.BESPOKE_OBJECTIVE, + SupplyCurveField.BESPOKE_CAPITAL_COST, + SupplyCurveField.BESPOKE_FIXED_OPERATING_COST, + SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST, + SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST, ] pd.testing.assert_frame_equal(meta1[cols], meta2[cols]) From 620a36a992a2cdf4cbf8e629681541f9f9098a25 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 11:26:44 -0600 Subject: [PATCH 65/88] Check for fcr column first --- reV/econ/economies_of_scale.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/reV/econ/economies_of_scale.py b/reV/econ/economies_of_scale.py index 31e48b0c9..58488e233 100644 --- a/reV/econ/economies_of_scale.py +++ b/reV/econ/economies_of_scale.py @@ -259,6 +259,10 @@ def fcr(self): out : float | np.ndarray Fixed charge rate from input data arg """ + fcr = self._data.get(SupplyCurveField.FIXED_CHARGE_RATE) + if fcr is not None and fcr > 0: + return fcr + key_list = ["fixed_charge_rate", "mean_fixed_charge_rate", "fcr", "mean_fcr"] return self._get_prioritized_keys(self._data, key_list) From 43ec152d36d6c17ba62f8208a7c4a43d59f1b536 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 11:32:10 -0600 Subject: [PATCH 66/88] No longer need to warn about passing through LCOE datasets as `h5_dsets` request --- reV/supply_curve/sc_aggregation.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/reV/supply_curve/sc_aggregation.py b/reV/supply_curve/sc_aggregation.py index 858e17b5a..61185e7b1 100644 --- a/reV/supply_curve/sc_aggregation.py +++ b/reV/supply_curve/sc_aggregation.py @@ -882,7 +882,6 @@ def _get_extra_dsets(gen, h5_dsets): 'system_capacity') missing_lcoe_source = [k for k in lcoe_recalc_req if k not in gen_dsets] - missing_lcoe_request = [] if isinstance(gen, Resource): source_fps = [gen.h5_file] @@ -897,9 +896,6 @@ def _get_extra_dsets(gen, h5_dsets): h5_dsets_data = None if h5_dsets is not None: - missing_lcoe_request = [ - k for k in lcoe_recalc_req if k not in h5_dsets - ] if not isinstance(h5_dsets, (list, tuple)): e = ( @@ -933,17 +929,6 @@ def _get_extra_dsets(gen, h5_dsets): logger.warning(msg) warn(msg, InputWarning) - if any(missing_lcoe_request): - msg = ( - "It is strongly advised that you include the following " - "datasets in the h5_dsets request in order to re-calculate " - "the LCOE from the multi-year mean CF and AEP: {}".format( - missing_lcoe_request - ) - ) - logger.warning(msg) - warn(msg, InputWarning) - return h5_dsets_data @classmethod From c0fa7a400a32024496d738e59efb01cf96aa0c6f Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 11:47:40 -0600 Subject: [PATCH 67/88] Add extra check for lcoe correctness --- tests/test_supply_curve_sc_aggregation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 1a3cd6610..99e4737f9 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -581,6 +581,16 @@ def test_recalc_lcoe(cap_cost_scale): summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] / summary[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW]) + expected_recalc_lcoe = lcoe_fcr(data["fixed_charge_rate"], + data["capital_cost"], + data["fixed_operating_cost"], + data["system_capacity"] + * np.array(annual_cf).mean() + * 8760, + data["variable_operating_cost"]) + assert np.allclose(summary[SupplyCurveField.MEAN_LCOE], + expected_recalc_lcoe) + fcr = summary[SupplyCurveField.FIXED_CHARGE_RATE] cap_cost = (summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] * summary[SupplyCurveField.CAPACITY_AC_MW]) From c77a8f4222b0bae4f2422d2d228b953f89c47e48 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 14:06:55 -0600 Subject: [PATCH 68/88] Update docstrings --- reV/supply_curve/points.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/reV/supply_curve/points.py b/reV/supply_curve/points.py index cbfa9b995..8f1c801f6 100644 --- a/reV/supply_curve/points.py +++ b/reV/supply_curve/points.py @@ -1751,6 +1751,11 @@ def mean_cf(self): factor is weighted by the exclusions (usually 0 or 1, but 0.5 exclusions will weight appropriately). + This value represents DC capacity factor for solar and AC + capacity factor for all other technologies. This is the capacity + factor that should be used for all cost calculations for ALL + technologies (to align with SAM). + Returns ------- mean_cf : float | None @@ -2017,6 +2022,11 @@ def capacity(self): """Get the estimated capacity in MW of the supply curve point in the current resource class with the applied exclusions. + This value represents DC capacity for solar and AC capacity for + all other technologies. This is the capacity that should be used + for all cost calculations for ALL technologies (to align with + SAM). + Returns ------- capacity : float @@ -2035,7 +2045,7 @@ def capacity_ac(self): """Get the AC estimated capacity in MW of the supply curve point in the current resource class with the applied exclusions. - This values is provided only for solar inputs that have + This value is provided only for solar inputs that have the "dc_ac_ratio" dataset in the generation file. If these conditions are not met, this value is `None`. @@ -2057,7 +2067,7 @@ def capacity_dc(self): """Get the DC estimated capacity in MW of the supply curve point in the current resource class with the applied exclusions. - This values is provided only for solar inputs that have + This value is provided only for solar inputs that have the "dc_ac_ratio" dataset in the generation file. If these conditions are not met, this value is `None`. From 9bca2bf67ffd330312ad0ebf25e38bc01bda7b13 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 14:41:21 -0600 Subject: [PATCH 69/88] Remove unused import --- tests/test_handlers_transmission.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_handlers_transmission.py b/tests/test_handlers_transmission.py index eade43156..9e3a983ba 100644 --- a/tests/test_handlers_transmission.py +++ b/tests/test_handlers_transmission.py @@ -10,7 +10,6 @@ from reV import TESTDATADIR from reV.handlers.transmission import TransmissionFeatures as TF -from reV.utilities import SupplyCurveField TRANS_COSTS_1 = { "line_tie_in_cost": 200, From cb18f8d50849400e62d01132c6da1b350a9cad0b Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 17:58:22 -0600 Subject: [PATCH 70/88] [WIP] New transmission output columns --- reV/handlers/transmission.py | 71 ++++---- reV/supply_curve/supply_curve.py | 252 +++++++++++++++++------------ reV/utilities/__init__.py | 54 +++++-- tests/test_supply_curve_compute.py | 182 ++++++++++++--------- 4 files changed, 342 insertions(+), 217 deletions(-) diff --git a/reV/handlers/transmission.py b/reV/handlers/transmission.py index c27713cf7..2734757d2 100644 --- a/reV/handlers/transmission.py +++ b/reV/handlers/transmission.py @@ -9,6 +9,7 @@ import pandas as pd from warnings import warn +from reV.utilities import SupplyCurveField from reV.utilities.exceptions import (HandlerWarning, HandlerKeyError, HandlerRuntimeError) @@ -153,12 +154,17 @@ def _parse_table(trans_table): raise trans_table = \ - trans_table.rename(columns={'trans_line_gid': 'trans_gid', - 'trans_gids': 'trans_line_gids'}) - - if 'dist_mi' in trans_table and 'dist_km' not in trans_table: - trans_table = trans_table.rename(columns={'dist_mi': 'dist_km'}) - trans_table['dist_km'] *= 1.60934 + trans_table.rename( + columns={'trans_line_gid': SupplyCurveField.TRANS_GID, + 'trans_gids': 'trans_line_gids'}) + + contains_dist_in_miles = "dist_mi" in trans_table + missing_km_dist = SupplyCurveField.DIST_SPUR_KM not in trans_table + if contains_dist_in_miles and missing_km_dist: + trans_table = trans_table.rename( + columns={"dist_mi": SupplyCurveField.DIST_SPUR_KM} + ) + trans_table[SupplyCurveField.DIST_SPUR_KM] *= 1.60934 return trans_table @@ -184,23 +190,28 @@ def _features_from_table(self, trans_table): features = {} cap_frac = self._avail_cap_frac - trans_features = trans_table.groupby('trans_gid').first() + trans_features = trans_table.groupby(SupplyCurveField.TRANS_GID) + trans_features = trans_features.first() for gid, feature in trans_features.iterrows(): - name = feature['category'].lower() + name = feature[SupplyCurveField.TRANS_TYPE].lower() feature_dict = {'type': name} if name == 'transline': - feature_dict['avail_cap'] = feature['ac_cap'] * cap_frac + feature_dict[SupplyCurveField.TRANS_CAPACITY] = ( + feature['ac_cap'] * cap_frac + ) elif name == 'substation': feature_dict['lines'] = json.loads(feature['trans_line_gids']) elif name == 'loadcen': - feature_dict['avail_cap'] = feature['ac_cap'] * cap_frac + feature_dict[SupplyCurveField.TRANS_CAPACITY] = ( + feature['ac_cap'] * cap_frac + ) elif name == 'pcaloadcen': - feature_dict['avail_cap'] = None + feature_dict[SupplyCurveField.TRANS_CAPACITY] = None else: msg = ('Cannot not recognize feature type "{}" ' @@ -297,7 +308,8 @@ def _substation_capacity(self, gid, line_gids): Substation available capacity """ try: - line_caps = [self[l_gid]['avail_cap'] for l_gid in line_gids] + line_caps = [self[l_gid][SupplyCurveField.TRANS_CAPACITY] + for l_gid in line_gids] except HandlerKeyError as e: msg = ('Could not find capacities for substation gid {} and ' 'connected lines: {}'.format(gid, line_gids)) @@ -331,8 +343,8 @@ def available_capacity(self, gid): feature = self[gid] - if 'avail_cap' in feature: - avail_cap = feature['avail_cap'] + if SupplyCurveField.TRANS_CAPACITY in feature: + avail_cap = feature[SupplyCurveField.TRANS_CAPACITY] elif 'lines' in feature: avail_cap = self._substation_capacity(gid, feature['lines']) @@ -387,7 +399,7 @@ def _connect(self, gid, capacity): capacity : float Capacity needed in MW """ - avail_cap = self[gid]['avail_cap'] + avail_cap = self[gid][SupplyCurveField.TRANS_CAPACITY] if avail_cap < capacity: msg = ("Cannot connect to {}: " @@ -397,7 +409,7 @@ def _connect(self, gid, capacity): logger.error(msg) raise RuntimeError(msg) - self[gid]['avail_cap'] -= capacity + self[gid][SupplyCurveField.TRANS_CAPACITY] -= capacity def _fill_lines(self, line_gids, line_caps, capacity): """ @@ -471,7 +483,7 @@ def _connect_to_substation(self, line_gids, capacity): Substation connection is limited by maximum capacity of the attached lines """ - line_caps = np.array([self[gid]['avail_cap'] + line_caps = np.array([self[gid][SupplyCurveField.TRANS_CAPACITY] for gid in line_gids]) if self._line_limited: gid = line_gids[np.argmax(line_caps)] @@ -603,8 +615,8 @@ def feature_capacity(cls, trans_table, avail_cap_frac=1): raise feature_cap = pd.Series(feature_cap) - feature_cap.name = 'avail_cap' - feature_cap.index.name = 'trans_gid' + feature_cap.name = SupplyCurveField.TRANS_CAPACITY + feature_cap.index.name = SupplyCurveField.TRANS_GID feature_cap = feature_cap.to_frame().reset_index() return feature_cap @@ -635,16 +647,20 @@ def _features_from_table(self, trans_table): features = {} - if 'avail_cap' not in trans_table: + if SupplyCurveField.TRANS_CAPACITY not in trans_table: kwargs = {'avail_cap_frac': self._avail_cap_frac} fc = TransmissionFeatures.feature_capacity(trans_table, **kwargs) - trans_table = trans_table.merge(fc, on='trans_gid') + trans_table = trans_table.merge(fc, on=SupplyCurveField.TRANS_GID) - trans_features = trans_table.groupby('trans_gid').first() + trans_features = trans_table.groupby(SupplyCurveField.TRANS_GID) + trans_features = trans_features.first() for gid, feature in trans_features.iterrows(): - name = feature['category'].lower() - feature_dict = {'type': name, 'avail_cap': feature['avail_cap']} + name = feature[SupplyCurveField.TRANS_TYPE].lower() + feature_dict = {'type': name, + SupplyCurveField.TRANS_CAPACITY: ( + feature[SupplyCurveField.TRANS_CAPACITY] + )} features[gid] = feature_dict return features @@ -665,7 +681,7 @@ def available_capacity(self, gid): default = 100% """ - return self[gid]['avail_cap'] + return self[gid][SupplyCurveField.TRANS_CAPACITY] @classmethod def feature_costs(cls, trans_table, capacity=None, line_tie_in_cost=14000, @@ -722,8 +738,9 @@ def feature_costs(cls, trans_table, capacity=None, line_tie_in_cost=14000, costs = [] for _, row in trans_table.iterrows(): tm = row.get('transmission_multiplier', 1) - costs.append(feature.cost(row['trans_gid'], - row['dist_km'], capacity=capacity, + costs.append(feature.cost(row[SupplyCurveField.TRANS_GID], + row[SupplyCurveField.DIST_SPUR_KM], + capacity=capacity, transmission_multiplier=tm)) except Exception: logger.exception("Error computing costs for all connections in {}" diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index a64ad88a8..1a7c59553 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -7,6 +7,7 @@ import json import logging import os +from itertools import chain from copy import deepcopy from warnings import warn @@ -24,6 +25,27 @@ logger = logging.getLogger(__name__) +# map is column name to relative order in which it should appear in output file +_REQUIRED_COMPUTE_AND_OUTPUT_COLS = { + SupplyCurveField.TRANS_GID: 0, + SupplyCurveField.TRANS_TYPE: 1, + SupplyCurveField.N_PARALLEL_TRANS: 2, + SupplyCurveField.DIST_SPUR_KM: 3, + SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW: 10, + SupplyCurveField.LCOT: 11, + SupplyCurveField.TOTAL_LCOE: 12, +} +_REQUIRED_OUTPUT_COLS = {SupplyCurveField.POI_LAT: 13, + SupplyCurveField.POI_LON: 14, + SupplyCurveField.REINFORCEMENT_POI_LAT: 15, + SupplyCurveField.REINFORCEMENT_POI_LON: 16, + SupplyCurveField.REINFORCEMENT_COST_PER_MW: 9, + SupplyCurveField.REINFORCEMENT_DIST_KM: 5} +DEFAULT_COLUMNS = tuple(str(field) + for field in chain(_REQUIRED_COMPUTE_AND_OUTPUT_COLS, + _REQUIRED_OUTPUT_COLS)) +"""Default output columns from supply chain computation (not ordered)""" + class SupplyCurve: """SupplyCurve""" @@ -299,15 +321,19 @@ def _parse_trans_table(trans_table): # also xformer_cost_p_mw -> xformer_cost_per_mw (not sure why there # would be a *_p_mw but here we are...) rename_map = { - "trans_line_gid": "trans_gid", + "trans_line_gid": SupplyCurveField.TRANS_GID, "trans_gids": "trans_line_gids", "xformer_cost_p_mw": "xformer_cost_per_mw", } trans_table = trans_table.rename(columns=rename_map) - if "dist_mi" in trans_table and "dist_km" not in trans_table: - trans_table = trans_table.rename(columns={"dist_mi": "dist_km"}) - trans_table["dist_km"] *= 1.60934 + contains_dist_in_miles = "dist_mi" in trans_table + missing_km_dist = SupplyCurveField.DIST_SPUR_KM not in trans_table + if contains_dist_in_miles and missing_km_dist: + trans_table = trans_table.rename( + columns={"dist_mi": SupplyCurveField.DIST_SPUR_KM} + ) + trans_table[SupplyCurveField.DIST_SPUR_KM] *= 1.60934 drop_cols = [SupplyCurveField.SC_GID, 'cap_left', SupplyCurveField.SC_POINT_GID] @@ -348,7 +374,7 @@ def _map_trans_capacity(trans_sc_table, nx = trans_sc_table[sc_capacity_col] / trans_sc_table["max_cap"] nx = np.ceil(nx).astype(int) - trans_sc_table["n_parallel_trans"] = nx + trans_sc_table[SupplyCurveField.N_PARALLEL_TRANS] = nx if (nx > 1).any(): mask = nx > 1 @@ -426,11 +452,12 @@ def _check_sub_trans_lines(cls, features): """ features = features.rename( columns={ - "trans_line_gid": "trans_gid", + "trans_line_gid": SupplyCurveField.TRANS_GID, "trans_gids": "trans_line_gids", } ) - mask = features["category"].str.lower() == "substation" + mask = (features[SupplyCurveField.TRANS_TYPE].str.casefold() + == "substation") if not any(mask): return [] @@ -441,7 +468,7 @@ def _check_sub_trans_lines(cls, features): line_gids = np.unique(np.concatenate(line_gids.values)) - test = np.isin(line_gids, features["trans_gid"].values) + test = np.isin(line_gids, features[SupplyCurveField.TRANS_GID].values) return line_gids[~test].tolist() @@ -667,9 +694,9 @@ def _map_tables(cls, sc_points, trans_table, trans_sc_table, sc_capacity_col=scc ) - trans_sc_table = \ - trans_sc_table.sort_values( - [SupplyCurveField.SC_GID, 'trans_gid']).reset_index(drop=True) + sort_cols = [SupplyCurveField.SC_GID, SupplyCurveField.TRANS_GID] + trans_sc_table = trans_sc_table.sort_values(sort_cols) + trans_sc_table = trans_sc_table.reset_index(drop=True) cls._check_sc_trans_table(sc_points, trans_sc_table) @@ -941,8 +968,9 @@ def compute_total_lcoe( Flag to consider friction layer on LCOE when "mean_lcoe_friction" is in the sc points input, by default True """ - if "trans_cap_cost_per_mw" in self._trans_table: - cost = self._trans_table["trans_cap_cost_per_mw"].values.copy() + tcc_per_mw_col = SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW + if tcc_per_mw_col in self._trans_table: + cost = self._trans_table[tcc_per_mw_col].values.copy() elif "trans_cap_cost" not in self._trans_table: scc = self._sc_capacity_col cost = self._compute_trans_cap_cost( @@ -954,11 +982,11 @@ def compute_total_lcoe( max_workers=max_workers, sc_capacity_col=scc, ) - self._trans_table["trans_cap_cost_per_mw"] = cost # $/MW + self._trans_table[tcc_per_mw_col] = cost # $/MW else: cost = self._trans_table["trans_cap_cost"].values.copy() # $ cost /= self._trans_table[self._sc_capacity_col] # $/MW - self._trans_table["trans_cap_cost_per_mw"] = cost + self._trans_table[tcc_per_mw_col] = cost cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" @@ -981,24 +1009,27 @@ def compute_total_lcoe( self._trans_table['lcot_floored_reinforcement'] = lcot_fr self._trans_table['lcoe_floored_reinforcement'] = lcoe_fr - if 'reinforcement_cost_per_mw' in self._trans_table: - logger.info("'reinforcement_cost_per_mw' column found in " - "transmission table. Adding reinforcement costs " - "to total LCOE.") + if SupplyCurveField.REINFORCEMENT_COST_PER_MW in self._trans_table: + logger.info("%s column found in transmission table. Adding " + "reinforcement costs to total LCOE.", + SupplyCurveField.REINFORCEMENT_COST_PER_MW) lcot_nr = (cost * fcr) / (cf_mean_arr * 8760) lcoe_nr = lcot_nr + resource_lcoe self._trans_table['lcot_no_reinforcement'] = lcot_nr self._trans_table['lcoe_no_reinforcement'] = lcoe_nr - r_cost = (self._trans_table['reinforcement_cost_per_mw'] - .values.copy()) + + col_name = SupplyCurveField.REINFORCEMENT_COST_PER_MW + r_cost = self._trans_table[col_name].values.copy() + self._trans_table[tcc_per_mw_col] += r_cost + r_cost *= self._trans_table[self._sc_capacity_col] # align with "mean_cf" r_cost /= self._trans_table[self._costs_capacity_col] cost += r_cost # $/MW lcot = (cost * fcr) / (cf_mean_arr * 8760) - self._trans_table['lcot'] = lcot - self._trans_table['total_lcoe'] = lcot + resource_lcoe + self._trans_table[SupplyCurveField.LCOT] = lcot + self._trans_table[SupplyCurveField.TOTAL_LCOE] = lcot + resource_lcoe if consider_friction: self._calculate_total_lcoe_friction() @@ -1009,7 +1040,7 @@ def _calculate_total_lcoe_friction(self): if SupplyCurveField.MEAN_LCOE_FRICTION in self._trans_table: lcoe_friction = ( - self._trans_table['lcot'] + self._trans_table[SupplyCurveField.LCOT] + self._trans_table[SupplyCurveField.MEAN_LCOE_FRICTION]) self._trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION] = ( lcoe_friction @@ -1106,15 +1137,15 @@ def _full_sort( avail_cap_frac=1, comp_wind_dirs=None, total_lcoe_fric=None, - sort_on="total_lcoe", + sort_on=SupplyCurveField.TOTAL_LCOE, columns=( - "trans_gid", - "trans_capacity", - "trans_type", - "trans_cap_cost_per_mw", - "dist_km", - "lcot", - "total_lcoe", + SupplyCurveField.TRANS_GID, + SupplyCurveField.TRANS_CAPACITY, + SupplyCurveField.TRANS_TYPE, + SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW, + SupplyCurveField.DIST_SPUR_KM, + SupplyCurveField.LCOT, + SupplyCurveField.TOTAL_LCOE, ), downwind=False, ): @@ -1170,22 +1201,21 @@ def _full_sort( trans_sc_gids = trans_table[SupplyCurveField.SC_GID].values.astype(int) # syntax is final_key: source_key (source from trans_table) - all_cols = {k: k for k in columns} - essentials = { - "trans_gid": "trans_gid", - "trans_capacity": "avail_cap", - "trans_type": "category", - "dist_km": "dist_km", - "trans_cap_cost_per_mw": "trans_cap_cost_per_mw", - "lcot": "lcot", - "total_lcoe": "total_lcoe", - } - all_cols.update(essentials) - - arrays = { - final_key: trans_table[source_key].values - for final_key, source_key in all_cols.items() - } + # TODO: Update this to list the uses SupplyCurveField + all_cols = [k for k in columns] + essentials = [SupplyCurveField.TRANS_GID, + SupplyCurveField.TRANS_CAPACITY, + SupplyCurveField.TRANS_TYPE, + SupplyCurveField.DIST_SPUR_KM, + SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW, + SupplyCurveField.LCOT, + SupplyCurveField.TOTAL_LCOE] + + for col in essentials: + if col not in all_cols: + all_cols.append(col) + + arrays = {col: trans_table[col].values for col in all_cols} sc_capacities = trans_table[self._sc_capacity_col].values @@ -1195,7 +1225,7 @@ def _full_sort( sc_gid = trans_sc_gids[i] if self._mask[sc_gid]: connect = trans_features.connect( - arrays["trans_gid"][i], sc_capacities[i] + arrays[SupplyCurveField.TRANS_GID][i], sc_capacities[i] ) if connect: connected += 1 @@ -1258,36 +1288,50 @@ def _check_feature_capacity(self, avail_cap_frac=1): Add the transmission connection feature capacity to the trans table if needed """ - if "avail_cap" not in self._trans_table: + if SupplyCurveField.TRANS_CAPACITY not in self._trans_table: kwargs = {"avail_cap_frac": avail_cap_frac} fc = TF.feature_capacity(self._trans_table, **kwargs) - self._trans_table = self._trans_table.merge(fc, on="trans_gid") + self._trans_table = self._trans_table.merge( + fc, on=SupplyCurveField.TRANS_GID) def _adjust_output_columns(self, columns, consider_friction): """Add extra output columns, if needed.""" - # These are essentially should-be-defaults that are not - # backwards-compatible, so have to explicitly check for them - extra_cols = ['ba_str', 'poi_lat', 'poi_lon', 'reinforcement_poi_lat', - 'reinforcement_poi_lon', SupplyCurveField.EOS_MULT, - SupplyCurveField.REG_MULT, - 'reinforcement_cost_per_mw', 'reinforcement_dist_km', - 'n_parallel_trans', SupplyCurveField.TOTAL_LCOE_FRICTION] - if not consider_friction: - extra_cols -= {SupplyCurveField.TOTAL_LCOE_FRICTION} - - extra_cols = [ - col - for col in extra_cols - if col in self._trans_table and col not in columns - ] - - return columns + extra_cols + + for col in _REQUIRED_COMPUTE_AND_OUTPUT_COLS: + if col not in columns: + columns.append(col) + + for col in _REQUIRED_OUTPUT_COLS: + if col not in self._trans_table: + self._trans_table[col] = None + if col not in columns: + columns.append(col) + + missing_cols = [col for col in columns if col not in self._trans_table] + if missing_cols: + msg = (f"The following requested columns are not found in " + f"transmission table: {missing_cols}.\nSkipping...") + logger.warning(msg) + warn(msg) + + columns = [col for col in columns if col in self._trans_table] + + fric_col = SupplyCurveField.TOTAL_LCOE_FRICTION + if consider_friction and fric_col in self._trans_table: + columns.append(fric_col) + + return sorted(columns, key=_column_sort_key) def _determine_sort_on(self, sort_on): """Determine the `sort_on` column from user input and trans table""" - if "reinforcement_cost_per_mw" in self._trans_table: + r_cost_col = SupplyCurveField.REINFORCEMENT_COST_PER_MW + found_reinforcement_costs = ( + r_cost_col in self._trans_table + and not self._trans_table[r_cost_col].isna().all() + ) + if found_reinforcement_costs: sort_on = sort_on or "lcoe_no_reinforcement" - return sort_on or "total_lcoe" + return sort_on or SupplyCurveField.TOTAL_LCOE def full_sort( self, @@ -1300,13 +1344,13 @@ def full_sort( consider_friction=True, sort_on=None, columns=( - "trans_gid", - "trans_capacity", - "trans_type", - "trans_cap_cost_per_mw", - "dist_km", - "lcot", - "total_lcoe", + SupplyCurveField.TRANS_GID, + SupplyCurveField.TRANS_CAPACITY, + SupplyCurveField.TRANS_TYPE, + SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW, + SupplyCurveField.DIST_SPUR_KM, + SupplyCurveField.LCOT, + SupplyCurveField.TOTAL_LCOE, ), wind_dirs=None, n_dirs=2, @@ -1387,8 +1431,10 @@ def full_sort( sort_on = self._determine_sort_on(sort_on) trans_table = self._trans_table.copy() - pos = trans_table["lcot"].isnull() - trans_table = trans_table.loc[~pos].sort_values([sort_on, "trans_gid"]) + pos = trans_table[SupplyCurveField.LCOT].isnull() + trans_table = trans_table.loc[~pos].sort_values( + [sort_on, SupplyCurveField.TRANS_GID] + ) total_lcoe_fric = None col_in_table = SupplyCurveField.MEAN_LCOE_FRICTION in trans_table @@ -1436,14 +1482,7 @@ def simple_sort( max_workers=None, consider_friction=True, sort_on=None, - columns=( - "trans_gid", - "trans_type", - "lcot", - "total_lcoe", - "dist_km", - "trans_cap_cost_per_mw", - ), + columns=DEFAULT_COLUMNS, wind_dirs=None, n_dirs=2, downwind=False, @@ -1482,9 +1521,8 @@ def simple_sort( will be built first, by default `None`, which will use total LCOE without any reinforcement costs as the sort value. columns : list | tuple, optional - Columns to preserve in output connections dataframe, - by default ('trans_gid', 'trans_capacity', 'trans_type', - 'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe') + Columns to preserve in output connections dataframe. + By default, :obj:`DEFAULT_COLUMNS`. wind_dirs : pandas.DataFrame | str, optional path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with the neighboring supply curve point gids and power-rose value at @@ -1512,19 +1550,16 @@ def simple_sort( max_workers=max_workers, consider_friction=consider_friction, ) - trans_table = self._trans_table.copy() + sort_on = self._determine_sort_on(sort_on) if isinstance(columns, tuple): columns = list(columns) - columns = self._adjust_output_columns(columns, consider_friction) - sort_on = self._determine_sort_on(sort_on) - connections = trans_table.sort_values([sort_on, 'trans_gid']) + trans_table = self._trans_table.copy() + connections = trans_table.sort_values( + [sort_on, SupplyCurveField.TRANS_GID]) connections = connections.groupby(SupplyCurveField.SC_GID).first() - rename = {'trans_gid': 'trans_gid', - 'category': 'trans_type'} - connections = connections.rename(columns=rename) connections = connections[columns].reset_index() supply_curve = self._sc_points.merge(connections, @@ -1553,14 +1588,7 @@ def run( transmission_costs=None, consider_friction=True, sort_on=None, - columns=( - "trans_gid", - "trans_type", - "trans_cap_cost_per_mw", - "dist_km", - "lcot", - "total_lcoe", - ), + columns=DEFAULT_COLUMNS, max_workers=None, competition=None, ): @@ -1628,8 +1656,7 @@ def run( By default ``None``. columns : list | tuple, optional Columns to preserve in output supply curve dataframe. - By default, ``('trans_gid', 'trans_type', - 'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe')``. + By default, :obj:`DEFAULT_COLUMNS`. max_workers : int, optional Number of workers to use to compute LCOT. If > 1, computation is run in parallel. If ``None``, computation @@ -1695,3 +1722,14 @@ def _format_sc_out_fpath(out_fpath): project_dir, out_fn = os.path.split(out_fpath) out_fn = out_fn.replace("supply_curve", "supply-curve") return os.path.join(project_dir, out_fn) + + +def _column_sort_key(col): + """Determine the sort order of the input column. """ + col_value = _REQUIRED_COMPUTE_AND_OUTPUT_COLS.get(col) + if col_value is None: + col_value = _REQUIRED_OUTPUT_COLS.get(col) + if col_value is None: + col_value = 1e6 + + return col_value, str(col) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 76560836f..90e3e0f59 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -146,27 +146,28 @@ class SupplyCurveField(FieldEnum): SC_POINT_ANNUAL_ENERGY_MW = "sc_point_annual_energy" MEAN_FRICTION = "mean_friction" MEAN_LCOE_FRICTION = "mean_lcoe_friction" - TOTAL_LCOE_FRICTION = "total_lcoe_friction" RAW_LCOE = "raw_lcoe" + EOS_MULT = "eos_mult" + REG_MULT = "reg_mult" + COST_BASE_OCC_USD_PER_AC_MW = "cost_base_occ_usd_per_ac_mw" + COST_SITE_OCC_USD_PER_AC_MW = "cost_site_occ_usd_per_ac_mw" + COST_BASE_FOC_USD_PER_AC_MW = "cost_base_foc_usd_per_ac_mw" + COST_SITE_FOC_USD_PER_AC_MW = "cost_site_foc_usd_per_ac_mw" + COST_BASE_VOC_USD_PER_AC_MW = "cost_base_voc_usd_per_ac_mw" + COST_SITE_VOC_USD_PER_AC_MW = "cost_site_voc_usd_per_ac_mw" + FIXED_CHARGE_RATE = "fixed_charge_rate" + + # Bespoke outputs POSSIBLE_X_COORDS = "possible_x_coords" POSSIBLE_Y_COORDS = "possible_y_coords" TURBINE_X_COORDS = "turbine_x_coords" TURBINE_Y_COORDS = "turbine_y_coords" N_TURBINES = "n_turbines" - EOS_MULT = "eos_mult" - REG_MULT = "reg_mult" INCLUDED_AREA = "included_area" INCLUDED_AREA_CAPACITY_DENSITY = "included_area_capacity_density" CONVEX_HULL_AREA = "convex_hull_area" CONVEX_HULL_CAPACITY_DENSITY = "convex_hull_capacity_density" FULL_CELL_CAPACITY_DENSITY = "full_cell_capacity_density" - COST_BASE_OCC_USD_PER_AC_MW = "cost_base_occ_usd_per_ac_mw" - COST_SITE_OCC_USD_PER_AC_MW = "cost_site_occ_usd_per_ac_mw" - COST_BASE_FOC_USD_PER_AC_MW = "cost_base_foc_usd_per_ac_mw" - COST_SITE_FOC_USD_PER_AC_MW = "cost_site_foc_usd_per_ac_mw" - COST_BASE_VOC_USD_PER_AC_MW = "cost_base_voc_usd_per_ac_mw" - COST_SITE_VOC_USD_PER_AC_MW = "cost_site_voc_usd_per_ac_mw" - FIXED_CHARGE_RATE = "fixed_charge_rate" BESPOKE_AEP = "bespoke_aep" BESPOKE_OBJECTIVE = "bespoke_objective" BESPOKE_CAPITAL_COST = "bespoke_capital_cost" @@ -174,6 +175,23 @@ class SupplyCurveField(FieldEnum): BESPOKE_VARIABLE_OPERATING_COST = "bespoke_variable_operating_cost" BESPOKE_BALANCE_OF_SYSTEM_COST = "bespoke_balance_of_system_cost" + # Transmission outputs + TRANS_GID = "trans_gid_m" + TOTAL_LCOE_FRICTION = "total_lcoe_friction_m" + TOTAL_LCOE = "total_lcoe_m" + TRANS_TYPE = "trans_type_m" + TRANS_CAPACITY = "trans_capacity_m" + DIST_SPUR_KM = "dist_km_m" # "dist_spur_km" + LCOT = "lcot_m" + TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw_m" + N_PARALLEL_TRANS = "n_parallel_trans_m" + POI_LAT = "poi_lat_m" + POI_LON = "poi_lon_m" + REINFORCEMENT_POI_LAT = "reinforcement_poi_lat_m" + REINFORCEMENT_POI_LON = "reinforcement_poi_lon_m" + REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw_m" + REINFORCEMENT_DIST_KM = "reinforcement_dist_km_m" + @classmethod def map_from_legacy(cls): """Map of legacy names to current values. @@ -204,6 +222,22 @@ class _LegacySCAliases(Enum): CAPACITY_AC_MW = "capacity" EOS_MULT = "capital_cost_multiplier" + TRANS_GID = "trans_gid" + TOTAL_LCOE_FRICTION = "total_lcoe_friction" + TOTAL_LCOE = "total_lcoe" + TRANS_TYPE = "trans_type", "category" + TRANS_CAPACITY = "trans_capacity", "avail_cap" + DIST_SPUR_KM = "dist_km" + LCOT = "lcot" + TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw" + N_PARALLEL_TRANS = "n_parallel_trans" + POI_LAT = "poi_lat" + POI_LON = "poi_lon" + REINFORCEMENT_POI_LAT = "reinforcement_poi_lat" + REINFORCEMENT_POI_LON = "reinforcement_poi_lon" + REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw" + REINFORCEMENT_DIST_KM = "reinforcement_dist_km" + class ModuleName(str, Enum): """A collection of the module names available in reV. diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index ce419a648..49066eb4a 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -13,7 +13,7 @@ from pandas.testing import assert_frame_equal from reV import TESTDATADIR -from reV.supply_curve.supply_curve import SupplyCurve +from reV.supply_curve.supply_curve import SupplyCurve, _REQUIRED_OUTPUT_COLS from reV.utilities import SupplyCurveField from reV.utilities.exceptions import SupplyCurveInputError @@ -50,13 +50,13 @@ MULTIPLIERS = pd.read_csv(path).rename(columns=LEGACY_SC_COL_MAP) SC_FULL_COLUMNS = ( - "trans_gid", - "trans_type", - "trans_capacity", - "trans_cap_cost_per_mw", - "dist_km", - "lcot", - "total_lcoe", + SupplyCurveField.TRANS_GID, + SupplyCurveField.TRANS_TYPE, + SupplyCurveField.TRANS_CAPACITY, + SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW, + SupplyCurveField.DIST_SPUR_KM, + SupplyCurveField.LCOT, + SupplyCurveField.TOTAL_LCOE, ) @@ -71,12 +71,16 @@ def baseline_verify(sc_full, fpath_baseline): baseline = baseline.rename(columns=LEGACY_SC_COL_MAP) # double check useful for when tables are changing # but lcoe should be the same - check = np.allclose(baseline["total_lcoe"], sc_full["total_lcoe"]) + check = np.allclose(baseline[SupplyCurveField.TOTAL_LCOE], + sc_full[SupplyCurveField.TOTAL_LCOE]) if not check: diff = np.abs( - baseline["total_lcoe"].values - sc_full["total_lcoe"] + baseline[SupplyCurveField.TOTAL_LCOE].values + - sc_full[SupplyCurveField.TOTAL_LCOE].values + ) + rel_diff = ( + 100 * diff / baseline[SupplyCurveField.TOTAL_LCOE].values ) - rel_diff = 100 * diff / baseline["total_lcoe"].values msg = ( "Total LCOE values differed from baseline. " "Maximum difference is {:.1f} ({:.1f}%), " @@ -162,7 +166,8 @@ def test_integrated_sc_full_friction(): sc_full = pd.read_csv(sc_full) assert SupplyCurveField.MEAN_LCOE_FRICTION in sc_full assert SupplyCurveField.TOTAL_LCOE_FRICTION in sc_full - test = sc_full[SupplyCurveField.MEAN_LCOE_FRICTION] + sc_full['lcot'] + test = (sc_full[SupplyCurveField.MEAN_LCOE_FRICTION] + + sc_full[SupplyCurveField.LCOT]) assert np.allclose(test, sc_full[SupplyCurveField.TOTAL_LCOE_FRICTION]) fpath_baseline = os.path.join( @@ -185,7 +190,7 @@ def test_integrated_sc_simple_friction(): assert SupplyCurveField.MEAN_LCOE_FRICTION in sc_simple assert SupplyCurveField.TOTAL_LCOE_FRICTION in sc_simple test = (sc_simple[SupplyCurveField.MEAN_LCOE_FRICTION] - + sc_simple['lcot']) + + sc_simple[SupplyCurveField.LCOT]) assert np.allclose(test, sc_simple[SupplyCurveField.TOTAL_LCOE_FRICTION]) @@ -226,11 +231,11 @@ def test_sc_warning1(): def test_sc_warning2(): """Run the full SC test without PCA load centers and verify warning.""" - mask = TRANS_TABLE["category"] == "PCALoadCen" + mask = TRANS_TABLE[SupplyCurveField.TRANS_TYPE] == "PCALoadCen" trans_table = TRANS_TABLE[~mask] tcosts = TRANS_COSTS_1.copy() avail_cap_frac = tcosts.pop("available_capacity", 1) - with warnings.catch_warnings(record=True) as w: + with warnings.catch_warnings(record=True) as caught_warnings: warnings.simplefilter("always") sc = SupplyCurve(SC_POINTS, trans_table, sc_features=MULTIPLIERS) with tempfile.TemporaryDirectory() as td: @@ -244,11 +249,8 @@ def test_sc_warning2(): columns=SC_FULL_COLUMNS, ) s1 = "Unconnected sc_gid" - s2 = str(w[0].message) - msg = "Warning failed! Should have Unconnected sc_gid: " "{}".format( - s2 - ) - assert s1 in s2, msg + msg = "Warning failed! Should have Unconnected sc_gid in warning!" + assert any(s1 in str(w.message) for w in caught_warnings), msg def test_parallel(): @@ -292,20 +294,24 @@ def verify_trans_cap(sc_table, trans_tables, trans_features = [] for path in trans_tables: - df = pd.read_csv(path) - trans_features.append(df[["trans_gid", "max_cap"]]) + df = pd.read_csv(path).rename(columns=LEGACY_SC_COL_MAP) + trans_features.append(df[[SupplyCurveField.TRANS_GID, "max_cap"]]) trans_features = pd.concat(trans_features) if isinstance(sc_table, str) and os.path.exists(sc_table): - sc_table = pd.read_csv(sc_table) + sc_table = pd.read_csv(sc_table).rename(columns=LEGACY_SC_COL_MAP) if "max_cap" in sc_table and "max_cap" in trans_features: sc_table = sc_table.drop("max_cap", axis=1) - test = sc_table.merge(trans_features, on='trans_gid', how='left') + test = sc_table.merge(trans_features, + on=SupplyCurveField.TRANS_GID, how='left') mask = test[cap_col] > test['max_cap'] - cols = [SupplyCurveField.SC_GID, 'trans_gid', cap_col, 'max_cap'] + cols = [SupplyCurveField.SC_GID, + SupplyCurveField.TRANS_GID, + cap_col, + 'max_cap'] msg = ("SC points connected to transmission features with " "max_cap < sc_cap:\n{}" .format(test.loc[mask, cols])) @@ -375,7 +381,8 @@ def test_substation_conns(): """ tcosts = TRANS_COSTS_1.copy() avail_cap_frac = tcosts.pop("available_capacity", 1) - drop_lines = np.where(TRANS_TABLE["category"] == "TransLine")[0] + drop_lines = np.where(TRANS_TABLE[SupplyCurveField.TRANS_TYPE] + == "TransLine")[0] drop_lines = np.random.choice(drop_lines, 10, replace=False) trans_table = TRANS_TABLE.drop(labels=drop_lines) @@ -407,12 +414,12 @@ def test_multi_parallel_trans(): """ columns = ( - "trans_gid", - "trans_type", - "n_parallel_trans", - "lcot", - "total_lcoe", - "trans_cap_cost_per_mw", + SupplyCurveField.TRANS_GID, + SupplyCurveField.TRANS_TYPE, + SupplyCurveField.N_PARALLEL_TRANS, + SupplyCurveField.LCOT, + SupplyCurveField.TOTAL_LCOE, + SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW, "max_cap", ) @@ -443,21 +450,21 @@ def test_multi_parallel_trans(): assert not (set(sc_2[SupplyCurveField.SC_POINT_GID]) - set(SC_POINTS[SupplyCurveField.SC_POINT_GID])) - assert (sc_2.n_parallel_trans > 1).any() + assert (sc_2[SupplyCurveField.N_PARALLEL_TRANS] > 1).any() - mask_2 = sc_2["n_parallel_trans"] > 1 + mask_2 = sc_2[SupplyCurveField.N_PARALLEL_TRANS] > 1 for gid in sc_2.loc[mask_2, SupplyCurveField.SC_GID]: nx_1 = sc_1.loc[(sc_1[SupplyCurveField.SC_GID] == gid), - 'n_parallel_trans'].values[0] + SupplyCurveField.N_PARALLEL_TRANS].values[0] nx_2 = sc_2.loc[(sc_2[SupplyCurveField.SC_GID] == gid), - 'n_parallel_trans'].values[0] + SupplyCurveField.N_PARALLEL_TRANS].values[0] assert nx_2 >= nx_1 if nx_1 != nx_2: lcot_1 = sc_1.loc[(sc_1[SupplyCurveField.SC_GID] == gid), - 'lcot'].values[0] + SupplyCurveField.LCOT].values[0] lcot_2 = sc_2.loc[(sc_2[SupplyCurveField.SC_GID] == gid), - 'lcot'].values[0] + SupplyCurveField.LCOT].values[0] assert lcot_2 > lcot_1 @@ -520,8 +527,10 @@ def test_least_cost_full_with_reinforcement(): sc_full_r = pd.read_csv(sc_full_r) verify_trans_cap(sc_full, trans_tables) - assert np.allclose(sc_full.trans_gid, sc_full_r.trans_gid) - assert not np.allclose(sc_full.total_lcoe, sc_full_r.total_lcoe) + assert np.allclose(sc_full[SupplyCurveField.TRANS_GID], + sc_full_r[SupplyCurveField.TRANS_GID]) + assert not np.allclose(sc_full[SupplyCurveField.TOTAL_LCOE], + sc_full_r[SupplyCurveField.TOTAL_LCOE]) # pylint: disable=no-member @@ -561,6 +570,10 @@ def test_least_cost_simple_with_reinforcement(): ) in_table = pd.read_csv(in_table) out_fp = os.path.join(td, f"costs_RI_{cap}MW.csv") + in_table["poi_lat"] = 1 + in_table["poi_lon"] = 2 + in_table["reinforcement_poi_lat"] = 3 + in_table["reinforcement_poi_lon"] = 4 in_table["reinforcement_cost_per_mw"] = 1e6 in_table["reinforcement_dist_km"] = 10 in_table.to_csv(out_fp, index=False) @@ -573,9 +586,24 @@ def test_least_cost_simple_with_reinforcement(): verify_trans_cap(sc_simple_r, trans_tables) - assert np.allclose(sc_simple.trans_gid, sc_simple_r.trans_gid) - assert not np.allclose(sc_simple.total_lcoe, - sc_simple_r.total_lcoe) + assert np.allclose(sc_simple[SupplyCurveField.TRANS_GID], + sc_simple_r[SupplyCurveField.TRANS_GID]) + assert not np.allclose(sc_simple[SupplyCurveField.TOTAL_LCOE], + sc_simple_r[SupplyCurveField.TOTAL_LCOE]) + + nan_cols = [SupplyCurveField.POI_LAT, + SupplyCurveField.POI_LON, + SupplyCurveField.REINFORCEMENT_POI_LAT, + SupplyCurveField.REINFORCEMENT_POI_LON] + for col in _REQUIRED_OUTPUT_COLS: + assert col in sc_simple + if col in nan_cols: + assert sc_simple[col].isna().all() + else: + assert np.allclose(sc_simple[col], 0) + + assert col in sc_simple_r + assert (sc_simple_r[col] > 0).all() # pylint: disable=no-member @@ -598,7 +626,7 @@ def test_least_cost_simple_with_trans_cap_cost_per_mw(r_costs): sort_on = "lcoe_no_reinforcement" in_table["reinforcement_cost_per_mw"] = t_gids[::-1] else: - sort_on = "total_lcoe" + sort_on = SupplyCurveField.TOTAL_LCOE in_table["reinforcement_cost_per_mw"] = 0 in_table["reinforcement_dist_km"] = 0 in_table["trans_cap_cost_per_mw"] = t_gids @@ -611,11 +639,12 @@ def test_least_cost_simple_with_trans_cap_cost_per_mw(r_costs): sc_simple = sc.run(out_fpath, fixed_charge_rate=0.1, simple=True, sort_on=sort_on) sc_simple = pd.read_csv(sc_simple) - assert (sc_simple["trans_gid"] == 42445).all() + assert (sc_simple[SupplyCurveField.TRANS_GID] == 42445).all() if not r_costs: lcot = 4244.5 / (sc_simple[SupplyCurveField.MEAN_CF_AC] * 8760) - assert np.allclose(lcot, sc_simple["lcot"], atol=0.001) + assert np.allclose(lcot, sc_simple[SupplyCurveField.LCOT], + atol=0.001) # pylint: disable=no-member @@ -673,14 +702,11 @@ def test_least_cost_simple_with_reinforcement_floor(): verify_trans_cap(sc_simple, trans_tables) -def test_least_cost_full_pass_through(): +@pytest.mark.parametrize("cols_exist", [True, False]) +def test_least_cost_full_pass_through(cols_exist): """ Test the full supply curve sorting passes through variables correctly """ - check_cols = {'poi_lat', 'poi_lon', 'reinforcement_poi_lat', - 'reinforcement_poi_lon', SupplyCurveField.EOS_MULT, - SupplyCurveField.REG_MULT, - 'reinforcement_cost_per_mw', 'reinforcement_dist_km'} with tempfile.TemporaryDirectory() as td: trans_tables = [] for cap in [100, 200, 400, 1000]: @@ -689,9 +715,9 @@ def test_least_cost_full_pass_through(): ) in_table = pd.read_csv(in_table) out_fp = os.path.join(td, f"costs_RI_{cap}MW.csv") - in_table["reinforcement_cost_per_mw"] = 0 - for col in check_cols: - in_table[col] = 0 + if cols_exist: + for col in _REQUIRED_OUTPUT_COLS: + in_table[col] = 0 in_table.to_csv(out_fp, index=False) trans_tables.append(out_fp) @@ -706,19 +732,19 @@ def test_least_cost_full_pass_through(): ) sc_full = pd.read_csv(sc_full) - for col in check_cols: + for col in _REQUIRED_OUTPUT_COLS: assert col in sc_full - assert np.allclose(sc_full[col], 0) + if cols_exist: + assert np.allclose(sc_full[col], 0) + else: + assert sc_full[col].isna().all() -def test_least_cost_simple_pass_through(): +@pytest.mark.parametrize("cols_exist", [True, False]) +def test_least_cost_simple_pass_through(cols_exist): """ Test the simple supply curve sorting passes through variables correctly """ - check_cols = {'poi_lat', 'poi_lon', 'reinforcement_poi_lat', - 'reinforcement_poi_lon', SupplyCurveField.EOS_MULT, - SupplyCurveField.REG_MULT, - 'reinforcement_cost_per_mw', 'reinforcement_dist_km'} with tempfile.TemporaryDirectory() as td: trans_tables = [] for cap in [100, 200, 400, 1000]: @@ -727,9 +753,9 @@ def test_least_cost_simple_pass_through(): ) in_table = pd.read_csv(in_table) out_fp = os.path.join(td, f"costs_RI_{cap}MW.csv") - in_table["reinforcement_cost_per_mw"] = 0 - for col in check_cols: - in_table[col] = 0 + if cols_exist: + for col in _REQUIRED_OUTPUT_COLS: + in_table[col] = 0 in_table.to_csv(out_fp, index=False) trans_tables.append(out_fp) @@ -738,9 +764,12 @@ def test_least_cost_simple_pass_through(): sc_simple = sc.run(out_fpath, fixed_charge_rate=0.1, simple=True) sc_simple = pd.read_csv(sc_simple) - for col in check_cols: + for col in _REQUIRED_OUTPUT_COLS: assert col in sc_simple - assert np.allclose(sc_simple[col], 0) + if cols_exist: + assert np.allclose(sc_simple[col], 0) + else: + assert sc_simple[col].isna().all() def test_least_cost_simple_with_ac_capacity_column(): @@ -790,15 +819,22 @@ def test_least_cost_simple_with_ac_capacity_column(): verify_trans_cap(sc_simple_ac_cap, trans_tables, cap_col=SupplyCurveField.CAPACITY_AC_MW) - assert np.allclose( - sc_simple["trans_cap_cost_per_mw"] * 1.02, - sc_simple_ac_cap["trans_cap_cost_per_mw"], + tcc_no_r_simple = ( + sc_simple[SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW] + - sc_simple[SupplyCurveField.REINFORCEMENT_COST_PER_MW] + ) + tcc_no_r_simple_ac_cap = ( + sc_simple_ac_cap[SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW] + - sc_simple_ac_cap[SupplyCurveField.REINFORCEMENT_COST_PER_MW] ) + assert np.allclose(tcc_no_r_simple * 1.02, tcc_no_r_simple_ac_cap) assert np.allclose( - sc_simple["reinforcement_cost_per_mw"], - sc_simple_ac_cap["reinforcement_cost_per_mw"], + sc_simple[SupplyCurveField.REINFORCEMENT_COST_PER_MW], + sc_simple_ac_cap[SupplyCurveField.REINFORCEMENT_COST_PER_MW], ) # Final reinforcement costs are slightly cheaper for AC capacity - assert np.all(sc_simple["lcot"] > sc_simple_ac_cap["lcot"]) - assert np.all(sc_simple["total_lcoe"] > sc_simple_ac_cap["total_lcoe"]) + assert np.all(sc_simple[SupplyCurveField.LCOT] + > sc_simple_ac_cap[SupplyCurveField.LCOT]) + assert np.all(sc_simple[SupplyCurveField.TOTAL_LCOE] + > sc_simple_ac_cap[SupplyCurveField.TOTAL_LCOE]) From eb0b1cd58a1f0113cfc8cd2f97855c8c2b1a8f6b Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 18:01:49 -0600 Subject: [PATCH 71/88] Fix recalc test --- tests/test_supply_curve_sc_aggregation.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_supply_curve_sc_aggregation.py b/tests/test_supply_curve_sc_aggregation.py index 99e4737f9..a924b4afe 100644 --- a/tests/test_supply_curve_sc_aggregation.py +++ b/tests/test_supply_curve_sc_aggregation.py @@ -588,8 +588,12 @@ def test_recalc_lcoe(cap_cost_scale): * np.array(annual_cf).mean() * 8760, data["variable_operating_cost"]) - assert np.allclose(summary[SupplyCurveField.MEAN_LCOE], - expected_recalc_lcoe) + if cap_cost_scale == "1": + assert np.allclose(summary[SupplyCurveField.MEAN_LCOE], + expected_recalc_lcoe) + else: + assert not np.allclose(summary[SupplyCurveField.MEAN_LCOE], + expected_recalc_lcoe) fcr = summary[SupplyCurveField.FIXED_CHARGE_RATE] cap_cost = (summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] From 4b9d4700f9740a00d0e737a9bf82f5a8a4ce7a7f Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 19:49:57 -0600 Subject: [PATCH 72/88] Fix remaining tests --- tests/test_bespoke.py | 8 ++++++-- tests/test_handlers_transmission.py | 10 ++++++++-- tests/test_hybrids.py | 4 ++-- tests/test_supply_curve_wind_dirs.py | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index c44120d96..d300fa36c 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -844,13 +844,17 @@ def test_bespoke_supply_curve(): assert len(test_ind) == 1 test_row = sc_full.iloc[test_ind] assert ( - test_row["total_lcoe"].values[0] + test_row[SupplyCurveField.TOTAL_LCOE].values[0] > inp_row[SupplyCurveField.MEAN_LCOE] ) fpath_baseline = os.path.join(TESTDATADIR, "sc_out/sc_full_lc.csv") sc_baseline = pd.read_csv(fpath_baseline) - assert np.allclose(sc_baseline["total_lcoe"], sc_full["total_lcoe"]) + sc_baseline = sc_baseline.rename( + columns=SupplyCurveField.map_from_legacy() + ) + assert np.allclose(sc_baseline[SupplyCurveField.TOTAL_LCOE], + sc_full[SupplyCurveField.TOTAL_LCOE]) @pytest.mark.parametrize("wlm", [2, 100]) diff --git a/tests/test_handlers_transmission.py b/tests/test_handlers_transmission.py index 9e3a983ba..b86f5d531 100644 --- a/tests/test_handlers_transmission.py +++ b/tests/test_handlers_transmission.py @@ -9,6 +9,7 @@ import pytest from reV import TESTDATADIR +from reV.utilities import SupplyCurveField from reV.handlers.transmission import TransmissionFeatures as TF TRANS_COSTS_1 = { @@ -82,6 +83,9 @@ def trans_table(): """Get the transmission mapping table""" path = os.path.join(TESTDATADIR, "trans_tables/ri_transmission_table.csv") trans_table = pd.read_csv(path) + trans_table = trans_table.rename( + columns=SupplyCurveField.map_from_legacy() + ) return trans_table @@ -132,7 +136,7 @@ def test_connect(trans_costs, capacity, gid, trans_table): avail_cap_frac = tcosts.pop("available_capacity") tf = TF(trans_table, avail_cap_frac=avail_cap_frac, **tcosts) - avail_cap = tf[gid].get("avail_cap", None) + avail_cap = tf[gid].get(SupplyCurveField.TRANS_CAPACITY, None) if avail_cap is not None: if avail_cap > capacity: assert tf.connect(gid, capacity, apply=False) @@ -184,7 +188,9 @@ def test_substation_load_spreading(i, trans_costs, trans_table): assert not any(missing), "New gids not in baseline: {}".format(missing) for line_id in line_gids: msg = "Bad line cap: {}".format(line_id) - assert LINE_CAPS[i][line_id] == tf[line_id]["avail_cap"], msg + expected_match = (LINE_CAPS[i][line_id] + == tf[line_id][SupplyCurveField.TRANS_CAPACITY]) + assert expected_match, msg def execute_pytest(capture="all", flags="-rapP"): diff --git a/tests/test_hybrids.py b/tests/test_hybrids.py index 61765b92c..184c64eb7 100644 --- a/tests/test_hybrids.py +++ b/tests/test_hybrids.py @@ -722,9 +722,9 @@ def test_hybrids_data_contains_col(solar_fpath, wind_fpath): """Test the 'contains_col' method of HybridsData for accuracy.""" h_data = HybridsData(solar_fpath, wind_fpath) - assert h_data.contains_col("trans_capacity") + assert h_data.contains_col(SupplyCurveField.TRANS_CAPACITY) assert h_data.contains_col("dist_mi") - assert h_data.contains_col("dist_km") + assert h_data.contains_col(SupplyCurveField.DIST_SPUR_KM) assert not h_data.contains_col("dne_col_for_test") diff --git a/tests/test_supply_curve_wind_dirs.py b/tests/test_supply_curve_wind_dirs.py index 1dff451c7..709312b89 100644 --- a/tests/test_supply_curve_wind_dirs.py +++ b/tests/test_supply_curve_wind_dirs.py @@ -77,7 +77,7 @@ def test_sc_full_wind_dirs(downwind): baseline = pd.read_csv(baseline) baseline = baseline.rename(columns=SupplyCurveField.map_from_legacy()) - assert_frame_equal(sc_out, baseline, check_dtype=False) + assert_frame_equal(sc_out[baseline.columns], baseline, check_dtype=False) @pytest.mark.parametrize('downwind', [False, True]) @@ -100,7 +100,7 @@ def test_sc_simple_wind_dirs(downwind): baseline = pd.read_csv(baseline) baseline = baseline.rename(columns=SupplyCurveField.map_from_legacy()) - assert_frame_equal(sc_out, baseline, check_dtype=False) + assert_frame_equal(sc_out[baseline.columns], baseline, check_dtype=False) def test_upwind_exclusion(): From 1d1daab3fc14a44b84b6b4dd28c5b729c9c9b969 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 19:52:50 -0600 Subject: [PATCH 73/88] Add offshore export cable outputs --- reV/supply_curve/supply_curve.py | 4 +++- reV/utilities/__init__.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index 1a7c59553..ec6f8682c 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -35,7 +35,9 @@ SupplyCurveField.LCOT: 11, SupplyCurveField.TOTAL_LCOE: 12, } -_REQUIRED_OUTPUT_COLS = {SupplyCurveField.POI_LAT: 13, +_REQUIRED_OUTPUT_COLS = {SupplyCurveField.DIST_EXPORT_KM: 4, + SupplyCurveField.EXPORT_COST_PER_MW: 8, + SupplyCurveField.POI_LAT: 13, SupplyCurveField.POI_LON: 14, SupplyCurveField.REINFORCEMENT_POI_LAT: 15, SupplyCurveField.REINFORCEMENT_POI_LON: 16, diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index 90e3e0f59..de0dca906 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -182,6 +182,8 @@ class SupplyCurveField(FieldEnum): TRANS_TYPE = "trans_type_m" TRANS_CAPACITY = "trans_capacity_m" DIST_SPUR_KM = "dist_km_m" # "dist_spur_km" + DIST_EXPORT_KM = "dist_export_km_m" + EXPORT_COST_PER_MW = "cost_export_usd_per_mw_m" LCOT = "lcot_m" TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw_m" N_PARALLEL_TRANS = "n_parallel_trans_m" @@ -228,6 +230,7 @@ class _LegacySCAliases(Enum): TRANS_TYPE = "trans_type", "category" TRANS_CAPACITY = "trans_capacity", "avail_cap" DIST_SPUR_KM = "dist_km" + DIST_EXPORT_KM = "dist_export_km" LCOT = "lcot" TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw" N_PARALLEL_TRANS = "n_parallel_trans" @@ -237,6 +240,7 @@ class _LegacySCAliases(Enum): REINFORCEMENT_POI_LON = "reinforcement_poi_lon" REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw" REINFORCEMENT_DIST_KM = "reinforcement_dist_km" + EXPORT_COST_PER_MW = "cost_export_usd_per_mw" class ModuleName(str, Enum): From 1199dc6e11fbf8e6d0b8bbc6e9f374c378339cbb Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 19:59:18 -0600 Subject: [PATCH 74/88] Add final supply curve output columns --- reV/supply_curve/supply_curve.py | 8 +++++--- reV/utilities/__init__.py | 6 +++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index ec6f8682c..d125792ad 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -36,13 +36,15 @@ SupplyCurveField.TOTAL_LCOE: 12, } _REQUIRED_OUTPUT_COLS = {SupplyCurveField.DIST_EXPORT_KM: 4, + SupplyCurveField.REINFORCEMENT_DIST_KM: 5, + SupplyCurveField.TIE_LINE_COST_PER_MW: 6, + SupplyCurveField.CONNECTION_COST_PER_MW: 7, SupplyCurveField.EXPORT_COST_PER_MW: 8, + SupplyCurveField.REINFORCEMENT_COST_PER_MW: 9, SupplyCurveField.POI_LAT: 13, SupplyCurveField.POI_LON: 14, SupplyCurveField.REINFORCEMENT_POI_LAT: 15, - SupplyCurveField.REINFORCEMENT_POI_LON: 16, - SupplyCurveField.REINFORCEMENT_COST_PER_MW: 9, - SupplyCurveField.REINFORCEMENT_DIST_KM: 5} + SupplyCurveField.REINFORCEMENT_POI_LON: 16} DEFAULT_COLUMNS = tuple(str(field) for field in chain(_REQUIRED_COMPUTE_AND_OUTPUT_COLS, _REQUIRED_OUTPUT_COLS)) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index de0dca906..f88a11839 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -183,6 +183,8 @@ class SupplyCurveField(FieldEnum): TRANS_CAPACITY = "trans_capacity_m" DIST_SPUR_KM = "dist_km_m" # "dist_spur_km" DIST_EXPORT_KM = "dist_export_km_m" + TIE_LINE_COST_PER_MW = "tie_line_cost_per_mw_m" + CONNECTION_COST_PER_MW = "connection_cost_per_mw_m" EXPORT_COST_PER_MW = "cost_export_usd_per_mw_m" LCOT = "lcot_m" TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw_m" @@ -231,6 +233,9 @@ class _LegacySCAliases(Enum): TRANS_CAPACITY = "trans_capacity", "avail_cap" DIST_SPUR_KM = "dist_km" DIST_EXPORT_KM = "dist_export_km" + TIE_LINE_COST_PER_MW = "tie_line_cost_per_mw" + CONNECTION_COST_PER_MW = "connection_cost_per_mw" + EXPORT_COST_PER_MW = "cost_export_usd_per_mw" LCOT = "lcot" TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw" N_PARALLEL_TRANS = "n_parallel_trans" @@ -240,7 +245,6 @@ class _LegacySCAliases(Enum): REINFORCEMENT_POI_LON = "reinforcement_poi_lon" REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw" REINFORCEMENT_DIST_KM = "reinforcement_dist_km" - EXPORT_COST_PER_MW = "cost_export_usd_per_mw" class ModuleName(str, Enum): From 6e4dbc2b00269bdfe439a5ba4a2a66da365d9b48 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 20:48:53 -0600 Subject: [PATCH 75/88] Linter fixes --- reV/supply_curve/supply_curve.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index d125792ad..5e820f52a 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -50,6 +50,7 @@ _REQUIRED_OUTPUT_COLS)) """Default output columns from supply chain computation (not ordered)""" + class SupplyCurve: """SupplyCurve""" @@ -1134,6 +1135,7 @@ def add_sum_cols(table, sum_cols): return table + # pylint: disable=C901 def _full_sort( self, trans_table, @@ -1205,8 +1207,7 @@ def _full_sort( trans_sc_gids = trans_table[SupplyCurveField.SC_GID].values.astype(int) # syntax is final_key: source_key (source from trans_table) - # TODO: Update this to list the uses SupplyCurveField - all_cols = [k for k in columns] + all_cols = list(columns) essentials = [SupplyCurveField.TRANS_GID, SupplyCurveField.TRANS_CAPACITY, SupplyCurveField.TRANS_TYPE, @@ -1314,7 +1315,7 @@ def _adjust_output_columns(self, columns, consider_friction): missing_cols = [col for col in columns if col not in self._trans_table] if missing_cols: msg = (f"The following requested columns are not found in " - f"transmission table: {missing_cols}.\nSkipping...") + f"transmission table: {missing_cols}.\nSkipping...") logger.warning(msg) warn(msg) From d3cd364d998a2a2adfba6e6f0bdf886f92de7931 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 20:58:07 -0600 Subject: [PATCH 76/88] Fix tests --- tests/test_supply_curve_compute.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index 49066eb4a..9b3b8abb8 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -591,11 +591,17 @@ def test_least_cost_simple_with_reinforcement(): assert not np.allclose(sc_simple[SupplyCurveField.TOTAL_LCOE], sc_simple_r[SupplyCurveField.TOTAL_LCOE]) + check_cols = [SupplyCurveField.POI_LAT, + SupplyCurveField.POI_LON, + SupplyCurveField.REINFORCEMENT_POI_LAT, + SupplyCurveField.REINFORCEMENT_POI_LON, + SupplyCurveField.REINFORCEMENT_COST_PER_MW, + SupplyCurveField.REINFORCEMENT_DIST_KM] nan_cols = [SupplyCurveField.POI_LAT, SupplyCurveField.POI_LON, SupplyCurveField.REINFORCEMENT_POI_LAT, SupplyCurveField.REINFORCEMENT_POI_LON] - for col in _REQUIRED_OUTPUT_COLS: + for col in check_cols: assert col in sc_simple if col in nan_cols: assert sc_simple[col].isna().all() @@ -605,6 +611,15 @@ def test_least_cost_simple_with_reinforcement(): assert col in sc_simple_r assert (sc_simple_r[col] > 0).all() + assert np.allclose( + sc_simple[SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW] + * 0.1 + / sc_simple[SupplyCurveField.MEAN_CF_AC] + / 8760, + sc_simple[SupplyCurveField.LCOT], + atol=0.001 + ) + # pylint: disable=no-member @pytest.mark.parametrize("r_costs", [True, False]) @@ -641,6 +656,15 @@ def test_least_cost_simple_with_trans_cap_cost_per_mw(r_costs): sc_simple = pd.read_csv(sc_simple) assert (sc_simple[SupplyCurveField.TRANS_GID] == 42445).all() + assert np.allclose( + sc_simple[SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW] + * 0.1 + / sc_simple[SupplyCurveField.MEAN_CF_AC] + / 8760, + sc_simple[SupplyCurveField.LCOT], + atol=0.001 + ) + if not r_costs: lcot = 4244.5 / (sc_simple[SupplyCurveField.MEAN_CF_AC] * 8760) assert np.allclose(lcot, sc_simple[SupplyCurveField.LCOT], From 824a0c293d2d7f5314fb4f1e53b14e49f54f1da5 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 23:12:17 -0600 Subject: [PATCH 77/88] Bump version --- reV/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reV/version.py b/reV/version.py index b657e3872..706db7cfc 100644 --- a/reV/version.py +++ b/reV/version.py @@ -2,4 +2,4 @@ reV Version number """ -__version__ = "0.8.9" +__version__ = "0.9.0" From abe0b0af78335d5124728e5a0ca2422b070343e8 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 23:33:50 -0600 Subject: [PATCH 78/88] Add mean wind speed as output --- reV/bespoke/bespoke.py | 1 + tests/test_bespoke.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 4e5b73a16..7f24e4743 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -1261,6 +1261,7 @@ def run_wind_plant_ts(self): self._outputs.update(means) + self._meta[SupplyCurveField.MEAN_RES] = self.res_df["windspeed"].mean() self._meta[SupplyCurveField.MEAN_CF_DC] = None self._meta[SupplyCurveField.MEAN_CF_AC] = None self._meta[SupplyCurveField.MEAN_LCOE] = None diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index d300fa36c..4af38ee75 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -92,6 +92,7 @@ SupplyCurveField.POSSIBLE_Y_COORDS, SupplyCurveField.N_TURBINES, SupplyCurveField.RES_GIDS, + SupplyCurveField.MEAN_RES, SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.CAPACITY_DC_MW, SupplyCurveField.MEAN_CF_AC, @@ -651,6 +652,7 @@ def test_bespoke(): assert len(f[dset]) == len(meta) assert f[dset].any() # not all zeros + assert np.allclose(meta[SupplyCurveField.MEAN_RES], f["ws_mean"]) assert np.allclose( f["annual_energy-means"] / 1000, meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] From c2cb02489e519fc39199d1bdacfdb0e5962b58c3 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Thu, 13 Jun 2024 23:37:42 -0600 Subject: [PATCH 79/88] Rename output supply curve columns --- reV/utilities/__init__.py | 147 +++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 64 deletions(-) diff --git a/reV/utilities/__init__.py b/reV/utilities/__init__.py index f88a11839..af648c072 100644 --- a/reV/utilities/__init__.py +++ b/reV/utilities/__init__.py @@ -119,36 +119,36 @@ class SupplyCurveField(FieldEnum): output (e.g. "convex_hull_area" is a bespoke-only output). """ - SC_POINT_GID = "sc_point_gid" - SOURCE_GIDS = "source_gids" SC_GID = "sc_gid" - GID_COUNTS = "gid_counts" - N_GIDS = "n_gids" - RES_GIDS = "res_gids" - GEN_GIDS = "gen_gids" - AREA_SQ_KM = "area_sq_km" LATITUDE = "latitude" LONGITUDE = "longitude" - ELEVATION = "elevation" - TIMEZONE = "timezone" - COUNTY = "county" - STATE = "state" COUNTRY = "country" - MEAN_CF_AC = "mean_cf" + STATE = "state" + COUNTY = "county" + ELEVATION = "elevation_m" + TIMEZONE = "timezone" + SC_POINT_GID = "sc_point_gid" + SC_ROW_IND = "sc_row_ind" + SC_COL_IND = "sc_col_ind" + SOURCE_GIDS = "source_gids" + RES_GIDS = "res_gids" + GEN_GIDS = "gen_gids" + GID_COUNTS = "gid_counts" + N_GIDS = "n_gids" + MEAN_RES = "resource" + MEAN_CF_AC = "capacity_factor_ac" MEAN_CF_DC = "capacity_factor_dc" - MEAN_LCOE = "mean_lcoe" - MEAN_RES = "mean_res" + MEAN_LCOE = "lcoe_site_usd_per_mwh" CAPACITY_AC_MW = "capacity_ac_mw" CAPACITY_DC_MW = "capacity_dc_mw" OFFSHORE = "offshore" - SC_ROW_IND = "sc_row_ind" - SC_COL_IND = "sc_col_ind" - SC_POINT_ANNUAL_ENERGY_MW = "sc_point_annual_energy" - MEAN_FRICTION = "mean_friction" - MEAN_LCOE_FRICTION = "mean_lcoe_friction" - RAW_LCOE = "raw_lcoe" - EOS_MULT = "eos_mult" - REG_MULT = "reg_mult" + AREA_SQ_KM = "area_developable_sq_km" + MEAN_FRICTION = "friction_site" + MEAN_LCOE_FRICTION = "lcoe_friction_usd_per_mwh" + RAW_LCOE = "lcoe_raw_usd_per_mwh" + EOS_MULT = "multiplier_cc_eos" + REG_MULT = "multiplier_cc_regional" + SC_POINT_ANNUAL_ENERGY_MW = "annual_energy_site_mwh" COST_BASE_OCC_USD_PER_AC_MW = "cost_base_occ_usd_per_ac_mw" COST_SITE_OCC_USD_PER_AC_MW = "cost_site_occ_usd_per_ac_mw" COST_BASE_FOC_USD_PER_AC_MW = "cost_base_foc_usd_per_ac_mw" @@ -163,38 +163,40 @@ class SupplyCurveField(FieldEnum): TURBINE_X_COORDS = "turbine_x_coords" TURBINE_Y_COORDS = "turbine_y_coords" N_TURBINES = "n_turbines" - INCLUDED_AREA = "included_area" - INCLUDED_AREA_CAPACITY_DENSITY = "included_area_capacity_density" - CONVEX_HULL_AREA = "convex_hull_area" - CONVEX_HULL_CAPACITY_DENSITY = "convex_hull_capacity_density" - FULL_CELL_CAPACITY_DENSITY = "full_cell_capacity_density" - BESPOKE_AEP = "bespoke_aep" - BESPOKE_OBJECTIVE = "bespoke_objective" - BESPOKE_CAPITAL_COST = "bespoke_capital_cost" - BESPOKE_FIXED_OPERATING_COST = "bespoke_fixed_operating_cost" - BESPOKE_VARIABLE_OPERATING_COST = "bespoke_variable_operating_cost" - BESPOKE_BALANCE_OF_SYSTEM_COST = "bespoke_balance_of_system_cost" + INCLUDED_AREA = "area_included_sq_km" + INCLUDED_AREA_CAPACITY_DENSITY = ( + "capacity_density_included_area_mw_per_km2" + ) + CONVEX_HULL_AREA = "area_convex_hull_sq_km" + CONVEX_HULL_CAPACITY_DENSITY = "capacity_density_convex_hull_mw_per_km2" + FULL_CELL_CAPACITY_DENSITY = "capacity_density_full_cell_mw_per_km2" + BESPOKE_AEP = "optimized_plant_aep" + BESPOKE_OBJECTIVE = "optimized_plant_objective" + BESPOKE_CAPITAL_COST = "optimized_plant_capital_cost" + BESPOKE_FIXED_OPERATING_COST = "optimized_plant_fixed_operating_cost" + BESPOKE_VARIABLE_OPERATING_COST = "optimized_plant_variable_operating_cost" + BESPOKE_BALANCE_OF_SYSTEM_COST = "optimized_plant_balance_of_system_cost" # Transmission outputs - TRANS_GID = "trans_gid_m" - TOTAL_LCOE_FRICTION = "total_lcoe_friction_m" - TOTAL_LCOE = "total_lcoe_m" - TRANS_TYPE = "trans_type_m" - TRANS_CAPACITY = "trans_capacity_m" - DIST_SPUR_KM = "dist_km_m" # "dist_spur_km" - DIST_EXPORT_KM = "dist_export_km_m" - TIE_LINE_COST_PER_MW = "tie_line_cost_per_mw_m" - CONNECTION_COST_PER_MW = "connection_cost_per_mw_m" - EXPORT_COST_PER_MW = "cost_export_usd_per_mw_m" - LCOT = "lcot_m" - TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw_m" - N_PARALLEL_TRANS = "n_parallel_trans_m" - POI_LAT = "poi_lat_m" - POI_LON = "poi_lon_m" - REINFORCEMENT_POI_LAT = "reinforcement_poi_lat_m" - REINFORCEMENT_POI_LON = "reinforcement_poi_lon_m" - REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw_m" - REINFORCEMENT_DIST_KM = "reinforcement_dist_km_m" + TRANS_GID = "trans_gid" + TRANS_TYPE = "trans_type" + TOTAL_LCOE_FRICTION = "lcoe_total_friction_usd_per_mwh" + TRANS_CAPACITY = "trans_capacity" + DIST_SPUR_KM = "dist_spur_km" + DIST_EXPORT_KM = "dist_export_km" + REINFORCEMENT_DIST_KM = "dist_reinforcement_km" + TIE_LINE_COST_PER_MW = "cost_spur_usd_per_mw" + CONNECTION_COST_PER_MW = "cost_poi_usd_per_mw" + EXPORT_COST_PER_MW = "cost_export_usd_per_mw" + REINFORCEMENT_COST_PER_MW = "cost_reinforcement_usd_per_mw" + TOTAL_TRANS_CAP_COST_PER_MW = "cost_total_trans_usd_per_mw" + LCOT = "lcot_usd_per_mwh" + TOTAL_LCOE = "lcoe_all_in_usd_per_mwh" + N_PARALLEL_TRANS = "count_num_parallel_trans" + POI_LAT = "latitude_poi" + POI_LON = "longitude_poi" + REINFORCEMENT_POI_LAT = "latitude_reinforcement_poi" + REINFORCEMENT_POI_LON = "longitude_reinforcement_poi" @classmethod def map_from_legacy(cls): @@ -223,28 +225,45 @@ class _LegacySCAliases(Enum): values where each string value represents a previously known alias. """ + ELEVATION = "elevation" + MEAN_RES = "mean_res" + MEAN_CF_AC = "mean_cf" + MEAN_LCOE = "mean_lcoe" CAPACITY_AC_MW = "capacity" - EOS_MULT = "capital_cost_multiplier" - - TRANS_GID = "trans_gid" - TOTAL_LCOE_FRICTION = "total_lcoe_friction" - TOTAL_LCOE = "total_lcoe" - TRANS_TYPE = "trans_type", "category" - TRANS_CAPACITY = "trans_capacity", "avail_cap" + AREA_SQ_KM = "area_sq_km" + MEAN_FRICTION = "mean_friction" + MEAN_LCOE_FRICTION = "mean_lcoe_friction" + RAW_LCOE = "raw_lcoe" + TRANS_TYPE = "category" + TRANS_CAPACITY = "avail_cap" DIST_SPUR_KM = "dist_km" - DIST_EXPORT_KM = "dist_export_km" + REINFORCEMENT_DIST_KM = "reinforcement_dist_km" TIE_LINE_COST_PER_MW = "tie_line_cost_per_mw" CONNECTION_COST_PER_MW = "connection_cost_per_mw" - EXPORT_COST_PER_MW = "cost_export_usd_per_mw" - LCOT = "lcot" + REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw" TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw" + LCOT = "lcot" + TOTAL_LCOE = "total_lcoe" + TOTAL_LCOE_FRICTION = "total_lcoe_friction" N_PARALLEL_TRANS = "n_parallel_trans" + EOS_MULT = "eos_mult", "capital_cost_multiplier" + REG_MULT = "reg_mult" + SC_POINT_ANNUAL_ENERGY_MW = "sc_point_annual_energy" POI_LAT = "poi_lat" POI_LON = "poi_lon" REINFORCEMENT_POI_LAT = "reinforcement_poi_lat" REINFORCEMENT_POI_LON = "reinforcement_poi_lon" - REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw" - REINFORCEMENT_DIST_KM = "reinforcement_dist_km" + BESPOKE_AEP = "bespoke_aep" + BESPOKE_OBJECTIVE = "bespoke_objective" + BESPOKE_CAPITAL_COST = "bespoke_capital_cost" + BESPOKE_FIXED_OPERATING_COST = "bespoke_fixed_operating_cost" + BESPOKE_VARIABLE_OPERATING_COST = "bespoke_variable_operating_cost" + BESPOKE_BALANCE_OF_SYSTEM_COST = "bespoke_balance_of_system_cost" + INCLUDED_AREA = "included_area" + INCLUDED_AREA_CAPACITY_DENSITY = "included_area_capacity_density" + CONVEX_HULL_AREA = "convex_hull_area" + CONVEX_HULL_CAPACITY_DENSITY = "convex_hull_capacity_density" + FULL_CELL_CAPACITY_DENSITY = "full_cell_capacity_density" class ModuleName(str, Enum): From 2370a054b3f79a91c10409433754402c840b651e Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Fri, 14 Jun 2024 00:37:17 -0600 Subject: [PATCH 80/88] Increase comparison tolerance --- tests/test_bespoke.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 4af38ee75..0e6b73a0b 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -652,7 +652,8 @@ def test_bespoke(): assert len(f[dset]) == len(meta) assert f[dset].any() # not all zeros - assert np.allclose(meta[SupplyCurveField.MEAN_RES], f["ws_mean"]) + assert np.allclose(meta[SupplyCurveField.MEAN_RES], f["ws_mean"], + atol=0.01) assert np.allclose( f["annual_energy-means"] / 1000, meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] From 3bd3106dd3edf057125afd7c79c8f3df49a3abdd Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 18:23:31 -0600 Subject: [PATCH 81/88] Don't force columns even if eos curve specified --- reV/supply_curve/sc_aggregation.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/reV/supply_curve/sc_aggregation.py b/reV/supply_curve/sc_aggregation.py index 61185e7b1..1b40b63f5 100644 --- a/reV/supply_curve/sc_aggregation.py +++ b/reV/supply_curve/sc_aggregation.py @@ -712,13 +712,6 @@ def __init__(self, excl_fpath, tm_dset, econ_fpath=None, logger.debug("Resource class bins: {}".format(self._res_class_bins)) - if self._cap_cost_scale is not None: - if self._h5_dsets is None: - self._h5_dsets = [] - - self._h5_dsets += list(BaseGen.LCOE_ARGS) - self._h5_dsets = list(set(self._h5_dsets)) - if self._power_density is None: msg = ( "Supply curve aggregation power density not specified. " From ef77bc470840ef2bcb0fa1196ba623fa8952a775 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 18:23:53 -0600 Subject: [PATCH 82/88] Scale all costs using AC capacity and capacity factor --- reV/supply_curve/supply_curve.py | 37 +++++++----------------------- tests/test_supply_curve_compute.py | 6 ++--- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index 5e820f52a..2556cf873 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -166,12 +166,6 @@ def __init__(self, sc_points, trans_table, sc_features=None, self._sc_points, trans_table, sc_capacity_col=sc_capacity_col ) self._sc_gids, self._mask = self._parse_sc_gids(self._trans_table) - self._costs_capacity_col = self._determine_cost_col( - SupplyCurveField.CAPACITY_DC_MW, SupplyCurveField.CAPACITY_AC_MW - ) - self._costs_cf_col = self._determine_cost_col( - SupplyCurveField.MEAN_CF_DC, SupplyCurveField.MEAN_CF_AC - ) def __repr__(self): msg = "{} with {} points".format(self.__class__.__name__, len(self)) @@ -191,16 +185,6 @@ def __getitem__(self, gid): return self._sc_points.iloc[i] - def _determine_cost_col(self, dc_col, ac_col): - """Determine the column used to scale costs (DC for solar runs)""" - if dc_col not in self._trans_table: - return ac_col - - if self._trans_table[dc_col].isna().all(): - return ac_col - - return dc_col - @staticmethod def _parse_sc_points(sc_points, sc_features=None): """ @@ -990,13 +974,14 @@ def compute_total_lcoe( self._trans_table[tcc_per_mw_col] = cost # $/MW else: cost = self._trans_table["trans_cap_cost"].values.copy() # $ - cost /= self._trans_table[self._sc_capacity_col] # $/MW + cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] # $/MW self._trans_table[tcc_per_mw_col] = cost - cost *= self._trans_table[self._sc_capacity_col] - # align with "mean_cf" - cost /= self._trans_table[self._costs_capacity_col] - cf_mean_arr = self._trans_table[self._costs_cf_col].values + self._trans_table[tcc_per_mw_col] = ( + self._trans_table[tcc_per_mw_col].astype("float32") + ) + cost = cost.astype("float32") + cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC].values resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE] if 'reinforcement_cost_floored_per_mw' in self._trans_table: @@ -1005,9 +990,6 @@ def compute_total_lcoe( "cost LCOE as sorting option.") fr_cost = (self._trans_table['reinforcement_cost_floored_per_mw'] .values.copy()) - fr_cost *= self._trans_table[self._sc_capacity_col] - # align with "mean_cf" - fr_cost /= self._trans_table[self._costs_capacity_col] lcot_fr = ((cost + fr_cost) * fcr) / (cf_mean_arr * 8760) lcoe_fr = lcot_fr + resource_lcoe @@ -1024,12 +1006,9 @@ def compute_total_lcoe( self._trans_table['lcoe_no_reinforcement'] = lcoe_nr col_name = SupplyCurveField.REINFORCEMENT_COST_PER_MW - r_cost = self._trans_table[col_name].values.copy() + r_cost = self._trans_table[col_name].astype("float32") + r_cost = r_cost.values.copy() self._trans_table[tcc_per_mw_col] += r_cost - - r_cost *= self._trans_table[self._sc_capacity_col] - # align with "mean_cf" - r_cost /= self._trans_table[self._costs_capacity_col] cost += r_cost # $/MW lcot = (cost * fcr) / (cf_mean_arr * 8760) diff --git a/tests/test_supply_curve_compute.py b/tests/test_supply_curve_compute.py index 9b3b8abb8..61d77ba50 100644 --- a/tests/test_supply_curve_compute.py +++ b/tests/test_supply_curve_compute.py @@ -857,8 +857,8 @@ def test_least_cost_simple_with_ac_capacity_column(): sc_simple_ac_cap[SupplyCurveField.REINFORCEMENT_COST_PER_MW], ) - # Final reinforcement costs are slightly cheaper for AC capacity + # sc_simple_ac_cap lower capacity so higher cost per unit assert np.all(sc_simple[SupplyCurveField.LCOT] - > sc_simple_ac_cap[SupplyCurveField.LCOT]) + < sc_simple_ac_cap[SupplyCurveField.LCOT]) assert np.all(sc_simple[SupplyCurveField.TOTAL_LCOE] - > sc_simple_ac_cap[SupplyCurveField.TOTAL_LCOE]) + < sc_simple_ac_cap[SupplyCurveField.TOTAL_LCOE]) From c26e0ff7730cf53c1dd1de0e1e6e281c34825ab8 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 19:32:58 -0600 Subject: [PATCH 83/88] Allow more signs in eos equation --- reV/econ/economies_of_scale.py | 3 ++- tests/test_econ_of_scale.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/reV/econ/economies_of_scale.py b/reV/econ/economies_of_scale.py index 58488e233..5db110910 100644 --- a/reV/econ/economies_of_scale.py +++ b/reV/econ/economies_of_scale.py @@ -113,7 +113,8 @@ def vars(self): """ var_names = [] if self._eqn is not None: - delimiters = ("*", "/", "+", "-", " ", "(", ")", "[", "]", ",") + delimiters = (">", "<", ">=", "<=", "==", ",", "*", "/", "+", "-", + " ", "(", ")", "[", "]") regex_pattern = "|".join(map(re.escape, delimiters)) var_names = [] for sub in re.split(regex_pattern, str(self._eqn)): diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index 3c23aab73..52e160f6d 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -221,6 +221,8 @@ def test_sc_agg_econ_scale(): eqn = ( f"2 * np.multiply(1000, {SupplyCurveField.CAPACITY_AC_MW}) ** -0.3" + f" * np.where(np.array([2, 5]) > 3)[0][0]" + f" * np.where(np.array([2, 1]) == 1)[0][0]" ) out_fp_base = os.path.join(td, "base") base = SupplyCurveAggregation( @@ -231,6 +233,8 @@ def test_sc_agg_econ_scale(): res_class_bins=RES_CLASS_BINS, data_layers=DATA_LAYERS, gids=list(np.arange(10)), + h5_dsets=["capital_cost", "fixed_operating_cost", + "fixed_charge_rate", "variable_operating_cost"], ) base.run(out_fp_base, gen_fpath=gen_temp, max_workers=1) @@ -244,6 +248,8 @@ def test_sc_agg_econ_scale(): data_layers=DATA_LAYERS, gids=list(np.arange(10)), cap_cost_scale=eqn, + h5_dsets=["capital_cost", "fixed_operating_cost", + "fixed_charge_rate", "variable_operating_cost"], ) sc.run(out_fp_sc, gen_fpath=gen_temp, max_workers=1) From 5884ead71b974cf0dce3ccb599f40d64dcf2546f Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 23:21:32 -0600 Subject: [PATCH 84/88] Pin numpy req (for short term only) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 5eb0a30d7..e9dca5920 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ NREL-gaps>=0.6.11 NREL-NRWAL>=0.0.7 NREL-PySAM~=4.1.0 NREL-rex>=0.2.85 +numpy~=1.26.4 packaging>=20.3 plotly>=4.7.1 plotting>=0.0.6 From 2650b533e5807afce341eeceff5b140ca386e97e Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 23:24:24 -0600 Subject: [PATCH 85/88] Convert to floats so that math works --- reV/supply_curve/supply_curve.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reV/supply_curve/supply_curve.py b/reV/supply_curve/supply_curve.py index 2556cf873..8c1d3f415 100644 --- a/reV/supply_curve/supply_curve.py +++ b/reV/supply_curve/supply_curve.py @@ -981,8 +981,10 @@ def compute_total_lcoe( self._trans_table[tcc_per_mw_col].astype("float32") ) cost = cost.astype("float32") - cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC].values + cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC] + cf_mean_arr = cf_mean_arr.values.astype("float32") resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE] + resource_lcoe = resource_lcoe.values.astype("float32") if 'reinforcement_cost_floored_per_mw' in self._trans_table: logger.info("'reinforcement_cost_floored_per_mw' column found in " From 23679d4994ee128c28b72c4c534383be7a4f2796 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 23:24:32 -0600 Subject: [PATCH 86/88] Fix tests --- tests/test_econ_of_scale.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_econ_of_scale.py b/tests/test_econ_of_scale.py index 52e160f6d..2186aaa0e 100644 --- a/tests/test_econ_of_scale.py +++ b/tests/test_econ_of_scale.py @@ -172,6 +172,12 @@ def test_econ_of_scale_baseline(): res_class_bins=RES_CLASS_BINS, data_layers=DATA_LAYERS, gids=list(np.arange(10)), + h5_dsets=[ + "capital_cost", + "fixed_operating_cost", + "fixed_charge_rate", + "variable_operating_cost" + ], ) base.run(out_fp_base, gen_fpath=gen_temp, max_workers=1) @@ -185,6 +191,12 @@ def test_econ_of_scale_baseline(): data_layers=DATA_LAYERS, gids=list(np.arange(10)), cap_cost_scale="1", + h5_dsets=[ + "capital_cost", + "fixed_operating_cost", + "fixed_charge_rate", + "variable_operating_cost" + ], ) sc.run(out_fp_sc, gen_fpath=gen_temp, max_workers=1) @@ -233,8 +245,12 @@ def test_sc_agg_econ_scale(): res_class_bins=RES_CLASS_BINS, data_layers=DATA_LAYERS, gids=list(np.arange(10)), - h5_dsets=["capital_cost", "fixed_operating_cost", - "fixed_charge_rate", "variable_operating_cost"], + h5_dsets=[ + "capital_cost", + "fixed_operating_cost", + "fixed_charge_rate", + "variable_operating_cost" + ], ) base.run(out_fp_base, gen_fpath=gen_temp, max_workers=1) @@ -248,8 +264,12 @@ def test_sc_agg_econ_scale(): data_layers=DATA_LAYERS, gids=list(np.arange(10)), cap_cost_scale=eqn, - h5_dsets=["capital_cost", "fixed_operating_cost", - "fixed_charge_rate", "variable_operating_cost"], + h5_dsets=[ + "capital_cost", + "fixed_operating_cost", + "fixed_charge_rate", + "variable_operating_cost" + ], ) sc.run(out_fp_sc, gen_fpath=gen_temp, max_workers=1) From 954b21e964b0b6bedc1db0879c4615c9101058cf Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 23:27:02 -0600 Subject: [PATCH 87/88] Fix temporary numpy pin for older python versions --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9dca5920..eea8e393b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ NREL-gaps>=0.6.11 NREL-NRWAL>=0.0.7 NREL-PySAM~=4.1.0 NREL-rex>=0.2.85 -numpy~=1.26.4 +numpy~=1.24.4 packaging>=20.3 plotly>=4.7.1 plotting>=0.0.6 From 86480a912364dc174338d4a6fff4f829591bc8a7 Mon Sep 17 00:00:00 2001 From: ppinchuk Date: Sun, 16 Jun 2024 23:31:25 -0600 Subject: [PATCH 88/88] Add back required meta field --- reV/bespoke/bespoke.py | 1 + tests/test_bespoke.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 7f24e4743..0c38fa9c7 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -840,6 +840,7 @@ def meta(self): self._meta = pd.DataFrame( { + "gid": self.sc_point.gid, # needed for collection SupplyCurveField.LATITUDE: self.sc_point.latitude, SupplyCurveField.LONGITUDE: self.sc_point.longitude, SupplyCurveField.COUNTRY: self.sc_point.country, diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 0e6b73a0b..9a9bb3da5 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -85,7 +85,8 @@ "(0.0975 * capital_cost + fixed_operating_cost) " "/ aep + variable_operating_cost" ) -EXPECTED_META_COLUMNS = [SupplyCurveField.SC_POINT_GID, +EXPECTED_META_COLUMNS = ["gid", # needed for H5 collection to work properly + SupplyCurveField.SC_POINT_GID, SupplyCurveField.TURBINE_X_COORDS, SupplyCurveField.TURBINE_Y_COORDS, SupplyCurveField.POSSIBLE_X_COORDS,