NREL-reV 0.8.7__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/METADATA +13 -10
  2. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/RECORD +43 -43
  3. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/WHEEL +1 -1
  4. reV/SAM/SAM.py +217 -133
  5. reV/SAM/econ.py +18 -14
  6. reV/SAM/generation.py +611 -422
  7. reV/SAM/windbos.py +93 -79
  8. reV/bespoke/bespoke.py +681 -377
  9. reV/bespoke/cli_bespoke.py +2 -0
  10. reV/bespoke/place_turbines.py +187 -43
  11. reV/config/output_request.py +2 -1
  12. reV/config/project_points.py +218 -140
  13. reV/econ/econ.py +166 -114
  14. reV/econ/economies_of_scale.py +91 -45
  15. reV/generation/base.py +331 -184
  16. reV/generation/generation.py +326 -200
  17. reV/generation/output_attributes/lcoe_fcr_inputs.json +38 -3
  18. reV/handlers/__init__.py +0 -1
  19. reV/handlers/exclusions.py +16 -15
  20. reV/handlers/multi_year.py +57 -26
  21. reV/handlers/outputs.py +6 -5
  22. reV/handlers/transmission.py +44 -27
  23. reV/hybrids/hybrid_methods.py +30 -30
  24. reV/hybrids/hybrids.py +305 -189
  25. reV/nrwal/nrwal.py +262 -168
  26. reV/qa_qc/cli_qa_qc.py +14 -10
  27. reV/qa_qc/qa_qc.py +217 -119
  28. reV/qa_qc/summary.py +228 -146
  29. reV/rep_profiles/rep_profiles.py +349 -230
  30. reV/supply_curve/aggregation.py +349 -188
  31. reV/supply_curve/competitive_wind_farms.py +90 -48
  32. reV/supply_curve/exclusions.py +138 -85
  33. reV/supply_curve/extent.py +75 -50
  34. reV/supply_curve/points.py +735 -390
  35. reV/supply_curve/sc_aggregation.py +357 -248
  36. reV/supply_curve/supply_curve.py +604 -347
  37. reV/supply_curve/tech_mapping.py +144 -82
  38. reV/utilities/__init__.py +274 -16
  39. reV/utilities/pytest_utils.py +8 -4
  40. reV/version.py +1 -1
  41. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/LICENSE +0 -0
  42. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/entry_points.txt +0 -0
  43. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/top_level.txt +0 -0
reV/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
@@ -796,11 +887,11 @@ class AbstractSamPv(AbstractSamSolar, ABC):
796
887
  simulate reduced performance over time.
797
888
  - ``analysis_period`` : Integer representing the number of years
798
889
  to include in the lifetime of the model generator. Required if
799
- ``system_use_lifetime_output``=1.
890
+ ``system_use_lifetime_output`` is set to 1.
800
891
  - ``dc_degradation`` : List of percentage values representing the
801
892
  annual DC degradation of capacity factors. Maybe a single value
802
893
  that will be compound each year or a vector of yearly rates.
803
- Required if ``system_use_lifetime_output``=1.
894
+ Required if ``system_use_lifetime_output`` is set to 1.
804
895
 
805
896
  You may also include the following ``reV``-specific keys:
806
897
 
@@ -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
 
@@ -1432,7 +1546,7 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1432
1546
  - The design temperature is lower than the resource
1433
1547
  temperature by a factor of ``MAX_RT_TO_EGS_RATIO``
1434
1548
 
1435
- If either of these conditions are true, the ``design_temp`` is a
1549
+ If either of these conditions are true, the ``design_temp`` is
1436
1550
  adjusted to match the resource temperature input in order to
1437
1551
  avoid SAM errors.
1438
1552
  - ``set_EGS_PDT_to_RT`` : Boolean flag to set EGS design
@@ -1508,11 +1622,12 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1508
1622
  ``time_index_step=2`` yields hourly output, and so forth).
1509
1623
 
1510
1624
  """
1625
+
1511
1626
  # Per Matt Prilliman on 2/22/24, it's unclear where this ratio originates,
1512
1627
  # but SAM errors out if it's exceeded.
1513
1628
  MAX_RT_TO_EGS_RATIO = 1.134324
1514
1629
  """Max value of ``resource_temperature``/``EGS_plan_design_temperature``"""
1515
- MODULE = 'geothermal'
1630
+ MODULE = "geothermal"
1516
1631
  PYSAM = PySamGeothermal
1517
1632
  PYSAM_WEATHER_TAG = "file_name"
1518
1633
  _RESOURCE_POTENTIAL_MULT = 1.001
@@ -1538,14 +1653,16 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1538
1653
  1D numpy array of capacity factor profile.
1539
1654
  Datatype is float32 and array length is 8760*time_interval.
1540
1655
  """
