NREL-reV 0.8.9__py3-none-any.whl → 0.9.2__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.
reV/bespoke/bespoke.py CHANGED
@@ -222,6 +222,7 @@ class BespokeSinglePlant:
222
222
  capital_cost_function,
223
223
  fixed_operating_cost_function,
224
224
  variable_operating_cost_function,
225
+ balance_of_system_cost_function,
225
226
  min_spacing="5x",
226
227
  wake_loss_multiplier=1,
227
228
  ga_kwargs=None,
@@ -262,19 +263,33 @@ class BespokeSinglePlant:
262
263
  return the objective to be minimized during layout optimization.
263
264
  Variables available are:
264
265
 
265
- - n_turbines: the number of turbines
266
- - system_capacity: wind plant capacity
267
- - aep: annual energy production
268
- - fixed_charge_rate: user input fixed_charge_rate if included
269
- as part of the sam system config.
270
- - self.wind_plant: the SAM wind plant object, through which
271
- all SAM variables can be accessed
272
- - 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
273
283
  by `capital_cost_function`
274
- - fixed_operating_cost: plant fixed annual operating cost as
275
- evaluated by `fixed_operating_cost_function`
276
- - variable_operating_cost: plant variable annual operating cost
277
- 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
278
293
 
279
294
  capital_cost_function : str
280
295
  The plant capital cost function as a string, must return the total
@@ -287,6 +302,16 @@ class BespokeSinglePlant:
287
302
  variable_operating_cost_function : str
288
303
  The plant annual variable operating cost function as a string, must
289
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
290
315
  variables as the objective_function.
291
316
  min_spacing : float | int | str
292
317
  Minimum spacing between turbines in meters. Can also be a string
@@ -431,6 +456,7 @@ class BespokeSinglePlant:
431
456
  self.variable_operating_cost_function = (
432
457
  variable_operating_cost_function
433
458
  )
459
+ self.balance_of_system_cost_function = balance_of_system_cost_function
434
460
  self.min_spacing = min_spacing
435
461
  self.wake_loss_multiplier = wake_loss_multiplier
436
462
  self.ga_kwargs = ga_kwargs or {}
@@ -540,7 +566,7 @@ class BespokeSinglePlant:
540
566
 
541
567
  # {meta_column: sam_sys_input_key}
542
568
  required = {
543
- SupplyCurveField.CAPACITY: "system_capacity",
569
+ SupplyCurveField.CAPACITY_AC_MW: "system_capacity",
544
570
  SupplyCurveField.TURBINE_X_COORDS: "wind_farm_xCoordinates",
545
571
  SupplyCurveField.TURBINE_Y_COORDS: "wind_farm_yCoordinates",
546
572
  }
@@ -812,28 +838,23 @@ class BespokeSinglePlant:
812
838
  [float(np.round(n, 1)) for n in self.sc_point.gid_counts]
813
839
  )
814
840
 
815
- with SupplyCurveExtent(
816
- self.sc_point._excl_fpath, resolution=self.sc_point.resolution
817
- ) as sc:
818
- row_ind, col_ind = sc.get_sc_row_col_ind(self.sc_point.gid)
819
-
820
841
  self._meta = pd.DataFrame(
821
842
  {
822
- SupplyCurveField.SC_POINT_GID: self.sc_point.gid,
823
- SupplyCurveField.SC_ROW_IND: row_ind,
824
- SupplyCurveField.SC_COL_IND: col_ind,
825
- SupplyCurveField.GID: self.sc_point.gid,
843
+ "gid": self.sc_point.gid, # needed for collection
826
844
  SupplyCurveField.LATITUDE: self.sc_point.latitude,
827
845
  SupplyCurveField.LONGITUDE: self.sc_point.longitude,
828
- SupplyCurveField.TIMEZONE: self.sc_point.timezone,
829
846
  SupplyCurveField.COUNTRY: self.sc_point.country,
830
847
  SupplyCurveField.STATE: self.sc_point.state,
831
848
  SupplyCurveField.COUNTY: self.sc_point.county,
832
849
  SupplyCurveField.ELEVATION: self.sc_point.elevation,
833
- SupplyCurveField.OFFSHORE: self.sc_point.offshore,
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,
834
854
  SupplyCurveField.RES_GIDS: res_gids,
835
855
  SupplyCurveField.GID_COUNTS: gid_counts,
836
856
  SupplyCurveField.N_GIDS: self.sc_point.n_gids,
857
+ SupplyCurveField.OFFSHORE: self.sc_point.offshore,
837
858
  SupplyCurveField.AREA_SQ_KM: self.sc_point.area,
838
859
  },
839
860
  index=[self.sc_point.gid],
@@ -1026,6 +1047,7 @@ class BespokeSinglePlant:
1026
1047
  self.capital_cost_function,
1027
1048
  self.fixed_operating_cost_function,
1028
1049
  self.variable_operating_cost_function,
1050
+ self.balance_of_system_cost_function,
1029
1051
  self.include_mask,
1030
1052
  self.pixel_side_length,
1031
1053
  self.min_spacing,
@@ -1046,13 +1068,13 @@ class BespokeSinglePlant:
1046
1068
  "multi-year mean AEP."
1047
1069
  )
