NREL-reV 0.14.4__py3-none-any.whl → 0.14.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NREL-reV
3
- Version: 0.14.4
3
+ Version: 0.14.5
4
4
  Summary: National Renewable Energy Laboratory's (NREL's) Renewable Energy Potential(V) Model: reV
5
5
  Author-email: Galen Maclaurin <galen.maclaurin@nrel.gov>
6
6
  Maintainer-email: Grant Buster <gbuster@nrel.gov>, Paul Pinchuk <ppinchuk@nrel.gov>
@@ -1,7 +1,7 @@
1
- nrel_rev-0.14.4.dist-info/licenses/LICENSE,sha256=hDwoTANtan2ZpufBlXm5C3W_PJ-mCqItvlcobgjxL7k,1526
1
+ nrel_rev-0.14.5.dist-info/licenses/LICENSE,sha256=hDwoTANtan2ZpufBlXm5C3W_PJ-mCqItvlcobgjxL7k,1526
2
2
  reV/__init__.py,sha256=tXTpWu_qVo3uotfSw_TJ-gNbidGaIPPfUTwBlpCMJ-g,856
3
- reV/cli.py,sha256=u7G5M5moA7q8fCgC_1MB30Z7R14GNcngVf6eVNkfQU8,1682
4
- reV/version.py,sha256=oIdidsSwDY_HSSZ-2OPfa9VlyBv1QAecZqvfdONcImU,51
3
+ reV/cli.py,sha256=tkJzwkGNz0h7PfKpjUAv6aM-Jpn_z8jUbXyjb5GCFBM,1792
4
+ reV/version.py,sha256=DNYUQmgQgGdiJDrMQAgH6IJ-51uB5WIunyqwp0YBFMM,51
5
5
  reV/SAM/SAM.py,sha256=3NK9rRaJzqH6wz7CU_5XguKCRhmoilOpDdFFsFgOaxQ,33163
6
6
  reV/SAM/__init__.py,sha256=LJqoncyKDY5ZP5WA4kboh561bce11F9Ge645Izah0EY,240
7
7
  reV/SAM/defaults.py,sha256=JQMJomX7wsbMzxKjx_IMnA_9QFsV2yWCyl_JToDVSJo,6703
@@ -16,11 +16,11 @@ reV/SAM/defaults/WY Southern-Flat Lands.srw,sha256=oSlFI7nnycW7SMnTXEXBXYkoh8rhV
16
16
  reV/SAM/defaults/geothermal.json,sha256=gnlxOBxxkbDDacIw6B9yP9iRkfmwj0WTWs8Zb71ytCo,7119
17
17
  reV/SAM/defaults/i_pvwattsv5.json,sha256=sF8rSe1bcqsKLXchvRxlp25dXOVe_q7rPJTf_vWR20Y,312
18
18
  reV/bespoke/__init__.py,sha256=vpXbyBUrUsTgK8UP_LafMjLiDg2CRG9WZLHPsOJoxek,109
19
- reV/bespoke/bespoke.py,sha256=PLs42cTGRrCihBZRO_6VR6eByGgTRBwLOEiXwPiz7dg,114863
19
+ reV/bespoke/bespoke.py,sha256=fZvNpNeKf8LYQbDDhX4I8iOgm_3MGdCf0aq-wzQcflo,116916
20
20
  reV/bespoke/cli_bespoke.py,sha256=b6Xu0GKpXqPX3qVJ6-z0FrO97uCsH_1dVOa4r6IvesQ,2911
21
21
  reV/bespoke/gradient_free.py,sha256=URWV1yiO2jyWk3_GOpfpLV_wlgJhXXGmTUwCB3WTV0Y,12015
22
22
  reV/bespoke/pack_turbs.py,sha256=Dcd9F8obF8LPztzeycB5kxa5hXKSCiz3jC1WeAFUx28,3508
23
- reV/bespoke/place_turbines.py,sha256=f1Or9X1b2_XOkhUgRNXZwpF_xZQTw1LFL2O2SRe1p1E,25883
23
+ reV/bespoke/place_turbines.py,sha256=pc5JofO9LAwwsQDGbxauGOoMeO6TXVr9uXABgVAADw0,27121
24
24
  reV/config/__init__.py,sha256=oqFNU4JESU_fPxFmPyQNFAXLDAdzmTlPuabXTe3Rf2Y,92
25
25
  reV/config/base_analysis_config.py,sha256=NvA3g5zQz8mIrV8ZSENLq0XBZGXa6RTGkwpZ76TVZj8,5615
26
26
  reV/config/base_config.py,sha256=a748VQ3CRs9RVi5sSEPcaWOyH6R3t5tssaFqZntHyaE,10075
@@ -28,7 +28,7 @@ reV/config/cli_project_points.py,sha256=6edOlLNOG-ZEbcpNS2MPfu-DXjcOTEh_MEvKOvQS
28
28
  reV/config/curtailment.py,sha256=1bH7xzxOmD4PwLKcXFNotMAa9iCfGBUm2DKTOXViCJg,5548
29
29
  reV/config/execution.py,sha256=hyf8W7XYUXE6tXBXs-4En7h_aDTYu8FzbslgBsKLJkQ,5046
30
30
  reV/config/output_request.py,sha256=cww5MxOUnXE_HwwLYitsFSwwUT8mWkctSElq9-zUl_8,4725
31
- reV/config/project_points.py,sha256=CSq7lHgWfJ1pq1XlWJ5eykmGGhumkxVcg__Ln9RR4-Y,41886
31
+ reV/config/project_points.py,sha256=BdLwWrESsDZDQJ59rki-1Z0xXF4QUXZkbB8nsPrtOEA,42147
32
32
  reV/config/sam_config.py,sha256=xvvx2FTuliq0Sk-BjRE3I9zdDmIdwHVBnWtXCcsoc40,7998
33
33
  reV/econ/__init__.py,sha256=UId1LNaAP9lErCEXVce6JZf0qVRUvwNFOPrajdRevGo,130
34
34
  reV/econ/cli_econ.py,sha256=2KNy3JQD0EKjStaoD2r6nv3ELFw88h2E_up-UKj_sfE,4286
@@ -75,25 +75,25 @@ reV/rep_profiles/cli_rep_profiles.py,sha256=i3fRolT7HTzTQePXpNLDyxwgpRa7FEfHO1mO
75
75
  reV/rep_profiles/rep_profiles.py,sha256=dJ2jcklbelYYNS3IUXIYHg8E0poXLOWrwdqeBBnqYIE,48271
76
76
  reV/supply_curve/__init__.py,sha256=dbf0cO0qmt1VhV8dnDddztrcpnwua9T2HBhM6tLKaP8,274
77
77
  reV/supply_curve/aggregation.py,sha256=DvIUmj0PbZ8NxgOSmpXvzn9wJnOiQO2Xm2w_WCRVK-U,40908
78
- reV/supply_curve/cli_sc_aggregation.py,sha256=iDBHlUUPg-Ce4N4Rqb4scn8A_Ygq2q4i9v_ypbH-krs,3387
78
+ reV/supply_curve/cli_sc_aggregation.py,sha256=fJwhOixN3VlzImHStWfcB8Bum_POY-RRgkhWi8Slgao,4633
79
79
  reV/supply_curve/cli_supply_curve.py,sha256=e-XrHQIe4OqWTL6u-TUAyHrw7Alk7vkXQ2HoLbE3zTM,2163
80
80
  reV/supply_curve/cli_tech_mapping.py,sha256=Dirn4JOmu_3BIP7WgcRLAepsreKqmDhChHLPEUqcDAo,1735
81
81
  reV/supply_curve/competitive_wind_farms.py,sha256=eOjM72-4oWtsqxB7Wh2gnB2zVAt4LY3iPE_DqWdXbQ4,15795
82
- reV/supply_curve/exclusions.py,sha256=4-ZxTO5Vlu03vie0V_74uvdajQfCuC8FE96Pg8I4U_c,42950
82
+ reV/supply_curve/exclusions.py,sha256=dzwgcXyDg5_MrcxKohW-j5YX7tWW6JlQimkUwLlhz2k,44056
83
83
  reV/supply_curve/extent.py,sha256=168Vcr2XQNkoLLn3xCLfvE3udi39wbtQ7GAbvw1yys0,17917
