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
@@ -4,33 +4,60 @@ reV supply curve module
4
4
  - Calculation of LCOT
5
5
  - Supply Curve creation
6
6
  """
7
- from copy import deepcopy
8
7
  import json
9
8
  import logging
10
- import numpy as np
11
9
  import os
12
- import pandas as pd
10
+ from itertools import chain
11
+ from copy import deepcopy
13
12
  from warnings import warn
14
13
 
14
+ import numpy as np
15
+ import pandas as pd
16
+ from rex import Resource
17
+ from rex.utilities import SpawnProcessPool, parse_table
18
+
15
19
  from reV.handlers.transmission import TransmissionCosts as TC
16
20
  from reV.handlers.transmission import TransmissionFeatures as TF
17
21
  from reV.supply_curve.competitive_wind_farms import CompetitiveWindFarms
18
- from reV.utilities.exceptions import SupplyCurveInputError, SupplyCurveError
19
- from reV.utilities import log_versions
22
+ from reV.utilities import SupplyCurveField, log_versions
23
+ from reV.utilities.exceptions import SupplyCurveError, SupplyCurveInputError
20
24
 
21
- from rex import Resource
22
- from rex.utilities import parse_table, SpawnProcessPool
25
+ logger = logging.getLogger(__name__)
23
26
 
24
27
 
25
- logger = logging.getLogger(__name__)
28
+ # map is column name to relative order in which it should appear in output file
29
+ _REQUIRED_COMPUTE_AND_OUTPUT_COLS = {
30
+ SupplyCurveField.TRANS_GID: 0,
31
+ SupplyCurveField.TRANS_TYPE: 1,
32
+ SupplyCurveField.N_PARALLEL_TRANS: 2,
33
+ SupplyCurveField.DIST_SPUR_KM: 3,
34
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW: 10,
35
+ SupplyCurveField.LCOT: 11,
36
+ SupplyCurveField.TOTAL_LCOE: 12,
37
+ }
38
+ _REQUIRED_OUTPUT_COLS = {SupplyCurveField.DIST_EXPORT_KM: 4,
39
+ SupplyCurveField.REINFORCEMENT_DIST_KM: 5,
40
+ SupplyCurveField.TIE_LINE_COST_PER_MW: 6,
41
+ SupplyCurveField.CONNECTION_COST_PER_MW: 7,
42
+ SupplyCurveField.EXPORT_COST_PER_MW: 8,
43
+ SupplyCurveField.REINFORCEMENT_COST_PER_MW: 9,
44
+ SupplyCurveField.POI_LAT: 13,
45
+ SupplyCurveField.POI_LON: 14,
46
+ SupplyCurveField.REINFORCEMENT_POI_LAT: 15,
47
+ SupplyCurveField.REINFORCEMENT_POI_LON: 16}
48
+ DEFAULT_COLUMNS = tuple(str(field)
49
+ for field in chain(_REQUIRED_COMPUTE_AND_OUTPUT_COLS,
50
+ _REQUIRED_OUTPUT_COLS))
51
+ """Default output columns from supply chain computation (not ordered)"""
26
52
 
27
53
 
28
54
  class SupplyCurve:
29
55
  """SupplyCurve"""
30
56
 
31
57
  def __init__(self, sc_points, trans_table, sc_features=None,
32
- sc_capacity_col='capacity'):
33
- """reV LCOT calculation and SupplyCurve sorting class.
58
+ # str() to fix docs
59
+ sc_capacity_col=str(SupplyCurveField.CAPACITY_AC_MW)):
60
+ """ReV LCOT calculation and SupplyCurve sorting class.
34
61
 
35
62
  ``reV`` supply curve computes the transmission costs associated
36
63
  with each supply curve point output by ``reV`` supply curve
@@ -127,15 +154,17 @@ class SupplyCurve:
127
154
  (mean_lcoe_friction + lcot) ($/MWh).
128
155
  """
129
156
  log_versions(logger)
130
- logger.info('Supply curve points input: {}'.format(sc_points))
131
- logger.info('Transmission table input: {}'.format(trans_table))
132
- logger.info('Supply curve capacity column: {}'.format(sc_capacity_col))
157
+ logger.info("Supply curve points input: {}".format(sc_points))
158
+ logger.info("Transmission table input: {}".format(trans_table))
159
+ logger.info("Supply curve capacity column: {}".format(sc_capacity_col))
133
160
 
134
161
  self._sc_capacity_col = sc_capacity_col
135
- self._sc_points = self._parse_sc_points(sc_points,
136
- sc_features=sc_features)
137
- self._trans_table = self._map_tables(self._sc_points, trans_table,
138
- sc_capacity_col=sc_capacity_col)
162
+ self._sc_points = self._parse_sc_points(
163
+ sc_points, sc_features=sc_features
164
+ )
165
+ self._trans_table = self._map_tables(
166
+ self._sc_points, trans_table, sc_capacity_col=sc_capacity_col
167
+ )
139
168
  self._sc_gids, self._mask = self._parse_sc_gids(self._trans_table)
140
169
 
141
170
  def __repr__(self):
@@ -177,31 +206,43 @@ class SupplyCurve:
177
206
  DataFrame of supply curve point summary with additional features
178
207
  added if supplied
179
208
  """
180
- if isinstance(sc_points, str) and sc_points.endswith('.h5'):
209
+ if isinstance(sc_points, str) and sc_points.endswith(".h5"):
181
210
  with Resource(sc_points) as res:
182
211
  sc_points = res.meta
183
- sc_points.index.name = 'sc_gid'
212
+ sc_points.index.name = SupplyCurveField.SC_GID
184
213
  sc_points = sc_points.reset_index()
185
214
  else:
186
215
  sc_points = parse_table(sc_points)
216
+ sc_points = sc_points.rename(
217
+ columns=SupplyCurveField.map_from_legacy())
187
218
 
188
- logger.debug('Supply curve points table imported with columns: {}'
189
- .format(sc_points.columns.values.tolist()))
219
+ logger.debug(
220
+ "Supply curve points table imported with columns: {}".format(
221
+ sc_points.columns.values.tolist()
222
+ )
223
+ )
190
224
 
191
225
  if sc_features is not None:
192
226
  sc_features = parse_table(sc_features)
193
- merge_cols = [c for c in sc_features
194
- if c in sc_points]
195
- sc_points = sc_points.merge(sc_features, on=merge_cols, how='left')
196
- logger.debug('Adding Supply Curve Features table with columns: {}'
197
- .format(sc_features.columns.values.tolist()))
198
-
199
- if 'transmission_multiplier' in sc_points:
200
- col = 'transmission_multiplier'
227
+ sc_features = sc_features.rename(
228
+ columns=SupplyCurveField.map_from_legacy())
229
+ merge_cols = [c for c in sc_features if c in sc_points]
230
+ sc_points = sc_points.merge(sc_features, on=merge_cols, how="left")
231
+ logger.debug(
232
+ "Adding Supply Curve Features table with columns: {}".format(
233
+ sc_features.columns.values.tolist()
234
+ )
235
+ )
236
+
237
+ if "transmission_multiplier" in sc_points:
238
+ col = "transmission_multiplier"
201
239
  sc_points.loc[:, col] = sc_points.loc[:, col].fillna(1)
202
240
 
203
- logger.debug('Final supply curve points table has columns: {}'
204
- .format(sc_points.columns.values.tolist()))
241
+ logger.debug(
242
+ "Final supply curve points table has columns: {}".format(
243
+ sc_points.columns.values.tolist()
244
+ )
245
+ )
205
246
 
206
247
  return sc_points
207
248
 
@@ -222,18 +263,20 @@ class SupplyCurve:
222
263
  Columns to merge on which maps the sc columns (keys) to the
223
264
  corresponding trans table columns (values)
224
265
  """
225
- sc_columns = [c for c in sc_columns if c.startswith('sc_')]
226
- trans_columns = [c for c in trans_columns if c.startswith('sc_')]
266
+ sc_columns = [c for c in sc_columns if c.startswith("sc_")]
267
+ trans_columns = [c for c in trans_columns if c.startswith("sc_")]
227
268
  merge_cols = {}
228
- for c_val in ['row', 'col']:
269
+ for c_val in ["row", "col"]:
229
270
  trans_col = [c for c in trans_columns if c_val in c]
230
271
  sc_col = [c for c in sc_columns if c_val in c]
