NREL-reV 0.8.6__py3-none-any.whl → 0.8.9__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 (38) hide show
  1. {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/METADATA +12 -10
  2. {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/RECORD +38 -38
  3. {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/WHEEL +1 -1
  4. reV/SAM/SAM.py +182 -133
  5. reV/SAM/econ.py +18 -14
  6. reV/SAM/generation.py +640 -414
  7. reV/SAM/windbos.py +93 -79
  8. reV/bespoke/bespoke.py +690 -445
  9. reV/bespoke/place_turbines.py +6 -6
  10. reV/config/project_points.py +220 -140
  11. reV/econ/econ.py +165 -113
  12. reV/econ/economies_of_scale.py +57 -34
  13. reV/generation/base.py +310 -183
  14. reV/generation/generation.py +309 -191
  15. reV/handlers/exclusions.py +16 -15
  16. reV/handlers/multi_year.py +12 -9
  17. reV/handlers/outputs.py +6 -5
  18. reV/hybrids/hybrid_methods.py +28 -30
  19. reV/hybrids/hybrids.py +304 -188
  20. reV/nrwal/nrwal.py +262 -168
  21. reV/qa_qc/cli_qa_qc.py +14 -10
  22. reV/qa_qc/qa_qc.py +217 -119
  23. reV/qa_qc/summary.py +228 -146
  24. reV/rep_profiles/rep_profiles.py +349 -230
  25. reV/supply_curve/aggregation.py +349 -188
  26. reV/supply_curve/competitive_wind_farms.py +90 -48
  27. reV/supply_curve/exclusions.py +138 -85
  28. reV/supply_curve/extent.py +75 -50
  29. reV/supply_curve/points.py +620 -295
  30. reV/supply_curve/sc_aggregation.py +396 -226
  31. reV/supply_curve/supply_curve.py +505 -308
  32. reV/supply_curve/tech_mapping.py +144 -82
  33. reV/utilities/__init__.py +199 -16
  34. reV/utilities/pytest_utils.py +8 -4
  35. reV/version.py +1 -1
  36. {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/LICENSE +0 -0
  37. {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/entry_points.txt +0 -0
  38. {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/top_level.txt +0 -0
reV/SAM/generation.py CHANGED
@@ -4,10 +4,10 @@
4
4
  Wraps the NREL-PySAM pvwattsv5, windpower, and tcsmolensalt modules with
5
5
  additional reV features.
6
6
  """
7
+
7
8
  import copy
8
- import os
9
9
  import logging
10
-
10
+ import os
11
11
  from abc import ABC, abstractmethod
12
12
  from tempfile import TemporaryDirectory
13
13
  from warnings import warn
@@ -26,22 +26,28 @@ import PySAM.TcsmoltenSalt as PySamCSP
26
26
  import PySAM.TroughPhysicalProcessHeat as PySamTpph
27
27
  import PySAM.Windpower as PySamWindPower
28
28
 
29
- from reV.losses import ScheduledLossesMixin, PowerCurveLossesMixin
30
- from reV.SAM.defaults import (DefaultGeothermal,
31
- DefaultPvWattsv5,
32
- DefaultPvWattsv8,
33
- DefaultPvSamv1,
34
- DefaultWindPower,
35
- DefaultTcsMoltenSalt,
36
- DefaultSwh,
37
- DefaultTroughPhysicalProcessHeat,
38
- DefaultLinearFresnelDsgIph,
39
- DefaultMhkWave)
29
+ from reV.losses import PowerCurveLossesMixin, ScheduledLossesMixin
30
+ from reV.SAM.defaults import (
31
+ DefaultGeothermal,
32
+ DefaultLinearFresnelDsgIph,
33
+ DefaultMhkWave,
34
+ DefaultPvSamv1,
35
+ DefaultPvWattsv5,
36
+ DefaultPvWattsv8,
37
+ DefaultSwh,
38
+ DefaultTcsMoltenSalt,
39
+ DefaultTroughPhysicalProcessHeat,
40
+ DefaultWindPower,
41
+ )
40
42
  from reV.SAM.econ import LCOE, SingleOwner
41
43
  from reV.SAM.SAM import RevPySam
44
+ from reV.utilities import ResourceMetaField, SupplyCurveField
42
45
  from reV.utilities.curtailment import curtail
43
- from reV.utilities.exceptions import (SAMInputWarning, SAMExecutionError,
44
- InputError)
46
+ from reV.utilities.exceptions import (
47
+ InputError,
48
+ SAMExecutionError,
49
+ SAMInputWarning,
50
+ )
45
51
 
46
52
  logger = logging.getLogger(__name__)
47
53
 
@@ -49,8 +55,15 @@ logger = logging.getLogger(__name__)
49
55
  class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
50
56
  """Base class for SAM generation simulations."""
51
57
 
52
- def __init__(self, resource, meta, sam_sys_inputs, site_sys_inputs=None,
53
- output_request=None, drop_leap=False):
58
+ def __init__(
59
+ self,
60
+ resource,
61
+ meta,
62
+ sam_sys_inputs,
63
+ site_sys_inputs=None,
64
+ output_request=None,
65
+ drop_leap=False,
66
+ ):
54
67
  """Initialize a SAM generation object.
55
68
 
56
69
  Parameters
@@ -88,11 +101,15 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
88
101
 
89
102
  # don't pass resource to base class,
90
103
  # set in concrete generation classes instead
91
- super().__init__(meta, sam_sys_inputs, output_request,
92
- site_sys_inputs=site_sys_inputs)
104
+ super().__init__(
105
+ meta,
106
+ sam_sys_inputs,
107
+ output_request,
108
+ site_sys_inputs=site_sys_inputs,
109
+ )
93
110
 
94
111
  # Set the site number using resource
95
- if hasattr(resource, 'name'):
112
+ if hasattr(resource, "name"):
96
113
  self._site = resource.name
97
114
  else:
98
115
  self._site = None
@@ -164,19 +181,24 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
164
181
  out_req_nomeans = copy.deepcopy(output_request)
165
182
  res_mean = None
166
183
  idx = resource.sites.index(res_gid)
167
- irrad_means = ('dni_mean', 'dhi_mean', 'ghi_mean',
168
- 'clearsky_dni_mean', 'clearsky_dhi_mean',
169
- 'clearsky_ghi_mean')
170
-
171
- if 'ws_mean' in out_req_nomeans:
172
- out_req_nomeans.remove('ws_mean')
184
+ irrad_means = (
185
+ "dni_mean",
186
+ "dhi_mean",
187
+ "ghi_mean",
188
+ "clearsky_dni_mean",
189
+ "clearsky_dhi_mean",
190
+ "clearsky_ghi_mean",
191
+ )
192
+
193
+ if "ws_mean" in out_req_nomeans:
194
+ out_req_nomeans.remove("ws_mean")
173
195
  res_mean = {}
174
- res_mean['ws_mean'] = resource['mean_windspeed', idx]
196
+ res_mean["ws_mean"] = resource["mean_windspeed", idx]
175
197
 
176
198
  else:
177
199
  for var in resource.var_list:
178
- label_1 = '{}_mean'.format(var)
179
- label_2 = 'mean_{}'.format(var)
200
+ label_1 = "{}_mean".format(var)
201
+ label_2 = "mean_{}".format(var)
180
202
  if label_1 in out_req_nomeans:
181
203
  out_req_nomeans.remove(label_1)
182
204
  if res_mean is None:
@@ -205,8 +227,9 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
205
227
  if pd.isna(resource).any().any():
206
228
  bad_vars = pd.isna(resource).any(axis=0)
207
229
  bad_vars = resource.columns[bad_vars].values.tolist()
208
- msg = ('Found NaN values for site {} in variables {}'
209
- .format(self.site, bad_vars))
230
+ msg = "Found NaN values for site {} in variables {}".format(
231
+ self.site, bad_vars
232
+ )
210
233
  logger.error(msg)
211
234
  raise InputError(msg)
212
235
 
@@ -234,37 +257,50 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
234
257
  Returns
235
258
  -------
236
259
  meta : pd.DataFrame | pd.Series
237
- Datafram or series for a single site. Will include "timezone"
260
+ Dataframe or series for a single site. Will include "timezone"
238
261
  and "elevation" from the sam and site system inputs if found.
239
262
  """
240
263
 
241
264
  if meta is not None:
265
+ axis = 0 if isinstance(meta, pd.core.series.Series) else 1
266
+ meta = meta.rename(
267
+ SupplyCurveField.map_to(ResourceMetaField), axis=axis
268
+ )
242
269
  if sam_sys_inputs is not None:
243
- if 'elevation' in sam_sys_inputs:
244
- meta['elevation'] = sam_sys_inputs['elevation']
245
- if 'timezone' in sam_sys_inputs:
246
- meta['timezone'] = int(sam_sys_inputs['timezone'])
270
+ if ResourceMetaField.ELEVATION in sam_sys_inputs:
271
+ meta[ResourceMetaField.ELEVATION] = sam_sys_inputs[
272
+ ResourceMetaField.ELEVATION
273
+ ]
274
+ if ResourceMetaField.TIMEZONE in sam_sys_inputs:
275
+ meta[ResourceMetaField.TIMEZONE] = int(
276
+ sam_sys_inputs[ResourceMetaField.TIMEZONE]
277
+ )
247
278
 
248
279
  # site-specific inputs take priority over generic system inputs
249
280
  if site_sys_inputs is not None:
250
- if 'elevation' in site_sys_inputs:
251
- meta['elevation'] = site_sys_inputs['elevation']
252
- if 'timezone' in site_sys_inputs:
253
- meta['timezone'] = int(site_sys_inputs['timezone'])
254
-
255
- if 'timezone' not in meta:
256
- msg = ('Need timezone input to run SAM gen. Not found in '
257
- 'resource meta or technology json input config.')
281
+ if ResourceMetaField.ELEVATION in site_sys_inputs:
282
+ meta[ResourceMetaField.ELEVATION] = site_sys_inputs[
283
+ ResourceMetaField.ELEVATION
284
+ ]
285
+ if ResourceMetaField.TIMEZONE in site_sys_inputs:
286
+ meta[ResourceMetaField.TIMEZONE] = int(
287
+ site_sys_inputs[ResourceMetaField.TIMEZONE]
288
+ )
289
+
290
+ if ResourceMetaField.TIMEZONE not in meta:
291
+ msg = (
292
+ "Need timezone input to run SAM gen. Not found in "
293
+ "resource meta or technology json input config."
294
+ )
258
295
  raise SAMExecutionError(msg)
259
296
 
260
297
  return meta
261
298
 
262
299
  @property
263
300
  def has_timezone(self):
264
- """ Returns true if instance has a timezone set """
265
- if self._meta is not None:
266
- if 'timezone' in self.meta:
267
- return True
301
+ """Returns true if instance has a timezone set"""
302
+ if self._meta is not None and ResourceMetaField.TIMEZONE in self.meta:
303
+ return True
268
304
 
269
305
  return False
270
306
 
@@ -276,7 +312,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
276
312
  output : float
277
313
  Mean capacity factor (fractional).
278
314
  """
279
- return self['capacity_factor'] / 100
315
+ return self["capacity_factor"] / 100
280
316
 
281
317
  def cf_profile(self):
282
318
  """Get hourly capacity factor (frac) profile in local timezone.
@@ -288,7 +324,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
288
324
  1D numpy array of capacity factor profile.
289
325
  Datatype is float32 and array length is 8760*time_interval.
290
326
  """
291
- return self.gen_profile() / self.sam_sys_inputs['system_capacity']
327
+ return self.gen_profile() / self.sam_sys_inputs["system_capacity"]
292
328
 
293
329
  def annual_energy(self):
294
330
  """Get annual energy generation value in kWh from SAM.
@@ -298,7 +334,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
298
334
  output : float
299
335
  Annual energy generation (kWh).
300
336
  """
301
- return self['annual_energy']
337
+ return self["annual_energy"]
302
338
 
303
339
  def energy_yield(self):
304
340
  """Get annual energy yield value in kwh/kw from SAM.
@@ -308,7 +344,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
308
344
  output : float
309
345
  Annual energy yield (kwh/kw).
310
346
  """
311
- return self['kwh_per_kw']
347
+ return self["kwh_per_kw"]
312
348
 
313
349
  def gen_profile(self):
314
350
  """Get power generation profile (local timezone) in kW.
@@ -320,7 +356,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
320
356
  1D array of hourly power generation in kW.
321
357
  Datatype is float32 and array length is 8760*time_interval.
322
358
  """
323
- return np.array(self['gen'], dtype=np.float32)
359
+ return np.array(self["gen"], dtype=np.float32)
324
360
 
325
361
  def collect_outputs(self, output_lookup=None):
326
362
  """Collect SAM output_request, convert timeseries outputs to UTC, and
@@ -335,12 +371,13 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
335
371
  """
336
372
 
337
373
  if output_lookup is None:
338
- output_lookup = {'cf_mean': self.cf_mean,
339
- 'cf_profile': self.cf_profile,
340
- 'annual_energy': self.annual_energy,
341
- 'energy_yield': self.energy_yield,
342
- 'gen_profile': self.gen_profile,
343
- }
374
+ output_lookup = {
375
+ "cf_mean": self.cf_mean,
376
+ "cf_profile": self.cf_profile,
377
+ "annual_energy": self.annual_energy,
378
+ "energy_yield": self.energy_yield,
379
+ "gen_profile": self.gen_profile,
380
+ }
344
381
 
345
382
  super().collect_outputs(output_lookup=output_lookup)
346
383
 
@@ -349,19 +386,31 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
349
386
 
350
387
  lcoe_out_reqs = None
351
388
  so_out_reqs = None
352
- lcoe_vars = ('lcoe_fcr', 'fixed_charge_rate', 'capital_cost',
353
- 'fixed_operating_cost', 'variable_operating_cost')
354
- so_vars = ('ppa_price', 'lcoe_real', 'lcoe_nom',
355
- 'project_return_aftertax_npv', 'flip_actual_irr',
356
- 'gross_revenue')
357
- if 'lcoe_fcr' in self.output_request:
389
+ lcoe_vars = (
390
+ "lcoe_fcr",
391
+ "fixed_charge_rate",
392
+ "capital_cost",
393
+ "fixed_operating_cost",
394
+ "variable_operating_cost",
395
+ )
396
+ so_vars = (
397
+ "ppa_price",
398
+ "lcoe_real",
399
+ "lcoe_nom",
400
+ "project_return_aftertax_npv",
401
+ "flip_actual_irr",
402
+ "gross_revenue",
403
+ )
404
+ if "lcoe_fcr" in self.output_request:
358
405
  lcoe_out_reqs = [r for r in self.output_request if r in lcoe_vars]
359
- self.output_request = [r for r in self.output_request
360
- if r not in lcoe_out_reqs]
406
+ self.output_request = [
407
+ r for r in self.output_request if r not in lcoe_out_reqs
408
+ ]
361
409
  elif any(x in self.output_request for x in so_vars):
362
410
  so_out_reqs = [r for r in self.output_request if r in so_vars]
363
- self.output_request = [r for r in self.output_request
364
- if r not in so_out_reqs]
411
+ self.output_request = [
412
+ r for r in self.output_request if r not in so_out_reqs
413
+ ]
365
414
 
366
415
  # Execute the SAM generation compute module (pvwattsv7, windpower, etc)
367
416
  self.run()
@@ -369,7 +418,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
369
418
  # Execute a follow-on SAM econ compute module
370
419
  # (lcoe_fcr, singleowner, etc)
371
420
  if lcoe_out_reqs is not None:
372
- self.sam_sys_inputs['annual_energy'] = self.annual_energy()
421
+ self.sam_sys_inputs["annual_energy"] = self.annual_energy()
373
422
  lcoe = LCOE(self.sam_sys_inputs, output_request=lcoe_out_reqs)
374
423
  lcoe.assign_inputs()
375
424
  lcoe.execute()
@@ -377,7 +426,7 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
377
426
  self.outputs.update(lcoe.outputs)
378
427
 
379
428
  elif so_out_reqs is not None:
380
- self.sam_sys_inputs['gen'] = self.gen_profile()
429
+ self.sam_sys_inputs["gen"] = self.gen_profile()
381
430
  so = SingleOwner(self.sam_sys_inputs, output_request=so_out_reqs)
382
431
  so.assign_inputs()
383
432
  so.execute()
@@ -393,10 +442,18 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
393
442
  self.collect_outputs()
394
443
 
395
444
  @classmethod
396
- def reV_run(cls, points_control, res_file, site_df,
397
- lr_res_file=None, output_request=('cf_mean',),
398
- drop_leap=False, gid_map=None, nn_map=None,
399
- bias_correct=None):
445
+ def reV_run(
446
+ cls,
447
+ points_control,
448
+ res_file,
449
+ site_df,
450
+ lr_res_file=None,
451
+ output_request=("cf_mean",),
452
+ drop_leap=False,
453
+ gid_map=None,
454
+ nn_map=None,
455
+ bias_correct=None,
456
+ ):
400
457
  """Execute SAM generation based on a reV points control instance.
401
458
 
402
459
  Parameters
@@ -460,24 +517,26 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
460
517
  out = {}
461
518
 
462
519
  # Get the RevPySam resource object
463
- resources = RevPySam.get_sam_res(res_file,
464
- points_control.project_points,
465
- points_control.project_points.tech,
466
- output_request=output_request,
467
- gid_map=gid_map,
468
- lr_res_file=lr_res_file,
469
- nn_map=nn_map,
470
- bias_correct=bias_correct)
520
+ resources = RevPySam.get_sam_res(
521
+ res_file,
522
+ points_control.project_points,
523
+ points_control.project_points.tech,
524
+ output_request=output_request,
525
+ gid_map=gid_map,
526
+ lr_res_file=lr_res_file,
527
+ nn_map=nn_map,
528
+ bias_correct=bias_correct,
529
+ )
471
530
 
472
531
  # run resource through curtailment filter if applicable
473
532
  curtailment = points_control.project_points.curtailment
474
533
  if curtailment is not None:
475
- resources = curtail(resources, curtailment,
476
- random_seed=curtailment.random_seed)
534
+ resources = curtail(
535
+ resources, curtailment, random_seed=curtailment.random_seed
536
+ )
477
537
 
478
538
  # iterate through project_points gen_gid values
479
539
  for gen_gid in points_control.project_points.sites:
480
-
481
540
  # Lookup the resource gid if there's a mapping and get the resource
482
541
  # data from the SAMResource object using the res_gid.
483
542
  res_gid = gen_gid if gid_map is None else gid_map[gen_gid]
@@ -490,15 +549,21 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
490
549
  _, inputs = points_control.project_points[gen_gid]
491
550
 
492
551
  # get resource data pass-throughs and resource means
493
- res_outs, out_req_cleaned = cls._get_res(site_res_df,
494
- output_request)
495
- res_mean, out_req_cleaned = cls._get_res_mean(resources, res_gid,
496
- out_req_cleaned)
552
+ res_outs, out_req_cleaned = cls._get_res(
553
+ site_res_df, output_request
554
+ )
555
+ res_mean, out_req_cleaned = cls._get_res_mean(
556
+ resources, res_gid, out_req_cleaned
557
+ )
497
558
 
498
559
  # iterate through requested sites.
499
- sim = cls(resource=site_res_df, meta=site_meta,
500
- sam_sys_inputs=inputs, output_request=out_req_cleaned,
501
- site_sys_inputs=dict(site_df.loc[gen_gid, :]))
560
+ sim = cls(
561
+ resource=site_res_df,
562
+ meta=site_meta,
563
+ sam_sys_inputs=inputs,
564
+ output_request=out_req_cleaned,
565
+ site_sys_inputs=dict(site_df.loc[gen_gid, :]),
566
+ )
502
567
  sim.run_gen_and_econ()
503
568
 
504
569
  # collect outputs to dictout
@@ -514,10 +579,20 @@ class AbstractSamGeneration(RevPySam, ScheduledLossesMixin, ABC):
514
579
 
515
580
 
516
581
  class AbstractSamGenerationFromWeatherFile(AbstractSamGeneration, ABC):
517
- """Base class for running sam generation with a weather file on disk. """
518
- WF_META_DROP_COLS = {'elevation', 'timezone', 'country', 'state', 'county',
519
- 'urban', 'population', 'landcover', 'latitude',
520
- 'longitude'}
582
+ """Base class for running sam generation with a weather file on disk."""
583
+
584
+ WF_META_DROP_COLS = {
585
+ ResourceMetaField.LATITUDE,
586
+ ResourceMetaField.LONGITUDE,
587
+ ResourceMetaField.ELEVATION,
588
+ ResourceMetaField.TIMEZONE,
589
+ ResourceMetaField.COUNTRY,
590
+ ResourceMetaField.STATE,
591
+ ResourceMetaField.COUNTY,
592
+ "urban",
593
+ "population",
594
+ "landcover",
595
+ }
521
596
 
522
597
  @property
523
598
  @abstractmethod
@@ -582,59 +657,62 @@ class AbstractSamGenerationFromWeatherFile(AbstractSamGeneration, ABC):
582
657
  """
583
658
  # pylint: disable=attribute-defined-outside-init,consider-using-with
584
659
  self._temp_dir = TemporaryDirectory()
585
- fname = os.path.join(self._temp_dir.name, 'weather.csv')
586
- logger.debug('Creating PySAM weather data file: {}'.format(fname))
660
+ fname = os.path.join(self._temp_dir.name, "weather.csv")
661
+ logger.debug("Creating PySAM weather data file: {}".format(fname))
587
662
 
588
663
  # ------- Process metadata
589
664
  m = pd.DataFrame(meta).T
590
- timezone = m['timezone']
591
- m['Source'] = 'NSRDB'
592
- m['Location ID'] = meta.name
593
- m['City'] = '-'
594
- m['State'] = m['state'].apply(lambda x: '-' if x == 'None' else x)
595
- m['Country'] = m['country'].apply(lambda x: '-' if x == 'None' else x)
596
- m['Latitude'] = m['latitude']
597
- m['Longitude'] = m['longitude']
598
- m['Time Zone'] = timezone
599
- m['Elevation'] = m['elevation']
600
- m['Local Time Zone'] = timezone
601
- m['Dew Point Units'] = 'c'
602
- m['DHI Units'] = 'w/m2'
603
- m['DNI Units'] = 'w/m2'
604
- m['Temperature Units'] = 'c'
605
- m['Pressure Units'] = 'mbar'
606
- m['Wind Speed'] = 'm/s'
665
+ timezone = m[ResourceMetaField.TIMEZONE]
666
+ m["Source"] = "NSRDB"
667
+ m["Location ID"] = meta.name
668
+ m["City"] = "-"
669
+ m["State"] = m["state"].apply(lambda x: "-" if x == "None" else x)
670
+ m["Country"] = m["country"].apply(lambda x: "-" if x == "None" else x)
671
+ m["Latitude"] = m[ResourceMetaField.LATITUDE]
672
+ m["Longitude"] = m[ResourceMetaField.LONGITUDE]
673
+ m["Time Zone"] = timezone
674
+ m["Elevation"] = m[ResourceMetaField.ELEVATION]
675
+ m["Local Time Zone"] = timezone
676
+ m["Dew Point Units"] = "c"
677
+ m["DHI Units"] = "w/m2"
678
+ m["DNI Units"] = "w/m2"
679
+ m["Temperature Units"] = "c"
680
+ m["Pressure Units"] = "mbar"
681
+ m["Wind Speed"] = "m/s"
607
682
  keep_cols = [c for c in m.columns if c not in self.WF_META_DROP_COLS]
608
- m[keep_cols].to_csv(fname, index=False, mode='w')
683
+ m[keep_cols].to_csv(fname, index=False, mode="w")
609
684
 
610
685
  # --------- Process data
611
- var_map = {'dni': 'DNI',
612
- 'dhi': 'DHI',
613
- 'wind_speed': 'Wind Speed',
614
- 'air_temperature': 'Temperature',
615
- 'dew_point': 'Dew Point',
616
- 'surface_pressure': 'Pressure',
617
- }
618
- resource = resource.rename(mapper=var_map, axis='columns')
686
+ var_map = {
687
+ "dni": "DNI",
688
+ "dhi": "DHI",
689
+ "wind_speed": "Wind Speed",
690
+ "air_temperature": "Temperature",
691
+ "dew_point": "Dew Point",
692
+ "surface_pressure": "Pressure",
693
+ }
694
+ resource = resource.rename(mapper=var_map, axis="columns")
619
695
 
620
696
  time_index = resource.index
621
697
  # Adjust from UTC to local time
622
- local = np.roll(resource.values, int(timezone * self.time_interval),
623
- axis=0)
624
- resource = pd.DataFrame(local, columns=resource.columns,
625
- index=time_index)
698
+ local = np.roll(
699
+ resource.values, int(timezone * self.time_interval), axis=0
700
+ )
701
+ resource = pd.DataFrame(
702
+ local, columns=resource.columns, index=time_index
703
+ )
626
704
  mask = (time_index.month == 2) & (time_index.day == 29)
627
705
  time_index = time_index[~mask]
628
706
 
629
707
  df = pd.DataFrame(index=time_index)
630
- df['Year'] = time_index.year
631
- df['Month'] = time_index.month
632
- df['Day'] = time_index.day
633
- df['Hour'] = time_index.hour
634
- df['Minute'] = time_index.minute
708
+ df["Year"] = time_index.year
709
+ df["Month"] = time_index.month
710
+ df["Day"] = time_index.day
711
+ df["Hour"] = time_index.hour
712
+ df["Minute"] = time_index.minute
635
713
  df = df.join(resource.loc[~mask])
636
714
 
637
- df.to_csv(fname, index=False, mode='a')
715
+ df.to_csv(fname, index=False, mode="a")
638
716
 
639
717
  return fname
640
718
 
@@ -703,83 +781,96 @@ class AbstractSamSolar(AbstractSamGeneration, ABC):
703
781
  self.time_interval = self.get_time_interval(resource.index.values)
704
782
 
705
783
  # map resource data names to SAM required data names
706
- var_map = {'dni': 'dn',
707
- 'dhi': 'df',
708
- 'ghi': 'gh',
709
- 'clearskydni': 'dn',
710
- 'clearskydhi': 'df',
711
- 'clearskyghi': 'gh',
712
- 'windspeed': 'wspd',
713
- 'airtemperature': 'tdry',
714
- 'temperature': 'tdry',
715
- 'temp': 'tdry',
716
- 'dewpoint': 'tdew',
717
- 'surfacepressure': 'pres',
718
- 'pressure': 'pres',
719
- 'surfacealbedo': 'albedo',
720
- }
721
- lower_case = {k: k.lower().replace(' ', '').replace('_', '')
722
- for k in resource.columns}
723
- irrad_vars = ['dn', 'df', 'gh']
724
-
725
- resource = resource.rename(mapper=lower_case, axis='columns')
726
- resource = resource.rename(mapper=var_map, axis='columns')
784
+ var_map = {
785
+ "dni": "dn",
786
+ "dhi": "df",
787
+ "ghi": "gh",
788
+ "clearskydni": "dn",
789
+ "clearskydhi": "df",
790
+ "clearskyghi": "gh",
791
+ "windspeed": "wspd",
792
+ "airtemperature": "tdry",
793
+ "temperature": "tdry",
794
+ "temp": "tdry",
795
+ "dewpoint": "tdew",
796
+ "surfacepressure": "pres",
797
+ "pressure": "pres",
798
+ "surfacealbedo": "albedo",
799
+ }
800
+ lower_case = {
801
+ k: k.lower().replace(" ", "").replace("_", "")
802
+ for k in resource.columns
803
+ }
804
+ irrad_vars = ["dn", "df", "gh"]
805
+
806
+ resource = resource.rename(mapper=lower_case, axis="columns")
807
+ resource = resource.rename(mapper=var_map, axis="columns")
727
808
  time_index = resource.index
728
- resource = {k: np.array(v) for (k, v) in
729
- resource.to_dict(orient='list').items()}
809
+ resource = {
810
+ k: np.array(v)
811
+ for (k, v) in resource.to_dict(orient="list").items()
812
+ }
730
813
 
731
814
  # set resource variables
732
815
  for var, arr in resource.items():
733
- if var != 'time_index':
734
-
816
+ if var != "time_index":
735
817
  # ensure that resource array length is multiple of 8760
736
818
  arr = self.ensure_res_len(arr, time_index)
737
- n_roll = int(self._meta['timezone'] * self.time_interval)
819
+ n_roll = int(
820
+ self._meta[ResourceMetaField.TIMEZONE] * self.time_interval
821
+ )
738
822
  arr = np.roll(arr, n_roll)
739
823
 
740
- if var in irrad_vars:
741
- if np.min(arr) < 0:
742
- warn('Solar irradiance variable "{}" has a minimum '
743
- 'value of {}. Truncating to zero.'
744
- .format(var, np.min(arr)), SAMInputWarning)
745
- arr = np.where(arr < 0, 0, arr)
824
+ if var in irrad_vars and np.min(arr) < 0:
825
+ warn(
826
+ 'Solar irradiance variable "{}" has a minimum '
827
+ "value of {}. Truncating to zero.".format(
828
+ var, np.min(arr)
829
+ ),
830
+ SAMInputWarning,
831
+ )
832
+ arr = np.where(arr < 0, 0, arr)
746
833
 
747
834
  resource[var] = arr.tolist()
748
835
 
749
- resource['lat'] = meta['latitude']
750
- resource['lon'] = meta['longitude']
751
- resource['tz'] = meta['timezone']
836
+ resource["lat"] = meta[ResourceMetaField.LATITUDE]
837
+ resource["lon"] = meta[ResourceMetaField.LONGITUDE]
838
+ resource["tz"] = meta[ResourceMetaField.TIMEZONE]
752
839
 
753
- if 'elevation' in meta:
754
- resource['elev'] = meta['elevation']
755
- else:
756
- resource['elev'] = 0.0
840
+ resource["elev"] = meta.get(ResourceMetaField.ELEVATION, 0.0)
757
841
 
758
842
  time_index = self.ensure_res_len(time_index, time_index)
759
- resource['minute'] = time_index.minute
760
- resource['hour'] = time_index.hour
761
- resource['month'] = time_index.month
762
- resource['year'] = time_index.year
763
- resource['day'] = time_index.day
843
+ resource["minute"] = time_index.minute
844
+ resource["hour"] = time_index.hour
845
+ resource["month"] = time_index.month
846
+ resource["year"] = time_index.year
847
+ resource["day"] = time_index.day
764
848
 
765
- if 'albedo' in resource:
766
- self['albedo'] = self.agg_albedo(
767
- time_index, resource.pop('albedo'))
849
+ if "albedo" in resource:
850
+ self["albedo"] = self.agg_albedo(
851
+ time_index, resource.pop("albedo")
852
+ )
768
853
 
769
- self['solar_resource_data'] = resource
854
+ self["solar_resource_data"] = resource
770
855
 
771
856
 
772
857
  class AbstractSamPv(AbstractSamSolar, ABC):
773
- """Photovoltaic (PV) generation with either pvwatts of detailed pv.
774
- """
858
+ """Photovoltaic (PV) generation with either pvwatts of detailed pv."""
775
859
 
776
860
  # set these class attrs in concrete subclasses
777
861
  MODULE = None
778
862
  PYSAM = None
779
863
 
780
864
  # pylint: disable=line-too-long
781
- def __init__(self, resource, meta, sam_sys_inputs, site_sys_inputs=None,
782
- output_request=None, drop_leap=False):
865
+ def __init__(
866
+ self,
867
+ resource,
868
+ meta,
869
+ sam_sys_inputs,
870
+ site_sys_inputs=None,
871
+ output_request=None,
872
+ drop_leap=False,
873
+ ):
783
874
  """Initialize a SAM solar object.
784
875
 
785
876
  See the PySAM :py:class:`~PySAM.Pvwattsv8.Pvwattsv8` (or older
@@ -878,10 +969,14 @@ class AbstractSamPv(AbstractSamSolar, ABC):
878
969
  meta = self._parse_meta(meta)
879
970
  sam_sys_inputs = self.set_latitude_tilt_az(sam_sys_inputs, meta)
880
971
 
881
- super().__init__(resource, meta, sam_sys_inputs,
882
- site_sys_inputs=site_sys_inputs,
883
- output_request=output_request,
884
- drop_leap=drop_leap)
972
+ super().__init__(
973
+ resource,
974
+ meta,
975
+ sam_sys_inputs,
976
+ site_sys_inputs=site_sys_inputs,
977
+ output_request=output_request,
978
+ drop_leap=drop_leap,
979
+ )
885
980
 
886
981
  def set_resource_data(self, resource, meta):
887
982
  """Set NSRDB resource data arrays.
@@ -905,15 +1000,19 @@ class AbstractSamPv(AbstractSamSolar, ABC):
905
1000
  respectively.
906
1001
 
907
1002
  """
908
- bad_location_input = ((meta['latitude'] < -90)
909
- | (meta['latitude'] > 90)
910
- | (meta['longitude'] < -180)
911
- | (meta['longitude'] > 180))
1003
+ bad_location_input = (
1004
+ (meta[ResourceMetaField.LATITUDE] < -90)
1005
+ | (meta[ResourceMetaField.LATITUDE] > 90)
1006
+ | (meta[ResourceMetaField.LONGITUDE] < -180)
1007
+ | (meta[ResourceMetaField.LONGITUDE] > 180)
1008
+ )
912
1009
  if bad_location_input.any():
913
- raise ValueError("Detected latitude/longitude values outside of "
914
- "the range -90 to 90 and -180 to 180, "
915
- "respectively. Please ensure input resource data"
916
- "locations conform to these ranges. ")
1010
+ raise ValueError(
1011
+ "Detected latitude/longitude values outside of "
1012
+ "the range -90 to 90 and -180 to 180, "
1013
+ "respectively. Please ensure input resource data"
1014
+ "locations conform to these ranges. "
1015
+ )
917
1016
  return super().set_resource_data(resource, meta)
918
1017
 
919
1018
  @staticmethod
@@ -934,36 +1033,43 @@ class AbstractSamPv(AbstractSamSolar, ABC):
934
1033
  sam_sys_inputs : dict
935
1034
  Site-agnostic SAM system model inputs arguments.
936
1035
  If for a pv simulation the "tilt" parameter was originally not
937
- present or set to 'lat' or 'latitude', the tilt will be set to
938
- the absolute value of the latitude found in meta and the azimuth
939
- will be 180 if lat>0, 0 if lat<0.
1036
+ present or set to 'lat' or MetaKeyName.LATITUDE, the tilt will be
1037
+ set to the absolute value of the latitude found in meta and the
1038
+ azimuth will be 180 if lat>0, 0 if lat<0.
940
1039
  """
941
1040
 
942
1041
  set_tilt = False
943
1042
  if sam_sys_inputs is not None and meta is not None:
944
- if 'tilt' not in sam_sys_inputs:
945
- warn('No tilt specified, setting at latitude.',
946
- SAMInputWarning)
1043
+ if "tilt" not in sam_sys_inputs:
1044
+ warn(
1045
+ "No tilt specified, setting at latitude.", SAMInputWarning
1046
+ )
1047
+ set_tilt = True
1048
+ elif (
1049
+ sam_sys_inputs["tilt"] == "lat"
1050
+ or sam_sys_inputs["tilt"] == ResourceMetaField.LATITUDE
1051
+ ) or (
1052
+ sam_sys_inputs["tilt"] == "lat"
1053
+ or sam_sys_inputs["tilt"] == ResourceMetaField.LATITUDE
1054
+ ):
947
1055
  set_tilt = True
948
- else:
949
- if (sam_sys_inputs['tilt'] == 'lat'
950
- or sam_sys_inputs['tilt'] == 'latitude'):
951
- set_tilt = True
952
1056
 
953
1057
  if set_tilt:
954
1058
  # set tilt to abs(latitude)
955
- sam_sys_inputs['tilt'] = np.abs(meta['latitude'])
956
- if meta['latitude'] > 0:
1059
+ sam_sys_inputs["tilt"] = np.abs(meta[ResourceMetaField.LATITUDE])
1060
+ if meta[ResourceMetaField.LATITUDE] > 0:
957
1061
  # above the equator, az = 180
958
- sam_sys_inputs['azimuth'] = 180
1062
+ sam_sys_inputs["azimuth"] = 180
959
1063
  else:
960
1064
  # below the equator, az = 0
961
- sam_sys_inputs['azimuth'] = 0
962
-
963
- logger.debug('Tilt specified at "latitude", setting tilt to: {}, '
964
- 'azimuth to: {}'
965
- .format(sam_sys_inputs['tilt'],
966
- sam_sys_inputs['azimuth']))
1065
+ sam_sys_inputs["azimuth"] = 0
1066
+
1067
+ logger.debug(
1068
+ 'Tilt specified at "latitude", setting tilt to: {}, '
1069
+ "azimuth to: {}".format(
1070
+ sam_sys_inputs["tilt"], sam_sys_inputs["azimuth"]
1071
+ )
1072
+ )
967
1073
  return sam_sys_inputs
968
1074
 
969
1075
  def system_capacity_ac(self):
@@ -976,8 +1082,10 @@ class AbstractSamPv(AbstractSamSolar, ABC):
976
1082
  cf_profile : float
977
1083
  AC nameplate = DC nameplate / ILR
978
1084
  """
979
- return (self.sam_sys_inputs['system_capacity']
980
- / self.sam_sys_inputs['dc_ac_ratio'])
1085
+ return (
1086
+ self.sam_sys_inputs["system_capacity"]
1087
+ / self.sam_sys_inputs["dc_ac_ratio"]
1088
+ )
981
1089
 
982
1090
  def cf_mean(self):
983
1091
  """Get mean capacity factor (fractional) from SAM.
@@ -990,7 +1098,7 @@ class AbstractSamPv(AbstractSamSolar, ABC):
990
1098
  Mean capacity factor (fractional).
991
1099
  PV CF is calculated as AC power / DC nameplate.
992
1100
  """
993
- return self['capacity_factor'] / 100
1101
+ return self["capacity_factor"] / 100
994
1102
 
995
1103
  def cf_mean_ac(self):
996
1104
  """Get mean AC capacity factor (fractional) from SAM.
@@ -1003,7 +1111,7 @@ class AbstractSamPv(AbstractSamSolar, ABC):
1003
1111
  Mean AC capacity factor (fractional).
1004
1112
  PV AC CF is calculated as AC power / AC nameplate.
1005
1113
  """
1006
- return self['capacity_factor_ac'] / 100
1114
+ return self["capacity_factor_ac"] / 100
1007
1115
 
1008
1116
  def cf_profile(self):
1009
1117
  """Get hourly capacity factor (frac) profile in local timezone.
@@ -1018,7 +1126,7 @@ class AbstractSamPv(AbstractSamSolar, ABC):
1018
1126
  Datatype is float32 and array length is 8760*time_interval.
1019
1127
  PV CF is calculated as AC power / DC nameplate.
1020
1128
  """
1021
- return self.gen_profile() / self.sam_sys_inputs['system_capacity']
1129
+ return self.gen_profile() / self.sam_sys_inputs["system_capacity"]
1022
1130
 
1023
1131
  def cf_profile_ac(self):
1024
1132
  """Get hourly AC capacity factor (frac) profile in local timezone.
@@ -1047,7 +1155,7 @@ class AbstractSamPv(AbstractSamSolar, ABC):
1047
1155
  1D array of AC inverter power generation in kW.
1048
1156
  Datatype is float32 and array length is 8760*time_interval.
1049
1157
  """
1050
- return np.array(self['gen'], dtype=np.float32)
1158
+ return np.array(self["gen"], dtype=np.float32)
1051
1159
 
1052
1160
  def ac(self):
1053
1161
  """Get AC inverter power generation profile (local timezone) in kW.
@@ -1059,7 +1167,7 @@ class AbstractSamPv(AbstractSamSolar, ABC):
1059
1167
  1D array of AC inverter power generation in kW.
1060
1168
  Datatype is float32 and array length is 8760*time_interval.
1061
1169
  """
1062
- return np.array(self['ac'], dtype=np.float32) / 1000
1170
+ return np.array(self["ac"], dtype=np.float32) / 1000
1063
1171
 
1064
1172
  def dc(self):
1065
1173
  """
@@ -1072,7 +1180,7 @@ class AbstractSamPv(AbstractSamSolar, ABC):
1072
1180
  1D array of DC array power generation in kW.
1073
1181
  Datatype is float32 and array length is 8760*time_interval.
1074
1182
  """
1075
- return np.array(self['dc'], dtype=np.float32) / 1000
1183
+ return np.array(self["dc"], dtype=np.float32) / 1000
1076
1184
 
1077
1185
  def clipped_power(self):
1078
1186
  """
@@ -1108,26 +1216,27 @@ class AbstractSamPv(AbstractSamSolar, ABC):
1108
1216
  """
1109
1217
 
1110
1218
  if output_lookup is None:
1111
- output_lookup = {'cf_mean': self.cf_mean,
1112
- 'cf_mean_ac': self.cf_mean_ac,
1113
- 'cf_profile': self.cf_profile,
1114
- 'cf_profile_ac': self.cf_profile_ac,
1115
- 'annual_energy': self.annual_energy,
1116
- 'energy_yield': self.energy_yield,
1117
- 'gen_profile': self.gen_profile,
1118
- 'ac': self.ac,
1119
- 'dc': self.dc,
1120
- 'clipped_power': self.clipped_power,
1121
- 'system_capacity_ac': self.system_capacity_ac,
1122
- }
1219
+ output_lookup = {
1220
+ "cf_mean": self.cf_mean,
1221
+ "cf_mean_ac": self.cf_mean_ac,
1222
+ "cf_profile": self.cf_profile,
1223
+ "cf_profile_ac": self.cf_profile_ac,
1224
+ "annual_energy": self.annual_energy,
1225
+ "energy_yield": self.energy_yield,
1226
+ "gen_profile": self.gen_profile,
1227
+ "ac": self.ac,
1228
+ "dc": self.dc,
1229
+ "clipped_power": self.clipped_power,
1230
+ "system_capacity_ac": self.system_capacity_ac,
1231
+ }
1123
1232
 
1124
1233
  super().collect_outputs(output_lookup=output_lookup)
1125
1234
 
1126
1235
 
1127
1236
  class PvWattsv5(AbstractSamPv):
1128
- """Photovoltaic (PV) generation with pvwattsv5.
1129
- """
1130
- MODULE = 'pvwattsv5'
1237
+ """Photovoltaic (PV) generation with pvwattsv5."""
1238
+
1239
+ MODULE = "pvwattsv5"
1131
1240
  PYSAM = PySamPv5
1132
1241
 
1133
1242
  @staticmethod
@@ -1142,9 +1251,9 @@ class PvWattsv5(AbstractSamPv):
1142
1251
 
1143
1252
 
1144
1253
  class PvWattsv7(AbstractSamPv):
1145
- """Photovoltaic (PV) generation with pvwattsv7.
1146
- """
1147
- MODULE = 'pvwattsv7'
1254
+ """Photovoltaic (PV) generation with pvwattsv7."""
1255
+
1256
+ MODULE = "pvwattsv7"
1148
1257
  PYSAM = PySamPv7
1149
1258
 
1150
1259
  @staticmethod
@@ -1159,9 +1268,9 @@ class PvWattsv7(AbstractSamPv):
1159
1268
 
1160
1269
 
1161
1270
  class PvWattsv8(AbstractSamPv):
1162
- """Photovoltaic (PV) generation with pvwattsv8.
1163
- """
1164
- MODULE = 'pvwattsv8'
1271
+ """Photovoltaic (PV) generation with pvwattsv8."""
1272
+
1273
+ MODULE = "pvwattsv8"
1165
1274
  PYSAM = PySamPv8
1166
1275
 
1167
1276
  @staticmethod
@@ -1178,7 +1287,7 @@ class PvWattsv8(AbstractSamPv):
1178
1287
  class PvSamv1(AbstractSamPv):
1179
1288
  """Detailed PV model"""
1180
1289
 
1181
- MODULE = 'Pvsamv1'
1290
+ MODULE = "Pvsamv1"
1182
1291
  PYSAM = PySamDetailedPv
1183
1292
 
1184
1293
  def ac(self):
@@ -1191,7 +1300,7 @@ class PvSamv1(AbstractSamPv):
1191
1300
  1D array of AC inverter power generation in kW.
1192
1301
  Datatype is float32 and array length is 8760*time_interval.
1193
1302
  """
1194
- return np.array(self['gen'], dtype=np.float32)
1303
+ return np.array(self["gen"], dtype=np.float32)
1195
1304
 
1196
1305
  def dc(self):
1197
1306
  """
@@ -1204,7 +1313,7 @@ class PvSamv1(AbstractSamPv):
1204
1313
  1D array of DC array power generation in kW.
1205
1314
  Datatype is float32 and array length is 8760*time_interval.
1206
1315
  """
1207
- return np.array(self['dc_net'], dtype=np.float32)
1316
+ return np.array(self["dc_net"], dtype=np.float32)
1208
1317
 
1209
1318
  @staticmethod
1210
1319
  def default():
@@ -1218,9 +1327,9 @@ class PvSamv1(AbstractSamPv):
1218
1327
 
1219
1328
 
1220
1329
  class TcsMoltenSalt(AbstractSamSolar):
1221
- """Concentrated Solar Power (CSP) generation with tower molten salt
1222
- """
1223
- MODULE = 'tcsmolten_salt'
1330
+ """Concentrated Solar Power (CSP) generation with tower molten salt"""
1331
+
1332
+ MODULE = "tcsmolten_salt"
1224
1333
  PYSAM = PySamCSP
1225
1334
 
1226
1335
  def cf_profile(self):
@@ -1234,7 +1343,7 @@ class TcsMoltenSalt(AbstractSamSolar):
1234
1343
  1D numpy array of capacity factor profile.
1235
1344
  Datatype is float32 and array length is 8760*time_interval.
1236
1345
  """
1237
- x = np.abs(self.gen_profile() / self.sam_sys_inputs['system_capacity'])
1346
+ x = np.abs(self.gen_profile() / self.sam_sys_inputs["system_capacity"])
1238
1347
  return x
1239
1348
 
1240
1349
  @staticmethod
@@ -1252,9 +1361,10 @@ class SolarWaterHeat(AbstractSamGenerationFromWeatherFile):
1252
1361
  """
1253
1362
  Solar Water Heater generation
1254
1363
  """
1255
- MODULE = 'solarwaterheat'
1364
+
1365
+ MODULE = "solarwaterheat"
1256
1366
  PYSAM = PySamSwh
1257
- PYSAM_WEATHER_TAG = 'solar_resource_file'
1367
+ PYSAM_WEATHER_TAG = "solar_resource_file"
1258
1368
 
1259
1369
  @staticmethod
1260
1370
  def default():
@@ -1271,9 +1381,10 @@ class LinearDirectSteam(AbstractSamGenerationFromWeatherFile):
1271
1381
  """
1272
1382
  Process heat linear Fresnel direct steam generation
1273
1383
  """
1274
- MODULE = 'lineardirectsteam'
1384
+
1385
+ MODULE = "lineardirectsteam"
1275
1386
  PYSAM = PySamLds
1276
- PYSAM_WEATHER_TAG = 'file_name'
1387
+ PYSAM_WEATHER_TAG = "file_name"
1277
1388
 
1278
1389
  def cf_mean(self):
1279
1390
  """Calculate mean capacity factor (fractional) from SAM.
@@ -1283,10 +1394,11 @@ class LinearDirectSteam(AbstractSamGenerationFromWeatherFile):
1283
1394
  output : float
1284
1395
  Mean capacity factor (fractional).
1285
1396
  """
1286
- net_power = self['annual_field_energy'] \
1287
- - self['annual_thermal_consumption'] # kW-hr
1397
+ net_power = (
1398
+ self["annual_field_energy"] - self["annual_thermal_consumption"]
1399
+ ) # kW-hr
1288
1400
  # q_pb_des is in MW, convert to kW-hr
1289
- name_plate = self['q_pb_des'] * 8760 * 1000
1401
+ name_plate = self["q_pb_des"] * 8760 * 1000
1290
1402
 
1291
1403
  return net_power / name_plate
1292
1404
 
@@ -1305,9 +1417,10 @@ class TroughPhysicalHeat(AbstractSamGenerationFromWeatherFile):
1305
1417
  """
1306
1418
  Trough Physical Process Heat generation
1307
1419
  """
1308
- MODULE = 'troughphysicalheat'
1420
+
1421
+ MODULE = "troughphysicalheat"
1309
1422
  PYSAM = PySamTpph
1310
- PYSAM_WEATHER_TAG = 'file_name'
1423
+ PYSAM_WEATHER_TAG = "file_name"
1311
1424
 
1312
1425
  def cf_mean(self):
1313
1426
  """Calculate mean capacity factor (fractional) from SAM.
@@ -1317,10 +1430,11 @@ class TroughPhysicalHeat(AbstractSamGenerationFromWeatherFile):
1317
1430
  output : float
1318
1431
  Mean capacity factor (fractional).
1319
1432
  """
1320
- net_power = self['annual_gross_energy'] \
1321
- - self['annual_thermal_consumption'] # kW-hr
1433
+ net_power = (
1434
+ self["annual_gross_energy"] - self["annual_thermal_consumption"]
1435
+ ) # kW-hr
1322
1436
  # q_pb_des is in MW, convert to kW-hr
1323
- name_plate = self['q_pb_design'] * 8760 * 1000
1437
+ name_plate = self["q_pb_design"] * 8760 * 1000
1324
1438
 
1325
1439
  return net_power / name_plate
1326
1440
 
@@ -1424,9 +1538,22 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1424
1538
  plant type. Either Binary (0) or Flash (1). Only values of 0
1425
1539
  or 1 allowed.
1426
1540
  - ``design_temp`` : EGS plant design temperature (in C). Only
1427
- affects EGS runs. If this value is set lower than the
1428
- resource temperature input, ``reV`` will adjust it to match
1429
- the latter in order to avoid SAM errors.
1541
+ affects EGS runs. This value may be adjusted internally by
1542
+ ``reV under the following conditions:
1543
+
1544
+ - The design temperature is larger than the resource
1545
+ temperature
1546
+ - The design temperature is lower than the resource
1547
+ temperature by a factor of ``MAX_RT_TO_EGS_RATIO``
1548
+
1549
+ If either of these conditions are true, the ``design_temp`` is a
1550
+ adjusted to match the resource temperature input in order to
1551
+ avoid SAM errors.
1552
+ - ``set_EGS_PDT_to_RT`` : Boolean flag to set EGS design
1553
+ temperature to match the resource temperature input. If this
1554
+ is ``True``, the ``design_temp`` input is ignored. This helps
1555
+ avoid SAM/GETEM errors when the plant design temperature is
1556
+ too high/low compared to the resource temperature.
1430
1557
  - ``geotherm.cost.inj_prod_well_ratio`` : Fraction representing
1431
1558
  the injection to production well ratio (0-1). SAM GUI defaults
1432
1559
  to 0.5 for this value, but it is recommended to set this to
@@ -1496,7 +1623,11 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1496
1623
 
1497
1624
  """
1498
1625
 
1499
- MODULE = 'geothermal'
1626
+ # Per Matt Prilliman on 2/22/24, it's unclear where this ratio originates,
1627
+ # but SAM errors out if it's exceeded.
1628
+ MAX_RT_TO_EGS_RATIO = 1.134324
1629
+ """Max value of ``resource_temperature``/``EGS_plan_design_temperature``"""
1630
+ MODULE = "geothermal"
1500
1631
  PYSAM = PySamGeothermal
1501
1632
  PYSAM_WEATHER_TAG = "file_name"
1502
1633
  _RESOURCE_POTENTIAL_MULT = 1.001
@@ -1522,14 +1653,16 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1522
1653
  1D numpy array of capacity factor profile.
1523
1654
  Datatype is float32 and array length is 8760*time_interval.
1524
1655
  """
1525
- return self.gen_profile() / self.sam_sys_inputs['nameplate']
1656
+ return self.gen_profile() / self.sam_sys_inputs["nameplate"]
1526
1657
 
1527
1658
  def assign_inputs(self):
1528
1659
  """Assign the self.sam_sys_inputs attribute to the PySAM object."""
1529
1660
  if self.sam_sys_inputs.get("ui_calculations_only"):
1530
- msg = ('reV requires model run - cannot set '
1531
- '"ui_calculations_only" to `True` (1). Automatically '
1532
- 'setting to `False` (0)!')
1661
+ msg = (
1662
+ "reV requires model run - cannot set "
1663
+ '"ui_calculations_only" to `True` (1). Automatically '
1664
+ "setting to `False` (0)!"
1665
+ )
1533
1666
  logger.warning(msg)
1534
1667
  warn(msg)
1535
1668
  self.sam_sys_inputs["ui_calculations_only"] = 0
@@ -1564,26 +1697,35 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1564
1697
  self._set_costs()
1565
1698
 
1566
1699
  def _set_resource_temperature(self, resource):
1567
- """Set resource temp from data if user did not specify it. """
1700
+ """Set resource temp from data if user did not specify it."""
1568
1701
 
1569
1702
  if "resource_temp" in self.sam_sys_inputs:
1570
- logger.debug("Found 'resource_temp' value in SAM config: {:.2f}"
1571
- .format(self.sam_sys_inputs["resource_temp"]))
1703
+ logger.debug(
1704
+ "Found 'resource_temp' value in SAM config: {:.2f}".format(
1705
+ self.sam_sys_inputs["resource_temp"]
1706
+ )
1707
+ )
1572
1708
  return
1573
1709
 
1574
1710
  val = set(resource["temperature"].unique())
1575
- logger.debug("Found {} value(s) for 'temperature' in resource data"
1576
- .format(len(val)))
1711
+ logger.debug(
1712
+ "Found {} value(s) for 'temperature' in resource data".format(
1713
+ len(val)
1714
+ )
1715
+ )
1577
1716
  if len(val) > 1:
1578
- msg = ("Found multiple values for 'temperature' for site {}: {}"
1579
- .format(self.site, val))
1717
+ msg = (
1718
+ "Found multiple values for 'temperature' for site "
1719
+ "{}: {}".format(self.site, val)
1720
+ )
1580
1721
  logger.error(msg)
1581
1722
  raise InputError(msg)
1582
1723
 
1583
1724
  val = val.pop()
1584
- logger.debug("Input 'resource_temp' not found in SAM config - setting "
1585
- "to {:.2f} based on input resource data."
1586
- .format(val))
1725
+ logger.debug(
1726
+ "Input 'resource_temp' not found in SAM config - setting "
1727
+ "to {:.2f} based on input resource data.".format(val)
1728
+ )
1587
1729
  self.sam_sys_inputs["resource_temp"] = val
1588
1730
 
1589
1731
  def _set_egs_plant_design_temperature(self):
@@ -1591,31 +1733,67 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1591
1733
  if self.sam_sys_inputs.get("resource_type") != 1:
1592
1734
  return # Not EGS run
1593
1735
 
1736
+ set_egs_pdt_to_rt = self.sam_sys_inputs.get("set_EGS_PDT_to_RT", False)
1594
1737
  egs_plant_design_temp = self.sam_sys_inputs.get("design_temp", 0)
1595
1738
  resource_temp = self.sam_sys_inputs["resource_temp"]
1739
+
1740
+ if set_egs_pdt_to_rt:
1741
+ msg = (
1742
+ "Setting EGS plant design temperature ({}C) to match "
1743
+ "resource temperature ({}C)".format(
1744
+ egs_plant_design_temp, resource_temp
1745
+ )
1746
+ )
1747
+ logger.info(msg)
1748
+ self.sam_sys_inputs["design_temp"] = resource_temp
1749
+ return
1750
+
1596
1751
  if egs_plant_design_temp > resource_temp:
1597
- msg = ('EGS plant design temperature ({}C) exceeds resource '
1598
- 'temperature ({}C). Lowering EGS plant design temperature '
1599
- 'to match resource temperature'
1600
- .format(egs_plant_design_temp, resource_temp))
1752
+ msg = (
1753
+ "EGS plant design temperature ({}C) exceeds resource "
1754
+ "temperature ({}C). Lowering EGS plant design temperature "
1755
+ "to match resource temperature".format(
1756
+ egs_plant_design_temp, resource_temp
1757
+ )
1758
+ )
1759
+ logger.warning(msg)
1760
+ warn(msg)
1761
+ self.sam_sys_inputs["design_temp"] = resource_temp
1762
+ return
1763
+
1764
+ if resource_temp / egs_plant_design_temp > self.MAX_RT_TO_EGS_RATIO:
1765
+ msg = (
1766
+ "EGS plant design temperature ({}C) is lower than resource "
1767
+ "temperature ({}C) by more than a factor of {}. Increasing "
1768
+ "EGS plant design temperature to match resource "
1769
+ "temperature".format(
1770
+ egs_plant_design_temp,
1771
+ resource_temp,
1772
+ self.MAX_RT_TO_EGS_RATIO,
1773
+ )
1774
+ )
1601
1775
  logger.warning(msg)
1602
1776
  warn(msg)
1603
1777
  self.sam_sys_inputs["design_temp"] = resource_temp
1604
1778
 
1605
1779
  def _set_nameplate_to_match_resource_potential(self, resource):
1606
- """Set the nameplate capacity to match the resource potential. """
1780
+ """Set the nameplate capacity to match the resource potential."""
1607
1781
 
1608
1782
  if "nameplate" in self.sam_sys_inputs:
1609
- msg = ('Found "nameplate" input in config! Resource potential '
1610
- 'from input data will be ignored. Nameplate capacity is {}'
1611
- .format(self.sam_sys_inputs["nameplate"]))
1783
+ msg = (
1784
+ 'Found "nameplate" input in config! Resource potential '
1785
+ "from input data will be ignored. Nameplate capacity is "
1786
+ "{}".format(self.sam_sys_inputs["nameplate"])
1787
+ )
1612
1788
  logger.info(msg)
1613
1789
  return
1614
1790
 
1615
1791
  val = set(resource["potential_MW"].unique())
1616
1792
  if len(val) > 1:
1617
- msg = ('Found multiple values for "potential_MW" for site {}: {}'
1618
- .format(self.site, val))
1793
+ msg = (
1794
+ 'Found multiple values for "potential_MW" for site '
1795
+ "{}: {}".format(self.site, val)
1796
+ )
1619
1797
  logger.error(msg)
1620
1798
  raise InputError(msg)
1621
1799
 
@@ -1642,63 +1820,82 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1642
1820
  self.sam_sys_inputs["resource_potential"] = -1
1643
1821
  return
1644
1822
 
1645
- gross_gen = (getattr(self.pysam.Outputs, "gross_output")
1646
- * self._RESOURCE_POTENTIAL_MULT)
1823
+ gross_gen = (
1824
+ self.pysam.Outputs.gross_output * self._RESOURCE_POTENTIAL_MULT
1825
+ )
1647
1826
  if "resource_potential" in self.sam_sys_inputs:
1648
- msg = ('Setting "resource_potential" is not allowed! Updating '
1649
- 'user input of {} to match the gross generation: {}'
1650
- .format(self.sam_sys_inputs["resource_potential"],
1651
- gross_gen))
1827
+ msg = (
1828
+ 'Setting "resource_potential" is not allowed! Updating '
1829
+ "user input of {} to match the gross generation: {}".format(
1830
+ self.sam_sys_inputs["resource_potential"], gross_gen
1831
+ )
1832
+ )
1652
1833
  logger.warning(msg)
1653
1834
  warn(msg)
1654
1835
 
1655
- logger.debug("Setting the resource potential to {} MW"
1656
- .format(gross_gen))
1836
+ logger.debug(
1837
+ "Setting the resource potential to {} MW".format(gross_gen)
1838
+ )
1657
1839
  self.sam_sys_inputs["resource_potential"] = gross_gen
1658
1840
 
1659
- ncw = self.sam_sys_inputs.pop("num_confirmation_wells",
1660
- self._DEFAULT_NUM_CONFIRMATION_WELLS)
1841
+ ncw = self.sam_sys_inputs.pop(
1842
+ "num_confirmation_wells", self._DEFAULT_NUM_CONFIRMATION_WELLS
1843
+ )
1661
1844
  self.sam_sys_inputs["prod_and_inj_wells_to_drill"] = (
1662
- getattr(self.pysam.Outputs, "num_wells_getem_output")
1845
+ self.pysam.Outputs.num_wells_getem_output
1663
1846
  - ncw
1664
- + getattr(self.pysam.Outputs, "num_wells_getem_inj"))
1847
+ + self.pysam.Outputs.num_wells_getem_inj
1848
+ )
1665
1849
  self["ui_calculations_only"] = 0
1666
1850
 
1667
1851
  def _set_costs(self):
1668
1852
  """Set the costs based on gross plant generation."""
1669
- plant_size_kw = (self.sam_sys_inputs["resource_potential"]
1670
- / self._RESOURCE_POTENTIAL_MULT) * 1000
1853
+ plant_size_kw = (
1854
+ self.sam_sys_inputs["resource_potential"]
1855
+ / self._RESOURCE_POTENTIAL_MULT
1856
+ ) * 1000
1671
1857
 
1672
1858
  cc_per_kw = self.sam_sys_inputs.pop("capital_cost_per_kw", None)
1673
1859
  if cc_per_kw is not None:
1674
1860
  capital_cost = cc_per_kw * plant_size_kw
1675
- logger.debug("Setting the capital_cost to ${:,.2f}"
1676
- .format(capital_cost))
1861
+ logger.debug(
1862
+ "Setting the capital_cost to ${:,.2f}".format(capital_cost)
1863
+ )
1677
1864
  self.sam_sys_inputs["capital_cost"] = capital_cost
1678
1865
 
1679
1866
  dc_per_well = self.sam_sys_inputs.pop("drill_cost_per_well", None)
1680
- num_wells = self.sam_sys_inputs.pop("prod_and_inj_wells_to_drill",
1681
- None)
1867
+ num_wells = self.sam_sys_inputs.pop(
1868
+ "prod_and_inj_wells_to_drill", None
1869
+ )
1682
1870
  if dc_per_well is not None:
1683
1871
  if num_wells is None:
1684
- msg = ('Could not determine number of wells to be drilled. '
1685
- 'No drilling costs added!')
1872
+ msg = (
1873
+ "Could not determine number of wells to be drilled. "
1874
+ "No drilling costs added!"
1875
+ )
1686
1876
  logger.warning(msg)
1687
1877
  warn(msg)
1688
1878
  else:
1689
1879
  capital_cost = self.sam_sys_inputs["capital_cost"]
1690
1880
  drill_cost = dc_per_well * num_wells
1691
- logger.debug("Setting the drilling cost to ${:,.2f} "
1692
- "({:.2f} wells at ${:,.2f} per well)"
1693
- .format(drill_cost, num_wells, dc_per_well))
1881
+ logger.debug(
1882
+ "Setting the drilling cost to ${:,.2f} "
1883
+ "({:.2f} wells at ${:,.2f} per well)".format(
1884
+ drill_cost, num_wells, dc_per_well
1885
+ )
1886
+ )
1694
1887
  self.sam_sys_inputs["capital_cost"] = capital_cost + drill_cost
1695
1888
 
1696
- foc_per_kw = self.sam_sys_inputs.pop("fixed_operating_cost_per_kw",
1697
- None)
1889
+ foc_per_kw = self.sam_sys_inputs.pop(
1890
+ "fixed_operating_cost_per_kw", None
1891
+ )
1698
1892
  if foc_per_kw is not None:
1699
1893
  fixed_operating_cost = foc_per_kw * plant_size_kw
1700
- logger.debug("Setting the fixed_operating_cost to ${:,.2f}"
1701
- .format(capital_cost))
1894
+ logger.debug(
1895
+ "Setting the fixed_operating_cost to ${:,.2f}".format(
1896
+ capital_cost
1897
+ )
1898
+ )
1702
1899
  self.sam_sys_inputs["fixed_operating_cost"] = fixed_operating_cost
1703
1900
 
1704
1901
  def _create_pysam_wfile(self, resource, meta):
@@ -1732,16 +1929,23 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1732
1929
  """
1733
1930
  # pylint: disable=attribute-defined-outside-init, consider-using-with
1734
1931
  self._temp_dir = TemporaryDirectory()
1735
- fname = os.path.join(self._temp_dir.name, 'weather.csv')
1736
- logger.debug('Creating PySAM weather data file: {}'.format(fname))
1932
+ fname = os.path.join(self._temp_dir.name, "weather.csv")
1933
+ logger.debug("Creating PySAM weather data file: {}".format(fname))
1737
1934
 
1738
1935
  # ------- Process metadata
1739
1936
  m = pd.DataFrame(meta).T
1740
- m = m.rename({"latitude": "Latitude", "longitude": "Longitude",
1741
- "timezone": "Time Zone"}, axis=1)
1742
-
1743
- m[["Latitude", "Longitude", "Time Zone"]].to_csv(fname, index=False,
1744
- mode='w')
1937
+ m = m.rename(
1938
+ {
1939
+ "latitude": "Latitude",
1940
+ "longitude": "Longitude",
1941
+ "timezone": "Time Zone",
1942
+ },
1943
+ axis=1,
1944
+ )
1945
+
1946
+ m[["Latitude", "Longitude", "Time Zone"]].to_csv(
1947
+ fname, index=False, mode="w"
1948
+ )
1745
1949
 
1746
1950
  # --------- Process data, blank for geothermal
1747
1951
  time_index = resource.index
@@ -1749,12 +1953,12 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1749
1953
  time_index = time_index[~mask]
1750
1954
 
1751
1955
  df = pd.DataFrame(index=time_index)
1752
- df['Year'] = time_index.year
1753
- df['Month'] = time_index.month
1754
- df['Day'] = time_index.day
1755
- df['Hour'] = time_index.hour
1756
- df['Minute'] = time_index.minute
1757
- df.to_csv(fname, index=False, mode='a')
1956
+ df["Year"] = time_index.year
1957
+ df["Month"] = time_index.month
1958
+ df["Day"] = time_index.day
1959
+ df["Hour"] = time_index.hour
1960
+ df["Minute"] = time_index.minute
1961
+ df.to_csv(fname, index=False, mode="a")
1758
1962
 
1759
1963
  return fname
1760
1964
 
@@ -1763,8 +1967,11 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1763
1967
  try:
1764
1968
  super().run_gen_and_econ()
1765
1969
  except SAMExecutionError as e:
1766
- logger.error("Skipping site {}; received sam error: {}"
1767
- .format(self._site, str(e)))
1970
+ logger.error(
1971
+ "Skipping site {}; received sam error: {}".format(
1972
+ self._site, str(e)
1973
+ )
1974
+ )
1768
1975
  self.outputs = {}
1769
1976
 
1770
1977
 
@@ -1863,9 +2070,9 @@ class AbstractSamWind(AbstractSamGeneration, PowerCurveLossesMixin, ABC):
1863
2070
 
1864
2071
 
1865
2072
  class WindPower(AbstractSamWind):
1866
- """Class for Wind generation from SAM
1867
- """
1868
- MODULE = 'windpower'
2073
+ """Class for Wind generation from SAM"""
2074
+
2075
+ MODULE = "windpower"
1869
2076
  PYSAM = PySamWindPower
1870
2077
 
1871
2078
  def set_resource_data(self, resource, meta):
@@ -1888,60 +2095,63 @@ class WindPower(AbstractSamWind):
1888
2095
  meta = self._parse_meta(meta)
1889
2096
 
1890
2097
  # map resource data names to SAM required data names
1891
- var_map = {'speed': 'windspeed',
1892
- 'direction': 'winddirection',
1893
- 'airtemperature': 'temperature',
1894
- 'temp': 'temperature',
1895
- 'surfacepressure': 'pressure',
1896
- 'relativehumidity': 'rh',
1897
- 'humidity': 'rh',
1898
- }
1899
- lower_case = {k: k.lower().replace(' ', '').replace('_', '')
1900
- for k in resource.columns}
1901
- resource = resource.rename(mapper=lower_case, axis='columns')
1902
- resource = resource.rename(mapper=var_map, axis='columns')
2098
+ var_map = {
2099
+ "speed": "windspeed",
2100
+ "direction": "winddirection",
2101
+ "airtemperature": "temperature",
2102
+ "temp": "temperature",
2103
+ "surfacepressure": "pressure",
2104
+ "relativehumidity": "rh",
2105
+ "humidity": "rh",
2106
+ }
2107
+ lower_case = {
2108
+ k: k.lower().replace(" ", "").replace("_", "")
2109
+ for k in resource.columns
2110
+ }
2111
+ resource = resource.rename(mapper=lower_case, axis="columns")
2112
+ resource = resource.rename(mapper=var_map, axis="columns")
1903
2113
 
1904
2114
  data_dict = {}
1905
- var_list = ['temperature', 'pressure', 'windspeed', 'winddirection']
1906
- if 'winddirection' not in resource:
1907
- resource['winddirection'] = 0.0
2115
+ var_list = ["temperature", "pressure", "windspeed", "winddirection"]
2116
+ if "winddirection" not in resource:
2117
+ resource["winddirection"] = 0.0
1908
2118
 
1909
2119
  time_index = resource.index
1910
2120
  self.time_interval = self.get_time_interval(resource.index.values)
1911
2121
 
1912
- data_dict['fields'] = [1, 2, 3, 4]
1913
- data_dict['heights'] = 4 * [self.sam_sys_inputs['wind_turbine_hub_ht']]
2122
+ data_dict["fields"] = [1, 2, 3, 4]
2123
+ data_dict["heights"] = 4 * [self.sam_sys_inputs["wind_turbine_hub_ht"]]
1914
2124
 
1915
- if 'rh' in resource:
2125
+ if "rh" in resource:
1916
2126
  # set relative humidity for icing.
1917
- rh = self.ensure_res_len(resource['rh'].values, time_index)
1918
- n_roll = int(meta['timezone'] * self.time_interval)
2127
+ rh = self.ensure_res_len(resource["rh"].values, time_index)
2128
+ n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
1919
2129
  rh = np.roll(rh, n_roll, axis=0)
1920
- data_dict['rh'] = rh.tolist()
2130
+ data_dict["rh"] = rh.tolist()
1921
2131
 
1922
2132
  # must be set as matrix in [temperature, pres, speed, direction] order
1923
2133
  # ensure that resource array length is multiple of 8760
1924
2134
  # roll the truncated resource array to local timezone
1925
2135
  temp = self.ensure_res_len(resource[var_list].values, time_index)
1926
- n_roll = int(meta['timezone'] * self.time_interval)
2136
+ n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
1927
2137
  temp = np.roll(temp, n_roll, axis=0)
1928
- data_dict['data'] = temp.tolist()
2138
+ data_dict["data"] = temp.tolist()
1929
2139
 
1930
- data_dict['lat'] = float(meta['latitude'])
1931
- data_dict['lon'] = float(meta['longitude'])
1932
- data_dict['tz'] = int(meta['timezone'])
1933
- data_dict['elev'] = float(meta['elevation'])
2140
+ data_dict["lat"] = float(meta[ResourceMetaField.LATITUDE])
2141
+ data_dict["lon"] = float(meta[ResourceMetaField.LONGITUDE])
2142
+ data_dict["tz"] = int(meta[ResourceMetaField.TIMEZONE])
2143
+ data_dict["elev"] = float(meta[ResourceMetaField.ELEVATION])
1934
2144
 
1935
2145
  time_index = self.ensure_res_len(time_index, time_index)
1936
- data_dict['minute'] = time_index.minute.tolist()
1937
- data_dict['hour'] = time_index.hour.tolist()
1938
- data_dict['year'] = time_index.year.tolist()
1939
- data_dict['month'] = time_index.month.tolist()
1940
- data_dict['day'] = time_index.day.tolist()
2146
+ data_dict["minute"] = time_index.minute.tolist()
2147
+ data_dict["hour"] = time_index.hour.tolist()
2148
+ data_dict["year"] = time_index.year.tolist()
2149
+ data_dict["month"] = time_index.month.tolist()
2150
+ data_dict["day"] = time_index.day.tolist()
1941
2151
 
1942
2152
  # add resource data to self.data and clear
1943
- self['wind_resource_data'] = data_dict
1944
- self['wind_resource_model_choice'] = 0
2153
+ self["wind_resource_data"] = data_dict
2154
+ self["wind_resource_model_choice"] = 0
1945
2155
 
1946
2156
  @staticmethod
1947
2157
  def default():
@@ -1959,12 +2169,19 @@ class WindPowerPD(AbstractSamGeneration, PowerCurveLossesMixin):
1959
2169
  """WindPower analysis with wind speed/direction joint probabilty
1960
2170
  distrubtion input"""
1961
2171
 
1962
- MODULE = 'windpower'
2172
+ MODULE = "windpower"
1963
2173
  PYSAM = PySamWindPower
1964
2174
 
1965
- def __init__(self, ws_edges, wd_edges, wind_dist,
1966
- meta, sam_sys_inputs,
1967
- site_sys_inputs=None, output_request=None):
2175
+ def __init__(
2176
+ self,
2177
+ ws_edges,
2178
+ wd_edges,
2179
+ wind_dist,
2180
+ meta,
2181
+ sam_sys_inputs,
2182
+ site_sys_inputs=None,
2183
+ output_request=None,
2184
+ ):
1968
2185
  """Initialize a SAM generation object for windpower with a
1969
2186
  speed/direction joint probability distribution.
1970
2187
 
@@ -1998,13 +2215,17 @@ class WindPowerPD(AbstractSamGeneration, PowerCurveLossesMixin):
1998
2215
 
1999
2216
  # don't pass resource to base class,
2000
2217
  # set in concrete generation classes instead
2001
- super().__init__(None, meta, sam_sys_inputs,
2002
- site_sys_inputs=site_sys_inputs,
2003
- output_request=output_request,
2004
- drop_leap=False)
2218
+ super().__init__(
2219
+ None,
2220
+ meta,
2221
+ sam_sys_inputs,
2222
+ site_sys_inputs=site_sys_inputs,
2223
+ output_request=output_request,
2224
+ drop_leap=False,
2225
+ )
2005
2226
 
2006
2227
  # Set the site number using meta data
2007
- if hasattr(meta, 'name'):
2228
+ if hasattr(meta, "name"):
2008
2229
  self._site = meta.name
2009
2230
  else:
2010
2231
  self._site = None
@@ -2037,19 +2258,21 @@ class WindPowerPD(AbstractSamGeneration, PowerCurveLossesMixin):
2037
2258
  wd_points = wd_edges[:-1] + np.diff(wd_edges) / 2
2038
2259
 
2039
2260
  wd_points, ws_points = np.meshgrid(wd_points, ws_points)
2040
- vstack = (ws_points.flatten(),
2041
- wd_points.flatten(),
2042
- wind_dist.flatten())
2261
+ vstack = (
2262
+ ws_points.flatten(),
2263
+ wd_points.flatten(),
2264
+ wind_dist.flatten(),
2265
+ )
2043
2266
  wrd = np.vstack(vstack).T.tolist()
2044
2267
 
2045
- self['wind_resource_model_choice'] = 2
2046
- self['wind_resource_distribution'] = wrd
2268
+ self["wind_resource_model_choice"] = 2
2269
+ self["wind_resource_distribution"] = wrd
2047
2270
 
2048
2271
 
2049
2272
  class MhkWave(AbstractSamGeneration):
2050
- """Class for Wave generation from SAM
2051
- """
2052
- MODULE = 'mhkwave'
2273
+ """Class for Wave generation from SAM"""
2274
+
2275
+ MODULE = "mhkwave"
2053
2276
  PYSAM = PySamMhkWave
2054
2277
 
2055
2278
  def set_resource_data(self, resource, meta):
@@ -2070,19 +2293,22 @@ class MhkWave(AbstractSamGeneration):
2070
2293
  meta = self._parse_meta(meta)
2071
2294
 
2072
2295
  # map resource data names to SAM required data names
2073
- var_map = {'significantwaveheight': 'significant_wave_height',
2074
- 'waveheight': 'significant_wave_height',
2075
- 'height': 'significant_wave_height',
2076
- 'swh': 'significant_wave_height',
2077
- 'energyperiod': 'energy_period',
2078
- 'waveperiod': 'energy_period',
2079
- 'period': 'energy_period',
2080
- 'ep': 'energy_period',
2081
- }
2082
- lower_case = {k: k.lower().replace(' ', '').replace('_', '')
2083
- for k in resource.columns}
2084
- resource = resource.rename(mapper=lower_case, axis='columns')
2085
- resource = resource.rename(mapper=var_map, axis='columns')
2296
+ var_map = {
2297
+ "significantwaveheight": "significant_wave_height",
2298
+ "waveheight": "significant_wave_height",
2299
+ "height": "significant_wave_height",
2300
+ "swh": "significant_wave_height",
2301
+ "energyperiod": "energy_period",
2302
+ "waveperiod": "energy_period",
2303
+ "period": "energy_period",
2304
+ "ep": "energy_period",
2305
+ }
2306
+ lower_case = {
2307
+ k: k.lower().replace(" ", "").replace("_", "")
2308
+ for k in resource.columns
2309
+ }
2310
+ resource = resource.rename(mapper=lower_case, axis="columns")
2311
+ resource = resource.rename(mapper=var_map, axis="columns")
2086
2312
 
2087
2313
  data_dict = {}
2088
2314
 
@@ -2092,24 +2318,24 @@ class MhkWave(AbstractSamGeneration):
2092
2318
  # must be set as matrix in [temperature, pres, speed, direction] order
2093
2319
  # ensure that resource array length is multiple of 8760
2094
2320
  # roll the truncated resource array to local timezone
2095
- for var in ['significant_wave_height', 'energy_period']:
2321
+ for var in ["significant_wave_height", "energy_period"]:
2096
2322
  arr = self.ensure_res_len(resource[var].values, time_index)
2097
- n_roll = int(meta['timezone'] * self.time_interval)
2323
+ n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
2098
2324
  data_dict[var] = np.roll(arr, n_roll, axis=0).tolist()
2099
2325
 
2100
- data_dict['lat'] = meta['latitude']
2101
- data_dict['lon'] = meta['longitude']
2102
- data_dict['tz'] = meta['timezone']
2326
+ data_dict["lat"] = meta[ResourceMetaField.LATITUDE]
2327
+ data_dict["lon"] = meta[ResourceMetaField.LONGITUDE]
2328
+ data_dict["tz"] = meta[ResourceMetaField.TIMEZONE]
2103
2329
 
2104
2330
  time_index = self.ensure_res_len(time_index, time_index)
2105
- data_dict['minute'] = time_index.minute
2106
- data_dict['hour'] = time_index.hour
2107
- data_dict['year'] = time_index.year
2108
- data_dict['month'] = time_index.month
2109
- data_dict['day'] = time_index.day
2331
+ data_dict["minute"] = time_index.minute
2332
+ data_dict["hour"] = time_index.hour
2333
+ data_dict["year"] = time_index.year
2334
+ data_dict["month"] = time_index.month
2335
+ data_dict["day"] = time_index.day
2110
2336
 
2111
2337
  # add resource data to self.data and clear
2112
- self['wave_resource_data'] = data_dict
2338
+ self["wave_resource_data"] = data_dict
2113
2339
 
2114
2340
  @staticmethod
2115
2341
  def default():