NREL-reV 0.8.7__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/METADATA +13 -10
  2. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/RECORD +43 -43
  3. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/WHEEL +1 -1
  4. reV/SAM/SAM.py +217 -133
  5. reV/SAM/econ.py +18 -14
  6. reV/SAM/generation.py +611 -422
  7. reV/SAM/windbos.py +93 -79
  8. reV/bespoke/bespoke.py +681 -377
  9. reV/bespoke/cli_bespoke.py +2 -0
  10. reV/bespoke/place_turbines.py +187 -43
  11. reV/config/output_request.py +2 -1
  12. reV/config/project_points.py +218 -140
  13. reV/econ/econ.py +166 -114
  14. reV/econ/economies_of_scale.py +91 -45
  15. reV/generation/base.py +331 -184
  16. reV/generation/generation.py +326 -200
  17. reV/generation/output_attributes/lcoe_fcr_inputs.json +38 -3
  18. reV/handlers/__init__.py +0 -1
  19. reV/handlers/exclusions.py +16 -15
  20. reV/handlers/multi_year.py +57 -26
  21. reV/handlers/outputs.py +6 -5
  22. reV/handlers/transmission.py +44 -27
  23. reV/hybrids/hybrid_methods.py +30 -30
  24. reV/hybrids/hybrids.py +305 -189
  25. reV/nrwal/nrwal.py +262 -168
  26. reV/qa_qc/cli_qa_qc.py +14 -10
  27. reV/qa_qc/qa_qc.py +217 -119
  28. reV/qa_qc/summary.py +228 -146
  29. reV/rep_profiles/rep_profiles.py +349 -230
  30. reV/supply_curve/aggregation.py +349 -188
  31. reV/supply_curve/competitive_wind_farms.py +90 -48
  32. reV/supply_curve/exclusions.py +138 -85
  33. reV/supply_curve/extent.py +75 -50
  34. reV/supply_curve/points.py +735 -390
  35. reV/supply_curve/sc_aggregation.py +357 -248
  36. reV/supply_curve/supply_curve.py +604 -347
  37. reV/supply_curve/tech_mapping.py +144 -82
  38. reV/utilities/__init__.py +274 -16
  39. reV/utilities/pytest_utils.py +8 -4
  40. reV/version.py +1 -1
  41. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/LICENSE +0 -0
  42. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/entry_points.txt +0 -0
  43. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/top_level.txt +0 -0
reV/bespoke/bespoke.py CHANGED
@@ -2,42 +2,47 @@
2
2
  """
3
3
  reV bespoke wind plant analysis tools
4
4
  """
5
+
5
6
  # pylint: disable=anomalous-backslash-in-string
6
- from inspect import signature
7
- import time
8
- import logging
9
7
  import copy
10
- import pandas as pd
11
- import numpy as np
12
- import os
13
8
  import json
14
- import psutil
9
+ import logging
10
+ import os
11
+ import time
12
+ from concurrent.futures import as_completed
15
13
  from importlib import import_module
14
+ from inspect import signature
16
15
  from numbers import Number
17
- from concurrent.futures import as_completed
18
16
  from warnings import warn
19
17
 
18
+ import numpy as np
19
+ import pandas as pd
20
+ import psutil
21
+ from rex.joint_pd.joint_pd import JointPD
22
+ from rex.multi_year_resource import MultiYearWindResource
23
+ from rex.renewable_resource import WindResource
24
+ from rex.utilities.bc_parse_table import parse_bc_table
25
+ from rex.utilities.execution import SpawnProcessPool
26
+ from rex.utilities.loggers import create_dirs, log_mem
27
+ from rex.utilities.utilities import parse_year
28
+
20
29
  from reV.config.output_request import SAMOutputRequest
21
- from reV.generation.generation import Gen
22
- from reV.SAM.generation import WindPower, WindPowerPD
23
30
  from reV.econ.utilities import lcoe_fcr
24
- from reV.handlers.outputs import Outputs
31
+ from reV.generation.generation import Gen
25
32
  from reV.handlers.exclusions import ExclusionLayers
33
+ from reV.handlers.outputs import Outputs
34
+ from reV.SAM.generation import WindPower, WindPowerPD
35
+ from reV.supply_curve.aggregation import AggFileHandler, BaseAggregation
26
36
  from reV.supply_curve.extent import SupplyCurveExtent
27
37
  from reV.supply_curve.points import AggregationSupplyCurvePoint as AggSCPoint
28
38
  from reV.supply_curve.points import SupplyCurvePoint
29
- from reV.supply_curve.aggregation import BaseAggregation, AggFileHandler
30
- from reV.utilities.exceptions import (EmptySupplyCurvePointError,
31
- FileInputError)
32
- from reV.utilities import log_versions, ModuleName
33
-
34
- from rex.utilities.bc_parse_table import parse_bc_table
35
- from rex.joint_pd.joint_pd import JointPD
36
- from rex.renewable_resource import WindResource
37
- from rex.multi_year_resource import MultiYearWindResource
38
- from rex.utilities.loggers import log_mem, create_dirs
39
- from rex.utilities.utilities import parse_year
40
- from rex.utilities.execution import SpawnProcessPool
39
+ from reV.utilities import (
40
+ ModuleName,
41
+ ResourceMetaField,
42
+ SupplyCurveField,
43
+ log_versions,
44
+ )
45
+ from reV.utilities.exceptions import EmptySupplyCurvePointError, FileInputError
41
46
 
42
47
  logger = logging.getLogger(__name__)
43
48
 
@@ -79,34 +84,44 @@ class BespokeMultiPlantData:
79
84
  self._pre_load_data()
80
85
 
81
86
  def _pre_load_data(self):
82
- """Pre-load the resource data. """
87
+ """Pre-load the resource data."""
83
88
 
84
89
  for sc_gid, gids in self.sc_gid_to_res_gid.items():
85
90
  hh = self.sc_gid_to_hh[sc_gid]
86
91
  self.hh_to_res_gids.setdefault(hh, set()).update(gids)
87
92
 
88
- self.hh_to_res_gids = {hh: sorted(gids)
89
- for hh, gids in self.hh_to_res_gids.items()}
93
+ self.hh_to_res_gids = {
94
+ hh: sorted(gids) for hh, gids in self.hh_to_res_gids.items()
95
+ }
90
96
 
91
97
  start_time = time.time()
92
- if '*' in self.res_fpath:
98
+ if "*" in self.res_fpath:
93
99
  handler = MultiYearWindResource
94
100
  else:
95
101
  handler = WindResource
96
102
 
97
103
  with handler(self.res_fpath) as res:
98
- self._wind_dirs = {hh: res[f"winddirection_{hh}m", :, gids]
99
- for hh, gids in self.hh_to_res_gids.items()}
100
- self._wind_speeds = {hh: res[f"windspeed_{hh}m", :, gids]
101
- for hh, gids in self.hh_to_res_gids.items()}
102
- self._temps = {hh: res[f"temperature_{hh}m", :, gids]
103
- for hh, gids in self.hh_to_res_gids.items()}
104
- self._pressures = {hh: res[f"pressure_{hh}m", :, gids]
105
- for hh, gids in self.hh_to_res_gids.items()}
104
+ self._wind_dirs = {
105
+ hh: res[f"winddirection_{hh}m", :, gids]
106
+ for hh, gids in self.hh_to_res_gids.items()
107
+ }
108
+ self._wind_speeds = {
109
+ hh: res[f"windspeed_{hh}m", :, gids]
110
+ for hh, gids in self.hh_to_res_gids.items()
111
+ }
112
+ self._temps = {
113
+ hh: res[f"temperature_{hh}m", :, gids]
114
+ for hh, gids in self.hh_to_res_gids.items()
115
+ }
116
+ self._pressures = {
117
+ hh: res[f"pressure_{hh}m", :, gids]
118
+ for hh, gids in self.hh_to_res_gids.items()
119
+ }
106
120
  self._time_index = res.time_index
107
121
 
108
- logger.debug(f"Data took {(time.time() - start_time) / 60:.2f} "
109
- f"min to load")
122
+ logger.debug(
123
+ f"Data took {(time.time() - start_time) / 60:.2f} " f"min to load"
124
+ )
110
125
 
111
126
  def get_preloaded_data_for_gid(self, sc_gid):
112
127
  """Get the pre-loaded data for a single SC GID.
@@ -125,12 +140,14 @@ class BespokeMultiPlantData:
125
140
  hh = self.sc_gid_to_hh[sc_gid]
126
141
  sc_point_res_gids = sorted(self.sc_gid_to_res_gid[sc_gid])
127
142
  data_inds = np.searchsorted(self.hh_to_res_gids[hh], sc_point_res_gids)
128
- return BespokeSinglePlantData(sc_point_res_gids,
129
- self._wind_dirs[hh][:, data_inds],
130
- self._wind_speeds[hh][:, data_inds],
131
- self._temps[hh][:, data_inds],
132
- self._pressures[hh][:, data_inds],
133
- self._time_index)
143
+ return BespokeSinglePlantData(
144
+ sc_point_res_gids,
145
+ self._wind_dirs[hh][:, data_inds],
146
+ self._wind_speeds[hh][:, data_inds],
147
+ self._temps[hh][:, data_inds],
148
+ self._pressures[hh][:, data_inds],
149
+ self._time_index,
150
+ )
134
151
 
135
152
 
136
153
  class BespokeSinglePlantData:
@@ -141,8 +158,9 @@ class BespokeSinglePlantData:
141
158
  reads to a single HDF5 file.
142
159
  """
143
160
 
144
- def __init__(self, data_inds, wind_dirs, wind_speeds, temps, pressures,
145
- time_index):
161
+ def __init__(
162
+ self, data_inds, wind_dirs, wind_speeds, temps, pressures, time_index
163
+ ):
146
164
  """Initialize BespokeSinglePlantData
147
165
 
148
166
  Parameters
@@ -186,24 +204,44 @@ class BespokeSinglePlantData:
186
204
 
187
205
 
188
206
  class BespokeSinglePlant:
189
- """Framework for analyzing and optimized a wind plant layout specific to
207
+ """Framework for analyzing and optimizing a wind plant layout specific to
190
208
  the local wind resource and exclusions for a single reV supply curve point.
191
209
  """
192
210
 
193
- DEPENDENCIES = ('shapely',)
211
+ DEPENDENCIES = ("shapely",)
194
212
  OUT_ATTRS = copy.deepcopy(Gen.OUT_ATTRS)
195
213
 