231
272
  if trans_col and sc_col:
232
273
  merge_cols[sc_col[0]] = trans_col[0]
233
274
 
234
275
  if len(merge_cols) != 2:
235
- msg = ('Did not find a unique set of sc row and column ids to '
236
- 'merge on: {}'.format(merge_cols))
276
+ msg = (
277
+ "Did not find a unique set of sc row and column ids to "
278
+ "merge on: {}".format(merge_cols)
279
+ )
237
280
  logger.error(msg)
238
281
  raise RuntimeError(msg)
239
282
 
@@ -266,24 +309,32 @@ class SupplyCurve:
266
309
  # legacy name: trans_gids
267
310
  # also xformer_cost_p_mw -> xformer_cost_per_mw (not sure why there
268
311
  # would be a *_p_mw but here we are...)
269
- rename_map = {'trans_line_gid': 'trans_gid',
270
- 'trans_gids': 'trans_line_gids',
271
- 'xformer_cost_p_mw': 'xformer_cost_per_mw'}
312
+ rename_map = {
313
+ "trans_line_gid": SupplyCurveField.TRANS_GID,
314
+ "trans_gids": "trans_line_gids",
315
+ "xformer_cost_p_mw": "xformer_cost_per_mw",
316
+ }
272
317
  trans_table = trans_table.rename(columns=rename_map)
273
318
 
274
- if 'dist_mi' in trans_table and 'dist_km' not in trans_table:
275
- trans_table = trans_table.rename(columns={'dist_mi': 'dist_km'})
276
- trans_table['dist_km'] *= 1.60934
319
+ contains_dist_in_miles = "dist_mi" in trans_table
320
+ missing_km_dist = SupplyCurveField.DIST_SPUR_KM not in trans_table
321
+ if contains_dist_in_miles and missing_km_dist:
322
+ trans_table = trans_table.rename(
323
+ columns={"dist_mi": SupplyCurveField.DIST_SPUR_KM}
324
+ )
325
+ trans_table[SupplyCurveField.DIST_SPUR_KM] *= 1.60934
277
326
 
278
- drop_cols = ['sc_gid', 'cap_left', 'sc_point_gid']
327
+ drop_cols = [SupplyCurveField.SC_GID, 'cap_left',
328
+ SupplyCurveField.SC_POINT_GID]
279
329
  drop_cols = [c for c in drop_cols if c in trans_table]
280
330
  if drop_cols:
281
331
  trans_table = trans_table.drop(columns=drop_cols)
282
332
 
283
- return trans_table
333
+ return trans_table.rename(columns=SupplyCurveField.map_from_legacy())
284
334
 
285
335
  @staticmethod
