Skip to content

Commit f4b0b40

Browse files
authored
Merge pull request #464 from NREL/pp/bespoke_cost_fixes
Bespoke cost fixes
2 parents a27dc0a + a7dbdcb commit f4b0b40

File tree

5 files changed

+124
-25
lines changed

5 files changed

+124
-25
lines changed

reV/bespoke/bespoke.py

+42-16
Original file line numberDiff line numberDiff line change
@@ -1382,32 +1382,45 @@ def run_plant_optimization(self):
13821382
eos_mult = (self.plant_optimizer.capital_cost
13831383
/ self.plant_optimizer.capacity
13841384
/ baseline_cost)
1385-
reg_mult = self.sam_sys_inputs.get("capital_cost_multiplier", 1)
1385+
reg_mult_cc = self.sam_sys_inputs.get(
1386+
"capital_cost_multiplier", 1)
1387+
reg_mult_foc = self.sam_sys_inputs.get(
1388+
"fixed_operating_cost_multiplier", 1)
1389+
reg_mult_voc = self.sam_sys_inputs.get(
1390+
"variable_operating_cost_multiplier", 1)
1391+
reg_mult_bos = self.sam_sys_inputs.get(
1392+
"balance_of_system_cost_multiplier", 1)
13861393

13871394
self._meta[SupplyCurveField.EOS_MULT] = eos_mult
1388-
self._meta[SupplyCurveField.REG_MULT] = reg_mult
1395+
self._meta[SupplyCurveField.REG_MULT] = reg_mult_cc
13891396

