Skip to content

Commit

Permalink
Merge pull request #464 from NREL/pp/bespoke_cost_fixes
Browse files Browse the repository at this point in the history
Bespoke cost fixes
  • Loading branch information
ppinchuk authored Aug 16, 2024
2 parents a27dc0a + a7dbdcb commit f4b0b40
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 25 deletions.
58 changes: 42 additions & 16 deletions reV/bespoke/bespoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,32 +1382,45 @@ def run_plant_optimization(self):
eos_mult = (self.plant_optimizer.capital_cost
/ self.plant_optimizer.capacity
/ baseline_cost)
reg_mult = self.sam_sys_inputs.get("capital_cost_multiplier", 1)
reg_mult_cc = self.sam_sys_inputs.get(
"capital_cost_multiplier", 1)
reg_mult_foc = self.sam_sys_inputs.get(
"fixed_operating_cost_multiplier", 1)
reg_mult_voc = self.sam_sys_inputs.get(
"variable_operating_cost_multiplier", 1)
reg_mult_bos = self.sam_sys_inputs.get(
"balance_of_system_cost_multiplier", 1)

self._meta[SupplyCurveField.EOS_MULT] = eos_mult
self._meta[SupplyCurveField.REG_MULT] = reg_mult
self._meta[SupplyCurveField.REG_MULT] = reg_mult_cc

cap_cost = (
self.plant_optimizer.capital_cost
+ self.plant_optimizer.balance_of_system_cost
)
self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = (
cap_cost / capacity_ac_mw
(self.plant_optimizer.capital_cost
+ self.plant_optimizer.balance_of_system_cost)
/ capacity_ac_mw
)
self._meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] = (
cap_cost / eos_mult / reg_mult / capacity_ac_mw
(self.plant_optimizer.capital_cost / eos_mult / reg_mult_cc
+ self.plant_optimizer.balance_of_system_cost / reg_mult_bos)
/ capacity_ac_mw
)
self._meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] = (
self.plant_optimizer.fixed_operating_cost / capacity_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.plant_optimizer.fixed_operating_cost
/ reg_mult_foc
/ capacity_ac_mw
)
self._meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] = (
self.plant_optimizer.variable_operating_cost / capacity_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.plant_optimizer.variable_operating_cost
/ reg_mult_voc
/ capacity_ac_mw
)
self._meta[SupplyCurveField.FIXED_CHARGE_RATE] = (
self.plant_optimizer.fixed_charge_rate
Expand Down Expand Up @@ -1489,8 +1502,9 @@ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
excl_dict=None, area_filter_kernel='queen', min_area=None,
resolution=64, excl_area=None, data_layers=None,
pre_extract_inclusions=False, prior_run=None, gid_map=None,
bias_correct=None, pre_load_data=False):
pre_extract_inclusions=False, eos_mult_baseline_cap_mw=200,
prior_run=None, gid_map=None, bias_correct=None,
pre_load_data=False):
"""reV bespoke analysis class.
Much like generation, ``reV`` bespoke analysis runs SAM
Expand Down Expand Up @@ -1855,6 +1869,13 @@ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
the `excl_dict` input. It is typically faster to compute
the inclusion mask on the fly with parallel workers.
By default, ``False``.
eos_mult_baseline_cap_mw : int | float, optional
Baseline plant capacity (MW) used to calculate economies of
scale (EOS) multiplier from the `capital_cost_function`. EOS
multiplier is calculated as the $-per-kW of the wind plant
divided by the $-per-kW of a plant with this baseline
capacity. By default, `200` (MW), which aligns the baseline
with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
prior_run : str, optional
Optional filepath to a bespoke output HDF5 file belonging to
a prior run. If specified, this module will only run the
Expand Down Expand Up @@ -1980,6 +2001,7 @@ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
self._ws_bins = ws_bins
self._wd_bins = wd_bins
self._data_layers = data_layers
self._eos_mult_baseline_cap_mw = eos_mult_baseline_cap_mw
self._prior_meta = self._parse_prior_run(prior_run)
self._gid_map = BespokeSinglePlant._parse_gid_map(gid_map)
self._bias_correct = Gen._parse_bc(bias_correct)
Expand Down Expand Up @@ -2453,8 +2475,8 @@ def run_serial(cls, excl_fpath, res_fpath, tm_dset,
area_filter_kernel='queen', min_area=None,
resolution=64, excl_area=0.0081, data_layers=None,
gids=None, exclusion_shape=None, slice_lookup=None,
prior_meta=None, gid_map=None, bias_correct=None,
pre_loaded_data=None):
eos_mult_baseline_cap_mw=200, prior_meta=None,
gid_map=None, bias_correct=None, pre_loaded_data=None):
"""
Standalone serial method to run bespoke optimization.
See BespokeWindPlants docstring for parameter description.
Expand Down Expand Up @@ -2520,6 +2542,7 @@ def run_serial(cls, excl_fpath, res_fpath, tm_dset,
excl_area=excl_area,
data_layers=data_layers,
exclusion_shape=exclusion_shape,
eos_mult_baseline_cap_mw=eos_mult_baseline_cap_mw,
prior_meta=prior_meta,
gid_map=gid_map,
bias_correct=bias_correct,
Expand Down Expand Up @@ -2612,6 +2635,7 @@ def run_parallel(self, max_workers=None):
gids=gid,
exclusion_shape=self.shape,
slice_lookup=copy.deepcopy(self.slice_lookup),
eos_mult_baseline_cap_mw=self._eos_mult_baseline_cap_mw,
prior_meta=self._get_prior_meta(gid),
gid_map=self._gid_map,
bias_correct=self._get_bc_for_gid(gid),
Expand Down Expand Up @@ -2676,6 +2700,7 @@ def run(self, out_fpath=None, max_workers=None):
afk = self._area_filter_kernel
wlm = self._wake_loss_multiplier
i_bc = self._get_bc_for_gid(gid)
ebc = self._eos_mult_baseline_cap_mw

si = self.run_serial(self._excl_fpath,
self._res_fpath,
Expand All @@ -2700,6 +2725,7 @@ def run(self, out_fpath=None, max_workers=None):
excl_area=self._excl_area,
data_layers=self._data_layers,
slice_lookup=slice_lookup,
eos_mult_baseline_cap_mw=ebc,
prior_meta=prior_meta,
gid_map=self._gid_map,
bias_correct=i_bc,
Expand Down
25 changes: 19 additions & 6 deletions reV/supply_curve/cli_sc_aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,25 @@ def _preprocessor(config, out_dir):
def _format_res_fpath(config):
"""Format res_fpath with year, if need be. """
res_fpath = config.setdefault("res_fpath", None)
if isinstance(res_fpath, str) and '{}' in res_fpath:
for year in range(1998, 2018):
if os.path.exists(res_fpath.format(year)):
break

config["res_fpath"] = res_fpath.format(year)
if isinstance(res_fpath, str):
if '{}' in res_fpath:
for year in range(1950, 2100):
if os.path.exists(res_fpath.format(year)):
break
else:
msg = ("Could not find any files that match the pattern"
"{!r}".format(res_fpath.format("<year>")))
logger.error(msg)
raise FileNotFoundError(msg)

res_fpath = res_fpath.format(year)

elif not os.path.exists(res_fpath):
msg = "Could not find resource file: {!r}".format(res_fpath)
logger.error(msg)
raise FileNotFoundError(msg)

config["res_fpath"] = res_fpath

return config

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
NREL-gaps>=0.6.11
NREL-NRWAL>=0.0.7
NREL-PySAM~=4.1.0
NREL-rex>=0.2.85
NREL-rex>=0.2.89
numpy~=1.24.4
packaging>=20.3
plotly>=4.7.1
Expand Down
20 changes: 18 additions & 2 deletions tests/test_bespoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,9 @@ def test_bespoke():
SiteDataField.GID: [33, 35],
SiteDataField.CONFIG: ["default"] * 2,
"extra_unused_data": [0, 42],
"capital_cost_multiplier": [1, 2],
"fixed_operating_cost_multiplier": [3, 4],
"variable_operating_cost_multiplier": [5, 6]
}
)
fully_excluded_points = pd.DataFrame(
Expand Down Expand Up @@ -674,6 +677,16 @@ def test_bespoke():
assert f[dset].shape[1] == len(meta)
assert f[dset].any() # not all zeros

assert not np.allclose(
meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW],
meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW])
assert not np.allclose(
meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW],
meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW])
assert not np.allclose(
meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW],
meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW])