1048
1070
 
1049
- fcr = lcoe_kwargs["fixed_charge_rate"]
1050
- cap_cost = lcoe_kwargs["capital_cost"]
1051
- foc = lcoe_kwargs["fixed_operating_cost"]
1052
- voc = lcoe_kwargs["variable_operating_cost"]
1053
- aep = self.outputs["annual_energy-means"]
1071
+ fcr = lcoe_kwargs['fixed_charge_rate']
1072
+ cc = lcoe_kwargs['capital_cost']
1073
+ foc = lcoe_kwargs['fixed_operating_cost']
1074
+ voc = lcoe_kwargs['variable_operating_cost']
1075
+ aep = self.outputs['annual_energy-means']
1054
1076
 
1055
- my_mean_lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc)
1077
+ my_mean_lcoe = lcoe_fcr(fcr, cc, foc, aep, voc)
1056
1078
 
1057
1079
  self._outputs["lcoe_fcr-means"] = my_mean_lcoe
1058
1080
  self._meta[SupplyCurveField.MEAN_LCOE] = my_mean_lcoe
@@ -1068,21 +1090,28 @@ class BespokeSinglePlant:
1068
1090
  sam_sys_inputs, normalized to the original system_capacity, and
1069
1091
  updated based on the bespoke optimized system_capacity, includes
1070
1092
  fixed_charge_rate, system_capacity (kW), capital_cost ($),
1071
- fixed_operating_cos ($), variable_operating_cost ($/kWh)
1072
- Data source priority: outputs, plant_optimizer,
1073
- 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
1074
1096
  """
1075
1097
 
1076
- kwargs_list = [
1077
- "fixed_charge_rate",
1078
- "system_capacity",
1079
- "capital_cost",
1080
- "fixed_operating_cost",
1081
- "variable_operating_cost",
1082
- ]
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
+ }
1083
1112
  lcoe_kwargs = {}
1084
1113
 
1085
- for kwarg in kwargs_list:
1114
+ for kwarg, meta_field in kwargs_map.items():
1086
1115
  if kwarg in self.outputs:
1087
1116
  lcoe_kwargs[kwarg] = self.outputs[kwarg]
1088
1117
  elif getattr(self.plant_optimizer, kwarg, None) is not None:
@@ -1092,11 +1121,13 @@ class BespokeSinglePlant:
1092
1121
  elif kwarg in self.meta:
1093
1122
  value = float(self.meta[kwarg].values[0])
1094
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
1095
1129
 
1096
- for k, v in lcoe_kwargs.items():
1097
- self._meta[k] = v
1098
-
1099
- 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]
1100
1131
  if any(missing):
1101
1132
  msg = (
1102
1133
  "Could not find these LCOE kwargs in outputs, "
@@ -1107,6 +1138,8 @@ class BespokeSinglePlant:
1107
1138
  logger.error(msg)
1108
1139
  raise KeyError(msg)
1109
1140
 
1141
+ bos = lcoe_kwargs.pop("balance_of_system_cost")
1142
+ lcoe_kwargs["capital_cost"] = lcoe_kwargs["capital_cost"] + bos
1110
1143
  return lcoe_kwargs
1111
1144
 
1112
1145
  @staticmethod
@@ -1153,19 +1186,18 @@ class BespokeSinglePlant:
1153
1186
  raise ModuleNotFoundError(msg)
1154
1187
 
1155
1188
  @staticmethod
1156
- def _check_sys_inputs(
1157
- plant1,
1158
- plant2,
1159
- ignore=(
1160
- "wind_resource_model_choice",
1161
- "wind_resource_data",
1162
- "wind_turbine_powercurve_powerout",
1163
- "hourly",
1164
- "capital_cost",
1165
- "fixed_operating_cost",
1166
- "variable_operating_cost",
1167
- ),
1168
- ):
1189
+ def _check_sys_inputs(plant1, plant2,
1190
+ ignore=('wind_resource_model_choice',
1191
+ 'wind_resource_data',
1192
+ 'wind_turbine_powercurve_powerout',
1193
+ 'hourly',
1194
+ 'capital_cost',
1195
+ 'fixed_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')):
1169
1201
  """Check two reV-SAM models for matching system inputs.
