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/hybrids/hybrids.py CHANGED
@@ -3,35 +3,46 @@
3
3
 
4
4
  @author: ppinchuk
5
5
  """
6
+
6
7
  import logging
7
- import numpy as np
8
8
  import re
9
- import pandas as pd
9
+ from collections import namedtuple
10
10
  from string import ascii_letters
11
11
  from warnings import warn
12
- from collections import namedtuple
13
-
14
- from reV.handlers.outputs import Outputs
15
- from reV.utilities.exceptions import (FileInputError, InputError,
16
- InputWarning, OutputWarning)
17
- from reV.hybrids.hybrid_methods import HYBRID_METHODS
18
12
 
13
+ import numpy as np
14
+ import pandas as pd
19
15
  from rex.resource import Resource
20
16
  from rex.utilities.utilities import to_records_array
21
17
 
18
+ from reV.handlers.outputs import Outputs
19
+ from reV.hybrids.hybrid_methods import HYBRID_METHODS
20
+ from reV.utilities import SupplyCurveField
21
+ from reV.utilities.exceptions import (
22
+ FileInputError,
23
+ InputError,
24
+ InputWarning,
25
+ OutputWarning,
26
+ )
27
+
22
28
  logger = logging.getLogger(__name__)
23
29
 
24
- MERGE_COLUMN = 'sc_point_gid'
30
+ MERGE_COLUMN = SupplyCurveField.SC_POINT_GID
25
31
  PROFILE_DSET_REGEX = 'rep_profiles_[0-9]+$'
26
32
  SOLAR_PREFIX = 'solar_'
27
33
  WIND_PREFIX = 'wind_'
28
34
  NON_DUPLICATE_COLS = {
29
- 'latitude', 'longitude', 'country', 'state', 'county', 'elevation',
30
- 'timezone', 'sc_point_gid', 'sc_row_ind', 'sc_col_ind'
35
+ SupplyCurveField.LATITUDE, SupplyCurveField.LONGITUDE,
36
+ SupplyCurveField.COUNTRY, SupplyCurveField.STATE, SupplyCurveField.COUNTY,
37
+ SupplyCurveField.ELEVATION, SupplyCurveField.TIMEZONE,
38
+ SupplyCurveField.SC_POINT_GID, SupplyCurveField.SC_ROW_IND,
39
+ SupplyCurveField.SC_COL_IND
31
40
  }
32
- DROPPED_COLUMNS = ['gid']
33
- DEFAULT_FILL_VALUES = {'solar_capacity': 0, 'wind_capacity': 0,
34
- 'solar_mean_cf': 0, 'wind_mean_cf': 0}
41
+ HYBRIDS_GID_COL = "gid"
42
+ DEFAULT_FILL_VALUES = {f'solar_{SupplyCurveField.CAPACITY_AC_MW}': 0,
43
+ f'wind_{SupplyCurveField.CAPACITY_AC_MW}': 0,
44
+ f'solar_{SupplyCurveField.MEAN_CF_AC}': 0,
45
+ f'wind_{SupplyCurveField.MEAN_CF_AC}': 0}
35
46
  OUTPUT_PROFILE_NAMES = ['hybrid_profile',
36
47
  'hybrid_solar_profile',
37
48
  'hybrid_wind_profile']
@@ -40,7 +51,8 @@ RatioColumns = namedtuple('RatioColumns', ['num', 'denom', 'fixed'],
40
51
 
41
52
 
42
53
  class ColNameFormatter:
43
- """Column name formatting helper class. """
54
+ """Column name formatting helper class."""
55
+
44
56
  ALLOWED = set(ascii_letters)
45
57
 
46
58
  @classmethod
@@ -61,11 +73,11 @@ class ColNameFormatter:
61
73
  The column name with all characters except ascii stripped
62
74
  and all lowercase.
63
75
  """
64
- return ''.join(c for c in n if c in cls.ALLOWED).lower()
76
+ return "".join(c for c in n if c in cls.ALLOWED).lower()
65
77
 
66
78
 
67
79
  class HybridsData:
68
- """Hybrids input data container. """
80
+ """Hybrids input data container."""
69
81
 
70
82
  def __init__(self, solar_fpath, wind_fpath):
71
83
  """
@@ -158,7 +170,8 @@ class HybridsData:
158
170
  """
159
171
  if self._hybrid_time_index is None:
160
172
  self._hybrid_time_index = self.solar_time_index.join(
161
- self.wind_time_index, how='inner')
173
+ self.wind_time_index, how="inner"
174
+ )
162
175
  return self._hybrid_time_index
163
176
 
164
177
  def contains_col(self, col_name):
@@ -202,9 +215,11 @@ class HybridsData:
202
215
  If len(time_index) < 8760 for the hybrid profile.