1390-
cap_cost = (
1391-
self.plant_optimizer.capital_cost
1392-
+ self.plant_optimizer.balance_of_system_cost
1393-
)
13941397
self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = (
1395-
cap_cost / capacity_ac_mw
1398+
(self.plant_optimizer.capital_cost
1399+
+ self.plant_optimizer.balance_of_system_cost)
1400+
/ capacity_ac_mw
13961401
)
13971402
self._meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] = (
1398-
cap_cost / eos_mult / reg_mult / capacity_ac_mw
1403+
(self.plant_optimizer.capital_cost / eos_mult / reg_mult_cc
1404+
+ self.plant_optimizer.balance_of_system_cost / reg_mult_bos)
1405+
/ capacity_ac_mw
13991406
)
14001407
self._meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] = (
1401-
self.plant_optimizer.fixed_operating_cost / capacity_ac_mw
1408+
self.plant_optimizer.fixed_operating_cost
1409+
/ capacity_ac_mw
14021410
)
14031411
self._meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW] = (
1404-
self.plant_optimizer.fixed_operating_cost / capacity_ac_mw
1412+
self.plant_optimizer.fixed_operating_cost
1413+
/ reg_mult_foc
1414+
/ capacity_ac_mw
14051415
)
14061416
self._meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] = (
1407-
self.plant_optimizer.variable_operating_cost / capacity_ac_mw
1417+
self.plant_optimizer.variable_operating_cost
1418+
/ capacity_ac_mw
14081419
)
14091420
self._meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] = (
1410-
self.plant_optimizer.variable_operating_cost / capacity_ac_mw
1421+
self.plant_optimizer.variable_operating_cost
1422+
/ reg_mult_voc
1423+
/ capacity_ac_mw
14111424
)
14121425
self._meta[SupplyCurveField.FIXED_CHARGE_RATE] = (
14131426
self.plant_optimizer.fixed_charge_rate
@@ -1489,8 +1502,9 @@ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
14891502
ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
14901503
excl_dict=None, area_filter_kernel='queen', min_area=None,
14911504
resolution=64, excl_area=None, data_layers=None,
1492-
pre_extract_inclusions=False, prior_run=None, gid_map=None,
1493-
bias_correct=None, pre_load_data=False):
1505+
pre_extract_inclusions=False, eos_mult_baseline_cap_mw=200,
1506+
prior_run=None, gid_map=None, bias_correct=None,
1507+
pre_load_data=False):
14941508
"""reV bespoke analysis class.
14951509
14961510
Much like generation, ``reV`` bespoke analysis runs SAM
@@ -1855,6 +1869,13 @@ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
18551869
the `excl_dict` input. It is typically faster to compute
18561870
the inclusion mask on the fly with parallel workers.
18571871
By default, ``False``.
1872+
eos_mult_baseline_cap_mw : int | float, optional
1873+
Baseline plant capacity (MW) used to calculate economies of
1874+
scale (EOS) multiplier from the `capital_cost_function`. EOS
1875+
multiplier is calculated as the $-per-kW of the wind plant
1876+
divided by the $-per-kW of a plant with this baseline
1877+
capacity. By default, `200` (MW), which aligns the baseline
1878+
with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
18581879
prior_run : str, optional
18591880
Optional filepath to a bespoke output HDF5 file belonging to
18601881
a prior run. If specified, this module will only run the
@@ -1980,6 +2001,7 @@ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
19802001
self._ws_bins = ws_bins
19812002
self._wd_bins = wd_bins
19822003
self._data_layers = data_layers
2004+
self._eos_mult_baseline_cap_mw = eos_mult_baseline_cap_mw
19832005
self._prior_meta = self._parse_prior_run(prior_run)
19842006
self._gid_map = BespokeSinglePlant._parse_gid_map(gid_map)
19852007
self._bias_correct = Gen._parse_bc(bias_correct)
@@ -2453,8 +2475,8 @@ def run_serial(cls, excl_fpath, res_fpath, tm_dset,
24532475
area_filter_kernel='queen', min_area=None,
24542476
resolution=64, excl_area=0.0081, data_layers=None,
24552477
gids=None, exclusion_shape=None, slice_lookup=None,
2456-
prior_meta=None, gid_map=None, bias_correct=None,
2457-
pre_loaded_data=None):
2478+
eos_mult_baseline_cap_mw=200, prior_meta=None,
2479+
gid_map=None, bias_correct=None, pre_loaded_data=None):
24582480
"""
24592481
Standalone serial method to run bespoke optimization.
24602482
See BespokeWindPlants docstring for parameter description.
@@ -2520,6 +2542,7 @@ def run_serial(cls, excl_fpath, res_fpath, tm_dset,
25202542
excl_area=excl_area,
25212543
data_layers=data_layers,
25222544
exclusion_shape=exclusion_shape,
2545+
eos_mult_baseline_cap_mw=eos_mult_baseline_cap_mw,
25232546
prior_meta=prior_meta,
25242547
gid_map=gid_map,
25252548
bias_correct=bias_correct,
@@ -2612,6 +2635,7 @@ def run_parallel(self, max_workers=None):
26122635
gids=gid,
26132636
exclusion_shape=self.shape,
26142637
slice_lookup=copy.deepcopy(self.slice_lookup),
2638+
eos_mult_baseline_cap_mw=self._eos_mult_baseline_cap_mw,
26152639
prior_meta=self._get_prior_meta(gid),
26162640
gid_map=self._gid_map,
26172641
bias_correct=self._get_bc_for_gid(gid),
@@ -2676,6 +2700,7 @@ def run(self, out_fpath=None, max_workers=None):
26762700
afk = self._area_filter_kernel
26772701
wlm = self._wake_loss_multiplier
26782702
i_bc = self._get_bc_for_gid(gid)
2703+
ebc = self._eos_mult_baseline_cap_mw
26792704

26802705
si = self.run_serial(self._excl_fpath,
26812706
self._res_fpath,
@@ -2700,6 +2725,7 @@ def run(self, out_fpath=None, max_workers=None):
27002725
excl_area=self._excl_area,
27012726
data_layers=self._data_layers,
27022727
slice_lookup=slice_lookup,
2728+
eos_mult_baseline_cap_mw=ebc,
27032729
prior_meta=prior_meta,
27042730
gid_map=self._gid_map,
27052731
bias_correct=i_bc,

reV/supply_curve/cli_sc_aggregation.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,25 @@ def _preprocessor(config, out_dir):
5050
def _format_res_fpath(config):
5151
"""Format res_fpath with year, if need be. """
5252
res_fpath = config.setdefault("res_fpath", None)
53-
if isinstance(res_fpath, str) and '{}' in res_fpath:
54-
for year in range(1998, 2018):
55-
if os.path.exists(res_fpath.format(year)):
56-
break
57-
58-
config["res_fpath"] = res_fpath.format(year)
53+
if isinstance(res_fpath, str):
54+
if '{}' in res_fpath:
55+
for year in range(1950, 2100):
56+
if os.path.exists(res_fpath.format(year)):
57+
break
58+
else:
59+
msg = ("Could not find any files that match the pattern"
60+
"{!r}".format(res_fpath.format("<year>")))
61+
logger.error(msg)
62+
raise FileNotFoundError(msg)
63+
64+
res_fpath = res_fpath.format(year)
65+
66+
elif not os.path.exists(res_fpath):
67+
msg = "Could not find resource file: {!r}".format(res_fpath)
68+
logger.error(msg)
69+
raise FileNotFoundError(msg)
70+
71+
config["res_fpath"] = res_fpath
5972

6073
return config
6174

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
NREL-gaps>=0.6.11
22
NREL-NRWAL>=0.0.7
33
NREL-PySAM~=4.1.0
4-
NREL-rex>=0.2.85
4+
NREL-rex>=0.2.89
55
numpy~=1.24.4
66
packaging>=20.3
77
plotly>=4.7.1

tests/test_bespoke.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,9 @@ def test_bespoke():
595595
SiteDataField.GID: [33, 35],
596596
SiteDataField.CONFIG: ["default"] * 2,
597597
"extra_unused_data": [0, 42],
598+
"capital_cost_multiplier": [1, 2],
599+
"fixed_operating_cost_multiplier": [3, 4],
600+
"variable_operating_cost_multiplier": [5, 6]
598601
}
599602
)
600603
fully_excluded_points = pd.DataFrame(
@@ -674,6 +677,16 @@ def test_bespoke():
674677
assert f[dset].shape[1] == len(meta)
675678
assert f[dset].any() # not all zeros
676679

680+
assert not np.allclose(
681+
meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW],
682+
meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW])
683+
assert not np.allclose(
684+
meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW],
685+
meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW])
686+
assert not np.allclose(
687+
meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW],
688+
meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW])
689+
677690
fcr = meta[SupplyCurveField.FIXED_CHARGE_RATE]
678691
cap_cost = (meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW]
679692
* meta[SupplyCurveField.CAPACITY_AC_MW])
@@ -689,12 +702,15 @@ def test_bespoke():
689702
* meta[SupplyCurveField.REG_MULT]
690703
* meta[SupplyCurveField.EOS_MULT])
691704
foc = (meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW]
692-
* meta[SupplyCurveField.CAPACITY_AC_MW])
705+
* meta[SupplyCurveField.CAPACITY_AC_MW]
706+
* np.array([3, 4]))
693707
voc = (meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW]
694-
* meta[SupplyCurveField.CAPACITY_AC_MW])
708+
* meta[SupplyCurveField.CAPACITY_AC_MW]
709+
* np.array([5, 6]))
695710
lcoe_base = lcoe_fcr(fcr, cap_cost, foc, aep, voc)
696711

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