84
- reV/supply_curve/points.py,sha256=d3dPZio1PAbc0qvLJ1CqzqbcUwqsKMQK2wxbek4_CJ8,93937
85
- reV/supply_curve/sc_aggregation.py,sha256=l7uXs-grLb0enblPNsEDKccSP11WZ2Pah5baWtgtvzE,68067
84
+ reV/supply_curve/points.py,sha256=dl518Bv_i2jOM1N-BAVZcK3chmv23BphvHO47zcbTxU,94142
85
+ reV/supply_curve/sc_aggregation.py,sha256=VaFKS_EIPqlxlpFlxUiZiI8q_mJWdamh9fSuF6LXm28,70843
86
86
  reV/supply_curve/supply_curve.py,sha256=9zhAA_9XSxE18j1Z9FuC71Wr3I0VuakfR5mt1_gHYuU,69931
87
- reV/supply_curve/tech_mapping.py,sha256=WP-o5Sk5q1Jz_F9Rpe77_fvTYGH8R4GnYDfRMSTuHB8,17804
88
- reV/utilities/__init__.py,sha256=ii0xiebw0PNg5Eq9kYcLrX9M3yVglyxnn9_fP_w4ILA,10871
87
+ reV/supply_curve/tech_mapping.py,sha256=xP8j2KMY58kZ5MSH13nIs8IoDtFhNCzGY4hQeKxW33w,17878
88
+ reV/utilities/__init__.py,sha256=b99Su_feREbuKkP71W_NrlO7GH9vQRsH7TrP7f4-Eg0,22703
89
89
  reV/utilities/_clean_readme.py,sha256=IFI9wGPX5nnLTNVLJzH8IOHq9unQlAlHRu4Namib0LA,709
90
- reV/utilities/cli_functions.py,sha256=1_T_sXz0Ct8lW-vOk3mMRcpD6NYsc9cGI7dEujIi9z4,3864
90
+ reV/utilities/cli_functions.py,sha256=3q7d0MHNpb6vnzWSxQMPNXH176nGp_CHMgHCLknmv1k,5154
91
91
  reV/utilities/curtailment.py,sha256=As902-2aLGnCiVEutYfAFIOwuV--_rCQhxGNOY9RB-4,5241
92
92
  reV/utilities/exceptions.py,sha256=f7sRGsbFLpmL6Caq_H1cD4GfVhnLMyvYUsLPA1UVDDE,3974
93
93
  reV/utilities/pytest_utils.py,sha256=spCw9yQ8KEYOkQZpCi9IEmaWIvIqHqbUPDXXNQJJ68U,3241
94
94
  reV/utilities/slots.py,sha256=xsw-JuUVZ0YeoCNuwP_HxGNxFMA4xRs1tuImXHIJqaU,2618
95
- nrel_rev-0.14.4.dist-info/METADATA,sha256=3lCV-lrlcYrhrUEtJbAKsj5e8kTahxjDhLujt2-XXqQ,10718
96
- nrel_rev-0.14.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- nrel_rev-0.14.4.dist-info/entry_points.txt,sha256=4IfJtZm2iMJwrbC8J0Or7VjZWnFpvCaHYVpvSWfIwDA,616
98
- nrel_rev-0.14.4.dist-info/top_level.txt,sha256=S6YF2ZYgXUB6n28SY0K2H8YB9tMJdXQ9CyQbo6VC89M,4
99
- nrel_rev-0.14.4.dist-info/RECORD,,
95
+ nrel_rev-0.14.5.dist-info/METADATA,sha256=UCWoXqsoas-MeRXDg1ytuva-qI6B3XDpY1MhGb3ew_g,10718
96
+ nrel_rev-0.14.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
+ nrel_rev-0.14.5.dist-info/entry_points.txt,sha256=4IfJtZm2iMJwrbC8J0Or7VjZWnFpvCaHYVpvSWfIwDA,616
98
+ nrel_rev-0.14.5.dist-info/top_level.txt,sha256=S6YF2ZYgXUB6n28SY0K2H8YB9tMJdXQ9CyQbo6VC89M,4
99
+ nrel_rev-0.14.5.dist-info/RECORD,,
reV/bespoke/bespoke.py CHANGED
@@ -310,7 +310,24 @@ class BespokeSinglePlant:
310
310
  cost ($) as evaluated by
311
311
  `balance_of_system_cost_function`
312
312
  - ``self.wind_plant``: the SAM wind plant object,
313
- through which all SAM variables can be accessed
313
+ through which all SAM variables can be accessed.
314
+
315
+ .. IMPORTANT::
316
+ When using the `self.wind_plant` variable,
317
+ DO NOT include quotes around variable names (keys).
318
+
319
+ - ❌ Wrong: ``self.wind_plant["annual_energy"]``
320
+ - ✅ Correct: ``self.wind_plant[annual_energy]``
321
+
322
+ .. IMPORTANT::
323
+ It's possible for SAM wind plant variables to be
324
+ ``None``, especially if something went wrong while
325
+ optimizing the wind plant layout. In this case,
326
+ your objective function may fail to evaluate and
327
+ terminate the program entirely. To avoid this, add
328
+ a default value for the variable in your objective
329
+ function, like so:
330
+ ``(self.wind_plant[annual_energy] or 0)``
314
331
 
315
332
  capital_cost_function : str
316
333
  The plant capital cost function as a string, must return the total
@@ -1265,6 +1282,14 @@ class BespokeSinglePlant:
1265
1282
  for k, v in plant.outputs.items():
1266
1283
  self._outputs[k + "-{}".format(year)] = v
1267
1284
 
1285
+ self._compute_output_means()
1286
+ self._add_extra_meta_columns()
1287
+ logger.debug("Timeseries analysis complete!")
1288
+
1289
+ return self.outputs
1290
+
1291
+ def _compute_output_means(self):
1292
+ """Compute time series means and store them in the outputs dict"""
1268
1293
  means = {}
1269
1294
  for k1, v1 in self._outputs.items():
1270
1295
  if isinstance(v1, Number) and parse_year(k1, option="boolean"):
@@ -1277,6 +1302,9 @@ class BespokeSinglePlant:
1277
1302
 
1278
1303
  self._outputs.update(means)
1279
1304
 
1305
+ def _add_extra_meta_columns(self):
1306
+ """Copy over some non-temporal datasets to meta"""
1307
+
1280
1308
  self._meta[SupplyCurveField.MEAN_RES] = self.res_df["windspeed"].mean()
1281
1309
  self._meta[SupplyCurveField.MEAN_CF_DC] = np.nan
1282
1310
  self._meta[SupplyCurveField.MEAN_CF_AC] = np.nan
@@ -1302,10 +1330,6 @@ class BespokeSinglePlant:
1302
1330
  self.outputs["annual_wake_loss_internal_percent-means"]
1303
1331
  )
1304
1332
 
1305
- logger.debug("Timeseries analysis complete!")
1306
-
1307
- return self.outputs
1308
-
1309
1333
  def run_plant_optimization(self):
1310
1334
  """Run the wind plant layout optimization and export outputs
1311
1335
  to outputs property.
@@ -1626,7 +1650,24 @@ class BespokeWindPlants(BaseAggregation):
1626
1650
  cost ($) as evaluated by
1627
1651
  `balance_of_system_cost_function`
1628
1652
  - ``self.wind_plant``: the SAM wind plant object,
1629
- through which all SAM variables can be accessed
1653
+ through which all SAM variables can be accessed.
1654
+
1655
+ .. IMPORTANT::
1656
+ When using the `self.wind_plant` variable,
1657
+ DO NOT include quotes around variable names (keys).
1658
+
1659
+ - ❌ Wrong: ``self.wind_plant["annual_energy"]``
1660
+ - ✅ Correct: ``self.wind_plant[annual_energy]``
1661
+
1662
+ .. IMPORTANT::
1663
+ It's possible for SAM wind plant variables to be
1664
+ ``None``, especially if something went wrong while
1665
+ optimizing the wind plant layout. In this case,
1666
+ your objective function may fail to evaluate and
1667
+ terminate the program entirely. To avoid this, add
1668
+ a default value for the variable in your objective
1669
+ function, like so:
1670
+ ``(self.wind_plant[annual_energy] or 0)``
1630
1671
 
1631
1672
  capital_cost_function : str
1632
1673
  The plant capital cost function written out as a string.
@@ -3,6 +3,7 @@
3
3
  """