203
216
  """
204
217
  if len(self.hybrid_time_index) < 8760:
205
- msg = ("The length of the merged time index ({}) is less than "
206
- "8760. Please ensure that the input profiles have a "
207
- "time index that overlaps >= 8760 times.")
218
+ msg = (
219
+ "The length of the merged time index ({}) is less than "
220
+ "8760. Please ensure that the input profiles have a "
221
+ "time index that overlaps >= 8760 times."
222
+ )
208
223
  e = msg.format(len(self.hybrid_time_index))
209
224
  logger.error(e)
210
225
  raise FileInputError(e)
@@ -220,17 +235,18 @@ class HybridsData:
220
235
  for fp in [self.solar_fpath, self.wind_fpath]:
221
236
  with Resource(fp) as res:
222
237
  profile_dset_names = [
223
- n for n in res.dsets
224
- if self.__profile_reg_check.match(n)
238
+ n for n in res.dsets if self.__profile_reg_check.match(n)
225
239
  ]
226
240
  if not profile_dset_names:
227
- msg = ("Did not find any data sets matching the regex: "
228
- "{!r} in {!r}. Please ensure that the profile data "
229
- "exists and that the data set is named correctly.")
241
+ msg = (
242
+ "Did not find any data sets matching the regex: "
243
+ "{!r} in {!r}. Please ensure that the profile data "
244
+ "exists and that the data set is named correctly."
245
+ )
230
246
  e = msg.format(PROFILE_DSET_REGEX, fp)
231
247
  logger.error(e)
232
248
  raise FileInputError(e)
233
- elif len(profile_dset_names) > 1:
249
+ if len(profile_dset_names) > 1:
234
250
  msg = ("Found more than one profile in {!r}: {}. "
235
251
  "This module is not intended for hybridization of "
236
252
  "multiple representative profiles. Please re-run "
@@ -238,8 +254,7 @@ class HybridsData:
238
254
  e = msg.format(fp, profile_dset_names)
239
255
  logger.error(e)
240
256
  raise FileInputError(e)
241
- else:
242
- self.profile_dset_names += profile_dset_names
257
+ self.profile_dset_names += profile_dset_names
243
258
 
244
259
  def _validate_merge_col_exists(self):
245
260
  """Validate the existence of the merge column.
@@ -250,13 +265,17 @@ class HybridsData:
250
265
  If merge column is missing from either the solar or
251
266
  the wind meta data.
252
267
  """
253
- msg = ("Cannot hybridize: merge column {!r} missing from the "
254
- "{} meta data! ({!r})")
268
+ msg = (
269
+ "Cannot hybridize: merge column {!r} missing from the "
270
+ "{} meta data! ({!r})"
271
+ )
255
272
 
256
273
  mc = ColNameFormatter.fmt(MERGE_COLUMN)
257
- for cols, fp, res in zip([self.__solar_cols, self.__wind_cols],
258
- [self.solar_fpath, self.wind_fpath],
259
- ['solar', 'wind']):
274
+ for cols, fp, res in zip(
275
+ [self.__solar_cols, self.__wind_cols],
276
+ [self.solar_fpath, self.wind_fpath],
277
+ ["solar", "wind"],
278
+ ):
260
279
  if mc not in cols:
261
280
  e = msg.format(MERGE_COLUMN, res, fp)
262
281
  logger.error(e)
@@ -271,16 +290,20 @@ class HybridsData:
271
290
  If merge column contains duplicate values in either the solar or
272
291
  the wind meta data.
273
292
  """
274
- msg = ("Duplicate {}s were found. This is likely due to resource "
275
- "class binning, which is not supported at this time. "
276
- "Please re-run supply curve aggregation without "
277
- "resource class binning and ensure there are no duplicate "
278
- "values in {!r}. File: {!r}")
293
+ msg = (
294
+ "Duplicate {}s were found. This is likely due to resource "
295
+ "class binning, which is not supported at this time. "
296
+ "Please re-run supply curve aggregation without "
297
+ "resource class binning and ensure there are no duplicate "
298
+ "values in {!r}. File: {!r}"
299
+ )
279
300
 
280
301
  mc = ColNameFormatter.fmt(MERGE_COLUMN)
281
- for ds, cols, fp in zip([self.solar_meta, self.wind_meta],
282
- [self.__solar_cols, self.__wind_cols],
283
- [self.solar_fpath, self.wind_fpath]):
302
+ for ds, cols, fp in zip(
303
+ [self.solar_meta, self.wind_meta],
304
+ [self.__solar_cols, self.__wind_cols],
305
+ [self.solar_fpath, self.wind_fpath],
306
+ ):
284
307
  merge_col = ds.columns[cols == mc].item()
285
308
  if not ds[merge_col].is_unique:
286
309
  e = msg.format(merge_col, merge_col, fp)
@@ -303,11 +326,14 @@ class HybridsData:
303
326
  self.merge_col_overlap_values = solar_vals & wind_vals
304
327
 
305
328
  if not self.merge_col_overlap_values:
306
- msg = ("No overlap detected in the values of {!r} across the "
307
- "input files. Please ensure that at least one of the "
308
- "{!r} values is the same for input files {!r} and {!r}")
309
- e = msg.format(merge_col, merge_col, self.solar_fpath,
310
- self.wind_fpath)
329
+ msg = (
330
+ "No overlap detected in the values of {!r} across the "
331
+ "input files. Please ensure that at least one of the "
332
+ "{!r} values is the same for input files {!r} and {!r}"
333
+ )
334
+ e = msg.format(
335
+ merge_col, merge_col, self.solar_fpath, self.wind_fpath
336
+ )
311
337
  logger.error(e)
312
338
  raise FileInputError(e)
313
339
 
@@ -315,12 +341,18 @@ class HybridsData:
315
341
  class MetaHybridizer:
316
342
  """Framework to handle hybridization of meta data."""
317
343
 
