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/supply_curve/supply_curve.py
CHANGED
@@ -4,33 +4,60 @@ reV supply curve module
|
|
4
4
|
- Calculation of LCOT
|
5
5
|
- Supply Curve creation
|
6
6
|
"""
|
7
|
-
from copy import deepcopy
|
8
7
|
import json
|
9
8
|
import logging
|
10
|
-
import numpy as np
|
11
9
|
import os
|
12
|
-
|
10
|
+
from itertools import chain
|
11
|
+
from copy import deepcopy
|
13
12
|
from warnings import warn
|
14
13
|
|
14
|
+
import numpy as np
|
15
|
+
import pandas as pd
|
16
|
+
from rex import Resource
|
17
|
+
from rex.utilities import SpawnProcessPool, parse_table
|
18
|
+
|
15
19
|
from reV.handlers.transmission import TransmissionCosts as TC
|
16
20
|
from reV.handlers.transmission import TransmissionFeatures as TF
|
17
21
|
from reV.supply_curve.competitive_wind_farms import CompetitiveWindFarms
|
18
|
-
from reV.utilities
|
19
|
-
from reV.utilities import
|
22
|
+
from reV.utilities import SupplyCurveField, log_versions
|
23
|
+
from reV.utilities.exceptions import SupplyCurveError, SupplyCurveInputError
|
20
24
|
|
21
|
-
|
22
|
-
from rex.utilities import parse_table, SpawnProcessPool
|
25
|
+
logger = logging.getLogger(__name__)
|
23
26
|
|
24
27
|
|
25
|
-
|
28
|
+
# map is column name to relative order in which it should appear in output file
|
29
|
+
_REQUIRED_COMPUTE_AND_OUTPUT_COLS = {
|
30
|
+
SupplyCurveField.TRANS_GID: 0,
|
31
|
+
SupplyCurveField.TRANS_TYPE: 1,
|
32
|
+
SupplyCurveField.N_PARALLEL_TRANS: 2,
|
33
|
+
SupplyCurveField.DIST_SPUR_KM: 3,
|
34
|
+
SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW: 10,
|
35
|
+
SupplyCurveField.LCOT: 11,
|
36
|
+
SupplyCurveField.TOTAL_LCOE: 12,
|
37
|
+
}
|
38
|
+
_REQUIRED_OUTPUT_COLS = {SupplyCurveField.DIST_EXPORT_KM: 4,
|
39
|
+
SupplyCurveField.REINFORCEMENT_DIST_KM: 5,
|
40
|
+
SupplyCurveField.TIE_LINE_COST_PER_MW: 6,
|
41
|
+
SupplyCurveField.CONNECTION_COST_PER_MW: 7,
|
42
|
+
SupplyCurveField.EXPORT_COST_PER_MW: 8,
|
43
|
+
SupplyCurveField.REINFORCEMENT_COST_PER_MW: 9,
|
44
|
+
SupplyCurveField.POI_LAT: 13,
|
45
|
+
SupplyCurveField.POI_LON: 14,
|
46
|
+
SupplyCurveField.REINFORCEMENT_POI_LAT: 15,
|
47
|
+
SupplyCurveField.REINFORCEMENT_POI_LON: 16}
|
48
|
+
DEFAULT_COLUMNS = tuple(str(field)
|
49
|
+
for field in chain(_REQUIRED_COMPUTE_AND_OUTPUT_COLS,
|
50
|
+
_REQUIRED_OUTPUT_COLS))
|
51
|
+
"""Default output columns from supply chain computation (not ordered)"""
|
26
52
|
|
27
53
|
|
28
54
|
class SupplyCurve:
|
29
55
|
"""SupplyCurve"""
|
30
56
|
|
31
57
|
def __init__(self, sc_points, trans_table, sc_features=None,
|
32
|
-
|
33
|
-
|
58
|
+
# str() to fix docs
|
59
|
+
sc_capacity_col=str(SupplyCurveField.CAPACITY_AC_MW)):
|
60
|
+
"""ReV LCOT calculation and SupplyCurve sorting class.
|
34
61
|
|
35
62
|
``reV`` supply curve computes the transmission costs associated
|
36
63
|
with each supply curve point output by ``reV`` supply curve
|
@@ -127,15 +154,17 @@ class SupplyCurve:
|
|
127
154
|
(mean_lcoe_friction + lcot) ($/MWh).
|
128
155
|
"""
|
129
156
|
log_versions(logger)
|
130
|
-
logger.info(
|
131
|
-
logger.info(
|
132
|
-
logger.info(
|
157
|
+
logger.info("Supply curve points input: {}".format(sc_points))
|
158
|
+
logger.info("Transmission table input: {}".format(trans_table))
|
159
|
+
logger.info("Supply curve capacity column: {}".format(sc_capacity_col))
|
133
160
|
|
134
161
|
self._sc_capacity_col = sc_capacity_col
|
135
|
-
self._sc_points = self._parse_sc_points(
|
136
|
-
|
137
|
-
|
138
|
-
|
162
|
+
self._sc_points = self._parse_sc_points(
|
163
|
+
sc_points, sc_features=sc_features
|
164
|
+
)
|
165
|
+
self._trans_table = self._map_tables(
|
166
|
+
self._sc_points, trans_table, sc_capacity_col=sc_capacity_col
|
167
|
+
)
|
139
168
|
self._sc_gids, self._mask = self._parse_sc_gids(self._trans_table)
|
140
169
|
|
141
170
|
def __repr__(self):
|
@@ -177,31 +206,43 @@ class SupplyCurve:
|
|
177
206
|
DataFrame of supply curve point summary with additional features
|
178
207
|
added if supplied
|
179
208
|
"""
|
180
|
-
if isinstance(sc_points, str) and sc_points.endswith(
|
209
|
+
if isinstance(sc_points, str) and sc_points.endswith(".h5"):
|
181
210
|
with Resource(sc_points) as res:
|
182
211
|
sc_points = res.meta
|
183
|
-
sc_points.index.name =
|
212
|
+
sc_points.index.name = SupplyCurveField.SC_GID
|
184
213
|
sc_points = sc_points.reset_index()
|
185
214
|
else:
|
186
215
|
sc_points = parse_table(sc_points)
|
216
|
+
sc_points = sc_points.rename(
|
217
|
+
columns=SupplyCurveField.map_from_legacy())
|
187
218
|
|
188
|
-
logger.debug(
|
189
|
-
|
219
|
+
logger.debug(
|
220
|
+
"Supply curve points table imported with columns: {}".format(
|
221
|
+
sc_points.columns.values.tolist()
|
222
|
+
)
|
223
|
+
)
|
190
224
|
|
191
225
|
if sc_features is not None:
|
192
226
|
sc_features = parse_table(sc_features)
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
227
|
+
sc_features = sc_features.rename(
|
228
|
+
columns=SupplyCurveField.map_from_legacy())
|
229
|
+
merge_cols = [c for c in sc_features if c in sc_points]
|
230
|
+
sc_points = sc_points.merge(sc_features, on=merge_cols, how="left")
|
231
|
+
logger.debug(
|
232
|
+
"Adding Supply Curve Features table with columns: {}".format(
|
233
|
+
sc_features.columns.values.tolist()
|
234
|
+
)
|
235
|
+
)
|
236
|
+
|
237
|
+
if "transmission_multiplier" in sc_points:
|
238
|
+
col = "transmission_multiplier"
|
201
239
|
sc_points.loc[:, col] = sc_points.loc[:, col].fillna(1)
|
202
240
|
|
203
|
-
logger.debug(
|
204
|
-
|
241
|
+
logger.debug(
|
242
|
+
"Final supply curve points table has columns: {}".format(
|
243
|
+
sc_points.columns.values.tolist()
|
244
|
+
)
|
245
|
+
)
|
205
246
|
|
206
247
|
return sc_points
|
207
248
|
|
@@ -222,18 +263,20 @@ class SupplyCurve:
|
|
222
263
|
Columns to merge on which maps the sc columns (keys) to the
|
223
264
|
corresponding trans table columns (values)
|
224
265
|
"""
|
225
|
-
sc_columns = [c for c in sc_columns if c.startswith(
|
226
|
-
trans_columns = [c for c in trans_columns if c.startswith(
|
266
|
+
sc_columns = [c for c in sc_columns if c.startswith("sc_")]
|
267
|
+
trans_columns = [c for c in trans_columns if c.startswith("sc_")]
|
227
268
|
merge_cols = {}
|
228
|
-
for c_val in [
|
269
|
+
for c_val in ["row", "col"]:
|
229
270
|
trans_col = [c for c in trans_columns if c_val in c]
|
230
271
|
sc_col = [c for c in sc_columns if c_val in c]
|
231
272
|
if trans_col and sc_col:
|
232
273
|
merge_cols[sc_col[0]] = trans_col[0]
|
233
274
|
|
234
275
|
if len(merge_cols) != 2:
|
235
|
-
msg = (
|
236
|
-
|
276
|
+
msg = (
|
277
|
+
"Did not find a unique set of sc row and column ids to "
|
278
|
+
"merge on: {}".format(merge_cols)
|
279
|
+
)
|
237
280
|
logger.error(msg)
|
238
281
|
raise RuntimeError(msg)
|
239
282
|
|
@@ -266,24 +309,32 @@ class SupplyCurve:
|
|
266
309
|
# legacy name: trans_gids
|
267
310
|
# also xformer_cost_p_mw -> xformer_cost_per_mw (not sure why there
|
268
311
|
# would be a *_p_mw but here we are...)
|
269
|
-
rename_map = {
|
270
|
-
|
271
|
-
|
312
|
+
rename_map = {
|
313
|
+
"trans_line_gid": SupplyCurveField.TRANS_GID,
|
314
|
+
"trans_gids": "trans_line_gids",
|
315
|
+
"xformer_cost_p_mw": "xformer_cost_per_mw",
|
316
|
+
}
|
272
317
|
trans_table = trans_table.rename(columns=rename_map)
|
273
318
|
|
274
|
-
|
275
|
-
|
276
|
-
|
319
|
+
contains_dist_in_miles = "dist_mi" in trans_table
|
320
|
+
missing_km_dist = SupplyCurveField.DIST_SPUR_KM not in trans_table
|
321
|
+
if contains_dist_in_miles and missing_km_dist:
|
322
|
+
trans_table = trans_table.rename(
|
323
|
+
columns={"dist_mi": SupplyCurveField.DIST_SPUR_KM}
|
324
|
+
)
|
325
|
+
trans_table[SupplyCurveField.DIST_SPUR_KM] *= 1.60934
|
277
326
|
|
278
|
-
drop_cols = [
|
327
|
+
drop_cols = [SupplyCurveField.SC_GID, 'cap_left',
|
328
|
+
SupplyCurveField.SC_POINT_GID]
|
279
329
|
drop_cols = [c for c in drop_cols if c in trans_table]
|
280
330
|
if drop_cols:
|
281
331
|
trans_table = trans_table.drop(columns=drop_cols)
|
282
332
|
|
283
|
-
return trans_table
|
333
|
+
return trans_table.rename(columns=SupplyCurveField.map_from_legacy())
|
284
334
|
|
285
335
|
@staticmethod
|
286
|
-
def _map_trans_capacity(trans_sc_table,
|
336
|
+
def _map_trans_capacity(trans_sc_table,
|
337
|
+
sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
|
287
338
|
"""
|
288
339
|
Map SC gids to transmission features based on capacity. For any SC
|
289
340
|
gids with capacity > the maximum transmission feature capacity, map
|
@@ -310,33 +361,42 @@ class SupplyCurve:
|
|
310
361
|
based on maximum capacity
|
311
362
|
"""
|
312
363
|
|
313
|
-
nx = trans_sc_table[sc_capacity_col] / trans_sc_table[
|
364
|
+
nx = trans_sc_table[sc_capacity_col] / trans_sc_table["max_cap"]
|
314
365
|
nx = np.ceil(nx).astype(int)
|
315
|
-
trans_sc_table[
|
366
|
+
trans_sc_table[SupplyCurveField.N_PARALLEL_TRANS] = nx
|
316
367
|
|
317
368
|
if (nx > 1).any():
|
318
369
|
mask = nx > 1
|
319
|
-
tie_line_cost = (
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
370
|
+
tie_line_cost = (
|
371
|
+
trans_sc_table.loc[mask, "tie_line_cost"] * nx[mask]
|
372
|
+
)
|
373
|
+
|
374
|
+
xformer_cost = (
|
375
|
+
trans_sc_table.loc[mask, "xformer_cost_per_mw"]
|
376
|
+
* trans_sc_table.loc[mask, "max_cap"]
|
377
|
+
* nx[mask]
|
378
|
+
)
|
379
|
+
|
380
|
+
conn_cost = (
|
381
|
+
xformer_cost
|
382
|
+
+ trans_sc_table.loc[mask, "sub_upgrade_cost"]
|
383
|
+
+ trans_sc_table.loc[mask, "new_sub_cost"]
|
384
|
+
)
|
328
385
|
|
329
386
|
trans_cap_cost = tie_line_cost + conn_cost
|
330
387
|
|
331
|
-
trans_sc_table.loc[mask,
|
332
|
-
trans_sc_table.loc[mask,
|
333
|
-
trans_sc_table.loc[mask,
|
334
|
-
trans_sc_table.loc[mask,
|
335
|
-
|
336
|
-
msg = (
|
337
|
-
|
338
|
-
|
339
|
-
|
388
|
+
trans_sc_table.loc[mask, "tie_line_cost"] = tie_line_cost
|
389
|
+
trans_sc_table.loc[mask, "xformer_cost"] = xformer_cost
|
390
|
+
trans_sc_table.loc[mask, "connection_cost"] = conn_cost
|
391
|
+
trans_sc_table.loc[mask, "trans_cap_cost"] = trans_cap_cost
|
392
|
+
|
393
|
+
msg = (
|
394
|
+
"{} SC points have a capacity that exceeds the maximum "
|
395
|
+
"transmission feature capacity and will be connected with "
|
396
|
+
"multiple parallel transmission features.".format(
|
397
|
+
(nx > 1).sum()
|
398
|
+
)
|
399
|
+
)
|
340
400
|
logger.info(msg)
|
341
401
|
|
342
402
|
return trans_sc_table
|
@@ -379,24 +439,31 @@ class SupplyCurve:
|
|
379
439
|
List of missing transmission line 'trans_gid's for all substations
|
380
440
|
in features table
|
381
441
|
"""
|
382
|
-
features = features.rename(
|
383
|
-
|
384
|
-
|
442
|
+
features = features.rename(
|
443
|
+
columns={
|
444
|
+
"trans_line_gid": SupplyCurveField.TRANS_GID,
|
445
|
+
"trans_gids": "trans_line_gids",
|
446
|
+
}
|
447
|
+
)
|
448
|
+
mask = (features[SupplyCurveField.TRANS_TYPE].str.casefold()
|
449
|
+
== "substation")
|
385
450
|
|
386
451
|
if not any(mask):
|
387
452
|
return []
|
388
453
|
|
389
|
-
line_gids =
|
390
|
-
|
454
|
+
line_gids = features.loc[mask, "trans_line_gids"].apply(
|
455
|
+
cls._parse_trans_line_gids
|
456
|
+
)
|
391
457
|
|
392
458
|
line_gids = np.unique(np.concatenate(line_gids.values))
|
393
459
|
|
394
|
-
test = np.isin(line_gids, features[
|
460
|
+
test = np.isin(line_gids, features[SupplyCurveField.TRANS_GID].values)
|
395
461
|
|
396
462
|
return line_gids[~test].tolist()
|
397
463
|
|
398
464
|
@classmethod
|
399
|
-
def _check_substation_conns(cls, trans_table,
|
465
|
+
def _check_substation_conns(cls, trans_table,
|
466
|
+
sc_cols=SupplyCurveField.SC_GID):
|
400
467
|
"""
|
401
468
|
Run checks on substation transmission features to make sure that
|
402
469
|
every sc point connecting to a substation can also connect to its
|
@@ -409,7 +476,7 @@ class SupplyCurve:
|
|
409
476
|
(should already be merged with SC points).
|
410
477
|
sc_cols : str | list, optional
|
411
478
|
Column(s) in trans_table with unique supply curve id,
|
412
|
-
by default
|
479
|
+
by default SupplyCurveField.SC_GID
|
413
480
|
"""
|
414
481
|
missing = {}
|
415
482
|
for sc_point, sc_table in trans_table.groupby(sc_cols):
|
@@ -418,10 +485,13 @@ class SupplyCurve:
|
|
418
485
|
missing[sc_point] = tl_gids
|
419
486
|
|
420
487
|
if any(missing):
|
421
|
-
msg = (
|
422
|
-
|
423
|
-
|
424
|
-
|
488
|
+
msg = (
|
489
|
+
"The following sc_gid (keys) were connected to substations "
|
490
|
+
"but were not connected to the respective transmission line"
|
491
|
+
" gids (values) which is required for full SC sort: {}".format(
|
492
|
+
missing
|
493
|
+
)
|
494
|
+
)
|
425
495
|
logger.error(msg)
|
426
496
|
raise SupplyCurveInputError(msg)
|
427
497
|
|
@@ -437,37 +507,50 @@ class SupplyCurve:
|
|
437
507
|
Table mapping supply curve points to transmission features
|
438
508
|
(should already be merged with SC points).
|
439
509
|
"""
|
440
|
-
sc_gids = set(sc_points[
|
441
|
-
trans_sc_gids = set(trans_table[
|
510
|
+
sc_gids = set(sc_points[SupplyCurveField.SC_GID].unique())
|
511
|
+
trans_sc_gids = set(trans_table[SupplyCurveField.SC_GID].unique())
|
442
512
|
missing = sorted(list(sc_gids - trans_sc_gids))
|
443
513
|
if any(missing):
|
444
|
-
msg = (
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
514
|
+
msg = (
|
515
|
+
"There are {} Supply Curve points with missing "
|
516
|
+
"transmission mappings. Supply curve points with no "
|
517
|
+
"transmission features will not be connected! "
|
518
|
+
"Missing sc_gid's: {}".format(len(missing), missing)
|
519
|
+
)
|
449
520
|
logger.warning(msg)
|
450
521
|
warn(msg)
|
451
522
|
|
452
523
|
if not any(trans_sc_gids) or not any(sc_gids):
|
453
|
-
msg = (
|
454
|
-
|
455
|
-
|
456
|
-
|
524
|
+
msg = (
|
525
|
+
"Merging of sc points table and transmission features "
|
526
|
+
"table failed with {} original sc gids and {} transmission "
|
527
|
+
"sc gids after table merge.".format(
|
528
|
+
len(sc_gids), len(trans_sc_gids)
|
529
|
+
)
|
530
|
+
)
|
457
531
|
logger.error(msg)
|
458
532
|
raise SupplyCurveError(msg)
|
459
533
|
|
460
|
-
logger.debug(
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
534
|
+
logger.debug(
|
535
|
+
"There are {} original SC gids and {} sc gids in the "
|
536
|
+
"merged transmission table.".format(
|
537
|
+
len(sc_gids), len(trans_sc_gids)
|
538
|
+
)
|
539
|
+
)
|
540
|
+
logger.debug(
|
541
|
+
"Transmission Table created with columns: {}".format(
|
542
|
+
trans_table.columns.values.tolist()
|
543
|
+
)
|
544
|
+
)
|
465
545
|
|
466
546
|
@classmethod
|
467
547
|
def _merge_sc_trans_tables(cls, sc_points, trans_table,
|
468
|
-
sc_cols=(
|
469
|
-
|
470
|
-
|
548
|
+
sc_cols=(SupplyCurveField.SC_GID,
|
549
|
+
SupplyCurveField.CAPACITY_AC_MW,
|
550
|
+
SupplyCurveField.MEAN_CF_AC,
|
551
|
+
SupplyCurveField.MEAN_LCOE),
|
552
|
+
sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW
|
553
|
+
):
|
471
554
|
"""
|
472
555
|
Merge the supply curve table with the transmission features table.
|
473
556
|
|
@@ -482,7 +565,8 @@ class SupplyCurve:
|
|
482
565
|
sc_cols : tuple | list, optional
|
483
566
|
List of column from sc_points to transfer into the trans table,
|
484
567
|
If the `sc_capacity_col` is not included, it will get added.
|
485
|
-
by default (
|
568
|
+
by default (SupplyCurveField.SC_GID, 'capacity', 'mean_cf',
|
569
|
+
'mean_lcoe')
|
486
570
|
sc_capacity_col : str, optional
|
487
571
|
Name of capacity column in `trans_sc_table`. The values in
|
488
572
|
this column determine the size of transmission lines built.
|
@@ -505,42 +589,57 @@ class SupplyCurve:
|
|
505
589
|
if isinstance(trans_table, (list, tuple)):
|
506
590
|
trans_sc_table = []
|
507
591
|
for table in trans_table:
|
508
|
-
trans_sc_table.append(
|
509
|
-
|
510
|
-
|
592
|
+
trans_sc_table.append(
|
593
|
+
cls._merge_sc_trans_tables(
|
594
|
+
sc_points,
|
595
|
+
table,
|
596
|
+
sc_cols=sc_cols,
|
597
|
+
sc_capacity_col=sc_capacity_col,
|
598
|
+
)
|
599
|
+
)
|
511
600
|
|
512
601
|
trans_sc_table = pd.concat(trans_sc_table)
|
513
602
|
else:
|
514
603
|
trans_table = cls._parse_trans_table(trans_table)
|
515
604
|
|
516
|
-
merge_cols = cls._get_merge_cols(
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
605
|
+
merge_cols = cls._get_merge_cols(
|
606
|
+
sc_points.columns, trans_table.columns
|
607
|
+
)
|
608
|
+
logger.info(
|
609
|
+
"Merging SC table and Trans Table with "
|
610
|
+
"{} mapping: {}".format(
|
611
|
+
"sc_table_col: trans_table_col", merge_cols
|
612
|
+
)
|
613
|
+
)
|
521
614
|
sc_points = sc_points.rename(columns=merge_cols)
|
522
615
|
merge_cols = list(merge_cols.values())
|
523
616
|
|
524
617
|
if isinstance(sc_cols, tuple):
|
525
618
|
sc_cols = list(sc_cols)
|
526
619
|
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
620
|
+
extra_cols = [SupplyCurveField.CAPACITY_DC_MW,
|
621
|
+
SupplyCurveField.MEAN_CF_DC,
|
622
|
+
SupplyCurveField.MEAN_LCOE_FRICTION,
|
623
|
+
"transmission_multiplier"]
|
624
|
+
for col in extra_cols:
|
625
|
+
if col in sc_points:
|
626
|
+
sc_cols.append(col)
|
532
627
|
|
533
628
|
sc_cols += merge_cols
|
534
629
|
sc_points = sc_points[sc_cols].copy()
|
535
|
-
trans_sc_table = trans_table.merge(
|
536
|
-
|
630
|
+
trans_sc_table = trans_table.merge(
|
631
|
+
sc_points, on=merge_cols, how="inner"
|
632
|
+
)
|
537
633
|
|
538
634
|
return trans_sc_table
|
539
635
|
|
540
636
|
@classmethod
|
541
637
|
def _map_tables(cls, sc_points, trans_table,
|
542
|
-
sc_cols=(
|
543
|
-
|
638
|
+
sc_cols=(SupplyCurveField.SC_GID,
|
639
|
+
SupplyCurveField.CAPACITY_AC_MW,
|
640
|
+
SupplyCurveField.MEAN_CF_AC,
|
641
|
+
SupplyCurveField.MEAN_LCOE),
|
642
|
+
sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
|
544
643
|
"""
|
545
644
|
Map supply curve points to transmission features
|
546
645
|
|
@@ -555,7 +654,9 @@ class SupplyCurve:
|
|
555
654
|
sc_cols : tuple | list, optional
|
556
655
|
List of column from sc_points to transfer into the trans table,
|
557
656
|
If the `sc_capacity_col` is not included, it will get added.
|
558
|
-
by default (
|
657
|
+
by default (SupplyCurveField.SC_GID,
|
658
|
+
SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF_AC,
|
659
|
+
SupplyCurveField.MEAN_LCOE)
|
559
660
|
sc_capacity_col : str, optional
|
560
661
|
Name of capacity column in `trans_sc_table`. The values in
|
561
662
|
this column determine the size of transmission lines built.
|
@@ -573,17 +674,18 @@ class SupplyCurve:
|
|
573
674
|
This is performed by an inner merging with trans_table
|
574
675
|
"""
|
575
676
|
scc = sc_capacity_col
|
576
|
-
trans_sc_table = cls._merge_sc_trans_tables(
|
577
|
-
|
578
|
-
|
677
|
+
trans_sc_table = cls._merge_sc_trans_tables(
|
678
|
+
sc_points, trans_table, sc_cols=sc_cols, sc_capacity_col=scc
|
679
|
+
)
|
579
680
|
|
580
|
-
if
|
581
|
-
trans_sc_table = cls._map_trans_capacity(
|
582
|
-
|
681
|
+
if "max_cap" in trans_sc_table:
|
682
|
+
trans_sc_table = cls._map_trans_capacity(
|
683
|
+
trans_sc_table, sc_capacity_col=scc
|
684
|
+
)
|
583
685
|
|
584
|
-
|
585
|
-
|
586
|
-
|
686
|
+
sort_cols = [SupplyCurveField.SC_GID, SupplyCurveField.TRANS_GID]
|
687
|
+
trans_sc_table = trans_sc_table.sort_values(sort_cols)
|
688
|
+
trans_sc_table = trans_sc_table.reset_index(drop=True)
|
587
689
|
|
588
690
|
cls._check_sc_trans_table(sc_points, trans_sc_table)
|
589
691
|
|
@@ -619,13 +721,14 @@ class SupplyCurve:
|
|
619
721
|
else:
|
620
722
|
kwargs = {}
|
621
723
|
|
622
|
-
trans_features = TF(
|
623
|
-
|
724
|
+
trans_features = TF(
|
725
|
+
trans_table, avail_cap_frac=avail_cap_frac, **kwargs
|
726
|
+
)
|
624
727
|
|
625
728
|
return trans_features
|
626
729
|
|
627
730
|
@staticmethod
|
628
|
-
def _parse_sc_gids(trans_table, gid_key=
|
731
|
+
def _parse_sc_gids(trans_table, gid_key=SupplyCurveField.SC_GID):
|
629
732
|
"""Extract unique sc gids, make bool mask from tranmission table
|
630
733
|
|
631
734
|
Parameters
|
@@ -652,7 +755,7 @@ class SupplyCurve:
|
|
652
755
|
|
653
756
|
@staticmethod
|
654
757
|
def _get_capacity(sc_gid, sc_table, connectable=True,
|
655
|
-
sc_capacity_col=
|
758
|
+
sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
|
656
759
|
"""
|
657
760
|
Get capacity of supply curve point
|
658
761
|
|
@@ -686,9 +789,10 @@ class SupplyCurve:
|
|
686
789
|
if len(capacity) == 1:
|
687
790
|
capacity = capacity[0]
|
688
791
|
else:
|
689
|
-
msg = (
|
690
|
-
|
691
|
-
|
792
|
+
msg = (
|
793
|
+
"Each supply curve point should only have "
|
794
|
+
"a single capacity, but {} has {}".format(sc_gid, capacity)
|
795
|
+
)
|
692
796
|
logger.error(msg)
|
693
797
|
raise RuntimeError(msg)
|
694
798
|
else:
|
@@ -700,7 +804,8 @@ class SupplyCurve:
|
|
700
804
|
def _compute_trans_cap_cost(cls, trans_table, trans_costs=None,
|
701
805
|
avail_cap_frac=1, max_workers=None,
|
702
806
|
connectable=True, line_limited=False,
|
703
|
-
sc_capacity_col=
|
807
|
+
sc_capacity_col=(
|
808
|
+
SupplyCurveField.CAPACITY_AC_MW)):
|
704
809
|
"""
|
705
810
|
Compute levelized cost of transmission for all combinations of
|
706
811
|
supply curve points and tranmission features in trans_table
|
@@ -749,9 +854,11 @@ class SupplyCurve:
|
|
749
854
|
"""
|
750
855
|
scc = sc_capacity_col
|
751
856
|
if scc not in trans_table:
|
752
|
-
raise SupplyCurveInputError(
|
753
|
-
|
754
|
-
|
857
|
+
raise SupplyCurveInputError(
|
858
|
+
"Supply curve table must have "
|
859
|
+
"supply curve point capacity column"
|
860
|
+
"({}) to compute lcot".format(scc)
|
861
|
+
)
|
755
862
|
|
756
863
|
if trans_costs is not None:
|
757
864
|
trans_costs = TF._parse_dictionary(trans_costs)
|
@@ -762,44 +869,66 @@ class SupplyCurve:
|
|
762
869
|
max_workers = os.cpu_count()
|
763
870
|
|
764
871
|
logger.info('Computing LCOT costs for all possible connections...')
|
765
|
-
groups = trans_table.groupby(
|
872
|
+
groups = trans_table.groupby(SupplyCurveField.SC_GID)
|
766
873
|
if max_workers > 1:
|
767
|
-
loggers = [__name__,
|
768
|
-
with SpawnProcessPool(
|
769
|
-
|
874
|
+
loggers = [__name__, "reV.handlers.transmission", "reV"]
|
875
|
+
with SpawnProcessPool(
|
876
|
+
max_workers=max_workers, loggers=loggers
|
877
|
+
) as exe:
|
770
878
|
futures = []
|
771
879
|
for sc_gid, sc_table in groups:
|
772
|
-
capacity = cls._get_capacity(
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
880
|
+
capacity = cls._get_capacity(
|
881
|
+
sc_gid,
|
882
|
+
sc_table,
|
883
|
+
connectable=connectable,
|
884
|
+
sc_capacity_col=scc,
|
885
|
+
)
|
886
|
+
futures.append(
|
887
|
+
exe.submit(
|
888
|
+
TC.feature_costs,
|
889
|
+
sc_table,
|
890
|
+
capacity=capacity,
|
891
|
+
avail_cap_frac=avail_cap_frac,
|
892
|
+
line_limited=line_limited,
|
893
|
+
**trans_costs,
|
894
|
+
)
|
895
|
+
)
|
780
896
|
|
781
897
|
cost = [future.result() for future in futures]
|
782
898
|
else:
|
783
899
|
cost = []
|
784
900
|
for sc_gid, sc_table in groups:
|
785
|
-
capacity = cls._get_capacity(
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
901
|
+
capacity = cls._get_capacity(
|
902
|
+
sc_gid,
|
903
|
+
sc_table,
|
904
|
+
connectable=connectable,
|
905
|
+
sc_capacity_col=scc,
|
906
|
+
)
|
907
|
+
cost.append(
|
908
|
+
TC.feature_costs(
|
909
|
+
sc_table,
|
910
|
+
capacity=capacity,
|
911
|
+
avail_cap_frac=avail_cap_frac,
|
912
|
+
line_limited=line_limited,
|
913
|
+
**trans_costs,
|
914
|
+
)
|
915
|
+
)
|
916
|
+
|
917
|
+
cost = np.hstack(cost).astype("float32")
|
918
|
+
logger.info("LCOT cost calculation is complete.")
|
796
919
|
|
797
920
|
return cost
|
798
921
|
|
799
|
-
def compute_total_lcoe(
|
800
|
-
|
801
|
-
|
802
|
-
|
922
|
+
def compute_total_lcoe(
|
923
|
+
self,
|
924
|
+
fcr,
|
925
|
+
transmission_costs=None,
|
926
|
+
avail_cap_frac=1,
|
927
|
+
line_limited=False,
|
928
|
+
connectable=True,
|
929
|
+
max_workers=None,
|
930
|
+
consider_friction=True,
|
931
|
+
):
|
803
932
|
"""
|
804
933
|
Compute LCOT and total LCOE for all sc point to transmission feature
|
805
934
|
connections
|
@@ -828,45 +957,65 @@ class SupplyCurve:
|
|
828
957
|
Flag to consider friction layer on LCOE when "mean_lcoe_friction"
|
829
958
|
is in the sc points input, by default True
|
830
959
|
"""
|
831
|
-
|
960
|
+
tcc_per_mw_col = SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW
|
961
|
+
if tcc_per_mw_col in self._trans_table:
|
962
|
+
cost = self._trans_table[tcc_per_mw_col].values.copy()
|
963
|
+
elif "trans_cap_cost" not in self._trans_table:
|
832
964
|
scc = self._sc_capacity_col
|
833
|
-
cost = self._compute_trans_cap_cost(
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
965
|
+
cost = self._compute_trans_cap_cost(
|
966
|
+
self._trans_table,
|
967
|
+
trans_costs=transmission_costs,
|
968
|
+
avail_cap_frac=avail_cap_frac,
|
969
|
+
line_limited=line_limited,
|
970
|
+
connectable=connectable,
|
971
|
+
max_workers=max_workers,
|
972
|
+
sc_capacity_col=scc,
|
973
|
+
)
|
974
|
+
self._trans_table[tcc_per_mw_col] = cost # $/MW
|
841
975
|
else:
|
842
|
-
cost = self._trans_table[
|
843
|
-
cost /= self._trans_table[
|
844
|
-
self._trans_table[
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
976
|
+
cost = self._trans_table["trans_cap_cost"].values.copy() # $
|
977
|
+
cost /= self._trans_table[SupplyCurveField.CAPACITY_AC_MW] # $/MW
|
978
|
+
self._trans_table[tcc_per_mw_col] = cost
|
979
|
+
|
980
|
+
self._trans_table[tcc_per_mw_col] = (
|
981
|
+
self._trans_table[tcc_per_mw_col].astype("float32")
|
982
|
+
)
|
983
|
+
cost = cost.astype("float32")
|
984
|
+
cf_mean_arr = self._trans_table[SupplyCurveField.MEAN_CF_AC]
|
985
|
+
cf_mean_arr = cf_mean_arr.values.astype("float32")
|
986
|
+
resource_lcoe = self._trans_table[SupplyCurveField.MEAN_LCOE]
|
987
|
+
resource_lcoe = resource_lcoe.values.astype("float32")
|
988
|
+
|
989
|
+
if 'reinforcement_cost_floored_per_mw' in self._trans_table:
|
990
|
+
logger.info("'reinforcement_cost_floored_per_mw' column found in "
|
991
|
+
"transmission table. Adding floored reinforcement "
|
992
|
+
"cost LCOE as sorting option.")
|
993
|
+
fr_cost = (self._trans_table['reinforcement_cost_floored_per_mw']
|
994
|
+
.values.copy())
|
995
|
+
|
996
|
+
lcot_fr = ((cost + fr_cost) * fcr) / (cf_mean_arr * 8760)
|
997
|
+
lcoe_fr = lcot_fr + resource_lcoe
|
998
|
+
self._trans_table['lcot_floored_reinforcement'] = lcot_fr
|
999
|
+
self._trans_table['lcoe_floored_reinforcement'] = lcoe_fr
|
1000
|
+
|
1001
|
+
if SupplyCurveField.REINFORCEMENT_COST_PER_MW in self._trans_table:
|
1002
|
+
logger.info("%s column found in transmission table. Adding "
|
1003
|
+
"reinforcement costs to total LCOE.",
|
1004
|
+
SupplyCurveField.REINFORCEMENT_COST_PER_MW)
|
1005
|
+
lcot_nr = (cost * fcr) / (cf_mean_arr * 8760)
|
1006
|
+
lcoe_nr = lcot_nr + resource_lcoe
|
1007
|
+
self._trans_table['lcot_no_reinforcement'] = lcot_nr
|
1008
|
+
self._trans_table['lcoe_no_reinforcement'] = lcoe_nr
|
1009
|
+
|
1010
|
+
col_name = SupplyCurveField.REINFORCEMENT_COST_PER_MW
|
1011
|
+
r_cost = self._trans_table[col_name].astype("float32")
|
1012
|
+
r_cost = r_cost.values.copy()
|
1013
|
+
self._trans_table[tcc_per_mw_col] += r_cost
|
862
1014
|
cost += r_cost # $/MW
|
863
1015
|
|
864
|
-
cf_mean_arr = self._trans_table['mean_cf'].values
|
865
1016
|
lcot = (cost * fcr) / (cf_mean_arr * 8760)
|
866
|
-
|
867
|
-
self._trans_table[
|
868
|
-
self._trans_table['total_lcoe'] = (self._trans_table['lcot']
|
869
|
-
+ self._trans_table['mean_lcoe'])
|
1017
|
+
self._trans_table[SupplyCurveField.LCOT] = lcot
|
1018
|
+
self._trans_table[SupplyCurveField.TOTAL_LCOE] = lcot + resource_lcoe
|
870
1019
|
|
871
1020
|
if consider_friction:
|
872
1021
|
self._calculate_total_lcoe_friction()
|
@@ -875,15 +1024,19 @@ class SupplyCurve:
|
|
875
1024
|
"""Look for site mean LCOE with friction in the trans table and if
|
876
1025
|
found make a total LCOE column with friction."""
|
877
1026
|
|
878
|
-
if
|
879
|
-
lcoe_friction = (
|
880
|
-
|
881
|
-
|
1027
|
+
if SupplyCurveField.MEAN_LCOE_FRICTION in self._trans_table:
|
1028
|
+
lcoe_friction = (
|
1029
|
+
self._trans_table[SupplyCurveField.LCOT]
|
1030
|
+
+ self._trans_table[SupplyCurveField.MEAN_LCOE_FRICTION])
|
1031
|
+
self._trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION] = (
|
1032
|
+
lcoe_friction
|
1033
|
+
)
|
882
1034
|
logger.info('Found mean LCOE with friction. Adding key '
|
883
1035
|
'"total_lcoe_friction" to trans table.')
|
884
1036
|
|
885
|
-
def _exclude_noncompetitive_wind_farms(
|
886
|
-
|
1037
|
+
def _exclude_noncompetitive_wind_farms(
|
1038
|
+
self, comp_wind_dirs, sc_gid, downwind=False
|
1039
|
+
):
|
887
1040
|
"""
|
888
1041
|
Exclude non-competitive wind farms for given sc_gid
|
889
1042
|
|
@@ -905,18 +1058,20 @@ class SupplyCurve:
|
|
905
1058
|
gid = comp_wind_dirs.check_sc_gid(sc_gid)
|
906
1059
|
if gid is not None:
|
907
1060
|
if comp_wind_dirs.mask[gid]:
|
908
|
-
exclude_gids = comp_wind_dirs[
|
1061
|
+
exclude_gids = comp_wind_dirs["upwind", gid]
|
909
1062
|
if downwind:
|
910
|
-
exclude_gids = np.append(
|
911
|
-
|
1063
|
+
exclude_gids = np.append(
|
1064
|
+
exclude_gids, comp_wind_dirs["downwind", gid]
|
1065
|
+
)
|
912
1066
|
for n in exclude_gids:
|
913
1067
|
check = comp_wind_dirs.exclude_sc_point_gid(n)
|
914
1068
|
if check:
|
915
|
-
sc_gids = comp_wind_dirs[
|
1069
|
+
sc_gids = comp_wind_dirs[SupplyCurveField.SC_GID, n]
|
916
1070
|
for sc_id in sc_gids:
|
917
1071
|
if self._mask[sc_id]:
|
918
|
-
logger.debug(
|
919
|
-
|
1072
|
+
logger.debug(
|
1073
|
+
"Excluding sc_gid {}".format(sc_id)
|
1074
|
+
)
|
920
1075
|
self._mask[sc_id] = False
|
921
1076
|
|
922
1077
|
return comp_wind_dirs
|
@@ -945,8 +1100,11 @@ class SupplyCurve:
|
|
945
1100
|
missing = [s for s in sum_labels if s not in table]
|
946
1101
|
|
947
1102
|
if any(missing):
|
948
|
-
logger.info(
|
949
|
-
|
1103
|
+
logger.info(
|
1104
|
+
'Could not make sum column "{}", missing: {}'.format(
|
1105
|
+
new_label, missing
|
1106
|
+
)
|
1107
|
+
)
|
950
1108
|
else:
|
951
1109
|
sum_arr = np.zeros(len(table))
|
952
1110
|
for s in sum_labels:
|
@@ -958,13 +1116,26 @@ class SupplyCurve:
|
|
958
1116
|
|
959
1117
|
return table
|
960
1118
|
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
1119
|
+
# pylint: disable=C901
|
1120
|
+
def _full_sort(
|
1121
|
+
self,
|
1122
|
+
trans_table,
|
1123
|
+
trans_costs=None,
|
1124
|
+
avail_cap_frac=1,
|
1125
|
+
comp_wind_dirs=None,
|
1126
|
+
total_lcoe_fric=None,
|
1127
|
+
sort_on=SupplyCurveField.TOTAL_LCOE,
|
1128
|
+
columns=(
|
1129
|
+
SupplyCurveField.TRANS_GID,
|
1130
|
+
SupplyCurveField.TRANS_CAPACITY,
|
1131
|
+
SupplyCurveField.TRANS_TYPE,
|
1132
|
+
SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
|
1133
|
+
SupplyCurveField.DIST_SPUR_KM,
|
1134
|
+
SupplyCurveField.LCOT,
|
1135
|
+
SupplyCurveField.TOTAL_LCOE,
|
1136
|
+
),
|
1137
|
+
downwind=False,
|
1138
|
+
):
|
968
1139
|
"""
|
969
1140
|
Internal method to handle full supply curve sorting
|
970
1141
|
|
@@ -1002,9 +1173,11 @@ class SupplyCurve:
|
|
1002
1173
|
Updated sc_points table with transmission connections, LCOT
|
1003
1174
|
and LCOE+LCOT based on full supply curve connections
|
1004
1175
|
"""
|
1005
|
-
trans_features = self._create_handler(
|
1006
|
-
|
1007
|
-
|
1176
|
+
trans_features = self._create_handler(
|
1177
|
+
self._trans_table,
|
1178
|
+
trans_costs=trans_costs,
|
1179
|
+
avail_cap_frac=avail_cap_frac,
|
1180
|
+
)
|
1008
1181
|
init_list = [np.nan] * int(1 + np.max(self._sc_gids))
|
1009
1182
|
columns = list(columns)
|
1010
1183
|
if sort_on not in columns:
|
@@ -1012,22 +1185,23 @@ class SupplyCurve:
|
|
1012
1185
|
|
1013
1186
|
conn_lists = {k: deepcopy(init_list) for k in columns}
|
1014
1187
|
|
1015
|
-
trans_sc_gids = trans_table[
|
1188
|
+
trans_sc_gids = trans_table[SupplyCurveField.SC_GID].values.astype(int)
|
1016
1189
|
|
1017
1190
|
# syntax is final_key: source_key (source from trans_table)
|
1018
|
-
all_cols =
|
1019
|
-
essentials =
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1191
|
+
all_cols = list(columns)
|
1192
|
+
essentials = [SupplyCurveField.TRANS_GID,
|
1193
|
+
SupplyCurveField.TRANS_CAPACITY,
|
1194
|
+
SupplyCurveField.TRANS_TYPE,
|
1195
|
+
SupplyCurveField.DIST_SPUR_KM,
|
1196
|
+
SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
|
1197
|
+
SupplyCurveField.LCOT,
|
1198
|
+
SupplyCurveField.TOTAL_LCOE]
|
1199
|
+
|
1200
|
+
for col in essentials:
|
1201
|
+
if col not in all_cols:
|
1202
|
+
all_cols.append(col)
|
1203
|
+
|
1204
|
+
arrays = {col: trans_table[col].values for col in all_cols}
|
1031
1205
|
|
1032
1206
|
sc_capacities = trans_table[self._sc_capacity_col].values
|
1033
1207
|
|
@@ -1036,52 +1210,62 @@ class SupplyCurve:
|
|
1036
1210
|
for i in range(len(trans_table)):
|
1037
1211
|
sc_gid = trans_sc_gids[i]
|
1038
1212
|
if self._mask[sc_gid]:
|
1039
|
-
connect = trans_features.connect(
|
1040
|
-
|
1213
|
+
connect = trans_features.connect(
|
1214
|
+
arrays[SupplyCurveField.TRANS_GID][i], sc_capacities[i]
|
1215
|
+
)
|
1041
1216
|
if connect:
|
1042
1217
|
connected += 1
|
1043
|
-
logger.debug(
|
1218
|
+
logger.debug("Connecting sc gid {}".format(sc_gid))
|
1044
1219
|
self._mask[sc_gid] = False
|
1045
1220
|
|
1046
1221
|
for col_name, data_arr in arrays.items():
|
1047
1222
|
conn_lists[col_name][sc_gid] = data_arr[i]
|
1048
1223
|
|
1049
1224
|
if total_lcoe_fric is not None:
|
1050
|
-
|
1051
|
-
|
1225
|
+
col_name = SupplyCurveField.TOTAL_LCOE_FRICTION
|
1226
|
+
conn_lists[col_name][sc_gid] = total_lcoe_fric[i]
|
1052
1227
|
|
1053
1228
|
current_prog = connected // (len(self) / 100)
|
1054
1229
|
if current_prog > progress:
|
1055
1230
|
progress = current_prog
|
1056
|
-
logger.info(
|
1057
|
-
|
1231
|
+
logger.info(
|
1232
|
+
"{} % of supply curve points connected".format(
|
1233
|
+
progress
|
1234
|
+
)
|
1235
|
+
)
|
1058
1236
|
|
1059
1237
|
if comp_wind_dirs is not None:
|
1060
|
-
comp_wind_dirs =
|
1238
|
+
comp_wind_dirs = (
|
1061
1239
|
self._exclude_noncompetitive_wind_farms(
|
1062
|
-
comp_wind_dirs, sc_gid, downwind=downwind
|
1240
|
+
comp_wind_dirs, sc_gid, downwind=downwind
|
1241
|
+
)
|
1242
|
+
)
|
1063
1243
|
|
1064
1244
|
index = range(0, int(1 + np.max(self._sc_gids)))
|
1065
1245
|
connections = pd.DataFrame(conn_lists, index=index)
|
1066
|
-
connections.index.name =
|
1246
|
+
connections.index.name = SupplyCurveField.SC_GID
|
1067
1247
|
connections = connections.dropna(subset=[sort_on])
|
1068
1248
|
connections = connections[columns].reset_index()
|
1069
1249
|
|
1070
|
-
sc_gids = self._sc_points[
|
1071
|
-
connected = connections[
|
1250
|
+
sc_gids = self._sc_points[SupplyCurveField.SC_GID].values
|
1251
|
+
connected = connections[SupplyCurveField.SC_GID].values
|
1072
1252
|
logger.debug('Connected gids {} out of total supply curve gids {}'
|
1073
1253
|
.format(len(connected), len(sc_gids)))
|
1074
1254
|
unconnected = ~np.isin(sc_gids, connected)
|
1075
1255
|
unconnected = sc_gids[unconnected].tolist()
|
1076
1256
|
|
1077
1257
|
if unconnected:
|
1078
|
-
msg = (
|
1079
|
-
|
1080
|
-
|
1258
|
+
msg = (
|
1259
|
+
"{} supply curve points were not connected to tranmission! "
|
1260
|
+
"Unconnected sc_gid's: {}".format(
|
1261
|
+
len(unconnected), unconnected
|
1262
|
+
)
|
1263
|
+
)
|
1081
1264
|
logger.warning(msg)
|
1082
1265
|
warn(msg)
|
1083
1266
|
|
1084
|
-
supply_curve = self._sc_points.merge(
|
1267
|
+
supply_curve = self._sc_points.merge(
|
1268
|
+
connections, on=SupplyCurveField.SC_GID)
|
1085
1269
|
|
1086
1270
|
return supply_curve.reset_index(drop=True)
|
1087
1271
|
|
@@ -1090,42 +1274,75 @@ class SupplyCurve:
|
|
1090
1274
|
Add the transmission connection feature capacity to the trans table if
|
1091
1275
|
needed
|
1092
1276
|
"""
|
1093
|
-
if
|
1094
|
-
kwargs = {
|
1277
|
+
if SupplyCurveField.TRANS_CAPACITY not in self._trans_table:
|
1278
|
+
kwargs = {"avail_cap_frac": avail_cap_frac}
|
1095
1279
|
fc = TF.feature_capacity(self._trans_table, **kwargs)
|
1096
|
-
self._trans_table = self._trans_table.merge(
|
1280
|
+
self._trans_table = self._trans_table.merge(
|
1281
|
+
fc, on=SupplyCurveField.TRANS_GID)
|
1097
1282
|
|
1098
1283
|
def _adjust_output_columns(self, columns, consider_friction):
|
1099
|
-
"""Add extra output columns, if needed.
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1284
|
+
"""Add extra output columns, if needed."""
|
1285
|
+
|
1286
|
+
for col in _REQUIRED_COMPUTE_AND_OUTPUT_COLS:
|
1287
|
+
if col not in columns:
|
1288
|
+
columns.append(col)
|
1289
|
+
|
1290
|
+
for col in _REQUIRED_OUTPUT_COLS:
|
1291
|
+
if col not in self._trans_table:
|
1292
|
+
self._trans_table[col] = None
|
1293
|
+
if col not in columns:
|
1294
|
+
columns.append(col)
|
1295
|
+
|
1296
|
+
missing_cols = [col for col in columns if col not in self._trans_table]
|
1297
|
+
if missing_cols:
|
1298
|
+
msg = (f"The following requested columns are not found in "
|
1299
|
+
f"transmission table: {missing_cols}.\nSkipping...")
|
1300
|
+
logger.warning(msg)
|
1301
|
+
warn(msg)
|
1108
1302
|
|
1109
|
-
|
1110
|
-
if col in self._trans_table and col not in columns]
|
1303
|
+
columns = [col for col in columns if col in self._trans_table]
|
1111
1304
|
|
1112
|
-
|
1305
|
+
fric_col = SupplyCurveField.TOTAL_LCOE_FRICTION
|
1306
|
+
if consider_friction and fric_col in self._trans_table:
|
1307
|
+
columns.append(fric_col)
|
1308
|
+
|
1309
|
+
return sorted(columns, key=_column_sort_key)
|
1113
1310
|
|
1114
1311
|
def _determine_sort_on(self, sort_on):
|
1115
1312
|
"""Determine the `sort_on` column from user input and trans table"""
|
1116
|
-
|
1313
|
+
r_cost_col = SupplyCurveField.REINFORCEMENT_COST_PER_MW
|
1314
|
+
found_reinforcement_costs = (
|
1315
|
+
r_cost_col in self._trans_table
|
1316
|
+
and not self._trans_table[r_cost_col].isna().all()
|
1317
|
+
)
|
1318
|
+
if found_reinforcement_costs:
|
1117
1319
|
sort_on = sort_on or "lcoe_no_reinforcement"
|
1118
|
-
return sort_on or
|
1119
|
-
|
1120
|
-
def full_sort(
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1320
|
+
return sort_on or SupplyCurveField.TOTAL_LCOE
|
1321
|
+
|
1322
|
+
def full_sort(
|
1323
|
+
self,
|
1324
|
+
fcr,
|
1325
|
+
transmission_costs=None,
|
1326
|
+
avail_cap_frac=1,
|
1327
|
+
line_limited=False,
|
1328
|
+
connectable=True,
|
1329
|
+
max_workers=None,
|
1330
|
+
consider_friction=True,
|
1331
|
+
sort_on=None,
|
1332
|
+
columns=(
|
1333
|
+
SupplyCurveField.TRANS_GID,
|
1334
|
+
SupplyCurveField.TRANS_CAPACITY,
|
1335
|
+
SupplyCurveField.TRANS_TYPE,
|
1336
|
+
SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
|
1337
|
+
SupplyCurveField.DIST_SPUR_KM,
|
1338
|
+
SupplyCurveField.LCOT,
|
1339
|
+
SupplyCurveField.TOTAL_LCOE,
|
1340
|
+
),
|
1341
|
+
wind_dirs=None,
|
1342
|
+
n_dirs=2,
|
1343
|
+
downwind=False,
|
1344
|
+
offshore_compete=False,
|
1345
|
+
):
|
1129
1346
|
"""
|
1130
1347
|
run full supply curve sorting
|
1131
1348
|
|
@@ -1180,14 +1397,17 @@ class SupplyCurve:
|
|
1180
1397
|
Updated sc_points table with transmission connections, LCOT
|
1181
1398
|
and LCOE+LCOT based on full supply curve connections
|
1182
1399
|
"""
|
1183
|
-
logger.info(
|
1400
|
+
logger.info("Starting full competitive supply curve sort.")
|
1184
1401
|
self._check_substation_conns(self._trans_table)
|
1185
|
-
self.compute_total_lcoe(
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1402
|
+
self.compute_total_lcoe(
|
1403
|
+
fcr,
|
1404
|
+
transmission_costs=transmission_costs,
|
1405
|
+
avail_cap_frac=avail_cap_frac,
|
1406
|
+
line_limited=line_limited,
|
1407
|
+
connectable=connectable,
|
1408
|
+
max_workers=max_workers,
|
1409
|
+
consider_friction=consider_friction,
|
1410
|
+
)
|
1191
1411
|
self._check_feature_capacity(avail_cap_frac=avail_cap_frac)
|
1192
1412
|
|
1193
1413
|
if isinstance(columns, tuple):
|
@@ -1197,12 +1417,16 @@ class SupplyCurve:
|
|
1197
1417
|
sort_on = self._determine_sort_on(sort_on)
|
1198
1418
|
|
1199
1419
|
trans_table = self._trans_table.copy()
|
1200
|
-
pos = trans_table[
|
1201
|
-
trans_table = trans_table.loc[~pos].sort_values(
|
1420
|
+
pos = trans_table[SupplyCurveField.LCOT].isnull()
|
1421
|
+
trans_table = trans_table.loc[~pos].sort_values(
|
1422
|
+
[sort_on, SupplyCurveField.TRANS_GID]
|
1423
|
+
)
|
1202
1424
|
|
1203
1425
|
total_lcoe_fric = None
|
1204
|
-
|
1205
|
-
|
1426
|
+
col_in_table = SupplyCurveField.MEAN_LCOE_FRICTION in trans_table
|
1427
|
+
if consider_friction and col_in_table:
|
1428
|
+
total_lcoe_fric = \
|
1429
|
+
trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION].values
|
1206
1430
|
|
1207
1431
|
comp_wind_dirs = None
|
1208
1432
|
if wind_dirs is not None:
|
@@ -1216,28 +1440,40 @@ class SupplyCurve:
|
|
1216
1440
|
|
1217
1441
|
msg += " windfarms"
|
1218
1442
|
logger.info(msg)
|
1219
|
-
comp_wind_dirs = CompetitiveWindFarms(
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1443
|
+
comp_wind_dirs = CompetitiveWindFarms(
|
1444
|
+
wind_dirs,
|
1445
|
+
self._sc_points,
|
1446
|
+
n_dirs=n_dirs,
|
1447
|
+
offshore=offshore_compete,
|
1448
|
+
)
|
1449
|
+
|
1450
|
+
supply_curve = self._full_sort(
|
1451
|
+
trans_table,
|
1452
|
+
trans_costs=transmission_costs,
|
1453
|
+
avail_cap_frac=avail_cap_frac,
|
1454
|
+
comp_wind_dirs=comp_wind_dirs,
|
1455
|
+
total_lcoe_fric=total_lcoe_fric,
|
1456
|
+
sort_on=sort_on,
|
1457
|
+
columns=columns,
|
1458
|
+
downwind=downwind,
|
1459
|
+
)
|
1231
1460
|
|
1232
1461
|
return supply_curve
|
1233
1462
|
|
1234
|
-
def simple_sort(
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1463
|
+
def simple_sort(
|
1464
|
+
self,
|
1465
|
+
fcr,
|
1466
|
+
transmission_costs=None,
|
1467
|
+
avail_cap_frac=1,
|
1468
|
+
max_workers=None,
|
1469
|
+
consider_friction=True,
|
1470
|
+
sort_on=None,
|
1471
|
+
columns=DEFAULT_COLUMNS,
|
1472
|
+
wind_dirs=None,
|
1473
|
+
n_dirs=2,
|
1474
|
+
downwind=False,
|
1475
|
+
offshore_compete=False,
|
1476
|
+
):
|
1241
1477
|
"""
|
1242
1478
|
Run simple supply curve sorting that does not take into account
|
1243
1479
|
available capacity
|
@@ -1271,9 +1507,8 @@ class SupplyCurve:
|
|
1271
1507
|
will be built first, by default `None`, which will use
|
1272
1508
|
total LCOE without any reinforcement costs as the sort value.
|
1273
1509
|
columns : list | tuple, optional
|
1274
|
-
Columns to preserve in output connections dataframe
|
1275
|
-
|
1276
|
-
'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe')
|
1510
|
+
Columns to preserve in output connections dataframe.
|
1511
|
+
By default, :obj:`DEFAULT_COLUMNS`.
|
1277
1512
|
wind_dirs : pandas.DataFrame | str, optional
|
1278
1513
|
path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
|
1279
1514
|
the neighboring supply curve point gids and power-rose value at
|
@@ -1292,47 +1527,57 @@ class SupplyCurve:
|
|
1292
1527
|
Updated sc_points table with transmission connections, LCOT
|
1293
1528
|
and LCOE+LCOT based on simple supply curve connections
|
1294
1529
|
"""
|
1295
|
-
logger.info(
|
1296
|
-
self.compute_total_lcoe(
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1530
|
+
logger.info("Starting simple supply curve sort (no capacity limits).")
|
1531
|
+
self.compute_total_lcoe(
|
1532
|
+
fcr,
|
1533
|
+
transmission_costs=transmission_costs,
|
1534
|
+
avail_cap_frac=avail_cap_frac,
|
1535
|
+
connectable=False,
|
1536
|
+
max_workers=max_workers,
|
1537
|
+
consider_friction=consider_friction,
|
1538
|
+
)
|
1539
|
+
sort_on = self._determine_sort_on(sort_on)
|
1302
1540
|
|
1303
1541
|
if isinstance(columns, tuple):
|
1304
1542
|
columns = list(columns)
|
1305
|
-
|
1306
1543
|
columns = self._adjust_output_columns(columns, consider_friction)
|
1307
|
-
sort_on = self._determine_sort_on(sort_on)
|
1308
1544
|
|
1309
|
-
|
1310
|
-
connections =
|
1311
|
-
|
1312
|
-
|
1313
|
-
connections = connections.rename(columns=rename)
|
1545
|
+
trans_table = self._trans_table.copy()
|
1546
|
+
connections = trans_table.sort_values(
|
1547
|
+
[sort_on, SupplyCurveField.TRANS_GID])
|
1548
|
+
connections = connections.groupby(SupplyCurveField.SC_GID).first()
|
1314
1549
|
connections = connections[columns].reset_index()
|
1315
1550
|
|
1316
|
-
supply_curve = self._sc_points.merge(connections,
|
1551
|
+
supply_curve = self._sc_points.merge(connections,
|
1552
|
+
on=SupplyCurveField.SC_GID)
|
1317
1553
|
if wind_dirs is not None:
|
1318
|
-
supply_curve =
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1554
|
+
supply_curve = CompetitiveWindFarms.run(
|
1555
|
+
wind_dirs,
|
1556
|
+
supply_curve,
|
1557
|
+
n_dirs=n_dirs,
|
1558
|
+
offshore=offshore_compete,
|
1559
|
+
sort_on=sort_on,
|
1560
|
+
downwind=downwind,
|
1561
|
+
)
|
1325
1562
|
|
1326
1563
|
supply_curve = supply_curve.reset_index(drop=True)
|
1327
1564
|
|
1328
1565
|
return supply_curve
|
1329
1566
|
|
1330
|
-
def run(
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1567
|
+
def run(
|
1568
|
+
self,
|
1569
|
+
out_fpath,
|
1570
|
+
fixed_charge_rate,
|
1571
|
+
simple=True,
|
1572
|
+
avail_cap_frac=1,
|
1573
|
+
line_limited=False,
|
1574
|
+
transmission_costs=None,
|
1575
|
+
consider_friction=True,
|
1576
|
+
sort_on=None,
|
1577
|
+
columns=DEFAULT_COLUMNS,
|
1578
|
+
max_workers=None,
|
1579
|
+
competition=None,
|
1580
|
+
):
|
1336
1581
|
"""Run Supply Curve Transmission calculations.
|
1337
1582
|
|
1338
1583
|
Run full supply curve taking into account available capacity of
|
@@ -1397,8 +1642,7 @@ class SupplyCurve:
|
|
1397
1642
|
By default ``None``.
|
1398
1643
|
columns : list | tuple, optional
|
1399
1644
|
Columns to preserve in output supply curve dataframe.
|
1400
|
-
By default,
|
1401
|
-
'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe')``.
|
1645
|
+
By default, :obj:`DEFAULT_COLUMNS`.
|
1402
1646
|
max_workers : int, optional
|
1403
1647
|
Number of workers to use to compute LCOT. If > 1,
|
1404
1648
|
computation is run in parallel. If ``None``, computation
|
@@ -1433,12 +1677,14 @@ class SupplyCurve:
|
|
1433
1677
|
str
|
1434
1678
|
Path to output supply curve.
|
1435
1679
|
"""
|
1436
|
-
kwargs = {
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1680
|
+
kwargs = {
|
1681
|
+
"fcr": fixed_charge_rate,
|
1682
|
+
"transmission_costs": transmission_costs,
|
1683
|
+
"consider_friction": consider_friction,
|
1684
|
+
"sort_on": sort_on,
|
1685
|
+
"columns": columns,
|
1686
|
+
"max_workers": max_workers,
|
1687
|
+
}
|
1442
1688
|
kwargs.update(competition or {})
|
1443
1689
|
|
1444
1690
|
if simple:
|
@@ -1457,8 +1703,19 @@ class SupplyCurve:
|
|
1457
1703
|
def _format_sc_out_fpath(out_fpath):
|
1458
1704
|
"""Add CSV file ending and replace underscore, if necessary."""
|
1459
1705
|
if not out_fpath.endswith(".csv"):
|
1460
|
-
out_fpath =
|
1706
|
+
out_fpath = "{}.csv".format(out_fpath)
|
1461
1707
|
|
1462
1708
|
project_dir, out_fn = os.path.split(out_fpath)
|
1463
1709
|
out_fn = out_fn.replace("supply_curve", "supply-curve")
|
1464
1710
|
return os.path.join(project_dir, out_fn)
|
1711
|
+
|
1712
|
+
|
1713
|
+
def _column_sort_key(col):
|
1714
|
+
"""Determine the sort order of the input column. """
|
1715
|
+
col_value = _REQUIRED_COMPUTE_AND_OUTPUT_COLS.get(col)
|
1716
|
+
if col_value is None:
|
1717
|
+
col_value = _REQUIRED_OUTPUT_COLS.get(col)
|
1718
|
+
if col_value is None:
|
1719
|
+
col_value = 1e6
|
1720
|
+
|
1721
|
+
return col_value, str(col)
|