4
4
  place turbines for bespoke wind plants
5
5
  """
6
+ import re
6
7
  from functools import wraps
7
8
 
8
9
  import numpy as np
@@ -96,7 +97,24 @@ class PlaceTurbines:
96
97
  cost ($) as evaluated by
97
98
  `balance_of_system_cost_function`
98
99
  - ``self.wind_plant``: the SAM wind plant object,
99
- through which all SAM variables can be accessed
100
+ through which all SAM variables can be accessed.
101
+
102
+ .. IMPORTANT::
103
+ When using the `self.wind_plant` variable,
104
+ DO NOT include quotes around variable names (keys).
105
+
106
+ - ❌ Wrong: ``self.wind_plant["annual_energy"]``
107
+ - ✅ Correct: ``self.wind_plant[annual_energy]``
108
+
109
+ .. IMPORTANT::
110
+ It's possible for SAM wind plant variables to be
111
+ ``None``, especially if something went wrong while
112
+ optimizing the wind plant layout. In this case,
113
+ your objective function may fail to evaluate and
114
+ terminate the program entirely. To avoid this, add
115
+ a default value for the variable in your objective
116
+ function, like so:
117
+ ``(self.wind_plant[annual_energy] or 0)``
100
118
 
101
119
  capital_cost_function : str
102
120
  The plant capital cost function as a string, must return the
@@ -133,13 +151,15 @@ class PlaceTurbines:
133
151
  # inputs
134
152
  self.wind_plant = wind_plant
135
153
 
136
- self.capital_cost_function = capital_cost_function
137
- self.fixed_operating_cost_function = fixed_operating_cost_function
138
- self.variable_operating_cost_function = \
139
- variable_operating_cost_function
140
- self.balance_of_system_cost_function = balance_of_system_cost_function
154
+ self.capital_cost_function = _fix_wp_keys(capital_cost_function)
155
+ self.fixed_operating_cost_function = _fix_wp_keys(
156
+ fixed_operating_cost_function)
157
+ self.variable_operating_cost_function = _fix_wp_keys(
158
+ variable_operating_cost_function)
159
+ self.balance_of_system_cost_function = _fix_wp_keys(
160
+ balance_of_system_cost_function)
141
161
 
142
- self.objective_function = objective_function
162
+ self.objective_function = _fix_wp_keys(objective_function)
143
163
  self.include_mask = include_mask
144
164
  self.pixel_side_length = pixel_side_length
145
165
  self.min_spacing = min_spacing
@@ -158,7 +178,6 @@ class PlaceTurbines:
158
178
  self.safe_polygons = None
159
179
  self._optimized_nn_conn_dist_m = None
160
180
 
161
- self.ILLEGAL = ('import ', 'os.', 'sys.', '.__', '__.', 'eval', 'exec')
162
181
  self._preflight(self.objective_function)
163
182
  self._preflight(self.capital_cost_function)
164
183
  self._preflight(self.fixed_operating_cost_function)
@@ -167,7 +186,10 @@ class PlaceTurbines:
167
186
 
168
187
  def _preflight(self, eqn):
169
188
  """Run preflight checks on the equation string."""
170
- for substr in self.ILLEGAL:
189
+ _illegal_substr = ('import ', 'os.', 'sys.', '.__', '__.', 'eval',
190
+ 'exec')
191
+
192
+ for substr in _illegal_substr:
171
193
  if substr in str(eqn):
172
194
  msg = ('Will not evaluate string which contains "{}": {}'
173
195
  .format(substr, eqn))
@@ -656,3 +678,10 @@ def _compute_nn_conn_dist(x_coords, y_coords):
656
678
  left_to_connect.mask[next_connection] = 1
657
679
 
658
680
  return total_dist
681
+
682
+
683
+ def _fix_wp_keys(eqn):
684
+ """Surround key of `self.wind_plant` in quotes"""
685
+ pattern = r'(self\.wind_plant\[\s*)([^\]]+?)(\s*\])'
686
+ replacement = r'\1"\2"\3'
687
+ return re.sub(pattern, replacement, str(eqn))
reV/cli.py CHANGED
@@ -10,7 +10,8 @@ from reV.generation.cli_gen import gen_command
10
10
  from reV.econ.cli_econ import econ_command
11
11
  from reV.handlers.cli_collect import collect_command
12
12
  from reV.handlers.cli_multi_year import my_command
13
- from reV.supply_curve.cli_sc_aggregation import sc_agg_command
13
+ from reV.supply_curve.cli_sc_aggregation import (sc_agg_command,
14
+ sc_col_descriptions)
14
15
  from reV.supply_curve.cli_supply_curve import sc_command
15
16
  from reV.supply_curve.cli_tech_mapping import tm_command
16
17
  from reV.rep_profiles.cli_rep_profiles import rep_profiles_command
@@ -31,6 +32,7 @@ commands = [bespoke_command, gen_command, econ_command, collect_command,
31
32
  main = make_cli(commands, info={"name": "reV", "version": __version__})
32
33
  main.add_command(qa_qc_extra)
33
34
  main.add_command(project_points)
35
+ main.add_command(sc_col_descriptions)
34
36
 
35
37
  # export GAPs commands to namespace for documentation
36
38
  batch = main.commands["batch"]
@@ -627,10 +627,14 @@ class ProjectPoints:
627
627
  # pylint: disable=no-member
628
628
  if SiteDataField.CONFIG not in df.columns:
629
629
  df[SiteDataField.CONFIG] = None
630
+ df[SiteDataField.CONFIG] = (df[SiteDataField.CONFIG]
631
+ .replace({np.nan: None}))
630
632
 
631
633
  # pylint: disable=no-member
632
634
  if SiteDataField.CURTAILMENT not in df.columns:
633
635
  df[SiteDataField.CURTAILMENT] = None
636
+ df[SiteDataField.CURTAILMENT] = (df[SiteDataField.CURTAILMENT]
637
+ .replace({np.nan: None}))
634
638
 
635
639
  gids = df[SiteDataField.GID].values
636
640
  if not np.array_equal(np.sort(gids), gids):
@@ -2,16 +2,21 @@
2
2
  """
3
3
  reV Supply Curve Aggregation CLI utility functions.
4
4
  """
5
- import os
6
5
  import logging
6
+ from pathlib import Path
7
7
 
8
+ import click
9
+ import pandas as pd
10
+ from rex import init_logger
8
11
  from rex.multi_file_resource import MultiFileResource
9
12
  from rex.utilities.utilities import check_res_file
10
13
  from gaps.cli import as_click_command, CLICommandFromClass
11
14
 
15
+ from reV import __version__
12
16
  from reV.supply_curve.sc_aggregation import SupplyCurveAggregation
13
17
  from reV.utilities import ModuleName
14
- from reV.utilities.cli_functions import parse_from_pipeline
18
+ from reV.utilities.cli_functions import (parse_from_pipeline,
19
+ compile_descriptions)
15
20
  from reV.utilities.exceptions import ConfigError
16
21
 
17
22
 
@@ -103,6 +108,38 @@ sc_agg_command = CLICommandFromClass(SupplyCurveAggregation, method="run",
103
108
  main = as_click_command(sc_agg_command)
104
109
 
105
110
 
111
+ @click.command()
112
+ @click.version_option(version=__version__)
113
+ @click.option('--sc_fpath', '-sc', type=click.Path(), default=None,
114
+ help='Supply curve CSV file path used to subset the columns')
115
+ @click.option('--out_fp', '-of', default=None, type=click.Path(),
116
+ help='Output CSV file for the column descriptions')
117
+ @click.pass_context
118
+ def sc_col_descriptions(ctx, sc_fpath, out_fp):
119
+ """Generate reV supply curve column descriptions"""
120
+ if ctx.obj.get('VERBOSE', False):
121
+ log_level = 'DEBUG'
122
+ else:
123
+ log_level = 'INFO'
124
+
125
+ init_logger('reV', log_level=log_level)
126
+
127
+ cols = []
128
+ if sc_fpath:
129
+ cols = pd.read_csv(sc_fpath, nrows=0).columns.tolist()
130
+
131
+ if not out_fp:
132
+ if sc_fpath:
133
+ sc_name = Path(sc_fpath).stem
134
+ out_fp = Path(sc_fpath).parent / f"{sc_name}_column_lookup.csv"
135
+ else:
136
+ out_fp = "rev_supply_curve_column_lookup.csv"
137
+
138
+ columns = compile_descriptions(cols)
139
+ columns.to_csv(out_fp, index=False)
140
+ logger.info("Column descriptions saved to %s", out_fp)
141
+
142
+
106
143
  if __name__ == '__main__':
107
144
  try:
108
145
  main(obj={})
@@ -3,6 +3,7 @@
3
3
  Generate reV inclusion mask from exclusion layers
4
4
  """