318
- _INTERNAL_COL_PREFIX = '_h_internal'
319
-
320
- def __init__(self, data, allow_solar_only=False,
321
- allow_wind_only=False, fillna=None,
322
- limits=None, ratio_bounds=None,
323
- ratio='solar_capacity/wind_capacity'):
344
+ _INTERNAL_COL_PREFIX = "_h_internal"
345
+
346
+ def __init__(
347
+ self,
348
+ data,
349
+ allow_solar_only=False,
350
+ allow_wind_only=False,
351
+ fillna=None,
352
+ limits=None,
353
+ ratio_bounds=None,
354
+ ratio="solar_capacity/wind_capacity",
355
+ ):
324
356
  """
325
357
  Parameters
326
358
  ----------
@@ -383,8 +415,8 @@ class MetaHybridizer:
383
415
  self._hybrid_meta = None
384
416
  self.__hybrid_meta_cols = None
385
417
  self.__col_name_map = None
386
- self.__solar_rpi_n = '{}_solar_rpidx'.format(self._INTERNAL_COL_PREFIX)
387
- self.__wind_rpi_n = '{}_wind_rpidx'.format(self._INTERNAL_COL_PREFIX)
418
+ self.__solar_rpi_n = "{}_solar_rpidx".format(self._INTERNAL_COL_PREFIX)
419
+ self.__wind_rpi_n = "{}_wind_rpidx".format(self._INTERNAL_COL_PREFIX)
388
420
 
389
421
  @property
390
422
  def hybrid_meta(self):
@@ -398,8 +430,7 @@ class MetaHybridizer:
398
430
  """
399
431
  if self._hybrid_meta is None or self.__hybrid_meta_cols is None:
400
432
  return self._hybrid_meta
401
- else:
402
- return self._hybrid_meta[self.__hybrid_meta_cols]
433
+ return self._hybrid_meta[self.__hybrid_meta_cols]
403
434
 
404
435
  def validate_input(self):
405
436
  """Validate the input parameters.
@@ -426,18 +457,20 @@ class MetaHybridizer:
426
457
  """
427
458
  for col in self._limits:
428
459
  self.__validate_col_prefix(
429
- col, (SOLAR_PREFIX, WIND_PREFIX), input_name='limits'
460
+ col, (SOLAR_PREFIX, WIND_PREFIX), input_name="limits"
430
461
  )
431
462
 
432
463
  @staticmethod
433
464
  def __validate_col_prefix(col, prefixes, input_name):
434
- """Validate the the col starts with the correct prefix. """
465
+ """Validate the the col starts with the correct prefix."""
435
466
 
436
467
  missing = [not col.startswith(p) for p in prefixes]
437
468
  if all(missing):
438
- msg = ("Input {0} column {1!r} does not start with a valid "
439
- "prefix: {2!r}. Please ensure that the {0} column "
440
- "names specify the correct resource prefix.")
469
+ msg = (
470
+ "Input {0} column {1!r} does not start with a valid "
471
+ "prefix: {2!r}. Please ensure that the {0} column "
472
+ "names specify the correct resource prefix."
473
+ )
441
474
  e = msg.format(input_name, col, prefixes)
442
475
  logger.error(e)
443
476
  raise InputError(e)
@@ -457,7 +490,7 @@ class MetaHybridizer:
457
490
  """
458
491
  for col in self._fillna:
459
492
  self.__validate_col_prefix(
460
- col, (SOLAR_PREFIX, WIND_PREFIX), input_name='fillna'
493
+ col, (SOLAR_PREFIX, WIND_PREFIX), input_name="fillna"
461
494
  )
462
495
 
463
496
  def _validate_ratio_input(self):
@@ -487,18 +520,22 @@ class MetaHybridizer:
487
520
 
488
521
  try:
489
522
  if len(self._ratio_bounds) != 2:
490
- msg = ("Length of input for ratio_bounds is {} - but is "
491
- "required to be of length 2. Please make sure this "
492
- "input is a len 2 container of floats. If you would "
493
- "like to specify a single ratio value, use the same "
494
- "float for both limits (i.e. ratio_bounds=(1, 1)).")
523
+ msg = (
524
+ "Length of input for ratio_bounds is {} - but is "
525
+ "required to be of length 2. Please make sure this "
526
+ "input is a len 2 container of floats. If you would "
527
+ "like to specify a single ratio value, use the same "
528
+ "float for both limits (i.e. ratio_bounds=(1, 1))."
529
+ )
495
530
  e = msg.format(len(self._ratio_bounds))
496
531
  logger.error(e)
497
532
  raise InputError(e)
498
533
  except TypeError:
499
- msg = ("Input for ratio_bounds not understood: {!r}. "
500
- "Please make sure this value is a len 2 container "
501
- "of floats.")
534
+ msg = (
535
+ "Input for ratio_bounds not understood: {!r}. "
536
+ "Please make sure this value is a len 2 container "
537
+ "of floats."
538
+ )
502
539
  e = msg.format(self._ratio_bounds)
503
540
  logger.error(e)
504
541
  raise InputError(e) from None
@@ -512,10 +549,12 @@ class MetaHybridizer:
512
549
  If `ratio` is not a string.
513
550
  """
514
551
  if not isinstance(self._ratio, str):
515
- msg = ("Ratio input type {} not understood. Please make sure "
516
- "the ratio input is a string in the form "
517
- "'numerator_column_name/denominator_column_name'. Ratio "
518
- "input: {!r}")
552
+ msg = (
553
+ "Ratio input type {} not understood. Please make sure "
554
+ "the ratio input is a string in the form "
555
+ "'numerator_column_name/denominator_column_name'. Ratio "
556
+ "input: {!r}"
557
+ )
519
558
  e = msg.format(type(self._ratio), self._ratio)