196
- def __init__(self, gid, excl, res, tm_dset, sam_sys_inputs,
197
- objective_function, capital_cost_function,
198
- fixed_operating_cost_function,
199
- variable_operating_cost_function,
200
- min_spacing='5x', wake_loss_multiplier=1, ga_kwargs=None,
201
- output_request=('system_capacity', 'cf_mean'),
202
- ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
203
- excl_dict=None, inclusion_mask=None, data_layers=None,
204
- resolution=64, excl_area=None, exclusion_shape=None,
205
- eos_mult_baseline_cap_mw=200, prior_meta=None, gid_map=None,
206
- bias_correct=None, pre_loaded_data=None, close=True):
214
+ def __init__(
215
+ self,
216
+ gid,
217
+ excl,
218
+ res,
219
+ tm_dset,
220
+ sam_sys_inputs,
221
+ objective_function,
222
+ capital_cost_function,
223
+ fixed_operating_cost_function,
224
+ variable_operating_cost_function,
225
+ balance_of_system_cost_function,
226
+ min_spacing="5x",
227
+ wake_loss_multiplier=1,
228
+ ga_kwargs=None,
229
+ output_request=("system_capacity", "cf_mean"),
230
+ ws_bins=(0.0, 20.0, 5.0),
231
+ wd_bins=(0.0, 360.0, 45.0),
232
+ excl_dict=None,
233
+ inclusion_mask=None,
234
+ data_layers=None,
235
+ resolution=64,
236
+ excl_area=None,
237
+ exclusion_shape=None,
238
+ eos_mult_baseline_cap_mw=200,
239
+ prior_meta=None,
240
+ gid_map=None,
241
+ bias_correct=None,
242
+ pre_loaded_data=None,
243
+ close=True,
244
+ ):
207
245
  """
208
246
  Parameters
209
247
  ----------
@@ -225,19 +263,33 @@ class BespokeSinglePlant:
225
263
  return the objective to be minimized during layout optimization.
226
264
  Variables available are:
227
265
 
228
- - n_turbines: the number of turbines
229
- - system_capacity: wind plant capacity
230
- - aep: annual energy production
231
- - fixed_charge_rate: user input fixed_charge_rate if included
232
- as part of the sam system config.
233
- - self.wind_plant: the SAM wind plant object, through which
234
- all SAM variables can be accessed
235
- - capital_cost: plant capital cost as evaluated
266
+ - ``n_turbines``: the number of turbines
267
+ - ``system_capacity``: wind plant capacity
268
+ - ``aep``: annual energy production
269
+ - ``avg_sl_dist_to_center_m``: Average straight-line
270
+ distance to the supply curve point center from all
271
+ turbine locations (in m). Useful for computing plant
272
+ BOS costs.
273
+ - ``avg_sl_dist_to_medoid_m``: Average straight-line
274
+ distance to the medoid of all turbine locations
275
+ (in m). Useful for computing plant BOS costs.
276
+ - ``nn_conn_dist_m``: Total BOS connection distance
277
+ using nearest-neighbor connections. This variable is
278
+ only available for the
279
+ ``balance_of_system_cost_function`` equation.
280
+ - ``fixed_charge_rate``: user input fixed_charge_rate if
281
+ included as part of the sam system config.
282
+ - ``capital_cost``: plant capital cost as evaluated
236
283
  by `capital_cost_function`
237
- - fixed_operating_cost: plant fixed annual operating cost as
238
- evaluated by `fixed_operating_cost_function`
239
- - variable_operating_cost: plant variable annual operating cost
240
- as evaluated by `variable_operating_cost_function`
284
+ - ``fixed_operating_cost``: plant fixed annual operating
285
+ cost as evaluated by `fixed_operating_cost_function`
286
+ - ``variable_operating_cost``: plant variable annual
287
+ operating cost as evaluated by
288
+ `variable_operating_cost_function`
289
+ - ``balance_of_system_cost``: plant balance of system
290
+ cost as evaluated by `balance_of_system_cost_function`
291
+ - ``self.wind_plant``: the SAM wind plant object,
292
+ through which all SAM variables can be accessed
241
293
 
242
294
  capital_cost_function : str
243
295
  The plant capital cost function as a string, must return the total
@@ -250,6 +302,16 @@ class BespokeSinglePlant:
250
302
  variable_operating_cost_function : str
251
303
  The plant annual variable operating cost function as a string, must
252
304
  return the variable operating cost in $/kWh. Has access to the same
305
+ variables as the objective_function. You can set this to "0"
306
+ to effectively ignore variable operating costs.
307
+ balance_of_system_cost_function : str
308
+ The plant balance-of-system cost function as a string, must
309
+ return the variable operating cost in $. Has access to the
310
+ same variables as the objective_function. You can set this
311
+ to "0" to effectively ignore balance-of-system costs.
312
+ balance_of_system_cost_function : str
313
+ The plant balance-of-system cost function as a string, must
314
+ return the variable operating cost in $. Has access to the same
253
315
  variables as the objective_function.
254
316
  min_spacing : float | int | str
255
317
  Minimum spacing between turbines in meters. Can also be a string
@@ -352,38 +414,49 @@ class BespokeSinglePlant:
352
414
  Flag to close object file handlers on exit.
353
415
  """
354
416
 
355
- logger.debug('Initializing BespokeSinglePlant for gid {}...'
356
- .format(gid))
357
- logger.debug('Resource filepath: {}'.format(res))
358
- logger.debug('Exclusion filepath: {}'.format(excl))
359
- logger.debug('Exclusion dict: {}'.format(excl_dict))
360
- logger.debug('Bespoke objective function: {}'
361
- .format(objective_function))
362
- logger.debug('Bespoke cost function: {}'.format(objective_function))
363
- logger.debug('Bespoke wake loss multiplier: {}'
364
- .format(wake_loss_multiplier))
365
- logger.debug('Bespoke GA initialization kwargs: {}'.format(ga_kwargs))
366
- logger.debug('Bespoke EOS multiplier baseline capacity: {:,} MW'
367
- .format(eos_mult_baseline_cap_mw))
368
-
369
- if isinstance(min_spacing, str) and min_spacing.endswith('x'):
417
+ logger.debug(
418
+ "Initializing BespokeSinglePlant for gid {}...".format(gid)
419
+ )
420
+ logger.debug("Resource filepath: {}".format(res))
421
+ logger.debug("Exclusion filepath: {}".format(excl))
422
+ logger.debug("Exclusion dict: {}".format(excl_dict))
423
+ logger.debug(
424
+ "Bespoke objective function: {}".format(objective_function)
425
+ )
426
+ logger.debug("Bespoke cost function: {}".format(objective_function))
427
+ logger.debug(
428
+ "Bespoke wake loss multiplier: {}".format(wake_loss_multiplier)
429
+ )
430
+ logger.debug("Bespoke GA initialization kwargs: {}".format(ga_kwargs))
431
+ logger.debug(
432
+ "Bespoke EOS multiplier baseline capacity: {:,} MW".format(
433
+ eos_mult_baseline_cap_mw
434
+ )
435
+ )
436
+
437
+ if isinstance(min_spacing, str) and min_spacing.endswith("x"):
370
438
  rotor_diameter = sam_sys_inputs["wind_turbine_rotor_diameter"]
371
- min_spacing = float(min_spacing.strip('x')) * rotor_diameter
439
+ min_spacing = float(min_spacing.strip("x")) * rotor_diameter
372
440
 
373
441
  if not isinstance(min_spacing, (int, float)):
374
442
  try:
375
443
  min_spacing = float(min_spacing)
376
444
  except Exception as e:
377
- msg = ('min_spacing must be numeric but received: {}, {}'
378
- .format(min_spacing, type(min_spacing)))
445
+ msg = (
446
+ "min_spacing must be numeric but received: {}, {}".format(
447
+ min_spacing, type(min_spacing)
448
+ )
449
+ )
379
450
  logger.error(msg)
380
451
  raise TypeError(msg) from e
381
452
 
382
453
  self.objective_function = objective_function
383
454
  self.capital_cost_function = capital_cost_function
384
455
  self.fixed_operating_cost_function = fixed_operating_cost_function
385
- self.variable_operating_cost_function = \
456
+ self.variable_operating_cost_function = (
386
457
  variable_operating_cost_function
458
+ )
459
+ self.balance_of_system_cost_function = balance_of_system_cost_function
387
460
  self.min_spacing = min_spacing
388
461
  self.wake_loss_multiplier = wake_loss_multiplier
389
462
  self.ga_kwargs = ga_kwargs or {}
@@ -411,26 +484,33 @@ class BespokeSinglePlant:
411
484
  Handler = self.get_wind_handler(res)
412
485
  res = res if not isinstance(res, str) else Handler(res)
413
486
 
414
- self._sc_point = AggSCPoint(gid, excl, res, tm_dset,
415
- excl_dict=excl_dict,
416
- inclusion_mask=inclusion_mask,
417
- resolution=resolution,
418
- excl_area=excl_area,
419
- exclusion_shape=exclusion_shape,
420
- close=close)
487
+ self._sc_point = AggSCPoint(
488
+ gid,
489
+ excl,
490
+ res,
491
+ tm_dset,
492
+ excl_dict=excl_dict,
493
+ inclusion_mask=inclusion_mask,
494
+ resolution=resolution,
495
+ excl_area=excl_area,
496
+ exclusion_shape=exclusion_shape,
497
+ close=close,
498
+ )
421
499
 
422
500
  self._parse_output_req()
423
501
  self._data_layers = data_layers
424
502
  self._parse_prior_run()
425
503
 
426
504
  def __str__(self):
427
- s = ('BespokeSinglePlant for reV SC gid {} with resolution {}'
428
- .format(self.sc_point.gid, self.sc_point.resolution))
505
+ s = "BespokeSinglePlant for reV SC gid {} with resolution {}".format(
506
+ self.sc_point.gid, self.sc_point.resolution
507
+ )
429
508
  return s
430
509
 
431
510
  def __repr__(self):
432
- s = ('BespokeSinglePlant for reV SC gid {} with resolution {}'
433
- .format(self.sc_point.gid, self.sc_point.resolution))
511
+ s = "BespokeSinglePlant for reV SC gid {} with resolution {}".format(
512
+ self.sc_point.gid, self.sc_point.resolution
513
+ )
434
514
  return s
435
515
 
436
516
  def __enter__(self):
@@ -447,14 +527,14 @@ class BespokeSinglePlant:
447
527
  (ws_mean, *_mean) if requested.
448
528
  """
449
529
 
450
- required = ('cf_mean', 'annual_energy')
530
+ required = ("cf_mean", "annual_energy")
451
531
  for req in required:
452
532
  if req not in self._out_req:
453
533
  self._out_req.append(req)
454
534
 
455
- if 'ws_mean' in self._out_req:
456
- self._out_req.remove('ws_mean')
457
- self._outputs['ws_mean'] = self.res_df['windspeed'].mean()
535
+ if "ws_mean" in self._out_req:
536
+ self._out_req.remove("ws_mean")
537
+ self._outputs["ws_mean"] = self.res_df["windspeed"].mean()
458
538
 
459
539
  for req in copy.deepcopy(self._out_req):
460
540
  if req in self.res_df:
@@ -463,17 +543,20 @@ class BespokeSinglePlant:
463
543
  year = annual_ti.year[0]
464
544
  mask = self.res_df.index.isin(annual_ti)
465
545
  arr = self.res_df.loc[mask, req].values.flatten()
466
- self._outputs[req + f'-{year}'] = arr
546
+ self._outputs[req + f"-{year}"] = arr
467
547
 
468
- elif req.replace('_mean', '') in self.res_df:
548
+ elif req.replace("_mean", "") in self.res_df:
469
549
  self._out_req.remove(req)
470
- dset = req.replace('_mean', '')
550
+ dset = req.replace("_mean", "")
471
551
  self._outputs[req] = self.res_df[dset].mean()
472
552
 
473
- if ('lcoe_fcr' in self._out_req
474
- and 'fixed_charge_rate' not in self.original_sam_sys_inputs):
475
- msg = ('User requested "lcoe_fcr" but did not input '
476
- '"fixed_charge_rate" in the SAM system config.')
553
+ if "lcoe_fcr" in self._out_req and (
554
+ "fixed_charge_rate" not in self.original_sam_sys_inputs
555
+ ):
556
+ msg = (
557
+ 'User requested "lcoe_fcr" but did not input '
558
+ '"fixed_charge_rate" in the SAM system config.'
559
+ )
477
560
  logger.error(msg)
478
561
  raise KeyError(msg)
479
562
 
@@ -482,14 +565,18 @@ class BespokeSinglePlant:
482
565
  sure the SAM system inputs are set accordingly."""
483
566
 
484
567
  # {meta_column: sam_sys_input_key}
485
- required = {'capacity': 'system_capacity',
486
- 'turbine_x_coords': 'wind_farm_xCoordinates',
487
- 'turbine_y_coords': 'wind_farm_yCoordinates'}
568
+ required = {
569
+ SupplyCurveField.CAPACITY_AC_MW: "system_capacity",
570
+ SupplyCurveField.TURBINE_X_COORDS: "wind_farm_xCoordinates",
571
+ SupplyCurveField.TURBINE_Y_COORDS: "wind_farm_yCoordinates",
572
+ }
488
573
 
489
574
  if self._prior_meta:
490
575
  missing = [k for k in required if k not in self.meta]
491
- msg = ('Prior bespoke run meta data is missing the following '
492
- 'required columns: {}'.format(missing))
576
+ msg = (
577
+ "Prior bespoke run meta data is missing the following "
578
+ "required columns: {}".format(missing)
579
+ )
493
580
  assert not any(missing), msg
494
581
 
495
582
  for meta_col, sam_sys_key in required.items():
@@ -497,7 +584,7 @@ class BespokeSinglePlant:
497
584
  self._sam_sys_inputs[sam_sys_key] = prior_value
498
585
 
499
586
  # convert reV supply curve cap in MW to SAM capacity in kW
500
- self._sam_sys_inputs['system_capacity'] *= 1e3
587
+ self._sam_sys_inputs["system_capacity"] *= 1e3
501
588
 
502
589
  @staticmethod
503
590
  def _parse_gid_map(gid_map):
@@ -522,15 +609,22 @@ class BespokeSinglePlant:
522
609
  """
523
610
 
524
611
  if isinstance(gid_map, str):
525
- if gid_map.endswith('.csv'):
526
- gid_map = pd.read_csv(gid_map).to_dict()
527
- assert 'gid' in gid_map, 'Need "gid" in gid_map column'
528
- assert 'gid_map' in gid_map, 'Need "gid_map" in gid_map column'
529
- gid_map = {gid_map['gid'][i]: gid_map['gid_map'][i]
530
- for i in gid_map['gid'].keys()}
531
-
532
- elif gid_map.endswith('.json'):
533
- with open(gid_map, 'r') as f:
612
+ if gid_map.endswith(".csv"):
613
+ gid_map = (
614
+ pd.read_csv(gid_map)
615
+ .rename(SupplyCurveField.map_to(ResourceMetaField), axis=1)
616
+ .to_dict()
617
+ )
618
+ err_msg = f"Need {ResourceMetaField.GID} in gid_map column"
619
+ assert ResourceMetaField.GID in gid_map, err_msg
620
+ assert "gid_map" in gid_map, 'Need "gid_map" in gid_map column'
621
+ gid_map = {
622
+ gid_map[ResourceMetaField.GID][i]: gid_map["gid_map"][i]
623
+ for i in gid_map[ResourceMetaField.GID]
624
+ }
625
+
626
+ elif gid_map.endswith(".json"):
627
+ with open(gid_map) as f:
534
628
  gid_map = json.load(f)
535
629
 
536
630
  return gid_map
@@ -563,19 +657,23 @@ class BespokeSinglePlant:
563
657
  Bias corrected windspeed data in same shape as input
564
658
  """
565
659
 
566
- if self._bias_correct is not None and dset.startswith('windspeed_'):
567
-
660
+ if self._bias_correct is not None and dset.startswith("windspeed_"):
568
661
  out = parse_bc_table(self._bias_correct, h5_gids)
569
662
  bc_fun, bc_fun_kwargs, bool_bc = out
570
663
 
571
664
  if bool_bc.any():
572
- logger.debug('Bias correcting windspeed with function {} '
573
- 'for h5 gids: {}'.format(bc_fun, h5_gids))
665
+ logger.debug(
666
+ "Bias correcting windspeed with function {} "
667
+ "for h5 gids: {}".format(bc_fun, h5_gids)
668
+ )
574
669
 
575
- bc_fun_kwargs['ws'] = ws[:, bool_bc]
670
+ bc_fun_kwargs["ws"] = ws[:, bool_bc]
576
671
  sig = signature(bc_fun)
577
- bc_fun_kwargs = {k: v for k, v in bc_fun_kwargs.items()
578
- if k in sig.parameters}
672
+ bc_fun_kwargs = {
673
+ k: v
674
+ for k, v in bc_fun_kwargs.items()
675
+ if k in sig.parameters
676
+ }
579
677
 
580
678
  ws[:, bool_bc] = bc_fun(**bc_fun_kwargs)
581
679
 
@@ -631,7 +729,7 @@ class BespokeSinglePlant:
631
729
  of degrees from north.
632
730
  """
633
731
 
634
- dset = f'winddirection_{self.hub_height}m'
732
+ dset = f"winddirection_{self.hub_height}m"
635
733
  gids = self.sc_point.h5_gid_set
636
734
  h5_gids = copy.deepcopy(gids)
637
735
  if self._gid_map is not None:
@@ -736,31 +834,31 @@ class BespokeSinglePlant:
736
834
  """
737
835
  if self._meta is None:
738
836
  res_gids = json.dumps([int(g) for g in self.sc_point.h5_gid_set])
739
- gid_counts = json.dumps([float(np.round(n, 1))
740
- for n in self.sc_point.gid_counts])
741
-
742
- with SupplyCurveExtent(self.sc_point._excl_fpath,
743
- resolution=self.sc_point.resolution) as sc:
744
- row_ind, col_ind = sc.get_sc_row_col_ind(self.sc_point.gid)
837
+ gid_counts = json.dumps(
838
+ [float(np.round(n, 1)) for n in self.sc_point.gid_counts]
839
+ )
745
840
 
746
841
  self._meta = pd.DataFrame(
747
- {'sc_point_gid': self.sc_point.gid,
748
- 'sc_row_ind': row_ind,
749
- 'sc_col_ind': col_ind,
750
- 'gid': self.sc_point.gid,
751
- 'latitude': self.sc_point.latitude,
752
- 'longitude': self.sc_point.longitude,
753
- 'timezone': self.sc_point.timezone,
754
- 'country': self.sc_point.country,
755
- 'state': self.sc_point.state,
756
- 'county': self.sc_point.county,
757
- 'elevation': self.sc_point.elevation,
758
- 'offshore': self.sc_point.offshore,
759
- 'res_gids': res_gids,
760
- 'gid_counts': gid_counts,
761
- 'n_gids': self.sc_point.n_gids,
762
- 'area_sq_km': self.sc_point.area,
763
- }, index=[self.sc_point.gid])
842
+ {
843
+ "gid": self.sc_point.gid, # needed for collection
844
+ SupplyCurveField.LATITUDE: self.sc_point.latitude,
845
+ SupplyCurveField.LONGITUDE: self.sc_point.longitude,
846
+ SupplyCurveField.COUNTRY: self.sc_point.country,
847
+ SupplyCurveField.STATE: self.sc_point.state,
848
+ SupplyCurveField.COUNTY: self.sc_point.county,
849
+ SupplyCurveField.ELEVATION: self.sc_point.elevation,
850
+ SupplyCurveField.TIMEZONE: self.sc_point.timezone,
851
+ SupplyCurveField.SC_POINT_GID: self.sc_point.sc_point_gid,
852
+ SupplyCurveField.SC_ROW_IND: self.sc_point.sc_row_ind,
853
+ SupplyCurveField.SC_COL_IND: self.sc_point.sc_col_ind,
854
+ SupplyCurveField.RES_GIDS: res_gids,
855
+ SupplyCurveField.GID_COUNTS: gid_counts,
856
+ SupplyCurveField.N_GIDS: self.sc_point.n_gids,
857
+ SupplyCurveField.OFFSHORE: self.sc_point.offshore,
858
+ SupplyCurveField.AREA_SQ_KM: self.sc_point.area,
859
+ },
860
+ index=[self.sc_point.gid],
861
+ )
764
862
 
765
863
  return self._meta
766
864
 
@@ -772,7 +870,7 @@ class BespokeSinglePlant:
772
870
  -------
773
871
  int
774
872
  """
775
- return int(self.sam_sys_inputs['wind_turbine_hub_ht'])
873
+ return int(self.sam_sys_inputs["wind_turbine_hub_ht"])
776
874
 
777
875
  @property
778
876
  def res_df(self):
@@ -792,21 +890,26 @@ class BespokeSinglePlant:
792
890
  ti = self._pre_loaded_data.time_index
793
891
 
794
892
  wd = self.get_weighted_res_dir()
795
- ws = self.get_weighted_res_ts(f'windspeed_{self.hub_height}m')
796
- temp = self.get_weighted_res_ts(f'temperature_{self.hub_height}m')
797
- pres = self.get_weighted_res_ts(f'pressure_{self.hub_height}m')
893
+ ws = self.get_weighted_res_ts(f"windspeed_{self.hub_height}m")
894
+ temp = self.get_weighted_res_ts(f"temperature_{self.hub_height}m")
895
+ pres = self.get_weighted_res_ts(f"pressure_{self.hub_height}m")
798
896
 
799
897
  # convert mbar to atm
800
898
  if np.nanmax(pres) > 1000:
801
899
  pres *= 9.86923e-6
802
900
 
803
- self._res_df = pd.DataFrame({'temperature': temp,
804
- 'pressure': pres,
805
- 'windspeed': ws,
806
- 'winddirection': wd}, index=ti)
807
-
808
- if 'time_index_step' in self.original_sam_sys_inputs:
809
- ti_step = self.original_sam_sys_inputs['time_index_step']
901
+ self._res_df = pd.DataFrame(
902
+ {
903
+ "temperature": temp,
904
+ "pressure": pres,
905
+ "windspeed": ws,
906
+ "winddirection": wd,
907
+ },
908
+ index=ti,
909
+ )
910
+
911
+ if "time_index_step" in self.original_sam_sys_inputs:
912
+ ti_step = self.original_sam_sys_inputs["time_index_step"]
810
913
  self._res_df = self._res_df.iloc[::ti_step]
811
914
 
812
915
  return self._res_df
@@ -857,9 +960,11 @@ class BespokeSinglePlant:
857
960
  ws_bins = JointPD._make_bins(*self._ws_bins)
858
961
  wd_bins = JointPD._make_bins(*self._wd_bins)
859
962
 
860
- hist_out = np.histogram2d(self.res_df['windspeed'],
861
- self.res_df['winddirection'],
862
- bins=(ws_bins, wd_bins))
963
+ hist_out = np.histogram2d(
964
+ self.res_df["windspeed"],
965
+ self.res_df["winddirection"],
966
+ bins=(ws_bins, wd_bins),
967
+ )
863
968
  self._wind_dist, self._ws_edges, self._wd_edges = hist_out
864
969
  self._wind_dist /= self._wind_dist.sum()
865
970
 
@@ -880,19 +985,20 @@ class BespokeSinglePlant:
880
985
  res_df = self.res_df[(self.res_df.index.year == year)]
881
986
  sam_inputs = copy.deepcopy(self.sam_sys_inputs)
882
987
 
883
- if 'lcoe_fcr' in self._out_req:
988
+ if "lcoe_fcr" in self._out_req:
884
989
  lcoe_kwargs = self.get_lcoe_kwargs()
885
990
  sam_inputs.update(lcoe_kwargs)
886
991
 
887
- i_wp = WindPower(res_df, self.meta, sam_inputs,
888
- output_request=self._out_req)
992
+ i_wp = WindPower(
993
+ res_df, self.meta, sam_inputs, output_request=self._out_req
994
+ )
889
995
  wind_plant_ts[year] = i_wp
890
996
 
891
997
  return wind_plant_ts
892
998
 
893
999
  @property
894
1000
  def wind_plant_pd(self):
895
- """reV WindPowerPD compute object for plant layout optimization based
1001
+ """ReV WindPowerPD compute object for plant layout optimization based
896
1002
  on wind joint probability distribution
897
1003
 
898
1004
  Returns
@@ -902,14 +1008,19 @@ class BespokeSinglePlant:
902
1008
 
903
1009
  if self._wind_plant_pd is None:
904
1010
  wind_dist, ws_edges, wd_edges = self.wind_dist
905
- self._wind_plant_pd = WindPowerPD(ws_edges, wd_edges, wind_dist,
906
- self.meta, self.sam_sys_inputs,
907
- output_request=self._out_req)
1011
+ self._wind_plant_pd = WindPowerPD(
1012
+ ws_edges,
1013
+ wd_edges,
1014
+ wind_dist,
1015
+ self.meta,
1016
+ self.sam_sys_inputs,
1017
+ output_request=self._out_req,
1018
+ )
908
1019
  return self._wind_plant_pd
909
1020
 
910
1021
  @property
911
1022
  def wind_plant_ts(self):
912
- """reV WindPower compute object(s) based on wind resource timeseries
1023
+ """ReV WindPower compute object(s) based on wind resource timeseries
913
1024
  data keyed by year
914
1025
 
915
1026
  Returns
@@ -929,16 +1040,19 @@ class BespokeSinglePlant:
929
1040
  if self._plant_optm is None:
930
1041
  # put import here to delay breaking due to special dependencies
931
1042
  from reV.bespoke.place_turbines import PlaceTurbines
1043
+
932
1044
  self._plant_optm = PlaceTurbines(
933
1045
  self.wind_plant_pd,
934
1046
  self.objective_function,
935
1047
  self.capital_cost_function,
936
1048
  self.fixed_operating_cost_function,
937
1049
  self.variable_operating_cost_function,
1050
+ self.balance_of_system_cost_function,
938
1051
  self.include_mask,
939
1052
  self.pixel_side_length,
940
1053
  self.min_spacing,
941
- self.wake_loss_multiplier)
1054
+ self.wake_loss_multiplier,
1055
+ )
942
1056
 
943
1057
  return self._plant_optm
944
1058
 
@@ -946,22 +1060,24 @@ class BespokeSinglePlant:
946
1060
  """Recalculate the multi-year mean LCOE based on the multi-year mean
947
1061
  annual energy production (AEP)"""
948
1062
 
949
- if 'lcoe_fcr-means' in self.outputs:
1063
+ if "lcoe_fcr-means" in self.outputs:
950
1064
  lcoe_kwargs = self.get_lcoe_kwargs()
951
1065
 
952
- logger.debug('Recalulating multi-year mean LCOE using '
953
- 'multi-year mean AEP.')
1066
+ logger.debug(
1067
+ "Recalulating multi-year mean LCOE using "
1068
+ "multi-year mean AEP."
1069
+ )
954
1070
 
955
1071
  fcr = lcoe_kwargs['fixed_charge_rate']
956
- cap_cost = lcoe_kwargs['capital_cost']
1072
+ cc = lcoe_kwargs['capital_cost']
957
1073
  foc = lcoe_kwargs['fixed_operating_cost']
958
1074
  voc = lcoe_kwargs['variable_operating_cost']
959
1075
  aep = self.outputs['annual_energy-means']
960
1076
 
961
- my_mean_lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc)
1077
+ my_mean_lcoe = lcoe_fcr(fcr, cc, foc, aep, voc)
962
1078
 
963
- self._outputs['lcoe_fcr-means'] = my_mean_lcoe
964
- self._meta['mean_lcoe'] = my_mean_lcoe
1079
+ self._outputs["lcoe_fcr-means"] = my_mean_lcoe
1080
+ self._meta[SupplyCurveField.MEAN_LCOE] = my_mean_lcoe
965
1081
 
966
1082
  def get_lcoe_kwargs(self):
967
1083
  """Get a namespace of arguments for calculating LCOE based on the
@@ -974,16 +1090,28 @@ class BespokeSinglePlant:
974
1090
  sam_sys_inputs, normalized to the original system_capacity, and
975
1091
  updated based on the bespoke optimized system_capacity, includes
976
1092
  fixed_charge_rate, system_capacity (kW), capital_cost ($),
977
- fixed_operating_cos ($), variable_operating_cost ($/kWh)
978
- Data source priority: outputs, plant_optimizer,
979
- original_sam_sys_inputs, meta
1093
+ fixed_operating_cos ($), variable_operating_cost ($/kWh),
1094
+ balance_of_system_cost ($). Data source priority: outputs,
1095
+ plant_optimizer, original_sam_sys_inputs, meta
980
1096
  """
981
1097
 
982
- kwargs_list = ['fixed_charge_rate', 'system_capacity', 'capital_cost',
983
- 'fixed_operating_cost', 'variable_operating_cost']
1098
+ kwargs_map = {
1099
+ "fixed_charge_rate": SupplyCurveField.FIXED_CHARGE_RATE,
1100
+ "system_capacity": SupplyCurveField.CAPACITY_AC_MW,
1101
+ "capital_cost": SupplyCurveField.BESPOKE_CAPITAL_COST,
1102
+ "fixed_operating_cost": (
1103
+ SupplyCurveField.BESPOKE_FIXED_OPERATING_COST
1104
+ ),
1105
+ "variable_operating_cost": (
1106
+ SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST
1107
+ ),
1108
+ "balance_of_system_cost": (
1109
+ SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST
1110
+ ),
1111
+ }
984
1112
  lcoe_kwargs = {}
985
1113
 
986
- for kwarg in kwargs_list:
1114
+ for kwarg, meta_field in kwargs_map.items():
987
1115
  if kwarg in self.outputs:
988
1116
  lcoe_kwargs[kwarg] = self.outputs[kwarg]
989
1117
  elif getattr(self.plant_optimizer, kwarg, None) is not None:
@@ -993,18 +1121,25 @@ class BespokeSinglePlant:
993
1121
  elif kwarg in self.meta:
994
1122
  value = float(self.meta[kwarg].values[0])
995
1123
  lcoe_kwargs[kwarg] = value
1124
+ elif meta_field in self.meta:
1125
+ value = float(self.meta[meta_field].values[0])
1126
+ if meta_field == SupplyCurveField.CAPACITY_AC_MW:
1127
+ value *= 1000 # MW to kW
1128
+ lcoe_kwargs[kwarg] = value
996
1129
 
997
- for k, v in lcoe_kwargs.items():
998
- self._meta[k] = v
999
-
1000
- missing = [k for k in kwargs_list if k not in lcoe_kwargs]
1130
+ missing = [k for k in kwargs_map if k not in lcoe_kwargs]
1001
1131
  if any(missing):
1002
- msg = ('Could not find these LCOE kwargs in outputs, '
1003
- 'plant_optimizer, original_sam_sys_inputs, or meta: {}'
1004
- .format(missing))
1132
+ msg = (
1133
+ "Could not find these LCOE kwargs in outputs, "
1134
+ "plant_optimizer, original_sam_sys_inputs, or meta: {}".format(
1135
+ missing
1136
+ )
1137
+ )
1005
1138
  logger.error(msg)
1006
1139
  raise KeyError(msg)
1007
1140
 
1141
+ bos = lcoe_kwargs.pop("balance_of_system_cost")
1142
+ lcoe_kwargs["capital_cost"] = lcoe_kwargs["capital_cost"] + bos
1008
1143
  return lcoe_kwargs
1009
1144
 
1010
1145
  @staticmethod
@@ -1024,7 +1159,7 @@ class BespokeSinglePlant:
1024
1159
  """
1025
1160
  handler = res
1026
1161
  if isinstance(res, str):
1027
- if '*' in res:
1162
+ if "*" in res:
1028
1163
  handler = MultiYearWindResource
1029
1164
  else:
1030
1165
  handler = WindResource
@@ -1042,9 +1177,11 @@ class BespokeSinglePlant:
1042
1177
  missing.append(name)
1043
1178
 
1044
1179
  if any(missing):
1045
- msg = ('The reV bespoke module depends on the following special '
1046
- 'dependencies that were not found in the active '
1047
- 'environment: {}'.format(missing))
1180
+ msg = (
1181
+ "The reV bespoke module depends on the following special "
1182
+ "dependencies that were not found in the active "
1183
+ "environment: {}".format(missing)
1184
+ )
1048
1185
  logger.error(msg)
1049
1186
  raise ModuleNotFoundError(msg)
1050
1187
 
@@ -1056,7 +1193,11 @@ class BespokeSinglePlant:
1056
1193
  'hourly',
1057
1194
  'capital_cost',
1058
1195
  'fixed_operating_cost',
1059
- 'variable_operating_cost')):
1196
+ 'variable_operating_cost',
1197
+ 'balance_of_system_cost',
1198
+ 'base_capital_cost',
1199
+ 'base_fixed_operating_cost',
1200
+ 'base_variable_operating_cost')):
1060
1201
  """Check two reV-SAM models for matching system inputs.
1061
1202
 
1062
1203
  Parameters
@@ -1066,13 +1207,13 @@ class BespokeSinglePlant:
1066
1207
  """
1067
1208
  bad = []
1068
1209
  for k, v in plant1.sam_sys_inputs.items():
1069
- if k not in plant2.sam_sys_inputs:
1070
- bad.append(k)
1071
- elif str(v) != str(plant2.sam_sys_inputs[k]):
1210
+ if k not in plant2.sam_sys_inputs or str(v) != str(
1211
+ plant2.sam_sys_inputs[k]
1212
+ ):
1072
1213
  bad.append(k)
1073
1214
  bad = [b for b in bad if b not in ignore]
1074
1215
  if any(bad):
1075
- msg = 'Inputs no longer match: {}'.format(bad)
1216
+ msg = "Inputs no longer match: {}".format(bad)
1076
1217
  logger.error(msg)
1077
1218
  raise RuntimeError(msg)
1078
1219
 
@@ -1088,41 +1229,60 @@ class BespokeSinglePlant:
1088
1229
  BespokeSinglePlant.outputs property.
1089
1230
  """
1090
1231
 
1091
- logger.debug('Running {} years of SAM timeseries analysis for {}'
1092
- .format(len(self.years), self))
1232
+ logger.debug(
1233
+ "Running {} years of SAM timeseries analysis for {}".format(
1234
+ len(self.years), self
1235
+ )
1236
+ )
1093
1237
  self._wind_plant_ts = self.initialize_wind_plant_ts()
1094
1238
  for year, plant in self.wind_plant_ts.items():
1095
1239
  self._check_sys_inputs(plant, self.wind_plant_pd)
1096
1240
  try:
1097
1241
  plant.run_gen_and_econ()
1098
1242
  except Exception as e:
1099
- msg = ('{} failed while trying to run SAM WindPower '
1100
- 'timeseries analysis for {}'.format(self, year))
1243
+ msg = (
1244
+ "{} failed while trying to run SAM WindPower "
1245
+ "timeseries analysis for {}".format(self, year)
1246
+ )
1101
1247
  logger.exception(msg)
1102
1248
  raise RuntimeError(msg) from e
1103
1249
 
1104
1250
  for k, v in plant.outputs.items():
1105
- self._outputs[k + '-{}'.format(year)] = v
1251
+ self._outputs[k + "-{}".format(year)] = v
1106
1252
 
1107
1253
  means = {}
1108
1254
  for k1, v1 in self._outputs.items():
1109
- if isinstance(v1, Number) and parse_year(k1, option='boolean'):
1255
+ if isinstance(v1, Number) and parse_year(k1, option="boolean"):
1110
1256
  year = parse_year(k1)
1111
- base_str = k1.replace(str(year), '')
1112
- all_values = [v2 for k2, v2 in self._outputs.items()
1113
- if base_str in k2]
1114
- means[base_str + 'means'] = np.mean(all_values)
1257
+ base_str = k1.replace(str(year), "")
1258
+ all_values = [
1259
+ v2 for k2, v2 in self._outputs.items() if base_str in k2
1260
+ ]
1261
+ means[base_str + "means"] = np.mean(all_values)
1115
1262
 
1116
1263
  self._outputs.update(means)
1117
1264
 
1265
+ self._meta[SupplyCurveField.MEAN_RES] = self.res_df["windspeed"].mean()
1266
+ self._meta[SupplyCurveField.MEAN_CF_DC] = None
1267
+ self._meta[SupplyCurveField.MEAN_CF_AC] = None
1268
+ self._meta[SupplyCurveField.MEAN_LCOE] = None
1269
+ self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = None
1118
1270
  # copy dataset outputs to meta data for supply curve table summary
1119
- if 'cf_mean-means' in self.outputs:
1120
- self._meta.loc[:, 'mean_cf'] = self.outputs['cf_mean-means']
1121
- if 'lcoe_fcr-means' in self.outputs:
1122
- self._meta.loc[:, 'mean_lcoe'] = self.outputs['lcoe_fcr-means']
1271
+ if "cf_mean-means" in self.outputs:
1272
+ self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[
1273
+ "cf_mean-means"
1274
+ ]
1275
+ if "lcoe_fcr-means" in self.outputs:
1276
+ self._meta.loc[:, SupplyCurveField.MEAN_LCOE] = self.outputs[
1277
+ "lcoe_fcr-means"
1278
+ ]
1123
1279
  self.recalc_lcoe()
1280
+ if "annual_energy-means" in self.outputs:
1281
+ self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = (
1282
+ self.outputs["annual_energy-means"] / 1000
1283
+ )
1124
1284
 
1125
- logger.debug('Timeseries analysis complete!')
1285
+ logger.debug("Timeseries analysis complete!")
1126
1286
 
1127
1287
  return self.outputs
1128
1288
 
@@ -1138,19 +1298,23 @@ class BespokeSinglePlant:
1138
1298
  BespokeSinglePlant.outputs property.
1139
1299
  """
1140
1300
 
1141
- logger.debug('Running plant layout optimization for {}'.format(self))
1301
+ logger.debug("Running plant layout optimization for {}".format(self))
1142
1302
  try:
1143
1303
  self.plant_optimizer.place_turbines(**self.ga_kwargs)
1144
1304
  except Exception as e:
1145
- msg = ('{} failed while trying to run the '
1146
- 'turbine placement optimizer'
1147
- .format(self))
1305
+ msg = (
1306
+ "{} failed while trying to run the "
1307
+ "turbine placement optimizer".format(self)
1308
+ )
1148
1309
  logger.exception(msg)
1149
1310
  raise RuntimeError(msg) from e
1150
1311
 
1151
- # TODO need to add:
1152
- # total cell area
1153
- # cell capacity density
1312
+ self._outputs["full_polygons"] = self.plant_optimizer.full_polygons
1313
+ self._outputs["packing_polygons"] = (
1314
+ self.plant_optimizer.packing_polygons
1315
+ )
1316
+ system_capacity_kw = self.plant_optimizer.capacity
1317
+ self._outputs["system_capacity"] = system_capacity_kw
1154
1318
 
1155
1319
  txc = [int(np.round(c)) for c in self.plant_optimizer.turbine_x]
1156
1320
  tyc = [int(np.round(c)) for c in self.plant_optimizer.turbine_y]
@@ -1162,62 +1326,110 @@ class BespokeSinglePlant:
1162
1326
  pxc = json.dumps(pxc)
1163
1327
  pyc = json.dumps(pyc)
1164
1328
 
1165
- self._meta["turbine_x_coords"] = txc
1166
- self._meta["turbine_y_coords"] = tyc
1167
- self._meta["possible_x_coords"] = pxc
1168
- self._meta["possible_y_coords"] = pyc
1169
-
1170
- self._outputs["full_polygons"] = self.plant_optimizer.full_polygons
1171
- self._outputs["packing_polygons"] = \
1172
- self.plant_optimizer.packing_polygons
1173
- self._outputs["system_capacity"] = self.plant_optimizer.capacity
1174
-
1175
- self._meta["n_turbines"] = self.plant_optimizer.nturbs
1176
- self._meta["bespoke_aep"] = self.plant_optimizer.aep
1177
- self._meta["bespoke_objective"] = self.plant_optimizer.objective
1178
- self._meta["bespoke_capital_cost"] = \
1329
+ self._meta[SupplyCurveField.TURBINE_X_COORDS] = txc
1330
+ self._meta[SupplyCurveField.TURBINE_Y_COORDS] = tyc
1331
+ self._meta[SupplyCurveField.POSSIBLE_X_COORDS] = pxc
1332
+ self._meta[SupplyCurveField.POSSIBLE_Y_COORDS] = pyc
1333
+
1334
+ self._meta[SupplyCurveField.N_TURBINES] = self.plant_optimizer.nturbs
1335
+ self._meta["avg_sl_dist_to_center_m"] = (
1336
+ self.plant_optimizer.avg_sl_dist_to_center_m
1337
+ )
1338
+ self._meta["avg_sl_dist_to_medoid_m"] = (
1339
+ self.plant_optimizer.avg_sl_dist_to_medoid_m
1340
+ )
1341
+ self._meta["nn_conn_dist_m"] = self.plant_optimizer.nn_conn_dist_m
1342
+ self._meta[SupplyCurveField.BESPOKE_AEP] = self.plant_optimizer.aep
1343
+ self._meta[SupplyCurveField.BESPOKE_OBJECTIVE] = (
1344
+ self.plant_optimizer.objective
1345
+ )
1346
+ self._meta[SupplyCurveField.BESPOKE_CAPITAL_COST] = (
1179
1347
  self.plant_optimizer.capital_cost
1180
- self._meta["bespoke_fixed_operating_cost"] = \
1348
+ )
1349
+ self._meta[SupplyCurveField.BESPOKE_FIXED_OPERATING_COST] = (
1181
1350
  self.plant_optimizer.fixed_operating_cost
1182
- self._meta["bespoke_variable_operating_cost"] = \
1351
+ )
1352
+ self._meta[SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST] = (
1183
1353
  self.plant_optimizer.variable_operating_cost
1184
- self._meta["included_area"] = self.plant_optimizer.area
1185
- self._meta["included_area_capacity_density"] = \
1354
+ )
1355
+ self._meta[SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST] = (
1356
+ self.plant_optimizer.balance_of_system_cost
1357
+ )
1358
+ self._meta[SupplyCurveField.INCLUDED_AREA] = self.plant_optimizer.area
1359
+ self._meta[SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY] = (
1186
1360
  self.plant_optimizer.capacity_density
1187
- self._meta["convex_hull_area"] = \
1361
+ )
1362
+ self._meta[SupplyCurveField.CONVEX_HULL_AREA] = (
1188
1363
  self.plant_optimizer.convex_hull_area
1189
- self._meta["convex_hull_capacity_density"] = \
1364
+ )
1365
+ self._meta[SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY] = (
1190
1366
  self.plant_optimizer.convex_hull_capacity_density
1191
- self._meta["full_cell_capacity_density"] = \
1367
+ )
1368
+ self._meta[SupplyCurveField.FULL_CELL_CAPACITY_DENSITY] = (
1192
1369
  self.plant_optimizer.full_cell_capacity_density
1193
-
1194
- logger.debug('Plant layout optimization complete!')
1370
+ )
1195
1371
 
1196
1372
  # copy dataset outputs to meta data for supply curve table summary
1197
1373
  # convert SAM system capacity in kW to reV supply curve cap in MW
1198
- self._meta['capacity'] = self.outputs['system_capacity'] / 1e3
1374
+ capacity_ac_mw = system_capacity_kw / 1e3
1375
+ self._meta[SupplyCurveField.CAPACITY_AC_MW] = capacity_ac_mw
1376
+ self._meta[SupplyCurveField.CAPACITY_DC_MW] = None
1199
1377
 
1200
1378
  # add required ReEDS multipliers to meta
1201
1379
  baseline_cost = self.plant_optimizer.capital_cost_per_kw(
1202
- capacity_mw=self._baseline_cap_mw)
1203
- self._meta['eos_mult'] = (self.plant_optimizer.capital_cost
1204
- / self.plant_optimizer.capacity
1205
- / baseline_cost)
1206
- self._meta['reg_mult'] = (self.sam_sys_inputs
1207
- .get("capital_cost_multiplier", 1))
1380
+ capacity_mw=self._baseline_cap_mw
1381
+ )
1382
+ eos_mult = (self.plant_optimizer.capital_cost
1383
+ / self.plant_optimizer.capacity
1384
+ / baseline_cost)
1385
+ reg_mult = self.sam_sys_inputs.get("capital_cost_multiplier", 1)
1208
1386
 
1387
+ self._meta[SupplyCurveField.EOS_MULT] = eos_mult
1388
+ self._meta[SupplyCurveField.REG_MULT] = reg_mult
1389
+
1390
+ cap_cost = (
1391
+ self.plant_optimizer.capital_cost
1392
+ + self.plant_optimizer.balance_of_system_cost
1393
+ )
1394
+ self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = (
1395
+ cap_cost / capacity_ac_mw
1396
+ )
1397
+ self._meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] = (
1398
+ cap_cost / eos_mult / reg_mult / capacity_ac_mw
1399
+ )
1400
+ self._meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] = (
1401
+ self.plant_optimizer.fixed_operating_cost / capacity_ac_mw
1402
+ )
1403
+ self._meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW] = (
1404
+ self.plant_optimizer.fixed_operating_cost / capacity_ac_mw
1405
+ )
1406
+ self._meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] = (
1407
+ self.plant_optimizer.variable_operating_cost / capacity_ac_mw
1408
+ )
1409
+ self._meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] = (
1410
+ self.plant_optimizer.variable_operating_cost / capacity_ac_mw
1411
+ )
1412
+ self._meta[SupplyCurveField.FIXED_CHARGE_RATE] = (
1413
+ self.plant_optimizer.fixed_charge_rate
1414
+ )
1415
+
1416
+ logger.debug("Plant layout optimization complete!")
1209
1417
  return self.outputs
1210
1418
 
1211
1419
  def agg_data_layers(self):
1212
1420
  """Aggregate optional data layers if requested and save to self.meta"""
1213
1421
  if self._data_layers is not None:
1214
- logger.debug('Aggregating {} extra data layers.'
1215
- .format(len(self._data_layers)))
1422
+ logger.debug(
1423
+ "Aggregating {} extra data layers.".format(
1424
+ len(self._data_layers)
1425
+ )
1426
+ )
1216
1427
  point_summary = self.meta.to_dict()
1217
- point_summary = self.sc_point.agg_data_layers(point_summary,
1218
- self._data_layers)
1428
+ point_summary = self.sc_point.agg_data_layers(
1429
+ point_summary, self._data_layers
1430
+ )
1219
1431
  self._meta = pd.DataFrame(point_summary)
1220
- logger.debug('Finished aggregating extra data layers.')
1432
+ logger.debug("Finished aggregating extra data layers.")
1221
1433
 
1222
1434
  @property
1223
1435
  def outputs(self):
@@ -1246,9 +1458,10 @@ class BespokeSinglePlant:
1246
1458
 
1247
1459
  with cls(*args, **kwargs) as bsp:
1248
1460
  if bsp._prior_meta:
1249
- logger.debug('Skipping bespoke plant optimization for gid {}. '
1250
- 'Received prior meta data for this point.'
1251
- .format(bsp.gid))
1461
+ logger.debug(
1462
+ "Skipping bespoke plant optimization for gid {}. "
1463
+ "Received prior meta data for this point.".format(bsp.gid)
1464
+ )
1252
1465
  else:
1253
1466
  _ = bsp.run_plant_optimization()
1254
1467
 
@@ -1257,9 +1470,9 @@ class BespokeSinglePlant:
1257
1470
 
1258
1471
  meta = bsp.meta
1259
1472
  out = bsp.outputs
1260
- out['meta'] = meta
1473
+ out["meta"] = meta
1261
1474
  for year, ti in zip(bsp.years, bsp.annual_time_indexes):
1262
- out['time_index-{}'.format(year)] = ti
1475
+ out["time_index-{}".format(year)] = ti
1263
1476
 
1264
1477
  return out
1265
1478
 
@@ -1269,7 +1482,8 @@ class BespokeWindPlants(BaseAggregation):
1269
1482
 
1270
1483
  def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
1271
1484
  capital_cost_function, fixed_operating_cost_function,
1272
- variable_operating_cost_function, project_points,
1485
+ variable_operating_cost_function,
1486
+ balance_of_system_cost_function, project_points,
1273
1487
  sam_files, min_spacing='5x', wake_loss_multiplier=1,
1274
1488
  ga_kwargs=None, output_request=('system_capacity', 'cf_mean'),
1275
1489
  ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
@@ -1349,17 +1563,30 @@ class BespokeWindPlants(BaseAggregation):
1349
1563
  - ``n_turbines``: the number of turbines
1350
1564
  - ``system_capacity``: wind plant capacity
1351
1565
  - ``aep``: annual energy production
1566
+ - ``avg_sl_dist_to_center_m``: Average straight-line
1567
+ distance to the supply curve point center from all
1568
+ turbine locations (in m). Useful for computing plant
1569
+ BOS costs.
1570
+ - ``avg_sl_dist_to_medoid_m``: Average straight-line
1571
+ distance to the medoid of all turbine locations
1572
+ (in m). Useful for computing plant BOS costs.
1573
+ - ``nn_conn_dist_m``: Total BOS connection distance
1574
+ using nearest-neighbor connections. This variable is
1575
+ only available for the
1576
+ ``balance_of_system_cost_function`` equation.
1352
1577
  - ``fixed_charge_rate``: user input fixed_charge_rate if
1353
1578
  included as part of the sam system config.
1354
- - ``self.wind_plant``: the SAM wind plant object,
1355
- through which all SAM variables can be accessed
1356
1579
  - ``capital_cost``: plant capital cost as evaluated
1357
1580
  by `capital_cost_function`
1358
1581
  - ``fixed_operating_cost``: plant fixed annual operating
1359
1582
  cost as evaluated by `fixed_operating_cost_function`
1360
1583
  - ``variable_operating_cost``: plant variable annual
1361
- operating cost, as evaluated by
1584
+ operating cost as evaluated by
1362
1585
  `variable_operating_cost_function`
1586
+ - ``balance_of_system_cost``: plant balance of system
1587
+ cost as evaluated by `balance_of_system_cost_function`
1588
+ - ``self.wind_plant``: the SAM wind plant object,
1589
+ through which all SAM variables can be accessed
1363
1590
 
1364
1591
  capital_cost_function : str
1365
1592
  The plant capital cost function written out as a string.
@@ -1376,6 +1603,13 @@ class BespokeWindPlants(BaseAggregation):
1376
1603
  out as a string. This expression must return the variable
1377
1604
  operating cost in $/kWh. This expression has access to the
1378
1605
  same variables as the `objective_function` argument above.
1606
+ You can set this to "0" to effectively ignore variable
1607
+ operating costs.
1608
+ balance_of_system_cost_function : str
1609
+ The plant balance-of-system cost function as a string, must
1610
+ return the variable operating cost in $. Has access to the
1611
+ same variables as the objective_function. You can set this
1612
+ to "0" to effectively ignore balance-of-system costs.
1379
1613
  project_points : int | list | tuple | str | dict | pd.DataFrame | slice
1380
1614
  Input specifying which sites to process. A single integer
1381
1615
  representing the supply curve GID of a site may be specified
@@ -1406,11 +1640,13 @@ class BespokeWindPlants(BaseAggregation):
1406
1640
  - ``capital_cost_multiplier``
1407
1641
  - ``fixed_operating_cost_multiplier``
1408
1642
  - ``variable_operating_cost_multiplier``
1643
+ - ``balance_of_system_cost_multiplier``
1409
1644
 
1410
1645
  These particular inputs are treated as multipliers to be
1411
1646
  applied to the respective cost curves
1412
1647
  (`capital_cost_function`, `fixed_operating_cost_function`,
1413
- and `variable_operating_cost_function`) both during and
1648
+ `variable_operating_cost_function`, and
1649
+ `balance_of_system_cost_function`) both during and
1414
1650
  after the optimization. A DataFrame following the same
1415
1651
  guidelines as the CSV input (or a dictionary that can be
1416
1652
  used to initialize such a DataFrame) may be used for this
@@ -1697,33 +1933,46 @@ class BespokeWindPlants(BaseAggregation):
1697
1933
  .format(fixed_operating_cost_function))
1698
1934
  logger.info('Bespoke variable operating cost function: {}'
1699
1935
  .format(variable_operating_cost_function))
1936
+ logger.info('Bespoke balance of system cost function: {}'
1937
+ .format(balance_of_system_cost_function))
1700
1938
  logger.info('Bespoke wake loss multiplier: {}'
1701
1939
  .format(wake_loss_multiplier))
1702
1940
  logger.info('Bespoke GA initialization kwargs: {}'.format(ga_kwargs))
1703
1941
 
1704
- logger.info('Bespoke pre-extracting exclusions: {}'
1705
- .format(pre_extract_inclusions))
1706
- logger.info('Bespoke pre-extracting resource data: {}'
1707
- .format(pre_load_data))
1708
- logger.info('Bespoke prior run: {}'.format(prior_run))
1709
- logger.info('Bespoke GID map: {}'.format(gid_map))
1710
- logger.info('Bespoke bias correction table: {}'.format(bias_correct))
1942
+ logger.info(
1943
+ "Bespoke pre-extracting exclusions: {}".format(
1944
+ pre_extract_inclusions
1945
+ )
1946
+ )
1947
+ logger.info(
1948
+ "Bespoke pre-extracting resource data: {}".format(pre_load_data)
1949
+ )
1950
+ logger.info("Bespoke prior run: {}".format(prior_run))
1951
+ logger.info("Bespoke GID map: {}".format(gid_map))
1952
+ logger.info("Bespoke bias correction table: {}".format(bias_correct))
1711
1953
 
1712
1954
  BespokeSinglePlant.check_dependencies()
1713
1955
 
1714
1956
  self._project_points = self._parse_points(project_points, sam_files)
1715
1957
 
1716
- super().__init__(excl_fpath, tm_dset, excl_dict=excl_dict,
1717
- area_filter_kernel=area_filter_kernel,
1718
- min_area=min_area, resolution=resolution,
1719
- excl_area=excl_area, gids=self._project_points.gids,
1720
- pre_extract_inclusions=pre_extract_inclusions)
1958
+ super().__init__(
1959
+ excl_fpath,
1960
+ tm_dset,
1961
+ excl_dict=excl_dict,
1962
+ area_filter_kernel=area_filter_kernel,
1963
+ min_area=min_area,
1964
+ resolution=resolution,
1965
+ excl_area=excl_area,
1966
+ gids=self._project_points.gids,
1967
+ pre_extract_inclusions=pre_extract_inclusions,
1968
+ )
1721
1969
 
1722
1970
  self._res_fpath = res_fpath
1723
1971
  self._obj_fun = objective_function
1724
1972
  self._cap_cost_fun = capital_cost_function
1725
1973
  self._foc_fun = fixed_operating_cost_function
1726
1974
  self._voc_fun = variable_operating_cost_function
1975
+ self._bos_fun = balance_of_system_cost_function
1727
1976
  self._min_spacing = min_spacing
1728
1977
  self._wake_loss_multiplier = wake_loss_multiplier
1729
1978
  self._ga_kwargs = ga_kwargs or {}
@@ -1742,8 +1991,11 @@ class BespokeWindPlants(BaseAggregation):
1742
1991
 
1743
1992
  self._slice_lookup = None
1744
1993
 
1745
- logger.info('Initialized BespokeWindPlants with project points: {}'
1746
- .format(self._project_points))
1994
+ logger.info(
1995
+ "Initialized BespokeWindPlants with project points: {}".format(
1996
+ self._project_points
1997
+ )
1998
+ )
1747
1999
 
1748
2000
  @staticmethod
1749
2001
  def _parse_points(points, sam_configs):
@@ -1755,8 +2007,8 @@ class BespokeWindPlants(BaseAggregation):
1755
2007
  Slice or list specifying project points, string pointing to a
1756
2008
  project points csv, or a fully instantiated PointsControl object.
1757
2009
  Can also be a single site integer value. Points csv should have
1758
- 'gid' and 'config' column, the config maps to the sam_configs dict
1759
- keys.
2010
+ `SiteDataField.GID` and 'config' column, the config maps to the
2011
+ sam_configs dict keys.
1760
2012
  sam_configs : dict | str | SAMConfig
1761
2013
  SAM input configuration ID(s) and file path(s). Keys are the SAM
1762
2014
  config ID(s) which map to the config column in the project points
@@ -1770,8 +2022,13 @@ class BespokeWindPlants(BaseAggregation):
1770
2022
  Project points object laying out the supply curve gids to