5
5
  import logging
6
+ import fnmatch
6
7
  from warnings import warn
7
8
 
8
9
  import numpy as np
@@ -132,10 +133,10 @@ class LayerMask:
132
133
  layer is equal to 1, 2, 3, 4, or 5**. Outside of these
133
134
  regions (i.e. outside of federal park regions), the viewshed
134
135
  exclusion is **NOT** applied. If the extent mask created by
135
- these options is not boolean, an error is thrown (i.e. do
136
- not specify `weight` or `use_as_weights`).
137
- By default ``None``, which applies the original layer mask
138
- to the full extent.
136
+ these options is not boolean, an error is thrown (in other
137
+ words, do not specify `weight` or `use_as_weights` or else
138
+ you will run into errors). By default ``None``, which
139
+ applies the original layer mask to the full extent.
139
140
  **kwargs
140
141
  Optional inputs to maintain legacy kwargs of ``inclusion_*``
141
142
  instead of ``include_*``.
@@ -600,19 +601,7 @@ class ExclusionMask:
600
601
  self._check_layers = check_layers
601
602
 
602
603
  if layers is not None:
603
- if not isinstance(layers, list):
604
- layers = [layers]
605
-
606
- missing = [layer.name for layer in layers
607
- if layer.name not in self.excl_layers]
608
- if any(missing):
609
- msg = ("ExclusionMask layers {} are missing from: {}"
610
- .format(missing, self._excl_h5))
611
- logger.error(msg)
612
- raise KeyError(msg)
613
-
614
- for layer in layers:
615
- self.add_layer(layer)
604
+ self._add_many_layers(layers)
616
605
 
617
606
  if kernel in ["queen", "rook"]:
618
607
  self._min_area = min_area
@@ -623,6 +612,22 @@ class ExclusionMask:
623
612
  else:
624
613
  raise KeyError('kernel must be "queen" or "rook"')
625
614
 
615
+ def _add_many_layers(self, layers):
616
+ """Add multiple layers (with check for missing layers)"""
617
+ if not isinstance(layers, list):
618
+ layers = [layers]
619
+
620
+ missing = [layer.name for layer in layers
621
+ if layer.name not in self.excl_layers]
622
+ if any(missing):
623
+ msg = ("ExclusionMask layers {} are missing from: {}"
624
+ .format(missing, self._excl_h5))
625
+ logger.error(msg)
626
+ raise KeyError(msg)
627
+
628
+ for layer in layers:
629
+ self.add_layer(layer)
630
+
626
631
  def __enter__(self):
627
632
  return self
628
633
 
@@ -1113,7 +1118,7 @@ class ExclusionMaskFromDict(ExclusionMask):
1113
1118
  excl_h5 : str | list | tuple
1114
1119
  Path to one or more exclusions .h5 files
1115
1120
  layers_dict : dict | NoneType
1116
- Dictionary of LayerMask arugments {layer: {kwarg: value}}
1121
+ Dictionary of LayerMask arguments {layer: {kwarg: value}}
1117
1122
  min_area : float | NoneType
1118
1123
  Minimum required contiguous area in sq-km
1119
1124
  kernel : str
@@ -1125,16 +1130,34 @@ class ExclusionMaskFromDict(ExclusionMask):
1125
1130
  Run a pre-flight check on each layer to ensure they contain
1126
1131
  un-excluded values
1127
1132
  """
1128
- if layers_dict is not None:
1129
- layers = []
1130
- for layer, kwargs in layers_dict.items():
1131
- layers.append(LayerMask(layer, **kwargs))
1132
- else:
1133
- layers = None
1134
-
1135
- super().__init__(excl_h5, layers=layers, min_area=min_area,
1133
+ super().__init__(excl_h5, layers=layers_dict, min_area=min_area,
1136
1134
  kernel=kernel, hsds=hsds, check_layers=check_layers)
1137
1135
 
1136
+ def _add_many_layers(self, layers):
1137
+ """Add multiple layers (with check for missing layers)"""
1138
+ missing = set()
1139
+ final_layers = {}
1140
+
1141
+ # sort pattern-first so that users can overwrite specific layers
1142
+ sorted_layers = sorted(layers, key=_unix_patterns_first)
1143
+ for layer_pattern in sorted_layers:
1144
+ kwargs = layers[layer_pattern]
1145
+ layer_names = fnmatch.filter(self.excl_layers, layer_pattern)
1146
+ if not layer_names:
1147
+ missing.add(layer_pattern)
1148
+
1149
+ for layer in layer_names:
1150
+ final_layers[layer] = LayerMask(layer, **kwargs)
1151
+
1152
+ if any(missing):
1153
+ msg = ("ExclusionMask layers {} are missing from: {}"
1154
+ .format(missing, self._excl_h5))
1155
+ logger.error(msg)
1156
+ raise KeyError(msg)
1157
+
1158
+ for layer in final_layers.values():
1159
+ self.add_layer(layer)
1160
+
1138
1161
  @classmethod
1139
1162
  def extract_inclusion_mask(cls, excl_fpath, tm_dset, excl_dict=None,
1140
1163
  area_filter_kernel='queen', min_area=None):
@@ -1295,3 +1318,9 @@ class FrictionMask(ExclusionMask):
1295
1318
  mask = f.mask
1296
1319
 
1297
1320
  return mask
1321
+
1322
+
1323
+ def _unix_patterns_first(layer_name):
1324
+ """Key that will put layer names with unix patterns first"""
1325
+ special_chars = {"?", "*", "!", "[", "]"}
1326
+ return -1 * any(char in layer_name for char in special_chars), layer_name
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # pylint: disable=anomalous-backslash-in-string
2
3
  """
3
4
  reV supply curve points frameworks.
4
5
  """
@@ -1452,8 +1453,12 @@ class GenerationSupplyCurvePoint(AggregationSupplyCurvePoint):
1452
1453
  """Supply curve point summary framework that ties a reV SC point to its
1453
1454
  respective generation and resource data."""
1454
1455
 
1455
- # technology-dependent power density estimates in MW/km2
1456
1456
  POWER_DENSITY = {"pv": 36, "wind": 3}
1457
+ """Technology-dependent power density estimates (in MW/km\ :sup:`2`).
1458
+
1459
+ The PV power density is a \**DC power density*\*, while the wind power
1460
+ density is an \**AC power density*\*.
1461
+ """
1457
1462
 
1458
1463
  def __init__(
1459
1464
  self,
@@ -2116,7 +2121,7 @@ class GenerationSupplyCurvePoint(AggregationSupplyCurvePoint):
2116
2121
  `None` for solar runs with "dc_ac_ratio" dataset in the
2117
2122
  generation file
2118
2123
  """
2119
- if self.power_density_ac is None:
2124
+ if "dc_ac_ratio" not in self.gen.datasets:
2120
2125
  return None
2121
2126
 
2122
2127
  return self.area * self.power_density_ac
@@ -2138,7 +2143,7 @@ class GenerationSupplyCurvePoint(AggregationSupplyCurvePoint):
2138
2143
  `None` for solar runs with "dc_ac_ratio" dataset in the
2139
2144
  generation file