1170
1202
 
1171
1203
  Parameters
@@ -1230,9 +1262,14 @@ class BespokeSinglePlant:
1230
1262
 
1231
1263
  self._outputs.update(means)
1232
1264
 
1265
+ self._meta[SupplyCurveField.MEAN_RES] = self.res_df["windspeed"].mean()
1266
+ self._meta[SupplyCurveField.MEAN_CF_DC] = np.nan
1267
+ self._meta[SupplyCurveField.MEAN_CF_AC] = np.nan
1268
+ self._meta[SupplyCurveField.MEAN_LCOE] = np.nan
1269
+ self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = np.nan
1233
1270
  # copy dataset outputs to meta data for supply curve table summary
1234
1271
  if "cf_mean-means" in self.outputs:
1235
- self._meta.loc[:, SupplyCurveField.MEAN_CF] = self.outputs[
1272
+ self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[
1236
1273
  "cf_mean-means"
1237
1274
  ]
1238
1275
  if "lcoe_fcr-means" in self.outputs:
@@ -1240,6 +1277,10 @@ class BespokeSinglePlant:
1240
1277
  "lcoe_fcr-means"
1241
1278
  ]
1242
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
+ )
1243
1284
 
1244
1285
  logger.debug("Timeseries analysis complete!")
1245
1286
 
@@ -1268,9 +1309,12 @@ class BespokeSinglePlant:
1268
1309
  logger.exception(msg)
1269
1310
  raise RuntimeError(msg) from e
1270
1311
 
1271
- # TODO need to add:
1272
- # total cell area
1273
- # 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
1274
1318
 
1275
1319
  txc = [int(np.round(c)) for c in self.plant_optimizer.turbine_x]
1276
1320
  tyc = [int(np.round(c)) for c in self.plant_optimizer.turbine_y]
@@ -1284,58 +1328,92 @@ class BespokeSinglePlant:
1284
1328
 
1285
1329
  self._meta[SupplyCurveField.TURBINE_X_COORDS] = txc
1286
1330
  self._meta[SupplyCurveField.TURBINE_Y_COORDS] = tyc
1287
- self._meta["possible_x_coords"] = pxc
1288
- self._meta["possible_y_coords"] = pyc
1331
+ self._meta[SupplyCurveField.POSSIBLE_X_COORDS] = pxc
1332
+ self._meta[SupplyCurveField.POSSIBLE_Y_COORDS] = pyc
1289
1333
 
1290
- self._outputs["full_polygons"] = self.plant_optimizer.full_polygons
1291
- self._outputs["packing_polygons"] = (
1292
- self.plant_optimizer.packing_polygons
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
1293
1337
  )
1294
- self._outputs["system_capacity"] = self.plant_optimizer.capacity
1295
-
1296
- self._meta["n_turbines"] = self.plant_optimizer.nturbs
1297
- self._meta["bespoke_aep"] = self.plant_optimizer.aep
1298
- self._meta["bespoke_objective"] = self.plant_optimizer.objective
1299
- self._meta["bespoke_capital_cost"] = self.plant_optimizer.capital_cost
1300
- self._meta["bespoke_fixed_operating_cost"] = (
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] = (
1347
+ self.plant_optimizer.capital_cost
1348
+ )
1349
+ self._meta[SupplyCurveField.BESPOKE_FIXED_OPERATING_COST] = (
1301
1350
  self.plant_optimizer.fixed_operating_cost
1302
1351
  )
