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.
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/METADATA +13 -10
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/RECORD +43 -43
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/WHEEL +1 -1
- reV/SAM/SAM.py +217 -133
- reV/SAM/econ.py +18 -14
- reV/SAM/generation.py +611 -422
- reV/SAM/windbos.py +93 -79
- reV/bespoke/bespoke.py +681 -377
- reV/bespoke/cli_bespoke.py +2 -0
- reV/bespoke/place_turbines.py +187 -43
- reV/config/output_request.py +2 -1
- reV/config/project_points.py +218 -140
- reV/econ/econ.py +166 -114
- reV/econ/economies_of_scale.py +91 -45
- reV/generation/base.py +331 -184
- reV/generation/generation.py +326 -200
- reV/generation/output_attributes/lcoe_fcr_inputs.json +38 -3
- reV/handlers/__init__.py +0 -1
- reV/handlers/exclusions.py +16 -15
- reV/handlers/multi_year.py +57 -26
- reV/handlers/outputs.py +6 -5
- reV/handlers/transmission.py +44 -27
- reV/hybrids/hybrid_methods.py +30 -30
- reV/hybrids/hybrids.py +305 -189
- reV/nrwal/nrwal.py +262 -168
- reV/qa_qc/cli_qa_qc.py +14 -10
- reV/qa_qc/qa_qc.py +217 -119
- reV/qa_qc/summary.py +228 -146
- reV/rep_profiles/rep_profiles.py +349 -230
- reV/supply_curve/aggregation.py +349 -188
- reV/supply_curve/competitive_wind_farms.py +90 -48
- reV/supply_curve/exclusions.py +138 -85
- reV/supply_curve/extent.py +75 -50
- reV/supply_curve/points.py +735 -390
- reV/supply_curve/sc_aggregation.py +357 -248
- reV/supply_curve/supply_curve.py +604 -347
- reV/supply_curve/tech_mapping.py +144 -82
- reV/utilities/__init__.py +274 -16
- reV/utilities/pytest_utils.py +8 -4
- reV/version.py +1 -1
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/LICENSE +0 -0
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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 =
|
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
|
-
|
30
|
-
|
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
|
-
|
33
|
-
DEFAULT_FILL_VALUES = {'
|
34
|
-
'
|
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
|
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=
|
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 = (
|
206
|
-
|
207
|
-
|
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 = (
|
228
|
-
|
229
|
-
|
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
|
-
|
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
|
-
|
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 = (
|
254
|
-
|
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(
|
258
|
-
|
259
|
-
|
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 = (
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
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(
|
282
|
-
|
283
|
-
|
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 = (
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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 =
|
319
|
-
|
320
|
-
def __init__(
|
321
|
-
|
322
|
-
|
323
|
-
|
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 =
|
387
|
-
self.__wind_rpi_n =
|
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
|
-
|
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=
|
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 = (
|
439
|
-
|
440
|
-
|
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=
|
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 = (
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
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 = (
|
500
|
-
|
501
|
-
|
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 = (
|
516
|
-
|
517
|
-
|
518
|
-
|
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
|
533
|
-
msg = (
|
534
|
-
|
535
|
-
|
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 = (
|
542
|
-
|
543
|
-
|
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=
|
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(
|
620
|
+
no_prefix_name = "_".join(col.split("_")[1:])
|
578
621
|
if not self.data.contains_col(no_prefix_name):
|
579
|
-
msg = (
|
580
|
-
|
581
|
-
|
582
|
-
|
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.
|
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
|
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,
|
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
|
-
|
692
|
+
if self._allow_solar_only and not self._allow_wind_only:
|
646
693
|
return 'left'
|
647
|
-
|
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 =
|
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
|
716
|
+
"""Drop any remaining duplicate and 'HYBRIDS_GID_COL' columns."""
|
670
717
|
self._hybrid_meta.drop(
|
671
|
-
duplicate_cols +
|
672
|
-
axis=1,
|
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
|
-
[
|
679
|
-
|
680
|
-
|
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(
|
738
|
+
if c.startswith("hybrid"):
|
687
739
|
first_index = 1
|
688
|
-
elif c.startswith(
|
740
|
+
elif c.startswith("solar"):
|
689
741
|
first_index = 2
|
690
|
-
elif c.startswith(
|
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
|
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(
|
699
|
-
|
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 = (
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
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
|
-
|
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=
|
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=
|
807
|
+
self.__warn_missing_col(col_name, action="limit")
|
750
808
|
|
751
809
|
def _limit_by_ratio(self):
|
752
|
-
"""
|
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 = (
|
795
|
-
|
796
|
-
|
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__(
|
847
|
-
|
848
|
-
|
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(
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
logger.info(
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
logger.info(
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
logger.info(
|
925
|
-
|
926
|
-
|
927
|
-
|
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,
|
932
|
-
|
933
|
-
|
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(
|
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(
|
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(
|
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(
|
1084
|
-
|
1085
|
-
|
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[
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
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 = [
|
1105
|
-
|
1106
|
-
|
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(
|
1109
|
-
|
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] = (
|
1117
|
-
|
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(
|
1150
|
-
|
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=
|
1265
|
+
with Outputs(fout, mode="a") as out:
|
1154
1266
|
hybrid_meta = to_records_array(self.hybrid_meta)
|
1155
|
-
out._create_dset(
|
1156
|
-
|
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=
|
1170
|
-
if
|
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[
|
1288
|
+
out["meta"] = hybrid_meta
|
1173
1289
|
|
1174
1290
|
for dset, data in self.profiles.items():
|
1175
1291
|
out[dset] = data
|