2140
2145
  """
2141
- if self.power_density_ac is None:
2146
+ if "dc_ac_ratio" not in self.gen.datasets:
2142
2147
  return None
2143
2148
 
2144
2149
  return self.area * self.power_density
@@ -245,6 +245,7 @@ class SupplyCurveAggFileHandler(AbstractAggFileHandler):
245
245
  class SupplyCurveAggregation(BaseAggregation):
246
246
  """SupplyCurveAggregation"""
247
247
 
248
+ # pylint: disable=line-too-long
248
249
  def __init__(self, excl_fpath, tm_dset, econ_fpath=None,
249
250
  excl_dict=None, area_filter_kernel='queen', min_area=None,
250
251
  resolution=64, excl_area=None, res_fpath=None, gids=None,
@@ -334,6 +335,12 @@ class SupplyCurveAggregation(BaseAggregation):
334
335
  "exclude_nodata": True,
335
336
  "nodata_value": -1
336
337
  },
338
+ "wildcard*exclusion": {
339
+ "exclude_values": 1,
340
+ },
341
+ "wildcard_unique_exclusion": {
342
+ "exclude_values": [1, 2, 3],
343
+ },
337
344
  "partial_setback": {
338
345
  "use_as_weights": True
339
346
  },
@@ -359,9 +366,24 @@ class SupplyCurveAggregation(BaseAggregation):
359
366
  ...
360
367
  }
361
368
 
362
- Note that all the keys given in this dictionary should be
363
- datasets of the `excl_fpath` file. If ``None`` or empty
364
- dictionary, no exclusions are applied. By default, ``None``.
369
+ Note that all the keys given in this dictionary must be
370
+ datasets of the `excl_fpath` file or you will get an error.
371
+ You *may* include Unix-style wildcards (i.e. ``*``, ``?``,
372
+ or ``[]``) in the keys, but note that the same exclusion
373
+ configuration will be applied to **all** datasets that match
374
+ the wildcard pattern unless you explicitly override it for a
375
+ specific layer. For example, in the configuration above,
376
+ **all** of the layers matching the pattern
377
+ ``wildcard*exclusion`` will be used as exclusions where the
378
+ respective layer values equal ``1``, **except** for the
379
+ ``wildcard_unique_exclusion`` layer, which will be used as
380
+ an exclusion wherever that particular layer values equal
381
+ ``1``, ``2``, or ``3``. You can use this strategy to
382
+ "exclude" layers from the wildcard match - simply set the
383
+ ``exclude_values`` key to a value that does not exist in
384
+ that layer and it will be effectively ignored. If ``None``
385
+ or empty dictionary, no exclusions are applied.
386
+ By default, ``None``.
365
387
  area_filter_kernel : {"queen", "rook"}, optional
366
388
  Contiguous area filter method to use on final exclusions
367
389
  mask. The filters are defined as::
@@ -484,16 +506,36 @@ class SupplyCurveAggregation(BaseAggregation):
484
506
  The ``"output_layer_name"`` is the column name under which
485
507
  the aggregated data will appear in the output CSV file. The
486
508
  ``"output_layer_name"`` does not have to match the ``dset``
487
- input value. The latter should match the layer name in the
509
+ input value. The ``dset`` should match the layer name in the
488
510
  HDF5 from which the data to aggregate should be pulled. The
489
- ``method`` should be one of
490
- ``{"mode", "mean", "min", "max", "sum", "category"}``,
491
- describing how the high-resolution data should be aggregated
492
- for each supply curve point. ``fpath`` is an optional key
493
- that can point to an HDF5 file containing the layer data. If
494
- left out, the data is assumed to exist in the file(s)
495
- specified by the `excl_fpath` input. If ``None``, no data
496
- layer aggregation is performed. By default, ``None``
511
+ ``method`` key should be one of the following:
512
+
513
+ - ``"mode"``: Output values will be the numerical mode
514
+ of the non-excluded high resolution data layer cell
515
+ values
516
+ - ``"mean"``: Output values will be the arithmetic mean
517
+ of the non-excluded high resolution data layer cell
518
+ values
519
+ - ``"min"``: Output values will be the numerical minimum
520
+ value of the non-excluded high resolution data layer
521
+ cell values
522
+ - ``"max"``: Output values will be the numerical maximum
523
+ value of the non-excluded high resolution data layer
524
+ cell values
525
+ - ``"sum"``: Output values will be the sum of the
526
+ non-excluded high resolution data layer cell values
527
+ - ``"category"``: Output values will be a string
528
+ representation of a dictionary where the keys are the
529
+ unique values of the non-excluded high resolution data
530
+ layer cells and the values are the *total
531
+ high-resolution pixel area* corresponding to that data
532
+ layer value
533
+
534
+ ``fpath`` is an optional key that can point to an HDF5 file
535
+ containing the layer data. If left out, the data is assumed
536
+ to exist in the file(s) specified by the `excl_fpath` input.
537
+ If ``None``, no data layer aggregation is performed.
538
+ By default, ``None``
497
539
  power_density : float | str, optional
498
540
  Power density value (in MW/km\ :sup:`2`) or filepath to
499
541
  variable power density CSV file containing the following
@@ -503,8 +545,15 @@ class SupplyCurveAggregation(BaseAggregation):
503
545
  - ``power_density`` : power density value (in
504
546
  MW/km\ :sup:`2`)
505
547
 
506
- If ``None``, a constant power density is inferred from the
507
- generation meta data technology. By default, ``None``.
548
+ If you are running reV for PV (more specifically, you have a
549
+ `dc_ac_ratio` in your generation file), then this input
550
+ should represent the \**DC power density*\*. For all other
551
+ technologies (wind, geothermal, etc), this input should
552
+ represent the \**AC power density*\*. If ``None``, a
553
+ constant power density value is pulled from
554
+ :obj:`~reV.supply_curve.points.GenerationSupplyCurvePoint.POWER_DENSITY`
555
+ by looking up the technology from the generation meta data.
556
+ By default, ``None``.
508
557
  friction_fpath : str, optional
509
558
  Filepath to friction surface data (cost based exclusions).
510
559
  Must be paired with the `friction_dset` input below. The
@@ -1218,7 +1267,7 @@ class SupplyCurveAggregation(BaseAggregation):
1218
1267
  .format(gid, zone_id))
1219
1268
  else:
1220
1269
  pointsum['res_class'] = ri
1221
- pointsum['zone_id'] = zone_id
1270
+ pointsum[SupplyCurveField.ZONE_ID] = zone_id
1222
1271
 
1223
1272
  summary.append(pointsum)
1224
1273
  logger.debug(
@@ -32,6 +32,7 @@ class TechMapping:
32
32
 
33
33
  def __init__(self, excl_fpath, sc_resolution=1200):
34
34
  """
35
+
35
36
  Parameters
36
37
  ----------
37
38
  excl_fpath : str
@@ -44,9 +45,9 @@ class TechMapping:
44
45
  map the exclusion pixels in 1200x1200 pixel chunks.
45
46
 
46
47
  .. Note:: This parameter does not affect the exclusion to resource
47
- (tech) mapping, which deviates from how the effect of the
48
- ``sc_resolution`` parameter works in other functionality within
49
- ``reV``.
48
+ (tech) mapping, which deviates from how the effect of the
49
+ ``sc_resolution`` parameter works in other functionality
50
+ within ``reV``.
50
51
 