286
- def _map_trans_capacity(trans_sc_table, sc_capacity_col='capacity'):
336
+ def _map_trans_capacity(trans_sc_table,
337
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
287
338
  """
288
339
  Map SC gids to transmission features based on capacity. For any SC
289
340
  gids with capacity > the maximum transmission feature capacity, map
@@ -310,33 +361,42 @@ class SupplyCurve:
310
361
  based on maximum capacity
311
362
  """
312
363
 
313
- nx = trans_sc_table[sc_capacity_col] / trans_sc_table['max_cap']
364
+ nx = trans_sc_table[sc_capacity_col] / trans_sc_table["max_cap"]
314
365
  nx = np.ceil(nx).astype(int)
315
- trans_sc_table['n_parallel_trans'] = nx
366
+ trans_sc_table[SupplyCurveField.N_PARALLEL_TRANS] = nx
316
367
 
317
368
  if (nx > 1).any():
318
369
  mask = nx > 1
319
- tie_line_cost = (trans_sc_table.loc[mask, 'tie_line_cost']
320
- * nx[mask])
321
-
322
- xformer_cost = (trans_sc_table.loc[mask, 'xformer_cost_per_mw']
323
- * trans_sc_table.loc[mask, 'max_cap'] * nx[mask])
324
-
325
- conn_cost = (xformer_cost
326
- + trans_sc_table.loc[mask, 'sub_upgrade_cost']
327
- + trans_sc_table.loc[mask, 'new_sub_cost'])
370
+ tie_line_cost = (
371
+ trans_sc_table.loc[mask, "tie_line_cost"] * nx[mask]
372
+ )
373
+
374
+ xformer_cost = (
375
+ trans_sc_table.loc[mask, "xformer_cost_per_mw"]
376
+ * trans_sc_table.loc[mask, "max_cap"]
377
+ * nx[mask]
378
+ )
379
+
380
+ conn_cost = (
381
+ xformer_cost
382
+ + trans_sc_table.loc[mask, "sub_upgrade_cost"]
383
+ + trans_sc_table.loc[mask, "new_sub_cost"]
384
+ )
328
385
 
329
386
  trans_cap_cost = tie_line_cost + conn_cost
330
387
 
331
- trans_sc_table.loc[mask, 'tie_line_cost'] = tie_line_cost
332
- trans_sc_table.loc[mask, 'xformer_cost'] = xformer_cost
333
- trans_sc_table.loc[mask, 'connection_cost'] = conn_cost
334
- trans_sc_table.loc[mask, 'trans_cap_cost'] = trans_cap_cost
335
-
336
- msg = ("{} SC points have a capacity that exceeds the maximum "
337
- "transmission feature capacity and will be connected with "
338
- "multiple parallel transmission features."
339
- .format((nx > 1).sum()))
388
+ trans_sc_table.loc[mask, "tie_line_cost"] = tie_line_cost
389
+ trans_sc_table.loc[mask, "xformer_cost"] = xformer_cost
390
+ trans_sc_table.loc[mask, "connection_cost"] = conn_cost
391
+ trans_sc_table.loc[mask, "trans_cap_cost"] = trans_cap_cost
392
+
393
+ msg = (
394
+ "{} SC points have a capacity that exceeds the maximum "
395
+ "transmission feature capacity and will be connected with "
396
+ "multiple parallel transmission features.".format(
397
+ (nx > 1).sum()
398
+ )
399
+ )
340
400
  logger.info(msg)
341
401
 
342
402
  return trans_sc_table
@@ -379,24 +439,31 @@ class SupplyCurve:
379
439
  List of missing transmission line 'trans_gid's for all substations
380
440
  in features table
381
441
  """
382
- features = features.rename(columns={'trans_line_gid': 'trans_gid',
383
- 'trans_gids': 'trans_line_gids'})
384
- mask = features['category'].str.lower() == 'substation'
442
+ features = features.rename(
443
+ columns={
444
+ "trans_line_gid": SupplyCurveField.TRANS_GID,
445
+ "trans_gids": "trans_line_gids",
446
+ }
447
+ )
448
+ mask = (features[SupplyCurveField.TRANS_TYPE].str.casefold()
449
+ == "substation")
385
450
 
386
451
  if not any(mask):
387
452
  return []
388
453
 
389
- line_gids = (features.loc[mask, 'trans_line_gids']
390
- .apply(cls._parse_trans_line_gids))
454
+ line_gids = features.loc[mask, "trans_line_gids"].apply(
455
+ cls._parse_trans_line_gids
456
+ )
391
457
 
392
458
  line_gids = np.unique(np.concatenate(line_gids.values))
393
459
 
394
- test = np.isin(line_gids, features['trans_gid'].values)
460
+ test = np.isin(line_gids, features[SupplyCurveField.TRANS_GID].values)
395
461
 
396
462
  return line_gids[~test].tolist()
397
463
 
398
464
  @classmethod
399
- def _check_substation_conns(cls, trans_table, sc_cols='sc_gid'):
465
+ def _check_substation_conns(cls, trans_table,
466
+ sc_cols=SupplyCurveField.SC_GID):
400
467
  """
401
468
  Run checks on substation transmission features to make sure that
402
469
  every sc point connecting to a substation can also connect to its
@@ -409,7 +476,7 @@ class SupplyCurve:
409
476
  (should already be merged with SC points).
410
477
  sc_cols : str | list, optional
411
478
  Column(s) in trans_table with unique supply curve id,
412
- by default 'sc_gid'
479
+ by default SupplyCurveField.SC_GID
413
480
  """
414
481
  missing = {}
415
482
  for sc_point, sc_table in trans_table.groupby(sc_cols):
@@ -418,10 +485,13 @@ class SupplyCurve:
418
485
  missing[sc_point] = tl_gids
419
486
 
420
487
  if any(missing):
421
- msg = ('The following sc_gid (keys) were connected to substations '
422
- 'but were not connected to the respective transmission line'
423
- ' gids (values) which is required for full SC sort: {}'
424
- .format(missing))
488
+ msg = (
489
+ "The following sc_gid (keys) were connected to substations "
490
+ "but were not connected to the respective transmission line"
491
+ " gids (values) which is required for full SC sort: {}".format(
492
+ missing
493
+ )
494
+ )
425
495
  logger.error(msg)
426
496
  raise SupplyCurveInputError(msg)
427
497
 
@@ -437,37 +507,50 @@ class SupplyCurve:
437
507
  Table mapping supply curve points to transmission features
438
508
  (should already be merged with SC points).
439
509
  """
440
- sc_gids = set(sc_points['sc_gid'].unique())
441
- trans_sc_gids = set(trans_table['sc_gid'].unique())
510
+ sc_gids = set(sc_points[SupplyCurveField.SC_GID].unique())
511
+ trans_sc_gids = set(trans_table[SupplyCurveField.SC_GID].unique())
442
512
  missing = sorted(list(sc_gids - trans_sc_gids))
443
513
  if any(missing):
444
- msg = ("There are {} Supply Curve points with missing "
445
- "transmission mappings. Supply curve points with no "
446
- "transmission features will not be connected! "
447
- "Missing sc_gid's: {}"
448
- .format(len(missing), missing))
514
+ msg = (
515
+ "There are {} Supply Curve points with missing "
516
+ "transmission mappings. Supply curve points with no "
517
+ "transmission features will not be connected! "
518
+ "Missing sc_gid's: {}".format(len(missing), missing)
519
+ )
449
520
  logger.warning(msg)
450
521
  warn(msg)
451
522
 
452
523
  if not any(trans_sc_gids) or not any(sc_gids):
453
- msg = ('Merging of sc points table and transmission features '
454
- 'table failed with {} original sc gids and {} transmission '
455
- 'sc gids after table merge.'
456
- .format(len(sc_gids), len(trans_sc_gids)))
524
+ msg = (
525
+ "Merging of sc points table and transmission features "
526
+ "table failed with {} original sc gids and {} transmission "
527
+ "sc gids after table merge.".format(
528
+ len(sc_gids), len(trans_sc_gids)
529
+ )
530
+ )
457
531
  logger.error(msg)
458
532
  raise SupplyCurveError(msg)
459
533
 
460
- logger.debug('There are {} original SC gids and {} sc gids in the '
461
- 'merged transmission table.'
462
- .format(len(sc_gids), len(trans_sc_gids)))
463
- logger.debug('Transmission Table created with columns: {}'
464
- .format(trans_table.columns.values.tolist()))
534
+ logger.debug(
535
+ "There are {} original SC gids and {} sc gids in the "
536
+ "merged transmission table.".format(
537
+ len(sc_gids), len(trans_sc_gids)
538
+ )
539
+ )
540
+ logger.debug(
541
+ "Transmission Table created with columns: {}".format(
542
+ trans_table.columns.values.tolist()
543
+ )
544
+ )
465
545
 
466
546
  @classmethod
467
547
  def _merge_sc_trans_tables(cls, sc_points, trans_table,
468
- sc_cols=('sc_gid', 'capacity', 'mean_cf',
469
- 'mean_lcoe'),
470
- sc_capacity_col='capacity'):
548
+ sc_cols=(SupplyCurveField.SC_GID,
549
+ SupplyCurveField.CAPACITY_AC_MW,
550
+ SupplyCurveField.MEAN_CF_AC,
551
+ SupplyCurveField.MEAN_LCOE),
552
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW
553
+ ):
471
554
  """
472
555
  Merge the supply curve table with the transmission features table.
473
556
 
@@ -482,7 +565,8 @@ class SupplyCurve:
482
565
  sc_cols : tuple | list, optional
483
566
  List of column from sc_points to transfer into the trans table,
484
567
  If the `sc_capacity_col` is not included, it will get added.
485
- by default ('sc_gid', 'capacity', 'mean_cf', 'mean_lcoe')
568
+ by default (SupplyCurveField.SC_GID, 'capacity', 'mean_cf',
569
+ 'mean_lcoe')
486
570
  sc_capacity_col : str, optional
487
571
  Name of capacity column in `trans_sc_table`. The values in
488
572
  this column determine the size of transmission lines built.
@@ -505,42 +589,57 @@ class SupplyCurve:
505
589
  if isinstance(trans_table, (list, tuple)):
506
590
  trans_sc_table = []
507
591
  for table in trans_table:
508
- trans_sc_table.append(cls._merge_sc_trans_tables(
509
- sc_points, table, sc_cols=sc_cols,
510
- sc_capacity_col=sc_capacity_col))
592
+ trans_sc_table.append(
593
+ cls._merge_sc_trans_tables(
594
+ sc_points,
595
+ table,
596
+ sc_cols=sc_cols,
597
+ sc_capacity_col=sc_capacity_col,
598
+ )
599
+ )
511
600
 
512
601
  trans_sc_table = pd.concat(trans_sc_table)
513
602
  else:
514
603
  trans_table = cls._parse_trans_table(trans_table)
515
604
 
516
- merge_cols = cls._get_merge_cols(sc_points.columns,
517
- trans_table.columns)
518
- logger.info('Merging SC table and Trans Table with '
519
- '{} mapping: {}'
520
- .format('sc_table_col: trans_table_col', merge_cols))
605
+ merge_cols = cls._get_merge_cols(
606
+ sc_points.columns, trans_table.columns
607
+ )
608
+ logger.info(
609
+ "Merging SC table and Trans Table with "
610
+ "{} mapping: {}".format(
611
+ "sc_table_col: trans_table_col", merge_cols
612
+ )
613
+ )
521
614
  sc_points = sc_points.rename(columns=merge_cols)
522
615
  merge_cols = list(merge_cols.values())
523
616
 
524
617
  if isinstance(sc_cols, tuple):
525
618
  sc_cols = list(sc_cols)
526
619
 
527
- if 'mean_lcoe_friction' in sc_points:
528
- sc_cols.append('mean_lcoe_friction')
529
-
530
- if 'transmission_multiplier' in sc_points:
531
- sc_cols.append('transmission_multiplier')
620
+ extra_cols = [SupplyCurveField.CAPACITY_DC_MW,
621
+ SupplyCurveField.MEAN_CF_DC,
622
+ SupplyCurveField.MEAN_LCOE_FRICTION,
623
+ "transmission_multiplier"]
624
+ for col in extra_cols:
625
+ if col in sc_points:
626
+ sc_cols.append(col)
532
627
 
533
628
  sc_cols += merge_cols
534
629
  sc_points = sc_points[sc_cols].copy()
535
- trans_sc_table = trans_table.merge(sc_points, on=merge_cols,
536
- how='inner')
630
+ trans_sc_table = trans_table.merge(
631
+ sc_points, on=merge_cols, how="inner"
632
+ )
537
633
 
538
634
  return trans_sc_table
539
635
 
540
636
  @classmethod
541
637
  def _map_tables(cls, sc_points, trans_table,
542
- sc_cols=('sc_gid', 'capacity', 'mean_cf', 'mean_lcoe'),
543
- sc_capacity_col='capacity'):
638
+ sc_cols=(SupplyCurveField.SC_GID,
639
+ SupplyCurveField.CAPACITY_AC_MW,
640
+ SupplyCurveField.MEAN_CF_AC,
641
+ SupplyCurveField.MEAN_LCOE),
642
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
544
643
  """
545
644
  Map supply curve points to transmission features
546
645
 
@@ -555,7 +654,9 @@ class SupplyCurve:
555
654
  sc_cols : tuple | list, optional
556
655
  List of column from sc_points to transfer into the trans table,
557
656
  If the `sc_capacity_col` is not included, it will get added.
558
- by default ('sc_gid', 'capacity', 'mean_cf', 'mean_lcoe')
657
+ by default (SupplyCurveField.SC_GID,
658
+ SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF_AC,
659
+ SupplyCurveField.MEAN_LCOE)
559
660
  sc_capacity_col : str, optional
560
661
  Name of capacity column in `trans_sc_table`. The values in
561
662
  this column determine the size of transmission lines built.
@@ -573,17 +674,18 @@ class SupplyCurve:
573
674
  This is performed by an inner merging with trans_table
574
675
  """
575
676
  scc = sc_capacity_col
576
- trans_sc_table = cls._merge_sc_trans_tables(sc_points, trans_table,
577
- sc_cols=sc_cols,
578
- sc_capacity_col=scc)
677
+ trans_sc_table = cls._merge_sc_trans_tables(
678
+ sc_points, trans_table, sc_cols=sc_cols, sc_capacity_col=scc
679
+ )
579
680
 
580
- if 'max_cap' in trans_sc_table:
581
- trans_sc_table = cls._map_trans_capacity(trans_sc_table,
582
- sc_capacity_col=scc)
681
+ if "max_cap" in trans_sc_table:
682
+ trans_sc_table = cls._map_trans_capacity(
683
+ trans_sc_table, sc_capacity_col=scc
684
+ )
583
685
 
584
- trans_sc_table = \
585
- trans_sc_table.sort_values(
586
- ['sc_gid', 'trans_gid']).reset_index(drop=True)
686
+ sort_cols = [SupplyCurveField.SC_GID, SupplyCurveField.TRANS_GID]
687
+ trans_sc_table = trans_sc_table.sort_values(sort_cols)
688
+ trans_sc_table = trans_sc_table.reset_index(drop=True)
587
689
 
588
690
  cls._check_sc_trans_table(sc_points, trans_sc_table)
589
691
 
@@ -619,13 +721,14 @@ class SupplyCurve:
619
721
  else:
620
722
  kwargs = {}
621
723
 
622
- trans_features = TF(trans_table, avail_cap_frac=avail_cap_frac,
623
- **kwargs)
724
+ trans_features = TF(
725
+ trans_table, avail_cap_frac=avail_cap_frac, **kwargs
726
+ )
624
727
 
625
728
  return trans_features
626
729
 
627
730
  @staticmethod
628
- def _parse_sc_gids(trans_table, gid_key='sc_gid'):
731
+ def _parse_sc_gids(trans_table, gid_key=SupplyCurveField.SC_GID):
629
732
  """Extract unique sc gids, make bool mask from tranmission table
630
733
 
631
734
  Parameters
@@ -652,7 +755,7 @@ class SupplyCurve:
652
755
 
653
756
  @staticmethod
654
757
  def _get_capacity(sc_gid, sc_table, connectable=True,
655
- sc_capacity_col='capacity'):
758
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
656
759
  """
657
760
  Get capacity of supply curve point
658
761
 
@@ -686,9 +789,10 @@ class SupplyCurve:
686
789
  if len(capacity) == 1:
687
790
  capacity = capacity[0]
688
791
  else:
689
- msg = ('Each supply curve point should only have '
690
- 'a single capacity, but {} has {}'
691
- .format(sc_gid, capacity))
792
+ msg = (
793
+ "Each supply curve point should only have "
794
+ "a single capacity, but {} has {}".format(sc_gid, capacity)
795
+ )
692
796
  logger.error(msg)
693
797
  raise RuntimeError(msg)
694
798
  else:
@@ -700,7 +804,8 @@ class SupplyCurve:
700
804
  def _compute_trans_cap_cost(cls, trans_table, trans_costs=None,
701
805
  avail_cap_frac=1, max_workers=None,
702
806
  connectable=True, line_limited=False,
703
- sc_capacity_col='capacity'):
807
+ sc_capacity_col=(
808
+ SupplyCurveField.CAPACITY_AC_MW)):
704
809
  """
705
810
  Compute levelized cost of transmission for all combinations of
706
811
  supply curve points and tranmission features in trans_table
@@ -749,9 +854,11 @@ class SupplyCurve:
749
854
  """
750
855
  scc = sc_capacity_col
751
856
  if scc not in trans_table:
752
- raise SupplyCurveInputError('Supply curve table must have '
753
- 'supply curve point capacity column'
754
- '({}) to compute lcot'.format(scc))
857
+ raise SupplyCurveInputError(
858
+ "Supply curve table must have "
859
+ "supply curve point capacity column"
860
+ "({}) to compute lcot".format(scc)
861
+ )
755
862
 
756
863
  if trans_costs is not None:
757
864
  trans_costs = TF._parse_dictionary(trans_costs)
@@ -762,44 +869,66 @@ class SupplyCurve:
762
869
  max_workers = os.cpu_count()
763
870
 
764
871
  logger.info('Computing LCOT costs for all possible connections...')
765
- groups = trans_table.groupby('sc_gid')
872
+ groups = trans_table.groupby(SupplyCurveField.SC_GID)
766
873
  if max_workers > 1:
767
- loggers = [__name__, 'reV.handlers.transmission', 'reV']
768
- with SpawnProcessPool(max_workers=max_workers,
769
- loggers=loggers) as exe:
874
+ loggers = [__name__, "reV.handlers.transmission", "reV"]
875
+ with SpawnProcessPool(
876
+ max_workers=max_workers, loggers=loggers
877
+ ) as exe:
770
878
  futures = []
771
879
  for sc_gid, sc_table in groups:
772
- capacity = cls._get_capacity(sc_gid, sc_table,
773
- connectable=connectable,
774
- sc_capacity_col=scc)
775
- futures.append(exe.submit(TC.feature_costs, sc_table,
776
- capacity=capacity,
777
- avail_cap_frac=avail_cap_frac,
778
- line_limited=line_limited,
779
- **trans_costs))
880
+ capacity = cls._get_capacity(
881
+ sc_gid,
882
+ sc_table,
883
+ connectable=connectable,
884
+ sc_capacity_col=scc,
885
+ )
886
+ futures.append(
887
+ exe.submit(
888
+ TC.feature_costs,
889
+ sc_table,
890
+ capacity=capacity,
891
+ avail_cap_frac=avail_cap_frac,
892
+ line_limited=line_limited,
893
+ **trans_costs,
894
+ )
895
+ )
780
896
 
781
897
  cost = [future.result() for future in futures]
782
898
  else:
783
899
  cost = []
784
900
  for sc_gid, sc_table in groups:
785
- capacity = cls._get_capacity(sc_gid, sc_table,
786
- connectable=connectable,
787
- sc_capacity_col=scc)
788
- cost.append(TC.feature_costs(sc_table,
789
- capacity=capacity,
790
- avail_cap_frac=avail_cap_frac,
791
- line_limited=line_limited,
792
- **trans_costs))
793
-
794
- cost = np.hstack(cost).astype('float32')
795
- logger.info('LCOT cost calculation is complete.')
901
+ capacity = cls._get_capacity(
902
+ sc_gid,
903
+ sc_table,
904
+ connectable=connectable,
905
+ sc_capacity_col=scc,
906
+ )
907
+ cost.append(
908
+ TC.feature_costs(
909
+ sc_table,
910
+ capacity=capacity,
911
+ avail_cap_frac=avail_cap_frac,
912
+ line_limited=line_limited,
913
+ **trans_costs,
914
+ )
915
+ )
916
+
917
+ cost = np.hstack(cost).astype("float32")
918
+ logger.info("LCOT cost calculation is complete.")
796
919
 
797
920
  return cost
798
921
 
799
- def compute_total_lcoe(self, fcr, transmission_costs=None,
800
- avail_cap_frac=1, line_limited=False,
801
- connectable=True, max_workers=None,
802
- consider_friction=True):
922
+ def compute_total_lcoe(
923
+ self,
924
+ fcr,
925
+ transmission_costs=None,
926
+ avail_cap_frac=1,
927
+ line_limited=False,
928
+ connectable=True,
929
+ max_workers=None,
930
+ consider_friction=True,
931
+ ):
803
932
  """
804
933
  Compute LCOT and total LCOE for all sc point to transmission feature
805
934
  connections
@@ -828,45 +957,65 @@ class SupplyCurve:
828
957
  Flag to consider friction layer on LCOE when "mean_lcoe_friction"
829
958
  is in the sc points input, by default True
830
959
  """
831
- if 'trans_cap_cost' not in self._trans_table:
960
+ tcc_per_mw_col = SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW
961
+ if tcc_per_mw_col in self._trans_table:
962
+ cost = self._trans_table[tcc_per_mw_col].values.copy()
963
+ elif "trans_cap_cost" not in self._trans_table:
832
964
  scc = self._sc_capacity_col
833
- cost = self._compute_trans_cap_cost(self._trans_table,
834
- trans_costs=transmission_costs,
835
- avail_cap_frac=avail_cap_frac,
836
- line_limited=line_limited,
837
- connectable=connectable,
838
- max_workers=max_workers,
839
- sc_capacity_col=scc)
840
- self._trans_table['trans_cap_cost_per_mw'] = cost # $/MW
965
+ cost = self._compute_trans_cap_cost(
966
+ self._trans_table,
967
+ trans_costs=transmission_costs,
968
+ avail_cap_frac=avail_cap_frac,
969
+ line_limited=line_limited,
970
+ connectable=connectable,
971
+ max_workers=max_workers,
972
+ sc_capacity_col=scc,
973
+ )
974
+ self._trans_table[tcc_per_mw_col] = cost # $/MW
841
975
  else:
842
- cost = self._trans_table['trans_cap_cost'].values.copy() # $
843
- cost /= self._trans_table[self._sc_capacity_col] # $/MW
844
- self._trans_table['trans_cap_cost_per_mw'] = cost
845
-
846
- cost *= self._trans_table[self._sc_capacity_col]
847
- cost /= self._trans_table['capacity'] # align with "mean_cf"
848
-
849
- if 'reinforcement_cost_per_mw' in self._trans_table:
850
- logger.info("'reinforcement_cost_per_mw' column found in "
851
- "transmission table. Adding reinforcement costs "
852
- "to total LCOE.")
853
- cf_mean_arr = self._trans_table['mean_cf'].values
854
- lcot = (cost * fcr) / (cf_mean_arr * 8760)
855
- lcoe = lcot + self._trans_table['mean_lcoe']
856
- self._trans_table['lcot_no_reinforcement'] = lcot
857
- self._trans_table['lcoe_no_reinforcement'] = lcoe
858
- r_cost = (self._trans_table['reinforcement_cost_per_mw']
859
- .values.copy())
860
- r_cost *= self._trans_table[self._sc_capacity_col]
861
- r_cost /= self._trans_table['capacity'] # align with "mean_cf"
976
+ cost = self._trans_table["trans_cap_cost"].values.copy() # $
977
+ cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] # $/MW
978
+ self._trans_table[tcc_per_mw_col] = cost
979
+
980
+ self._trans_table[tcc_per_mw_col] = (
981
+ self._trans_table[tcc_per_mw_col].astype("float32")
982
+ )
983
+ cost = cost.astype("float32")
984
+ cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC]
985
+ cf_mean_arr = cf_mean_arr.values.astype("float32")
986
+ resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE]
987
+ resource_lcoe = resource_lcoe.values.astype("float32")
988
+
989
+ if 'reinforcement_cost_floored_per_mw' in self._trans_table:
990
+ logger.info("'reinforcement_cost_floored_per_mw' column found in "
991
+ "transmission table. Adding floored reinforcement "
992
+ "cost LCOE as sorting option.")
993
+ fr_cost = (self._trans_table['reinforcement_cost_floored_per_mw']
994
+ .values.copy())
995
+
996
+ lcot_fr = ((cost + fr_cost) * fcr) / (cf_mean_arr * 8760)
997
+ lcoe_fr = lcot_fr + resource_lcoe
998
+ self._trans_table['lcot_floored_reinforcement'] = lcot_fr
999
+ self._trans_table['lcoe_floored_reinforcement'] = lcoe_fr
1000
+
1001
+ if SupplyCurveField.REINFORCEMENT_COST_PER_MW in self._trans_table:
1002
+ logger.info("%s column found in transmission table. Adding "
1003
+ "reinforcement costs to total LCOE.",
1004
+ SupplyCurveField.REINFORCEMENT_COST_PER_MW)
1005
+ lcot_nr = (cost * fcr) / (cf_mean_arr * 8760)
1006
+ lcoe_nr = lcot_nr + resource_lcoe
1007
+ self._trans_table['lcot_no_reinforcement'] = lcot_nr
1008
+ self._trans_table['lcoe_no_reinforcement'] = lcoe_nr
1009
+
1010
+ col_name = SupplyCurveField.REINFORCEMENT_COST_PER_MW
1011
+ r_cost = self._trans_table[col_name].astype("float32")
1012
+ r_cost = r_cost.values.copy()
1013
+ self._trans_table[tcc_per_mw_col] += r_cost
862
1014
  cost += r_cost # $/MW
863
1015
 
864
- cf_mean_arr = self._trans_table['mean_cf'].values
865
1016
  lcot = (cost * fcr) / (cf_mean_arr * 8760)
866
-
867
- self._trans_table['lcot'] = lcot
868
- self._trans_table['total_lcoe'] = (self._trans_table['lcot']
869
- + self._trans_table['mean_lcoe'])
1017
+ self._trans_table[SupplyCurveField.LCOT] = lcot
1018
+ self._trans_table[SupplyCurveField.TOTAL_LCOE] = lcot + resource_lcoe
870
1019
 
871
1020
  if consider_friction:
872
1021
  self._calculate_total_lcoe_friction()
@@ -875,15 +1024,19 @@ class SupplyCurve:
875
1024
  """Look for site mean LCOE with friction in the trans table and if
876
1025
  found make a total LCOE column with friction."""
877
1026
 
878
- if 'mean_lcoe_friction' in self._trans_table:
879
- lcoe_friction = (self._trans_table['lcot']
880
- + self._trans_table['mean_lcoe_friction'])
881
- self._trans_table['total_lcoe_friction'] = lcoe_friction
1027
+ if SupplyCurveField.MEAN_LCOE_FRICTION in self._trans_table:
1028
+ lcoe_friction = (
1029
+ self._trans_table[SupplyCurveField.LCOT]
1030
+ + self._trans_table[SupplyCurveField.MEAN_LCOE_FRICTION])
1031
+ self._trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION] = (
1032
+ lcoe_friction
1033
+ )
882
1034
  logger.info('Found mean LCOE with friction. Adding key '
883
1035
  '"total_lcoe_friction" to trans table.')
884
1036
 
885
- def _exclude_noncompetitive_wind_farms(self, comp_wind_dirs, sc_gid,
886
- downwind=False):
1037
+ def _exclude_noncompetitive_wind_farms(
1038
+ self, comp_wind_dirs, sc_gid, downwind=False
1039
+ ):
887
1040
  """
888
1041
  Exclude non-competitive wind farms for given sc_gid
889
1042
 
@@ -905,18 +1058,20 @@ class SupplyCurve:
905
1058
  gid = comp_wind_dirs.check_sc_gid(sc_gid)
906
1059
  if gid is not None:
907
1060
  if comp_wind_dirs.mask[gid]:
908
- exclude_gids = comp_wind_dirs['upwind', gid]
1061
+ exclude_gids = comp_wind_dirs["upwind", gid]
909
1062
  if downwind:
910
- exclude_gids = np.append(exclude_gids,
911
- comp_wind_dirs['downwind', gid])
1063
+ exclude_gids = np.append(
1064
+ exclude_gids, comp_wind_dirs["downwind", gid]
1065
+ )
912
1066
  for n in exclude_gids:
913
1067
  check = comp_wind_dirs.exclude_sc_point_gid(n)
914
1068
  if check:
915
- sc_gids = comp_wind_dirs['sc_gid', n]
1069
+ sc_gids = comp_wind_dirs[SupplyCurveField.SC_GID, n]
916
1070
  for sc_id in sc_gids:
917
1071
  if self._mask[sc_id]:
918
- logger.debug('Excluding sc_gid {}'
919
- .format(sc_id))
1072
+ logger.debug(
1073
+ "Excluding sc_gid {}".format(sc_id)
1074
+ )
920
1075
  self._mask[sc_id] = False
921
1076
 
922
1077
  return comp_wind_dirs
@@ -945,8 +1100,11 @@ class SupplyCurve:
945
1100
  missing = [s for s in sum_labels if s not in table]
946
1101
 
947
1102
  if any(missing):
948
- logger.info('Could not make sum column "{}", missing: {}'
949
- .format(new_label, missing))
1103
+ logger.info(
1104
+ 'Could not make sum column "{}", missing: {}'.format(
1105
+ new_label, missing
1106
+ )
1107
+ )
950
1108
  else:
951
1109
  sum_arr = np.zeros(len(table))
952
1110
  for s in sum_labels:
@@ -958,13 +1116,26 @@ class SupplyCurve:
958
1116
 
959
1117
  return table
960
1118
 
961
- def _full_sort(self, trans_table, trans_costs=None,
962
- avail_cap_frac=1, comp_wind_dirs=None,
963
- total_lcoe_fric=None, sort_on='total_lcoe',
964
- columns=('trans_gid', 'trans_capacity', 'trans_type',
965
- 'trans_cap_cost_per_mw', 'dist_km', 'lcot',
966
- 'total_lcoe'),
967
- downwind=False):
1119
+ # pylint: disable=C901
1120
+ def _full_sort(
1121
+ self,
1122
+ trans_table,
1123
+ trans_costs=None,
1124
+ avail_cap_frac=1,
1125
+ comp_wind_dirs=None,
1126
+ total_lcoe_fric=None,
1127
+ sort_on=SupplyCurveField.TOTAL_LCOE,
1128
+ columns=(
1129
+ SupplyCurveField.TRANS_GID,
1130
+ SupplyCurveField.TRANS_CAPACITY,
1131
+ SupplyCurveField.TRANS_TYPE,
1132
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
1133
+ SupplyCurveField.DIST_SPUR_KM,
1134
+ SupplyCurveField.LCOT,
1135
+ SupplyCurveField.TOTAL_LCOE,
1136
+ ),
1137
+ downwind=False,
1138
+ ):
968
1139
  """
969
1140
  Internal method to handle full supply curve sorting
970
1141
 
@@ -1002,9 +1173,11 @@ class SupplyCurve:
1002
1173
  Updated sc_points table with transmission connections, LCOT
1003
1174
  and LCOE+LCOT based on full supply curve connections
1004
1175
  """
1005
- trans_features = self._create_handler(self._trans_table,
1006
- trans_costs=trans_costs,
1007
- avail_cap_frac=avail_cap_frac)
1176
+ trans_features = self._create_handler(
1177
+ self._trans_table,
1178
+ trans_costs=trans_costs,
1179
+ avail_cap_frac=avail_cap_frac,
1180
+ )
1008
1181
  init_list = [np.nan] * int(1 + np.max(self._sc_gids))
1009
1182
  columns = list(columns)
1010
1183
  if sort_on not in columns:
@@ -1012,22 +1185,23 @@ class SupplyCurve:
1012
1185
 
1013
1186
  conn_lists = {k: deepcopy(init_list) for k in columns}
1014
1187
 
1015
- trans_sc_gids = trans_table['sc_gid'].values.astype(int)
1188
+ trans_sc_gids = trans_table[SupplyCurveField.SC_GID].values.astype(int)
1016
1189
 
1017
1190
  # syntax is final_key: source_key (source from trans_table)
1018
- all_cols = {k: k for k in columns}
1019
- essentials = {'trans_gid': 'trans_gid',
1020
- 'trans_capacity': 'avail_cap',
1021
- 'trans_type': 'category',
1022
- 'dist_km': 'dist_km',
1023
- 'trans_cap_cost_per_mw': 'trans_cap_cost_per_mw',
1024
- 'lcot': 'lcot',
1025
- 'total_lcoe': 'total_lcoe',
1026
- }
1027
- all_cols.update(essentials)
1028
-
1029
- arrays = {final_key: trans_table[source_key].values
1030
- for final_key, source_key in all_cols.items()}
1191
+ all_cols = list(columns)
1192
+ essentials = [SupplyCurveField.TRANS_GID,
1193
+ SupplyCurveField.TRANS_CAPACITY,
1194
+ SupplyCurveField.TRANS_TYPE,
1195
+ SupplyCurveField.DIST_SPUR_KM,
1196
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
1197
+ SupplyCurveField.LCOT,
1198
+ SupplyCurveField.TOTAL_LCOE]
1199
+
1200
+ for col in essentials:
1201
+ if col not in all_cols:
1202
+ all_cols.append(col)
1203
+
1204
+ arrays = {col: trans_table[col].values for col in all_cols}
1031
1205
 
1032
1206
  sc_capacities = trans_table[self._sc_capacity_col].values
1033
1207
 
@@ -1036,52 +1210,62 @@ class SupplyCurve:
1036
1210
  for i in range(len(trans_table)):
1037
1211
  sc_gid = trans_sc_gids[i]
1038
1212
  if self._mask[sc_gid]:
1039
- connect = trans_features.connect(arrays['trans_gid'][i],
1040
- sc_capacities[i])
1213
+ connect = trans_features.connect(
1214
+ arrays[SupplyCurveField.TRANS_GID][i], sc_capacities[i]
1215
+ )
1041
1216
  if connect:
1042
1217
  connected += 1
1043
- logger.debug('Connecting sc gid {}'.format(sc_gid))
1218
+ logger.debug("Connecting sc gid {}".format(sc_gid))
1044
1219
  self._mask[sc_gid] = False
1045
1220
 
1046
1221
  for col_name, data_arr in arrays.items():
1047
1222
  conn_lists[col_name][sc_gid] = data_arr[i]
1048
1223
 
1049
1224
  if total_lcoe_fric is not None:
1050
- conn_lists['total_lcoe_friction'][sc_gid] = \
1051
- total_lcoe_fric[i]
1225
+ col_name = SupplyCurveField.TOTAL_LCOE_FRICTION
1226
+ conn_lists[col_name][sc_gid] = total_lcoe_fric[i]
1052
1227
 
1053
1228
  current_prog = connected // (len(self) / 100)
1054
1229
  if current_prog > progress:
1055
1230
  progress = current_prog
1056
- logger.info('{} % of supply curve points connected'
1057
- .format(progress))
1231
+ logger.info(
1232
+ "{} % of supply curve points connected".format(
1233
+ progress
1234
+ )
1235
+ )
1058
1236
 
1059
1237
  if comp_wind_dirs is not None:
1060
- comp_wind_dirs = \
1238
+ comp_wind_dirs = (
1061
1239
  self._exclude_noncompetitive_wind_farms(
1062
- comp_wind_dirs, sc_gid, downwind=downwind)
1240
+ comp_wind_dirs, sc_gid, downwind=downwind
1241
+ )
1242
+ )
1063
1243
 
1064
1244
  index = range(0, int(1 + np.max(self._sc_gids)))
1065
1245
  connections = pd.DataFrame(conn_lists, index=index)
1066
- connections.index.name = 'sc_gid'
1246
+ connections.index.name = SupplyCurveField.SC_GID
1067
1247
  connections = connections.dropna(subset=[sort_on])
1068
1248
  connections = connections[columns].reset_index()
1069
1249
 
1070
- sc_gids = self._sc_points['sc_gid'].values
1071
- connected = connections['sc_gid'].values
1250
+ sc_gids = self._sc_points[SupplyCurveField.SC_GID].values
1251
+ connected = connections[SupplyCurveField.SC_GID].values
1072
1252
  logger.debug('Connected gids {} out of total supply curve gids {}'
1073
1253
  .format(len(connected), len(sc_gids)))
1074
1254
  unconnected = ~np.isin(sc_gids, connected)
1075
1255
  unconnected = sc_gids[unconnected].tolist()
1076
1256
 
1077
1257
  if unconnected:
1078
- msg = ("{} supply curve points were not connected to tranmission! "
1079
- "Unconnected sc_gid's: {}"
1080
- .format(len(unconnected), unconnected))
1258
+ msg = (
1259
+ "{} supply curve points were not connected to tranmission! "
1260
+ "Unconnected sc_gid's: {}".format(
1261
+ len(unconnected), unconnected
1262
+ )
1263
+ )
1081
1264
  logger.warning(msg)
1082
1265
  warn(msg)
1083
1266
 
1084
- supply_curve = self._sc_points.merge(connections, on='sc_gid')
1267
+ supply_curve = self._sc_points.merge(
1268
+ connections, on=SupplyCurveField.SC_GID)
1085
1269
 
1086
1270
  return supply_curve.reset_index(drop=True)
1087
1271
 
@@ -1090,42 +1274,75 @@ class SupplyCurve:
1090
1274
  Add the transmission connection feature capacity to the trans table if
1091
1275
  needed
1092
1276
  """
1093
- if 'avail_cap' not in self._trans_table:
1094
- kwargs = {'avail_cap_frac': avail_cap_frac}
1277
+ if SupplyCurveField.TRANS_CAPACITY not in self._trans_table:
1278
+ kwargs = {"avail_cap_frac": avail_cap_frac}
1095
1279
  fc = TF.feature_capacity(self._trans_table, **kwargs)
1096
- self._trans_table = self._trans_table.merge(fc, on='trans_gid')
1280
+ self._trans_table = self._trans_table.merge(
1281
+ fc, on=SupplyCurveField.TRANS_GID)
1097
1282
 
1098
1283
  def _adjust_output_columns(self, columns, consider_friction):
1099
- """Add extra output columns, if needed. """
1100
- # These are essentially should-be-defaults that are not
1101
- # backwards-compatible, so have to explicitly check for them
1102
- extra_cols = ['ba_str', 'poi_lat', 'poi_lon', 'reinforcement_poi_lat',
1103
- 'reinforcement_poi_lon', 'eos_mult', 'reg_mult',
1104
- 'reinforcement_cost_per_mw', 'reinforcement_dist_km',
1105
- 'n_parallel_trans', 'total_lcoe_friction']
1106
- if not consider_friction:
1107
- extra_cols -= {'total_lcoe_friction'}
1284
+ """Add extra output columns, if needed."""
1285
+
1286
+ for col in _REQUIRED_COMPUTE_AND_OUTPUT_COLS:
1287
+ if col not in columns:
1288
+ columns.append(col)
1289
+
1290
+ for col in _REQUIRED_OUTPUT_COLS:
1291
+ if col not in self._trans_table:
1292
+ self._trans_table[col] = None
1293
+ if col not in columns:
1294
+ columns.append(col)
1295
+
1296
+ missing_cols = [col for col in columns if col not in self._trans_table]
1297
+ if missing_cols:
1298
+ msg = (f"The following requested columns are not found in "
1299
+ f"transmission table: {missing_cols}.\nSkipping...")
1300
+ logger.warning(msg)
1301
+ warn(msg)
1108
1302
 
1109
- extra_cols = [col for col in extra_cols
1110
- if col in self._trans_table and col not in columns]
1303
+ columns = [col for col in columns if col in self._trans_table]
1111
1304
 
1112
- return columns + extra_cols
1305
+ fric_col = SupplyCurveField.TOTAL_LCOE_FRICTION
1306
+ if consider_friction and fric_col in self._trans_table:
1307
+ columns.append(fric_col)
1308
+
1309
+ return sorted(columns, key=_column_sort_key)
1113
1310
 
1114
1311
  def _determine_sort_on(self, sort_on):
1115
1312
  """Determine the `sort_on` column from user input and trans table"""
1116
- if 'reinforcement_cost_per_mw' in self._trans_table:
1313
+ r_cost_col = SupplyCurveField.REINFORCEMENT_COST_PER_MW
1314
+ found_reinforcement_costs = (
1315
+ r_cost_col in self._trans_table
1316
+ and not self._trans_table[r_cost_col].isna().all()
1317
+ )
1318
+ if found_reinforcement_costs:
1117
1319
  sort_on = sort_on or "lcoe_no_reinforcement"
1118
- return sort_on or 'total_lcoe'
1119
-
1120
- def full_sort(self, fcr, transmission_costs=None,
1121
- avail_cap_frac=1, line_limited=False,
1122
- connectable=True, max_workers=None,
1123
- consider_friction=True, sort_on=None,
1124
- columns=('trans_gid', 'trans_capacity', 'trans_type',
1125
- 'trans_cap_cost_per_mw', 'dist_km', 'lcot',
1126
- 'total_lcoe'),
1127
- wind_dirs=None, n_dirs=2, downwind=False,
1128
- offshore_compete=False):
1320
+ return sort_on or SupplyCurveField.TOTAL_LCOE
1321
+
1322
+ def full_sort(
1323
+ self,
1324
+ fcr,
1325
+ transmission_costs=None,
1326
+ avail_cap_frac=1,
1327
+ line_limited=False,
1328
+ connectable=True,
1329
+ max_workers=None,
1330
+ consider_friction=True,
1331
+ sort_on=None,
1332
+ columns=(
1333
+ SupplyCurveField.TRANS_GID,
1334
+ SupplyCurveField.TRANS_CAPACITY,
1335
+ SupplyCurveField.TRANS_TYPE,
1336
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
1337
+ SupplyCurveField.DIST_SPUR_KM,
1338
+ SupplyCurveField.LCOT,
1339
+ SupplyCurveField.TOTAL_LCOE,
1340
+ ),
1341
+ wind_dirs=None,
1342
+ n_dirs=2,
1343
+ downwind=False,
1344
+ offshore_compete=False,
1345
+ ):
1129
1346
  """
1130
1347
  run full supply curve sorting
1131
1348
 
@@ -1180,14 +1397,17 @@ class SupplyCurve:
1180
1397
  Updated sc_points table with transmission connections, LCOT
1181
1398
  and LCOE+LCOT based on full supply curve connections
1182
1399
  """
1183
- logger.info('Starting full competitive supply curve sort.')
1400
+ logger.info("Starting full competitive supply curve sort.")
1184
1401
  self._check_substation_conns(self._trans_table)
1185
- self.compute_total_lcoe(fcr, transmission_costs=transmission_costs,
1186
- avail_cap_frac=avail_cap_frac,
1187
- line_limited=line_limited,
1188
- connectable=connectable,
1189
- max_workers=max_workers,
1190
- consider_friction=consider_friction)
1402
+ self.compute_total_lcoe(
1403
+ fcr,
1404
+ transmission_costs=transmission_costs,
1405
+ avail_cap_frac=avail_cap_frac,
1406
+ line_limited=line_limited,
1407
+ connectable=connectable,
1408
+ max_workers=max_workers,
1409
+ consider_friction=consider_friction,
1410
+ )
1191
1411
  self._check_feature_capacity(avail_cap_frac=avail_cap_frac)
1192
1412
 
1193
1413
  if isinstance(columns, tuple):
@@ -1197,12 +1417,16 @@ class SupplyCurve:
1197
1417
  sort_on = self._determine_sort_on(sort_on)
1198
1418
 
1199
1419
  trans_table = self._trans_table.copy()
1200
- pos = trans_table['lcot'].isnull()
1201
- trans_table = trans_table.loc[~pos].sort_values([sort_on, 'trans_gid'])
1420
+ pos = trans_table[SupplyCurveField.LCOT].isnull()
1421
+ trans_table = trans_table.loc[~pos].sort_values(
1422
+ [sort_on, SupplyCurveField.TRANS_GID]
1423
+ )
1202
1424
 
1203
1425
  total_lcoe_fric = None
1204
- if consider_friction and 'mean_lcoe_friction' in trans_table:
1205
- total_lcoe_fric = trans_table['total_lcoe_friction'].values
1426
+ col_in_table = SupplyCurveField.MEAN_LCOE_FRICTION in trans_table
1427
+ if consider_friction and col_in_table:
1428
+ total_lcoe_fric = \
1429
+ trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION].values
1206
1430
 
1207
1431
  comp_wind_dirs = None
1208
1432
  if wind_dirs is not None:
@@ -1216,28 +1440,40 @@ class SupplyCurve:
1216
1440
 
1217
1441
  msg += " windfarms"
1218
1442
  logger.info(msg)
1219
- comp_wind_dirs = CompetitiveWindFarms(wind_dirs,
1220
- self._sc_points,
1221
- n_dirs=n_dirs,
1222
- offshore=offshore_compete)
1223
-
1224
- supply_curve = self._full_sort(trans_table,
1225
- trans_costs=transmission_costs,
1226
- avail_cap_frac=avail_cap_frac,
1227
- comp_wind_dirs=comp_wind_dirs,
1228
- total_lcoe_fric=total_lcoe_fric,
1229
- sort_on=sort_on, columns=columns,
1230
- downwind=downwind)
1443
+ comp_wind_dirs = CompetitiveWindFarms(
1444
+ wind_dirs,
1445
+ self._sc_points,
1446
+ n_dirs=n_dirs,
1447
+ offshore=offshore_compete,
1448
+ )
1449
+
1450
+ supply_curve = self._full_sort(
1451
+ trans_table,
1452
+ trans_costs=transmission_costs,
1453
+ avail_cap_frac=avail_cap_frac,
1454
+ comp_wind_dirs=comp_wind_dirs,
1455
+ total_lcoe_fric=total_lcoe_fric,
1456
+ sort_on=sort_on,
1457
+ columns=columns,
1458
+ downwind=downwind,
1459
+ )
1231
1460
 
1232
1461
  return supply_curve
1233
1462
 
1234
- def simple_sort(self, fcr, transmission_costs=None,
1235
- avail_cap_frac=1, max_workers=None,
1236
- consider_friction=True, sort_on=None,
1237
- columns=('trans_gid', 'trans_type', 'lcot', 'total_lcoe',
1238
- 'dist_km', 'trans_cap_cost_per_mw'),
1239
- wind_dirs=None, n_dirs=2, downwind=False,
1240
- offshore_compete=False):
1463
+ def simple_sort(
1464
+ self,
1465
+ fcr,
1466
+ transmission_costs=None,
1467
+ avail_cap_frac=1,
1468
+ max_workers=None,
1469
+ consider_friction=True,
1470
+ sort_on=None,
1471
+ columns=DEFAULT_COLUMNS,
1472
+ wind_dirs=None,
1473
+ n_dirs=2,
1474
+ downwind=False,
1475
+ offshore_compete=False,
1476
+ ):
1241
1477
  """