1771
2023
  analyze.
1772
2024
  """
1773
- pc = Gen.get_pc(points, points_range=None, sam_configs=sam_configs,
1774
- tech='windpower', sites_per_worker=1)
2025
+ pc = Gen.get_pc(
2026
+ points,
2027
+ points_range=None,
2028
+ sam_configs=sam_configs,
2029
+ tech="windpower",
2030
+ sites_per_worker=1,
2031
+ )
1775
2032
 
1776
2033
  return pc.project_points
1777
2034
 
@@ -1801,15 +2058,16 @@ class BespokeWindPlants(BaseAggregation):
1801
2058
 
1802
2059
  if prior_run is not None:
1803
2060
  assert os.path.isfile(prior_run)
1804
- assert prior_run.endswith('.h5')
2061
+ assert prior_run.endswith(".h5")
1805
2062
 
1806
- with Outputs(prior_run, mode='r') as f:
2063
+ with Outputs(prior_run, mode="r") as f:
1807
2064
  meta = f.meta
2065
+ meta = meta.rename(columns=SupplyCurveField.map_from_legacy())
1808
2066
 
1809
2067
  # pylint: disable=no-member
1810
2068
  for col in meta.columns:
1811
2069
  val = meta[col].values[0]
1812
- if isinstance(val, str) and val[0] == '[' and val[-1] == ']':
2070
+ if isinstance(val, str) and val[0] == "[" and val[-1] == "]":
1813
2071
  meta[col] = meta[col].apply(json.loads)
1814
2072
 
1815
2073
  return meta
@@ -1830,7 +2088,7 @@ class BespokeWindPlants(BaseAggregation):
1830
2088
  meta = None
1831
2089
 
1832
2090
  if self._prior_meta is not None:
1833
- mask = self._prior_meta['gid'] == gid
2091
+ mask = self._prior_meta[SupplyCurveField.SC_POINT_GID] == gid
1834
2092
  if any(mask):
1835
2093
  meta = self._prior_meta[mask]
1836
2094
 
@@ -1846,14 +2104,19 @@ class BespokeWindPlants(BaseAggregation):
1846
2104
  for path in paths:
1847
2105
  if not os.path.exists(path):
1848
2106
  raise FileNotFoundError(
1849
- 'Could not find required exclusions file: '
1850
- '{}'.format(path))
2107
+ "Could not find required exclusions file: " "{}".format(
2108
+ path
2109
+ )
2110
+ )
1851
2111
 
1852
2112
  with ExclusionLayers(paths) as excl:
1853
2113
  if self._tm_dset not in excl:
1854
- raise FileInputError('Could not find techmap dataset "{}" '
1855
- 'in the exclusions file(s): {}'
1856
- .format(self._tm_dset, paths))
2114
+ raise FileInputError(
2115
+ 'Could not find techmap dataset "{}" '
2116
+ "in the exclusions file(s): {}".format(
2117
+ self._tm_dset, paths
2118
+ )
2119
+ )
1857
2120
 
1858
2121
  # just check that this file exists, cannot check res_fpath if *glob
1859
2122
  Handler = BespokeSinglePlant.get_wind_handler(self._res_fpath)
@@ -1861,22 +2124,28 @@ class BespokeWindPlants(BaseAggregation):
1861
2124
  assert any(f.dsets)
1862
2125
 
1863
2126
  def _pre_load_data(self, pre_load_data):
1864
- """Pre-load resource data, if requested. """
2127
+ """Pre-load resource data, if requested."""
1865
2128
  if not pre_load_data:
1866
2129
  return
1867
2130
 
1868
- sc_gid_to_hh = {gid: self._hh_for_sc_gid(gid)
1869
- for gid in self._project_points.df["gid"]}
2131
+ sc_gid_to_hh = {
2132
+ gid: self._hh_for_sc_gid(gid)
2133
+ for gid in self._project_points.df[ResourceMetaField.GID]
2134
+ }
1870
2135
 
1871
2136
  with ExclusionLayers(self._excl_fpath) as excl:
1872
2137
  tm = excl[self._tm_dset]
1873
2138
 
1874
2139
  scp_kwargs = {"shape": self.shape, "resolution": self._resolution}
1875
- slices = {gid: SupplyCurvePoint.get_agg_slices(gid=gid, **scp_kwargs)
1876
- for gid in self._project_points.df["gid"]}
2140
+ slices = {
2141
+ gid: SupplyCurvePoint.get_agg_slices(gid=gid, **scp_kwargs)
2142
+ for gid in self._project_points.df[ResourceMetaField.GID]
2143
+ }
1877
2144
 
1878
- sc_gid_to_res_gid = {gid: sorted(set(tm[slx, sly].flatten()))
1879
- for gid, (slx, sly) in slices.items()}
2145
+ sc_gid_to_res_gid = {
2146
+ gid: sorted(set(tm[slx, sly].flatten()))
2147
+ for gid, (slx, sly) in slices.items()
2148
+ }
1880
2149
 
1881
2150
  for sc_gid, res_gids in sc_gid_to_res_gid.items():
1882
2151
  if res_gids[0] < 0:
@@ -1884,13 +2153,14 @@ class BespokeWindPlants(BaseAggregation):
1884
2153
 
1885
2154
  if self._gid_map is not None:
1886
2155
  for sc_gid, res_gids in sc_gid_to_res_gid.items():
1887
- sc_gid_to_res_gid[sc_gid] = sorted(self._gid_map[g]
1888
- for g in res_gids)
2156
+ sc_gid_to_res_gid[sc_gid] = sorted(
2157
+ self._gid_map[g] for g in res_gids
2158
+ )
1889
2159
 
1890
2160
  logger.info("Pre-loading resource data for Bespoke run... ")
1891
- self._pre_loaded_data = BespokeMultiPlantData(self._res_fpath,
1892
- sc_gid_to_hh,
1893
- sc_gid_to_res_gid)
2161
+ self._pre_loaded_data = BespokeMultiPlantData(
2162
+ self._res_fpath, sc_gid_to_hh, sc_gid_to_res_gid
2163
+ )
1894
2164
 
1895
2165
  def _hh_for_sc_gid(self, sc_gid):
1896
2166
  """Fetch the hh for a given sc_gid"""
@@ -1898,7 +2168,7 @@ class BespokeWindPlants(BaseAggregation):
1898
2168
  return int(config["wind_turbine_hub_ht"])
1899
2169
 
1900
2170
  def _pre_loaded_data_for_sc_gid(self, sc_gid):
1901
- """Pre-load data for a given SC GID, if requested. """
2171
+ """Pre-load data for a given SC GID, if requested."""
1902
2172
  if self._pre_loaded_data is None:
1903
2173
  return None
1904
2174
 
@@ -1926,9 +2196,12 @@ class BespokeWindPlants(BaseAggregation):
1926
2196
  if self._bias_correct is not None:
1927
2197
  h5_gids = []
1928
2198
  try:
1929
- scp_kwargs = dict(gid=gid, excl=self._excl_fpath,
1930
- tm_dset=self._tm_dset,
1931
- resolution=self._resolution)
2199
+ scp_kwargs = dict(
2200
+ gid=gid,
2201
+ excl=self._excl_fpath,
2202
+ tm_dset=self._tm_dset,
2203
+ resolution=self._resolution,
2204
+ )
1932
2205
  with SupplyCurvePoint(**scp_kwargs) as scp:
1933
2206
  h5_gids = scp.h5_gid_set
1934
2207
  except EmptySupplyCurvePointError:
@@ -1972,7 +2245,7 @@ class BespokeWindPlants(BaseAggregation):
1972
2245
  -------
1973
2246
  pd.DataFrame
1974
2247
  """
1975
- meta = [self.outputs[g]['meta'] for g in self.completed_gids]
2248
+ meta = [self.outputs[g]["meta"] for g in self.completed_gids]
1976
2249
  if len(self.completed_gids) > 1:
1977
2250
  meta = pd.concat(meta, axis=0)
1978
2251
  else:
@@ -1981,10 +2254,11 @@ class BespokeWindPlants(BaseAggregation):
1981
2254
 
1982
2255
  @property
1983
2256
  def slice_lookup(self):
1984
- """dict | None: Lookup mapping sc_point_gid to exclusion slice. """
2257
+ """Dict | None: Lookup mapping sc_point_gid to exclusion slice."""
1985
2258
  if self._slice_lookup is None and self._inclusion_mask is not None:
1986
- with SupplyCurveExtent(self._excl_fpath,
1987
- resolution=self._resolution) as sc:
2259
+ with SupplyCurveExtent(
2260
+ self._excl_fpath, resolution=self._resolution
2261
+ ) as sc:
1988
2262
  assert self.shape == self._inclusion_mask.shape
1989
2263
  self._slice_lookup = sc.get_slice_lookup(self.gids)
1990
2264
 
@@ -2013,8 +2287,13 @@ class BespokeWindPlants(BaseAggregation):
2013
2287
  site_data = self._project_points.df.iloc[gid_idx]
2014
2288
 
2015
2289
  site_sys_inputs = self._project_points[gid][1]
2016
- site_sys_inputs.update({k: v for k, v in site_data.to_dict().items()
2017
- if not (isinstance(v, float) and np.isnan(v))})
2290
+ site_sys_inputs.update(
2291
+ {
2292
+ k: v
2293
+ for k, v in site_data.to_dict().items()
2294
+ if not (isinstance(v, float) and np.isnan(v))
2295
+ }
2296
+ )
2018
2297
  return site_sys_inputs
2019
2298
 
2020
2299
  def _init_fout(self, out_fpath, sample):
@@ -2033,13 +2312,14 @@ class BespokeWindPlants(BaseAggregation):
2033
2312
  if not os.path.exists(out_dir):
2034
2313
  create_dirs(out_dir)
2035
2314
 
2036
- with Outputs(out_fpath, mode='w') as f:
2037
- f._set_meta('meta', self.meta, attrs={})
2038
- ti_dsets = [d for d in sample.keys()
2039
- if d.startswith('time_index-')]
2315
+ with Outputs(out_fpath, mode="w") as f:
2316
+ f._set_meta("meta", self.meta, attrs={})
2317
+ ti_dsets = [
2318
+ d for d in sample.keys() if d.startswith("time_index-")
2319
+ ]
2040
2320
  for dset in ti_dsets:
2041
2321
  f._set_time_index(dset, sample[dset], attrs={})
2042
- f._set_time_index('time_index', sample[dset], attrs={})
2322
+ f._set_time_index("time_index", sample[dset], attrs={})
2043
2323
 
2044
2324
  def _collect_out_arr(self, dset, sample):