51
52
  """
52
53
  self._excl_fpath = excl_fpath
@@ -433,7 +434,7 @@ class TechMapping:
433
434
  techmap (exclusions-to-resource mapping data) will be saved.
434
435
 
435
436
  .. Important:: If this dataset already exists in the h5 file,
436
- it will be overwritten.
437
+ it will be overwritten.
437
438
 
438
439
  sc_resolution : int | None, optional
439
440
  Defines how many exclusion pixels are mapped at a time. Units
@@ -442,9 +443,9 @@ class TechMapping:
442
443
  map the exclusion pixels in 1200x1200 pixel chunks.
443
444
 
444
445
  .. Note:: This parameter does not affect the exclusion to resource
445
- (tech) mapping, which deviates from how the effect of the
446
- ``sc_resolution`` parameter works in other functionality within
447
- ``reV``.
446
+ (tech) mapping, which deviates from how the effect of the
447
+ ``sc_resolution`` parameter works in other functionality
448
+ within ``reV``.
448
449
 
449
450
  dist_margin : float, optional
450
451
  Extra margin to multiply times the computed distance between
reV/utilities/__init__.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """reV utilities."""
3
- from enum import Enum
3
+ import ast
4
+ import inspect
5
+ from enum import Enum, EnumMeta
4
6
 
5
7
  import PySAM
6
8
  from rex.utilities.loggers import log_versions as rex_log_versions
@@ -8,7 +10,50 @@ from rex.utilities.loggers import log_versions as rex_log_versions
8
10
  from reV.version import __version__
9
11
 
10
12
 
11
- class FieldEnum(str, Enum):
13
+ class _DocstringEnumMeta(EnumMeta):
14
+ """Metaclass to assign docstrings to Enum members"""
15
+
16
+ def __new__(metacls, clsname, bases, clsdict):
17
+ cls = super().__new__(metacls, clsname, bases, clsdict)
18
+
19
+ try:
20
+ source = inspect.getsource(cls)
21
+ except TypeError:
22
+ return cls # source not available (e.g., in interactive shell)
23
+
24
+ module = ast.parse(source)
25
+
26
+ for node in ast.iter_child_nodes(module):
27
+ if isinstance(node, ast.ClassDef) and node.name == cls.__name__:
28
+ prev = None
29
+ for body_item in node.body:
30
+ if isinstance(body_item, ast.Assign):
31
+ target = body_item.targets[0]
32
+ if isinstance(target, ast.Name):
33
+ name = target.id
34
+ prev = body_item
35
+ elif (isinstance(body_item, ast.Expr)
36
+ and isinstance(body_item.value, ast.Constant)):
37
+ if prev:
38
+ doc = body_item.value.s
39
+ member = cls.__members__.get(name)
40
+ if member:
41
+ member._description = (doc.strip()
42
+ .replace("\n ", " "))
43
+ prev = None
44
+ return cls
45
+
46
+
47
+ class DocEnum(Enum, metaclass=_DocstringEnumMeta):
48
+ """Base Enum class with docstring support"""
49
+
50
+ @property
51
+ def description(self):
52
+ """Description of enum member pulled from docstring"""
53
+ return getattr(self, '_description', None)
54
+
55
+
56
+ class FieldEnum(str, DocEnum):
12
57
  """Base Field enum with some mapping methods."""
13
58
 
14
59
  @classmethod
@@ -118,90 +163,373 @@ class SupplyCurveField(FieldEnum):
118
163
 
119
164
  Not all of these columns are guaranteed in every supply-curve like
120
165
  output (e.g. "convex_hull_area" is a bespoke-only output).
166
+
167
+ The docstrings for each field are used as a description when
168
+ exporting metadata information about supply curve columns. See
169
+ TBA for details.
121
170
  """
122
171
 
172
+ # ############## #
173
+ # Shared outputs #
174
+ # ############## #
175
+
123
176
  SC_GID = "sc_gid"
177
+ """Supply curve GID (Specific to this particular supply curve output)"""
178
+
124
179
  LATITUDE = "latitude"
180
+ """Centroid latitude of the supply curve grid-cell"""
181
+
125
182
  LONGITUDE = "longitude"
183
+ """Centroid longitude of the supply curve grid-cell"""
184
+
126
185
  COUNTRY = "country"
186
+ """Country of the supply curve grid-cell"""
187
+
127
188
  STATE = "state"
189
+ """State of the supply curve grid-cell"""
190
+
128
191
  COUNTY = "county"
192
+ """County of the supply curve grid-cell"""
193
+
129
194
  ELEVATION = "elevation_m"
195
+ """Mean elevation of the supply curve grid-cell"""
196
+
130
197
  TIMEZONE = "timezone"
198
+ """
199
+ Timezone of supply curve grid-cell, expressed as an hourly offset from UTC
200
+ """
201
+
131
202
  SC_POINT_GID = "sc_point_gid"
203
+ """
204
+ Unique ID that can be used to match supply curve grid-cells across reV
205
+ supply curves at the same resolution
206
+ """
207
+
132
208
  SC_ROW_IND = "sc_row_ind"
209
+ """Supply curve grid-cell row ID (Invariant across supply curves)"""
210
+
133
211
  SC_COL_IND = "sc_col_ind"
212
+ """Supply curve grid-cell column ID (Invariant across supply curves)"""
213
+
134
214
  SOURCE_GIDS = "source_gids"
215
+
135
216
  RES_GIDS = "res_gids"
217
+ """List of resource GID's mapped to this supply curve grid-cells"""
218
+
136
219
  GEN_GIDS = "gen_gids"
220
+ """List of generation GID's mapped to this supply curve point"""
221
+
137
222
  GID_COUNTS = "gid_counts"
223
+ """
224
+ Number of high-resolution cells corresponding to each generation GID
225
+ for this supply curve point
226
+ """
227
+
138
228
  N_GIDS = "n_gids"
229
+ """
230
+ Total number of not fully excluded pixels associated with the available
231
+ resource/generation gids
232
+ """
233
+
139
234
  ZONE_ID = "zone_id"
235
+ """Zone ID of the supply curve grid-cell, if applicable. Defaults to 1."""
236
+
140
237
  MEAN_RES = "resource"
238
+ """
239
+ Mean resource (e.g. wind speed, gha, temperature, etc.) across the supply
240
+ curve grid-cell
241
+ """
242
+
141
243
  MEAN_CF_AC = "capacity_factor_ac"
244
+ """Mean capacity factor (AC) across supply curve grid-cell"""
245
+
142
246
  MEAN_CF_DC = "capacity_factor_dc"
247
+ """Mean capacity factor (DC) across supply curve grid-cell"""
248
+
143
249
  WAKE_LOSSES = "losses_wakes_pct"
250
+ """Mean wake losses across supply curve grid-cell"""
251
+
144
252
  MEAN_LCOE = "lcoe_site_usd_per_mwh"
253
+ """
254
+ Mean power plant levelized cost of energy across supply curve grid-cell
255
+ """
256
+
145
257
  CAPACITY_AC_MW = "capacity_ac_mw"
258
+ """
259
+ Capacity of system based on area_sq_km * AC capacity density assumption
260
+ """
261
+
146
262
  CAPACITY_DC_MW = "capacity_dc_mw"
263
+ """
264
+ Capacity of system based on area_sq_km * DC capacity density assumption
265
+ """
266
+
147
267
  OFFSHORE = "offshore"
268
+ """
269
+ Flag value indicating if the supply curve grid-cell is offshore (1)
270
+ or not (0)
271
+ """
272
+
148
273
  AREA_SQ_KM = "area_developable_sq_km"
274
+ """Developable area after spatial exclusions applied"""
275
+
149
276
  MEAN_FRICTION = "friction_site"
277
+
150
278
  MEAN_LCOE_FRICTION = "lcoe_friction_usd_per_mwh"
279
+
151
280
  RAW_LCOE = "lcoe_raw_usd_per_mwh"
281
+ """
282
+ Mean power plant levelized cost of energy across supply curve grid-cell
283
+ without any multipliers or economies of scale applied
284
+ """
285
+
152
286
  EOS_MULT = "multiplier_cc_eos"
287
+ """
288
+ Capital cost economies of Scale (EOS) multiplier value (defaults to `1`
289
+ if no EOS curve was specified)
290
+ """
291
+
153
292
  FIXED_EOS_MULT = "multiplier_foc_eos"
293
+ """
294
+ Fixed operating cost economies of Scale (EOS) multiplier value (defaults
295
+ to `1` if no EOS curve was specified)
296
+ """
297
+
154
298
  VAR_EOS_MULT = "multiplier_voc_eos"
299
+ """
300
+ Variable operating cost economies of Scale (EOS) multiplier value
301
+ (defaults to `1` if no EOS curve was specified)
302
+ """
303
+
155
304
  REG_MULT = "multiplier_cc_regional"
305
+ """
306
+ Regional capital cost multiplier to capture taxes, labor, land lease
307
+ regional differences
308
+ """
309
+
156
310
  SC_POINT_ANNUAL_ENERGY_MWH = "annual_energy_site_mwh"
311
+ """
312
+ Total annual energy for supply curve grid-cell (computed using
313
+ "capacity_ac_mw" and "capacity_factor_ac")
314
+ """
315
+
157
316
  COST_BASE_CC_USD_PER_AC_MW = "cost_base_cc_usd_per_ac_mw"
317
+ """
318
+ Included-area weighted capital cost for supply curve grid-cell with no
319
+ multipliers or economies of scale applied (defaults to `None` for
320
+ non-LCOE runs)
321
+ """
322
+
158
323
  COST_SITE_CC_USD_PER_AC_MW = "cost_site_cc_usd_per_ac_mw"
324
+ """
325
+ Included-area weighted capital cost for supply curve grid-cell
326
+ (defaults to `None` for non-LCOE runs)
327
+ """
328
+
159
329
  COST_BASE_FOC_USD_PER_AC_MW = "cost_base_foc_usd_per_ac_mw"