520
559
  logger.error(e)
521
560
  raise InputError(e)
@@ -529,18 +568,22 @@ class MetaHybridizer:
529
568
  If the '/' character is missing or of there are too many
530
569
  '/' characters.
531
570
  """
532
- if '/' not in self._ratio:
533
- msg = ("Ratio input {} does not contain the '/' character. "
534
- "Please make sure the ratio input is a string in the form "
535
- "'numerator_column_name/denominator_column_name'")
571
+ if "/" not in self._ratio:
572
+ msg = (
573
+ "Ratio input {} does not contain the '/' character. "
574
+ "Please make sure the ratio input is a string in the form "
575
+ "'numerator_column_name/denominator_column_name'"
576
+ )
536
577
  e = msg.format(self._ratio)
537
578
  logger.error(e)
538
579
  raise InputError(e)
539
580
 
540
581
  if len(self._ratio_cols) != 2:
541
- msg = ("Ratio input {} contains too many '/' characters. Please "
542
- "make sure the ratio input is a string in the form "
543
- "'numerator_column_name/denominator_column_name'.")
582
+ msg = (
583
+ "Ratio input {} contains too many '/' characters. Please "
584
+ "make sure the ratio input is a string in the form "
585
+ "'numerator_column_name/denominator_column_name'."
586
+ )
544
587
  e = msg.format(self._ratio)
545
588
  logger.error(e)
546
589
  raise InputError(e)
@@ -561,7 +604,7 @@ class MetaHybridizer:
561
604
 
562
605
  for col in self._ratio_cols:
563
606
  self.__validate_col_prefix(
564
- col, (SOLAR_PREFIX, WIND_PREFIX), input_name='ratios'
607
+ col, (SOLAR_PREFIX, WIND_PREFIX), input_name="ratios"
565
608
  )
566
609
 
567
610
  def _validate_ratio_cols_exist(self):
@@ -574,27 +617,30 @@ class MetaHybridizer:
574
617
  """
575
618
 
576
619
  for col in self._ratio_cols:
577
- no_prefix_name = "_".join(col.split('_')[1:])
620
+ no_prefix_name = "_".join(col.split("_")[1:])
578
621
  if not self.data.contains_col(no_prefix_name):
579
- msg = ("Input ratios column {!r} not found in either meta "
580
- "data! Please check the input files {!r} and {!r}")
581
- e = msg.format(no_prefix_name, self.data.solar_fpath,
582
- self.data.wind_fpath)
622
+ msg = (
623
+ "Input ratios column {!r} not found in either meta "
624
+ "data! Please check the input files {!r} and {!r}"
625
+ )
626
+ e = msg.format(
627
+ no_prefix_name, self.data.solar_fpath, self.data.wind_fpath
628
+ )
583
629
  logger.error(e)
584
630
  raise FileInputError(e)
585
631
 
586
632
  @property
587
633
  def _ratio_cols(self):
588
- """Get the ratio columns from the ratio input. """
634
+ """Get the ratio columns from the ratio input."""
589
635
  if self._ratio is None:
590
636
  return []
591
- return self._ratio.strip().split('/')
637
+ return self._ratio.strip().split("/")
592
638
 
593
639
  def hybridize(self):
594
640
  """Combine the solar and wind metas and run hybridize methods."""
595
641
  self._format_meta_pre_merge()
596
642
  self._merge_solar_wind_meta()
597
- self._verify_lat_long_match_post_merge()
643
+ self._verify_lat_lon_match_post_merge()
598
644
  self._format_meta_post_merge()
599
645
  self._fillna_meta_cols()
600
646
  self._apply_limits()
@@ -603,7 +649,7 @@ class MetaHybridizer:
603
649
  self._sort_hybrid_meta_cols()
604
650
 
605
651
  def _format_meta_pre_merge(self):
