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/bespoke/bespoke.py
CHANGED
@@ -2,42 +2,47 @@
|
|
2
2
|
"""
|
3
3
|
reV bespoke wind plant analysis tools
|
4
4
|
"""
|
5
|
+
|
5
6
|
# pylint: disable=anomalous-backslash-in-string
|
6
|
-
from inspect import signature
|
7
|
-
import time
|
8
|
-
import logging
|
9
7
|
import copy
|
10
|
-
import pandas as pd
|
11
|
-
import numpy as np
|
12
|
-
import os
|
13
8
|
import json
|
14
|
-
import
|
9
|
+
import logging
|
10
|
+
import os
|
11
|
+
import time
|
12
|
+
from concurrent.futures import as_completed
|
15
13
|
from importlib import import_module
|
14
|
+
from inspect import signature
|
16
15
|
from numbers import Number
|
17
|
-
from concurrent.futures import as_completed
|
18
16
|
from warnings import warn
|
19
17
|
|
18
|
+
import numpy as np
|
19
|
+
import pandas as pd
|
20
|
+
import psutil
|
21
|
+
from rex.joint_pd.joint_pd import JointPD
|
22
|
+
from rex.multi_year_resource import MultiYearWindResource
|
23
|
+
from rex.renewable_resource import WindResource
|
24
|
+
from rex.utilities.bc_parse_table import parse_bc_table
|
25
|
+
from rex.utilities.execution import SpawnProcessPool
|
26
|
+
from rex.utilities.loggers import create_dirs, log_mem
|
27
|
+
from rex.utilities.utilities import parse_year
|
28
|
+
|
20
29
|
from reV.config.output_request import SAMOutputRequest
|
21
|
-
from reV.generation.generation import Gen
|
22
|
-
from reV.SAM.generation import WindPower, WindPowerPD
|
23
30
|
from reV.econ.utilities import lcoe_fcr
|
24
|
-
from reV.
|
31
|
+
from reV.generation.generation import Gen
|
25
32
|
from reV.handlers.exclusions import ExclusionLayers
|
33
|
+
from reV.handlers.outputs import Outputs
|
34
|
+
from reV.SAM.generation import WindPower, WindPowerPD
|
35
|
+
from reV.supply_curve.aggregation import AggFileHandler, BaseAggregation
|
26
36
|
from reV.supply_curve.extent import SupplyCurveExtent
|
27
37
|
from reV.supply_curve.points import AggregationSupplyCurvePoint as AggSCPoint
|
28
38
|
from reV.supply_curve.points import SupplyCurvePoint
|
29
|
-
from reV.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
from
|
36
|
-
from rex.renewable_resource import WindResource
|
37
|
-
from rex.multi_year_resource import MultiYearWindResource
|
38
|
-
from rex.utilities.loggers import log_mem, create_dirs
|
39
|
-
from rex.utilities.utilities import parse_year
|
40
|
-
from rex.utilities.execution import SpawnProcessPool
|
39
|
+
from reV.utilities import (
|
40
|
+
ModuleName,
|
41
|
+
ResourceMetaField,
|
42
|
+
SupplyCurveField,
|
43
|
+
log_versions,
|
44
|
+
)
|
45
|
+
from reV.utilities.exceptions import EmptySupplyCurvePointError, FileInputError
|
41
46
|
|
42
47
|
logger = logging.getLogger(__name__)
|
43
48
|
|
@@ -79,34 +84,44 @@ class BespokeMultiPlantData:
|
|
79
84
|
self._pre_load_data()
|
80
85
|
|
81
86
|
def _pre_load_data(self):
|
82
|
-
"""Pre-load the resource data.
|
87
|
+
"""Pre-load the resource data."""
|
83
88
|
|
84
89
|
for sc_gid, gids in self.sc_gid_to_res_gid.items():
|
85
90
|
hh = self.sc_gid_to_hh[sc_gid]
|
86
91
|
self.hh_to_res_gids.setdefault(hh, set()).update(gids)
|
87
92
|
|
88
|
-
self.hh_to_res_gids = {
|
89
|
-
|
93
|
+
self.hh_to_res_gids = {
|
94
|
+
hh: sorted(gids) for hh, gids in self.hh_to_res_gids.items()
|
95
|
+
}
|
90
96
|
|
91
97
|
start_time = time.time()
|
92
|
-
if
|
98
|
+
if "*" in self.res_fpath:
|
93
99
|
handler = MultiYearWindResource
|
94
100
|
else:
|
95
101
|
handler = WindResource
|
96
102
|
|
97
103
|
with handler(self.res_fpath) as res:
|
98
|
-
self._wind_dirs = {
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
self.
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
self._wind_dirs = {
|
105
|
+
hh: res[f"winddirection_{hh}m", :, gids]
|
106
|
+
for hh, gids in self.hh_to_res_gids.items()
|
107
|
+
}
|
108
|
+
self._wind_speeds = {
|
109
|
+
hh: res[f"windspeed_{hh}m", :, gids]
|
110
|
+
for hh, gids in self.hh_to_res_gids.items()
|
111
|
+
}
|
112
|
+
self._temps = {
|
113
|
+
hh: res[f"temperature_{hh}m", :, gids]
|
114
|
+
for hh, gids in self.hh_to_res_gids.items()
|
115
|
+
}
|
116
|
+
self._pressures = {
|
117
|
+
hh: res[f"pressure_{hh}m", :, gids]
|
118
|
+
for hh, gids in self.hh_to_res_gids.items()
|
119
|
+
}
|
106
120
|
self._time_index = res.time_index
|
107
121
|
|
108
|
-
logger.debug(
|
109
|
-
|
122
|
+
logger.debug(
|
123
|
+
f"Data took {(time.time() - start_time) / 60:.2f} " f"min to load"
|
124
|
+
)
|
110
125
|
|
111
126
|
def get_preloaded_data_for_gid(self, sc_gid):
|
112
127
|
"""Get the pre-loaded data for a single SC GID.
|
@@ -125,12 +140,14 @@ class BespokeMultiPlantData:
|
|
125
140
|
hh = self.sc_gid_to_hh[sc_gid]
|
126
141
|
sc_point_res_gids = sorted(self.sc_gid_to_res_gid[sc_gid])
|
127
142
|
data_inds = np.searchsorted(self.hh_to_res_gids[hh], sc_point_res_gids)
|
128
|
-
return BespokeSinglePlantData(
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
143
|
+
return BespokeSinglePlantData(
|
144
|
+
sc_point_res_gids,
|
145
|
+
self._wind_dirs[hh][:, data_inds],
|
146
|
+
self._wind_speeds[hh][:, data_inds],
|
147
|
+
self._temps[hh][:, data_inds],
|
148
|
+
self._pressures[hh][:, data_inds],
|
149
|
+
self._time_index,
|
150
|
+
)
|
134
151
|
|
135
152
|
|
136
153
|
class BespokeSinglePlantData:
|
@@ -141,8 +158,9 @@ class BespokeSinglePlantData:
|
|
141
158
|
reads to a single HDF5 file.
|
142
159
|
"""
|
143
160
|
|
144
|
-
def __init__(
|
145
|
-
|
161
|
+
def __init__(
|
162
|
+
self, data_inds, wind_dirs, wind_speeds, temps, pressures, time_index
|
163
|
+
):
|
146
164
|
"""Initialize BespokeSinglePlantData
|
147
165
|
|
148
166
|
Parameters
|
@@ -186,24 +204,44 @@ class BespokeSinglePlantData:
|
|
186
204
|
|
187
205
|
|
188
206
|
class BespokeSinglePlant:
|
189
|
-
"""Framework for analyzing and
|
207
|
+
"""Framework for analyzing and optimizing a wind plant layout specific to
|
190
208
|
the local wind resource and exclusions for a single reV supply curve point.
|
191
209
|
"""
|
192
210
|
|
193
|
-
DEPENDENCIES = (
|
211
|
+
DEPENDENCIES = ("shapely",)
|
194
212
|
OUT_ATTRS = copy.deepcopy(Gen.OUT_ATTRS)
|
195
213
|
|
196
|
-
def __init__(
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
214
|
+
def __init__(
|
215
|
+
self,
|
216
|
+
gid,
|
217
|
+
excl,
|
218
|
+
res,
|
219
|
+
tm_dset,
|
220
|
+
sam_sys_inputs,
|
221
|
+
objective_function,
|
222
|
+
capital_cost_function,
|
223
|
+
fixed_operating_cost_function,
|
224
|
+
variable_operating_cost_function,
|
225
|
+
balance_of_system_cost_function,
|
226
|
+
min_spacing="5x",
|
227
|
+
wake_loss_multiplier=1,
|
228
|
+
ga_kwargs=None,
|
229
|
+
output_request=("system_capacity", "cf_mean"),
|
230
|
+
ws_bins=(0.0, 20.0, 5.0),
|
231
|
+
wd_bins=(0.0, 360.0, 45.0),
|
232
|
+
excl_dict=None,
|
233
|
+
inclusion_mask=None,
|
234
|
+
data_layers=None,
|
235
|
+
resolution=64,
|
236
|
+
excl_area=None,
|
237
|
+
exclusion_shape=None,
|
238
|
+
eos_mult_baseline_cap_mw=200,
|
239
|
+
prior_meta=None,
|
240
|
+
gid_map=None,
|
241
|
+
bias_correct=None,
|
242
|
+
pre_loaded_data=None,
|
243
|
+
close=True,
|
244
|
+
):
|
207
245
|
"""
|
208
246
|
Parameters
|
209
247
|
----------
|
@@ -225,19 +263,33 @@ class BespokeSinglePlant:
|
|
225
263
|
return the objective to be minimized during layout optimization.
|
226
264
|
Variables available are:
|
227
265
|
|
228
|
-
- n_turbines
|
229
|
-
- system_capacity
|
230
|
-
- aep
|
231
|
-
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
-
|
266
|
+
- ``n_turbines``: the number of turbines
|
267
|
+
- ``system_capacity``: wind plant capacity
|
268
|
+
- ``aep``: annual energy production
|
269
|
+
- ``avg_sl_dist_to_center_m``: Average straight-line
|
270
|
+
distance to the supply curve point center from all
|
271
|
+
turbine locations (in m). Useful for computing plant
|
272
|
+
BOS costs.
|
273
|
+
- ``avg_sl_dist_to_medoid_m``: Average straight-line
|
274
|
+
distance to the medoid of all turbine locations
|
275
|
+
(in m). Useful for computing plant BOS costs.
|
276
|
+
- ``nn_conn_dist_m``: Total BOS connection distance
|
277
|
+
using nearest-neighbor connections. This variable is
|
278
|
+
only available for the
|
279
|
+
``balance_of_system_cost_function`` equation.
|
280
|
+
- ``fixed_charge_rate``: user input fixed_charge_rate if
|
281
|
+
included as part of the sam system config.
|
282
|
+
- ``capital_cost``: plant capital cost as evaluated
|
236
283
|
by `capital_cost_function`
|
237
|
-
- fixed_operating_cost
|
238
|
-
evaluated by `fixed_operating_cost_function`
|
239
|
-
- variable_operating_cost
|
240
|
-
as evaluated by
|
284
|
+
- ``fixed_operating_cost``: plant fixed annual operating
|
285
|
+
cost as evaluated by `fixed_operating_cost_function`
|
286
|
+
- ``variable_operating_cost``: plant variable annual
|
287
|
+
operating cost as evaluated by
|
288
|
+
`variable_operating_cost_function`
|
289
|
+
- ``balance_of_system_cost``: plant balance of system
|
290
|
+
cost as evaluated by `balance_of_system_cost_function`
|
291
|
+
- ``self.wind_plant``: the SAM wind plant object,
|
292
|
+
through which all SAM variables can be accessed
|
241
293
|
|
242
294
|
capital_cost_function : str
|
243
295
|
The plant capital cost function as a string, must return the total
|
@@ -250,6 +302,16 @@ class BespokeSinglePlant:
|
|
250
302
|
variable_operating_cost_function : str
|
251
303
|
The plant annual variable operating cost function as a string, must
|
252
304
|
return the variable operating cost in $/kWh. Has access to the same
|
305
|
+
variables as the objective_function. You can set this to "0"
|
306
|
+
to effectively ignore variable operating costs.
|
307
|
+
balance_of_system_cost_function : str
|
308
|
+
The plant balance-of-system cost function as a string, must
|
309
|
+
return the variable operating cost in $. Has access to the
|
310
|
+
same variables as the objective_function. You can set this
|
311
|
+
to "0" to effectively ignore balance-of-system costs.
|
312
|
+
balance_of_system_cost_function : str
|
313
|
+
The plant balance-of-system cost function as a string, must
|
314
|
+
return the variable operating cost in $. Has access to the same
|
253
315
|
variables as the objective_function.
|
254
316
|
min_spacing : float | int | str
|
255
317
|
Minimum spacing between turbines in meters. Can also be a string
|
@@ -352,38 +414,49 @@ class BespokeSinglePlant:
|
|
352
414
|
Flag to close object file handlers on exit.
|
353
415
|
"""
|
354
416
|
|
355
|
-
logger.debug(
|
356
|
-
|
357
|
-
|
358
|
-
logger.debug(
|
359
|
-
logger.debug(
|
360
|
-
logger.debug(
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
logger.debug(
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
417
|
+
logger.debug(
|
418
|
+
"Initializing BespokeSinglePlant for gid {}...".format(gid)
|
419
|
+
)
|
420
|
+
logger.debug("Resource filepath: {}".format(res))
|
421
|
+
logger.debug("Exclusion filepath: {}".format(excl))
|
422
|
+
logger.debug("Exclusion dict: {}".format(excl_dict))
|
423
|
+
logger.debug(
|
424
|
+
"Bespoke objective function: {}".format(objective_function)
|
425
|
+
)
|
426
|
+
logger.debug("Bespoke cost function: {}".format(objective_function))
|
427
|
+
logger.debug(
|
428
|
+
"Bespoke wake loss multiplier: {}".format(wake_loss_multiplier)
|
429
|
+
)
|
430
|
+
logger.debug("Bespoke GA initialization kwargs: {}".format(ga_kwargs))
|
431
|
+
logger.debug(
|
432
|
+
"Bespoke EOS multiplier baseline capacity: {:,} MW".format(
|
433
|
+
eos_mult_baseline_cap_mw
|
434
|
+
)
|
435
|
+
)
|
436
|
+
|
437
|
+
if isinstance(min_spacing, str) and min_spacing.endswith("x"):
|
370
438
|
rotor_diameter = sam_sys_inputs["wind_turbine_rotor_diameter"]
|
371
|
-
min_spacing = float(min_spacing.strip(
|
439
|
+
min_spacing = float(min_spacing.strip("x")) * rotor_diameter
|
372
440
|
|
373
441
|
if not isinstance(min_spacing, (int, float)):
|
374
442
|
try:
|
375
443
|
min_spacing = float(min_spacing)
|
376
444
|
except Exception as e:
|
377
|
-
msg = (
|
378
|
-
|
445
|
+
msg = (
|
446
|
+
"min_spacing must be numeric but received: {}, {}".format(
|
447
|
+
min_spacing, type(min_spacing)
|
448
|
+
)
|
449
|
+
)
|
379
450
|
logger.error(msg)
|
380
451
|
raise TypeError(msg) from e
|
381
452
|
|
382
453
|
self.objective_function = objective_function
|
383
454
|
self.capital_cost_function = capital_cost_function
|
384
455
|
self.fixed_operating_cost_function = fixed_operating_cost_function
|
385
|
-
self.variable_operating_cost_function =
|
456
|
+
self.variable_operating_cost_function = (
|
386
457
|
variable_operating_cost_function
|
458
|
+
)
|
459
|
+
self.balance_of_system_cost_function = balance_of_system_cost_function
|
387
460
|
self.min_spacing = min_spacing
|
388
461
|
self.wake_loss_multiplier = wake_loss_multiplier
|
389
462
|
self.ga_kwargs = ga_kwargs or {}
|
@@ -411,26 +484,33 @@ class BespokeSinglePlant:
|
|
411
484
|
Handler = self.get_wind_handler(res)
|
412
485
|
res = res if not isinstance(res, str) else Handler(res)
|
413
486
|
|
414
|
-
self._sc_point = AggSCPoint(
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
487
|
+
self._sc_point = AggSCPoint(
|
488
|
+
gid,
|
489
|
+
excl,
|
490
|
+
res,
|
491
|
+
tm_dset,
|
492
|
+
excl_dict=excl_dict,
|
493
|
+
inclusion_mask=inclusion_mask,
|
494
|
+
resolution=resolution,
|
495
|
+
excl_area=excl_area,
|
496
|
+
exclusion_shape=exclusion_shape,
|
497
|
+
close=close,
|
498
|
+
)
|
421
499
|
|
422
500
|
self._parse_output_req()
|
423
501
|
self._data_layers = data_layers
|
424
502
|
self._parse_prior_run()
|
425
503
|
|
426
504
|
def __str__(self):
|
427
|
-
s =
|
428
|
-
|
505
|
+
s = "BespokeSinglePlant for reV SC gid {} with resolution {}".format(
|
506
|
+
self.sc_point.gid, self.sc_point.resolution
|
507
|
+
)
|
429
508
|
return s
|
430
509
|
|
431
510
|
def __repr__(self):
|
432
|
-
s =
|
433
|
-
|
511
|
+
s = "BespokeSinglePlant for reV SC gid {} with resolution {}".format(
|
512
|
+
self.sc_point.gid, self.sc_point.resolution
|
513
|
+
)
|
434
514
|
return s
|
435
515
|
|
436
516
|
def __enter__(self):
|
@@ -447,14 +527,14 @@ class BespokeSinglePlant:
|
|
447
527
|
(ws_mean, *_mean) if requested.
|
448
528
|
"""
|
449
529
|
|
450
|
-
required = (
|
530
|
+
required = ("cf_mean", "annual_energy")
|
451
531
|
for req in required:
|
452
532
|
if req not in self._out_req:
|
453
533
|
self._out_req.append(req)
|
454
534
|
|
455
|
-
if
|
456
|
-
self._out_req.remove(
|
457
|
-
self._outputs[
|
535
|
+
if "ws_mean" in self._out_req:
|
536
|
+
self._out_req.remove("ws_mean")
|
537
|
+
self._outputs["ws_mean"] = self.res_df["windspeed"].mean()
|
458
538
|
|
459
539
|
for req in copy.deepcopy(self._out_req):
|
460
540
|
if req in self.res_df:
|
@@ -463,17 +543,20 @@ class BespokeSinglePlant:
|
|
463
543
|
year = annual_ti.year[0]
|
464
544
|
mask = self.res_df.index.isin(annual_ti)
|
465
545
|
arr = self.res_df.loc[mask, req].values.flatten()
|
466
|
-
self._outputs[req + f
|
546
|
+
self._outputs[req + f"-{year}"] = arr
|
467
547
|
|
468
|
-
elif req.replace(
|
548
|
+
elif req.replace("_mean", "") in self.res_df:
|
469
549
|
self._out_req.remove(req)
|
470
|
-
dset = req.replace(
|
550
|
+
dset = req.replace("_mean", "")
|
471
551
|
self._outputs[req] = self.res_df[dset].mean()
|
472
552
|
|
473
|
-
if
|
474
|
-
|
475
|
-
|
476
|
-
|
553
|
+
if "lcoe_fcr" in self._out_req and (
|
554
|
+
"fixed_charge_rate" not in self.original_sam_sys_inputs
|
555
|
+
):
|
556
|
+
msg = (
|
557
|
+
'User requested "lcoe_fcr" but did not input '
|
558
|
+
'"fixed_charge_rate" in the SAM system config.'
|
559
|
+
)
|
477
560
|
logger.error(msg)
|
478
561
|
raise KeyError(msg)
|
479
562
|
|
@@ -482,14 +565,18 @@ class BespokeSinglePlant:
|
|
482
565
|
sure the SAM system inputs are set accordingly."""
|
483
566
|
|
484
567
|
# {meta_column: sam_sys_input_key}
|
485
|
-
required = {
|
486
|
-
|
487
|
-
|
568
|
+
required = {
|
569
|
+
SupplyCurveField.CAPACITY_AC_MW: "system_capacity",
|
570
|
+
SupplyCurveField.TURBINE_X_COORDS: "wind_farm_xCoordinates",
|
571
|
+
SupplyCurveField.TURBINE_Y_COORDS: "wind_farm_yCoordinates",
|
572
|
+
}
|
488
573
|
|
489
574
|
if self._prior_meta:
|
490
575
|
missing = [k for k in required if k not in self.meta]
|
491
|
-
msg = (
|
492
|
-
|
576
|
+
msg = (
|
577
|
+
"Prior bespoke run meta data is missing the following "
|
578
|
+
"required columns: {}".format(missing)
|
579
|
+
)
|
493
580
|
assert not any(missing), msg
|
494
581
|
|
495
582
|
for meta_col, sam_sys_key in required.items():
|
@@ -497,7 +584,7 @@ class BespokeSinglePlant:
|
|
497
584
|
self._sam_sys_inputs[sam_sys_key] = prior_value
|
498
585
|
|
499
586
|
# convert reV supply curve cap in MW to SAM capacity in kW
|
500
|
-
self._sam_sys_inputs[
|
587
|
+
self._sam_sys_inputs["system_capacity"] *= 1e3
|
501
588
|
|
502
589
|
@staticmethod
|
503
590
|
def _parse_gid_map(gid_map):
|
@@ -522,15 +609,22 @@ class BespokeSinglePlant:
|
|
522
609
|
"""
|
523
610
|
|
524
611
|
if isinstance(gid_map, str):
|
525
|
-
if gid_map.endswith(
|
526
|
-
gid_map =
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
612
|
+
if gid_map.endswith(".csv"):
|
613
|
+
gid_map = (
|
614
|
+
pd.read_csv(gid_map)
|
615
|
+
.rename(SupplyCurveField.map_to(ResourceMetaField), axis=1)
|
616
|
+
.to_dict()
|
617
|
+
)
|
618
|
+
err_msg = f"Need {ResourceMetaField.GID} in gid_map column"
|
619
|
+
assert ResourceMetaField.GID in gid_map, err_msg
|
620
|
+
assert "gid_map" in gid_map, 'Need "gid_map" in gid_map column'
|
621
|
+
gid_map = {
|
622
|
+
gid_map[ResourceMetaField.GID][i]: gid_map["gid_map"][i]
|
623
|
+
for i in gid_map[ResourceMetaField.GID]
|
624
|
+
}
|
625
|
+
|
626
|
+
elif gid_map.endswith(".json"):
|
627
|
+
with open(gid_map) as f:
|
534
628
|
gid_map = json.load(f)
|
535
629
|
|
536
630
|
return gid_map
|
@@ -563,19 +657,23 @@ class BespokeSinglePlant:
|
|
563
657
|
Bias corrected windspeed data in same shape as input
|
564
658
|
"""
|
565
659
|
|
566
|
-
if self._bias_correct is not None and dset.startswith(
|
567
|
-
|
660
|
+
if self._bias_correct is not None and dset.startswith("windspeed_"):
|
568
661
|
out = parse_bc_table(self._bias_correct, h5_gids)
|
569
662
|
bc_fun, bc_fun_kwargs, bool_bc = out
|
570
663
|
|
571
664
|
if bool_bc.any():
|
572
|
-
logger.debug(
|
573
|
-
|
665
|
+
logger.debug(
|
666
|
+
"Bias correcting windspeed with function {} "
|
667
|
+
"for h5 gids: {}".format(bc_fun, h5_gids)
|
668
|
+
)
|
574
669
|
|
575
|
-
bc_fun_kwargs[
|
670
|
+
bc_fun_kwargs["ws"] = ws[:, bool_bc]
|
576
671
|
sig = signature(bc_fun)
|
577
|
-
bc_fun_kwargs = {
|
578
|
-
|
672
|
+
bc_fun_kwargs = {
|
673
|
+
k: v
|
674
|
+
for k, v in bc_fun_kwargs.items()
|
675
|
+
if k in sig.parameters
|
676
|
+
}
|
579
677
|
|
580
678
|
ws[:, bool_bc] = bc_fun(**bc_fun_kwargs)
|
581
679
|
|
@@ -631,7 +729,7 @@ class BespokeSinglePlant:
|
|
631
729
|
of degrees from north.
|
632
730
|
"""
|
633
731
|
|
634
|
-
dset = f
|
732
|
+
dset = f"winddirection_{self.hub_height}m"
|
635
733
|
gids = self.sc_point.h5_gid_set
|
636
734
|
h5_gids = copy.deepcopy(gids)
|
637
735
|
if self._gid_map is not None:
|
@@ -736,31 +834,31 @@ class BespokeSinglePlant:
|
|
736
834
|
"""
|
737
835
|
if self._meta is None:
|
738
836
|
res_gids = json.dumps([int(g) for g in self.sc_point.h5_gid_set])
|
739
|
-
gid_counts = json.dumps(
|
740
|
-
|
741
|
-
|
742
|
-
with SupplyCurveExtent(self.sc_point._excl_fpath,
|
743
|
-
resolution=self.sc_point.resolution) as sc:
|
744
|
-
row_ind, col_ind = sc.get_sc_row_col_ind(self.sc_point.gid)
|
837
|
+
gid_counts = json.dumps(
|
838
|
+
[float(np.round(n, 1)) for n in self.sc_point.gid_counts]
|
839
|
+
)
|
745
840
|
|
746
841
|
self._meta = pd.DataFrame(
|
747
|
-
{
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
842
|
+
{
|
843
|
+
"gid": self.sc_point.gid, # needed for collection
|
844
|
+
SupplyCurveField.LATITUDE: self.sc_point.latitude,
|
845
|
+
SupplyCurveField.LONGITUDE: self.sc_point.longitude,
|
846
|
+
SupplyCurveField.COUNTRY: self.sc_point.country,
|
847
|
+
SupplyCurveField.STATE: self.sc_point.state,
|
848
|
+
SupplyCurveField.COUNTY: self.sc_point.county,
|
849
|
+
SupplyCurveField.ELEVATION: self.sc_point.elevation,
|
850
|
+
SupplyCurveField.TIMEZONE: self.sc_point.timezone,
|
851
|
+
SupplyCurveField.SC_POINT_GID: self.sc_point.sc_point_gid,
|
852
|
+
SupplyCurveField.SC_ROW_IND: self.sc_point.sc_row_ind,
|
853
|
+
SupplyCurveField.SC_COL_IND: self.sc_point.sc_col_ind,
|
854
|
+
SupplyCurveField.RES_GIDS: res_gids,
|
855
|
+
SupplyCurveField.GID_COUNTS: gid_counts,
|
856
|
+
SupplyCurveField.N_GIDS: self.sc_point.n_gids,
|
857
|
+
SupplyCurveField.OFFSHORE: self.sc_point.offshore,
|
858
|
+
SupplyCurveField.AREA_SQ_KM: self.sc_point.area,
|
859
|
+
},
|
860
|
+
index=[self.sc_point.gid],
|
861
|
+
)
|
764
862
|
|
765
863
|
return self._meta
|
766
864
|
|
@@ -772,7 +870,7 @@ class BespokeSinglePlant:
|
|
772
870
|
-------
|
773
871
|
int
|
774
872
|
"""
|
775
|
-
return int(self.sam_sys_inputs[
|
873
|
+
return int(self.sam_sys_inputs["wind_turbine_hub_ht"])
|
776
874
|
|
777
875
|
@property
|
778
876
|
def res_df(self):
|
@@ -792,21 +890,26 @@ class BespokeSinglePlant:
|
|
792
890
|
ti = self._pre_loaded_data.time_index
|
793
891
|
|
794
892
|
wd = self.get_weighted_res_dir()
|
795
|
-
ws = self.get_weighted_res_ts(f
|
796
|
-
temp = self.get_weighted_res_ts(f
|
797
|
-
pres = self.get_weighted_res_ts(f
|
893
|
+
ws = self.get_weighted_res_ts(f"windspeed_{self.hub_height}m")
|
894
|
+
temp = self.get_weighted_res_ts(f"temperature_{self.hub_height}m")
|
895
|
+
pres = self.get_weighted_res_ts(f"pressure_{self.hub_height}m")
|
798
896
|
|
799
897
|
# convert mbar to atm
|
800
898
|
if np.nanmax(pres) > 1000:
|
801
899
|
pres *= 9.86923e-6
|
802
900
|
|
803
|
-
self._res_df = pd.DataFrame(
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
901
|
+
self._res_df = pd.DataFrame(
|
902
|
+
{
|
903
|
+
"temperature": temp,
|
904
|
+
"pressure": pres,
|
905
|
+
"windspeed": ws,
|
906
|
+
"winddirection": wd,
|
907
|
+
},
|
908
|
+
index=ti,
|
909
|
+
)
|
910
|
+
|
911
|
+
if "time_index_step" in self.original_sam_sys_inputs:
|
912
|
+
ti_step = self.original_sam_sys_inputs["time_index_step"]
|
810
913
|
self._res_df = self._res_df.iloc[::ti_step]
|
811
914
|
|
812
915
|
return self._res_df
|
@@ -857,9 +960,11 @@ class BespokeSinglePlant:
|
|
857
960
|
ws_bins = JointPD._make_bins(*self._ws_bins)
|
858
961
|
wd_bins = JointPD._make_bins(*self._wd_bins)
|
859
962
|
|
860
|
-
hist_out = np.histogram2d(
|
861
|
-
|
862
|
-
|
963
|
+
hist_out = np.histogram2d(
|
964
|
+
self.res_df["windspeed"],
|
965
|
+
self.res_df["winddirection"],
|
966
|
+
bins=(ws_bins, wd_bins),
|
967
|
+
)
|
863
968
|
self._wind_dist, self._ws_edges, self._wd_edges = hist_out
|
864
969
|
self._wind_dist /= self._wind_dist.sum()
|
865
970
|
|
@@ -880,19 +985,20 @@ class BespokeSinglePlant:
|
|
880
985
|
res_df = self.res_df[(self.res_df.index.year == year)]
|
881
986
|
sam_inputs = copy.deepcopy(self.sam_sys_inputs)
|
882
987
|
|
883
|
-
if
|
988
|
+
if "lcoe_fcr" in self._out_req:
|
884
989
|
lcoe_kwargs = self.get_lcoe_kwargs()
|
885
990
|
sam_inputs.update(lcoe_kwargs)
|
886
991
|
|
887
|
-
i_wp = WindPower(
|
888
|
-
|
992
|
+
i_wp = WindPower(
|
993
|
+
res_df, self.meta, sam_inputs, output_request=self._out_req
|
994
|
+
)
|
889
995
|
wind_plant_ts[year] = i_wp
|
890
996
|
|
891
997
|
return wind_plant_ts
|
892
998
|
|
893
999
|
@property
|
894
1000
|
def wind_plant_pd(self):
|
895
|
-
"""
|
1001
|
+
"""ReV WindPowerPD compute object for plant layout optimization based
|
896
1002
|
on wind joint probability distribution
|
897
1003
|
|
898
1004
|
Returns
|
@@ -902,14 +1008,19 @@ class BespokeSinglePlant:
|
|
902
1008
|
|
903
1009
|
if self._wind_plant_pd is None:
|
904
1010
|
wind_dist, ws_edges, wd_edges = self.wind_dist
|
905
|
-
self._wind_plant_pd = WindPowerPD(
|
906
|
-
|
907
|
-
|
1011
|
+
self._wind_plant_pd = WindPowerPD(
|
1012
|
+
ws_edges,
|
1013
|
+
wd_edges,
|
1014
|
+
wind_dist,
|
1015
|
+
self.meta,
|
1016
|
+
self.sam_sys_inputs,
|
1017
|
+
output_request=self._out_req,
|
1018
|
+
)
|
908
1019
|
return self._wind_plant_pd
|
909
1020
|
|
910
1021
|
@property
|
911
1022
|
def wind_plant_ts(self):
|
912
|
-
"""
|
1023
|
+
"""ReV WindPower compute object(s) based on wind resource timeseries
|
913
1024
|
data keyed by year
|
914
1025
|
|
915
1026
|
Returns
|
@@ -929,16 +1040,19 @@ class BespokeSinglePlant:
|
|
929
1040
|
if self._plant_optm is None:
|
930
1041
|
# put import here to delay breaking due to special dependencies
|
931
1042
|
from reV.bespoke.place_turbines import PlaceTurbines
|
1043
|
+
|
932
1044
|
self._plant_optm = PlaceTurbines(
|
933
1045
|
self.wind_plant_pd,
|
934
1046
|
self.objective_function,
|
935
1047
|
self.capital_cost_function,
|
936
1048
|
self.fixed_operating_cost_function,
|
937
1049
|
self.variable_operating_cost_function,
|
1050
|
+
self.balance_of_system_cost_function,
|
938
1051
|
self.include_mask,
|
939
1052
|
self.pixel_side_length,
|
940
1053
|
self.min_spacing,
|
941
|
-
self.wake_loss_multiplier
|
1054
|
+
self.wake_loss_multiplier,
|
1055
|
+
)
|
942
1056
|
|
943
1057
|
return self._plant_optm
|
944
1058
|
|
@@ -946,22 +1060,24 @@ class BespokeSinglePlant:
|
|
946
1060
|
"""Recalculate the multi-year mean LCOE based on the multi-year mean
|
947
1061
|
annual energy production (AEP)"""
|
948
1062
|
|
949
|
-
if
|
1063
|
+
if "lcoe_fcr-means" in self.outputs:
|
950
1064
|
lcoe_kwargs = self.get_lcoe_kwargs()
|
951
1065
|
|
952
|
-
logger.debug(
|
953
|
-
|
1066
|
+
logger.debug(
|
1067
|
+
"Recalulating multi-year mean LCOE using "
|
1068
|
+
"multi-year mean AEP."
|
1069
|
+
)
|
954
1070
|
|
955
1071
|
fcr = lcoe_kwargs['fixed_charge_rate']
|
956
|
-
|
1072
|
+
cc = lcoe_kwargs['capital_cost']
|
957
1073
|
foc = lcoe_kwargs['fixed_operating_cost']
|
958
1074
|
voc = lcoe_kwargs['variable_operating_cost']
|
959
1075
|
aep = self.outputs['annual_energy-means']
|
960
1076
|
|
961
|
-
my_mean_lcoe = lcoe_fcr(fcr,
|
1077
|
+
my_mean_lcoe = lcoe_fcr(fcr, cc, foc, aep, voc)
|
962
1078
|
|
963
|
-
self._outputs[
|
964
|
-
self._meta[
|
1079
|
+
self._outputs["lcoe_fcr-means"] = my_mean_lcoe
|
1080
|
+
self._meta[SupplyCurveField.MEAN_LCOE] = my_mean_lcoe
|
965
1081
|
|
966
1082
|
def get_lcoe_kwargs(self):
|
967
1083
|
"""Get a namespace of arguments for calculating LCOE based on the
|
@@ -974,16 +1090,28 @@ class BespokeSinglePlant:
|
|
974
1090
|
sam_sys_inputs, normalized to the original system_capacity, and
|
975
1091
|
updated based on the bespoke optimized system_capacity, includes
|
976
1092
|
fixed_charge_rate, system_capacity (kW), capital_cost ($),
|
977
|
-
fixed_operating_cos ($), variable_operating_cost ($/kWh)
|
978
|
-
Data source priority: outputs,
|
979
|
-
original_sam_sys_inputs, meta
|
1093
|
+
fixed_operating_cos ($), variable_operating_cost ($/kWh),
|
1094
|
+
balance_of_system_cost ($). Data source priority: outputs,
|
1095
|
+
plant_optimizer, original_sam_sys_inputs, meta
|
980
1096
|
"""
|
981
1097
|
|
982
|
-
|
983
|
-
|
1098
|
+
kwargs_map = {
|
1099
|
+
"fixed_charge_rate": SupplyCurveField.FIXED_CHARGE_RATE,
|
1100
|
+
"system_capacity": SupplyCurveField.CAPACITY_AC_MW,
|
1101
|
+
"capital_cost": SupplyCurveField.BESPOKE_CAPITAL_COST,
|
1102
|
+
"fixed_operating_cost": (
|
1103
|
+
SupplyCurveField.BESPOKE_FIXED_OPERATING_COST
|
1104
|
+
),
|
1105
|
+
"variable_operating_cost": (
|
1106
|
+
SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST
|
1107
|
+
),
|
1108
|
+
"balance_of_system_cost": (
|
1109
|
+
SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST
|
1110
|
+
),
|
1111
|
+
}
|
984
1112
|
lcoe_kwargs = {}
|
985
1113
|
|
986
|
-
for kwarg in
|
1114
|
+
for kwarg, meta_field in kwargs_map.items():
|
987
1115
|
if kwarg in self.outputs:
|
988
1116
|
lcoe_kwargs[kwarg] = self.outputs[kwarg]
|
989
1117
|
elif getattr(self.plant_optimizer, kwarg, None) is not None:
|
@@ -993,18 +1121,25 @@ class BespokeSinglePlant:
|
|
993
1121
|
elif kwarg in self.meta:
|
994
1122
|
value = float(self.meta[kwarg].values[0])
|
995
1123
|
lcoe_kwargs[kwarg] = value
|
1124
|
+
elif meta_field in self.meta:
|
1125
|
+
value = float(self.meta[meta_field].values[0])
|
1126
|
+
if meta_field == SupplyCurveField.CAPACITY_AC_MW:
|
1127
|
+
value *= 1000 # MW to kW
|
1128
|
+
lcoe_kwargs[kwarg] = value
|
996
1129
|
|
997
|
-
for k
|
998
|
-
self._meta[k] = v
|
999
|
-
|
1000
|
-
missing = [k for k in kwargs_list if k not in lcoe_kwargs]
|
1130
|
+
missing = [k for k in kwargs_map if k not in lcoe_kwargs]
|
1001
1131
|
if any(missing):
|
1002
|
-
msg = (
|
1003
|
-
|
1004
|
-
|
1132
|
+
msg = (
|
1133
|
+
"Could not find these LCOE kwargs in outputs, "
|
1134
|
+
"plant_optimizer, original_sam_sys_inputs, or meta: {}".format(
|
1135
|
+
missing
|
1136
|
+
)
|
1137
|
+
)
|
1005
1138
|
logger.error(msg)
|
1006
1139
|
raise KeyError(msg)
|
1007
1140
|
|
1141
|
+
bos = lcoe_kwargs.pop("balance_of_system_cost")
|
1142
|
+
lcoe_kwargs["capital_cost"] = lcoe_kwargs["capital_cost"] + bos
|
1008
1143
|
return lcoe_kwargs
|
1009
1144
|
|
1010
1145
|
@staticmethod
|
@@ -1024,7 +1159,7 @@ class BespokeSinglePlant:
|
|
1024
1159
|
"""
|
1025
1160
|
handler = res
|
1026
1161
|
if isinstance(res, str):
|
1027
|
-
if
|
1162
|
+
if "*" in res:
|
1028
1163
|
handler = MultiYearWindResource
|
1029
1164
|
else:
|
1030
1165
|
handler = WindResource
|
@@ -1042,9 +1177,11 @@ class BespokeSinglePlant:
|
|
1042
1177
|
missing.append(name)
|
1043
1178
|
|
1044
1179
|
if any(missing):
|
1045
|
-
msg = (
|
1046
|
-
|
1047
|
-
|
1180
|
+
msg = (
|
1181
|
+
"The reV bespoke module depends on the following special "
|
1182
|
+
"dependencies that were not found in the active "
|
1183
|
+
"environment: {}".format(missing)
|
1184
|
+
)
|
1048
1185
|
logger.error(msg)
|
1049
1186
|
raise ModuleNotFoundError(msg)
|
1050
1187
|
|
@@ -1056,7 +1193,11 @@ class BespokeSinglePlant:
|
|
1056
1193
|
'hourly',
|
1057
1194
|
'capital_cost',
|
1058
1195
|
'fixed_operating_cost',
|
1059
|
-
'variable_operating_cost'
|
1196
|
+
'variable_operating_cost',
|
1197
|
+
'balance_of_system_cost',
|
1198
|
+
'base_capital_cost',
|
1199
|
+
'base_fixed_operating_cost',
|
1200
|
+
'base_variable_operating_cost')):
|
1060
1201
|
"""Check two reV-SAM models for matching system inputs.
|
1061
1202
|
|
1062
1203
|
Parameters
|
@@ -1066,13 +1207,13 @@ class BespokeSinglePlant:
|
|
1066
1207
|
"""
|
1067
1208
|
bad = []
|
1068
1209
|
for k, v in plant1.sam_sys_inputs.items():
|
1069
|
-
if k not in plant2.sam_sys_inputs
|
1070
|
-
|
1071
|
-
|
1210
|
+
if k not in plant2.sam_sys_inputs or str(v) != str(
|
1211
|
+
plant2.sam_sys_inputs[k]
|
1212
|
+
):
|
1072
1213
|
bad.append(k)
|
1073
1214
|
bad = [b for b in bad if b not in ignore]
|
1074
1215
|
if any(bad):
|
1075
|
-
msg =
|
1216
|
+
msg = "Inputs no longer match: {}".format(bad)
|
1076
1217
|
logger.error(msg)
|
1077
1218
|
raise RuntimeError(msg)
|
1078
1219
|
|
@@ -1088,41 +1229,60 @@ class BespokeSinglePlant:
|
|
1088
1229
|
BespokeSinglePlant.outputs property.
|
1089
1230
|
"""
|
1090
1231
|
|
1091
|
-
logger.debug(
|
1092
|
-
|
1232
|
+
logger.debug(
|
1233
|
+
"Running {} years of SAM timeseries analysis for {}".format(
|
1234
|
+
len(self.years), self
|
1235
|
+
)
|
1236
|
+
)
|
1093
1237
|
self._wind_plant_ts = self.initialize_wind_plant_ts()
|
1094
1238
|
for year, plant in self.wind_plant_ts.items():
|
1095
1239
|
self._check_sys_inputs(plant, self.wind_plant_pd)
|
1096
1240
|
try:
|
1097
1241
|
plant.run_gen_and_econ()
|
1098
1242
|
except Exception as e:
|
1099
|
-
msg = (
|
1100
|
-
|
1243
|
+
msg = (
|
1244
|
+
"{} failed while trying to run SAM WindPower "
|
1245
|
+
"timeseries analysis for {}".format(self, year)
|
1246
|
+
)
|
1101
1247
|
logger.exception(msg)
|
1102
1248
|
raise RuntimeError(msg) from e
|
1103
1249
|
|
1104
1250
|
for k, v in plant.outputs.items():
|
1105
|
-
self._outputs[k +
|
1251
|
+
self._outputs[k + "-{}".format(year)] = v
|
1106
1252
|
|
1107
1253
|
means = {}
|
1108
1254
|
for k1, v1 in self._outputs.items():
|
1109
|
-
if isinstance(v1, Number) and parse_year(k1, option=
|
1255
|
+
if isinstance(v1, Number) and parse_year(k1, option="boolean"):
|
1110
1256
|
year = parse_year(k1)
|
1111
|
-
base_str = k1.replace(str(year),
|
1112
|
-
all_values = [
|
1113
|
-
|
1114
|
-
|
1257
|
+
base_str = k1.replace(str(year), "")
|
1258
|
+
all_values = [
|
1259
|
+
v2 for k2, v2 in self._outputs.items() if base_str in k2
|
1260
|
+
]
|
1261
|
+
means[base_str + "means"] = np.mean(all_values)
|
1115
1262
|
|
1116
1263
|
self._outputs.update(means)
|
1117
1264
|
|
1265
|
+
self._meta[SupplyCurveField.MEAN_RES] = self.res_df["windspeed"].mean()
|
1266
|
+
self._meta[SupplyCurveField.MEAN_CF_DC] = None
|
1267
|
+
self._meta[SupplyCurveField.MEAN_CF_AC] = None
|
1268
|
+
self._meta[SupplyCurveField.MEAN_LCOE] = None
|
1269
|
+
self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = None
|
1118
1270
|
# copy dataset outputs to meta data for supply curve table summary
|
1119
|
-
if
|
1120
|
-
self._meta.loc[:,
|
1121
|
-
|
1122
|
-
|
1271
|
+
if "cf_mean-means" in self.outputs:
|
1272
|
+
self._meta.loc[:, SupplyCurveField.MEAN_CF_AC] = self.outputs[
|
1273
|
+
"cf_mean-means"
|
1274
|
+
]
|
1275
|
+
if "lcoe_fcr-means" in self.outputs:
|
1276
|
+
self._meta.loc[:, SupplyCurveField.MEAN_LCOE] = self.outputs[
|
1277
|
+
"lcoe_fcr-means"
|
1278
|
+
]
|
1123
1279
|
self.recalc_lcoe()
|
1280
|
+
if "annual_energy-means" in self.outputs:
|
1281
|
+
self._meta[SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW] = (
|
1282
|
+
self.outputs["annual_energy-means"] / 1000
|
1283
|
+
)
|
1124
1284
|
|
1125
|
-
logger.debug(
|
1285
|
+
logger.debug("Timeseries analysis complete!")
|
1126
1286
|
|
1127
1287
|
return self.outputs
|
1128
1288
|
|
@@ -1138,19 +1298,23 @@ class BespokeSinglePlant:
|
|
1138
1298
|
BespokeSinglePlant.outputs property.
|
1139
1299
|
"""
|
1140
1300
|
|
1141
|
-
logger.debug(
|
1301
|
+
logger.debug("Running plant layout optimization for {}".format(self))
|
1142
1302
|
try:
|
1143
1303
|
self.plant_optimizer.place_turbines(**self.ga_kwargs)
|
1144
1304
|
except Exception as e:
|
1145
|
-
msg = (
|
1146
|
-
|
1147
|
-
|
1305
|
+
msg = (
|
1306
|
+
"{} failed while trying to run the "
|
1307
|
+
"turbine placement optimizer".format(self)
|
1308
|
+
)
|
1148
1309
|
logger.exception(msg)
|
1149
1310
|
raise RuntimeError(msg) from e
|
1150
1311
|
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1312
|
+
self._outputs["full_polygons"] = self.plant_optimizer.full_polygons
|
1313
|
+
self._outputs["packing_polygons"] = (
|
1314
|
+
self.plant_optimizer.packing_polygons
|
1315
|
+
)
|
1316
|
+
system_capacity_kw = self.plant_optimizer.capacity
|
1317
|
+
self._outputs["system_capacity"] = system_capacity_kw
|
1154
1318
|
|
1155
1319
|
txc = [int(np.round(c)) for c in self.plant_optimizer.turbine_x]
|
1156
1320
|
tyc = [int(np.round(c)) for c in self.plant_optimizer.turbine_y]
|
@@ -1162,62 +1326,110 @@ class BespokeSinglePlant:
|
|
1162
1326
|
pxc = json.dumps(pxc)
|
1163
1327
|
pyc = json.dumps(pyc)
|
1164
1328
|
|
1165
|
-
self._meta[
|
1166
|
-
self._meta[
|
1167
|
-
self._meta[
|
1168
|
-
self._meta[
|
1169
|
-
|
1170
|
-
self.
|
1171
|
-
self.
|
1172
|
-
self.plant_optimizer.
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
self._meta["
|
1178
|
-
self._meta[
|
1329
|
+
self._meta[SupplyCurveField.TURBINE_X_COORDS] = txc
|
1330
|
+
self._meta[SupplyCurveField.TURBINE_Y_COORDS] = tyc
|
1331
|
+
self._meta[SupplyCurveField.POSSIBLE_X_COORDS] = pxc
|
1332
|
+
self._meta[SupplyCurveField.POSSIBLE_Y_COORDS] = pyc
|
1333
|
+
|
1334
|
+
self._meta[SupplyCurveField.N_TURBINES] = self.plant_optimizer.nturbs
|
1335
|
+
self._meta["avg_sl_dist_to_center_m"] = (
|
1336
|
+
self.plant_optimizer.avg_sl_dist_to_center_m
|
1337
|
+
)
|
1338
|
+
self._meta["avg_sl_dist_to_medoid_m"] = (
|
1339
|
+
self.plant_optimizer.avg_sl_dist_to_medoid_m
|
1340
|
+
)
|
1341
|
+
self._meta["nn_conn_dist_m"] = self.plant_optimizer.nn_conn_dist_m
|
1342
|
+
self._meta[SupplyCurveField.BESPOKE_AEP] = self.plant_optimizer.aep
|
1343
|
+
self._meta[SupplyCurveField.BESPOKE_OBJECTIVE] = (
|
1344
|
+
self.plant_optimizer.objective
|
1345
|
+
)
|
1346
|
+
self._meta[SupplyCurveField.BESPOKE_CAPITAL_COST] = (
|
1179
1347
|
self.plant_optimizer.capital_cost
|
1180
|
-
|
1348
|
+
)
|
1349
|
+
self._meta[SupplyCurveField.BESPOKE_FIXED_OPERATING_COST] = (
|
1181
1350
|
self.plant_optimizer.fixed_operating_cost
|
1182
|
-
|
1351
|
+
)
|
1352
|
+
self._meta[SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST] = (
|
1183
1353
|
self.plant_optimizer.variable_operating_cost
|
1184
|
-
|
1185
|
-
self._meta[
|
1354
|
+
)
|
1355
|
+
self._meta[SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST] = (
|
1356
|
+
self.plant_optimizer.balance_of_system_cost
|
1357
|
+
)
|
1358
|
+
self._meta[SupplyCurveField.INCLUDED_AREA] = self.plant_optimizer.area
|
1359
|
+
self._meta[SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY] = (
|
1186
1360
|
self.plant_optimizer.capacity_density
|
1187
|
-
|
1361
|
+
)
|
1362
|
+
self._meta[SupplyCurveField.CONVEX_HULL_AREA] = (
|
1188
1363
|
self.plant_optimizer.convex_hull_area
|
1189
|
-
|
1364
|
+
)
|
1365
|
+
self._meta[SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY] = (
|
1190
1366
|
self.plant_optimizer.convex_hull_capacity_density
|
1191
|
-
|
1367
|
+
)
|
1368
|
+
self._meta[SupplyCurveField.FULL_CELL_CAPACITY_DENSITY] = (
|
1192
1369
|
self.plant_optimizer.full_cell_capacity_density
|
1193
|
-
|
1194
|
-
logger.debug('Plant layout optimization complete!')
|
1370
|
+
)
|
1195
1371
|
|
1196
1372
|
# copy dataset outputs to meta data for supply curve table summary
|
1197
1373
|
# convert SAM system capacity in kW to reV supply curve cap in MW
|
1198
|
-
|
1374
|
+
capacity_ac_mw = system_capacity_kw / 1e3
|
1375
|
+
self._meta[SupplyCurveField.CAPACITY_AC_MW] = capacity_ac_mw
|
1376
|
+
self._meta[SupplyCurveField.CAPACITY_DC_MW] = None
|
1199
1377
|
|
1200
1378
|
# add required ReEDS multipliers to meta
|
1201
1379
|
baseline_cost = self.plant_optimizer.capital_cost_per_kw(
|
1202
|
-
capacity_mw=self._baseline_cap_mw
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1380
|
+
capacity_mw=self._baseline_cap_mw
|
1381
|
+
)
|
1382
|
+
eos_mult = (self.plant_optimizer.capital_cost
|
1383
|
+
/ self.plant_optimizer.capacity
|
1384
|
+
/ baseline_cost)
|
1385
|
+
reg_mult = self.sam_sys_inputs.get("capital_cost_multiplier", 1)
|
1208
1386
|
|
1387
|
+
self._meta[SupplyCurveField.EOS_MULT] = eos_mult
|
1388
|
+
self._meta[SupplyCurveField.REG_MULT] = reg_mult
|
1389
|
+
|
1390
|
+
cap_cost = (
|
1391
|
+
self.plant_optimizer.capital_cost
|
1392
|
+
+ self.plant_optimizer.balance_of_system_cost
|
1393
|
+
)
|
1394
|
+
self._meta[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW] = (
|
1395
|
+
cap_cost / capacity_ac_mw
|
1396
|
+
)
|
1397
|
+
self._meta[SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW] = (
|
1398
|
+
cap_cost / eos_mult / reg_mult / capacity_ac_mw
|
1399
|
+
)
|
1400
|
+
self._meta[SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW] = (
|
1401
|
+
self.plant_optimizer.fixed_operating_cost / capacity_ac_mw
|
1402
|
+
)
|
1403
|
+
self._meta[SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW] = (
|
1404
|
+
self.plant_optimizer.fixed_operating_cost / capacity_ac_mw
|
1405
|
+
)
|
1406
|
+
self._meta[SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW] = (
|
1407
|
+
self.plant_optimizer.variable_operating_cost / capacity_ac_mw
|
1408
|
+
)
|
1409
|
+
self._meta[SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW] = (
|
1410
|
+
self.plant_optimizer.variable_operating_cost / capacity_ac_mw
|
1411
|
+
)
|
1412
|
+
self._meta[SupplyCurveField.FIXED_CHARGE_RATE] = (
|
1413
|
+
self.plant_optimizer.fixed_charge_rate
|
1414
|
+
)
|
1415
|
+
|
1416
|
+
logger.debug("Plant layout optimization complete!")
|
1209
1417
|
return self.outputs
|
1210
1418
|
|
1211
1419
|
def agg_data_layers(self):
|
1212
1420
|
"""Aggregate optional data layers if requested and save to self.meta"""
|
1213
1421
|
if self._data_layers is not None:
|
1214
|
-
logger.debug(
|
1215
|
-
|
1422
|
+
logger.debug(
|
1423
|
+
"Aggregating {} extra data layers.".format(
|
1424
|
+
len(self._data_layers)
|
1425
|
+
)
|
1426
|
+
)
|
1216
1427
|
point_summary = self.meta.to_dict()
|
1217
|
-
point_summary = self.sc_point.agg_data_layers(
|
1218
|
-
|
1428
|
+
point_summary = self.sc_point.agg_data_layers(
|
1429
|
+
point_summary, self._data_layers
|
1430
|
+
)
|
1219
1431
|
self._meta = pd.DataFrame(point_summary)
|
1220
|
-
logger.debug(
|
1432
|
+
logger.debug("Finished aggregating extra data layers.")
|
1221
1433
|
|
1222
1434
|
@property
|
1223
1435
|
def outputs(self):
|
@@ -1246,9 +1458,10 @@ class BespokeSinglePlant:
|
|
1246
1458
|
|
1247
1459
|
with cls(*args, **kwargs) as bsp:
|
1248
1460
|
if bsp._prior_meta:
|
1249
|
-
logger.debug(
|
1250
|
-
|
1251
|
-
|
1461
|
+
logger.debug(
|
1462
|
+
"Skipping bespoke plant optimization for gid {}. "
|
1463
|
+
"Received prior meta data for this point.".format(bsp.gid)
|
1464
|
+
)
|
1252
1465
|
else:
|
1253
1466
|
_ = bsp.run_plant_optimization()
|
1254
1467
|
|
@@ -1257,9 +1470,9 @@ class BespokeSinglePlant:
|
|
1257
1470
|
|
1258
1471
|
meta = bsp.meta
|
1259
1472
|
out = bsp.outputs
|
1260
|
-
out[
|
1473
|
+
out["meta"] = meta
|
1261
1474
|
for year, ti in zip(bsp.years, bsp.annual_time_indexes):
|
1262
|
-
out[
|
1475
|
+
out["time_index-{}".format(year)] = ti
|
1263
1476
|
|
1264
1477
|
return out
|
1265
1478
|
|
@@ -1269,7 +1482,8 @@ class BespokeWindPlants(BaseAggregation):
|
|
1269
1482
|
|
1270
1483
|
def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function,
|
1271
1484
|
capital_cost_function, fixed_operating_cost_function,
|
1272
|
-
variable_operating_cost_function,
|
1485
|
+
variable_operating_cost_function,
|
1486
|
+
balance_of_system_cost_function, project_points,
|
1273
1487
|
sam_files, min_spacing='5x', wake_loss_multiplier=1,
|
1274
1488
|
ga_kwargs=None, output_request=('system_capacity', 'cf_mean'),
|
1275
1489
|
ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
|
@@ -1349,17 +1563,30 @@ class BespokeWindPlants(BaseAggregation):
|
|
1349
1563
|
- ``n_turbines``: the number of turbines
|
1350
1564
|
- ``system_capacity``: wind plant capacity
|
1351
1565
|
- ``aep``: annual energy production
|
1566
|
+
- ``avg_sl_dist_to_center_m``: Average straight-line
|
1567
|
+
distance to the supply curve point center from all
|
1568
|
+
turbine locations (in m). Useful for computing plant
|
1569
|
+
BOS costs.
|
1570
|
+
- ``avg_sl_dist_to_medoid_m``: Average straight-line
|
1571
|
+
distance to the medoid of all turbine locations
|
1572
|
+
(in m). Useful for computing plant BOS costs.
|
1573
|
+
- ``nn_conn_dist_m``: Total BOS connection distance
|
1574
|
+
using nearest-neighbor connections. This variable is
|
1575
|
+
only available for the
|
1576
|
+
``balance_of_system_cost_function`` equation.
|
1352
1577
|
- ``fixed_charge_rate``: user input fixed_charge_rate if
|
1353
1578
|
included as part of the sam system config.
|
1354
|
-
- ``self.wind_plant``: the SAM wind plant object,
|
1355
|
-
through which all SAM variables can be accessed
|
1356
1579
|
- ``capital_cost``: plant capital cost as evaluated
|
1357
1580
|
by `capital_cost_function`
|
1358
1581
|
- ``fixed_operating_cost``: plant fixed annual operating
|
1359
1582
|
cost as evaluated by `fixed_operating_cost_function`
|
1360
1583
|
- ``variable_operating_cost``: plant variable annual
|
1361
|
-
operating cost
|
1584
|
+
operating cost as evaluated by
|
1362
1585
|
`variable_operating_cost_function`
|
1586
|
+
- ``balance_of_system_cost``: plant balance of system
|
1587
|
+
cost as evaluated by `balance_of_system_cost_function`
|
1588
|
+
- ``self.wind_plant``: the SAM wind plant object,
|
1589
|
+
through which all SAM variables can be accessed
|
1363
1590
|
|
1364
1591
|
capital_cost_function : str
|
1365
1592
|
The plant capital cost function written out as a string.
|
@@ -1376,6 +1603,13 @@ class BespokeWindPlants(BaseAggregation):
|
|
1376
1603
|
out as a string. This expression must return the variable
|
1377
1604
|
operating cost in $/kWh. This expression has access to the
|
1378
1605
|
same variables as the `objective_function` argument above.
|
1606
|
+
You can set this to "0" to effectively ignore variable
|
1607
|
+
operating costs.
|
1608
|
+
balance_of_system_cost_function : str
|
1609
|
+
The plant balance-of-system cost function as a string, must
|
1610
|
+
return the variable operating cost in $. Has access to the
|
1611
|
+
same variables as the objective_function. You can set this
|
1612
|
+
to "0" to effectively ignore balance-of-system costs.
|
1379
1613
|
project_points : int | list | tuple | str | dict | pd.DataFrame | slice
|
1380
1614
|
Input specifying which sites to process. A single integer
|
1381
1615
|
representing the supply curve GID of a site may be specified
|
@@ -1406,11 +1640,13 @@ class BespokeWindPlants(BaseAggregation):
|
|
1406
1640
|
- ``capital_cost_multiplier``
|
1407
1641
|
- ``fixed_operating_cost_multiplier``
|
1408
1642
|
- ``variable_operating_cost_multiplier``
|
1643
|
+
- ``balance_of_system_cost_multiplier``
|
1409
1644
|
|
1410
1645
|
These particular inputs are treated as multipliers to be
|
1411
1646
|
applied to the respective cost curves
|
1412
1647
|
(`capital_cost_function`, `fixed_operating_cost_function`,
|
1413
|
-
|
1648
|
+
`variable_operating_cost_function`, and
|
1649
|
+
`balance_of_system_cost_function`) both during and
|
1414
1650
|
after the optimization. A DataFrame following the same
|
1415
1651
|
guidelines as the CSV input (or a dictionary that can be
|
1416
1652
|
used to initialize such a DataFrame) may be used for this
|
@@ -1697,33 +1933,46 @@ class BespokeWindPlants(BaseAggregation):
|
|
1697
1933
|
.format(fixed_operating_cost_function))
|
1698
1934
|
logger.info('Bespoke variable operating cost function: {}'
|
1699
1935
|
.format(variable_operating_cost_function))
|
1936
|
+
logger.info('Bespoke balance of system cost function: {}'
|
1937
|
+
.format(balance_of_system_cost_function))
|
1700
1938
|
logger.info('Bespoke wake loss multiplier: {}'
|
1701
1939
|
.format(wake_loss_multiplier))
|
1702
1940
|
logger.info('Bespoke GA initialization kwargs: {}'.format(ga_kwargs))
|
1703
1941
|
|
1704
|
-
logger.info(
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
logger.info(
|
1710
|
-
|
1942
|
+
logger.info(
|
1943
|
+
"Bespoke pre-extracting exclusions: {}".format(
|
1944
|
+
pre_extract_inclusions
|
1945
|
+
)
|
1946
|
+
)
|
1947
|
+
logger.info(
|
1948
|
+
"Bespoke pre-extracting resource data: {}".format(pre_load_data)
|
1949
|
+
)
|
1950
|
+
logger.info("Bespoke prior run: {}".format(prior_run))
|
1951
|
+
logger.info("Bespoke GID map: {}".format(gid_map))
|
1952
|
+
logger.info("Bespoke bias correction table: {}".format(bias_correct))
|
1711
1953
|
|
1712
1954
|
BespokeSinglePlant.check_dependencies()
|
1713
1955
|
|
1714
1956
|
self._project_points = self._parse_points(project_points, sam_files)
|
1715
1957
|
|
1716
|
-
super().__init__(
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1958
|
+
super().__init__(
|
1959
|
+
excl_fpath,
|
1960
|
+
tm_dset,
|
1961
|
+
excl_dict=excl_dict,
|
1962
|
+
area_filter_kernel=area_filter_kernel,
|
1963
|
+
min_area=min_area,
|
1964
|
+
resolution=resolution,
|
1965
|
+
excl_area=excl_area,
|
1966
|
+
gids=self._project_points.gids,
|
1967
|
+
pre_extract_inclusions=pre_extract_inclusions,
|
1968
|
+
)
|
1721
1969
|
|
1722
1970
|
self._res_fpath = res_fpath
|
1723
1971
|
self._obj_fun = objective_function
|
1724
1972
|
self._cap_cost_fun = capital_cost_function
|
1725
1973
|
self._foc_fun = fixed_operating_cost_function
|
1726
1974
|
self._voc_fun = variable_operating_cost_function
|
1975
|
+
self._bos_fun = balance_of_system_cost_function
|
1727
1976
|
self._min_spacing = min_spacing
|
1728
1977
|
self._wake_loss_multiplier = wake_loss_multiplier
|
1729
1978
|
self._ga_kwargs = ga_kwargs or {}
|
@@ -1742,8 +1991,11 @@ class BespokeWindPlants(BaseAggregation):
|
|
1742
1991
|
|
1743
1992
|
self._slice_lookup = None
|
1744
1993
|
|
1745
|
-
logger.info(
|
1746
|
-
|
1994
|
+
logger.info(
|
1995
|
+
"Initialized BespokeWindPlants with project points: {}".format(
|
1996
|
+
self._project_points
|
1997
|
+
)
|
1998
|
+
)
|
1747
1999
|
|
1748
2000
|
@staticmethod
|
1749
2001
|
def _parse_points(points, sam_configs):
|
@@ -1755,8 +2007,8 @@ class BespokeWindPlants(BaseAggregation):
|
|
1755
2007
|
Slice or list specifying project points, string pointing to a
|
1756
2008
|
project points csv, or a fully instantiated PointsControl object.
|
1757
2009
|
Can also be a single site integer value. Points csv should have
|
1758
|
-
|
1759
|
-
keys.
|
2010
|
+
`SiteDataField.GID` and 'config' column, the config maps to the
|
2011
|
+
sam_configs dict keys.
|
1760
2012
|
sam_configs : dict | str | SAMConfig
|
1761
2013
|
SAM input configuration ID(s) and file path(s). Keys are the SAM
|
1762
2014
|
config ID(s) which map to the config column in the project points
|
@@ -1770,8 +2022,13 @@ class BespokeWindPlants(BaseAggregation):
|
|
1770
2022
|
Project points object laying out the supply curve gids to
|
1771
2023
|
analyze.
|
1772
2024
|
"""
|
1773
|
-
pc = Gen.get_pc(
|
1774
|
-
|
2025
|
+
pc = Gen.get_pc(
|
2026
|
+
points,
|
2027
|
+
points_range=None,
|
2028
|
+
sam_configs=sam_configs,
|
2029
|
+
tech="windpower",
|
2030
|
+
sites_per_worker=1,
|
2031
|
+
)
|
1775
2032
|
|
1776
2033
|
return pc.project_points
|
1777
2034
|
|
@@ -1801,15 +2058,16 @@ class BespokeWindPlants(BaseAggregation):
|
|
1801
2058
|
|
1802
2059
|
if prior_run is not None:
|
1803
2060
|
assert os.path.isfile(prior_run)
|
1804
|
-
assert prior_run.endswith(
|
2061
|
+
assert prior_run.endswith(".h5")
|
1805
2062
|
|
1806
|
-
with Outputs(prior_run, mode=
|
2063
|
+
with Outputs(prior_run, mode="r") as f:
|
1807
2064
|
meta = f.meta
|
2065
|
+
meta = meta.rename(columns=SupplyCurveField.map_from_legacy())
|
1808
2066
|
|
1809
2067
|
# pylint: disable=no-member
|
1810
2068
|
for col in meta.columns:
|
1811
2069
|
val = meta[col].values[0]
|
1812
|
-
if isinstance(val, str) and val[0] ==
|
2070
|
+
if isinstance(val, str) and val[0] == "[" and val[-1] == "]":
|
1813
2071
|
meta[col] = meta[col].apply(json.loads)
|
1814
2072
|
|
1815
2073
|
return meta
|
@@ -1830,7 +2088,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
1830
2088
|
meta = None
|
1831
2089
|
|
1832
2090
|
if self._prior_meta is not None:
|
1833
|
-
mask = self._prior_meta[
|
2091
|
+
mask = self._prior_meta[SupplyCurveField.SC_POINT_GID] == gid
|
1834
2092
|
if any(mask):
|
1835
2093
|
meta = self._prior_meta[mask]
|
1836
2094
|
|
@@ -1846,14 +2104,19 @@ class BespokeWindPlants(BaseAggregation):
|
|
1846
2104
|
for path in paths:
|
1847
2105
|
if not os.path.exists(path):
|
1848
2106
|
raise FileNotFoundError(
|
1849
|
-
|
1850
|
-
|
2107
|
+
"Could not find required exclusions file: " "{}".format(
|
2108
|
+
path
|
2109
|
+
)
|
2110
|
+
)
|
1851
2111
|
|
1852
2112
|
with ExclusionLayers(paths) as excl:
|
1853
2113
|
if self._tm_dset not in excl:
|
1854
|
-
raise FileInputError(
|
1855
|
-
|
1856
|
-
|
2114
|
+
raise FileInputError(
|
2115
|
+
'Could not find techmap dataset "{}" '
|
2116
|
+
"in the exclusions file(s): {}".format(
|
2117
|
+
self._tm_dset, paths
|
2118
|
+
)
|
2119
|
+
)
|
1857
2120
|
|
1858
2121
|
# just check that this file exists, cannot check res_fpath if *glob
|
1859
2122
|
Handler = BespokeSinglePlant.get_wind_handler(self._res_fpath)
|
@@ -1861,22 +2124,28 @@ class BespokeWindPlants(BaseAggregation):
|
|
1861
2124
|
assert any(f.dsets)
|
1862
2125
|
|
1863
2126
|
def _pre_load_data(self, pre_load_data):
|
1864
|
-
"""Pre-load resource data, if requested.
|
2127
|
+
"""Pre-load resource data, if requested."""
|
1865
2128
|
if not pre_load_data:
|
1866
2129
|
return
|
1867
2130
|
|
1868
|
-
sc_gid_to_hh = {
|
1869
|
-
|
2131
|
+
sc_gid_to_hh = {
|
2132
|
+
gid: self._hh_for_sc_gid(gid)
|
2133
|
+
for gid in self._project_points.df[ResourceMetaField.GID]
|
2134
|
+
}
|
1870
2135
|
|
1871
2136
|
with ExclusionLayers(self._excl_fpath) as excl:
|
1872
2137
|
tm = excl[self._tm_dset]
|
1873
2138
|
|
1874
2139
|
scp_kwargs = {"shape": self.shape, "resolution": self._resolution}
|
1875
|
-
slices = {
|
1876
|
-
|
2140
|
+
slices = {
|
2141
|
+
gid: SupplyCurvePoint.get_agg_slices(gid=gid, **scp_kwargs)
|
2142
|
+
for gid in self._project_points.df[ResourceMetaField.GID]
|
2143
|
+
}
|
1877
2144
|
|
1878
|
-
sc_gid_to_res_gid = {
|
1879
|
-
|
2145
|
+
sc_gid_to_res_gid = {
|
2146
|
+
gid: sorted(set(tm[slx, sly].flatten()))
|
2147
|
+
for gid, (slx, sly) in slices.items()
|
2148
|
+
}
|
1880
2149
|
|
1881
2150
|
for sc_gid, res_gids in sc_gid_to_res_gid.items():
|
1882
2151
|
if res_gids[0] < 0:
|
@@ -1884,13 +2153,14 @@ class BespokeWindPlants(BaseAggregation):
|
|
1884
2153
|
|
1885
2154
|
if self._gid_map is not None:
|
1886
2155
|
for sc_gid, res_gids in sc_gid_to_res_gid.items():
|
1887
|
-
sc_gid_to_res_gid[sc_gid] = sorted(
|
1888
|
-
|
2156
|
+
sc_gid_to_res_gid[sc_gid] = sorted(
|
2157
|
+
self._gid_map[g] for g in res_gids
|
2158
|
+
)
|
1889
2159
|
|
1890
2160
|
logger.info("Pre-loading resource data for Bespoke run... ")
|
1891
|
-
self._pre_loaded_data = BespokeMultiPlantData(
|
1892
|
-
|
1893
|
-
|
2161
|
+
self._pre_loaded_data = BespokeMultiPlantData(
|
2162
|
+
self._res_fpath, sc_gid_to_hh, sc_gid_to_res_gid
|
2163
|
+
)
|
1894
2164
|
|
1895
2165
|
def _hh_for_sc_gid(self, sc_gid):
|
1896
2166
|
"""Fetch the hh for a given sc_gid"""
|
@@ -1898,7 +2168,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
1898
2168
|
return int(config["wind_turbine_hub_ht"])
|
1899
2169
|
|
1900
2170
|
def _pre_loaded_data_for_sc_gid(self, sc_gid):
|
1901
|
-
"""Pre-load data for a given SC GID, if requested.
|
2171
|
+
"""Pre-load data for a given SC GID, if requested."""
|
1902
2172
|
if self._pre_loaded_data is None:
|
1903
2173
|
return None
|
1904
2174
|
|
@@ -1926,9 +2196,12 @@ class BespokeWindPlants(BaseAggregation):
|
|
1926
2196
|
if self._bias_correct is not None:
|
1927
2197
|
h5_gids = []
|
1928
2198
|
try:
|
1929
|
-
scp_kwargs = dict(
|
1930
|
-
|
1931
|
-
|
2199
|
+
scp_kwargs = dict(
|
2200
|
+
gid=gid,
|
2201
|
+
excl=self._excl_fpath,
|
2202
|
+
tm_dset=self._tm_dset,
|
2203
|
+
resolution=self._resolution,
|
2204
|
+
)
|
1932
2205
|
with SupplyCurvePoint(**scp_kwargs) as scp:
|
1933
2206
|
h5_gids = scp.h5_gid_set
|
1934
2207
|
except EmptySupplyCurvePointError:
|
@@ -1972,7 +2245,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
1972
2245
|
-------
|
1973
2246
|
pd.DataFrame
|
1974
2247
|
"""
|
1975
|
-
meta = [self.outputs[g][
|
2248
|
+
meta = [self.outputs[g]["meta"] for g in self.completed_gids]
|
1976
2249
|
if len(self.completed_gids) > 1:
|
1977
2250
|
meta = pd.concat(meta, axis=0)
|
1978
2251
|
else:
|
@@ -1981,10 +2254,11 @@ class BespokeWindPlants(BaseAggregation):
|
|
1981
2254
|
|
1982
2255
|
@property
|
1983
2256
|
def slice_lookup(self):
|
1984
|
-
"""
|
2257
|
+
"""Dict | None: Lookup mapping sc_point_gid to exclusion slice."""
|
1985
2258
|
if self._slice_lookup is None and self._inclusion_mask is not None:
|
1986
|
-
with SupplyCurveExtent(
|
1987
|
-
|
2259
|
+
with SupplyCurveExtent(
|
2260
|
+
self._excl_fpath, resolution=self._resolution
|
2261
|
+
) as sc:
|
1988
2262
|
assert self.shape == self._inclusion_mask.shape
|
1989
2263
|
self._slice_lookup = sc.get_slice_lookup(self.gids)
|
1990
2264
|
|
@@ -2013,8 +2287,13 @@ class BespokeWindPlants(BaseAggregation):
|
|
2013
2287
|
site_data = self._project_points.df.iloc[gid_idx]
|
2014
2288
|
|
2015
2289
|
site_sys_inputs = self._project_points[gid][1]
|
2016
|
-
site_sys_inputs.update(
|
2017
|
-
|
2290
|
+
site_sys_inputs.update(
|
2291
|
+
{
|
2292
|
+
k: v
|
2293
|
+
for k, v in site_data.to_dict().items()
|
2294
|
+
if not (isinstance(v, float) and np.isnan(v))
|
2295
|
+
}
|
2296
|
+
)
|
2018
2297
|
return site_sys_inputs
|
2019
2298
|
|
2020
2299
|
def _init_fout(self, out_fpath, sample):
|
@@ -2033,13 +2312,14 @@ class BespokeWindPlants(BaseAggregation):
|
|
2033
2312
|
if not os.path.exists(out_dir):
|
2034
2313
|
create_dirs(out_dir)
|
2035
2314
|
|
2036
|
-
with Outputs(out_fpath, mode=
|
2037
|
-
f._set_meta(
|
2038
|
-
ti_dsets = [
|
2039
|
-
|
2315
|
+
with Outputs(out_fpath, mode="w") as f:
|
2316
|
+
f._set_meta("meta", self.meta, attrs={})
|
2317
|
+
ti_dsets = [
|
2318
|
+
d for d in sample.keys() if d.startswith("time_index-")
|
2319
|
+
]
|
2040
2320
|
for dset in ti_dsets:
|
2041
2321
|
f._set_time_index(dset, sample[dset], attrs={})
|
2042
|
-
f._set_time_index(
|
2322
|
+
f._set_time_index("time_index", sample[dset], attrs={})
|
2043
2323
|
|
2044
2324
|
def _collect_out_arr(self, dset, sample):
|
2045
2325
|
"""Collect single-plant data arrays into complete arrays with data from
|
@@ -2070,8 +2350,9 @@ class BespokeWindPlants(BaseAggregation):
|
|
2070
2350
|
shape = (len(single_arr), len(self.completed_gids))
|
2071
2351
|
sample_num = single_arr[0]
|
2072
2352
|
else:
|
2073
|
-
msg =
|
2074
|
-
|
2353
|
+
msg = 'Not writing dataset "{}" of type "{}" to disk.'.format(
|
2354
|
+
dset, type(single_arr)
|
2355
|
+
)
|
2075
2356
|
logger.info(msg)
|
2076
2357
|
return None
|
2077
2358
|
|
@@ -2082,8 +2363,9 @@ class BespokeWindPlants(BaseAggregation):
|
|
2082
2363
|
full_arr = np.zeros(shape, dtype=dtype)
|
2083
2364
|
|
2084
2365
|
# collect data from all wind plants
|
2085
|
-
logger.info(
|
2086
|
-
|
2366
|
+
logger.info(
|
2367
|
+
'Collecting dataset "{}" with final shape {}'.format(dset, shape)
|
2368
|
+
)
|
2087
2369
|
for i, gid in enumerate(self.completed_gids):
|
2088
2370
|
if len(full_arr.shape) == 1:
|
2089
2371
|
full_arr[i] = self.outputs[gid][dset]
|
@@ -2107,16 +2389,18 @@ class BespokeWindPlants(BaseAggregation):
|
|
2107
2389
|
Full filepath to desired .h5 output file, the .h5 extension has
|
2108
2390
|
been added if it was not already present.
|
2109
2391
|
"""
|
2110
|
-
if not out_fpath.endswith(
|
2111
|
-
out_fpath +=
|
2392
|
+
if not out_fpath.endswith(".h5"):
|
2393
|
+
out_fpath += ".h5"
|
2112
2394
|
|
2113
2395
|
if ModuleName.BESPOKE not in out_fpath:
|
2114
2396
|
extension_with_module = "_{}.h5".format(ModuleName.BESPOKE)
|
2115
2397
|
out_fpath = out_fpath.replace(".h5", extension_with_module)
|
2116
2398
|
|
2117
2399
|
if not self.completed_gids:
|
2118
|
-
msg = (
|
2119
|
-
|
2400
|
+
msg = (
|
2401
|
+
"No output data found! It is likely that all requested "
|
2402
|
+
"points are excluded."
|
2403
|
+
)
|
2120
2404
|
logger.warning(msg)
|
2121
2405
|
warn(msg)
|
2122
2406
|
return out_fpath
|
@@ -2124,31 +2408,34 @@ class BespokeWindPlants(BaseAggregation):
|
|
2124
2408
|
sample = self.outputs[self.completed_gids[0]]
|
2125
2409
|
self._init_fout(out_fpath, sample)
|
2126
2410
|
|
2127
|
-
dsets = [
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2411
|
+
dsets = [
|
2412
|
+
d
|
2413
|
+
for d in sample.keys()
|
2414
|
+
if not d.startswith("time_index-") and d != "meta"
|
2415
|
+
]
|
2416
|
+
with Outputs(out_fpath, mode="a") as f:
|
2131
2417
|
for dset in dsets:
|
2132
2418
|
full_arr = self._collect_out_arr(dset, sample)
|
2133
2419
|
if full_arr is not None:
|
2134
2420
|
dset_no_year = dset
|
2135
|
-
if parse_year(dset, option=
|
2421
|
+
if parse_year(dset, option="boolean"):
|
2136
2422
|
year = parse_year(dset)
|
2137
|
-
dset_no_year = dset.replace(
|
2423
|
+
dset_no_year = dset.replace("-{}".format(year), "")
|
2138
2424
|
|
2139
2425
|
attrs = BespokeSinglePlant.OUT_ATTRS.get(dset_no_year, {})
|
2140
2426
|
attrs = copy.deepcopy(attrs)
|
2141
|
-
dtype = attrs.pop(
|
2142
|
-
chunks = attrs.pop(
|
2427
|
+
dtype = attrs.pop("dtype", np.float32)
|
2428
|
+
chunks = attrs.pop("chunks", None)
|
2143
2429
|
try:
|
2144
|
-
f.write_dataset(
|
2145
|
-
|
2430
|
+
f.write_dataset(
|
2431
|
+
dset, full_arr, dtype, chunks=chunks, attrs=attrs
|
2432
|
+
)
|
2146
2433
|
except Exception as e:
|
2147
2434
|
msg = 'Failed to write "{}" to disk.'.format(dset)
|
2148
2435
|
logger.exception(msg)
|
2149
|
-
raise
|
2436
|
+
raise OSError(msg) from e
|
2150
2437
|
|
2151
|
-
logger.info(
|
2438
|
+
logger.info("Saved output data to: {}".format(out_fpath))
|
2152
2439
|
return out_fpath
|
2153
2440
|
|
2154
2441
|
# pylint: disable=arguments-renamed
|
@@ -2158,6 +2445,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
2158
2445
|
capital_cost_function,
|
2159
2446
|
fixed_operating_cost_function,
|
2160
2447
|
variable_operating_cost_function,
|
2448
|
+
balance_of_system_cost_function,
|
2161
2449
|
min_spacing='5x', wake_loss_multiplier=1, ga_kwargs=None,
|
2162
2450
|
output_request=('system_capacity', 'cf_mean'),
|
2163
2451
|
ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0),
|
@@ -2195,18 +2483,19 @@ class BespokeWindPlants(BaseAggregation):
|
|
2195
2483
|
Handler = BespokeSinglePlant.get_wind_handler(res_fpath)
|
2196
2484
|
|
2197
2485
|
# pre-extract handlers so they are not repeatedly initialized
|
2198
|
-
file_kwargs = {
|
2199
|
-
|
2200
|
-
|
2201
|
-
|
2202
|
-
|
2486
|
+
file_kwargs = {
|
2487
|
+
"excl_dict": excl_dict,
|
2488
|
+
"area_filter_kernel": area_filter_kernel,
|
2489
|
+
"min_area": min_area,
|
2490
|
+
"h5_handler": Handler,
|
2491
|
+
}
|
2203
2492
|
|
2204
2493
|
with AggFileHandler(excl_fpath, res_fpath, **file_kwargs) as fh:
|
2205
2494
|
n_finished = 0
|
2206
2495
|
for gid in gids:
|
2207
2496
|
gid_inclusions = cls._get_gid_inclusion_mask(
|
2208
|
-
inclusion_mask, gid, slice_lookup,
|
2209
|
-
|
2497
|
+
inclusion_mask, gid, slice_lookup, resolution=resolution
|
2498
|
+
)
|
2210
2499
|
try:
|
2211
2500
|
bsp_plant_out = BespokeSinglePlant.run(
|
2212
2501
|
gid,
|
@@ -2218,6 +2507,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
2218
2507
|
capital_cost_function,
|
2219
2508
|
fixed_operating_cost_function,
|
2220
2509
|
variable_operating_cost_function,
|
2510
|
+
balance_of_system_cost_function,
|
2221
2511
|
min_spacing=min_spacing,
|
2222
2512
|
wake_loss_multiplier=wake_loss_multiplier,
|
2223
2513
|
ga_kwargs=ga_kwargs,
|
@@ -2234,20 +2524,26 @@ class BespokeWindPlants(BaseAggregation):
|
|
2234
2524
|
gid_map=gid_map,
|
2235
2525
|
bias_correct=bias_correct,
|
2236
2526
|
pre_loaded_data=pre_loaded_data,
|
2237
|
-
close=False
|
2527
|
+
close=False,
|
2528
|
+
)
|
2238
2529
|
|
2239
2530
|
except EmptySupplyCurvePointError:
|
2240
|
-
logger.debug(
|
2241
|
-
|
2531
|
+
logger.debug(
|
2532
|
+
"SC gid {} is fully excluded or does not "
|
2533
|
+
"have any valid source data!".format(gid)
|
2534
|
+
)
|
2242
2535
|
except Exception as e:
|
2243
|
-
msg =
|
2536
|
+
msg = "SC gid {} failed!".format(gid)
|
2244
2537
|
logger.exception(msg)
|
2245
2538
|
raise RuntimeError(msg) from e
|
2246
2539
|
else:
|
2247
2540
|
n_finished += 1
|
2248
|
-
logger.debug(
|
2249
|
-
|
2250
|
-
|
2541
|
+
logger.debug(
|
2542
|
+
"Serial bespoke: "
|
2543
|
+
"{} out of {} points complete".format(
|
2544
|
+
n_finished, len(gids)
|
2545
|
+
)
|
2546
|
+
)
|
2251
2547
|
log_mem(logger)
|
2252
2548
|
out[gid] = bsp_plant_out
|
2253
2549
|
|
@@ -2269,17 +2565,18 @@ class BespokeWindPlants(BaseAggregation):
|
|
2269
2565
|
Bespoke outputs keyed by sc point gid
|
2270
2566
|
"""
|
2271
2567
|
|
2272
|
-
logger.info(
|
2273
|
-
|
2274
|
-
|
2275
|
-
|
2568
|
+
logger.info(
|
2569
|
+
"Running bespoke optimization for points {} through {} "
|
2570
|
+
"at a resolution of {} on {} cores.".format(
|
2571
|
+
self.gids[0], self.gids[-1], self._resolution, max_workers
|
2572
|
+
)
|
2573
|
+
)
|
2276
2574
|
|
2277
2575
|
futures = []
|
2278
2576
|
out = {}
|
2279
2577
|
n_finished = 0
|
2280
|
-
loggers = [__name__,
|
2578
|
+
loggers = [__name__, "reV.supply_curve.point_summary", "reV"]
|
2281
2579
|
with SpawnProcessPool(max_workers=max_workers, loggers=loggers) as exe:
|
2282
|
-
|
2283
2580
|
# iterate through split executions, submitting each to worker
|
2284
2581
|
for gid in self.gids:
|
2285
2582
|
# submit executions and append to futures list
|
@@ -2298,6 +2595,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
2298
2595
|
self._cap_cost_fun,
|
2299
2596
|
self._foc_fun,
|
2300
2597
|
self._voc_fun,
|
2598
|
+
self._bos_fun,
|
2301
2599
|
self._min_spacing,
|
2302
2600
|
wake_loss_multiplier=self._wake_loss_multiplier,
|
2303
2601
|
ga_kwargs=self._ga_kwargs,
|
@@ -2325,12 +2623,17 @@ class BespokeWindPlants(BaseAggregation):
|
|
2325
2623
|
out.update(future.result())
|
2326
2624
|
if n_finished % 10 == 0:
|
2327
2625
|
mem = psutil.virtual_memory()
|
2328
|
-
logger.info(
|
2329
|
-
|
2330
|
-
|
2331
|
-
|
2332
|
-
|
2333
|
-
|
2626
|
+
logger.info(
|
2627
|
+
"Parallel bespoke futures collected: "
|
2628
|
+
"{} out of {}. Memory usage is {:.3f} GB out "
|
2629
|
+
"of {:.3f} GB ({:.2f}% utilized).".format(
|
2630
|
+
n_finished,
|
2631
|
+
len(futures),
|
2632
|
+
mem.used / 1e9,
|
2633
|
+
mem.total / 1e9,
|
2634
|
+
100 * mem.used / mem.total,
|
2635
|
+
)
|
2636
|
+
)
|
2334
2637
|
|
2335
2638
|
return out
|
2336
2639
|
|
@@ -2356,7 +2659,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
2356
2659
|
"""
|
2357
2660
|
|
2358
2661
|
# parallel job distribution test.
|
2359
|
-
if self._obj_fun ==
|
2662
|
+
if self._obj_fun == "test":
|
2360
2663
|
return True
|
2361
2664
|
|
2362
2665
|
if max_workers == 1:
|
@@ -2382,6 +2685,7 @@ class BespokeWindPlants(BaseAggregation):
|
|
2382
2685
|
self._cap_cost_fun,
|
2383
2686
|
self._foc_fun,
|
2384
2687
|
self._voc_fun,
|
2688
|
+
self._bos_fun,
|
2385
2689
|
min_spacing=self._min_spacing,
|
2386
2690
|
wake_loss_multiplier=wlm,
|
2387
2691
|
ga_kwargs=self._ga_kwargs,
|