330
+ """
331
+ Included-area weighted fixed operating cost for supply curve grid-cell
332
+ with no multipliers or economies of scale applied (defaults to `None` for
333
+ non-LCOE runs)
334
+ """
335
+
160
336
  COST_SITE_FOC_USD_PER_AC_MW = "cost_site_foc_usd_per_ac_mw"
337
+ """
338
+ Included-area weighted fixed operating cost for supply curve grid-cell
339
+ (defaults to `None` for non-LCOE runs)
340
+ """
341
+
161
342
  COST_BASE_VOC_USD_PER_AC_MWH = "cost_base_voc_usd_per_ac_mwh"
343
+ """
344
+ Included-area weighted variable operating cost for supply curve grid-cell
345
+ with no multipliers or economies of scale applied (defaults to `None` for
346
+ non-LCOE runs)
347
+ """
348
+
162
349
  COST_SITE_VOC_USD_PER_AC_MWH = "cost_site_voc_usd_per_ac_mwh"
350
+ """
351
+ Included-area weighted variable operating cost for supply curve grid-cell
352
+ (defaults to `None` for non-LCOE runs)
353
+ """
354
+
163
355
  FIXED_CHARGE_RATE = "fixed_charge_rate"
356
+ """
357
+ Fixed charge rate used for LCOE computation
358
+ (defaults to `None` for non-LCOE runs)
359
+ """
360
+
361
+ # ############### #
362
+ # Bespoke outputs #
363
+ # ############### #
164
364
 
165
- # Bespoke outputs
166
365
  POSSIBLE_X_COORDS = "possible_x_coords"
366
+ """
367
+ List of turbine x coordinates considered during layout optimization
368
+ (in meters relative to grid-cell)
369
+ """
370
+
167
371
  POSSIBLE_Y_COORDS = "possible_y_coords"
372
+ """
373
+ List of turbine y coordinates considered during layout optimization
374
+ (in meters relative to grid-cell)
375
+ """
376
+
168
377
  TURBINE_X_COORDS = "turbine_x_coords"
378
+ """
379
+ List of optimized layout turbine x coordinates
380
+ (in meters relative to grid-cell)
381
+ """
382
+
169
383
  TURBINE_Y_COORDS = "turbine_y_coords"
384
+ """
385
+ List of optimized layout turbine y coordinates
386
+ (in meters relative to grid-cell)
387
+ """
388
+
170
389
  N_TURBINES = "n_turbines"
390
+ """
391
+ Number of turbines in the optimized layout for this supply curve
392
+ grid-cell
393
+ """
394
+
171
395
  INCLUDED_AREA = "area_included_sq_km"
396
+ """Area available for wind turbine layout optimization"""
397
+
172
398
  INCLUDED_AREA_CAPACITY_DENSITY = (
173
399
  "capacity_density_included_area_mw_per_km2"
174
400
  )
401
+ """
402
+ Capacity density of the optimized wind plant layout defined using the
403
+ area available after removing the exclusions
404
+ """
405
+
175
406
  CONVEX_HULL_AREA = "area_convex_hull_sq_km"
407
+ """Area of the convex hull of the optimized wind plant layout"""
408
+
176
409
  CONVEX_HULL_CAPACITY_DENSITY = "capacity_density_convex_hull_mw_per_km2"
410
+ """
411
+ Capacity density of the optimized wind plant layout defined using the
412
+ convex hull area of the layout
413
+ """
414
+
177
415
  FULL_CELL_CAPACITY_DENSITY = "capacity_density_full_cell_mw_per_km2"
416
+ """
417
+ Capacity density of the optimized wind plant layout defined using the full
418
+ non-excluded area of the supply curve grid-cell
419
+ """
420
+
178
421
  BESPOKE_AEP = "optimized_plant_aep"
422
+ """
423
+ Annual energy production of the optimized wind plant layout computed using
424
+ wind speed/direction joint probability distribution (as opposed to
425
+ historical weather data)
426
+ """
427
+
179
428
  BESPOKE_OBJECTIVE = "optimized_plant_objective"
429
+ """
430
+ Objective function value of the optimized wind plant layout. This is
431
+ typically the LCOE computed using wind speed/direction joint probability
432
+ distribution (as opposed to historical weather data)
433
+ """
434
+
180
435
  BESPOKE_CAPITAL_COST = "optimized_plant_capital_cost"
436
+ """Capital cost of the optimized wind plant layout"""
437
+
181
438
  BESPOKE_FIXED_OPERATING_COST = "optimized_plant_fixed_operating_cost"
439
+ """Annual fixed operating cost of the optimized wind plant layout"""
440
+
182
441
  BESPOKE_VARIABLE_OPERATING_COST = "optimized_plant_variable_operating_cost"
442
+ """Variable operating cost of the optimized wind plant layout"""
443
+
183
444
  BESPOKE_BALANCE_OF_SYSTEM_COST = "optimized_plant_balance_of_system_cost"
445
+ """Balance of system cost of the optimized wind plant layout"""
446
+
447
+ # #################### #
448
+ # Transmission outputs #
449
+ # #################### #
184
450
 
185
- # Transmission outputs
186
451
  TRANS_GID = "trans_gid"
452
+ """Transmission connection feature GID"""
453
+
187
454
  TRANS_TYPE = "trans_type"
455
+ """Transmission connection feature type"""
456
+
188
457
  TOTAL_LCOE_FRICTION = "lcoe_total_friction_usd_per_mwh"
189
458
  TRANS_CAPACITY = "trans_capacity"
459
+
190
460
  DIST_SPUR_KM = "dist_spur_km"
461
+ """
462
+ Distance between the grid-cell centroid and cheapest available electrical
463
+ substation. Used in lcot calculations.
464
+ """
465
+
191
466
  DIST_EXPORT_KM = "dist_export_km"
467
+ """Length of the offshore export cable"""
468
+
192
469
  REINFORCEMENT_DIST_KM = "dist_reinforcement_km"
193
- TIE_LINE_COST_PER_MW = "cost_spur_usd_per_mw"
194
- CONNECTION_COST_PER_MW = "cost_poi_usd_per_mw"
195
- EXPORT_COST_PER_MW = "cost_export_usd_per_mw"
196
- REINFORCEMENT_COST_PER_MW = "cost_reinforcement_usd_per_mw"
197
- TOTAL_TRANS_CAP_COST_PER_MW = "cost_total_trans_usd_per_mw"
470
+ """
471
+ Distance between the connected substation and nearest regional load
472
+ center. Used in lcot calculations.
473
+ """
474
+
475
+ TIE_LINE_COST_PER_MW = "cost_spur_usd_per_mw_ac"
476
+ """
477
+ Cost of the spur line used to connect the grid-cell centroid with the
478
+ cheapest available electrical substation
479
+ """
480
+
481
+ CONNECTION_COST_PER_MW = "cost_poi_usd_per_mw_ac"
482
+ """Substation connection/upgrade/installation cost"""
483
+
484
+ EXPORT_COST_PER_MW = "cost_export_usd_per_mw_ac"
485
+ """Cost of the offshore export cable """
486
+
487
+ REINFORCEMENT_COST_PER_MW = "cost_reinforcement_usd_per_mw_ac"
488
+ """Non-levelized reinforcement transmission capital costs"""
489
+
490
+ TOTAL_TRANS_CAP_COST_PER_MW = "cost_total_trans_usd_per_mw_ac"
491
+ """
492
+ Non-levelized spur and point-of-interconnection transmission capital costs
493
+ """
494
+
198
495
  LCOT = "lcot_usd_per_mwh"
496
+ """
497
+ Levelized cost of transmission. Includes spur-transmission,
498
+ point-of-interconnection, and reinforcement costs.
499
+ """
500
+
199
501
  TOTAL_LCOE = "lcoe_all_in_usd_per_mwh"
502
+ """All-in LCOE. Includes site-lcoe + lcot"""
503
+
200
504
  N_PARALLEL_TRANS = "count_num_parallel_trans"
505
+ """
506
+ Number of parallel transmission lines connecting the grid-cell centroid
507
+ with the cheapest available electrical substation
508
+ """
509
+
201
510
  POI_LAT = "latitude_poi"
511
+ """
512
+ Latitude of the cheapest available electrical substation for the supply
513
+ curve grid-cell
514
+ """
515
+
202
516
  POI_LON = "longitude_poi"