699715
out_fpath_pre = os.path.join(td, 'bespoke_out_pre.h5')
700716
bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION,

tests/test_supply_curve_sc_aggregation.py

+44
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
SupplyCurveAggregation,
2727
_warn_about_large_datasets,
2828
)
29+
from reV.supply_curve.cli_sc_aggregation import _format_res_fpath
2930
from reV.handlers.exclusions import LATITUDE
3031
from reV.utilities import ModuleName, SupplyCurveField
3132

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

667669

670+
def test_format_res_fpath():
671+
"""Test the format_res_fpath function."""
672+
assert _format_res_fpath({"test": 1}) == {"test": 1, "res_fpath": None}
673+
674+
with tempfile.TemporaryDirectory() as td:
675+
test_file = os.path.join(td, "gen.h5")
676+
config = {"res_fpath": test_file}
677+
with pytest.raises(FileNotFoundError) as error:
678+
_format_res_fpath(config)
679+
assert "Could not find resource file" in str(error)
680+
assert "gen.h5" in str(error)
681+
682+
with open(test_file, 'w'):
683+
pass
684+
685+
assert _format_res_fpath(config) == config
686+
687+
688+
def test_format_res_fpath_with_year_pattern():
689+
"""Test the format_res_fpath function with {} substitute for year."""
690+
691+
with tempfile.TemporaryDirectory() as td:
692+
tf = os.path.join(td, "gen_{}.h5")
693+
config = {"res_fpath": tf}
694+
with pytest.raises(FileNotFoundError) as error:
695+
_format_res_fpath(config)
696+
assert "Could not find any files that match the pattern" in str(error)
697+
assert "gen_<year>.h5" in str(error)
698+
699+
with open(tf.format(2012), 'w'):
700+
pass
701+
702+
config = {"res_fpath": tf}
703+
assert _format_res_fpath(config) == {"res_fpath": tf.format(2012)}
704+
705+
with open(tf.format(2010), 'w'):
706+
pass
707+
708+
config = {"res_fpath": tf}
709+
assert _format_res_fpath(config) == {"res_fpath": tf.format(2010)}
710+
711+
668712
def execute_pytest(capture="all", flags="-rapP"):
669713
"""Execute module as pytest with detailed summary report.
670714

0 commit comments

Comments
 (0)