fcr = meta[SupplyCurveField.FIXED_CHARGE_RATE]
cap_cost = (meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW]
* meta[SupplyCurveField.CAPACITY_AC_MW])
Expand All @@ -689,12 +702,15 @@ def test_bespoke():
* meta[SupplyCurveField.REG_MULT]
* meta[SupplyCurveField.EOS_MULT])
foc = (meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW]
* meta[SupplyCurveField.CAPACITY_AC_MW])
* meta[SupplyCurveField.CAPACITY_AC_MW]
* np.array([3, 4]))
voc = (meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW]
* meta[SupplyCurveField.CAPACITY_AC_MW])
* meta[SupplyCurveField.CAPACITY_AC_MW]
* np.array([5, 6]))
lcoe_base = lcoe_fcr(fcr, cap_cost, foc, aep, voc)

assert np.allclose(lcoe_site, lcoe_base)
assert np.allclose(meta[SupplyCurveField.REG_MULT], [1, 2])

out_fpath_pre = os.path.join(td, 'bespoke_out_pre.h5')
bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION,
Expand Down
44 changes: 44 additions & 0 deletions tests/test_supply_curve_sc_aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
SupplyCurveAggregation,
_warn_about_large_datasets,
)
from reV.supply_curve.cli_sc_aggregation import _format_res_fpath
from reV.handlers.exclusions import LATITUDE
from reV.utilities import ModuleName, SupplyCurveField


EXCL = os.path.join(TESTDATADIR, 'ri_exclusions/ri_exclusions.h5')
RES = os.path.join(TESTDATADIR, 'nsrdb/ri_100_nsrdb_2012.h5')
GEN = os.path.join(TESTDATADIR, 'gen_out/ri_my_pv_gen.h5')
Expand Down Expand Up @@ -665,6 +667,48 @@ def test_cli_basic_agg(runner, clear_loggers, tm_dset, pre_extract):
assert out_csv_fn in fn_list


def test_format_res_fpath():
"""Test the format_res_fpath function."""
assert _format_res_fpath({"test": 1}) == {"test": 1, "res_fpath": None}

with tempfile.TemporaryDirectory() as td:
test_file = os.path.join(td, "gen.h5")
config = {"res_fpath": test_file}
with pytest.raises(FileNotFoundError) as error:
_format_res_fpath(config)
assert "Could not find resource file" in str(error)
assert "gen.h5" in str(error)

with open(test_file, 'w'):
pass

assert _format_res_fpath(config) == config


def test_format_res_fpath_with_year_pattern():
"""Test the format_res_fpath function with {} substitute for year."""

with tempfile.TemporaryDirectory() as td:
tf = os.path.join(td, "gen_{}.h5")
config = {"res_fpath": tf}
with pytest.raises(FileNotFoundError) as error:
_format_res_fpath(config)
assert "Could not find any files that match the pattern" in str(error)
assert "gen_<year>.h5" in str(error)

with open(tf.format(2012), 'w'):
pass

config = {"res_fpath": tf}
assert _format_res_fpath(config) == {"res_fpath": tf.format(2012)}

with open(tf.format(2010), 'w'):
pass

config = {"res_fpath": tf}
assert _format_res_fpath(config) == {"res_fpath": tf.format(2010)}


def execute_pytest(capture="all", flags="-rapP"):
"""Execute module as pytest with detailed summary report.
Expand Down

0 comments on commit f4b0b40

Please sign in to comment.