517
+ """
518
+ Longitude of the cheapest available electrical substation for the supply
519
+ curve grid-cell
520
+ """
521
+
203
522
  REINFORCEMENT_POI_LAT = "latitude_reinforcement_poi"
523
+ """
524
+ Latitude of the nearest regional load center for the supply curve
525
+ grid-cell
526
+ """
527
+
204
528
  REINFORCEMENT_POI_LON = "longitude_reinforcement_poi"
529
+ """
530
+ Longitude of the nearest regional load center for the supply curve
531
+ grid-cell
532
+ """
205
533
 
206
534
  @classmethod
207
535
  def map_from_legacy(cls):
@@ -222,6 +550,11 @@ class SupplyCurveField(FieldEnum):
222
550
 
223
551
  return legacy_map
224
552
 
553
+ @property
554
+ def units(self):
555
+ """Units of the supply curve column, or ``"N/A"`` if not applicable"""
556
+ return _SC_UNITS.get(self, "N/A")
557
+
225
558
 
226
559
  class _LegacySCAliases(Enum):
227
560
  """Legacy supply curve column names.
@@ -243,10 +576,13 @@ class _LegacySCAliases(Enum):
243
576
  TRANS_CAPACITY = "avail_cap"
244
577
  DIST_SPUR_KM = "dist_km"
245
578
  REINFORCEMENT_DIST_KM = "reinforcement_dist_km"
246
- TIE_LINE_COST_PER_MW = "tie_line_cost_per_mw"
247
- CONNECTION_COST_PER_MW = "connection_cost_per_mw"
248
- REINFORCEMENT_COST_PER_MW = "reinforcement_cost_per_mw"
249
- TOTAL_TRANS_CAP_COST_PER_MW = "trans_cap_cost_per_mw"
579
+ TIE_LINE_COST_PER_MW = "tie_line_cost_per_mw", "cost_spur_usd_per_mw"
580
+ CONNECTION_COST_PER_MW = "connection_cost_per_mw", "cost_poi_usd_per_mw"
581
+ EXPORT_COST_PER_MW = "cost_export_usd_per_mw"
582
+ REINFORCEMENT_COST_PER_MW = ("reinforcement_cost_per_mw",
583
+ "cost_reinforcement_usd_per_mw")
584
+ TOTAL_TRANS_CAP_COST_PER_MW = ("trans_cap_cost_per_mw",
585
+ "cost_total_trans_usd_per_mw")
250
586
  LCOT = "lcot"
251
587
  TOTAL_LCOE = "total_lcoe"
252
588
  TOTAL_LCOE_FRICTION = "total_lcoe_friction"
@@ -332,3 +668,55 @@ def log_versions(logger):
332
668
  logger.info("Running with reV version {}".format(__version__))
333
669
  rex_log_versions(logger)
334
670
  logger.debug("- PySAM version {}".format(PySAM.__version__))
671
+
672
+
673
+ _SC_UNITS = {
674
+ SupplyCurveField.ELEVATION: "m",
675
+ SupplyCurveField.LATITUDE: "degrees",
676
+ SupplyCurveField.LONGITUDE: "degrees",
677
+
678
+ SupplyCurveField.AREA_SQ_KM: "km2",
679
+ SupplyCurveField.CAPACITY_AC_MW: "MWac",
680
+ SupplyCurveField.CAPACITY_DC_MW: "MWdc",
681
+ SupplyCurveField.MEAN_CF_AC: "ratio",
682
+ SupplyCurveField.MEAN_CF_DC: "ratio",
683
+ SupplyCurveField.WAKE_LOSSES: "%",
684
+ SupplyCurveField.MEAN_LCOE: "$/MWh",
685
+ SupplyCurveField.RAW_LCOE: "$/MWh",
686
+ SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MWH: "MWh",
687
+ SupplyCurveField.COST_BASE_CC_USD_PER_AC_MW: "$/MWac",
688
+ SupplyCurveField.COST_SITE_CC_USD_PER_AC_MW: "$/MWac",
689
+ SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW: "$/MWac",
690
+ SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW: "$/MWac",
691
+ SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MWH: "$/MWh",
692
+ SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MWH: "$/MWh",
693
+
694
+ SupplyCurveField.BESPOKE_AEP: "MWh",
695
+ SupplyCurveField.BESPOKE_CAPITAL_COST: "$",
696
+ SupplyCurveField.BESPOKE_FIXED_OPERATING_COST: "$/year",
697
+ SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST: "$/kWh",
698
+ SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST: "$",
699
+ SupplyCurveField.INCLUDED_AREA: "km2",
700
+ SupplyCurveField.INCLUDED_AREA_CAPACITY_DENSITY: "MW/km2",
701
+ SupplyCurveField.CONVEX_HULL_AREA: "km2",
702
+ SupplyCurveField.CONVEX_HULL_CAPACITY_DENSITY: "MW/km2",
703
+ SupplyCurveField.FULL_CELL_CAPACITY_DENSITY: "MW/km2",
704
+
705
+ SupplyCurveField.LCOT: "$/MWh",
706
+ SupplyCurveField.MEAN_RES: "varies",
707
+ SupplyCurveField.REINFORCEMENT_COST_PER_MW: "$/MWac",
708
+ SupplyCurveField.REINFORCEMENT_DIST_KM: "km",
709
+ SupplyCurveField.TOTAL_LCOE: "$/MWh",
710
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW: "$/MWac",
711
+ SupplyCurveField.DIST_SPUR_KM: "km",
712
+ SupplyCurveField.DIST_EXPORT_KM: "km",
713
+ SupplyCurveField.TIE_LINE_COST_PER_MW: "$/MWac",
714
+ SupplyCurveField.CONNECTION_COST_PER_MW: "$/MWac",
715
+ SupplyCurveField.EXPORT_COST_PER_MW: "$/MWac",
716
+
717
+ SupplyCurveField.POI_LAT: "degrees",
718
+ SupplyCurveField.POI_LON: "degrees",
719
+ SupplyCurveField.REINFORCEMENT_POI_LAT: "degrees",
720
+ SupplyCurveField.REINFORCEMENT_POI_LON: "degrees",
721
+
722
+ }
@@ -5,10 +5,11 @@ General CLI utility functions.
5
5
  import logging
6
6
  from warnings import warn
7
7
 
8
+ import pandas as pd
8
9
  from gaps.pipeline import Status
9
10
  from rex.utilities.loggers import init_mult
10
11
 
11
- from reV.utilities import ModuleName
12
+ from reV.utilities import ModuleName, SupplyCurveField
12
13
  from reV.utilities.exceptions import ConfigWarning, PipelineError
13
14
 
14
15
 
@@ -115,3 +116,44 @@ def parse_from_pipeline(config, out_dir, config_key, target_modules):
115
116
  .format(config_key, val[0]))
116
117
 
117
118
  return config
119
+
120
+
121
+ def compile_descriptions(cols=None):
122
+ """Compile a meta table with reV column descriptions.
123
+
124
+ Descriptions are pulled from the
125
+ :class:`~reV.utilities.SupplyCurveField` enum, which
126
+ contains the known reV supply curve field descriptions. Columns
127
+ which do not have a known description are excluded from the
128
+ output.
129
+
130
+ Parameters
131
+ ----------
132
+ cols : iterable, optional
133
+ Optional iterable of column names to include in the output.
134
+ By default, ``None``, which compiles all known reV supply curve
135
+ field descriptions.
136
+
137
+ Returns
138
+ -------
139
+ pd.DataFrame
140
+ Pandas DataFrame containing column names, corresponding units,
141
+ and descriptions for each column. Only columns that have a known
142
+ description are included in the output.
143
+ """
144
+ if not cols:
145
+ cols = [c.value for c in SupplyCurveField]
146
+
147
+ data = []
148
+ for col in cols:
149
+ try:
150
+ scf = SupplyCurveField(col)
151
+ except ValueError:
152
+ continue
153
+
154
+ if scf.description is None:
155
+ continue
156
+
157
+ data.append((str(scf), scf.units, scf.description))
158
+
159
+ return pd.DataFrame(data, columns=["reV Column", "Units", "Description"])
reV/version.py CHANGED
@@ -2,4 +2,4 @@
2
2
  reV Version number
3
3
  """
4
4
 
5
- __version__ = "0.14.4"
5
+ __version__ = "0.14.5"