2045
2325
  """Collect single-plant data arrays into complete arrays with data from
@@ -2070,8 +2350,9 @@ class BespokeWindPlants(BaseAggregation):
2070
2350
  shape = (len(single_arr), len(self.completed_gids))
2071
2351
  sample_num = single_arr[0]
2072
2352
  else:
2073
- msg = ('Not writing dataset "{}" of type "{}" to disk.'
2074
- .format(dset, type(single_arr)))
2353
+ msg = 'Not writing dataset "{}" of type "{}" to disk.'.format(
2354
+ dset, type(single_arr)
2355
+ )
2075
2356
  logger.info(msg)
2076
2357
  return None
2077
2358
 
@@ -2082,8 +2363,9 @@ class BespokeWindPlants(BaseAggregation):
2082
2363
  full_arr = np.zeros(shape, dtype=dtype)
2083
2364
 
2084
2365
  # collect data from all wind plants
2085
- logger.info('Collecting dataset "{}" with final shape {}'
2086
- .format(dset, shape))
2366
+ logger.info(
2367
+ 'Collecting dataset "{}" with final shape {}'.format(dset, shape)
2368
+ )
2087
2369
  for i, gid in enumerate(self.completed_gids):
2088
2370
  if len(full_arr.shape) == 1:
2089
2371
  full_arr[i] = self.outputs[gid][dset]
@@ -2107,16 +2389,18 @@ class BespokeWindPlants(BaseAggregation):
2107
2389
  Full filepath to desired .h5 output file, the .h5 extension has
2108
2390
  been added if it was not already present.
2109
2391
  """
2110
- if not out_fpath.endswith('.h5'):
2111
- out_fpath += '.h5'
2392
+ if not out_fpath.endswith(".h5"):
2393
+ out_fpath += ".h5"
2112
2394
 
2113
2395
  if ModuleName.BESPOKE not in out_fpath:
2114
2396
  extension_with_module = "_{}.h5".format(ModuleName.BESPOKE)
2115
2397
  out_fpath = out_fpath.replace(".h5", extension_with_module)
2116
2398
 
2117
2399
  if not self.completed_gids:
2118
- msg = ("No output data found! It is likely that all requested "
2119
- "points are excluded.")
2400
+ msg = (
2401
+ "No output data found! It is likely that all requested "
2402
+ "points are excluded."
2403
+ )
2120
2404
  logger.warning(msg)
2121
2405
  warn(msg)
2122
2406
  return out_fpath
@@ -2124,31 +2408,34 @@ class BespokeWindPlants(BaseAggregation):
2124
2408
  sample = self.outputs[self.completed_gids[0]]
2125
2409
  self._init_fout(out_fpath, sample)
2126
2410
 
2127
- dsets = [d for d in sample.keys()
2128
- if not d.startswith('time_index-')
2129
- and d != 'meta']
2130
- with Outputs(out_fpath, mode='a') as f:
2411
+ dsets = [
2412
+ d
2413
+ for d in sample.keys()
2414
+ if not d.startswith("time_index-") and d != "meta"
2415
+ ]
2416
+ with Outputs(out_fpath, mode="a") as f:
2131
2417
  for dset in dsets:
2132
2418
  full_arr = self._collect_out_arr(dset, sample)
2133
2419
  if full_arr is not None:
2134
2420
  dset_no_year = dset
2135
- if parse_year(dset, option='boolean'):
2421
+ if parse_year(dset, option="boolean"):
2136
2422
  year = parse_year(dset)
2137
- dset_no_year = dset.replace('-{}'.format(year), '')
2423
+ dset_no_year = dset.replace("-{}".format(year), "")
2138
2424
 
2139
2425
  attrs = BespokeSinglePlant.OUT_ATTRS.get(dset_no_year, {})
2140
2426
  attrs = copy.deepcopy(attrs)
2141
- dtype = attrs.pop('dtype', np.float32)
2142
- chunks = attrs.pop('chunks', None)
2427
+ dtype = attrs.pop("dtype", np.float32)
2428
+ chunks = attrs.pop("chunks", None)
2143
2429
  try:
2144
- f.write_dataset(dset, full_arr, dtype, chunks=chunks,
2145
- attrs=attrs)
2430
+ f.write_dataset(
2431
+ dset, full_arr, dtype, chunks=chunks, attrs=attrs
2432
+ )
2146
2433
  except Exception as e:
2147
2434
  msg = 'Failed to write "{}" to disk.'.format(dset)
2148
2435
  logger.exception(msg)
2149
- raise IOError(msg) from e
2436
+ raise OSError(msg) from e
2150
2437
 
2151
- logger.info('Saved output data to: {}'.format(out_fpath))
2438
+ logger.info("Saved output data to: {}".format(out_fpath))
2152
2439
  return out_fpath
2153
2440
 
2154
2441
  # pylint: disable=arguments-renamed
@@ -2158,6 +2445,7 @@ class BespokeWindPlants(BaseAggregation):
2158
2445
  capital_cost_function,
2159
2446
  fixed_operating_cost_function,
2160
2447
  variable_operating_cost_function,
2448
+ balance_of_system_cost_function,
2161
2449
  min_spacing='5x', wake_loss_multiplier=1, ga_kwargs=None,
2162
2450
  output_request=('system_capacity', 'cf_mean'),
2163
2451
  ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
@@ -2195,18 +2483,19 @@ class BespokeWindPlants(BaseAggregation):
2195
2483
  Handler = BespokeSinglePlant.get_wind_handler(res_fpath)
2196
2484
 
2197
2485
  # pre-extract handlers so they are not repeatedly initialized
2198
- file_kwargs = {'excl_dict': excl_dict,
2199
- 'area_filter_kernel': area_filter_kernel,
2200
- 'min_area': min_area,
2201
- 'h5_handler': Handler,
2202
- }
2486
+ file_kwargs = {
2487
+ "excl_dict": excl_dict,
2488
+ "area_filter_kernel": area_filter_kernel,
2489
+ "min_area": min_area,
2490
+ "h5_handler": Handler,
2491
+ }
2203
2492
 
2204
2493
  with AggFileHandler(excl_fpath, res_fpath, **file_kwargs) as fh:
2205
2494
  n_finished = 0
2206
2495
  for gid in gids:
2207
2496
  gid_inclusions = cls._get_gid_inclusion_mask(
2208
- inclusion_mask, gid, slice_lookup,
2209
- resolution=resolution)
2497
+ inclusion_mask, gid, slice_lookup, resolution=resolution
2498
+ )
2210
2499
  try:
2211
2500
  bsp_plant_out = BespokeSinglePlant.run(
2212
2501
  gid,
@@ -2218,6 +2507,7 @@ class BespokeWindPlants(BaseAggregation):
2218
2507
  capital_cost_function,
2219
2508
  fixed_operating_cost_function,
2220
2509
  variable_operating_cost_function,
2510
+ balance_of_system_cost_function,
2221
2511
  min_spacing=min_spacing,
2222
2512
  wake_loss_multiplier=wake_loss_multiplier,
2223
2513
  ga_kwargs=ga_kwargs,
@@ -2234,20 +2524,26 @@ class BespokeWindPlants(BaseAggregation):
2234
2524
  gid_map=gid_map,
2235
2525
  bias_correct=bias_correct,
2236
2526
  pre_loaded_data=pre_loaded_data,
2237
- close=False)
2527
+ close=False,
2528
+ )
2238
2529
 
2239
2530
  except EmptySupplyCurvePointError:
2240
- logger.debug('SC gid {} is fully excluded or does not '
2241
- 'have any valid source data!'.format(gid))
2531
+ logger.debug(
2532
+ "SC gid {} is fully excluded or does not "
2533
+ "have any valid source data!".format(gid)
2534
+ )
2242
2535
  except Exception as e:
2243
- msg = 'SC gid {} failed!'.format(gid)
2536
+ msg = "SC gid {} failed!".format(gid)
2244
2537
  logger.exception(msg)
2245
2538
  raise RuntimeError(msg) from e
2246
2539
  else:
2247
2540
  n_finished += 1
2248
- logger.debug('Serial bespoke: '
2249
- '{} out of {} points complete'
2250
- .format(n_finished, len(gids)))
2541
+ logger.debug(
2542
+ "Serial bespoke: "
2543
+ "{} out of {} points complete".format(
2544
+ n_finished, len(gids)
2545
+ )
2546
+ )
2251
2547
  log_mem(logger)
2252
2548
  out[gid] = bsp_plant_out
2253
2549
 
@@ -2269,17 +2565,18 @@ class BespokeWindPlants(BaseAggregation):
2269
2565
  Bespoke outputs keyed by sc point gid
2270
2566
  """
2271
2567
 
2272
- logger.info('Running bespoke optimization for points {} through {} '
2273
- 'at a resolution of {} on {} cores.'
2274
- .format(self.gids[0], self.gids[-1], self._resolution,
2275
- max_workers))
2568
+ logger.info(
2569
+ "Running bespoke optimization for points {} through {} "
2570
+ "at a resolution of {} on {} cores.".format(
2571
+ self.gids[0], self.gids[-1], self._resolution, max_workers
2572
+ )
2573
+ )
2276
2574
 
2277
2575
  futures = []
2278
2576
  out = {}
2279
2577
  n_finished = 0
2280
- loggers = [__name__, 'reV.supply_curve.point_summary', 'reV']
2578
+ loggers = [__name__, "reV.supply_curve.point_summary", "reV"]
2281
2579
  with SpawnProcessPool(max_workers=max_workers, loggers=loggers) as exe:
2282
-
2283
2580
  # iterate through split executions, submitting each to worker
2284
2581
  for gid in self.gids:
2285
2582
  # submit executions and append to futures list
@@ -2298,6 +2595,7 @@ class BespokeWindPlants(BaseAggregation):
2298
2595
  self._cap_cost_fun,
2299
2596
  self._foc_fun,
2300
2597
  self._voc_fun,
2598
+ self._bos_fun,
2301
2599
  self._min_spacing,
2302
2600
  wake_loss_multiplier=self._wake_loss_multiplier,
2303
2601
  ga_kwargs=self._ga_kwargs,
@@ -2325,12 +2623,17 @@ class BespokeWindPlants(BaseAggregation):
2325
2623
  out.update(future.result())
2326
2624
  if n_finished % 10 == 0:
2327
2625
  mem = psutil.virtual_memory()
2328
- logger.info('Parallel bespoke futures collected: '
2329
- '{} out of {}. Memory usage is {:.3f} GB out '
2330
- 'of {:.3f} GB ({:.2f}% utilized).'
2331
- .format(n_finished, len(futures),
2332
- mem.used / 1e9, mem.total / 1e9,
2333
- 100 * mem.used / mem.total))
2626
+ logger.info(
2627
+ "Parallel bespoke futures collected: "
2628
+ "{} out of {}. Memory usage is {:.3f} GB out "
2629
+ "of {:.3f} GB ({:.2f}% utilized).".format(
2630
+ n_finished,
2631
+ len(futures),
2632
+ mem.used / 1e9,
2633
+ mem.total / 1e9,
2634
+ 100 * mem.used / mem.total,
2635
+ )
2636
+ )
2334
2637
 
2335
2638
  return out
2336
2639
 
@@ -2356,7 +2659,7 @@ class BespokeWindPlants(BaseAggregation):
2356
2659
  """
2357
2660
 
2358
2661
  # parallel job distribution test.
2359
- if self._obj_fun == 'test':
2662
+ if self._obj_fun == "test":
2360
2663
  return True
2361
2664
 
2362
2665
  if max_workers == 1:
@@ -2382,6 +2685,7 @@ class BespokeWindPlants(BaseAggregation):
2382
2685
  self._cap_cost_fun,
2383
2686
  self._foc_fun,
2384
2687
  self._voc_fun,
2688
+ self._bos_fun,
2385
2689
  min_spacing=self._min_spacing,
2386
2690
  wake_loss_multiplier=wlm,
2387
2691
  ga_kwargs=self._ga_kwargs,