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.
- {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/METADATA +12 -10
- {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/RECORD +38 -38
- {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/WHEEL +1 -1
- reV/SAM/SAM.py +182 -133
- reV/SAM/econ.py +18 -14
- reV/SAM/generation.py +640 -414
- reV/SAM/windbos.py +93 -79
- reV/bespoke/bespoke.py +690 -445
- reV/bespoke/place_turbines.py +6 -6
- reV/config/project_points.py +220 -140
- reV/econ/econ.py +165 -113
- reV/econ/economies_of_scale.py +57 -34
- reV/generation/base.py +310 -183
- reV/generation/generation.py +309 -191
- reV/handlers/exclusions.py +16 -15
- reV/handlers/multi_year.py +12 -9
- reV/handlers/outputs.py +6 -5
- reV/hybrids/hybrid_methods.py +28 -30
- reV/hybrids/hybrids.py +304 -188
- reV/nrwal/nrwal.py +262 -168
- reV/qa_qc/cli_qa_qc.py +14 -10
- reV/qa_qc/qa_qc.py +217 -119
- reV/qa_qc/summary.py +228 -146
- reV/rep_profiles/rep_profiles.py +349 -230
- reV/supply_curve/aggregation.py +349 -188
- reV/supply_curve/competitive_wind_farms.py +90 -48
- reV/supply_curve/exclusions.py +138 -85
- reV/supply_curve/extent.py +75 -50
- reV/supply_curve/points.py +620 -295
- reV/supply_curve/sc_aggregation.py +396 -226
- reV/supply_curve/supply_curve.py +505 -308
- reV/supply_curve/tech_mapping.py +144 -82
- reV/utilities/__init__.py +199 -16
- reV/utilities/pytest_utils.py +8 -4
- reV/version.py +1 -1
- {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/LICENSE +0 -0
- {NREL_reV-0.8.6.dist-info → NREL_reV-0.8.9.dist-info}/entry_points.txt +0 -0
- {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
|
30
|
-
from reV.SAM.defaults import (
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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 (
|
44
|
-
|
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__(
|
53
|
-
|
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__(
|
92
|
-
|
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,
|
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 = (
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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[
|
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 =
|
179
|
-
label_2 =
|
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 =
|
209
|
-
|
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
|
-
|
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
|
244
|
-
meta[
|
245
|
-
|
246
|
-
|
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
|
251
|
-
meta[
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
"""
|
265
|
-
if self._meta is not None:
|
266
|
-
|
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[
|
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[
|
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[
|
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[
|
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[
|
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 = {
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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 = (
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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 = [
|
360
|
-
|
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 = [
|
364
|
-
|
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[
|
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[
|
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(
|
397
|
-
|
398
|
-
|
399
|
-
|
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(
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
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(
|
476
|
-
|
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(
|
494
|
-
|
495
|
-
|
496
|
-
|
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(
|
500
|
-
|
501
|
-
|
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
|
-
|
519
|
-
|
520
|
-
|
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,
|
586
|
-
logger.debug(
|
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[
|
591
|
-
m[
|
592
|
-
m[
|
593
|
-
m[
|
594
|
-
m[
|
595
|
-
m[
|
596
|
-
m[
|
597
|
-
m[
|
598
|
-
m[
|
599
|
-
m[
|
600
|
-
m[
|
601
|
-
m[
|
602
|
-
m[
|
603
|
-
m[
|
604
|
-
m[
|
605
|
-
m[
|
606
|
-
m[
|
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=
|
683
|
+
m[keep_cols].to_csv(fname, index=False, mode="w")
|
609
684
|
|
610
685
|
# --------- Process data
|
611
|
-
var_map = {
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
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(
|
623
|
-
|
624
|
-
|
625
|
-
|
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[
|
631
|
-
df[
|
632
|
-
df[
|
633
|
-
df[
|
634
|
-
df[
|
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=
|
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 = {
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
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 = {
|
729
|
-
|
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 !=
|
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(
|
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
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
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[
|
750
|
-
resource[
|
751
|
-
resource[
|
836
|
+
resource["lat"] = meta[ResourceMetaField.LATITUDE]
|
837
|
+
resource["lon"] = meta[ResourceMetaField.LONGITUDE]
|
838
|
+
resource["tz"] = meta[ResourceMetaField.TIMEZONE]
|
752
839
|
|
753
|
-
|
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[
|
760
|
-
resource[
|
761
|
-
resource[
|
762
|
-
resource[
|
763
|
-
resource[
|
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
|
766
|
-
self[
|
767
|
-
time_index, resource.pop(
|
849
|
+
if "albedo" in resource:
|
850
|
+
self["albedo"] = self.agg_albedo(
|
851
|
+
time_index, resource.pop("albedo")
|
852
|
+
)
|
768
853
|
|
769
|
-
self[
|
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__(
|
782
|
-
|
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__(
|
882
|
-
|
883
|
-
|
884
|
-
|
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 = (
|
909
|
-
|
910
|
-
|
911
|
-
|
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(
|
914
|
-
|
915
|
-
|
916
|
-
|
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
|
938
|
-
the absolute value of the latitude found in meta and the
|
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
|
945
|
-
warn(
|
946
|
-
|
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[
|
956
|
-
if meta[
|
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[
|
1062
|
+
sam_sys_inputs["azimuth"] = 180
|
959
1063
|
else:
|
960
1064
|
# below the equator, az = 0
|
961
|
-
sam_sys_inputs[
|
962
|
-
|
963
|
-
logger.debug(
|
964
|
-
|
965
|
-
|
966
|
-
|
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 (
|
980
|
-
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
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 = {
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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[
|
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[
|
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 =
|
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[
|
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
|
-
|
1364
|
+
|
1365
|
+
MODULE = "solarwaterheat"
|
1256
1366
|
PYSAM = PySamSwh
|
1257
|
-
PYSAM_WEATHER_TAG =
|
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
|
-
|
1384
|
+
|
1385
|
+
MODULE = "lineardirectsteam"
|
1275
1386
|
PYSAM = PySamLds
|
1276
|
-
PYSAM_WEATHER_TAG =
|
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 =
|
1287
|
-
- self[
|
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[
|
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
|
-
|
1420
|
+
|
1421
|
+
MODULE = "troughphysicalheat"
|
1309
1422
|
PYSAM = PySamTpph
|
1310
|
-
PYSAM_WEATHER_TAG =
|
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 =
|
1321
|
-
- self[
|
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[
|
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.
|
1428
|
-
|
1429
|
-
|
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
|
-
|
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[
|
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 = (
|
1531
|
-
|
1532
|
-
|
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(
|
1571
|
-
|
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(
|
1576
|
-
|
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 = (
|
1579
|
-
|
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(
|
1585
|
-
|
1586
|
-
|
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 = (
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
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 = (
|
1610
|
-
|
1611
|
-
|
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 = (
|
1618
|
-
|
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 = (
|
1646
|
-
|
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 = (
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
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(
|
1656
|
-
|
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(
|
1660
|
-
|
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
|
-
|
1845
|
+
self.pysam.Outputs.num_wells_getem_output
|
1663
1846
|
- ncw
|
1664
|
-
+
|
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 = (
|
1670
|
-
|
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(
|
1676
|
-
|
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(
|
1681
|
-
|
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 = (
|
1685
|
-
|
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(
|
1692
|
-
|
1693
|
-
|
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(
|
1697
|
-
|
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(
|
1701
|
-
|
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,
|
1736
|
-
logger.debug(
|
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(
|
1741
|
-
|
1742
|
-
|
1743
|
-
|
1744
|
-
|
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[
|
1753
|
-
df[
|
1754
|
-
df[
|
1755
|
-
df[
|
1756
|
-
df[
|
1757
|
-
df.to_csv(fname, index=False, mode=
|
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(
|
1767
|
-
|
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 =
|
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 = {
|
1892
|
-
|
1893
|
-
|
1894
|
-
|
1895
|
-
|
1896
|
-
|
1897
|
-
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
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 = [
|
1906
|
-
if
|
1907
|
-
resource[
|
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[
|
1913
|
-
data_dict[
|
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
|
2125
|
+
if "rh" in resource:
|
1916
2126
|
# set relative humidity for icing.
|
1917
|
-
rh = self.ensure_res_len(resource[
|
1918
|
-
n_roll = int(meta[
|
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[
|
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[
|
2136
|
+
n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
|
1927
2137
|
temp = np.roll(temp, n_roll, axis=0)
|
1928
|
-
data_dict[
|
2138
|
+
data_dict["data"] = temp.tolist()
|
1929
2139
|
|
1930
|
-
data_dict[
|
1931
|
-
data_dict[
|
1932
|
-
data_dict[
|
1933
|
-
data_dict[
|
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[
|
1937
|
-
data_dict[
|
1938
|
-
data_dict[
|
1939
|
-
data_dict[
|
1940
|
-
data_dict[
|
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[
|
1944
|
-
self[
|
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 =
|
2172
|
+
MODULE = "windpower"
|
1963
2173
|
PYSAM = PySamWindPower
|
1964
2174
|
|
1965
|
-
def __init__(
|
1966
|
-
|
1967
|
-
|
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__(
|
2002
|
-
|
2003
|
-
|
2004
|
-
|
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,
|
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 = (
|
2041
|
-
|
2042
|
-
|
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[
|
2046
|
-
self[
|
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 =
|
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 = {
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
2081
|
-
|
2082
|
-
|
2083
|
-
|
2084
|
-
|
2085
|
-
|
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 [
|
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[
|
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[
|
2101
|
-
data_dict[
|
2102
|
-
data_dict[
|
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[
|
2106
|
-
data_dict[
|
2107
|
-
data_dict[
|
2108
|
-
data_dict[
|
2109
|
-
data_dict[
|
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[
|
2338
|
+
self["wave_resource_data"] = data_dict
|
2113
2339
|
|
2114
2340
|
@staticmethod
|
2115
2341
|
def default():
|