1242
1478
  Run simple supply curve sorting that does not take into account
1243
1479
  available capacity
@@ -1271,9 +1507,8 @@ class SupplyCurve:
1271
1507
  will be built first, by default `None`, which will use
1272
1508
  total LCOE without any reinforcement costs as the sort value.
1273
1509
  columns : list | tuple, optional
1274
- Columns to preserve in output connections dataframe,
1275
- by default ('trans_gid', 'trans_capacity', 'trans_type',
1276
- 'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe')
1510
+ Columns to preserve in output connections dataframe.
1511
+ By default, :obj:`DEFAULT_COLUMNS`.
1277
1512
  wind_dirs : pandas.DataFrame | str, optional
1278
1513
  path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
1279
1514
  the neighboring supply curve point gids and power-rose value at
@@ -1292,47 +1527,57 @@ class SupplyCurve:
1292
1527
  Updated sc_points table with transmission connections, LCOT
1293
1528
  and LCOE+LCOT based on simple supply curve connections
1294
1529
  """
1295
- logger.info('Starting simple supply curve sort (no capacity limits).')
1296
- self.compute_total_lcoe(fcr, transmission_costs=transmission_costs,
1297
- avail_cap_frac=avail_cap_frac,
1298
- connectable=False,
1299
- max_workers=max_workers,
1300
- consider_friction=consider_friction)
1301
- trans_table = self._trans_table.copy()
1530
+ logger.info("Starting simple supply curve sort (no capacity limits).")
1531
+ self.compute_total_lcoe(
1532
+ fcr,
1533
+ transmission_costs=transmission_costs,
1534
+ avail_cap_frac=avail_cap_frac,
1535
+ connectable=False,
1536
+ max_workers=max_workers,
1537
+ consider_friction=consider_friction,
1538
+ )
1539
+ sort_on = self._determine_sort_on(sort_on)
1302
1540
 
1303
1541
  if isinstance(columns, tuple):
1304
1542
  columns = list(columns)
1305
-
1306
1543
  columns = self._adjust_output_columns(columns, consider_friction)
1307
- sort_on = self._determine_sort_on(sort_on)
1308
1544
 
1309
- connections = trans_table.sort_values([sort_on, 'trans_gid'])
1310
- connections = connections.groupby('sc_gid').first()
1311
- rename = {'trans_gid': 'trans_gid',
1312
- 'category': 'trans_type'}
1313
- connections = connections.rename(columns=rename)
1545
+ trans_table = self._trans_table.copy()
1546
+ connections = trans_table.sort_values(
1547
+ [sort_on, SupplyCurveField.TRANS_GID])
1548
+ connections = connections.groupby(SupplyCurveField.SC_GID).first()
1314
1549
  connections = connections[columns].reset_index()
1315
1550
 
1316
- supply_curve = self._sc_points.merge(connections, on='sc_gid')
1551
+ supply_curve = self._sc_points.merge(connections,
1552
+ on=SupplyCurveField.SC_GID)
1317
1553
  if wind_dirs is not None:
1318
- supply_curve = \
1319
- CompetitiveWindFarms.run(wind_dirs,
1320
- supply_curve,
1321
- n_dirs=n_dirs,
1322
- offshore=offshore_compete,
1323
- sort_on=sort_on,
1324
- downwind=downwind)
1554
+ supply_curve = CompetitiveWindFarms.run(
1555
+ wind_dirs,
1556
+ supply_curve,
1557
+ n_dirs=n_dirs,
1558
+ offshore=offshore_compete,
1559
+ sort_on=sort_on,
1560
+ downwind=downwind,
1561
+ )
1325
1562
 
1326
1563
  supply_curve = supply_curve.reset_index(drop=True)
1327
1564
 
1328
1565
  return supply_curve
1329
1566
 
1330
- def run(self, out_fpath, fixed_charge_rate, simple=True, avail_cap_frac=1,
1331
- line_limited=False, transmission_costs=None,
1332
- consider_friction=True, sort_on=None,
1333
- columns=('trans_gid', 'trans_type', 'trans_cap_cost_per_mw',
1334
- 'dist_km', 'lcot', 'total_lcoe'),
1335
- max_workers=None, competition=None):
1567
+ def run(
1568
+ self,
1569
+ out_fpath,
1570
+ fixed_charge_rate,
1571
+ simple=True,
1572
+ avail_cap_frac=1,
1573
+ line_limited=False,
1574
+ transmission_costs=None,
1575
+ consider_friction=True,
1576
+ sort_on=None,
1577
+ columns=DEFAULT_COLUMNS,
1578
+ max_workers=None,
1579
+ competition=None,
1580
+ ):
1336
1581
  """Run Supply Curve Transmission calculations.
1337
1582
 
1338
1583
  Run full supply curve taking into account available capacity of
@@ -1397,8 +1642,7 @@ class SupplyCurve:
1397
1642
  By default ``None``.
1398
1643
  columns : list | tuple, optional
1399
1644
  Columns to preserve in output supply curve dataframe.
1400
- By default, ``('trans_gid', 'trans_type',
1401
- 'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe')``.
1645
+ By default, :obj:`DEFAULT_COLUMNS`.
1402
1646
  max_workers : int, optional
1403
1647
  Number of workers to use to compute LCOT. If > 1,
1404
1648
  computation is run in parallel. If ``None``, computation
@@ -1433,12 +1677,14 @@ class SupplyCurve:
1433
1677
  str
1434
1678
  Path to output supply curve.
1435
1679
  """
1436
- kwargs = {"fcr": fixed_charge_rate,
1437
- "transmission_costs": transmission_costs,
1438
- "consider_friction": consider_friction,
1439
- "sort_on": sort_on,
1440
- "columns": columns,
1441
- "max_workers": max_workers}
1680
+ kwargs = {
1681
+ "fcr": fixed_charge_rate,
1682
+ "transmission_costs": transmission_costs,
1683
+ "consider_friction": consider_friction,
1684
+ "sort_on": sort_on,
1685
+ "columns": columns,
1686
+ "max_workers": max_workers,
1687
+ }
1442
1688
  kwargs.update(competition or {})
1443
1689
 
1444
1690
  if simple:
@@ -1457,8 +1703,19 @@ class SupplyCurve:
1457
1703
  def _format_sc_out_fpath(out_fpath):
1458
1704
  """Add CSV file ending and replace underscore, if necessary."""
1459
1705
  if not out_fpath.endswith(".csv"):
1460
- out_fpath = '{}.csv'.format(out_fpath)
1706
+ out_fpath = "{}.csv".format(out_fpath)
1461
1707
 
1462
1708
  project_dir, out_fn = os.path.split(out_fpath)
1463
1709
  out_fn = out_fn.replace("supply_curve", "supply-curve")
1464
1710
  return os.path.join(project_dir, out_fn)
1711
+
1712
+
1713
+ def _column_sort_key(col):
1714
+ """Determine the sort order of the input column. """
1715
+ col_value = _REQUIRED_COMPUTE_AND_OUTPUT_COLS.get(col)
1716
+ if col_value is None:
1717
+ col_value = _REQUIRED_OUTPUT_COLS.get(col)
1718
+ if col_value is None:
1719
+ col_value = 1e6
1720
+
1721
+ return col_value, str(col)