1541
- return self.gen_profile() / self.sam_sys_inputs['nameplate']
1656
+ return self.gen_profile() / self.sam_sys_inputs["nameplate"]
1542
1657
 
1543
1658
  def assign_inputs(self):
1544
1659
  """Assign the self.sam_sys_inputs attribute to the PySAM object."""
1545
1660
  if self.sam_sys_inputs.get("ui_calculations_only"):
1546
- msg = ('reV requires model run - cannot set '
1547
- '"ui_calculations_only" to `True` (1). Automatically '
1548
- '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
+ )
1549
1666
  logger.warning(msg)
1550
1667
  warn(msg)
1551
1668
  self.sam_sys_inputs["ui_calculations_only"] = 0
@@ -1580,26 +1697,35 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1580
1697
  self._set_costs()
1581
1698
 
1582
1699
  def _set_resource_temperature(self, resource):
1583
- """Set resource temp from data if user did not specify it. """
1700
+ """Set resource temp from data if user did not specify it."""
1584
1701
 
1585
1702
  if "resource_temp" in self.sam_sys_inputs:
1586
- logger.debug("Found 'resource_temp' value in SAM config: {:.2f}"
1587
- .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
+ )
1588
1708
  return
1589
1709
 
1590
1710
  val = set(resource["temperature"].unique())
1591
- logger.debug("Found {} value(s) for 'temperature' in resource data"
1592
- .format(len(val)))
1711
+ logger.debug(
1712
+ "Found {} value(s) for 'temperature' in resource data".format(
1713
+ len(val)
1714
+ )
1715
+ )
1593
1716
  if len(val) > 1:
1594
- msg = ("Found multiple values for 'temperature' for site {}: {}"
1595
- .format(self.site, val))
1717
+ msg = (
1718
+ "Found multiple values for 'temperature' for site "
1719
+ "{}: {}".format(self.site, val)
1720
+ )
1596
1721
  logger.error(msg)
1597
1722
  raise InputError(msg)
1598
1723
 
1599
1724
  val = val.pop()
1600
- logger.debug("Input 'resource_temp' not found in SAM config - setting "
1601
- "to {:.2f} based on input resource data."
1602
- .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
+ )
1603
1729
  self.sam_sys_inputs["resource_temp"] = val
1604
1730
 
1605
1731
  def _set_egs_plant_design_temperature(self):
@@ -1612,47 +1738,62 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1612
1738
  resource_temp = self.sam_sys_inputs["resource_temp"]
1613
1739
 
1614
1740
  if set_egs_pdt_to_rt:
1615
- msg = ('Setting EGS plant design temperature ({}C) to match '
1616
- 'resource temperature ({}C)'
1617
- .format(egs_plant_design_temp, resource_temp))
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
+ )
1618
1747
  logger.info(msg)
1619
1748
  self.sam_sys_inputs["design_temp"] = resource_temp
1620
1749
  return
1621
1750
 
1622
1751
  if egs_plant_design_temp > resource_temp:
1623
- msg = ('EGS plant design temperature ({}C) exceeds resource '
1624
- 'temperature ({}C). Lowering EGS plant design temperature '
1625
- 'to match resource temperature'
1626
- .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
+ )
1627
1759
  logger.warning(msg)
1628
1760
  warn(msg)
1629
1761
  self.sam_sys_inputs["design_temp"] = resource_temp
1630
1762
  return
1631
1763
 
1632
1764
  if resource_temp / egs_plant_design_temp > self.MAX_RT_TO_EGS_RATIO:
1633
- msg = ('EGS plant design temperature ({}C) is lower than resource '
1634
- 'temperature ({}C) by more than a factor of {}. Increasing '
1635
- 'EGS plant design temperature to match resource temperature'
1636
- .format(egs_plant_design_temp, resource_temp,
1637
- 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
+ )
1638
1775
  logger.warning(msg)
1639
1776
  warn(msg)
1640
1777
  self.sam_sys_inputs["design_temp"] = resource_temp
1641
1778
 
1642
1779
  def _set_nameplate_to_match_resource_potential(self, resource):
1643
- """Set the nameplate capacity to match the resource potential. """
1780
+ """Set the nameplate capacity to match the resource potential."""
1644
1781
 
1645
1782
  if "nameplate" in self.sam_sys_inputs:
1646
- msg = ('Found "nameplate" input in config! Resource potential '
1647
- 'from input data will be ignored. Nameplate capacity is {}'
1648
- .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
+ )
1649
1788
  logger.info(msg)
1650
1789
  return
1651
1790
 
1652
1791
  val = set(resource["potential_MW"].unique())
1653
1792
  if len(val) > 1:
1654
- msg = ('Found multiple values for "potential_MW" for site {}: {}'
1655
- .format(self.site, val))
1793
+ msg = (
1794
+ 'Found multiple values for "potential_MW" for site '
1795
+ "{}: {}".format(self.site, val)
1796
+ )
1656
1797
  logger.error(msg)
1657
1798
  raise InputError(msg)
1658
1799
 
@@ -1679,63 +1820,82 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1679
1820
  self.sam_sys_inputs["resource_potential"] = -1
1680
1821
  return
1681
1822
 
1682
- gross_gen = (getattr(self.pysam.Outputs, "gross_output")
1683
- * self._RESOURCE_POTENTIAL_MULT)
1823
+ gross_gen = (
1824
+ self.pysam.Outputs.gross_output * self._RESOURCE_POTENTIAL_MULT
1825
+ )
1684
1826
  if "resource_potential" in self.sam_sys_inputs:
1685
- msg = ('Setting "resource_potential" is not allowed! Updating '
1686
- 'user input of {} to match the gross generation: {}'
1687
- .format(self.sam_sys_inputs["resource_potential"],
1688
- 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
+ )
1689
1833
  logger.warning(msg)
1690
1834
  warn(msg)
1691
1835
 
1692
- logger.debug("Setting the resource potential to {} MW"
1693
- .format(gross_gen))
1836
+ logger.debug(
1837
+ "Setting the resource potential to {} MW".format(gross_gen)
1838
+ )
1694
1839
  self.sam_sys_inputs["resource_potential"] = gross_gen
1695
1840
 
1696
- ncw = self.sam_sys_inputs.pop("num_confirmation_wells",
1697
- self._DEFAULT_NUM_CONFIRMATION_WELLS)
1841
+ ncw = self.sam_sys_inputs.pop(
1842
+ "num_confirmation_wells", self._DEFAULT_NUM_CONFIRMATION_WELLS
1843
+ )
1698
1844
  self.sam_sys_inputs["prod_and_inj_wells_to_drill"] = (
1699
- getattr(self.pysam.Outputs, "num_wells_getem_output")
1845
+ self.pysam.Outputs.num_wells_getem_output
1700
1846
  - ncw
1701
- + getattr(self.pysam.Outputs, "num_wells_getem_inj"))
1847
+ + self.pysam.Outputs.num_wells_getem_inj
1848
+ )
1702
1849
  self["ui_calculations_only"] = 0
1703
1850
 
1704
1851
  def _set_costs(self):
1705
1852
  """Set the costs based on gross plant generation."""
1706
- plant_size_kw = (self.sam_sys_inputs["resource_potential"]
1707
- / self._RESOURCE_POTENTIAL_MULT) * 1000
1853
+ plant_size_kw = (
1854
+ self.sam_sys_inputs["resource_potential"]
1855
+ / self._RESOURCE_POTENTIAL_MULT
1856
+ ) * 1000
1708
1857
 
1709
1858
  cc_per_kw = self.sam_sys_inputs.pop("capital_cost_per_kw", None)
1710
1859
  if cc_per_kw is not None:
1711
1860
  capital_cost = cc_per_kw * plant_size_kw
1712
- logger.debug("Setting the capital_cost to ${:,.2f}"
1713
- .format(capital_cost))
1861
+ logger.debug(
1862
+ "Setting the capital_cost to ${:,.2f}".format(capital_cost)
1863
+ )
1714
1864
  self.sam_sys_inputs["capital_cost"] = capital_cost
1715
1865
 
1716
1866
  dc_per_well = self.sam_sys_inputs.pop("drill_cost_per_well", None)
1717
- num_wells = self.sam_sys_inputs.pop("prod_and_inj_wells_to_drill",
1718
- None)
1867
+ num_wells = self.sam_sys_inputs.pop(
1868
+ "prod_and_inj_wells_to_drill", None
1869
+ )
1719
1870
  if dc_per_well is not None:
1720
1871
  if num_wells is None:
1721
- msg = ('Could not determine number of wells to be drilled. '
1722
- 'No drilling costs added!')
1872
+ msg = (
1873
+ "Could not determine number of wells to be drilled. "
1874
+ "No drilling costs added!"
1875
+ )
1723
1876
  logger.warning(msg)
1724
1877
  warn(msg)
1725
1878
  else:
1726
1879
  capital_cost = self.sam_sys_inputs["capital_cost"]
1727
1880
  drill_cost = dc_per_well * num_wells
1728
- logger.debug("Setting the drilling cost to ${:,.2f} "
1729
- "({:.2f} wells at ${:,.2f} per well)"
1730
- .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
+ )
1731
1887
  self.sam_sys_inputs["capital_cost"] = capital_cost + drill_cost
1732
1888
 
1733
- foc_per_kw = self.sam_sys_inputs.pop("fixed_operating_cost_per_kw",
1734
- None)
1889
+ foc_per_kw = self.sam_sys_inputs.pop(
1890
+ "fixed_operating_cost_per_kw", None
1891
+ )
1735
1892
  if foc_per_kw is not None:
1736
1893
  fixed_operating_cost = foc_per_kw * plant_size_kw
1737
- logger.debug("Setting the fixed_operating_cost to ${:,.2f}"
1738
- .format(capital_cost))
1894
+ logger.debug(
1895
+ "Setting the fixed_operating_cost to ${:,.2f}".format(
1896
+ capital_cost
1897
+ )
1898
+ )
1739
1899
  self.sam_sys_inputs["fixed_operating_cost"] = fixed_operating_cost
1740
1900
 
1741
1901
  def _create_pysam_wfile(self, resource, meta):
@@ -1769,16 +1929,23 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1769
1929
  """
1770
1930
  # pylint: disable=attribute-defined-outside-init, consider-using-with
1771
1931
  self._temp_dir = TemporaryDirectory()
1772
- fname = os.path.join(self._temp_dir.name, 'weather.csv')
1773
- 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))
1774
1934
 
1775
1935
  # ------- Process metadata
1776
1936
  m = pd.DataFrame(meta).T
1777
- m = m.rename({"latitude": "Latitude", "longitude": "Longitude",
1778
- "timezone": "Time Zone"}, axis=1)
1779
-
1780
- m[["Latitude", "Longitude", "Time Zone"]].to_csv(fname, index=False,
1781
- 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
+ )
1782
1949
 
1783
1950
  # --------- Process data, blank for geothermal
1784
1951
  time_index = resource.index
@@ -1786,12 +1953,12 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1786
1953
  time_index = time_index[~mask]
1787
1954
 
1788
1955
  df = pd.DataFrame(index=time_index)
1789
- df['Year'] = time_index.year
1790
- df['Month'] = time_index.month
1791
- df['Day'] = time_index.day
1792
- df['Hour'] = time_index.hour
1793
- df['Minute'] = time_index.minute
1794
- 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")
1795
1962
 
1796
1963
  return fname
1797
1964
 
@@ -1800,8 +1967,11 @@ class Geothermal(AbstractSamGenerationFromWeatherFile):
1800
1967
  try:
1801
1968
  super().run_gen_and_econ()
1802
1969
  except SAMExecutionError as e:
1803
- logger.error("Skipping site {}; received sam error: {}"
1804
- .format(self._site, str(e)))
1970
+ logger.error(
1971
+ "Skipping site {}; received sam error: {}".format(
1972
+ self._site, str(e)
1973
+ )
1974
+ )
1805
1975
  self.outputs = {}
1806
1976
 
1807
1977
 
@@ -1900,9 +2070,9 @@ class AbstractSamWind(AbstractSamGeneration, PowerCurveLossesMixin, ABC):
1900
2070
 
1901
2071
 
1902
2072
  class WindPower(AbstractSamWind):
1903
- """Class for Wind generation from SAM
1904
- """
1905
- MODULE = 'windpower'
2073
+ """Class for Wind generation from SAM"""
2074
+
2075
+ MODULE = "windpower"
1906
2076
  PYSAM = PySamWindPower
1907
2077
 
1908
2078
  def set_resource_data(self, resource, meta):
@@ -1925,60 +2095,63 @@ class WindPower(AbstractSamWind):
1925
2095
  meta = self._parse_meta(meta)
1926
2096
 
1927
2097
  # map resource data names to SAM required data names
1928
- var_map = {'speed': 'windspeed',
1929
- 'direction': 'winddirection',
1930
- 'airtemperature': 'temperature',
1931
- 'temp': 'temperature',
1932
- 'surfacepressure': 'pressure',
1933
- 'relativehumidity': 'rh',
1934
- 'humidity': 'rh',
1935
- }
1936
- lower_case = {k: k.lower().replace(' ', '').replace('_', '')
1937
- for k in resource.columns}
1938
- resource = resource.rename(mapper=lower_case, axis='columns')
1939
- 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")
1940
2113
 
1941
2114
  data_dict = {}
1942
- var_list = ['temperature', 'pressure', 'windspeed', 'winddirection']
1943
- if 'winddirection' not in resource:
1944
- resource['winddirection'] = 0.0
2115
+ var_list = ["temperature", "pressure", "windspeed", "winddirection"]
2116
+ if "winddirection" not in resource:
2117
+ resource["winddirection"] = 0.0
1945
2118
 
1946
2119
  time_index = resource.index
1947
2120
  self.time_interval = self.get_time_interval(resource.index.values)
1948
2121
 
1949
- data_dict['fields'] = [1, 2, 3, 4]
1950
- 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"]]
1951
2124
 
1952
- if 'rh' in resource:
2125
+ if "rh" in resource:
1953
2126
  # set relative humidity for icing.
1954
- rh = self.ensure_res_len(resource['rh'].values, time_index)
1955
- 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)
1956
2129
  rh = np.roll(rh, n_roll, axis=0)
1957
- data_dict['rh'] = rh.tolist()
2130
+ data_dict["rh"] = rh.tolist()
1958
2131
 
1959
2132
  # must be set as matrix in [temperature, pres, speed, direction] order
1960
2133
  # ensure that resource array length is multiple of 8760
1961
2134
  # roll the truncated resource array to local timezone
1962
2135
  temp = self.ensure_res_len(resource[var_list].values, time_index)
1963
- n_roll = int(meta['timezone'] * self.time_interval)
2136
+ n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
1964
2137
  temp = np.roll(temp, n_roll, axis=0)
1965
- data_dict['data'] = temp.tolist()
2138
+ data_dict["data"] = temp.tolist()
1966
2139
 
1967
- data_dict['lat'] = float(meta['latitude'])
1968
- data_dict['lon'] = float(meta['longitude'])
1969
- data_dict['tz'] = int(meta['timezone'])
1970
- 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])
1971
2144
 
1972
2145
  time_index = self.ensure_res_len(time_index, time_index)
1973
- data_dict['minute'] = time_index.minute.tolist()
1974
- data_dict['hour'] = time_index.hour.tolist()
1975
- data_dict['year'] = time_index.year.tolist()
1976
- data_dict['month'] = time_index.month.tolist()
1977
- 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()
1978
2151
 
1979
2152
  # add resource data to self.data and clear
1980
- self['wind_resource_data'] = data_dict
1981
- self['wind_resource_model_choice'] = 0
2153
+ self["wind_resource_data"] = data_dict
2154
+ self["wind_resource_model_choice"] = 0
1982
2155
 
1983
2156
  @staticmethod
1984
2157
  def default():
@@ -1996,12 +2169,19 @@ class WindPowerPD(AbstractSamGeneration, PowerCurveLossesMixin):
1996
2169
  """WindPower analysis with wind speed/direction joint probabilty
1997
2170
  distrubtion input"""
1998
2171
 
1999
- MODULE = 'windpower'
2172
+ MODULE = "windpower"
2000
2173
  PYSAM = PySamWindPower
2001
2174
 
2002
- def __init__(self, ws_edges, wd_edges, wind_dist,
2003
- meta, sam_sys_inputs,
2004
- 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
+ ):
2005
2185
  """Initialize a SAM generation object for windpower with a
2006
2186
  speed/direction joint probability distribution.
2007
2187
 
@@ -2035,13 +2215,17 @@ class WindPowerPD(AbstractSamGeneration, PowerCurveLossesMixin):
2035
2215
 
2036
2216
  # don't pass resource to base class,
2037
2217
  # set in concrete generation classes instead
2038
- super().__init__(None, meta, sam_sys_inputs,
2039
- site_sys_inputs=site_sys_inputs,
2040
- output_request=output_request,
2041
- 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
+ )
2042
2226
 
2043
2227
  # Set the site number using meta data
2044
- if hasattr(meta, 'name'):
2228
+ if hasattr(meta, "name"):
2045
2229
  self._site = meta.name
2046
2230
  else:
2047
2231
  self._site = None
@@ -2074,19 +2258,21 @@ class WindPowerPD(AbstractSamGeneration, PowerCurveLossesMixin):
2074
2258
  wd_points = wd_edges[:-1] + np.diff(wd_edges) / 2
2075
2259
 
2076
2260
  wd_points, ws_points = np.meshgrid(wd_points, ws_points)
2077
- vstack = (ws_points.flatten(),
2078
- wd_points.flatten(),
2079
- wind_dist.flatten())
2261
+ vstack = (
2262
+ ws_points.flatten(),
2263
+ wd_points.flatten(),
2264
+ wind_dist.flatten(),
2265
+ )
2080
2266
  wrd = np.vstack(vstack).T.tolist()
2081
2267
 
2082
- self['wind_resource_model_choice'] = 2
2083
- self['wind_resource_distribution'] = wrd
2268
+ self["wind_resource_model_choice"] = 2
2269
+ self["wind_resource_distribution"] = wrd
2084
2270
 
2085
2271
 
2086
2272
  class MhkWave(AbstractSamGeneration):
2087
- """Class for Wave generation from SAM
2088
- """
2089
- MODULE = 'mhkwave'
2273
+ """Class for Wave generation from SAM"""
2274
+
2275
+ MODULE = "mhkwave"
2090
2276
  PYSAM = PySamMhkWave
2091
2277
 
2092
2278
  def set_resource_data(self, resource, meta):
@@ -2107,19 +2293,22 @@ class MhkWave(AbstractSamGeneration):
2107
2293
  meta = self._parse_meta(meta)
2108
2294
 
2109
2295
  # map resource data names to SAM required data names
2110
- var_map = {'significantwaveheight': 'significant_wave_height',
2111
- 'waveheight': 'significant_wave_height',
2112
- 'height': 'significant_wave_height',
2113
- 'swh': 'significant_wave_height',
2114
- 'energyperiod': 'energy_period',
2115
- 'waveperiod': 'energy_period',
2116
- 'period': 'energy_period',
2117
- 'ep': 'energy_period',
2118
- }
2119
- lower_case = {k: k.lower().replace(' ', '').replace('_', '')
2120
- for k in resource.columns}
2121
- resource = resource.rename(mapper=lower_case, axis='columns')
2122
- 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")
2123
2312
 
2124
2313
  data_dict = {}
2125
2314
 
@@ -2129,24 +2318,24 @@ class MhkWave(AbstractSamGeneration):
2129
2318
  # must be set as matrix in [temperature, pres, speed, direction] order
2130
2319
  # ensure that resource array length is multiple of 8760
2131
2320
  # roll the truncated resource array to local timezone
2132
- for var in ['significant_wave_height', 'energy_period']:
2321
+ for var in ["significant_wave_height", "energy_period"]:
2133
2322
  arr = self.ensure_res_len(resource[var].values, time_index)
2134
- n_roll = int(meta['timezone'] * self.time_interval)
2323
+ n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
2135
2324
  data_dict[var] = np.roll(arr, n_roll, axis=0).tolist()
2136
2325
 
2137
- data_dict['lat'] = meta['latitude']
2138
- data_dict['lon'] = meta['longitude']
2139
- 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]
2140
2329
 
2141
2330
  time_index = self.ensure_res_len(time_index, time_index)
2142
- data_dict['minute'] = time_index.minute
2143
- data_dict['hour'] = time_index.hour
2144
- data_dict['year'] = time_index.year
2145
- data_dict['month'] = time_index.month
2146
- 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
2147
2336
 
2148
2337
  # add resource data to self.data and clear
2149
- self['wave_resource_data'] = data_dict
2338
+ self["wave_resource_data"] = data_dict
2150
2339
 
2151
2340
  @staticmethod
2152
2341
  def default():