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.
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/METADATA +13 -10
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/RECORD +43 -43
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/WHEEL +1 -1
- reV/SAM/SAM.py +217 -133
- reV/SAM/econ.py +18 -14
- reV/SAM/generation.py +611 -422
- reV/SAM/windbos.py +93 -79
- reV/bespoke/bespoke.py +681 -377
- reV/bespoke/cli_bespoke.py +2 -0
- reV/bespoke/place_turbines.py +187 -43
- reV/config/output_request.py +2 -1
- reV/config/project_points.py +218 -140
- reV/econ/econ.py +166 -114
- reV/econ/economies_of_scale.py +91 -45
- reV/generation/base.py +331 -184
- reV/generation/generation.py +326 -200
- reV/generation/output_attributes/lcoe_fcr_inputs.json +38 -3
- reV/handlers/__init__.py +0 -1
- reV/handlers/exclusions.py +16 -15
- reV/handlers/multi_year.py +57 -26
- reV/handlers/outputs.py +6 -5
- reV/handlers/transmission.py +44 -27
- reV/hybrids/hybrid_methods.py +30 -30
- reV/hybrids/hybrids.py +305 -189
- 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 +735 -390
- reV/supply_curve/sc_aggregation.py +357 -248
- reV/supply_curve/supply_curve.py +604 -347
- reV/supply_curve/tech_mapping.py +144 -82
- reV/utilities/__init__.py +274 -16
- reV/utilities/pytest_utils.py +8 -4
- reV/version.py +1 -1
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/LICENSE +0 -0
- {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
@@ -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
|
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
|
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__(
|
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
|
|
@@ -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
|
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 =
|
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[
|
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 = (
|
1547
|
-
|
1548
|
-
|
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(
|
1587
|
-
|
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(
|
1592
|
-
|
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 = (
|
1595
|
-
|
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(
|
1601
|
-
|
1602
|
-
|
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 = (
|
1616
|
-
|
1617
|
-
|
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 = (
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
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 = (
|
1634
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
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 = (
|
1647
|
-
|
1648
|
-
|
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 = (
|
1655
|
-
|
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 = (
|
1683
|
-
|
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 = (
|
1686
|
-
|
1687
|
-
|
1688
|
-
|
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(
|
1693
|
-
|
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(
|
1697
|
-
|
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
|
-
|
1845
|
+
self.pysam.Outputs.num_wells_getem_output
|
1700
1846
|
- ncw
|
1701
|
-
+
|
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 = (
|
1707
|
-
|
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(
|
1713
|
-
|
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(
|
1718
|
-
|
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 = (
|
1722
|
-
|
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(
|
1729
|
-
|
1730
|
-
|
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(
|
1734
|
-
|
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(
|
1738
|
-
|
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,
|
1773
|
-
logger.debug(
|
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(
|
1778
|
-
|
1779
|
-
|
1780
|
-
|
1781
|
-
|
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[
|
1790
|
-
df[
|
1791
|
-
df[
|
1792
|
-
df[
|
1793
|
-
df[
|
1794
|
-
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")
|
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(
|
1804
|
-
|
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 =
|
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 = {
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
|
1933
|
-
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
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 = [
|
1943
|
-
if
|
1944
|
-
resource[
|
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[
|
1950
|
-
data_dict[
|
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
|
2125
|
+
if "rh" in resource:
|
1953
2126
|
# set relative humidity for icing.
|
1954
|
-
rh = self.ensure_res_len(resource[
|
1955
|
-
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)
|
1956
2129
|
rh = np.roll(rh, n_roll, axis=0)
|
1957
|
-
data_dict[
|
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[
|
2136
|
+
n_roll = int(meta[ResourceMetaField.TIMEZONE] * self.time_interval)
|
1964
2137
|
temp = np.roll(temp, n_roll, axis=0)
|
1965
|
-
data_dict[
|
2138
|
+
data_dict["data"] = temp.tolist()
|
1966
2139
|
|
1967
|
-
data_dict[
|
1968
|
-
data_dict[
|
1969
|
-
data_dict[
|
1970
|
-
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])
|
1971
2144
|
|
1972
2145
|
time_index = self.ensure_res_len(time_index, time_index)
|
1973
|
-
data_dict[
|
1974
|
-
data_dict[
|
1975
|
-
data_dict[
|
1976
|
-
data_dict[
|
1977
|
-
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()
|
1978
2151
|
|
1979
2152
|
# add resource data to self.data and clear
|
1980
|
-
self[
|
1981
|
-
self[
|
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 =
|
2172
|
+
MODULE = "windpower"
|
2000
2173
|
PYSAM = PySamWindPower
|
2001
2174
|
|
2002
|
-
def __init__(
|
2003
|
-
|
2004
|
-
|
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__(
|
2039
|
-
|
2040
|
-
|
2041
|
-
|
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,
|
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 = (
|
2078
|
-
|
2079
|
-
|
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[
|
2083
|
-
self[
|
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 =
|
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 = {
|
2111
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
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 [
|
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[
|
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[
|
2138
|
-
data_dict[
|
2139
|
-
data_dict[
|
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[
|
2143
|
-
data_dict[
|
2144
|
-
data_dict[
|
2145
|
-
data_dict[
|
2146
|
-
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
|
2147
2336
|
|
2148
2337
|
# add resource data to self.data and clear
|
2149
|
-
self[
|
2338
|
+
self["wave_resource_data"] = data_dict
|
2150
2339
|
|
2151
2340
|
@staticmethod
|
2152
2341
|
def default():
|