1303
- self._meta["bespoke_variable_operating_cost"] = (
1352
+ self._meta[SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST] = (
1304
1353
  self.plant_optimizer.variable_operating_cost
1305
1354
  )
1306
- self._meta["included_area"] = self.plant_optimizer.area
1307
- self._meta["included_area_capacity_density"] = (
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] = (
1308
1360
  self.plant_optimizer.capacity_density
1309
1361
  )
1310
- self._meta["convex_hull_area"] = self.plant_optimizer.convex_hull_area
1311
- self._meta["convex_hull_capacity_density"] = (
1362
+ self._meta[SupplyCurveField.CONVEX_HULL_AREA] = (
1363
+ self.plant_optimizer.convex_hull_area
1364
+ )
1365
+ self._meta[SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY] = (
1312
1366
  self.plant_optimizer.convex_hull_capacity_density
1313
1367
  )
1314
- self._meta["full_cell_capacity_density"] = (
1368
+ self._meta[SupplyCurveField.FULL_CELL_CAPACITY_DENSITY] = (
1315
1369
  self.plant_optimizer.full_cell_capacity_density
1316
1370
  )
1317
1371
 
1318
- logger.debug("Plant layout optimization complete!")
1319
-
1320
1372
  # copy dataset outputs to meta data for supply curve table summary
1321
1373
  # convert SAM system capacity in kW to reV supply curve cap in MW
1322
- self._meta[SupplyCurveField.CAPACITY] = (
1323
- self.outputs["system_capacity"] / 1e3
1324
- )
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] = np.nan
1325
1377
 
1326
1378
  # add required ReEDS multipliers to meta
1327
1379
  baseline_cost = self.plant_optimizer.capital_cost_per_kw(
1328
1380
  capacity_mw=self._baseline_cap_mw
1329
1381
  )
1330
- self._meta[SupplyCurveField.EOS_MULT] = (
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)
1386
+
1387
+ self._meta[SupplyCurveField.EOS_MULT] = eos_mult
1388
+ self._meta[SupplyCurveField.REG_MULT] = reg_mult
1389
+
1390
+ cap_cost = (
1331
1391
  self.plant_optimizer.capital_cost
1332
- / self.plant_optimizer.capacity
1333
- / baseline_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
1334
1408
  )
1335
- self._meta[SupplyCurveField.REG_MULT] = self.sam_sys_inputs.get(
1336
- "capital_cost_multiplier", 1
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
1337
1414
  )
1338
1415
 
1416
+ logger.debug("Plant layout optimization complete!")
1339
1417
  return self.outputs
1340
1418
 
1341
1419
  def agg_data_layers(self):
@@ -1402,36 +1480,18 @@ class BespokeSinglePlant:
1402
1480
  class BespokeWindPlants(BaseAggregation):
1403
1481
  """BespokeWindPlants"""
1404
1482
 
1405
- def __init__(
1406
- self,
1407
- excl_fpath,
1408
- res_fpath,
1409
- tm_dset,
1410
- objective_function,
1411
- capital_cost_function,
1412
- fixed_operating_cost_function,
1413
- variable_operating_cost_function,
1414
- project_points,
1415
- sam_files,
1416
- min_spacing="5x",
1417
- wake_loss_multiplier=1,
1418
- ga_kwargs=None,
1419
- output_request=("system_capacity", "cf_mean"),
1420
- ws_bins=(0.0, 20.0, 5.0),
1421
- wd_bins=(0.0, 360.0, 45.0),
1422
- excl_dict=None,
1423
- area_filter_kernel="queen",
1424
- min_area=None,
1425
- resolution=64,
1426
- excl_area=None,
1427
- data_layers=None,
1428
- pre_extract_inclusions=False,
1429
- prior_run=None,
1430
- gid_map=None,
1431
- bias_correct=None,
1432
- pre_load_data=False,
1433
- ):
1434
- r"""ReV bespoke analysis class.
1483
+ def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
1484
+ capital_cost_function, fixed_operating_cost_function,
1485
+ variable_operating_cost_function,
1486
+ balance_of_system_cost_function, project_points,
1487
+ sam_files, min_spacing='5x', wake_loss_multiplier=1,
1488
+ ga_kwargs=None, output_request=('system_capacity', 'cf_mean'),
1489
+ ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
1490
+ excl_dict=None, area_filter_kernel='queen', min_area=None,
1491
+ 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):
1494
+ """reV bespoke analysis class.
1435
1495
 
1436
1496
  Much like generation, ``reV`` bespoke analysis runs SAM
1437
1497
  simulations by piping in renewable energy resource data (usually
@@ -1503,17 +1563,30 @@ class BespokeWindPlants(BaseAggregation):
1503
1563
  - ``n_turbines``: the number of turbines
1504
1564
  - ``system_capacity``: wind plant capacity
1505
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.
1506
1577
  - ``fixed_charge_rate``: user input fixed_charge_rate if
1507
1578
  included as part of the sam system config.
1508
- - ``self.wind_plant``: the SAM wind plant object,
1509
- through which all SAM variables can be accessed
1510
1579
  - ``capital_cost``: plant capital cost as evaluated
1511
1580
  by `capital_cost_function`
1512
1581
  - ``fixed_operating_cost``: plant fixed annual operating
1513
1582
  cost as evaluated by `fixed_operating_cost_function`
1514
1583
  - ``variable_operating_cost``: plant variable annual
1515
- operating cost, as evaluated by
1584
+ operating cost as evaluated by
1516
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
1517
1590
 
1518
1591
  capital_cost_function : str
1519
1592
  The plant capital cost function written out as a string.
@@ -1530,6 +1603,13 @@ class BespokeWindPlants(BaseAggregation):
1530
1603
  out as a string. This expression must return the variable
1531
1604
  operating cost in $/kWh. This expression has access to the
1532
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.
1533
1613
  project_points : int | list | tuple | str | dict | pd.DataFrame | slice
1534
1614
  Input specifying which sites to process. A single integer
1535
1615
  representing the supply curve GID of a site may be specified
@@ -1538,7 +1618,7 @@ class BespokeWindPlants(BaseAggregation):
1538
1618
  multiple sites can be specified to evaluate ``reV`` at
1539
1619
  multiple specific locations. A string pointing to a project
1540
1620
  points CSV file may also be specified. Typically, the CSV
1541
- contains two columns:
1621
+ contains the following columns:
1542
1622
 
1543
1623
  - ``gid``: Integer specifying the supply curve GID of
1544
1624
  each site.
@@ -1555,16 +1635,18 @@ class BespokeWindPlants(BaseAggregation):
1555
1635
  site-specific capital cost value for each location). Columns
1556
1636
  that do not correspond to a config key may also be included,
1557
1637
  but they will be ignored. The CSV file input can also have
1558
- these extra columns:
1638
+ these extra, optional columns:
1559
1639
 
1560
1640
  - ``capital_cost_multiplier``
1561
1641
  - ``fixed_operating_cost_multiplier``
1562
1642
  - ``variable_operating_cost_multiplier``
1643
+ - ``balance_of_system_cost_multiplier``
1563
1644
 
1564
1645
  These particular inputs are treated as multipliers to be
1565
1646
  applied to the respective cost curves
1566
1647
  (`capital_cost_function`, `fixed_operating_cost_function`,
1567
- and `variable_operating_cost_function`) both during and
1648
+ `variable_operating_cost_function`, and
1649
+ `balance_of_system_cost_function`) both during and
1568
1650
  after the optimization. A DataFrame following the same
1569
1651
  guidelines as the CSV input (or a dictionary that can be
1570
1652
  used to initialize such a DataFrame) may be used for this
@@ -1839,30 +1921,23 @@ class BespokeWindPlants(BaseAggregation):
1839
1921
  """
1840
1922
 
1841
1923
  log_versions(logger)
1842
- logger.info("Initializing BespokeWindPlants...")
1843
- logger.info("Resource filepath: {}".format(res_fpath))
1844
- logger.info("Exclusion filepath: {}".format(excl_fpath))
1845
- logger.debug("Exclusion dict: {}".format(excl_dict))
1846
- logger.info(
1847
- "Bespoke objective function: {}".format(objective_function)
1848
- )
1849
- logger.info(
1850
- "Bespoke capital cost function: {}".format(capital_cost_function)
1851
- )
1852
- logger.info(
1853
- "Bespoke fixed operating cost function: {}".format(
1854
- fixed_operating_cost_function
1855
- )
1856
- )
1857
- logger.info(
1858
- "Bespoke variable operating cost function: {}".format(
1859
- variable_operating_cost_function
1860
- )
1861
- )
1862
- logger.info(
1863
- "Bespoke wake loss multiplier: {}".format(wake_loss_multiplier)
1864
- )
1865
- logger.info("Bespoke GA initialization kwargs: {}".format(ga_kwargs))
1924
+ logger.info('Initializing BespokeWindPlants...')
1925
+ logger.info('Resource filepath: {}'.format(res_fpath))
1926
+ logger.info('Exclusion filepath: {}'.format(excl_fpath))
1927
+ logger.debug('Exclusion dict: {}'.format(excl_dict))
1928
+ logger.info('Bespoke objective function: {}'
1929
+ .format(objective_function))
1930
+ logger.info('Bespoke capital cost function: {}'
1931
+ .format(capital_cost_function))
1932
+ logger.info('Bespoke fixed operating cost function: {}'
1933
+ .format(fixed_operating_cost_function))
1934
+ logger.info('Bespoke variable operating cost function: {}'
1935
+ .format(variable_operating_cost_function))
1936
+ logger.info('Bespoke balance of system cost function: {}'
1937
+ .format(balance_of_system_cost_function))
1938
+ logger.info('Bespoke wake loss multiplier: {}'
1939
+ .format(wake_loss_multiplier))
1940
+ logger.info('Bespoke GA initialization kwargs: {}'.format(ga_kwargs))
1866
1941
 
1867
1942
  logger.info(
1868
1943
  "Bespoke pre-extracting exclusions: {}".format(
@@ -1897,6 +1972,7 @@ class BespokeWindPlants(BaseAggregation):
1897
1972
  self._cap_cost_fun = capital_cost_function
1898
1973
  self._foc_fun = fixed_operating_cost_function
1899
1974
  self._voc_fun = variable_operating_cost_function
1975
+ self._bos_fun = balance_of_system_cost_function
1900
1976
  self._min_spacing = min_spacing
1901
1977
  self._wake_loss_multiplier = wake_loss_multiplier
1902
1978
  self._ga_kwargs = ga_kwargs or {}
@@ -1931,7 +2007,7 @@ class BespokeWindPlants(BaseAggregation):
1931
2007
  Slice or list specifying project points, string pointing to a
1932
2008
  project points csv, or a fully instantiated PointsControl object.
1933
2009
  Can also be a single site integer value. Points csv should have
1934
- `SupplyCurveField.GID` and 'config' column, the config maps to the
2010
+ `SiteDataField.GID` and 'config' column, the config maps to the
1935
2011
  sam_configs dict keys.
1936
2012
  sam_configs : dict | str | SAMConfig
1937
2013
  SAM input configuration ID(s) and file path(s). Keys are the SAM
@@ -1986,6 +2062,7 @@ class BespokeWindPlants(BaseAggregation):
1986
2062
 
1987
2063
  with Outputs(prior_run, mode="r") as f:
1988
2064
  meta = f.meta
2065
+ meta = meta.rename(columns=SupplyCurveField.map_from_legacy())
1989
2066
 
1990
2067
  # pylint: disable=no-member
1991
2068
  for col in meta.columns:
@@ -2011,7 +2088,7 @@ class BespokeWindPlants(BaseAggregation):
2011
2088
  meta = None
2012
2089
 
2013
2090
  if self._prior_meta is not None:
2014
- mask = self._prior_meta[SupplyCurveField.GID] == gid
2091
+ mask = self._prior_meta[SupplyCurveField.SC_POINT_GID] == gid
2015
2092
  if any(mask):
2016
2093
  meta = self._prior_meta[mask]
2017
2094
 
@@ -2363,37 +2440,21 @@ class BespokeWindPlants(BaseAggregation):
2363
2440
 
2364
2441
  # pylint: disable=arguments-renamed
2365
2442
  @classmethod
2366
- def run_serial(
2367
- cls,
2368
- excl_fpath,
2369
- res_fpath,
2370
- tm_dset,
2371
- sam_sys_inputs,
2372
- objective_function,
2373
- capital_cost_function,
2374
- fixed_operating_cost_function,
2375
- variable_operating_cost_function,
2376
- min_spacing="5x",
2377
- wake_loss_multiplier=1,
2378
- ga_kwargs=None,
2379
- output_request=("system_capacity", "cf_mean"),
2380
- ws_bins=(0.0, 20.0, 5.0),
2381
- wd_bins=(0.0, 360.0, 45.0),
2382
- excl_dict=None,
2383
- inclusion_mask=None,
2384
- area_filter_kernel="queen",
2385
- min_area=None,
2386
- resolution=64,
2387
- excl_area=0.0081,
2388
- data_layers=None,
2389
- gids=None,
2390
- exclusion_shape=None,
2391
- slice_lookup=None,
2392
- prior_meta=None,
2393
- gid_map=None,
2394
- bias_correct=None,
2395
- pre_loaded_data=None,
2396
- ):
2443
+ def run_serial(cls, excl_fpath, res_fpath, tm_dset,
2444
+ sam_sys_inputs, objective_function,
2445
+ capital_cost_function,
2446
+ fixed_operating_cost_function,
2447
+ variable_operating_cost_function,
2448
+ balance_of_system_cost_function,
2449
+ min_spacing='5x', wake_loss_multiplier=1, ga_kwargs=None,
2450
+ output_request=('system_capacity', 'cf_mean'),
2451
+ ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
2452
+ excl_dict=None, inclusion_mask=None,
2453
+ area_filter_kernel='queen', min_area=None,
2454
+ resolution=64, excl_area=0.0081, data_layers=None,
2455
+ gids=None, exclusion_shape=None, slice_lookup=None,
2456
+ prior_meta=None, gid_map=None, bias_correct=None,
2457
+ pre_loaded_data=None):
2397
2458
  """
2398
2459
  Standalone serial method to run bespoke optimization.
2399
2460
  See BespokeWindPlants docstring for parameter description.
@@ -2446,6 +2507,7 @@ class BespokeWindPlants(BaseAggregation):
2446
2507
  capital_cost_function,
2447
2508
  fixed_operating_cost_function,
2448
2509
  variable_operating_cost_function,
2510
+ balance_of_system_cost_function,
2449
2511
  min_spacing=min_spacing,
2450
2512
  wake_loss_multiplier=wake_loss_multiplier,
2451
2513
  ga_kwargs=ga_kwargs,
@@ -2523,39 +2585,37 @@ class BespokeWindPlants(BaseAggregation):
2523
2585
  rs, cs = self.slice_lookup[gid]
2524
2586
  gid_incl_mask = self._inclusion_mask[rs, cs]
2525
2587
 
2526
- futures.append(
2527
- exe.submit(
2528
- self.run_serial,
2529
- self._excl_fpath,
2530
- self._res_fpath,
2531
- self._tm_dset,
2532
- self.sam_sys_inputs_with_site_data(gid),
2533
- self._obj_fun,
2534
- self._cap_cost_fun,
2535
- self._foc_fun,
2536
- self._voc_fun,
2537
- self._min_spacing,
2538
- wake_loss_multiplier=self._wake_loss_multiplier,
2539
- ga_kwargs=self._ga_kwargs,
2540
- output_request=self._output_request,
2541
- ws_bins=self._ws_bins,
2542
- wd_bins=self._wd_bins,
2543
- excl_dict=self._excl_dict,
2544
- inclusion_mask=gid_incl_mask,
2545
- area_filter_kernel=self._area_filter_kernel,
2546
- min_area=self._min_area,
2547
- resolution=self._resolution,
2548
- excl_area=self._excl_area,
2549
- data_layers=self._data_layers,
2550
- gids=gid,
2551
- exclusion_shape=self.shape,
2552
- slice_lookup=copy.deepcopy(self.slice_lookup),
2553
- prior_meta=self._get_prior_meta(gid),
2554
- gid_map=self._gid_map,
2555
- bias_correct=self._get_bc_for_gid(gid),
2556
- pre_loaded_data=self._pre_loaded_data_for_sc_gid(gid),
2557
- )
2558
- )
2588
+ futures.append(exe.submit(
2589
+ self.run_serial,
2590
+ self._excl_fpath,
2591
+ self._res_fpath,
2592
+ self._tm_dset,
2593
+ self.sam_sys_inputs_with_site_data(gid),
2594
+ self._obj_fun,
2595
+ self._cap_cost_fun,
2596
+ self._foc_fun,
2597
+ self._voc_fun,
2598
+ self._bos_fun,
2599
+ self._min_spacing,
2600
+ wake_loss_multiplier=self._wake_loss_multiplier,
2601
+ ga_kwargs=self._ga_kwargs,
2602
+ output_request=self._output_request,
2603
+ ws_bins=self._ws_bins,
2604
+ wd_bins=self._wd_bins,
2605
+ excl_dict=self._excl_dict,
2606
+ inclusion_mask=gid_incl_mask,
2607
+ area_filter_kernel=self._area_filter_kernel,
2608
+ min_area=self._min_area,
2609
+ resolution=self._resolution,
2610
+ excl_area=self._excl_area,
2611
+ data_layers=self._data_layers,
2612
+ gids=gid,
2613
+ exclusion_shape=self.shape,
2614
+ slice_lookup=copy.deepcopy(self.slice_lookup),
2615
+ prior_meta=self._get_prior_meta(gid),
2616
+ gid_map=self._gid_map,
2617
+ bias_correct=self._get_bc_for_gid(gid),
2618
+ pre_loaded_data=self._pre_loaded_data_for_sc_gid(gid)))
2559
2619
 
2560
2620
  # gather results
2561
2621
  for future in as_completed(futures):
@@ -2617,35 +2677,34 @@ class BespokeWindPlants(BaseAggregation):
2617
2677
  wlm = self._wake_loss_multiplier
2618
2678
  i_bc = self._get_bc_for_gid(gid)
2619
2679
 
2620
- si = self.run_serial(
2621
- self._excl_fpath,
2622
- self._res_fpath,
2623
- self._tm_dset,
2624
- sam_inputs,
2625
- self._obj_fun,
2626
- self._cap_cost_fun,
2627
- self._foc_fun,
2628
- self._voc_fun,
2629
- min_spacing=self._min_spacing,
2630
- wake_loss_multiplier=wlm,
2631
- ga_kwargs=self._ga_kwargs,
2632
- output_request=self._output_request,
2633
- ws_bins=self._ws_bins,
2634
- wd_bins=self._wd_bins,
2635
- excl_dict=self._excl_dict,
2636
- inclusion_mask=gid_incl_mask,
2637
- area_filter_kernel=afk,
2638
- min_area=self._min_area,
2639
- resolution=self._resolution,
2640
- excl_area=self._excl_area,
2641
- data_layers=self._data_layers,
2642
- slice_lookup=slice_lookup,
2643
- prior_meta=prior_meta,
2644
- gid_map=self._gid_map,
2645
- bias_correct=i_bc,
2646
- gids=gid,
2647
- pre_loaded_data=pre_loaded_data,
2648
- )
2680
+ si = self.run_serial(self._excl_fpath,
2681
+ self._res_fpath,
2682
+ self._tm_dset,
2683
+ sam_inputs,
2684
+ self._obj_fun,
2685
+ self._cap_cost_fun,
2686
+ self._foc_fun,
2687
+ self._voc_fun,
2688
+ self._bos_fun,
2689
+ min_spacing=self._min_spacing,
2690
+ wake_loss_multiplier=wlm,
2691
+ ga_kwargs=self._ga_kwargs,
2692
+ output_request=self._output_request,
2693
+ ws_bins=self._ws_bins,
2694
+ wd_bins=self._wd_bins,
2695
+ excl_dict=self._excl_dict,
2696
+ inclusion_mask=gid_incl_mask,
2697
+ area_filter_kernel=afk,
2698
+ min_area=self._min_area,
2699
+ resolution=self._resolution,
2700
+ excl_area=self._excl_area,
2701
+ data_layers=self._data_layers,
2702
+ slice_lookup=slice_lookup,
2703
+ prior_meta=prior_meta,
2704
+ gid_map=self._gid_map,
2705
+ bias_correct=i_bc,
2706
+ gids=gid,
2707
+ pre_loaded_data=pre_loaded_data)
2649
2708
  self._outputs.update(si)
2650
2709
  else:
2651
2710
  self._outputs = self.run_parallel(max_workers=max_workers)