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.
@@ -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._aep_after_scaled_wake_losses()
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
 
@@ -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
  }
@@ -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, troughphysicalheat, lineardirectsteam)
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, troughphysicalheat, lineardirectsteam)
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 | pd.DataFrame | slice | list
536
- Slice specifying project points, string pointing to a project
537
- points csv, or a dataframe containing the effective csv contents.
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 | reV.config.curtailment.Curtailment
691
- None if no curtailment, reV curtailment config object if
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 isinstance(curtailment_input, (str, dict)):
695
- # pointer to config file or explicit input namespace,
696
- # instantiate curtailment config object
697
- curtailment = Curtailment(curtailment_input)
698
-
699
- elif isinstance(curtailment_input, (Curtailment, type(None))):
700
- # pre-initialized curtailment object or no curtailment (None)
701
- curtailment = curtailment_input
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, troughphysicalheat, lineardirectsteam)
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, troughphysicalheat, lineardirectsteam)
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, troughphysicalheat, lineardirectsteam, econ)
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, troughphysicalheat, lineardirectsteam)
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, troughphysicalheat, lineardirectsteam)
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, troughphysicalheat, lineardirectsteam)
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
 
@@ -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``: Key in the `sam_files` input dictionary
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`` (or left out completely) if you specify only
182
- a single SAM configuration file as the `sam_files`
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
- for a single year are spread out over multiple files). In
242
- all cases, the resource data must be readable by
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. Alternatively,
258
- this input can be a list of explicit files to process. In
259
- this case, the length of the list must match the length of
260
- the `analysis_years` input exactly, and the path are
261
- assumed to align with the `analysis_years` (i.e. the first
262
- path corresponds to the first analysis year, the second
263
- path corresponds to the second analysis year, and so on).
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
- Inputs for curtailment parameters, which can be:
340
-
341
- - Explicit namespace of curtailment variables (dict)
342
- - Pointer to curtailment config file with path (str)
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. By default, ``None``.
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, troughphysicalheat, lineardirectsteam)
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,
@@ -27,6 +27,13 @@
27
27
  "type": "array",
28
28
  "units": "kW"
29
29
  },
30
+ "gen_heat": {
31
+ "chunks": null,
32
+ "dtype": "float32",
33
+ "scale_factor": 1,
34
+ "type": "array",
35
+ "units": "kW"
36
+ },
30
37
  "m_dot_field": {
31
38
  "chunks": null,
32
39
  "dtype": "float32",
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 ``hourly`` key to signify which hourly
527
- capacity factors should be adjusted with outage losses. If no
528
- outage info is specified in ``sam_sys_inputs``, no scheduled
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 ``hourly`` key to
546
- signify which hourly capacity factors should be adjusted with
547
- outage losses. If the user specifies other hourly adjustment
548
- factors via the ``hourly`` key, the effect is combined. For
549
- example, if the user inputs a 33% hourly adjustment factor and
550
- reV schedules an outage for 70% of the farm down for the same
551
- hour, then the resulting adjustment factor is
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['hourly'])))
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('hourly', [0] * 8760)
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['hourly'] = (1 - final_hourly_mult) * 100
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
+
@@ -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