Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bespoke cost fixes #464

Merged
merged 5 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading