NREL-reV 0.9.7__py3-none-any.whl → 0.12.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {NREL_reV-0.9.7.dist-info → nrel_rev-0.12.2.dist-info}/METADATA +28 -32
- {NREL_reV-0.9.7.dist-info → nrel_rev-0.12.2.dist-info}/RECORD +30 -29
- {NREL_reV-0.9.7.dist-info → nrel_rev-0.12.2.dist-info}/WHEEL +1 -1
- {NREL_reV-0.9.7.dist-info → nrel_rev-0.12.2.dist-info}/entry_points.txt +0 -1
- reV/SAM/SAM.py +30 -7
- reV/SAM/defaults/WY Southern-Flat Lands.srw +8765 -0
- reV/SAM/defaults/geothermal.json +27 -16
- reV/SAM/defaults/i_pvwattsv5.json +1 -1
- reV/SAM/defaults.py +3 -19
- reV/SAM/generation.py +18 -52
- reV/bespoke/bespoke.py +39 -81
- reV/bespoke/cli_bespoke.py +0 -2
- reV/bespoke/pack_turbs.py +15 -17
- reV/bespoke/place_turbines.py +2 -25
- reV/config/output_request.py +10 -1
- reV/config/project_points.py +126 -19
- reV/generation/base.py +22 -4
- reV/generation/generation.py +45 -22
- reV/generation/output_attributes/linear_fresnel.json +7 -0
- reV/losses/power_curve.py +3 -0
- reV/losses/scheduled.py +35 -14
- reV/supply_curve/aggregation.py +47 -0
- reV/supply_curve/points.py +54 -0
- reV/supply_curve/sc_aggregation.py +76 -38
- reV/utilities/__init__.py +2 -0
- reV/utilities/_clean_readme.py +28 -0
- reV/utilities/curtailment.py +17 -13
- reV/version.py +1 -1
- reV/bespoke/plotting_functions.py +0 -178
- {NREL_reV-0.9.7.dist-info → nrel_rev-0.12.2.dist-info/licenses}/LICENSE +0 -0
- {NREL_reV-0.9.7.dist-info → nrel_rev-0.12.2.dist-info}/top_level.txt +0 -0
reV/bespoke/place_turbines.py
CHANGED
@@ -49,8 +49,7 @@ class PlaceTurbines:
|
|
49
49
|
fixed_operating_cost_function,
|
50
50
|
variable_operating_cost_function,
|
51
51
|
balance_of_system_cost_function,
|
52
|
-
include_mask, pixel_side_length, min_spacing
|
53
|
-
wake_loss_multiplier=1):
|
52
|
+
include_mask, pixel_side_length, min_spacing):
|
54
53
|
"""
|
55
54
|
Parameters
|
56
55
|
----------
|
@@ -118,12 +117,6 @@ class PlaceTurbines:
|
|
118
117
|
Side length (m) of a single pixel of the `include_mask`.
|
119
118
|
min_spacing : float
|
120
119
|
The minimum spacing between turbines (in meters).
|
121
|
-
wake_loss_multiplier : float, optional
|
122
|
-
A multiplier used to scale the annual energy lost due to
|
123
|
-
wake losses. **IMPORTANT**: This multiplier will ONLY be
|
124
|
-
applied during the optimization process and will NOT be
|
125
|
-
come through in output values such as aep, any of the cost
|
126
|
-
functions, or even the output objective.
|
127
120
|
"""
|
128
121
|
|
129
122
|
# inputs
|
@@ -139,7 +132,6 @@ class PlaceTurbines:
|
|
139
132
|
self.include_mask = include_mask
|
140
133
|
self.pixel_side_length = pixel_side_length
|
141
134
|
self.min_spacing = min_spacing
|
142
|
-
self.wake_loss_multiplier = wake_loss_multiplier
|
143
135
|
|
144
136
|
# internal variables
|
145
137
|
self.nrows, self.ncols = np.shape(include_mask)
|
@@ -269,7 +261,7 @@ class PlaceTurbines:
|
|
269
261
|
|
270
262
|
self.wind_plant.assign_inputs()
|
271
263
|
self.wind_plant.execute()
|
272
|
-
aep = self.
|
264
|
+
aep = self.wind_plant['annual_energy']
|
273
265
|
avg_sl_dist_to_center_m = self._avg_sl_dist_to_cent(x_locs, y_locs)
|
274
266
|
avg_sl_dist_to_medoid_m = self._avg_sl_dist_to_med(x_locs, y_locs)
|
275
267
|
if "nn_conn_dist_m" in self.balance_of_system_cost_function:
|
@@ -301,21 +293,6 @@ class PlaceTurbines:
|
|
301
293
|
|
302
294
|
return objective
|
303
295
|
|
304
|
-
def _aep_after_scaled_wake_losses(self):
|
305
|
-
"""AEP after scaling the energy lost due to wake."""
|
306
|
-
wake_loss_pct = self.wind_plant['wake_losses']
|
307
|
-
aep = self.wind_plant['annual_energy']
|
308
|
-
agep = self.wind_plant['annual_gross_energy']
|
309
|
-
|
310
|
-
energy_lost_due_to_wake = wake_loss_pct / 100 * agep
|
311
|
-
aep_after_wake_losses = agep - energy_lost_due_to_wake
|
312
|
-
other_losses_multiplier = 1 - aep / aep_after_wake_losses
|
313
|
-
|
314
|
-
scaled_wake_losses = (self.wake_loss_multiplier
|
315
|
-
* energy_lost_due_to_wake)
|
316
|
-
aep_after_scaled_wake_losses = max(0, agep - scaled_wake_losses)
|
317
|
-
return aep_after_scaled_wake_losses * (1 - other_losses_multiplier)
|
318
|
-
|
319
296
|
def optimize(self, **kwargs):
|
320
297
|
"""Optimize wind farm layout.
|
321
298
|
|
reV/config/output_request.py
CHANGED
@@ -101,5 +101,14 @@ class SAMOutputRequest(OutputRequest):
|
|
101
101
|
'dc_power': 'dc',
|
102
102
|
'clipping': 'clipped_power',
|
103
103
|
'clipped': 'clipped_power',
|
104
|
-
'clip': 'clipped_power'
|
104
|
+
'clip': 'clipped_power',
|
105
|
+
'capacity': 'system_capacity',
|
106
|
+
'wd': 'winddirection',
|
107
|
+
'wind_direction': 'winddirection',
|
108
|
+
'wind-direction': 'winddirection',
|
109
|
+
'wl': 'annual_wake_loss_internal_percent',
|
110
|
+
'wl_kwh': 'annual_wake_loss_internal_kWh',
|
111
|
+
'wl_pct': 'annual_wake_loss_total_percent',
|
112
|
+
'wl_ts': 'wake_loss_internal_percent',
|
113
|
+
'wl_kwh': 'wake_loss_internal_kW',
|
105
114
|
}
|
reV/config/project_points.py
CHANGED
@@ -24,6 +24,7 @@ from reV.utilities import SiteDataField, SupplyCurveField
|
|
24
24
|
from reV.utilities.exceptions import ConfigError, ConfigWarning
|
25
25
|
|
26
26
|
logger = logging.getLogger(__name__)
|
27
|
+
_DEFAULT_CURTAIL_KEY = "default"
|
27
28
|
|
28
29
|
|
29
30
|
class PointsControl:
|
@@ -246,7 +247,7 @@ class ProjectPoints:
|
|
246
247
|
pre loaded SAMConfig object.
|
247
248
|
tech : str, optional
|
248
249
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
249
|
-
solarwaterheat,
|
250
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
250
251
|
The string should be lower-cased with spaces and _ removed,
|
251
252
|
by default None
|
252
253
|
res_file : str | NoneType
|
@@ -269,6 +270,7 @@ class ProjectPoints:
|
|
269
270
|
self._tech = str(tech)
|
270
271
|
self._h = self._d = None
|
271
272
|
self._curtailment = self._parse_curtailment(curtailment)
|
273
|
+
self._check_points_curtailment_mapping()
|
272
274
|
|
273
275
|
def __getitem__(self, site):
|
274
276
|
"""Get the SAM config ID and dictionary for the requested site.
|
@@ -444,7 +446,7 @@ class ProjectPoints:
|
|
444
446
|
-------
|
445
447
|
_tech : str
|
446
448
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
447
|
-
solarwaterheat,
|
449
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
448
450
|
The string should be lower-cased with spaces and _ removed.
|
449
451
|
"""
|
450
452
|
return "windpower" if "wind" in self._tech.lower() else self._tech
|
@@ -532,10 +534,9 @@ class ProjectPoints:
|
|
532
534
|
|
533
535
|
Parameters
|
534
536
|
----------
|
535
|
-
points : int | str |
|
536
|
-
Slice specifying project points, string pointing to a
|
537
|
-
points csv
|
538
|
-
Can also be a single integer site value.
|
537
|
+
points : int | str | slice | list
|
538
|
+
Slice specifying project points, string pointing to a
|
539
|
+
project points csv. Can also be a single integer site value.
|
539
540
|
res_file : str | NoneType
|
540
541
|
Optional resource file to find maximum length of project points if
|
541
542
|
points slice stop is None.
|
@@ -625,6 +626,10 @@ class ProjectPoints:
|
|
625
626
|
if SiteDataField.CONFIG not in df.columns:
|
626
627
|
df[SiteDataField.CONFIG] = None
|
627
628
|
|
629
|
+
# pylint: disable=no-member
|
630
|
+
if SiteDataField.CURTAILMENT not in df.columns:
|
631
|
+
df[SiteDataField.CURTAILMENT] = None
|
632
|
+
|
628
633
|
gids = df[SiteDataField.GID].values
|
629
634
|
if not np.array_equal(np.sort(gids), gids):
|
630
635
|
msg = (
|
@@ -687,18 +692,29 @@ class ProjectPoints:
|
|
687
692
|
|
688
693
|
Returns
|
689
694
|
-------
|
690
|
-
curtailments : NoneType |
|
691
|
-
None if no curtailment, reV curtailment config
|
692
|
-
curtailment is being assessed.
|
695
|
+
curtailments : NoneType | dict
|
696
|
+
None if no curtailment, dictionary of reV curtailment config
|
697
|
+
objects if curtailment is being assessed.
|
693
698
|
"""
|
694
|
-
if
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
699
|
+
if curtailment_input is None:
|
700
|
+
return None
|
701
|
+
|
702
|
+
if isinstance(curtailment_input, str):
|
703
|
+
# pointer to config file - instantiate curtailment config
|
704
|
+
# object under default key
|
705
|
+
curtailment = {
|
706
|
+
_DEFAULT_CURTAIL_KEY: Curtailment(curtailment_input)}
|
707
|
+
|
708
|
+
elif isinstance(curtailment_input, dict):
|
709
|
+
# pointer to dict of configs - instantiate all
|
710
|
+
# curtailment config objects
|
711
|
+
curtailment = {k: Curtailment(v)
|
712
|
+
for k, v in curtailment_input.items()}
|
713
|
+
|
714
|
+
elif isinstance(curtailment_input, Curtailment):
|
715
|
+
# pre-initialized curtailment object - instantiate
|
716
|
+
# curtailment config object under default key
|
717
|
+
curtailment = {_DEFAULT_CURTAIL_KEY: curtailment_input}
|
702
718
|
|
703
719
|
else:
|
704
720
|
curtailment = None
|
@@ -790,6 +806,75 @@ class ProjectPoints:
|
|
790
806
|
logger.error(msg)
|
791
807
|
raise ConfigError(msg)
|
792
808
|
|
809
|
+
def _check_points_curtailment_mapping(self):
|
810
|
+
"""
|
811
|
+
Check to ensure the project points (df) and curtailment configs
|
812
|
+
are compatible. Update as necessary or break
|
813
|
+
"""
|
814
|
+
if not self.curtailment:
|
815
|
+
return
|
816
|
+
|
817
|
+
# Extract unique config references from project_points DataFrame
|
818
|
+
df_configs = self.df[SiteDataField.CURTAILMENT].unique()
|
819
|
+
curtail_configs = self.curtailment
|
820
|
+
|
821
|
+
can_fill_null = (len(df_configs) == 1
|
822
|
+
and df_configs[0] is None
|
823
|
+
and len(curtail_configs) == 1)
|
824
|
+
if can_fill_null:
|
825
|
+
self._df[SiteDataField.CURTAILMENT] = list(curtail_configs)[0]
|
826
|
+
df_configs = self.df[SiteDataField.CURTAILMENT].unique()
|
827
|
+
|
828
|
+
# Checks to make sure that the same number of curtailment config
|
829
|
+
# files as references in project_points DataFrame
|
830
|
+
if len(set(df_configs) - {None}) > len(curtail_configs):
|
831
|
+
msg = (
|
832
|
+
"Points references {} curtailment configs while only "
|
833
|
+
"{} curtailment configs were provided!".format(
|
834
|
+
len(df_configs), len(curtail_configs)
|
835
|
+
)
|
836
|
+
)
|
837
|
+
logger.error(msg)
|
838
|
+
raise ConfigError(msg)
|
839
|
+
|
840
|
+
|
841
|
+
unused_configs = set(curtail_configs) - set(df_configs)
|
842
|
+
if unused_configs:
|
843
|
+
msg = ("One or more curtailment configurations not found in "
|
844
|
+
"project points and are thus ignored: {}"
|
845
|
+
.format(unused_configs))
|
846
|
+
logger.warning(msg)
|
847
|
+
warn(msg, UserWarning)
|
848
|
+
|
849
|
+
# Check to see if config references in project_points DataFrame
|
850
|
+
# are valid file paths, if compare with curtailment configs
|
851
|
+
# and update as needed
|
852
|
+
configs = {}
|
853
|
+
for config in df_configs:
|
854
|
+
if config is None:
|
855
|
+
continue
|
856
|
+
if os.path.isfile(config):
|
857
|
+
configs[config] = config
|
858
|
+
elif config in curtail_configs:
|
859
|
+
configs[config] = curtail_configs[config]
|
860
|
+
else:
|
861
|
+
msg = ("Curtailment {} does not map to a valid "
|
862
|
+
"configuration file".format(config))
|
863
|
+
logger.error(msg)
|
864
|
+
raise ConfigError(msg)
|
865
|
+
|
866
|
+
# If configs has any keys that are not in curtailment configs then
|
867
|
+
# something really weird happened so raise an error.
|
868
|
+
if any(set(configs) - set(curtail_configs)):
|
869
|
+
msg = (
|
870
|
+
"A wild config has appeared! Requested config keys for "
|
871
|
+
"ProjectPoints are {} and previous config keys are {}".format(
|
872
|
+
list(configs), list(curtail_configs)
|
873
|
+
)
|
874
|
+
)
|
875
|
+
logger.error(msg)
|
876
|
+
raise ConfigError(msg)
|
877
|
+
|
793
878
|
def join_df(self, df2, key=SiteDataField.GID):
|
794
879
|
"""Join new df2 to the _df attribute using the _df's gid as pkey.
|
795
880
|
|
@@ -840,6 +925,28 @@ class ProjectPoints:
|
|
840
925
|
|
841
926
|
return list(sites)
|
842
927
|
|
928
|
+
def get_sites_from_curtailment(self, curtailment):
|
929
|
+
"""Get a site list that corresponds to a curtailment key.
|
930
|
+
|
931
|
+
Parameters
|
932
|
+
----------
|
933
|
+
curtailment : str
|
934
|
+
Curtailment configuration ID associated with sites.
|
935
|
+
|
936
|
+
Returns
|
937
|
+
-------
|
938
|
+
sites : list
|
939
|
+
List of sites associated with the requested curtailment ID.
|
940
|
+
If the curtailment ID is not recognized, an empty list is
|
941
|
+
returned.
|
942
|
+
"""
|
943
|
+
sites = self.df.loc[
|
944
|
+
(self.df[SiteDataField.CURTAILMENT] == curtailment),
|
945
|
+
SiteDataField.GID
|
946
|
+
].values
|
947
|
+
|
948
|
+
return list(sites)
|
949
|
+
|
843
950
|
@classmethod
|
844
951
|
def split(cls, i0, i1, project_points):
|
845
952
|
"""Return split instance of a ProjectPoints instance w/ site subset.
|
@@ -936,7 +1043,7 @@ class ProjectPoints:
|
|
936
1043
|
pre loaded SAMConfig object.
|
937
1044
|
tech : str, optional
|
938
1045
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
939
|
-
solarwaterheat,
|
1046
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
940
1047
|
The string should be lower-cased with spaces and _ removed,
|
941
1048
|
by default None
|
942
1049
|
curtailment : NoneType | dict | str | config.curtailment.Curtailment
|
@@ -1035,7 +1142,7 @@ class ProjectPoints:
|
|
1035
1142
|
pre loaded SAMConfig object.
|
1036
1143
|
tech : str, optional
|
1037
1144
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
1038
|
-
solarwaterheat,
|
1145
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
1039
1146
|
The string should be lower-cased with spaces and _ removed,
|
1040
1147
|
by default None
|
1041
1148
|
curtailment : NoneType | dict | str | config.curtailment.Curtailment
|
reV/generation/base.py
CHANGED
@@ -368,7 +368,7 @@ class BaseGen(ABC):
|
|
368
368
|
-------
|
369
369
|
tech : str
|
370
370
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
371
|
-
solarwaterheat,
|
371
|
+
solarwaterheat, lineardirectsteam, geothermal, econ)
|
372
372
|
The string should be lower-cased with spaces and _ removed.
|
373
373
|
"""
|
374
374
|
return self.project_points.tech
|
@@ -540,7 +540,7 @@ class BaseGen(ABC):
|
|
540
540
|
pre loaded SAMConfig object.
|
541
541
|
tech : str
|
542
542
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
543
|
-
solarwaterheat,
|
543
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
544
544
|
The string should be lower-cased with spaces and _ removed.
|
545
545
|
sites_per_worker : int
|
546
546
|
Number of sites to run in series on a worker. None defaults to the
|
@@ -622,7 +622,7 @@ class BaseGen(ABC):
|
|
622
622
|
pre loaded SAMConfig object.
|
623
623
|
tech : str
|
624
624
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
625
|
-
solarwaterheat,
|
625
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
626
626
|
The string should be lower-cased with spaces and _ removed.
|
627
627
|
sites_per_worker : int
|
628
628
|
Number of sites to run in series on a worker. None defaults to the
|
@@ -783,7 +783,7 @@ class BaseGen(ABC):
|
|
783
783
|
A PointsControl instance dictating what sites and configs are run.
|
784
784
|
tech : str
|
785
785
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
786
|
-
solarwaterheat,
|
786
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
787
787
|
The string should be lower-cased with spaces and _ removed.
|
788
788
|
res_file : str
|
789
789
|
Filepath to single resource file, multi-h5 directory,
|
@@ -1147,11 +1147,29 @@ class BaseGen(ABC):
|
|
1147
1147
|
if not isinstance(value, np.ndarray):
|
1148
1148
|
value = np.array(value)
|
1149
1149
|
|
1150
|
+
value = self._patch_wave_gen_pysam_5(var, value)
|
1150
1151
|
self._out[var][:, i] = value.T
|
1151
1152
|
|
1152
1153
|
elif value != 0:
|
1153
1154
|
self._out[var][i] = value
|
1154
1155
|
|
1156
|
+
def _patch_wave_gen_pysam_5(self, var, value):
|
1157
|
+
"""Patch the "gen" output of wave generation for PySAm 5+
|
1158
|
+
|
1159
|
+
As of PySAM 5+, the "gen" array is of shape 8760, but only the
|
1160
|
+
first 2920 entires are populated.
|
1161
|
+
See this line: https://github.com/NREL/ssc/blob/2098300044a9be7745c2b93b911adb2d9dc3c282/ssc/cmod_mhk_wave.cpp#L687
|
1162
|
+
"""
|
1163
|
+
if self.tech.casefold() != "mhkwave":
|
1164
|
+
return value
|
1165
|
+
if var.casefold() not in ("gen", "cf_profile", "gen_profile"):
|
1166
|
+
return value
|
1167
|
+
|
1168
|
+
if any(value[2920:]):
|
1169
|
+
msg = "Found non-zero values at end of gen array!"
|
1170
|
+
raise ValueError(msg)
|
1171
|
+
return value[:2920]
|
1172
|
+
|
1155
1173
|
def site_index(self, site_gid, out_index=False):
|
1156
1174
|
"""Get the index corresponding to the site gid.
|
1157
1175
|
|
reV/generation/generation.py
CHANGED
@@ -27,7 +27,6 @@ from reV.SAM.generation import (
|
|
27
27
|
PvWattsv8,
|
28
28
|
SolarWaterHeat,
|
29
29
|
TcsMoltenSalt,
|
30
|
-
TroughPhysicalHeat,
|
31
30
|
WindPower,
|
32
31
|
)
|
33
32
|
from reV.utilities import ModuleName, ResourceMetaField, SupplyCurveField
|
@@ -68,7 +67,6 @@ class Gen(BaseGen):
|
|
68
67
|
"pvwattsv8": PvWattsv8,
|
69
68
|
"solarwaterheat": SolarWaterHeat,
|
70
69
|
"tcsmoltensalt": TcsMoltenSalt,
|
71
|
-
"troughphysicalheat": TroughPhysicalHeat,
|
72
70
|
"windpower": WindPower,
|
73
71
|
}
|
74
72
|
|
@@ -175,12 +173,20 @@ class Gen(BaseGen):
|
|
175
173
|
|
176
174
|
- ``gid``: Integer specifying the generation GID of each
|
177
175
|
site.
|
178
|
-
- ``config``:
|
176
|
+
- ``config``: This is an *optional* column that contains
|
177
|
+
a key from the `sam_files` input dictionary
|
179
178
|
(see below) corresponding to the SAM configuration to
|
180
179
|
use for each particular site. This value can also be
|
181
|
-
``None``
|
182
|
-
a single SAM configuration file as the
|
183
|
-
input.
|
180
|
+
``None``, ``"default"``, or left out completely if you
|
181
|
+
specify only a single SAM configuration file as the
|
182
|
+
`sam_files` input.
|
183
|
+
- ``curtailment``: This is an *optional* column that
|
184
|
+
contains a key from the `curtailment` input dictionary
|
185
|
+
(see below) corresponding to the curtailment to apply
|
186
|
+
at that particular site. This value can also be
|
187
|
+
``None``, ``"default"``, or left out completely if you
|
188
|
+
specify only a single curtailment configuration file
|
189
|
+
as the `curtailment` input.
|
184
190
|
- ``capital_cost_multiplier``: This is an *optional*
|
185
191
|
multiplier input that, if included, will be used to
|
186
192
|
regionally scale the ``capital_cost`` input in the SAM
|
@@ -238,9 +244,9 @@ class Gen(BaseGen):
|
|
238
244
|
Filepath to resource data. This input can be path to a
|
239
245
|
single resource HDF5 file or a path including a wildcard
|
240
246
|
input like ``/h5_dir/prefix*suffix`` (i.e. if your datasets
|
241
|
-
|
242
|
-
all cases, the resource
|
243
|
-
:py:class:`rex.resource.Resource`
|
247
|
+
like wind speed, wind direction, pressure, and so on are
|
248
|
+
spread out over multiple files). In all cases, the resource
|
249
|
+
data must be readable by :py:class:`rex.resource.Resource`
|
244
250
|
or :py:class:`rex.multi_file_resource.MultiFileResource`.
|
245
251
|
(i.e. the resource data conform to the
|
246
252
|
`rex data format <https://tinyurl.com/3fy7v5kx>`_). This
|
@@ -254,13 +260,19 @@ class Gen(BaseGen):
|
|
254
260
|
|
255
261
|
.. Note:: If executing ``reV`` from the command line, this
|
256
262
|
input string can contain brackets ``{}`` that will be
|
257
|
-
filled in by the `analysis_years` input.
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
263
|
+
filled in by the `analysis_years` input. If your datasets
|
264
|
+
span multiple files (e.g. "wtk_wind_speed_2012.h5",
|
265
|
+
"wtk_pressure_2012.h5", "wtk_wind_direction_2012.h5"), you
|
266
|
+
may use a wildcard input along with brackets, like so:
|
267
|
+
``"wtk_*_{}.h5"``. Alternatively, this input can be a list
|
268
|
+
of explicit files to process. In this case, the length of
|
269
|
+
the list must match the length of the `analysis_years`
|
270
|
+
input exactly, and the paths are assumed to align with the
|
271
|
+
`analysis_years` (i.e. the first path corresponds to the
|
272
|
+
first analysis year, the second path corresponds to the
|
273
|
+
second analysis year, and so on). Wild cards are allowed,
|
274
|
+
even if you list out the years explicitly (i.e.
|
275
|
+
``["wtk_*_2012.h5", "wtk_*_2013.h5", ...]``)
|
264
276
|
|
265
277
|
.. Important:: If you are using custom resource data (i.e.
|
266
278
|
not NSRDB/WTK/Sup3rCC, etc.), ensure the following:
|
@@ -336,15 +348,26 @@ class Gen(BaseGen):
|
|
336
348
|
|
337
349
|
By default, ``None``.
|
338
350
|
curtailment : dict | str, optional
|
339
|
-
|
340
|
-
|
341
|
-
-
|
342
|
-
|
351
|
+
Input for curtailment parameters, which can be one of:
|
352
|
+
|
353
|
+
- Single string representing path to curtailment config
|
354
|
+
file. In this case, the curtailment config is given
|
355
|
+
the name "default" and applied everywhere (if the
|
356
|
+
project points "curtailment" column is missing or all
|
357
|
+
``None``) or only where the project points
|
358
|
+
"curtailment" column contains a value of "default"
|
359
|
+
- Dictionary mapping user-defined curtailment "names" to
|
360
|
+
either A) strings (paths) or B) explicit namespaces of
|
361
|
+
curtailment configurations (dicts). Mixing these two
|
362
|
+
_is_ allowed.
|
343
363
|
|
344
364
|
The allowed key-value input pairs in the curtailment
|
345
365
|
configuration are documented as properties of the
|
346
366
|
:class:`reV.config.curtailment.Curtailment` class. If
|
347
|
-
``None``, no curtailment is modeled.
|
367
|
+
``None``, no curtailment is modeled. You can select which
|
368
|
+
curtailment gets applied to which site using the
|
369
|
+
"curtailment" column key in the project points input.
|
370
|
+
By default, ``None``.
|
348
371
|
gid_map : dict | str, optional
|
349
372
|
Mapping of unique integer generation gids (keys) to single
|
350
373
|
integer resource gids (values). This enables unique
|
@@ -650,7 +673,7 @@ class Gen(BaseGen):
|
|
650
673
|
A PointsControl instance dictating what sites and configs are run.
|
651
674
|
tech : str
|
652
675
|
SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
|
653
|
-
solarwaterheat,
|
676
|
+
solarwaterheat, lineardirectsteam, geothermal)
|
654
677
|
The string should be lower-cased with spaces and _ removed.
|
655
678
|
res_file : str
|
656
679
|
Filepath to single resource file, multi-h5 directory,
|
reV/losses/power_curve.py
CHANGED
@@ -722,6 +722,9 @@ class PowerCurveLossesMixin:
|
|
722
722
|
if isinstance(power_curve_losses_info, str):
|
723
723
|
power_curve_losses_info = json.loads(power_curve_losses_info)
|
724
724
|
|
725
|
+
logger.info("Applying power curve losses using the following input:"
|
726
|
+
"\n{}".format(power_curve_losses_info))
|
727
|
+
|
725
728
|
loss_input = PowerCurveLossesInput(power_curve_losses_info)
|
726
729
|
if loss_input.target <= 0:
|
727
730
|
logger.debug("Power curve target loss is 0. Skipping power curve "
|
reV/losses/scheduled.py
CHANGED
@@ -7,6 +7,7 @@ import warnings
|
|
7
7
|
import json
|
8
8
|
|
9
9
|
import numpy as np
|
10
|
+
import PySAM
|
10
11
|
|
11
12
|
from reV.losses.utils import (convert_to_full_month_names,
|
12
13
|
filter_unknown_month_names,
|
@@ -523,10 +524,10 @@ class ScheduledLossesMixin:
|
|
523
524
|
information is expected to be a list of dictionaries containing
|
524
525
|
outage specifications. See :class:`Outage` for a description of
|
525
526
|
the specifications allowed for each outage. The scheduled losses
|
526
|
-
are passed to SAM via the ``
|
527
|
-
capacity factors should be adjusted with outage losses.
|
528
|
-
outage info is specified in ``sam_sys_inputs``, no
|
529
|
-
losses are added.
|
527
|
+
are passed to SAM via the ``adjust_hourly`` key to signify which
|
528
|
+
hourly capacity factors should be adjusted with outage losses.
|
529
|
+
If no outage info is specified in ``sam_sys_inputs``, no
|
530
|
+
scheduled losses are added.
|
530
531
|
|
531
532
|
Parameters
|
532
533
|
----------
|
@@ -542,13 +543,14 @@ class ScheduledLossesMixin:
|
|
542
543
|
|
543
544
|
Notes
|
544
545
|
-----
|
545
|
-
The scheduled losses are passed to SAM via the ``
|
546
|
-
signify which hourly capacity factors should be adjusted
|
547
|
-
outage losses. If the user specifies other hourly
|
548
|
-
factors via the ``
|
549
|
-
example, if the user inputs a 33% hourly
|
550
|
-
reV schedules an outage for 70% of the
|
551
|
-
hour, then the resulting adjustment
|
546
|
+
The scheduled losses are passed to SAM via the ``adjust_hourly``
|
547
|
+
key to signify which hourly capacity factors should be adjusted
|
548
|
+
with outage losses. If the user specifies other hourly
|
549
|
+
adjustment factors via the ``adjust_hourly`` key, the effect is
|
550
|
+
combined. For example, if the user inputs a 33% hourly
|
551
|
+
adjustment factor and reV schedules an outage for 70% of the
|
552
|
+
farm down for the same hour, then the resulting adjustment
|
553
|
+
factor is
|
552
554
|
|
553
555
|
.. math: 1 - [(1 - 70/100) * (1 - 33/100)] = 0.799
|
554
556
|
|
@@ -573,7 +575,7 @@ class ScheduledLossesMixin:
|
|
573
575
|
self._add_outages_to_sam_inputs(hourly_outages)
|
574
576
|
|
575
577
|
logger.debug("Hourly adjustment factors after scheduled outages: {}"
|
576
|
-
.format(list(self.sam_sys_inputs['
|
578
|
+
.format(list(self.sam_sys_inputs['adjust_hourly'])))
|
577
579
|
|
578
580
|
def _user_outage_input(self):
|
579
581
|
"""Get outage and seed info from config. """
|
@@ -600,12 +602,15 @@ class ScheduledLossesMixin:
|
|
600
602
|
"""Add the hourly adjustment factors to config, checking user input."""
|
601
603
|
|
602
604
|
hourly_mult = 1 - outages / 100
|
605
|
+
hourly_mult = self._fix_pysam_bug(hourly_mult)
|
603
606
|
|
604
|
-
user_hourly_input = self.sam_sys_inputs.pop('
|
607
|
+
user_hourly_input = self.sam_sys_inputs.pop('adjust_hourly',
|
608
|
+
[0] * 8760)
|
605
609
|
user_hourly_mult = 1 - np.array(user_hourly_input) / 100
|
606
610
|
|
607
611
|
final_hourly_mult = hourly_mult * user_hourly_mult
|
608
|
-
self.sam_sys_inputs['
|
612
|
+
self.sam_sys_inputs['adjust_hourly'] = (1 - final_hourly_mult) * 100
|
613
|
+
self.sam_sys_inputs['adjust_en_hourly'] = 1
|
609
614
|
|
610
615
|
@property
|
611
616
|
def outage_seed(self):
|
@@ -626,3 +631,19 @@ class ScheduledLossesMixin:
|
|
626
631
|
pass
|
627
632
|
|
628
633
|
return self.__base_seed
|
634
|
+
|
635
|
+
def _fix_pysam_bug(self, hourly_mult):
|
636
|
+
"""Fix PySAM bug that squares HAF user input"""
|
637
|
+
if getattr(self, "MODULE", "").casefold() != "windpower":
|
638
|
+
return hourly_mult
|
639
|
+
|
640
|
+
bugged_pysam_version = (PySAM.__version__.startswith("5")
|
641
|
+
or PySAM.__version__.startswith("6"))
|
642
|
+
if not bugged_pysam_version:
|
643
|
+
return hourly_mult
|
644
|
+
|
645
|
+
# Bug in PySAM windpower module that applies HAF twice (i.e.
|
646
|
+
# squares the input values), so we sqrt the desired loss value
|
647
|
+
return np.sqrt(hourly_mult)
|
648
|
+
|
649
|
+
|
reV/supply_curve/aggregation.py
CHANGED
@@ -438,6 +438,53 @@ class BaseAggregation(ABC):
|
|
438
438
|
|
439
439
|
return gid_inclusions
|
440
440
|
|
441
|
+
@staticmethod
|
442
|
+
def _get_gid_zones(excl_fpath, zones_dset, gid, slice_lookup):
|
443
|
+
"""
|
444
|
+
Get zones 2D array for desired gid.
|
445
|
+
|
446
|
+
Parameters
|
447
|
+
----------
|
448
|
+
excl_fpath : str | None, optional
|
449
|
+
Filepath to HDF5 file containing `zones_dset`. If not specified,
|
450
|
+
output of function will be an array containing all values equal to
|
451
|
+
1.
|
452
|
+
zones_dset : str | None, optional
|
453
|
+
Dataset name in the `excl_fpath` file containing the zones to be
|
454
|
+
loaded. If not specified, output of function will be an array
|
455
|
+
containing all values equal to 1.
|
456
|
+
gid : int
|
457
|
+
sc_point_gid value, used to extract the applicable subset of zones.
|
458
|
+
slice_lookup : dict
|
459
|
+
Mapping of sc_point_gids to exclusion/inclusion row and column
|
460
|
+
slices
|
461
|
+
|
462
|
+
Returns
|
463
|
+
-------
|
464
|
+
zones : ndarray | None
|
465
|
+
2D array of zones for desired gid.
|
466
|
+
"""
|
467
|
+
|
468
|
+
row_slice, col_slice = slice_lookup[gid]
|
469
|
+
if excl_fpath is not None and zones_dset is not None:
|
470
|
+
with ExclusionLayers(excl_fpath) as fh:
|
471
|
+
if zones_dset not in fh:
|
472
|
+
msg = (
|
473
|
+
f"Could not find zones_dset {zones_dset} in "
|
474
|
+
f"excl_fpath {excl_fpath}."
|
475
|
+
)
|
476
|
+
logger.error(msg)
|
477
|
+
raise FileInputError(msg)
|
478
|
+
zones = fh[zones_dset, row_slice, col_slice]
|
479
|
+
else:
|
480
|
+
shape = (
|
481
|
+
row_slice.stop - row_slice.start,
|
482
|
+
col_slice.stop - col_slice.start
|
483
|
+
)
|
484
|
+
zones = np.ones(shape, dtype="uint8")
|
485
|
+
|
486
|
+
return zones
|
487
|
+
|
441
488
|
@staticmethod
|
442
489
|
def _parse_gen_index(gen_fpath):
|
443
490
|
"""Parse gen outputs for an array of generation gids corresponding to
|