606
- """Prepare solar and wind meta for merging. """
652
+ """Prepare solar and wind meta for merging."""
607
653
  self.__col_name_map = {
608
654
  ColNameFormatter.fmt(c): c
609
655
  for c in self.data.solar_meta.columns.values
@@ -616,49 +662,50 @@ class MetaHybridizer:
616
662
 
617
663
  @staticmethod
618
664
  def _rename_cols(df, prefix):
619
- """Replace column names with the ColNameFormatter.fmt is needed. """
665
+ """Replace column names with the ColNameFormatter.fmt is needed."""
620
666
  df.columns = [
621
667
  ColNameFormatter.fmt(col_name)
622
668
  if col_name in NON_DUPLICATE_COLS
623
- else '{}{}'.format(prefix, col_name)
669
+ else "{}{}".format(prefix, col_name)
624
670
  for col_name in df.columns.values
625
671
  ]
626
672
 
627
673
  def _save_rep_prof_index_internally(self):
628
- """Save rep profiles index in hybrid meta for access later. """
674
+ """Save rep profiles index in hybrid meta for access later."""
629
675
 
630
676
  self.data.solar_meta[self.__solar_rpi_n] = self.data.solar_meta.index
631
677
  self.data.wind_meta[self.__wind_rpi_n] = self.data.wind_meta.index
632
678
 
633
679
  def _merge_solar_wind_meta(self):
634
- """Merge the wind and solar meta DataFrames. """
680
+ """Merge the wind and solar meta DataFrames."""
635
681
  self._hybrid_meta = self.data.solar_meta.merge(
636
682
  self.data.wind_meta,
637
683
  on=ColNameFormatter.fmt(MERGE_COLUMN),
638
- suffixes=[None, '_x'], how=self._merge_type()
684
+ suffixes=[None, "_x"],
685
+ how=self._merge_type(),
639
686
  )
640
687
 
641
688
  def _merge_type(self):
642
- """Determine the type of merge to use for meta based on user input. """
689
+ """Determine the type of merge to use for meta based on user input."""
643
690
  if self._allow_solar_only and self._allow_wind_only:
644
691
  return 'outer'
645
- elif self._allow_solar_only and not self._allow_wind_only:
692
+ if self._allow_solar_only and not self._allow_wind_only:
646
693
  return 'left'
647
- elif not self._allow_solar_only and self._allow_wind_only:
694
+ if not self._allow_solar_only and self._allow_wind_only:
648
695
  return 'right'
649
696
  return 'inner'
650
697
 
651
698
  def _format_meta_post_merge(self):
652
- """Format hybrid meta after merging. """
699
+ """Format hybrid meta after merging."""
653
700
 
654
701
  duplicate_cols = [n for n in self._hybrid_meta.columns if "_x" in n]
655
702
  self._propagate_duplicate_cols(duplicate_cols)
656
703
  self._drop_cols(duplicate_cols)
657
704
  self._hybrid_meta.rename(self.__col_name_map, inplace=True, axis=1)
658
- self._hybrid_meta.index.name = 'gid'
705
+ self._hybrid_meta.index.name = HYBRIDS_GID_COL
659
706
 
660
707
  def _propagate_duplicate_cols(self, duplicate_cols):
661
- """Fill missing column values from outer merge. """
708
+ """Fill missing column values from outer merge."""
662
709
  for duplicate in duplicate_cols:
663
710
  no_suffix = "_".join(duplicate.split("_")[:-1])
664
711
  null_idx = self._hybrid_meta[no_suffix].isnull()
@@ -666,49 +713,61 @@ class MetaHybridizer:
666
713
  self._hybrid_meta.loc[null_idx, no_suffix] = non_null_vals
667
714
 
668
715
  def _drop_cols(self, duplicate_cols):
669
- """Drop any remaning duplicate and 'DROPPED_COLUMNS' columns. """
716
+ """Drop any remaining duplicate and 'HYBRIDS_GID_COL' columns."""
670
717
  self._hybrid_meta.drop(
671
- duplicate_cols + DROPPED_COLUMNS,
672
- axis=1, inplace=True, errors='ignore'
718
+ duplicate_cols + [HYBRIDS_GID_COL],
719
+ axis=1,
720
+ inplace=True,
721
+ errors="ignore",
673
722
  )
674
723
 
675
724
  def _sort_hybrid_meta_cols(self):
676
- """Sort the columns of the hybrid meta. """
725
+ """Sort the columns of the hybrid meta."""
677
726
  self.__hybrid_meta_cols = sorted(
678
- [c for c in self._hybrid_meta.columns
679
- if not c.startswith(self._INTERNAL_COL_PREFIX)],
680
- key=self._column_sorting_key
727
+ [
728
+ c
729
+ for c in self._hybrid_meta.columns
730
+ if not c.startswith(self._INTERNAL_COL_PREFIX)
731
+ ],
732
+ key=self._column_sorting_key,
681
733
  )
682
734
 
683
735
  def _column_sorting_key(self, c):
684
- """Helper function to sort hybrid meta columns. """
736
+ """Helper function to sort hybrid meta columns."""
685
737
  first_index = 0
686
- if c.startswith('hybrid'):
738
+ if c.startswith("hybrid"):
687
739
  first_index = 1
688
- elif c.startswith('solar'):
740
+ elif c.startswith("solar"):
689
741
  first_index = 2
690
- elif c.startswith('wind'):
742
+ elif c.startswith("wind"):
691
743
  first_index = 3
692
744
  elif c == MERGE_COLUMN:
693
745
  first_index = -1
694
746
  return first_index, self._hybrid_meta.columns.get_loc(c)
695
747
 
696
- def _verify_lat_long_match_post_merge(self):
748
+ def _verify_lat_lon_match_post_merge(self):
697
749
  """Verify that all the lat/lon values match post merge."""
698
- lat = self._verify_col_match_post_merge(col_name='latitude')
699
- lon = self._verify_col_match_post_merge(col_name='longitude')
750
+ lat = self._verify_col_match_post_merge(
751
+ col_name=ColNameFormatter.fmt(SupplyCurveField.LATITUDE)
752
+ )
753
+ lon = self._verify_col_match_post_merge(
754
+ col_name=ColNameFormatter.fmt(SupplyCurveField.LONGITUDE)
755
+ )
700
756
  if not lat or not lon:
701
- msg = ("Detected mismatched coordinate values (latitude or "
702
- "longitude) post merge. Please ensure that all matching "
703
- "values of {!r} correspond to the same values of latitude "
704
- "and longitude across the input files {!r} and {!r}")
705
- e = msg.format(MERGE_COLUMN, self.data.solar_fpath,
706
- self.data.wind_fpath)
757
+ msg = (
758
+ "Detected mismatched coordinate values (latitude or "
759
+ "longitude) post merge. Please ensure that all matching "
760
+ "values of {!r} correspond to the same values of latitude "
761
+ "and longitude across the input files {!r} and {!r}"
762
+ )
763
+ e = msg.format(
764
+ MERGE_COLUMN, self.data.solar_fpath, self.data.wind_fpath
765
+ )
707
766
  logger.error(e)
708
767
  raise FileInputError(e)
709
768
 
710
769
  def _verify_col_match_post_merge(self, col_name):
711
- """Verify that all (non-null) values in a column match post merge. """
770
+ """Verify that all (non-null) values in a column match post merge."""
712
771
  c1, c2 = col_name, '{}_x'.format(col_name)
713
772
  if c1 in self._hybrid_meta.columns and c2 in self._hybrid_meta.columns:
714
773
  compare_df = self._hybrid_meta[
@@ -716,23 +775,22 @@ class MetaHybridizer:
716
775
  & (self._hybrid_meta[c2].notnull())
717
776
  ]
718
777
  return np.allclose(compare_df[c1], compare_df[c2])
719
- else:
720
- return True
778
+ return True
721
779
 
722
780
  def _fillna_meta_cols(self):
723
- """Fill N/A values as specified by user (and internals). """
781
+ """Fill N/A values as specified by user (and internals)."""
724
782
  for col_name, fill_value in self._fillna.items():
725
783
  if col_name in self._hybrid_meta.columns:
726
784
  self._hybrid_meta[col_name].fillna(fill_value, inplace=True)
727
785
  else:
728
- self.__warn_missing_col(col_name, action='fill')
786
+ self.__warn_missing_col(col_name, action="fill")
729
787
 
730
788
  self._hybrid_meta[self.__solar_rpi_n].fillna(-1, inplace=True)
731
789
  self._hybrid_meta[self.__wind_rpi_n].fillna(-1, inplace=True)
732
790
 
733
791
  @staticmethod
734
792
  def __warn_missing_col(col_name, action):
735
- """Warn that a column the user request an action for is missing. """
793
+ """Warn that a column the user request an action for is missing."""
736
794
  msg = ("Skipping {} values for {!r}: Unable to find column "
737
795
  "in hybrid meta. Did you forget to prefix with "
738
796
  "{!r} or {!r}? ")
@@ -741,15 +799,15 @@ class MetaHybridizer:
741
799
  warn(w, InputWarning)
742
800
 
743
801
  def _apply_limits(self):
744
- """Clip column values as specified by user. """
802
+ """Clip column values as specified by user."""
745
803
  for col_name, max_value in self._limits.items():
746
804
  if col_name in self._hybrid_meta.columns:
747
805
  self._hybrid_meta[col_name].clip(upper=max_value, inplace=True)
748
806
  else:
749
- self.__warn_missing_col(col_name, action='limit')
807
+ self.__warn_missing_col(col_name, action="limit")
750
808
 
751
809
  def _limit_by_ratio(self):
752
- """ Limit the given pair of ratio columns based on input ratio. """
810
+ """Limit the given pair of ratio columns based on input ratio."""
753
811
 
754
812
  if self._ratio_bounds is None:
755
813
  return
@@ -765,8 +823,7 @@ class MetaHybridizer:
765
823
  denominator_vals = self._hybrid_meta[denominator_col].copy()
766
824
 
767
825
  ratios = (
768
- numerator_vals.loc[overlap_idx]
769
- / denominator_vals.loc[overlap_idx]
826
+ numerator_vals.loc[overlap_idx] / denominator_vals.loc[overlap_idx]
770
827
  )
771
828
  ratio_too_low = (ratios < min_ratio) & overlap_idx
772
829
  ratio_too_high = (ratios > max_ratio) & overlap_idx
@@ -784,16 +841,18 @@ class MetaHybridizer:
784
841
  self._hybrid_meta[h_denom_name] = denominator_vals.values
785
842
 
786
843
  def _add_hybrid_cols(self):
787
- """Add new hybrid columns using registered hybrid methods. """
844
+ """Add new hybrid columns using registered hybrid methods."""
788
845
  for new_col_name, method in HYBRID_METHODS.items():
789
846
  out = method(self)
790
847
  if out is not None:
791
848
  try:
792
849
  self._hybrid_meta[new_col_name] = out
793
850
  except ValueError as e:
794
- msg = ("Unable to add {!r} column to hybrid meta. The "
795
- "following exception was raised when adding "
796
- "the data output by '{}': {!r}.")
851
+ msg = (
852
+ "Unable to add {!r} column to hybrid meta. The "
853
+ "following exception was raised when adding "
854
+ "the data output by '{}': {!r}."
855
+ )
797
856
  w = msg.format(new_col_name, method.__name__, e)
798
857
  logger.warning(w)
799
858
  warn(w, OutputWarning)
@@ -843,9 +902,17 @@ class MetaHybridizer:
843
902
  class Hybridization:
844
903
  """Hybridization"""
845
904
 
846
- def __init__(self, solar_fpath, wind_fpath, allow_solar_only=False,
847
- allow_wind_only=False, fillna=None, limits=None,
848
- ratio_bounds=None, ratio='solar_capacity/wind_capacity'):
905
+ def __init__(
906
+ self,
907
+ solar_fpath,
908
+ wind_fpath,
909
+ allow_solar_only=False,
910
+ allow_wind_only=False,
911
+ fillna=None,
912
+ limits=None,
913
+ ratio_bounds=None,
914
+ ratio="solar_capacity/wind_capacity",
915
+ ):
849
916
  """Framework to handle hybridization of SC and corresponding profiles.
850
917
 
851
918
  ``reV`` hybrids computes a "hybrid" wind and solar supply curve,
@@ -909,34 +976,57 @@ class Hybridization:
909
976
  variables. By default ``'solar_capacity/wind_capacity'``.
910
977
  """
911
978
 
912
- logger.info('Running hybridization of rep profiles with solar_fpath: '
913
- '"{}"'.format(solar_fpath))
914
- logger.info('Running hybridization of rep profiles with solar_fpath: '
915
- '"{}"'.format(wind_fpath))
916
- logger.info('Running hybridization of rep profiles with '
917
- 'allow_solar_only: "{}"'.format(allow_solar_only))
918
- logger.info('Running hybridization of rep profiles with '
919
- 'allow_wind_only: "{}"'.format(allow_wind_only))
920
- logger.info('Running hybridization of rep profiles with fillna: "{}"'
921
- .format(fillna))
922
- logger.info('Running hybridization of rep profiles with limits: "{}"'
923
- .format(limits))
924
- logger.info('Running hybridization of rep profiles with ratio_bounds: '
925
- '"{}"'.format(ratio_bounds))
926
- logger.info('Running hybridization of rep profiles with ratio: "{}"'
927
- .format(ratio))
979
+ logger.info(
980
+ "Running hybridization of rep profiles with solar_fpath: "
981
+ '"{}"'.format(solar_fpath)
982
+ )
983
+ logger.info(
984
+ "Running hybridization of rep profiles with solar_fpath: "
985
+ '"{}"'.format(wind_fpath)
986
+ )
987
+ logger.info(
988
+ "Running hybridization of rep profiles with "
989
+ 'allow_solar_only: "{}"'.format(allow_solar_only)
990
+ )
991
+ logger.info(
992
+ "Running hybridization of rep profiles with "
993
+ 'allow_wind_only: "{}"'.format(allow_wind_only)
994
+ )
995
+ logger.info(
996
+ 'Running hybridization of rep profiles with fillna: "{}"'.format(
997
+ fillna
998
+ )
999
+ )
1000
+ logger.info(
1001
+ 'Running hybridization of rep profiles with limits: "{}"'.format(
1002
+ limits
1003
+ )
1004
+ )
1005
+ logger.info(
1006
+ "Running hybridization of rep profiles with ratio_bounds: "
1007
+ '"{}"'.format(ratio_bounds)
1008
+ )
1009
+ logger.info(
1010
+ 'Running hybridization of rep profiles with ratio: "{}"'.format(
1011
+ ratio
1012
+ )
1013
+ )
928
1014
 
929
1015
  self.data = HybridsData(solar_fpath, wind_fpath)
930
1016
  self.meta_hybridizer = MetaHybridizer(
931
- data=self.data, allow_solar_only=allow_solar_only,
932
- allow_wind_only=allow_wind_only, fillna=fillna, limits=limits,
933
- ratio_bounds=ratio_bounds, ratio=ratio
1017
+ data=self.data,
1018
+ allow_solar_only=allow_solar_only,
1019
+ allow_wind_only=allow_wind_only,
1020
+ fillna=fillna,
1021
+ limits=limits,
1022
+ ratio_bounds=ratio_bounds,
1023
+ ratio=ratio,
934
1024
  )
935
1025
  self._profiles = None
936
1026
  self._validate_input()
937
1027
 
938
1028
  def _validate_input(self):
939
- """Validate the user input and input files. """
1029
+ """Validate the user input and input files."""
940
1030
  self.data.validate()
941
1031
  self.meta_hybridizer.validate_input()
942
1032
 
@@ -1042,7 +1132,7 @@ class Hybridization:
1042
1132
  if fout is not None:
1043
1133
  self.save_profiles(fout, save_hybrid_meta=save_hybrid_meta)
1044
1134
 
1045
- logger.info('Hybridization of representative profiles complete!')
1135
+ logger.info("Hybridization of representative profiles complete!")
1046
1136
  return fout
1047
1137
 
1048
1138
  def run_meta(self):
@@ -1067,54 +1157,68 @@ class Hybridization:
1067
1157
  hybridized profiles as attributes.
1068
1158
  """
1069
1159
 
1070
- logger.info('Running hybrid profile calculations.')
1160
+ logger.info("Running hybrid profile calculations.")
1071
1161
 
1072
1162
  self._init_profiles()
1073
1163
  self._compute_hybridized_profile_components()
1074
1164
  self._compute_hybridized_profiles_from_components()
1075
1165
 
1076
- logger.info('Profile hybridization complete.')
1166
+ logger.info("Profile hybridization complete.")
1077
1167
 
1078
1168
  return self
1079
1169
 
1080
1170
  def _init_profiles(self):
1081
1171
  """Initialize the output rep profiles attribute."""
1082
1172
  self._profiles = {
1083
- k: np.zeros((len(self.hybrid_time_index), len(self.hybrid_meta)),
1084
- dtype=np.float32)
1085
- for k in OUTPUT_PROFILE_NAMES}
1173
+ k: np.zeros(
1174
+ (len(self.hybrid_time_index), len(self.hybrid_meta)),
1175
+ dtype=np.float32,
1176
+ )
1177
+ for k in OUTPUT_PROFILE_NAMES
1178
+ }
1086
1179
 
1087
1180
  def _compute_hybridized_profile_components(self):
1088
- """Compute the resource components of the hybridized profiles. """
1181
+ """Compute the resource components of the hybridized profiles."""
1089
1182
 
1090
1183
  for params in self.__rep_profile_hybridization_params:
1091
1184
  col, (hybrid_idxs, solar_idxs), fpath, p_name, dset_name = params
1092
1185
  capacity = self.hybrid_meta.loc[hybrid_idxs, col].values
1093
1186
 
1094
1187
  with Resource(fpath) as res:
1095
- data = res[dset_name,
1096
- res.time_index.isin(self.hybrid_time_index)]
1097
- self._profiles[p_name][:, hybrid_idxs] = (data[:, solar_idxs]
1098
- * capacity)
1188
+ data = res[
1189
+ dset_name, res.time_index.isin(self.hybrid_time_index)
1190
+ ]
1191
+ self._profiles[p_name][:, hybrid_idxs] = (
1192
+ data[:, solar_idxs] * capacity
1193
+ )
1099
1194
 
1100
1195
  @property
1101
1196
  def __rep_profile_hybridization_params(self):
1102
- """Zip the rep profile hybridization parameters. """
1197
+ """Zip the rep profile hybridization parameters."""
1103
1198
 
1104
- cap_col_names = ['hybrid_solar_capacity', 'hybrid_wind_capacity']
1105
- idx_maps = [self.meta_hybridizer.solar_profile_indices_map,
1106
- self.meta_hybridizer.wind_profile_indices_map]
1199
+ cap_col_names = [f"hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}",
1200
+ f"hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}"]
1201
+ idx_maps = [
1202
+ self.meta_hybridizer.solar_profile_indices_map,
1203
+ self.meta_hybridizer.wind_profile_indices_map,
1204
+ ]
1107
1205
  fpaths = [self.data.solar_fpath, self.data.wind_fpath]
1108
- zipped = zip(cap_col_names, idx_maps, fpaths, OUTPUT_PROFILE_NAMES[1:],
1109
- self.data.profile_dset_names)
1206
+ zipped = zip(
1207
+ cap_col_names,
1208
+ idx_maps,
1209
+ fpaths,
1210
+ OUTPUT_PROFILE_NAMES[1:],
1211
+ self.data.profile_dset_names,
1212
+ )
1110
1213
  return zipped
1111
1214
 
1112
1215
  def _compute_hybridized_profiles_from_components(self):
1113
- """Compute the hybridized profiles from the resource components. """
1216
+ """Compute the hybridized profiles from the resource components."""
1114
1217
 
1115
1218
  hp_name, sp_name, wp_name = OUTPUT_PROFILE_NAMES
1116
- self._profiles[hp_name] = (self._profiles[sp_name]
1117
- + self._profiles[wp_name])
1219
+ self._profiles[hp_name] = (
1220
+ self._profiles[sp_name] + self._profiles[wp_name]
1221
+ )
1118
1222
 
1119
1223
  def _init_h5_out(self, fout, save_hybrid_meta=True):
1120
1224
  """Initialize an output h5 file for hybrid profiles.
@@ -1146,14 +1250,26 @@ class Hybridization:
1146
1250
  except ValueError:
1147
1251
  pass
1148
1252
 
1149
- Outputs.init_h5(fout, dsets, shapes, attrs, chunks, dtypes,
1150
- meta, time_index=self.hybrid_time_index)
1253
+ Outputs.init_h5(
1254
+ fout,
1255
+ dsets,
1256
+ shapes,
1257
+ attrs,
1258
+ chunks,
1259
+ dtypes,
1260
+ meta,
1261
+ time_index=self.hybrid_time_index,
1262
+ )
1151
1263
 
1152
1264
  if save_hybrid_meta:
1153
- with Outputs(fout, mode='a') as out:
1265
+ with Outputs(fout, mode="a") as out:
1154
1266
  hybrid_meta = to_records_array(self.hybrid_meta)
1155
- out._create_dset('meta', hybrid_meta.shape,
1156
- hybrid_meta.dtype, data=hybrid_meta)
1267
+ out._create_dset(
1268
+ "meta",
1269
+ hybrid_meta.shape,
1270
+ hybrid_meta.dtype,
1271
+ data=hybrid_meta,
1272
+ )
1157
1273
 
1158
1274
  def _write_h5_out(self, fout, save_hybrid_meta=True):
1159
1275
  """Write hybrid profiles and meta to an output file.
@@ -1166,10 +1282,10 @@ class Hybridization:
1166
1282
  Flag to save hybrid SC table to hybrid rep profile output.
1167
1283
  """
1168
1284
 
1169
- with Outputs(fout, mode='a') as out:
1170
- if 'meta' in out.datasets and save_hybrid_meta:
1285
+ with Outputs(fout, mode="a") as out:
1286
+ if "meta" in out.datasets and save_hybrid_meta:
1171
1287
  hybrid_meta = to_records_array(self.hybrid_meta)
1172
- out['meta'] = hybrid_meta
1288
+ out["meta"] = hybrid_meta
1173
1289
 
1174
1290
  for dset, data in self.profiles.items():
1175
1291
  out[dset] = data